dsh-code 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/lib/index.mjs +6679 -5232
- package/lib/startup.mjs +1 -1
- package/lib/{theme-7u5Qo3dF.mjs → theme-B3orFUYz.mjs} +8 -0
- package/lib/types/app.d.ts +32 -45
- package/lib/types/attachments.d.ts +16 -7
- package/lib/types/completion.d.ts +29 -0
- package/lib/types/composer.d.ts +150 -0
- package/lib/types/git-workflow.d.ts +6 -0
- package/lib/types/index.d.ts +6 -188
- package/lib/types/locales/en.d.ts +65 -5
- package/lib/types/{authorization-panel.d.ts → panels/authorization-panel.d.ts} +6 -1
- package/lib/types/panels/completion-panel.d.ts +13 -0
- package/lib/types/panels/interaction-bars.d.ts +38 -0
- package/lib/types/{kernel-panels.d.ts → panels/kernel-panels.d.ts} +19 -38
- package/lib/types/{language-panel.d.ts → panels/language-panel.d.ts} +1 -1
- package/lib/types/panels/model-panels.d.ts +86 -0
- package/lib/types/{theme-panel.d.ts → panels/theme-panel.d.ts} +1 -1
- package/lib/types/{update-panel.d.ts → panels/update-panel.d.ts} +22 -1
- package/lib/types/provider-settings.d.ts +11 -0
- package/lib/types/render/inspector.d.ts +8 -0
- package/lib/types/render/status.d.ts +4 -1
- package/lib/types/render/text.d.ts +4 -0
- package/lib/types/runner/harness-gate.d.ts +83 -0
- package/lib/types/runner/input-history.d.ts +31 -0
- package/lib/types/runner/mode-cycle.d.ts +44 -0
- package/lib/types/runner/preferences.d.ts +40 -0
- package/lib/types/runner/quit.d.ts +27 -0
- package/lib/types/runner/search-rows.d.ts +38 -0
- package/lib/types/runner/session-io.d.ts +46 -0
- package/lib/types/runner/session-target.d.ts +42 -0
- package/lib/types/runner/startup-config.d.ts +33 -0
- package/lib/types/runner/submissions.d.ts +87 -0
- package/lib/types/session/attach.d.ts +39 -0
- package/lib/types/{history.d.ts → session/history.d.ts} +10 -0
- package/lib/types/{session-directory.d.ts → session/session-directory.d.ts} +25 -1
- package/lib/types/{session-switch.d.ts → session/session-switch.d.ts} +8 -0
- package/lib/types/{store.d.ts → session/store.d.ts} +1 -1
- package/lib/types/{subagents.d.ts → session/subagents.d.ts} +8 -1
- package/lib/types/settings-file.d.ts +10 -0
- package/lib/types/{panel-accent.d.ts → ui/panel-accent.d.ts} +1 -1
- package/lib/types/ui/panel-gap.d.ts +6 -0
- package/lib/types/ui/query-editor.d.ts +10 -0
- package/lib/types/ui/styled-rows.d.ts +8 -0
- package/lib/types/{terminal-title.d.ts → ui/terminal-title.d.ts} +1 -1
- package/lib/types/ui/ui-contract.d.ts +12 -0
- package/lib/types/ui/use-frames.d.ts +6 -0
- package/lib/types/ui/use-stable-input.d.ts +7 -0
- package/lib/types/version.d.ts +2 -0
- package/package.json +7 -5
- package/src/app.ts +707 -4108
- package/src/attachments.ts +65 -19
- package/src/completion.ts +117 -0
- package/src/composer.ts +1956 -0
- package/src/git-workflow.ts +18 -0
- package/src/index.ts +164 -645
- package/src/input-split.ts +24 -4
- package/src/internals.ts +1 -1
- package/src/locales/en.ts +66 -5
- package/src/locales/zh.ts +66 -5
- package/src/{authorization-panel.ts → panels/authorization-panel.ts} +30 -8
- package/src/panels/completion-panel.ts +79 -0
- package/src/panels/interaction-bars.ts +567 -0
- package/src/{kernel-panels.ts → panels/kernel-panels.ts} +142 -90
- package/src/{language-panel.ts → panels/language-panel.ts} +4 -4
- package/src/panels/model-panels.ts +1021 -0
- package/src/{theme-panel.ts → panels/theme-panel.ts} +5 -5
- package/src/{update-panel.ts → panels/update-panel.ts} +117 -10
- package/src/provider-settings.ts +38 -0
- package/src/rainbow.ts +13 -3
- package/src/render/inspector.ts +23 -0
- package/src/render/status.ts +80 -29
- package/src/render/text.ts +10 -1
- package/src/runner/harness-gate.ts +168 -0
- package/src/runner/input-history.ts +77 -0
- package/src/runner/mode-cycle.ts +49 -0
- package/src/runner/preferences.ts +67 -0
- package/src/runner/quit.ts +53 -0
- package/src/runner/search-rows.ts +55 -0
- package/src/runner/session-io.ts +206 -0
- package/src/runner/session-target.ts +81 -0
- package/src/runner/startup-config.ts +54 -0
- package/src/runner/submissions.ts +157 -0
- package/src/session/attach.ts +87 -0
- package/src/{fork.ts → session/fork.ts} +11 -7
- package/src/{history.ts → session/history.ts} +14 -0
- package/src/{session-directory.ts → session/session-directory.ts} +83 -4
- package/src/{session-switch.ts → session/session-switch.ts} +14 -0
- package/src/{store.ts → session/store.ts} +20 -2
- package/src/{subagents.ts → session/subagents.ts} +12 -1
- package/src/settings-file.ts +19 -1
- package/src/{panel-accent.ts → ui/panel-accent.ts} +1 -1
- package/src/ui/panel-gap.ts +9 -0
- package/src/ui/query-editor.ts +16 -0
- package/src/ui/styled-rows.ts +124 -0
- package/src/{terminal-title.ts → ui/terminal-title.ts} +1 -1
- package/src/ui/ui-contract.ts +10 -0
- package/src/ui/use-frames.ts +23 -0
- package/src/ui/use-stable-input.ts +17 -0
- package/src/version.ts +5 -0
- /package/lib/types/{fork.d.ts → session/fork.d.ts} +0 -0
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
/** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
|
|
2
2
|
|
|
3
|
-
import { createElement, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
|
3
|
+
import { createElement, useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
|
4
4
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
5
|
-
import type { ModelDirectory, ModelRow } from '
|
|
6
|
-
import type { SubagentRow } from '
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
13
|
-
import type {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
5
|
+
import type { ModelDirectory, ModelRow } from '../models.ts'
|
|
6
|
+
import type { SubagentRow } from '../session/subagents.ts'
|
|
7
|
+
import type { SearchRow } from '../runner/search-rows.ts'
|
|
8
|
+
export type { SearchRow } from '../runner/search-rows.ts'
|
|
9
|
+
import type { ScheduleRow } from '../render/projection.ts'
|
|
10
|
+
import type { PermissionRow } from '../permissions.ts'
|
|
11
|
+
import type { PresetRow } from '../presets.ts'
|
|
12
|
+
import type { PluginRow } from '../plugin-inventory.ts'
|
|
13
|
+
import type { SessionDirectoryOptions, SessionRow } from '../session/session-directory.ts'
|
|
14
|
+
import { formatRelativeTime, matchSessionRow } from '../session/session-directory.ts'
|
|
15
|
+
import type { ReviewBranch, ReviewCommit, ReviewSelection } from '../git-workflow.ts'
|
|
16
|
+
import { expandedDocumentViewport, panelViewport, revealRow } from '../render/inspector.ts'
|
|
17
|
+
import { markdownLines, textLines, type LineStyle, type StyledLine } from '../render/lines.ts'
|
|
18
|
+
import { usageLines, type UsageView } from '../render/usage.ts'
|
|
19
|
+
import { deleteLastGrapheme } from '../render/editor.ts'
|
|
20
|
+
import { stripPasteMarkers } from '../keyboard.ts'
|
|
21
|
+
import { DEFAULT_STATUSLINE_ITEMS, localizedStatusItems, type StatusItemId } from '../render/status.ts'
|
|
22
|
+
import { singleLineText, truncateColumns } from '../render/text.ts'
|
|
23
|
+
import { panelAccent } from '../ui/panel-accent.ts'
|
|
24
|
+
import { editQuery } from '../ui/query-editor.ts'
|
|
25
|
+
export { editQuery } from '../ui/query-editor.ts'
|
|
26
|
+
import { t } from '../i18n.ts'
|
|
27
|
+
import { getPalette, inkColor } from '../theme.ts'
|
|
24
28
|
|
|
25
29
|
interface ListFrameProps {
|
|
26
30
|
readonly title: string
|
|
@@ -96,18 +100,6 @@ function ListFrame(props: ListFrameProps): ReactElement {
|
|
|
96
100
|
)
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
/**
|
|
100
|
-
* Apply one keystroke to a panel search query. IME commits arrive as one
|
|
101
|
-
* multi-character chunk, so the whole printable run is appended; paste
|
|
102
|
-
* markers are stripped and control-laden chunks are ignored.
|
|
103
|
-
*/
|
|
104
|
-
export function editQuery(query: string, input: string, key: { backspace?: boolean; delete?: boolean }): string | undefined {
|
|
105
|
-
if (key.backspace || key.delete) return deleteLastGrapheme(query)
|
|
106
|
-
const text = stripPasteMarkers(input)
|
|
107
|
-
if (text !== '' && !/[\u0000-\u001f\u007f]/u.test(text)) return query + text
|
|
108
|
-
return undefined
|
|
109
|
-
}
|
|
110
|
-
|
|
111
103
|
export function ModePanel({ current, load, select, close }: {
|
|
112
104
|
current: string
|
|
113
105
|
load: () => Promise<readonly PresetRow[]>
|
|
@@ -119,13 +111,13 @@ export function ModePanel({ current, load, select, close }: {
|
|
|
119
111
|
const [cursor, setCursor] = useState(0)
|
|
120
112
|
const [loading, setLoading] = useState(true)
|
|
121
113
|
const [error, setError] = useState<string>()
|
|
122
|
-
const refresh = (): void => {
|
|
114
|
+
const refresh = useCallback((): void => {
|
|
123
115
|
setLoading(true); setError(undefined)
|
|
124
116
|
Promise.resolve().then(load).then(value => { setRows(value); setLoading(false) }, reason => {
|
|
125
117
|
setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false)
|
|
126
118
|
})
|
|
127
|
-
}
|
|
128
|
-
useEffect(refresh, [])
|
|
119
|
+
}, [load])
|
|
120
|
+
useEffect(refresh, [refresh])
|
|
129
121
|
const visible = useMemo(() => rows.filter(row => `${row.id} ${row.name ?? ''} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
|
|
130
122
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
|
|
131
123
|
useInput((input, key) => {
|
|
@@ -160,13 +152,13 @@ export function PermissionPanel({ current, load, select, close }: {
|
|
|
160
152
|
const [cursor, setCursor] = useState(0)
|
|
161
153
|
const [loading, setLoading] = useState(true)
|
|
162
154
|
const [error, setError] = useState<string>()
|
|
163
|
-
const refresh = (): void => {
|
|
155
|
+
const refresh = useCallback((): void => {
|
|
164
156
|
setLoading(true); setError(undefined)
|
|
165
157
|
Promise.resolve().then(load).then(value => { setRows(value); setLoading(false) }, reason => {
|
|
166
158
|
setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false)
|
|
167
159
|
})
|
|
168
|
-
}
|
|
169
|
-
useEffect(refresh, [])
|
|
160
|
+
}, [load])
|
|
161
|
+
useEffect(refresh, [refresh])
|
|
170
162
|
const visible = useMemo(() => rows.filter(row => `${row.id} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
|
|
171
163
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
|
|
172
164
|
useInput((input, key) => {
|
|
@@ -192,7 +184,8 @@ export function PluginPanel({ load, close, initialQuery = '' }: { load: () => re
|
|
|
192
184
|
const [query, setQuery] = useState(initialQuery)
|
|
193
185
|
const [cursor, setCursor] = useState(0)
|
|
194
186
|
const [expanded, setExpanded] = useState(false)
|
|
195
|
-
|
|
187
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- epoch is the panel's explicit registry refresh trigger
|
|
188
|
+
const rows = useMemo(() => load().filter(row => `${row.entryId} ${row.moduleName} ${row.phase ?? ''}`.toLowerCase().includes(query.toLowerCase())), [epoch, load, query])
|
|
196
189
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
|
|
197
190
|
useInput((input, key) => {
|
|
198
191
|
if (key.escape) return close()
|
|
@@ -283,7 +276,7 @@ export function JobsPanel({ load, close }: { load: () => readonly JobRow[]; clos
|
|
|
283
276
|
})
|
|
284
277
|
}
|
|
285
278
|
|
|
286
|
-
export function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }: {
|
|
279
|
+
export function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, presetId, close }: {
|
|
287
280
|
currentCwd: string
|
|
288
281
|
load: (options: SessionDirectoryOptions, signal?: AbortSignal) => Promise<readonly SessionRow[]>
|
|
289
282
|
readTranscript: (id: string, signal?: AbortSignal) => Promise<string>
|
|
@@ -296,12 +289,20 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
296
289
|
reloadToken?: number
|
|
297
290
|
/** Opened via /delete: hint-first delete mode. */
|
|
298
291
|
deleteMode?: boolean
|
|
292
|
+
/** `/delete <id>` argument to resolve after the listing loads. */
|
|
293
|
+
presetId?: string
|
|
299
294
|
close: () => void
|
|
300
295
|
}): ReactElement {
|
|
301
296
|
// Codex resume-picker default: the CURRENT directory's root sessions; the
|
|
302
297
|
// cwd filter widens to all only on request (the old default leaked every
|
|
303
298
|
// directory's sessions into what read as a current-directory view).
|
|
304
|
-
const [options, setOptions] = useState<SessionDirectoryOptions>({
|
|
299
|
+
const [options, setOptions] = useState<SessionDirectoryOptions>({
|
|
300
|
+
sessions: presetId === undefined || presetId === '' ? 'roots' : 'all',
|
|
301
|
+
cwd: presetId === undefined || presetId === '' ? 'current' : 'all',
|
|
302
|
+
sort: 'newest',
|
|
303
|
+
currentCwd,
|
|
304
|
+
query: '',
|
|
305
|
+
})
|
|
305
306
|
const [focus, setFocus] = useState(0)
|
|
306
307
|
const [density, setDensity] = useState<'comfortable' | 'dense'>('comfortable')
|
|
307
308
|
const [rows, setRows] = useState<readonly SessionRow[]>([])
|
|
@@ -313,6 +314,7 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
313
314
|
/** Ctrl+F-gated search: typing filters only while searching (codex). */
|
|
314
315
|
const [searching, setSearching] = useState(false)
|
|
315
316
|
/** Reference clock pinned per row render, so relative times never drift mid-list. */
|
|
317
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally re-pin when the listing or its options change
|
|
316
318
|
const now = useMemo(() => Date.now(), [rows, options])
|
|
317
319
|
const transcriptLoad = useRef<AbortController>()
|
|
318
320
|
useEffect(() => () => transcriptLoad.current?.abort(), [])
|
|
@@ -325,8 +327,22 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
325
327
|
if (!controller.signal.aborted) { setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false) }
|
|
326
328
|
})
|
|
327
329
|
return () => controller.abort()
|
|
328
|
-
}, [options, reloadToken])
|
|
330
|
+
}, [load, options, reloadToken])
|
|
329
331
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
|
|
332
|
+
const presetArmed = useRef(false)
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (presetArmed.current || presetId === undefined || presetId === '' || rows.length === 0 || requestDelete === undefined) return
|
|
335
|
+
try {
|
|
336
|
+
const row = matchSessionRow(rows, presetId)
|
|
337
|
+
presetArmed.current = true
|
|
338
|
+
const index = rows.findIndex(candidate => candidate.id === row.id)
|
|
339
|
+
if (index >= 0) setCursor(index)
|
|
340
|
+
requestDelete(row)
|
|
341
|
+
} catch (reason: unknown) {
|
|
342
|
+
presetArmed.current = true
|
|
343
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
344
|
+
}
|
|
345
|
+
}, [rows, presetId, requestDelete])
|
|
330
346
|
const cycle = (): void => {
|
|
331
347
|
if (focus === 3) {
|
|
332
348
|
setDensity(current => current === 'comfortable' ? 'dense' : 'comfortable')
|
|
@@ -363,7 +379,6 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
363
379
|
if (key.pageDown) return setCursor(value => Math.min(rows.length - 1, value + 8))
|
|
364
380
|
if (input === 'g') return setCursor(0)
|
|
365
381
|
if (input === 'G') return setCursor(Math.max(0, rows.length - 1))
|
|
366
|
-
if (input === 'd' && rows[cursor] !== undefined && requestDelete !== undefined) return requestDelete(rows[cursor])
|
|
367
382
|
if (input === 'e' && rows[cursor] !== undefined) {
|
|
368
383
|
return setExpanded(value => value === rows[cursor].id ? undefined : rows[cursor].id)
|
|
369
384
|
}
|
|
@@ -379,7 +394,13 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
379
394
|
)
|
|
380
395
|
return
|
|
381
396
|
}
|
|
382
|
-
if (key.return && rows[cursor]
|
|
397
|
+
if (key.return && rows[cursor] !== undefined) {
|
|
398
|
+
if (deleteMode) {
|
|
399
|
+
if (requestDelete !== undefined && !rows[cursor].live) requestDelete(rows[cursor])
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
if (rows[cursor].resumable) select(rows[cursor])
|
|
403
|
+
}
|
|
383
404
|
}, { isActive: transcript === undefined })
|
|
384
405
|
if (transcript !== undefined) {
|
|
385
406
|
return createElement(DocumentPanel, {
|
|
@@ -392,15 +413,17 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
392
413
|
const pendingRow = deleteConfirmId === undefined ? undefined : rows.find(row => row.id === deleteConfirmId)
|
|
393
414
|
const toolbar = `[${focus === 0 ? '>' : ''}${options.sessions}] [${focus === 1 ? '>' : ''}${options.cwd} cwd] [${focus === 2 ? '>' : ''}${options.sort}] [${focus === 3 ? '>' : ''}${density}]`
|
|
394
415
|
return createElement(ListFrame, {
|
|
395
|
-
title: deleteConfirmId
|
|
396
|
-
?
|
|
397
|
-
:
|
|
416
|
+
title: deleteConfirmId !== undefined
|
|
417
|
+
? t('panel.resume.deleteTitle', { target: pendingRow === undefined ? deleteConfirmId.slice(-12) : pendingRow.title ?? pendingRow.id })
|
|
418
|
+
: deleteMode
|
|
419
|
+
? t('panel.delete.title', { search: searching ? ` — ${t('panel.searching')}` : '', toolbar })
|
|
420
|
+
: t('panel.resume.title', { mode: '', search: searching ? ` — ${t('panel.searching')}` : '', toolbar }),
|
|
398
421
|
rows: rows.map(row => ({
|
|
399
422
|
key: row.id,
|
|
400
|
-
disabled: !row.resumable,
|
|
423
|
+
disabled: deleteMode ? row.live : !row.resumable,
|
|
401
424
|
text: `${row.subagent ? '↳' : '○'} ${row.title ?? row.id.slice(-12)}${density === 'comfortable' ? ` · ${formatRelativeTime(row.updatedAt ?? row.createdAt, now)} · ${row.workspace} · ${row.preset}` : ''}${row.live ? ' · live' : ''}${expanded === row.id ? ` · ${row.id} · ${row.cwd}${row.parent === undefined ? '' : ` · parent ${row.parent}`}` : ''}`,
|
|
402
425
|
})), cursor, loading, error, query: options.query, searching,
|
|
403
|
-
footer: t('panel.footer.resume'),
|
|
426
|
+
footer: t(deleteMode ? 'panel.footer.delete' : 'panel.footer.resume'),
|
|
404
427
|
})
|
|
405
428
|
}
|
|
406
429
|
|
|
@@ -441,24 +464,50 @@ function DocumentRows({ lines }: { lines: readonly StyledLine[] }): ReactElement
|
|
|
441
464
|
)
|
|
442
465
|
}
|
|
443
466
|
|
|
444
|
-
function DocumentPanel({ title, text, error, close }: {
|
|
467
|
+
function DocumentPanel({ title, text, error, close, onRefresh, onExpand }: {
|
|
445
468
|
title: string
|
|
446
469
|
text?: string
|
|
447
470
|
error?: string
|
|
448
471
|
close: () => void
|
|
472
|
+
/** Re-read the document; offered as `r` (running subagents keep moving). */
|
|
473
|
+
onRefresh?: () => void
|
|
474
|
+
/** Replace this preview with a fuller surface (subagent attachment). */
|
|
475
|
+
onExpand?: () => void
|
|
449
476
|
}): ReactElement {
|
|
450
477
|
const stdout = useStdout().stdout
|
|
451
|
-
|
|
478
|
+
// Expanded mode: the document replaces the transcript area (near the whole
|
|
479
|
+
// terminal minus the bottom live region), entered from the preview with
|
|
480
|
+
// Enter/f. Esc leaves for the caller's list directly — the preview is a
|
|
481
|
+
// peek step, not a level to back through.
|
|
482
|
+
const [expanded, setExpanded] = useState(false)
|
|
483
|
+
const viewport = expanded
|
|
484
|
+
? expandedDocumentViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
485
|
+
: panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
452
486
|
const [scroll, setScroll] = useState(0)
|
|
453
487
|
const lines = useMemo(() => text === undefined ? [] : markdownLines(text, viewport.contentColumns), [text, viewport.contentColumns])
|
|
488
|
+
const maxScroll = Math.max(0, lines.length - viewport.bodyRows)
|
|
454
489
|
useInput((input, key) => {
|
|
455
490
|
if (key.escape || input === 'q' || input === 't') return close()
|
|
491
|
+
// Ctrl+D keeps its step-out meaning: empty-draft exit in the composer,
|
|
492
|
+
// leave-the-document here.
|
|
493
|
+
if (expanded && key.ctrl && input === 'd') return close()
|
|
494
|
+
if (!expanded && (key.return || input === 'f')) {
|
|
495
|
+
if (onExpand !== undefined) {
|
|
496
|
+
close()
|
|
497
|
+
onExpand()
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
setExpanded(true)
|
|
501
|
+
setScroll(value => Math.min(value, Math.max(0, lines.length - expandedDocumentViewport(stdout?.columns ?? 80, stdout?.rows ?? 30).bodyRows)))
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
if (expanded && input === 'r' && onRefresh !== undefined) return onRefresh()
|
|
456
505
|
if (key.upArrow) return setScroll(value => Math.max(0, value - 1))
|
|
457
|
-
if (key.downArrow) return setScroll(value => Math.min(
|
|
506
|
+
if (key.downArrow) return setScroll(value => Math.min(maxScroll, value + 1))
|
|
458
507
|
if (key.pageUp) return setScroll(value => Math.max(0, value - Math.max(1, viewport.bodyRows - 1)))
|
|
459
|
-
if (key.pageDown) return setScroll(value => Math.min(
|
|
508
|
+
if (key.pageDown) return setScroll(value => Math.min(maxScroll, value + Math.max(1, viewport.bodyRows - 1)))
|
|
460
509
|
if (input === 'g') return setScroll(0)
|
|
461
|
-
if (input === 'G') return setScroll(
|
|
510
|
+
if (input === 'G') return setScroll(maxScroll)
|
|
462
511
|
})
|
|
463
512
|
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('document.compact'), viewport.contentColumns))
|
|
464
513
|
const body: readonly StyledLine[] = error !== undefined
|
|
@@ -469,10 +518,12 @@ function DocumentPanel({ title, text, error, close }: {
|
|
|
469
518
|
const accent = panelAccent('kernel-transcript', getPalette().dim, getPalette().brandBright)
|
|
470
519
|
return createElement(
|
|
471
520
|
Box,
|
|
472
|
-
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
473
|
-
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(title), viewport.contentColumns)),
|
|
521
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: expanded ? inkColor(accent.title) : inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
522
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(title) + (expanded ? ` · ${t('document.expanded')}` : ''), viewport.contentColumns)),
|
|
474
523
|
createElement(DocumentRows, { lines: body }),
|
|
475
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(
|
|
524
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(expanded
|
|
525
|
+
? t('document.footerExpanded', { from: lines.length === 0 ? 0 : scroll + 1, to: Math.min(lines.length, scroll + viewport.bodyRows), total: lines.length, refresh: onRefresh === undefined ? '' : ` · ${t('document.refreshHint')}` })
|
|
526
|
+
: t('document.footer', { from: lines.length === 0 ? 0 : scroll + 1, to: Math.min(lines.length, scroll + viewport.bodyRows), total: lines.length }), viewport.contentColumns)),
|
|
476
527
|
)
|
|
477
528
|
}
|
|
478
529
|
|
|
@@ -580,6 +631,7 @@ export function ReviewPickerPanel({ loadBranches, loadCommits, choose, close }:
|
|
|
580
631
|
const [loading, setLoading] = useState(false)
|
|
581
632
|
const [error, setError] = useState<string>()
|
|
582
633
|
const loadRef = useRef<AbortController>()
|
|
634
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally re-pin when review rows change
|
|
583
635
|
const now = useMemo(() => Date.now(), [rows])
|
|
584
636
|
|
|
585
637
|
useEffect(() => {
|
|
@@ -715,24 +767,6 @@ export function ReviewPickerPanel({ loadBranches, loadCommits, choose, close }:
|
|
|
715
767
|
)
|
|
716
768
|
}
|
|
717
769
|
|
|
718
|
-
/** One cross-session full-text search hit mapped from the session-query engine. */
|
|
719
|
-
export interface SearchRow {
|
|
720
|
-
/** Session id (Enter resumes it through the switch machinery). */
|
|
721
|
-
readonly id: string
|
|
722
|
-
/** Display label: session title or the short id form. */
|
|
723
|
-
readonly label: string
|
|
724
|
-
/** Secondary facts line (workspace · preset markers). */
|
|
725
|
-
readonly detail: string
|
|
726
|
-
/** Bounded plain-text excerpt around the strongest match. */
|
|
727
|
-
readonly snippet: string
|
|
728
|
-
/** Match timestamp (relative labels derive from it). */
|
|
729
|
-
readonly updatedAt: number
|
|
730
|
-
/** Whether the hit is a delegated subagent conversation (not resumable). */
|
|
731
|
-
readonly subagent: boolean
|
|
732
|
-
/** Whether Enter may switch into it. */
|
|
733
|
-
readonly resumable: boolean
|
|
734
|
-
}
|
|
735
|
-
|
|
736
770
|
/**
|
|
737
771
|
* The /search panel: full-text search over every persisted session through
|
|
738
772
|
* the in-process session-query engine (the same corpus the model's
|
|
@@ -815,6 +849,7 @@ export function SearchPanel({ load, select, initialQuery = '', close }: {
|
|
|
815
849
|
})
|
|
816
850
|
const stdout = useStdout().stdout
|
|
817
851
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
852
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally re-pin when a search result set lands
|
|
818
853
|
const now = useMemo(() => Date.now(), [rows, searched])
|
|
819
854
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
820
855
|
const state = loading ? 'searching…' : error !== undefined ? `error: ${error}` : rows.length === 0 ? 'no results yet' : `❯ ${rows[cursor]?.label ?? ''}`
|
|
@@ -929,7 +964,7 @@ export function StatuslinePanel({ enabled, change, close }: {
|
|
|
929
964
|
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
930
965
|
const offset = revealRow(0, cursor, order.length, bodyRows)
|
|
931
966
|
const visible = order.slice(offset, offset + bodyRows)
|
|
932
|
-
const meta = new Map(
|
|
967
|
+
const meta = new Map(localizedStatusItems().map(item => [item.id, item]))
|
|
933
968
|
const accent = panelAccent('statusline', getPalette().dim, getPalette().brandBright)
|
|
934
969
|
return createElement(
|
|
935
970
|
Box,
|
|
@@ -1059,13 +1094,15 @@ interface AgentsEntry {
|
|
|
1059
1094
|
* Enter/t opening the child's full transcript in the shared read-only
|
|
1060
1095
|
* document view (the same projection the exporter uses).
|
|
1061
1096
|
*/
|
|
1062
|
-
export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
1097
|
+
export function AgentsPanel({ live, load, readTranscript, attach, close }: {
|
|
1063
1098
|
/** Live feed rows (child sessions observed this process). */
|
|
1064
1099
|
live: readonly SubagentRow[]
|
|
1065
1100
|
/** Load this session's persisted child sessions by lineage. */
|
|
1066
1101
|
load: () => Promise<readonly SessionRow[]>
|
|
1067
1102
|
/** Read one child session's full transcript as markdown. */
|
|
1068
1103
|
readTranscript: (id: string, signal?: AbortSignal) => Promise<string>
|
|
1104
|
+
/** Attach to one child as the whole view (the runner's live buses). */
|
|
1105
|
+
attach: ((id: string, label: string) => void) | undefined
|
|
1069
1106
|
close: () => void
|
|
1070
1107
|
}): ReactElement {
|
|
1071
1108
|
const [dirRows, setDirRows] = useState<readonly SessionRow[] | undefined>(undefined)
|
|
@@ -1075,7 +1112,7 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
1075
1112
|
const [transcript, setTranscript] = useState<{ id: string; text?: string; error?: string }>()
|
|
1076
1113
|
const transcriptLoad = useRef<AbortController>()
|
|
1077
1114
|
useEffect(() => () => transcriptLoad.current?.abort(), [])
|
|
1078
|
-
const refresh = (): void => {
|
|
1115
|
+
const refresh = useCallback((): void => {
|
|
1079
1116
|
setLoading(true)
|
|
1080
1117
|
setError(undefined)
|
|
1081
1118
|
Promise.resolve().then(load).then(value => {
|
|
@@ -1085,8 +1122,8 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
1085
1122
|
setError(reason instanceof Error ? reason.message : String(reason))
|
|
1086
1123
|
setLoading(false)
|
|
1087
1124
|
})
|
|
1088
|
-
}
|
|
1089
|
-
useEffect(refresh, [])
|
|
1125
|
+
}, [load])
|
|
1126
|
+
useEffect(refresh, [refresh])
|
|
1090
1127
|
// Live feed rows first (they carry the running state), then persisted
|
|
1091
1128
|
// children only the directory knows — settled subagents from earlier turns.
|
|
1092
1129
|
const rows = useMemo<readonly AgentsEntry[]>(() => {
|
|
@@ -1112,22 +1149,26 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
1112
1149
|
return [...feedRows, ...persisted]
|
|
1113
1150
|
}, [live, dirRows])
|
|
1114
1151
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (row === undefined) return
|
|
1152
|
+
/** (Re-)read one child's transcript; the id is captured, not re-derived. */
|
|
1153
|
+
const readTranscriptOf = (id: string): void => {
|
|
1118
1154
|
transcriptLoad.current?.abort()
|
|
1119
|
-
setTranscript({ id
|
|
1155
|
+
setTranscript({ id })
|
|
1120
1156
|
const controller = new AbortController()
|
|
1121
1157
|
transcriptLoad.current = controller
|
|
1122
|
-
Promise.resolve().then(() => readTranscript(
|
|
1158
|
+
Promise.resolve().then(() => readTranscript(id, controller.signal)).then(
|
|
1123
1159
|
text => {
|
|
1124
|
-
if (!controller.signal.aborted) setTranscript({ id
|
|
1160
|
+
if (!controller.signal.aborted) setTranscript({ id, text })
|
|
1125
1161
|
},
|
|
1126
1162
|
reason => {
|
|
1127
|
-
if (!controller.signal.aborted) setTranscript({ id
|
|
1163
|
+
if (!controller.signal.aborted) setTranscript({ id, error: reason instanceof Error ? reason.message : String(reason) })
|
|
1128
1164
|
},
|
|
1129
1165
|
)
|
|
1130
1166
|
}
|
|
1167
|
+
const openTranscript = (): void => {
|
|
1168
|
+
const row = rows[cursor]
|
|
1169
|
+
if (row === undefined) return
|
|
1170
|
+
readTranscriptOf(row.id)
|
|
1171
|
+
}
|
|
1131
1172
|
useInput((input, key) => {
|
|
1132
1173
|
if (key.escape || input === 'q') return close()
|
|
1133
1174
|
if (input === 'r') return refresh()
|
|
@@ -1144,6 +1185,17 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
1144
1185
|
transcriptLoad.current?.abort()
|
|
1145
1186
|
setTranscript(undefined)
|
|
1146
1187
|
},
|
|
1188
|
+
// Running subagents keep moving: `r` re-reads the same child.
|
|
1189
|
+
onRefresh: () => readTranscriptOf(transcript.id),
|
|
1190
|
+
// Enter/f in the preview attaches the child as the whole view when the
|
|
1191
|
+
// runner wired the live buses; without them the bordered expansion
|
|
1192
|
+
// keeps serving the same document.
|
|
1193
|
+
...(attach === undefined ? {} : {
|
|
1194
|
+
onExpand: () => {
|
|
1195
|
+
const label = rows.find(row => row.id === transcript.id)?.label ?? transcript.id.slice(-12)
|
|
1196
|
+
attach(transcript.id, label)
|
|
1197
|
+
},
|
|
1198
|
+
}),
|
|
1147
1199
|
})
|
|
1148
1200
|
}
|
|
1149
1201
|
return createElement(ListFrame, {
|
|
@@ -1187,7 +1239,7 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
|
|
|
1187
1239
|
const [loading, setLoading] = useState(true)
|
|
1188
1240
|
const [cursor, setCursor] = useState(0)
|
|
1189
1241
|
const [effortFor, setEffortFor] = useState<ModelRow | undefined>(undefined)
|
|
1190
|
-
const refresh = (): void => {
|
|
1242
|
+
const refresh = useCallback((): void => {
|
|
1191
1243
|
setLoading(true)
|
|
1192
1244
|
setError(undefined)
|
|
1193
1245
|
Promise.resolve().then(load).then(value => {
|
|
@@ -1197,8 +1249,8 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
|
|
|
1197
1249
|
setError(reason instanceof Error ? reason.message : String(reason))
|
|
1198
1250
|
setLoading(false)
|
|
1199
1251
|
})
|
|
1200
|
-
}
|
|
1201
|
-
useEffect(refresh, [])
|
|
1252
|
+
}, [load])
|
|
1253
|
+
useEffect(refresh, [refresh])
|
|
1202
1254
|
const rows = useMemo(() => directory?.rows ?? [], [directory])
|
|
1203
1255
|
// The list opens on the override's own row (index 0 is the inherit row).
|
|
1204
1256
|
useEffect(() => {
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { createElement, useState, type ReactElement } from 'react'
|
|
8
8
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
9
|
-
import { panelViewport } from '
|
|
10
|
-
import { truncateColumns } from '
|
|
11
|
-
import { LANGUAGES, t, type LanguageName } from '
|
|
12
|
-
import { getPalette, inkColor } from '
|
|
9
|
+
import { panelViewport } from '../render/inspector.ts'
|
|
10
|
+
import { truncateColumns } from '../render/text.ts'
|
|
11
|
+
import { LANGUAGES, t, type LanguageName } from '../i18n.ts'
|
|
12
|
+
import { getPalette, inkColor } from '../theme.ts'
|
|
13
13
|
|
|
14
14
|
export function LanguagePanel({ current, select, close }: {
|
|
15
15
|
current: LanguageName
|