fix(tui): make resume picker full-screen
This commit is contained in:
@@ -30,7 +30,7 @@ The footer sums the session's reported usage as `↑<uncached input> ↓<output>
|
||||
|
||||
`/status` adds a point-in-time diagnostics card to the transcript and remains available while the agent runs. It reports the session id, title, working directory, selected provider/model, reasoning-block visibility, agent state, event/turn/step/tool-call counts, exact input/output/cache token buckets, KV-cache hit rate, token-meter context use and capacity, creation time, and latest event time. Missing titles, models, cache input, or context capacity are labeled instead of inferred. The card is terminal-only and does not duplicate the compact footer.
|
||||
|
||||
`/resume` opens a keyboard selector over the current workspace. Candidates are sorted by last logged activity and searchable by log-backed title or session id; each row reports current/live/persisted state, last turn outcome, recent provider/model, and durable goal phase when present. The current session, a session already live in this runtime, an unreadable log, a mismatched cwd, or a session whose logged provider has no current adapter remains visible but disabled. Selection repeats those checks and requires the current agent to be idle before flushing the current session. The TUI then stops the terminal UI and calls the optional host-owned `TuiRuntime.handoffResume`; where `process.execve` is available, the shipped `dsh` host disposes the app and replaces its process. Resume restores the same `SessionId`, transcript, title, todos, and durable goal; goal activation remains disarmed and the TUI asks for human confirmation or `/goal resume`.
|
||||
`/resume` opens a full-viewport keyboard selector over the current workspace instead of a centered dialog. Its focused search field starts immediately after the search glyph and emits pi-tui's cursor marker, so terminal IME composition remains anchored inside the field. Candidates are sorted by last logged activity and searchable by log-backed title or session id; each row reports current/live/persisted state, last turn outcome, recent provider/model, and durable goal phase when present. Up/Down and Page Up/Page Down navigate, Enter resumes, Escape clears a non-empty search before a second Escape cancels, and Ctrl+C cancels directly. The current session, a session already live in this runtime, an unreadable log, a mismatched cwd, or a session whose logged provider has no current adapter remains visible but disabled. Selection repeats those checks and requires the current agent to be idle before flushing the current session. The TUI then stops the terminal UI and calls the optional host-owned `TuiRuntime.handoffResume`; where `process.execve` is available, the shipped `dsh` host disposes the app and replaces its process. Resume restores the same `SessionId`, transcript, title, todos, and durable goal; goal activation remains disarmed and the TUI asks for human confirmation or `/goal resume`.
|
||||
|
||||
`resumeCommand` remains the deployment-owned fallback: exiting prints it only after the current session is durable, and a host without in-place handoff shows the selected session's command. `{session}` expands to the session id. TUI code never executes the template or arbitrary shell text.
|
||||
|
||||
@@ -49,8 +49,6 @@ The footer sums the session's reported usage as `↑<uncached input> ↓<output>
|
||||
| `questionDialogMaxHeight` | `20` | Question-panel maximum rows |
|
||||
| `modelDialogWidth` | `72` | Model-selector width in columns |
|
||||
| `modelDialogMaxHeight` | `20` | Model-selector maximum rows |
|
||||
| `resumeDialogWidth` | `88` | Resume-selector width in columns |
|
||||
| `resumeDialogMaxHeight` | `24` | Resume-selector maximum rows |
|
||||
| `fileSearchMaxResults` | `20` | Maximum file and directory candidates shown for one `@` query |
|
||||
| `fileSearchMaxEntries` | `10000` | Maximum paths retained in the bounded workspace index used by bare fuzzy queries |
|
||||
| `fileSearchExcludedDirectories` | `['.git', 'node_modules']` | Directory basenames omitted from traversal and direct completion |
|
||||
|
||||
@@ -205,10 +205,6 @@ export interface TuiConfig {
|
||||
modelDialogWidth?: number
|
||||
/** Model-selector maximum height in terminal rows. */
|
||||
modelDialogMaxHeight?: number
|
||||
/** Resume-selector width in terminal columns. */
|
||||
resumeDialogWidth?: number
|
||||
/** Resume-selector maximum height in terminal rows. */
|
||||
resumeDialogMaxHeight?: number
|
||||
/** Maximum fuzzy file candidates displayed for one `@` query. */
|
||||
fileSearchMaxResults?: number
|
||||
/** Maximum paths retained in one `@` workspace index. */
|
||||
@@ -239,8 +235,6 @@ const questionDialogWidthSchema = z.number().step(1).min(20).default(200)
|
||||
const questionDialogMaxHeightSchema = z.number().step(1).min(6).default(20)
|
||||
const modelDialogWidthSchema = z.number().step(1).min(20).default(72)
|
||||
const modelDialogMaxHeightSchema = z.number().step(1).min(6).default(20)
|
||||
const resumeDialogWidthSchema = z.number().step(1).min(36).default(88)
|
||||
const resumeDialogMaxHeightSchema = z.number().step(1).min(8).default(24)
|
||||
const fileSearchMaxResultsSchema = z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_RESULTS)
|
||||
const fileSearchMaxEntriesSchema = z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_ENTRIES)
|
||||
const fileSearchExcludedDirectoriesSchema = z.array(z.string()).default([...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES])
|
||||
@@ -260,8 +254,6 @@ const tuiConfigSchemaFields = {
|
||||
questionDialogMaxHeight: questionDialogMaxHeightSchema,
|
||||
modelDialogWidth: modelDialogWidthSchema,
|
||||
modelDialogMaxHeight: modelDialogMaxHeightSchema,
|
||||
resumeDialogWidth: resumeDialogWidthSchema,
|
||||
resumeDialogMaxHeight: resumeDialogMaxHeightSchema,
|
||||
fileSearchMaxResults: fileSearchMaxResultsSchema,
|
||||
fileSearchMaxEntries: fileSearchMaxEntriesSchema,
|
||||
fileSearchExcludedDirectories: fileSearchExcludedDirectoriesSchema,
|
||||
@@ -302,8 +294,6 @@ export const Config: z<Config> = z.object({
|
||||
questionDialogMaxHeight: tuiConfigSchemaFields.questionDialogMaxHeight,
|
||||
modelDialogWidth: tuiConfigSchemaFields.modelDialogWidth,
|
||||
modelDialogMaxHeight: tuiConfigSchemaFields.modelDialogMaxHeight,
|
||||
resumeDialogWidth: tuiConfigSchemaFields.resumeDialogWidth,
|
||||
resumeDialogMaxHeight: tuiConfigSchemaFields.resumeDialogMaxHeight,
|
||||
fileSearchMaxResults: tuiConfigSchemaFields.fileSearchMaxResults,
|
||||
fileSearchMaxEntries: tuiConfigSchemaFields.fileSearchMaxEntries,
|
||||
fileSearchExcludedDirectories: tuiConfigSchemaFields.fileSearchExcludedDirectories,
|
||||
@@ -324,8 +314,6 @@ export interface ResolvedTuiConfig {
|
||||
questionDialogMaxHeight: number
|
||||
modelDialogWidth: number
|
||||
modelDialogMaxHeight: number
|
||||
resumeDialogWidth: number
|
||||
resumeDialogMaxHeight: number
|
||||
fileSearchMaxResults: number
|
||||
fileSearchMaxEntries: number
|
||||
fileSearchExcludedDirectories: string[]
|
||||
@@ -370,8 +358,6 @@ export function resolveTuiConfig(config: TuiConfig | undefined): ResolvedTuiConf
|
||||
questionDialogMaxHeight: config?.questionDialogMaxHeight ?? 20,
|
||||
modelDialogWidth: config?.modelDialogWidth ?? 72,
|
||||
modelDialogMaxHeight: config?.modelDialogMaxHeight ?? 20,
|
||||
resumeDialogWidth: config?.resumeDialogWidth ?? 88,
|
||||
resumeDialogMaxHeight: config?.resumeDialogMaxHeight ?? 24,
|
||||
fileSearchMaxResults: config?.fileSearchMaxResults ?? DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
||||
fileSearchMaxEntries: config?.fileSearchMaxEntries ?? DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
||||
fileSearchExcludedDirectories: [...(config?.fileSearchExcludedDirectories ?? DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES)],
|
||||
@@ -1346,9 +1332,9 @@ function summarizeResumeCandidate(
|
||||
}
|
||||
}
|
||||
|
||||
/** Searchable keyboard selector over detached, preflighted resume summaries. */
|
||||
class ResumeDialog implements Component, Focusable {
|
||||
private query = ''
|
||||
/** Full-viewport keyboard selector over detached, preflighted resume summaries. */
|
||||
class ResumePicker implements Component, Focusable {
|
||||
private readonly search = new Input()
|
||||
private selectedIndex = 0
|
||||
private error = ''
|
||||
focused = false
|
||||
@@ -1356,87 +1342,133 @@ class ResumeDialog implements Component, Focusable {
|
||||
constructor(
|
||||
private readonly candidates: readonly ResumeCandidate[],
|
||||
private readonly maxVisible: number,
|
||||
private readonly workspaceLabel: string,
|
||||
private readonly viewportRows: () => number,
|
||||
private readonly palette: Palette,
|
||||
private readonly done: (candidate: ResumeCandidate) => void,
|
||||
private readonly cancel: () => void,
|
||||
) {}
|
||||
|
||||
invalidate(): void {}
|
||||
invalidate(): void {
|
||||
this.search.invalidate()
|
||||
}
|
||||
|
||||
private filtered(): ResumeCandidate[] {
|
||||
const query = this.query.trim().toLocaleLowerCase()
|
||||
const query = this.search.getValue().trim().toLocaleLowerCase()
|
||||
if (query === '') return [...this.candidates]
|
||||
return this.candidates.filter(candidate => candidate.title.toLocaleLowerCase().includes(query)
|
||||
|| candidate.record.header.id.toLocaleLowerCase().includes(query))
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
this.invalidate()
|
||||
const filtered = this.filtered()
|
||||
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
||||
if (matchesKey(data, Key.ctrl('c'))) {
|
||||
this.cancel()
|
||||
return
|
||||
}
|
||||
if (matchesKey(data, Key.up)) {
|
||||
if (matchesKey(data, Key.escape)) {
|
||||
if (this.search.getValue() === '') this.cancel()
|
||||
else {
|
||||
this.search.setValue('')
|
||||
this.selectedIndex = 0
|
||||
this.error = ''
|
||||
}
|
||||
} else if (matchesKey(data, Key.up)) {
|
||||
this.selectedIndex = filtered.length === 0
|
||||
? 0
|
||||
: (this.selectedIndex + filtered.length - 1) % filtered.length
|
||||
} else if (matchesKey(data, Key.down)) {
|
||||
this.selectedIndex = filtered.length === 0 ? 0 : (this.selectedIndex + 1) % filtered.length
|
||||
} else if (matchesKey(data, Key.pageUp)) {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible)
|
||||
} else if (matchesKey(data, Key.pageDown)) {
|
||||
this.selectedIndex = Math.min(
|
||||
Math.max(0, filtered.length - 1),
|
||||
this.selectedIndex + this.maxVisible,
|
||||
)
|
||||
} else if (matchesKey(data, Key.enter)) {
|
||||
const selected = filtered[this.selectedIndex]
|
||||
if (selected === undefined) this.error = 'No session matches this search.'
|
||||
else if (selected.disabledReason !== undefined) this.error = selected.disabledReason
|
||||
else this.done(selected)
|
||||
} else if (data === '\x7f' || data === '\b') {
|
||||
this.query = Array.from(this.query).slice(0, -1).join('')
|
||||
this.selectedIndex = 0
|
||||
this.error = ''
|
||||
} else if (!Array.from(data).some(character => character < ' ' || character === '\x7f')) {
|
||||
this.query += data
|
||||
this.selectedIndex = 0
|
||||
this.error = ''
|
||||
} else {
|
||||
const previous = this.search.getValue()
|
||||
this.search.focused = this.focused
|
||||
this.search.handleInput(data)
|
||||
if (this.search.getValue() !== previous) {
|
||||
this.selectedIndex = 0
|
||||
this.error = ''
|
||||
}
|
||||
}
|
||||
this.invalidate()
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const innerWidth = Math.max(1, width - 4)
|
||||
this.search.focused = this.focused
|
||||
const height = Math.max(1, this.viewportRows())
|
||||
const horizontalPadding = width >= 12 ? 2 : 0
|
||||
const contentWidth = Math.max(1, width - horizontalPadding * 2)
|
||||
const indent = ' '.repeat(horizontalPadding)
|
||||
const filtered = this.filtered()
|
||||
if (this.selectedIndex >= filtered.length) this.selectedIndex = Math.max(0, filtered.length - 1)
|
||||
const start = Math.max(0, Math.min(
|
||||
this.selectedIndex - Math.floor(this.maxVisible / 2),
|
||||
filtered.length - this.maxVisible,
|
||||
))
|
||||
const end = Math.min(filtered.length, start + this.maxVisible)
|
||||
const body: string[] = [
|
||||
this.query === ''
|
||||
? `${this.palette.muted('Search:')} ${this.palette.dim('title or session id')}`
|
||||
: this.palette.text(`Search: ${displayText(this.query)}`),
|
||||
const selected = filtered[this.selectedIndex]
|
||||
const position = selected === undefined ? 0 : this.selectedIndex + 1
|
||||
const lines: string[] = [
|
||||
'',
|
||||
`${indent}${this.palette.bold(this.palette.accent(`Resume session (${position} of ${filtered.length})`))}`,
|
||||
'',
|
||||
]
|
||||
|
||||
const searchInnerWidth = Math.max(1, contentWidth - 4)
|
||||
lines.push(`${indent}${this.palette.dim(`╭${'─'.repeat(Math.max(0, contentWidth - 2))}╮`)}`)
|
||||
const searchContent = (this.search.render(searchInnerWidth)[0] ?? '').replace(/^> /u, '⌕ ')
|
||||
const clippedSearch = truncateToWidth(searchContent, searchInnerWidth, '')
|
||||
lines.push(
|
||||
`${indent}${this.palette.dim('│')} ${clippedSearch}${' '.repeat(Math.max(0, searchInnerWidth - visibleWidth(clippedSearch)))} ${this.palette.dim('│')}`,
|
||||
`${indent}${this.palette.dim(`╰${'─'.repeat(Math.max(0, contentWidth - 2))}╯`)}`,
|
||||
'',
|
||||
`${indent}${this.palette.muted(displayText(this.workspaceLabel))}`,
|
||||
'',
|
||||
)
|
||||
|
||||
const candidateBudget = Math.max(1, Math.floor((height - 13) / 4))
|
||||
const visibleCount = Math.min(this.maxVisible, candidateBudget)
|
||||
const start = Math.max(0, Math.min(
|
||||
this.selectedIndex - Math.floor(visibleCount / 2),
|
||||
filtered.length - visibleCount,
|
||||
))
|
||||
const end = Math.min(filtered.length, start + visibleCount)
|
||||
const push = (line: string): void => {
|
||||
lines.push(`${indent}${truncateToWidth(line, contentWidth, '…')}`)
|
||||
}
|
||||
for (let index = start; index < end; index += 1) {
|
||||
const candidate = filtered[index] as ResumeCandidate
|
||||
const selected = index === this.selectedIndex
|
||||
const active = index === this.selectedIndex
|
||||
const status = [
|
||||
candidate.disabledReason === 'current session' ? 'current' : undefined,
|
||||
candidate.record.live ? 'live' : undefined,
|
||||
candidate.record.persisted ? 'persisted' : undefined,
|
||||
].filter((value): value is string => value !== undefined).join(' · ')
|
||||
const lead = `${selected ? '›' : ' '} ${displayText(candidate.title)}`
|
||||
body.push(selected ? this.palette.bold(this.palette.accent(lead)) : lead)
|
||||
const lead = `${active ? '❯' : ' '} ${displayText(candidate.title)}`
|
||||
push(active ? this.palette.bold(this.palette.accent(lead)) : lead)
|
||||
const route = candidate.route === undefined ? 'route unavailable' : `${candidate.route.provider}/${candidate.route.model}`
|
||||
const goal = candidate.goalPhase === undefined ? '' : ` · goal ${candidate.goalPhase}`
|
||||
body.push(this.palette.muted(` ${new Date(candidate.lastActivityAt).toISOString()} · ${candidate.lastTurn} · ${route}${goal}`))
|
||||
body.push(this.palette.dim(` ${status} · ${displayText(candidate.record.header.id)}`))
|
||||
push(this.palette.muted(` ${new Date(candidate.lastActivityAt).toISOString()} · ${candidate.lastTurn} · ${route}${goal}`))
|
||||
push(this.palette.dim(` ${status} · ${displayText(candidate.record.header.id)}`))
|
||||
if (candidate.disabledReason !== undefined) {
|
||||
body.push(this.palette.warning(` unavailable: ${displayText(candidate.disabledReason)}`))
|
||||
push(this.palette.warning(` unavailable: ${displayText(candidate.disabledReason)}`))
|
||||
}
|
||||
}
|
||||
if (filtered.length === 0) body.push(this.palette.warning('No matching sessions.'))
|
||||
if (filtered.length > this.maxVisible) body.push(this.palette.dim(`${this.selectedIndex + 1}/${filtered.length}`))
|
||||
body.push('', this.palette.dim('Type to search • ↑/↓ navigate • Enter resume • Esc cancel'))
|
||||
if (this.error !== '') body.push(this.palette.error(displayText(this.error)))
|
||||
return renderDialog('Resume session', body.flatMap(line => wrapTextWithAnsi(line, innerWidth)), width, this.palette)
|
||||
if (filtered.length === 0) push(this.palette.warning('No matching sessions.'))
|
||||
if (this.error !== '') {
|
||||
lines.push('')
|
||||
push(this.palette.error(displayText(this.error)))
|
||||
}
|
||||
|
||||
const footer = `${indent}${this.palette.dim('Type to search • ↑/↓ navigate • Enter resume • Esc clear/cancel')}`
|
||||
while (lines.length < height - 2) lines.push('')
|
||||
lines.push(footer, '')
|
||||
return lines.slice(0, height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2932,18 +2964,20 @@ export function createTuiChat(
|
||||
|| a.record.header.id.localeCompare(b.record.header.id))
|
||||
if (isDisposed() || scan !== resumeScan) return
|
||||
const session = overlayManager.open({
|
||||
create: () => new ResumeDialog(
|
||||
create: host => new ResumePicker(
|
||||
candidates,
|
||||
resolved.maxResumeOptions,
|
||||
runtime.formatCwd?.(agent.session.header.cwd) ?? formatCwd(agent.session.header.cwd),
|
||||
() => host.viewport.rows,
|
||||
palette,
|
||||
(candidate) => { void handoffResume(candidate, session) },
|
||||
() => { void session.close() },
|
||||
),
|
||||
options: {
|
||||
width: resolved.resumeDialogWidth,
|
||||
maxHeight: resolved.resumeDialogMaxHeight,
|
||||
anchor: 'center',
|
||||
margin: 1,
|
||||
width: '100%',
|
||||
maxHeight: '100%',
|
||||
anchor: 'top-left',
|
||||
margin: 0,
|
||||
},
|
||||
})
|
||||
resumeOverlay = session
|
||||
|
||||
@@ -1,69 +1,51 @@
|
||||
terminal 92x32 buffer=normal length=32 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "DSH snapshot"
|
||||
cursor hidden column=0 viewportRow=31 bufferRow=31
|
||||
cursor hidden column=6 viewportRow=4 bufferRow=4
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-blue bold
|
||||
style 10-16 bold
|
||||
1| " Snapshot agent ready."
|
||||
style 1-21 fg=bright-black
|
||||
2| " deepseek-v4-flash • main-session"
|
||||
style 1-34 dim
|
||||
3| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
4| " "
|
||||
style 1-1 inverse
|
||||
5| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
6| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
|
||||
style 0-43 dim
|
||||
style 65-91 dim
|
||||
7-8| <blank>
|
||||
9| " ╭ Resume session ──────────────────────────────────────────────────────────────────────╮ "
|
||||
style 2-89 fg=bright-blue
|
||||
10| " │ Search: title or session id │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-10 fg=bright-black
|
||||
style 12-30 dim
|
||||
style 89-89 fg=bright-blue
|
||||
11| " │ │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 89-89 fg=bright-blue
|
||||
12| " │ › Untitled session │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-21 fg=bright-blue bold
|
||||
style 89-89 fg=bright-blue
|
||||
13| " │ 2026-07-23T08:00:00.000Z · no completed turn · route unavailable │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-69 fg=bright-black
|
||||
style 89-89 fg=bright-blue
|
||||
14| " │ current · live · main-session │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-34 dim
|
||||
style 89-89 fg=bright-blue
|
||||
15| " │ unavailable: current session │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-33 fg=yellow
|
||||
style 89-89 fg=bright-blue
|
||||
16| " │ Resume selector design │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 89-89 fg=bright-blue
|
||||
17| " │ 2024-01-01T00:00:08.000Z · turn 1: completed · deepseek/deepseek-v4-pro │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-76 fg=bright-black
|
||||
style 89-89 fg=bright-blue
|
||||
18| " │ persisted · earlier-session │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-32 dim
|
||||
style 89-89 fg=bright-blue
|
||||
19| " │ │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 89-89 fg=bright-blue
|
||||
20| " │ Type to search • ↑/↓ navigate • Enter resume • Esc cancel │ "
|
||||
style 2-2 fg=bright-blue
|
||||
style 4-60 dim
|
||||
style 89-89 fg=bright-blue
|
||||
21| " ╰──────────────────────────────────────────────────────────────────────────────────────╯ "
|
||||
style 2-89 fg=bright-blue
|
||||
22-31| <blank>
|
||||
0| " "
|
||||
1| " Resume session (1 of 2) "
|
||||
style 2-24 fg=bright-blue bold
|
||||
2| " "
|
||||
3| " ╭──────────────────────────────────────────────────────────────────────────────────────╮ "
|
||||
style 2-89 dim
|
||||
4| " │ ⌕ │ "
|
||||
style 2-2 dim
|
||||
style 6-6 inverse
|
||||
style 89-89 dim
|
||||
5| " ╰──────────────────────────────────────────────────────────────────────────────────────╯ "
|
||||
style 2-89 dim
|
||||
6| " "
|
||||
7| " /workspace/project "
|
||||
style 2-19 fg=bright-black
|
||||
8| " "
|
||||
9| " ❯ Untitled session "
|
||||
style 2-19 fg=bright-blue bold
|
||||
10| " 2026-07-23T08:00:00.000Z · no completed turn · route unavailable "
|
||||
style 2-67 fg=bright-black
|
||||
11| " current · live · main-session "
|
||||
style 2-32 dim
|
||||
12| " unavailable: current session "
|
||||
style 2-31 fg=yellow
|
||||
13| " Resume selector design "
|
||||
14| " 2024-01-01T00:00:08.000Z · turn 1: completed · deepseek/deepseek-v4-pro "
|
||||
style 2-74 fg=bright-black
|
||||
15| " persisted · earlier-session "
|
||||
style 2-30 dim
|
||||
16| " "
|
||||
17| " "
|
||||
18| " "
|
||||
19| " "
|
||||
20| " "
|
||||
21| " "
|
||||
22| " "
|
||||
23| " "
|
||||
24| " "
|
||||
25| " "
|
||||
26| " "
|
||||
27| " "
|
||||
28| " "
|
||||
29| " "
|
||||
30| " Type to search • ↑/↓ navigate • Enter resume • Esc clear/cancel "
|
||||
style 2-70 dim
|
||||
31| " "
|
||||
|
||||
@@ -159,8 +159,6 @@ describe('TUI config', () => {
|
||||
questionDialogMaxHeight: 20,
|
||||
modelDialogWidth: 72,
|
||||
modelDialogMaxHeight: 20,
|
||||
resumeDialogWidth: 88,
|
||||
resumeDialogMaxHeight: 24,
|
||||
fileSearchMaxResults: 20,
|
||||
fileSearchMaxEntries: 10_000,
|
||||
fileSearchExcludedDirectories: ['.git', 'node_modules'],
|
||||
@@ -179,8 +177,6 @@ describe('TUI config', () => {
|
||||
questionDialogMaxHeight: 14,
|
||||
modelDialogWidth: 64,
|
||||
modelDialogMaxHeight: 16,
|
||||
resumeDialogWidth: 84,
|
||||
resumeDialogMaxHeight: 22,
|
||||
fileSearchMaxResults: 7,
|
||||
fileSearchMaxEntries: 123,
|
||||
fileSearchExcludedDirectories: ['.git', 'generated'],
|
||||
@@ -198,8 +194,6 @@ describe('TUI config', () => {
|
||||
questionDialogMaxHeight: 14,
|
||||
modelDialogWidth: 64,
|
||||
modelDialogMaxHeight: 16,
|
||||
resumeDialogWidth: 84,
|
||||
resumeDialogMaxHeight: 22,
|
||||
fileSearchMaxResults: 7,
|
||||
fileSearchMaxEntries: 123,
|
||||
fileSearchExcludedDirectories: ['.git', 'generated'],
|
||||
@@ -269,7 +263,7 @@ describe('resume command and /resume', () => {
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('opens a newest-active-first searchable selector and Esc cancels without side effects', async () => {
|
||||
it('opens a newest-active-first searchable selector and Esc clears before cancelling', async () => {
|
||||
const older = header('older-session', 500, '/workspace')
|
||||
const newer = header('newer-session', 2000, '/workspace')
|
||||
const handoff = vi.fn<NonNullable<TuiRuntime['handoffResume']>>()
|
||||
@@ -296,7 +290,11 @@ describe('resume command and /resume', () => {
|
||||
expect(output).not.toContain('foreign-session')
|
||||
result.terminal.send('Older')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Search: Older')
|
||||
expect(result.terminal.output).toContain('⌕ Older')
|
||||
result.terminal.send('\x1b')
|
||||
await tick()
|
||||
expect(result.terminal.output.slice(result.terminal.output.lastIndexOf('Resume session')))
|
||||
.not.toContain('⌕ Older')
|
||||
result.terminal.send('\x1b')
|
||||
await tick()
|
||||
expect(handoff).not.toHaveBeenCalled()
|
||||
@@ -325,7 +323,9 @@ describe('resume command and /resume', () => {
|
||||
result.terminal.send('\x7f')
|
||||
result.terminal.send('\x7f')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Search: title or session id')
|
||||
const cleared = result.terminal.output.slice(result.terminal.output.lastIndexOf('Resume session'))
|
||||
expect(cleared).toContain('⌕ ')
|
||||
expect(cleared).not.toContain('zz')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('current session')
|
||||
@@ -349,7 +349,7 @@ describe('resume command and /resume', () => {
|
||||
result.terminal.send('/resume')
|
||||
result.terminal.send('\r')
|
||||
await tick(); await tick()
|
||||
expect(result.terminal.output).toContain('1/3')
|
||||
expect(result.terminal.output).toContain('(1 of 3)')
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user