小橙子 a7a48d15f3
feat: zero-prompt session handoff via two-layer LLM summary (#2)
* feat(server): two-layer session summary service for cross-session handoff

Backs the desktop "Continue from here" auto-handoff path. When a user
returns to a project to start a new chat session, the next-session AI
needs to know what the previous session was about; the existing
zero-token textarea-prefill carries 80 tokens of metadata that's good
for a human to read but useless for an AI ("title" alone tells it nothing
about the actual conversation). This service generates a compact
two-layer summary (project-level + recent-detailed) of the previous
session, caches it on disk, and lets the WS handler stage it as
`--append-system-prompt` on the next CLI launch so the receiving AI
starts with context but without paying for the whole prior transcript.

Token economics:
  - One LLM call per session, capped at 1500 output tokens
    (CLAUDE_CODE_HANDOFF_MAX_TOKENS, env-overridable).
  - Input transcript is tail-sliced at 80k chars
    (CLAUDE_CODE_HANDOFF_INPUT_CHARS) — recent context dominates the
    hand-off, older turns drop first; tool uses are condensed to
    one-liners so the input budget isn't spent on tool mechanics.
  - Cached on disk as `<sessionId>.summary.json` next to the JSONL.
    First call after a session has new turns (baseMessageCount drift)
    triggers regeneration; otherwise it's an instant disk hit.
  - Generation reuses the user's active provider via the same
    resolution pattern as titleService.ts: prefer the configured
    haiku/cheap model on the active provider, fall back to its main
    model. OpenAI Codex (ChatGPT OAuth) path uses the existing
    anthropicToOpenaiResponses + streaming response path.
  - All failures degrade silently to `null` so the welcome-screen
    falls back to the existing zero-token textarea prefill.

Wiring:
  - sessionSummaryService.ts (NEW) exports getSessionSummary,
    invalidateSessionSummary, and formatHandoffSystemPrompt for the
    WS handler.
  - sessions API gains GET/POST/DELETE /api/sessions/:id/summary.
  - conversationService SessionStartOptions gets handoffSystemPrompt;
    getRuntimeArgs appends it via --append-system-prompt independently
    of orchestration so both can be active at once.
  - WS handler adds `set_handoff_summary` ClientMessage type, an
    in-memory handoffSummarySessions map (one-shot consumed at next
    CLI start), getRuntimeSettings reads-and-deletes the staged
    summary so unrelated restarts don't re-attach a stale prompt,
    and session-end cleanup drops it.

Tested live via Chrome DevTools MCP (commit 2 covers the frontend
side):
  - POST /api/sessions/<id>/summary on a 9-message transcript:
    489 input + 335 output tokens, mimo-v2.5 produced clean two-layer
    JSON, written to <sessionId>.summary.json (~3KB).
  - Subsequent GET hits cache in ~40ms.
  - End-to-end: clicked "Continue from here" → server resolved cached
    summary → WS staged → CLI relaunched with --append-system-prompt
    carrying the formatted hand-off → mimo received it and listed
    exactly the modified files in the previous session it never saw,
    correctly identifying `feat/session-handoff-summary` branch state.

Confidence: high
Scope-risk: moderate (new endpoint, new service, new WS message type,
new SessionStartOptions field; all read-only / append-only; failures
fall back to existing zero-token path).
Tested: live MCP smoke + targeted curl on this branch.
Not-tested: server-side unit tests for sessionSummaryService (planned
follow-up — covers JSONL parsing, transcript tail-slice, summary
parsing, cache staleness detection, formatHandoffSystemPrompt
formatting). Live integration validated the happy path end-to-end.

* feat(desktop): auto-handoff flow on "Continue from here"

When a user clicks the Recent activity panel's "Continue from here"
button, the desktop now resolves the previous session's two-layer
summary server-side, stages it as the next session's system prompt
addendum (via WS set_handoff_summary), and auto-sends a short trigger
message so the receiving AI starts with full context — no textarea
preview, no manual confirm. Falls back to the original zero-token
textarea-prefill path on any failure.

User flow:
  1. User clicks "从这里继续" → button shows spinner + label
     "正在准备上下文..." while the summary resolves.
  2. projectsApi.resolveSessionSummaryForHandoff tries cache first
     (instant) and only falls back to LLM generation on miss.
  3. EmptySession path: createSession → connectToSession → WS
     set_handoff_summary → sendMessage(continueTriggerMessage). Server
     stages the summary in handoffSummarySessions and the next CLI
     launch picks it up via --append-system-prompt.
  4. ActiveSession's empty welcome state: same WS staging + auto-send
     against the live session — server schedules a runtime restart so
     the CLI relaunches with the hand-off context.
  5. On any error (provider down / generation failed / network), card
     falls back to dispatching the existing composer-prefill event with
     the static hand-off paragraph; user can edit and send manually.

Files:
  - desktop/src/api/projects.ts: SessionSummary type + getSessionSummary
    + generateSessionSummary (90s timeout — first generation can take
    30-60s on long transcripts) + resolveSessionSummaryForHandoff helper
    that prefers cache.
  - desktop/src/components/welcome/RecentActivityCard.tsx: replaced
    onApplyHandoff (sync, just writes to textarea) with onAutoHandoff
    (async, host owns the full flow). Button shows progress_activity
    spinner + i18n'd "preparing context..." label while the host's
    promise is in flight; disabled during pending.
  - desktop/src/pages/EmptySession.tsx, ActiveSession.tsx: implement
    the host-side handoff flow — resolve summary, then either create
    new session + WS stage + auto-send, or fall back.
  - desktop/src/types/chat.ts: ClientMessage union gains
    set_handoff_summary.
  - i18n: 2 new keys per locale (handoffGenerating, continueTriggerMessage)
    across en/zh/zh-TW/jp/kr.

Tested live (companion to ce53f503 server-side commit):
  - Cache-hit path: card click → spinner ~50ms → new session → CLI
    launched with --append-system-prompt → mimo-v2.5-pro received the
    formatted hand-off and listed exactly the previously-modified files
    on feat/session-handoff-summary, identified the feature state
    correctly, and asked to verify TS — proving it had ground-truth
    context from the previous session it never directly saw.
  - Cache-miss path: ~10-15s LLM generation, button stays in loading
    state, then proceeds.
  - Failure path: simulated by manually breaking the provider URL —
    button completes silently, textarea gets the fallback static
    paragraph (existing zero-token path).
  - Screenshot: artifacts/desktop-handoff-auto-resume.png

bun run lint passes (tsc --noEmit clean).

Confidence: high
Scope-risk: moderate (new auto-send-on-click behavior; mitigated by
silent fallback to existing textarea path and a generous timeout).
Tested: live MCP smoke for cache-hit happy path + manual failure path.
Not-tested: vitest unit tests for the new RecentActivityCard handoff
states (planned follow-up).

* fix(handoff): cache-only WS handler so summary generation can't double-run

Acted on a code review against the auto-handoff feature. Two findings,
one real one false-positive (left a defensive note for the false one
to keep future maintainers from "fixing" what isn't broken):

REAL ISSUE: handleSetHandoffSummary used getSessionSummary(), which
falls back to LLM generation on cache miss. The frontend's "Continue
from here" path always calls POST /api/sessions/:id/summary first
(which performs any needed LLM call), and only dispatches the WS
set_handoff_summary message AFTER the HTTP returned a successful
summary. So the WS handler should always find the cached summary on
disk. If it somehow doesn't, the OLD behavior would silently re-invoke
the LLM — blocking the WS handler for up to 60s and double-charging
the user. New behavior: read cache only, fail-fast with a clear
warning log if the cache is missing, and let the new session start
without hand-off context (the trigger message reads as a normal
"continue" prompt). Better than hanging or surprise-billing.

  - sessionSummaryService.ts: new exported getCachedSessionSummary
    helper — pure disk read, never calls the LLM.
  - ws/handler.ts: handleSetHandoffSummary swapped over with a
    detailed comment explaining the contract.

FALSE POSITIVE (kept as inline doc): the review claimed
EmptySession's flow has a race because wsManager.send happens before
the WS open. Actually wsManager.send tolerates a not-yet-OPEN socket
by queueing into pendingMessages and flushing on ws.onopen (see
desktop/src/api/websocket.ts). The current code is correct. Added a
defense-in-depth comment at the call site so a future "fix" doesn't
introduce an isConnected() gate that would break the contract.

Confidence: high
Scope-risk: narrow (one WS handler swap + one new export, no
behavioral change on the happy path; only the LLM-double-call edge
case is now fail-fast).
Tested: bun run lint passes (tsc --noEmit clean). Live MCP smoke
already covered the happy path on the previous commit; this commit
narrows a failure mode the smoke didn't actively exercise.

* feat(desktop): handoff progress stages + preview panel

Two UX additions on top of the auto-handoff flow:

1. Stage-aware progress label inside the "Continue from here" button.
   Replaces the opaque "Preparing context..." spinner with a localized
   walk-through:
     preparing            (default while host kicks off)
     reading-cache        (during the GET /api/sessions/:id/summary)
     generating-summary   (during the POST → LLM round-trip on cache miss)
     starting-session     (during createSession + WS staging + auto-send)
   The card stays dumb — host calls a setStage(...) callback handed to
   it as the third arg of onAutoHandoff. Card maps the stage to the
   corresponding i18n key; defaults to "preparing" when the host hasn't
   advanced. Falls back gracefully if a host doesn't call setStage at
   all (the spinner just shows "preparing" the whole time).

2. "Preview summary" toggle next to "Continue from here". Click expands
   a 280px-max scrollable inline panel showing the cached summary's
   main + recent layers, plus a one-liner with the model used and token
   counts. If no summary is cached yet (first time the user is about to
   click Continue), the panel shows "尚未生成摘要…" so the user knows
   what to expect.

   Card fetches the cached summary on mount via projectsApi
   .getSessionSummary (cache-only GET, never triggers an LLM call). The
   fetch is opportunistic: if it fails or returns null, the preview
   panel just shows the "no preview yet" hint.

i18n: 9 new keys per locale across en/zh/zh-TW/jp/kr — three stage
labels (handoffStage.readingCache, generatingSummary, startingSession),
preview toggle/close labels, three preview-panel section labels, and
the "no preview yet" placeholder.

Tested live in browser via Chrome DevTools MCP:
  - Empty cache state: preview button visible, panel shows "no preview
    yet" placeholder.
  - Primed cache (mimo-v2.5 summary, 17.5s LLM call): reload → preview
    panel renders with full main + recent content (292px high), token
    metadata line at the bottom.
  - Stage progression visible to the eye on the next "Continue from
    here" click during the cache-miss path; cache-hit path skips the
    "generating-summary" stage and goes straight to "starting-session".
  - Screenshot: artifacts/desktop-handoff-preview-panel.png

bun run lint passes (tsc --noEmit clean).

Confidence: high
Scope-risk: narrow (UI-only additions; existing onAutoHandoff signature
gained a third arg, both hosts updated; default value pathway preserved
when callers don't use setStage).

* feat(desktop): handoff chip in chat header + tests

The previous commits in this PR plumbed in the auto-handoff machinery
(server summary service, frontend resolution + auto-send, progress
stages, preview panel). What's missing is a *persistent visual signal*
in the active chat that the AI was bootstrapped with prior context.
Without it, a returning user sees an ordinary chat with no hint that
they pre-loaded a summary, and may distrust or forget that context is
present.

This adds a small chip in the ActiveSession chat header next to the
existing token / message-count chips:

    ↗ 接续上次 (639 t)

with a hover tooltip:

    接续自"<previous session title>"。AI 启动时已带上了 639 tokens
    的上次会话摘要作为系统提示。

The chip is purely informational — clicking does nothing. Token count
is a frontend-side estimate (chars / 4) of the staged summary's
main+recent text size, so it tells the user "roughly how big is the
hand-off addendum in my system prompt right now". The exact tokens are
also surfaced server-side in ContextUsageIndicator's "System prompt"
category, so this chip doesn't double-count.

Plumbing:
  - sessionRuntimeStore gains a new handoffInfo: Record<sessionId,
    SessionHandoffInfo> field, persisted to localStorage under
    cc-haha-session-handoff. Mirrors the existing coordinatorModes
    pattern: setHandoffInfo / clearHandoffInfo actions, automatic
    cleanup in clearSelection and migration in moveSelection so the
    drafts → real-session-id transition during session creation
    doesn't lose the handoff record.
  - SessionHandoffInfo carries previousSessionId, snapshotted previous
    title, approxTokens, and generatedAt for staleness display.
  - RecentActivityCard's onAutoHandoff signature gains a third arg
    previousSessionTitle so the host can stash it without re-fetching
    the recent-activity payload.
  - EmptySession + ActiveSession hosts call setHandoffInfo on the
    target session right before WS staging, so the chip lights up the
    moment the new session opens (or the live session is restarted).
  - i18n: 2 new keys per locale (session.handoffChip,
    session.handoffChipTooltip).

Tests: new desktop/src/stores/sessionRuntimeStore.test.ts with 6
focused tests:
  - setHandoffInfo persists to store + localStorage
  - clearHandoffInfo removes selectively
  - clearHandoffInfo on missing key is a no-op
  - clearSelection cleans up handoffInfo for the same key
  - moveSelection migrates handoffInfo from draft → real key
  - handoffInfo and coordinatorModes are independent

Verified live in the browser via Chrome DevTools MCP:
  - Click "Continue from here" → cache-hit summary path → chip appears
    in the new session's header showing exact previous title and
    639-token approx count.
  - localStorage persists across reload (chip survives F5).
  - Multiple sessions: the chip is per-session, cleared on session
    close (via clearSelection wiring).
  - Screenshot: artifacts/desktop-handoff-chip-in-header.png

bun run lint passes (tsc --noEmit clean).
bun run vitest passes for chatStore + EmptySession (124/124) +
sessionRuntimeStore (6/6 new).

Confidence: high
Scope-risk: narrow (one new persisted store field, additive UI in the
header, no behavioral change on the handoff path itself).

---------

Co-authored-by: 你的姓名 <you@example.com>
2026-06-10 18:35:02 +08:00
..
2026-06-10 07:12:53 +08:00

Claude Code Haha Desktop

基于 Tauri 2 + React 的桌面客户端。

开发

bun install
bun run tauri dev

构建

# macOS (Apple Silicon)
./scripts/build-macos-arm64.sh

# Windows (x64, MSI only)
.\scripts\build-windows-x64.ps1

构建产物位于 build-artifacts/ 目录,文件名会显式包含平台、架构和包类型。

常见问题

macOS 提示"已损坏,无法打开"

xattr -cr /Applications/Claude\ Code\ Haha.app