feat(cli): add session recap with /recap and auto-show on return#3434
Merged
Conversation
Users often open an old session days later and need to scroll through pages to remember where they left off. This change adds a short "where did I leave off" recap — a 1-3 sentence summary generated by the fast model — so they can resume without re-reading the history. Two triggers: - /recap: manual slash command. - Auto: when the terminal has been blurred for 5+ minutes and gets focused again (uses the existing DECSET 1004 focus protocol via useFocus). Gated on streamingState === Idle so it never interrupts an active turn. Only fires once per blur cycle. The recap is rendered in dim color with a chevron prefix, visually distinct from assistant replies. A new `general.showSessionRecap` setting controls the auto-trigger (default on). /recap works independent of the setting. Implementation notes: - generateSessionRecap uses fastModel (falls back to main model), tools: [], maxOutputTokens: 300, and a tight system prompt. It strips tool calls / responses from history before sending — tool responses can hold 10K+ tokens of file content that drown the recap in irrelevant detail. The 30-message window respects turn boundaries (slice never starts on a dangling model/tool response). - Output is wrapped in <recap>...</recap> tags; the extractor returns empty (skips render) if the tag is missing, preventing model reasoning from leaking into the UI. - All failures are silent (return null) and logged via a scoped debugLogger; recap is best-effort and must never break main flow. - /recap refuses to run while a turn is pending.
wenshao
commented
Apr 19, 2026
If the user disables showSessionRecap while an auto-recap LLM call is already in flight, the previous code returned early without aborting. The pending .then would still pass its idle/abort guards and append the recap, producing an unwanted message after the user has opted out. Abort the controller and clear it eagerly so the resolved promise no longer adds to history.
yiliang114
reviewed
Apr 19, 2026
yiliang114
reviewed
Apr 19, 2026
Two related issues from review: 1. /recap was only refusing when ui.pendingItem was set, but a normal model reply runs with streamingState === Responding and a null pendingItem. Invoking /recap mid-stream would generate a recap from a partial conversation and insert it between the user prompt and the assistant reply. 2. useAwaySummary cleared blurredAtRef before checking isIdle, so if focus returned during a still-streaming turn (after a >5min blur) the recap was permanently dropped — there was no later retry when the turn became idle, because isIdle was not in the effect deps. Fixes: - Expose isIdleRef on CommandContext.ui (mirrors btwAbortControllerRef pattern). Plumb it from AppContainer through useSlashCommandProcessor. - recapCommand now refuses when isIdleRef.current is false OR pendingItem is non-null. - useAwaySummary preserves blurredAtRef on the !isIdle bail and adds isIdle to the effect deps, so the trigger re-evaluates when the current turn finishes. - Brief blurs (< AWAY_THRESHOLD_MS) still reset blurredAtRef. Also seeds isIdleRef in nonInteractiveUi and mockCommandContext so the new field has a sensible default outside the interactive UI.
wenshao
commented
Apr 19, 2026
- User docs: add /recap to the Session and Project Management table in features/commands.md and a dedicated subsection covering manual use, the auto-trigger, the dim-color rendering, and the fast-model tip. - User docs: add general.showSessionRecap row to the configuration settings reference. - Design doc: docs/design/session-recap/session-recap-design.md covers motivation, the two trigger paths, the per-file architecture, prompt design with the <recap> tag and three-tier extractor, history filtering rationale (functionResponse can be 10K+ tokens), the useAwaySummary state machine, the isIdleRef gating for /recap, model selection, observability, and out-of-scope items.
filterToDialog kept any non-empty text part, but @google/genai's Part type also marks model reasoning with part.thought / part.thoughtSignature. That hidden chain-of-thought was being fed to the recap LLM and could get summarized as if it were user-visible dialogue. Drop parts where either flag is set. Update the design doc's History 过滤 section to call this out alongside the existing tool-call/response rationale.
…chine, sharpen UX wording Audit of the session recap docs against the implementation found three issues worth fixing: - Design doc claimed debug logs were enabled via a QWEN_CODE_DEBUG_LOGGING env var. That var does not exist; debug logs are written to ~/.qwen/debug/<sessionId>.txt by default, gated by QWEN_DEBUG_LOG_FILE. Replace with the accurate path + opt-out behavior, and tell the reader to grep for the [SESSION_RECAP] tag. - Design doc's useAwaySummary state machine table was missing the isFocused && blurredAtRef === null path (taken on first render and right after a brief-blur reset). Add the row. - User doc's "Refuses to run ... failures are silent" line conflated the inline-error refusal with silent generation failures, and "(when the conversation is idle)" used internal jargon. Split the two cases and spell out what "idle" means, including the wait-then-fire behavior when focus returns mid-turn.
yiliang114
reviewed
Apr 19, 2026
…e modes
The previous wording said "Generation/network failures are silent — the
recap simply does not appear", but recapCommand returns a user-facing
info message ("Not enough conversation context for a recap yet.") in
exactly that path, and also returns inline messages for the
config-not-loaded and busy-turn guards.
Only the auto-trigger path is truly silent (it just skips addItem when
generateSessionRecap returns null). Split the two paths in the doc so
the manual command's "always responds with something" behavior is
distinguished from the auto-trigger's no-op-on-failure behavior.
Two doc-vs-code mismatches in the design doc's "System Prompt" section, caught with the same lens as yiliang114's failure-mode review: - The bullet list claimed RECAP_SYSTEM_PROMPT forbids "推测用户意图" and "用 'you' 称呼用户". Those rules existed in an early draft but were dropped when the <recap> tag rules were added; the current prompt has no such restrictions. Replace with the actual rules and add a "与 RECAP_SYSTEM_PROMPT 一一对应" marker so future edits stay in sync. - The doc said systemInstruction "覆盖" the main agent prompt. True for the agent prompt portion, but GeminiClient.generateContent internally calls getCustomSystemPrompt which appends user memory (QWEN.md / 自动 memory) as a suffix. Spell that out — the final system prompt is recap prompt + user memory, which is actually useful project context for the recap.
The repo convention for docs/design is English (7 of 8 existing files; auto-memory/memory-system.md is the only Chinese one). The first version of this design doc followed the auto-memory example, which turned out to be the wrong sample. Translate to English while preserving the existing structure, the state-machine table, the prompt-vs-doc 1:1 alignment, the QWEN_DEBUG_LOG_FILE description, and the failure-mode notes added in prior commits.
wenshao
commented
Apr 19, 2026
The interactive success path inserts the away_recap history item
directly via ui.addItem and then returned `{type: 'message',
messageType: 'info', content: ''}`. The slash-command processor's
'message' case unconditionally calls addMessage, which adds another
HistoryItemInfo with empty text. The empty info renders as nothing
(StatusMessage early-returns null), but it still bloats the in-memory
history list and shows up in /export and saved sessions.
Return void on the interactive success path and on the abort path so
the processor's `if (result)` check skips the message-handler branch
entirely. Widen the action's return type to `void | SlashCommandActionReturn`
to match (same shape as btwCommand).
wenshao
commented
Apr 19, 2026
wenshao
left a comment
Collaborator
Author
There was a problem hiding this comment.
No issues found. LGTM! ✅ — gpt-5.4 via Qwen Code /review
yiliang114
approved these changes
Apr 19, 2026
yiliang114
left a comment
Collaborator
There was a problem hiding this comment.
LGTM. The follow-up fixes addressed the review feedback, and the current head passes the full CI matrix.
4 tasks
4 tasks
mabry1985
added a commit
to protoLabsAI/protoCLI
that referenced
this pull request
May 3, 2026
When the terminal regains focus after >= awayThresholdMinutes (default 5) of sustained blur, generate a `/recap`-style "where you left off" summary and append it to history. Off by default; enable via `general.showSessionRecap`. Adapted from the auto-recap-on-return feature in QwenLM/qwen-code's `useAwaySummary` hook (introduced in QwenLM#3434, finalized in QwenLM#3482) — but takes a focused slice rather than a full cherry-pick, since upstream's version churned through a sticky-banner placement that QwenLM#3482 then tore back out, and most of the conflict surface was unrelated layout work we don't have. What this lands: - `packages/core/src/services/sessionRecap.ts` — thin wrapper around `generateRecap` that pulls history off the GeminiClient and returns `{ text } | null`, matching upstream's API so the hook is portable. - `packages/cli/src/ui/hooks/useAwaySummary.ts` — port of the upstream hook. Uses our existing `HistoryItemRecap` (`type: 'recap'`) instead of the upstream-only `away_recap` type so manual `/recap` and the auto-fire path share one render path. - Settings: `general.showSessionRecap` (boolean, default false) and `general.sessionRecapAwayThresholdMinutes` (number, default 5). - Wired in `AppContainer.tsx` next to `useFocus()`; `isIdle` is derived from `streamingState === StreamingState.Idle`. - Mirrors Claude Code's dedup gating: needs at least 3 user messages total before firing, and at least 2 new user messages since the previous recap before another can fire — prevents back-to-back near-duplicate recaps after a brief alt-tab cycle with no work between. Cleanup: stripped vestigial `awayRecapItem` / `setAwayRecapItem` fields from `UIStateContext` that referenced a never-imported `HistoryItemAwayRecap` type. No consumers; the auto-fire path addItem's directly into history instead of holding a sticky banner. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xaelistic
pushed a commit
to xaelistic/qwen-code
that referenced
this pull request
Jun 7, 2026
…nLM#3434) * feat(cli): add session recap with /recap and auto-show on return Users often open an old session days later and need to scroll through pages to remember where they left off. This change adds a short "where did I leave off" recap — a 1-3 sentence summary generated by the fast model — so they can resume without re-reading the history. Two triggers: - /recap: manual slash command. - Auto: when the terminal has been blurred for 5+ minutes and gets focused again (uses the existing DECSET 1004 focus protocol via useFocus). Gated on streamingState === Idle so it never interrupts an active turn. Only fires once per blur cycle. The recap is rendered in dim color with a chevron prefix, visually distinct from assistant replies. A new `general.showSessionRecap` setting controls the auto-trigger (default on). /recap works independent of the setting. Implementation notes: - generateSessionRecap uses fastModel (falls back to main model), tools: [], maxOutputTokens: 300, and a tight system prompt. It strips tool calls / responses from history before sending — tool responses can hold 10K+ tokens of file content that drown the recap in irrelevant detail. The 30-message window respects turn boundaries (slice never starts on a dangling model/tool response). - Output is wrapped in <recap>...</recap> tags; the extractor returns empty (skips render) if the tag is missing, preventing model reasoning from leaking into the UI. - All failures are silent (return null) and logged via a scoped debugLogger; recap is best-effort and must never break main flow. - /recap refuses to run while a turn is pending. * fix(cli): abort in-flight recap when showSessionRecap is disabled If the user disables showSessionRecap while an auto-recap LLM call is already in flight, the previous code returned early without aborting. The pending .then would still pass its idle/abort guards and append the recap, producing an unwanted message after the user has opted out. Abort the controller and clear it eagerly so the resolved promise no longer adds to history. * fix(cli): gate /recap and auto-recap on streaming idle state Two related issues from review: 1. /recap was only refusing when ui.pendingItem was set, but a normal model reply runs with streamingState === Responding and a null pendingItem. Invoking /recap mid-stream would generate a recap from a partial conversation and insert it between the user prompt and the assistant reply. 2. useAwaySummary cleared blurredAtRef before checking isIdle, so if focus returned during a still-streaming turn (after a >5min blur) the recap was permanently dropped — there was no later retry when the turn became idle, because isIdle was not in the effect deps. Fixes: - Expose isIdleRef on CommandContext.ui (mirrors btwAbortControllerRef pattern). Plumb it from AppContainer through useSlashCommandProcessor. - recapCommand now refuses when isIdleRef.current is false OR pendingItem is non-null. - useAwaySummary preserves blurredAtRef on the !isIdle bail and adds isIdle to the effect deps, so the trigger re-evaluates when the current turn finishes. - Brief blurs (< AWAY_THRESHOLD_MS) still reset blurredAtRef. Also seeds isIdleRef in nonInteractiveUi and mockCommandContext so the new field has a sensible default outside the interactive UI. * docs: document /recap command, showSessionRecap setting, and design - User docs: add /recap to the Session and Project Management table in features/commands.md and a dedicated subsection covering manual use, the auto-trigger, the dim-color rendering, and the fast-model tip. - User docs: add general.showSessionRecap row to the configuration settings reference. - Design doc: docs/design/session-recap/session-recap-design.md covers motivation, the two trigger paths, the per-file architecture, prompt design with the <recap> tag and three-tier extractor, history filtering rationale (functionResponse can be 10K+ tokens), the useAwaySummary state machine, the isIdleRef gating for /recap, model selection, observability, and out-of-scope items. * fix(core): exclude thought parts from session recap context filterToDialog kept any non-empty text part, but @google/genai's Part type also marks model reasoning with part.thought / part.thoughtSignature. That hidden chain-of-thought was being fed to the recap LLM and could get summarized as if it were user-visible dialogue. Drop parts where either flag is set. Update the design doc's History 过滤 section to call this out alongside the existing tool-call/response rationale. * docs(session-recap): correct debug-logging guidance, fill in state machine, sharpen UX wording Audit of the session recap docs against the implementation found three issues worth fixing: - Design doc claimed debug logs were enabled via a QWEN_CODE_DEBUG_LOGGING env var. That var does not exist; debug logs are written to ~/.qwen/debug/<sessionId>.txt by default, gated by QWEN_DEBUG_LOG_FILE. Replace with the accurate path + opt-out behavior, and tell the reader to grep for the [SESSION_RECAP] tag. - Design doc's useAwaySummary state machine table was missing the isFocused && blurredAtRef === null path (taken on first render and right after a brief-blur reset). Add the row. - User doc's "Refuses to run ... failures are silent" line conflated the inline-error refusal with silent generation failures, and "(when the conversation is idle)" used internal jargon. Split the two cases and spell out what "idle" means, including the wait-then-fire behavior when focus returns mid-turn. * docs(session-recap): correctly describe /recap vs auto-trigger failure modes The previous wording said "Generation/network failures are silent — the recap simply does not appear", but recapCommand returns a user-facing info message ("Not enough conversation context for a recap yet.") in exactly that path, and also returns inline messages for the config-not-loaded and busy-turn guards. Only the auto-trigger path is truly silent (it just skips addItem when generateSessionRecap returns null). Split the two paths in the doc so the manual command's "always responds with something" behavior is distinguished from the auto-trigger's no-op-on-failure behavior. * docs(session-recap): align prompt-rules section with the actual prompt Two doc-vs-code mismatches in the design doc's "System Prompt" section, caught with the same lens as yiliang114's failure-mode review: - The bullet list claimed RECAP_SYSTEM_PROMPT forbids "推测用户意图" and "用 'you' 称呼用户". Those rules existed in an early draft but were dropped when the <recap> tag rules were added; the current prompt has no such restrictions. Replace with the actual rules and add a "与 RECAP_SYSTEM_PROMPT 一一对应" marker so future edits stay in sync. - The doc said systemInstruction "覆盖" the main agent prompt. True for the agent prompt portion, but GeminiClient.generateContent internally calls getCustomSystemPrompt which appends user memory (QWEN.md / 自动 memory) as a suffix. Spell that out — the final system prompt is recap prompt + user memory, which is actually useful project context for the recap. * docs(session-recap): translate design doc to English The repo convention for docs/design is English (7 of 8 existing files; auto-memory/memory-system.md is the only Chinese one). The first version of this design doc followed the auto-memory example, which turned out to be the wrong sample. Translate to English while preserving the existing structure, the state-machine table, the prompt-vs-doc 1:1 alignment, the QWEN_DEBUG_LOG_FILE description, and the failure-mode notes added in prior commits. * fix(cli): drop empty info return from /recap interactive success path The interactive success path inserts the away_recap history item directly via ui.addItem and then returned `{type: 'message', messageType: 'info', content: ''}`. The slash-command processor's 'message' case unconditionally calls addMessage, which adds another HistoryItemInfo with empty text. The empty info renders as nothing (StatusMessage early-returns null), but it still bloats the in-memory history list and shows up in /export and saved sessions. Return void on the interactive success path and on the abort path so the processor's `if (result)` check skips the message-handler branch entirely. Widen the action's return type to `void | SlashCommandActionReturn` to match (same shape as btwCommand).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Opening an old conversation a few days later, you have to scroll through pages
to remember what you were doing and what came next.
/resumeonly reloadsthe messages — there is no built-in "where did I leave off" summary.
This PR adds a short recap (1-3 sentences) that appears either on demand or
when you return to the terminal after stepping away.
What this adds
1.
/recapslash commandGenerates and displays the recap immediately:
The recap is rendered in
theme.text.secondary(dim grey) with a❯prefix — visually distinct from real assistant replies, so it doesn't get
confused for new model output.
2. Auto-trigger on focus return
Uses the existing
useFocushook (DECSET 1004 focus protocol). When theterminal has been blurred for 5+ minutes and gets focused again, a
recap is generated and added to the history. Conditions:
streamingState === Idle— never interrupts an active turn.nothing).
call is in flight will suppress the late-arriving recap.
Controlled by
general.showSessionRecap(default:true). The manual/recapcommand works regardless of the setting.Implementation
packages/core/src/services/sessionRecap.tsfastModel(falls back to main model) withtools: [], customsystemInstruction, and a 30-message windowpackages/cli/src/ui/hooks/useAwaySummary.tsuseFocus, fires recap on qualifying focus return, aborts on unmountpackages/cli/src/ui/commands/recapCommand.ts/recap, refuses when a turn is pendingStatusMessages.tsx+HistoryItemDisplay.tsx+types.tsaway_recaphistory item with dim renderersettingsSchema.tsgeneral.showSessionRecapboolean (default true)AppContainer.tsx,BuiltinCommandLoader.ts,core/src/index.tsKey design choices
Filter tool calls/responses out of the recap context. A single
functionResponsecan hold a 10K+ token file dump that drowns the recapLLM in irrelevant detail and inflates cost.
filterToDialogkeeps onlytext parts of
user/modelmessages.Structured output with extraction. The model is instructed to wrap its
answer in
<recap>...</recap>; the extractor returns empty (and theservice returns
null) when the tag is missing, instead of guessing. Thisprevents reasoning preamble from leaking into the UI — we observed GLM
models prepending a thinking paragraph before the answer otherwise.
Best-effort, never breaks main flow. All failures (network error,
malformed response, unavailable client) are silently caught and logged
through a scoped
debugLogger('SESSION_RECAP'). The user never sees anerror from the recap path.
Token-tight one-shot call.
tools: [],maxOutputTokens: 300,temperature: 0.3, customsystemInstructionoverriding the main agentprompt. Uses
fastModelwhen configured (e.g.,qwen3-coder-flash),falling back to the session model.
What's intentionally not in this PR
/recap— 3-5s wait is tolerable; can beadded later with a pending item pattern like
/summary.tested manually; tests can be added separately.
QWEN_CODE_ENABLE_AWAY_SUMMARYenv var — covers the "telemetrydisabled" case in Claude Code's equivalent. Not relevant to Qwen Code's
current telemetry model.
/resume— would be a natural follow-up;useResumeCommandhas a clean hook point.
Test plan
npm run typecheck— 0 errorsnpm run lint— clean on all touched filesuseFocusandBuiltinCommandLoadertests still pass/helplists/recap;/recapafter amulti-turn conversation produces clean dim-color output without
preamble; tool calls in history are correctly filtered
outside CI — tmux focus events are not a reliable proxy)