Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
821979e
docs: design spec for @ session reference + tabbed completion UI
LaZzyMan Jul 17, 2026
8a6c19d
docs: implementation plan for @ session reference + tabbed completion UI
LaZzyMan Jul 17, 2026
d03b338
feat(cli): add @session: mention ref parser
LaZzyMan Jul 17, 2026
c8c67af
feat(core): add SessionReferenceService for slimmed session injection
LaZzyMan Jul 17, 2026
5fe3554
feat(cli): inject slimmed prior-session context on @session: mention
LaZzyMan Jul 17, 2026
fea0161
feat(cli): surface prior sessions as @ completion suggestions
LaZzyMan Jul 17, 2026
36004b5
feat(cli): tabbed category layout for @ completion dropdown
LaZzyMan Jul 17, 2026
c3f482a
feat(cli): ←/→ tab switching for @ completion categories
LaZzyMan Jul 17, 2026
05b876c
docs: mark @ session reference design as implemented
LaZzyMan Jul 17, 2026
dfd60e8
fix(cli): preserve assistant text on tool-call turns, keep newest tur…
LaZzyMan Jul 17, 2026
2214de2
docs(core): clarify SessionReferenceService slimming rules
LaZzyMan Jul 17, 2026
08bbbc2
perf(cli): remove @ completion input latency from session listing
LaZzyMan Jul 20, 2026
9083aab
Revert "perf(cli): remove @ completion input latency from session lis…
LaZzyMan Jul 20, 2026
fdbb7e4
Merge remote-tracking branch 'origin/main' into lazzy/at-session-ref
LaZzyMan Jul 20, 2026
0e81ea7
Merge remote-tracking branch 'origin/main' into lazzy/at-session-ref
LaZzyMan Jul 20, 2026
0b6c474
fix(cli): harden session ref resolution and i18n tab labels (#7302)
qwen-code-ci-bot Jul 21, 2026
d7c341f
fix(cli): guard session ref I/O errors to never abort the turn (#7302)
Jul 21, 2026
ffd6645
fix(cli): strip session: prefix in completion filter and align tab gu…
Jul 21, 2026
40cc4cc
fix(cli): address review feedback on session refs and tab tests (#7302)
qwen-code-ci-bot Jul 21, 2026
645c8a3
fix(cli): use SESSION_MENTION_PREFIX constant in completion filter (#…
qwen-code-ci-bot Jul 21, 2026
a50b760
fix(cli): address review feedback on session refs and tab tests (#7302)
Jul 21, 2026
34e57e8
fix(cli): address review feedback on session refs and tab tests (#7302)
Jul 21, 2026
e9e4bcf
fix(cli): surface real session lookup errors and test left tab switch…
Jul 21, 2026
58f29ea
fix(cli): reset suggestion indices when active category tab disappear…
Jul 21, 2026
d7eb219
fix(cli): address review feedback on session refs docs and tests (#7302)
Jul 22, 2026
7d90d1f
fix(cli): address review feedback on session refs and completion orde…
Jul 22, 2026
cb9341b
docs(cli): clarify title matching semantics in session reference desi…
Jul 22, 2026
015f283
Merge branch 'main' into lazzy/at-session-ref
wenshao Jul 23, 2026
c9be8c9
Merge branch 'main' into lazzy/at-session-ref
wenshao Jul 23, 2026
5605c03
test(cli): assert error card in ambiguous session title test (#7302)
Jul 23, 2026
91d60e9
Merge branch 'main' into lazzy/at-session-ref
wenshao Jul 23, 2026
7a54bfc
fix(core,cli): address review feedback on session refs (#7302)
qwen-code-ci-bot Jul 23, 2026
65023a2
fix(core,cli): address review feedback on session refs (#7302)
Jul 23, 2026
c995c6d
fix(core,cli): address review feedback on session refs (#7302)
Jul 23, 2026
1e32b91
fix(core): address review feedback on session refs (#7302)
Jul 23, 2026
4f655fb
fix(core,cli): address review feedback on session refs (#7302)
qwen-code-ci-bot Jul 23, 2026
37b9d99
fix(core): address review feedback on session refs (#7302)
Jul 23, 2026
74881a1
fix(cli): address review feedback on session refs (#7302)
Jul 24, 2026
eade9fd
fix(core,cli): address review feedback on session refs (#7302)
Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,136 changes: 1,136 additions & 0 deletions docs/superpowers/plans/2026-07-17-at-session-reference.md

Large diffs are not rendered by default.

193 changes: 193 additions & 0 deletions docs/superpowers/specs/2026-07-17-at-session-reference-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# `@` Session Reference + Tabbed Completion UI — Design

Date: 2026-07-17
Branch: `lazzy/at-session-ref`
Status: Implemented

## 1. Goal

Two related enhancements to the interactive `@` mention feature:

1. **Reference prior sessions via `@`.** Let a user pull a _condensed_ copy of an
earlier chat session's history into the current context as reference material —
without having to `fork` the session. The reference is injected as read-only
context, not as a resumed/forked timeline.
2. **Tabbed `@` completion UI.** Because `@` now surfaces more categories
(files, directories, sessions, MCP servers/resources, extensions), redesign
the suggestion dropdown into a **tab-switched** layout so it stays usable.

## 2. Decisions (locked)

| Dimension | Decision |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| How the session is condensed | **Mechanical slimming, NO LLM call** — deterministic |
| What slimming keeps | **user + assistant text + a one-line summary per tool call** (name + status; no bulky tool results) |
| Referenceable scope | **Current project only** (project-hash scoped, same as `resume`) |
| How `@` surfaces sessions | **Bare `@` shows a Sessions group** (no prefix required to discover); a `@session:` prefix also works for direct addressing |
| Completion UI shape | **Tab switch** — top tab bar (All / Files / Sessions / MCP / Extensions), single list below shows the active tab |
| Injected size cap | **Fixed token budget with tail-retention** — drop oldest turns first, mark as truncated |

Explicitly rejected: LLM summarization (conflicts with the "inject slimmed
original, no model call" decision), reusing `ChatCompressionService` (it calls
the model and is coupled to a live `GeminiChat`), cross-project referencing.

## 3. Architecture (Approach 1 — core service + localized UI changes)

Split into two independent halves: **backend** (parse + load + slim + inject)
lives in core and is a pure, unit-testable function; **frontend** (tabbed
dropdown) is a localized render-layer + keybinding change.

```
packages/core/src/services/
sessionReferenceService.ts [NEW] load → slim → budget-trim → injectable text

packages/cli/src/ui/hooks/
sessionMentionRef.ts [NEW] parse/build/validate @session:<id|title>
atCommandProcessor.ts [EDIT] new @session: routing branch → service → Part
useAtCompletion.ts [EDIT] new session-suggestion producer (category tag)
useCompletion.ts [EDIT] track activeCategory; reset index on tab switch

packages/cli/src/ui/components/
SuggestionsDisplay.tsx [EDIT] tab bar + filter rows by active category
InputPrompt.tsx [EDIT] new keybinding to switch tab (←/→)
```

### Module responsibilities

| Unit | Responsibility | In → Out | Reuses |
| --------------------------- | ---------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------- |
| `SessionReferenceService` | Turn a session id into injectable slimmed text | `sessionId` → `{ text, meta, truncated }` | `SessionService.loadSession`, `filterToDialog`, `estimateContentTokens` |
| `sessionMentionRef.ts` | Parse/build/validate `@session:` refs | string ↔ `{ id?, title? }` | — |
| `atCommandProcessor` (edit) | Route `@session:` → service → injected part | ref token → `Part` | `SessionReferenceService` |
| session producer (edit) | List referenceable sessions as suggestions | pattern → `Suggestion[]` (`category:'session'`) | `SessionService.listSessions` |
| `SuggestionsDisplay` (edit) | Render category tabs; show active tab's rows | `Suggestion[]` + `activeCategory` → TUI | — |

## 4. Backend: session → injectable text

1. **Resolve ref.** `@session:<arg>` where `<arg>` is a session UUID or a
(custom) title. UUID → direct; title → `SessionService.findSessionsByTitle`
(active, current-project only). Ambiguous title (>1 match) → the completion
UI already disambiguates; at submit time a still-ambiguous title is reported
as an unresolved mention (left as literal text, with a note), not guessed.
Note: title matching compares against the session's explicit `customTitle`
(set by auto-title or `/rename`), not the first-prompt label shown in the
completion dropdown. The picker always inserts `@session:<uuid>`, so the
mainline path is unaffected; a hand-typed `@session:<free text>` resolves
only when it case-insensitively equals an existing session title.
2. **Load.** `SessionService.loadSession(id)` → `ConversationRecord.messages`
(`ChatRecord[]`). Guard: `sessionBelongsToCurrentProject` (already enforced
inside `loadSession`) — cross-project ids resolve to "not found".
3. **Slim (deterministic, no model call).**
- Reuse `filterToDialog` to keep user + assistant visible text, dropping
thoughts.
- For records that are tool calls (`message.parts` functionCall /
`toolCallResult`), emit a single line: `[tool: <displayName> — <status>]`.
Do **not** include tool result bodies.
- Preserve chronological order; render as a labeled block:
`--- Referenced session "<title>" (slimmed, read-only) ---\n<body>`.
4. **Budget-trim.** Estimate with `estimateContentTokens`. Cap at a fixed budget
(default `SESSION_REF_TOKEN_BUDGET`, ~8k, configurable). If over budget,
**drop oldest turns first** (tail-retention) and prepend a
`[earlier turns omitted]` marker so the model knows it is truncated.
Return `truncated: true` in meta.
5. **Fast path.** If the loaded records contain a `chat_compression` record with
`systemPayload.compressedHistory`, that snapshot MAY be used as the slimmed
body directly (already condensed) — optional optimization, not required for v1.

Output shape:

```ts
interface SlimmedSessionReference {
text: string; // labeled, budget-trimmed block
meta: {
sessionId: string;
title: string;
messageCount: number;
approxTokens: number;
};
truncated: boolean;
}
```

### Injection (atCommandProcessor)

New routing branch, ordered **after** extension/MCP refs and **before** the
filesystem path fall-through (so `session:` with its `:` is never mistaken for a
path). Resolved session text is added to `scopedMentionParts` (same bucket as
MCP-server context), so final assembly stays grouped-by-type. The `@session:…`
token is left verbatim in the prompt text; the model correlates it with the
`--- Referenced session … ---` block. A tool-call display card
("Referenced session") is emitted, mirroring the existing Read File / Activate
Extension cards. Unresolved / not-found / cross-project refs fall back to literal
text with a surfaced note (never silently dropped).

## 5. Frontend: tabbed completion UI

- **Suggestion type.** Add `category?: SuggestionCategory` to `Suggestion`
(`'file' | 'session' | 'mcp' | 'extension'`). Existing `sourceBadge` stays for
inline labels; `category` drives tab grouping. Files with no tag default to
`'file'`.
- **Producer.** In `useAtCompletion.ts` add a session producer that calls
`SessionService.listSessions` (current project), maps each to a `Suggestion`
with `category:'session'`, `label` = title (fallback: first user prompt,
truncated), `value` = `@session:<id>`, `description` = first user prompt
(when session has a custom title; otherwise omitted). Shown on **bare `@`** (like extensions) and filtered by pattern.
- **Rendering.** `SuggestionsDisplay.tsx` gains an optional top tab bar modeled on
`StatsDialog`'s `StatsTabs` / `handleTabChange` / `useKeypress` trio: tabs are
`All` + each non-empty category. `All` shows every suggestion (current
behavior); a specific tab filters to that category.
When only one category is present, the tab bar is hidden (no regression for
plain file completion).
- **Keyboard.** `↑/↓` selects within the active tab (unchanged). Tab **switching**
uses `←/→` (and/or `Shift+Tab`) via a **new keybinding Command**, because
`Command.ACCEPT_SUGGESTION` already binds BOTH `Tab` and `Enter` in
`InputPrompt.tsx` — reusing `Tab` would collide. `useCompletion` tracks
`activeCategory` and resets `activeSuggestionIndex` / scroll on switch.
- **State ownership.** `activeCategory` lives in `useCompletion` (alongside
`activeSuggestionIndex`), so accept/scroll logic stays in one place.

## 6. Error handling

| Case | Behavior |
| -------------------------------------- | ----------------------------------------------------------------- |
| Session id not found / cross-project | Ref left as literal text + surfaced note; no throw |
| Title matches >1 session at submit | Reported as ambiguous unresolved mention (literal text + note) |
| Loaded history empty after slimming | Inject a short `(no textual content)` note instead of empty block |
| Over token budget | Tail-retain, prepend `[earlier turns omitted]`, `truncated:true` |
| `listSessions` fails (I/O) in producer | Session tab shows empty/"unavailable"; other tabs unaffected |
| Only one category available | Tab bar hidden; behaves exactly as today |

## 7. Testing

**Core (unit, no model):**

- `sessionReferenceService`: slimming keeps user+assistant text; tool records
collapse to one-line summaries; tool result bodies excluded; chronological
order; budget trim drops oldest first + sets `truncated`; empty-history note;
cross-project id → not found.
- `sessionMentionRef`: parse/build/validate round-trip; UUID vs title; malformed.

**CLI (component / hook):**

- `atCommandProcessor`: `@session:<id>` routes to service and injects into
`scopedMentionParts`; ordering vs MCP/ext/file; unresolved → literal fallback;
display card emitted.
- `useAtCompletion`: session producer appears on bare `@`; filters by pattern;
current-project scoping.
- `SuggestionsDisplay`: tab bar renders per non-empty category; `All` shows all;
filtering by active category; single-category hides tab bar.
- `InputPrompt` / keybinding: `←/→` switches tab without triggering accept;
`Tab`/`Enter` still accept; index resets on switch.

**Manual / e2e:** type `@`, switch to Sessions tab, pick a prior session, submit,
confirm the slimmed block reaches the model as a labeled context part and a
"Referenced session" card is shown.

## 8. Out of scope (v1)

- LLM-generated summaries of referenced sessions.
- Cross-project / cross-workspace session referencing.
- Referencing archived sessions by `@` (title search is active-only, matching
existing `findSessionsByTitle`).
- Left/right split-column dropdown layout (chose tab switch instead).
- Referencing a _range_ / specific messages within a session (whole-session only).
13 changes: 13 additions & 0 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export enum Command {
ACCEPT_SUGGESTION = 'acceptSuggestion',
COMPLETION_UP = 'completionUp',
COMPLETION_DOWN = 'completionDown',
COMPLETION_TAB_LEFT = 'completionTabLeft',
COMPLETION_TAB_RIGHT = 'completionTabRight',

// Text input
SUBMIT = 'submit',
Expand Down Expand Up @@ -185,6 +187,17 @@ export const defaultKeyBindings: KeyBindingConfig = {
{ key: 'down', shift: false },
{ key: 'n', ctrl: true },
],
// Completion category tab switching (for the tabbed @ completion UI).
// Bound to Ctrl+arrows rather than plain arrows so the bare arrow keys keep
// moving the caret in the editable input buffer (plain arrows only switch
// tabs in modal dialogs, which have no text buffer). Alt/Option+arrows still
// perform word movement.
[Command.COMPLETION_TAB_LEFT]: [
{ key: 'left', shift: false, ctrl: true, command: false },
],
[Command.COMPLETION_TAB_RIGHT]: [
{ key: 'right', shift: false, ctrl: true, command: false },
],

// Text input
// Must also exclude shift to allow shift+enter for newline
Expand Down
123 changes: 123 additions & 0 deletions packages/cli/src/ui/components/InputPrompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ describe('InputPrompt', () => {
setActiveSuggestionIndex: vi.fn(),
setShowSuggestions: vi.fn(),
handleAutocomplete: vi.fn(),
activeCategory: 'all' as const,
availableCategories: ['all'] as Array<'all'>,
switchCategory: vi.fn(),
};
mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion);

Expand Down Expand Up @@ -2549,6 +2552,126 @@ describe('InputPrompt', () => {
unmount();
});

it('should NOT switch category on left/right when availableCategories <= 2', async () => {
const switchCategory = vi.fn();
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
completionMode: CompletionMode.AT,
showSuggestions: true,
Comment thread
qwen-code-dev-bot marked this conversation as resolved.
suggestions: [
{ label: 'file.ts', value: 'file.ts' },
{ label: 'other.ts', value: 'other.ts' },
],
activeSuggestionIndex: 0,
isPerfectMatch: false,
availableCategories: ['all'],
switchCategory,
});
props.buffer.setText('@file');

const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
await wait();

stdin.write('\x1b[C'); // right arrow
await wait();
stdin.write('\x1b[D'); // left arrow
await wait();

expect(switchCategory).not.toHaveBeenCalled();
unmount();
});

it('should NOT switch category on Ctrl+left/right when availableCategories is exactly 2', async () => {
const switchCategory = vi.fn();
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
completionMode: CompletionMode.AT,
showSuggestions: true,
suggestions: [{ label: 'file.ts', value: 'file.ts', category: 'file' }],
activeSuggestionIndex: 0,
isPerfectMatch: false,
availableCategories: ['all', 'file'],
switchCategory,
});
props.buffer.setText('@file');

const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
await wait();

stdin.write('\x1b[1;5C'); // Ctrl+right arrow
await wait();
stdin.write('\x1b[1;5D'); // Ctrl+left arrow
await wait();

// With only 2 entries (all + one real category) the tab bar is hidden,
// so Ctrl+arrows must not trigger category switching.
expect(switchCategory).not.toHaveBeenCalled();
unmount();
});

it('should switch category on Ctrl+left/right when availableCategories > 2', async () => {
const switchCategory = vi.fn();
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
completionMode: CompletionMode.AT,
showSuggestions: true,
suggestions: [
{ label: 'file.ts', value: 'file.ts', category: 'file' },
{ label: 'sess', value: 'sess', category: 'session' },
],
activeSuggestionIndex: 0,
isPerfectMatch: false,
availableCategories: ['all', 'file', 'session'],
switchCategory,
});
props.buffer.setText('@');

const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
await wait();

stdin.write('\x1b[1;5C'); // Ctrl+right arrow
await wait();

expect(switchCategory).toHaveBeenCalledWith(1);

stdin.write('\x1b[1;5D'); // Ctrl+left arrow
await wait();

expect(switchCategory).toHaveBeenCalledWith(-1);
unmount();
});

it('should NOT switch category on plain left/right when availableCategories > 2 (caret stays free)', async () => {
const switchCategory = vi.fn();
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
completionMode: CompletionMode.AT,
showSuggestions: true,
suggestions: [
{ label: 'file.ts', value: 'file.ts', category: 'file' },
{ label: 'sess', value: 'sess', category: 'session' },
],
activeSuggestionIndex: 0,
isPerfectMatch: false,
availableCategories: ['all', 'file', 'session'],
switchCategory,
});
props.buffer.setText('@');

const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
await wait();

stdin.write('\x1b[C'); // plain right arrow
await wait();
stdin.write('\x1b[D'); // plain left arrow
await wait();

// Plain arrows must not be hijacked for tab switching, so they remain
// available to move the caret in the editable buffer.
expect(switchCategory).not.toHaveBeenCalled();
unmount();
});

it('should reset history navigation after submitting on Enter', async () => {
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
Expand Down
Loading
Loading