Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
12 changes: 10 additions & 2 deletions packages/cli/src/ui/components/SessionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ function SessionListItemView({
? prefixChars.scrollDown
: prefixChars.normal;

const promptText = session.customTitle || session.prompt || '(empty prompt)';
const promptText =
session.customTitle ||
session.prompt ||
session.goalObjective ||
'(empty prompt)';
// Reserve space for the checkbox when multi-select is active so the
// prompt column doesn't shift between modes.
const checkboxWidth = isChecked === undefined ? 0 : 4; // "[x] "
Expand Down Expand Up @@ -274,7 +278,11 @@ export function SessionPicker(props: SessionPickerProps) {
<SessionPreview
sessionService={sessionService}
sessionId={picker.previewSessionId}
sessionTitle={previewed?.customTitle ?? previewed?.prompt ?? undefined}
sessionTitle={
previewed?.customTitle ||
previewed?.prompt ||
previewed?.goalObjective
}
messageCount={previewed?.messageCount}
mtime={previewed?.mtime}
gitBranch={previewed?.gitBranch}
Expand Down
54 changes: 54 additions & 0 deletions packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,33 @@ describe('SessionPicker', () => {
});

describe('Display', () => {
it('falls back to the Goal objective when the session has no title or prompt', async () => {
const sessions = [
createMockSession({
customTitle: undefined,
prompt: '',
goalObjective: 'Ship the requested change',
}),
];
const mockService = createMockSessionService(sessions);

const { lastFrame } = render(
<KeypressProvider kittyProtocolEnabled={false}>
<SessionPicker
sessionService={mockService as never}
onSelect={vi.fn()}
onCancel={vi.fn()}
/>
</KeypressProvider>,
);

await flush();

const output = lastFrame() ?? '';
expect(output).toContain('Ship the requested change');
expect(output).not.toContain('(empty prompt)');
});

it('should show session metadata', async () => {
const sessions = [
createMockSession({
Expand Down Expand Up @@ -759,6 +786,33 @@ describe('SessionPicker', () => {
};
}

it('uses the Goal objective as the preview title', async () => {
const sessions = [
createMockSession({
sessionId: 's1',
prompt: '',
goalObjective: 'Ship the requested change',
}),
];
const service = createMockSessionService(sessions);
service.loadSession.mockResolvedValue(fakeResumedData('s1'));

const { stdin, lastFrame } = renderPicker(
<SessionPicker
sessionService={service as never}
onSelect={vi.fn()}
onCancel={vi.fn()}
enablePreview
/>,
);

await flush();
stdin.write(' ');
await flush();

expect(lastFrame() ?? '').toContain('Ship the requested change');
});

it('renders tool_group items without crashing (stub Providers mounted)', async () => {
// The previewed session contains a function call + tool_result, which
// produces a `tool_group` HistoryItem that exercises ToolGroupMessage
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/ui/utils/sessionPickerUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe('sessionPickerUtils', () => {
s({ sessionId: 'b', customTitle: 'Add OAuth flow', gitBranch: 'feat' }),
s({ sessionId: 'c', prompt: 'random work', gitBranch: 'hotfix/login' }),
s({ sessionId: 'd', prompt: 'unrelated', gitBranch: 'main' }),
s({ sessionId: 'e', goalObjective: 'Ship release notes' }),
];

it('passes everything through when no filter is set', () => {
Expand All @@ -98,6 +99,11 @@ describe('sessionPickerUtils', () => {
expect(result.map((x) => x.sessionId)).toEqual(['b']);
});

it('matches the query against the Goal objective', () => {
const result = filterSessions(sessions, false, undefined, 'release');
expect(result.map((x) => x.sessionId)).toEqual(['e']);
});

it('composes branch filter and query as AND', () => {
// Branch filter narrows to main; query then drops the unrelated row
// even though 'unrelated' is on main.
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/ui/utils/sessionPickerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function truncateToDisplayWidth(text: string, maxWidth: number): string {

/**
* Returns true when the session matches the query as a substring on any of:
* customTitle, first prompt, gitBranch.
* customTitle, first prompt, Goal objective, gitBranch.
*
* Empty queries match everything. The query is expected pre-normalized —
* `filterSessions` does the trim+lowercase once before the per-session
Expand All @@ -78,6 +78,7 @@ function matchesQuery(
const haystacks: Array<string | undefined> = [
session.customTitle,
session.prompt,
session.goalObjective,
session.gitBranch,
];
for (const h of haystacks) {
Expand All @@ -91,8 +92,9 @@ function matchesQuery(
*
* Branch filter and query filter compose (AND): when both are active, a
* session must satisfy both. Query is matched case-insensitively against
* customTitle, prompt, and gitBranch — branch is included in query matching
* so users can type a branch name without first toggling branch-filter.
* customTitle, prompt, Goal objective, and gitBranch — branch is included in
* query matching so users can type a branch name without first toggling
* branch-filter.
*/
export function filterSessions(
sessions: SessionListItem[],
Expand Down
Loading
Loading