dsh-code 1.0.6 → 1.2.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 +123 -26
- package/README.md +124 -27
- package/bin/deepseek.mjs +283 -35
- package/cordis.patch.yml +97 -0
- package/lib/index.mjs +5008 -881
- package/lib/session-query.mjs +150 -0
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +106 -62
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +100 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +107 -29
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +450 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +130 -4
- package/lib/types/render/status.d.ts +9 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +17 -0
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +66 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +75 -0
- package/lib/types/version.d.ts +4 -3
- package/package.json +246 -90
- package/src/app.ts +1369 -509
- package/src/approval.ts +166 -166
- package/src/authorization-panel.ts +19 -16
- package/src/editor-keys.ts +371 -371
- package/src/git-workflow.ts +229 -3
- package/src/i18n.ts +68 -0
- package/src/index.ts +534 -80
- package/src/input-split.ts +3 -3
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +554 -86
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +489 -0
- package/src/locales/zh.ts +488 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +208 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +25 -24
- package/src/render/export.ts +116 -95
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +572 -21
- package/src/render/status.ts +59 -39
- package/src/render/text.ts +14 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +8 -6
- package/src/session-query.ts +239 -0
- package/src/startup.ts +3 -3
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +190 -0
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +256 -0
- package/src/update.ts +126 -0
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
package/src/kernel-panels.ts
CHANGED
|
@@ -4,17 +4,22 @@ import { createElement, useEffect, useMemo, useRef, useState, type ReactElement
|
|
|
4
4
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
5
5
|
import type { ModelDirectory, ModelRow } from './models.ts'
|
|
6
6
|
import type { SubagentRow } from './subagents.ts'
|
|
7
|
+
import type { ScheduleRow } from './render/projection.ts'
|
|
7
8
|
import type { PermissionRow } from './permissions.ts'
|
|
8
9
|
import type { PresetRow } from './presets.ts'
|
|
9
10
|
import type { PluginRow } from './plugin-inventory.ts'
|
|
10
11
|
import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
|
|
11
12
|
import { formatRelativeTime } from './session-directory.ts'
|
|
13
|
+
import type { ReviewBranch, ReviewCommit, ReviewSelection } from './git-workflow.ts'
|
|
12
14
|
import { panelViewport, revealRow } from './render/inspector.ts'
|
|
13
15
|
import { markdownLines, textLines, type LineStyle, type StyledLine } from './render/lines.ts'
|
|
16
|
+
import { usageLines, type UsageView } from './render/usage.ts'
|
|
14
17
|
import { deleteLastGrapheme } from './render/editor.ts'
|
|
15
18
|
import { stripPasteMarkers } from './keyboard.ts'
|
|
16
19
|
import { DEFAULT_STATUSLINE_ITEMS, STATUS_ITEMS, type StatusItemId } from './render/status.ts'
|
|
17
20
|
import { singleLineText, truncateColumns } from './render/text.ts'
|
|
21
|
+
import { panelAccent } from './panel-accent.ts'
|
|
22
|
+
import { t } from './i18n.ts'
|
|
18
23
|
import { getPalette, inkColor } from './theme.ts'
|
|
19
24
|
|
|
20
25
|
interface ListFrameProps {
|
|
@@ -39,9 +44,9 @@ function isSearchToggle(input: string, key: { ctrl?: boolean }): boolean {
|
|
|
39
44
|
/** The search-state line: gated panels show only an ACTIVE filter (the ctrl+f
|
|
40
45
|
* toggle lives in the footer), direct-typing panels keep the plain prompt. */
|
|
41
46
|
function searchLine(searching: boolean | undefined, query: string): string {
|
|
42
|
-
if (searching === true) return
|
|
43
|
-
if (searching === false) return query === '' ? '' :
|
|
44
|
-
return
|
|
47
|
+
if (searching === true) return query === '' ? t('panel.searchIdleStop') : `${t('panel.searchPrefix')}${query}`
|
|
48
|
+
if (searching === false) return query === '' ? '' : `${t('panel.searchPrefix')}${query}`
|
|
49
|
+
return query === '' ? t('panel.searchIdle') : `${t('panel.searchPrefix')}${query}`
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function ListFrame(props: ListFrameProps): ReactElement {
|
|
@@ -71,10 +76,11 @@ function ListFrame(props: ListFrameProps): ReactElement {
|
|
|
71
76
|
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
72
77
|
const offset = revealRow(0, props.cursor, stateRows.length, bodyRows)
|
|
73
78
|
const visible = stateRows.slice(offset, offset + bodyRows)
|
|
79
|
+
const accent = panelAccent('kernel-list', getPalette().dim, getPalette().brandBright)
|
|
74
80
|
return createElement(
|
|
75
81
|
Box,
|
|
76
|
-
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(
|
|
77
|
-
createElement(Text, { color: inkColor(
|
|
82
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
83
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(props.title), viewport.contentColumns)),
|
|
78
84
|
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(searchLine(props.searching, props.query)), viewport.contentColumns)),
|
|
79
85
|
...visible.map((row, index) => {
|
|
80
86
|
const absolute = offset + index
|
|
@@ -104,9 +110,9 @@ export function editQuery(query: string, input: string, key: { backspace?: boole
|
|
|
104
110
|
|
|
105
111
|
export function ModePanel({ current, load, select, close }: {
|
|
106
112
|
current: string
|
|
107
|
-
load()
|
|
108
|
-
select(id: string)
|
|
109
|
-
close()
|
|
113
|
+
load: () => Promise<readonly PresetRow[]>
|
|
114
|
+
select: (id: string) => void
|
|
115
|
+
close: () => void
|
|
110
116
|
}): ReactElement {
|
|
111
117
|
const [rows, setRows] = useState<readonly PresetRow[]>([])
|
|
112
118
|
const [query, setQuery] = useState('')
|
|
@@ -123,29 +129,31 @@ export function ModePanel({ current, load, select, close }: {
|
|
|
123
129
|
const visible = useMemo(() => rows.filter(row => `${row.id} ${row.name ?? ''} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
|
|
124
130
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
|
|
125
131
|
useInput((input, key) => {
|
|
126
|
-
if (key.escape
|
|
132
|
+
if (key.escape) return close()
|
|
133
|
+
// q closes only while the query is empty; mid-filter it is query text.
|
|
134
|
+
if (input === 'q' && query === '') return close()
|
|
127
135
|
if (input === 'r' && query === '') return refresh()
|
|
128
136
|
if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
|
|
129
137
|
if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
|
|
130
138
|
// Empty/loading/filtered-out lists have no row at the cursor: a bare
|
|
131
139
|
// `?.broken === undefined` check passes on undefined and crashes the
|
|
132
140
|
// process on the `!.id` access (PermissionPanel guards this correctly).
|
|
133
|
-
if (key.return && visible[cursor] !== undefined && visible[cursor]
|
|
141
|
+
if (key.return && visible[cursor] !== undefined && visible[cursor].broken === undefined) return select(visible[cursor].id)
|
|
134
142
|
const next = editQuery(query, input, key)
|
|
135
143
|
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
136
144
|
})
|
|
137
145
|
return createElement(ListFrame, {
|
|
138
|
-
title:
|
|
146
|
+
title: t('panel.mode.title', { current }),
|
|
139
147
|
rows: visible.map(row => ({ key: row.id, disabled: row.broken !== undefined, text: `${row.id === current ? '●' : '○'} ${row.name ?? row.id} · ${row.description ?? row.trust}${row.broken === undefined ? '' : ` · broken: ${row.broken}`}` })),
|
|
140
|
-
cursor, loading, error, query, footer: '
|
|
148
|
+
cursor, loading, error, query, footer: t('panel.footer.chooseSwitch'),
|
|
141
149
|
})
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
export function PermissionPanel({ current, load, select, close }: {
|
|
145
153
|
current: string
|
|
146
|
-
load()
|
|
147
|
-
select(id: string)
|
|
148
|
-
close()
|
|
154
|
+
load: () => Promise<readonly PermissionRow[]>
|
|
155
|
+
select: (id: string) => void
|
|
156
|
+
close: () => void
|
|
149
157
|
}): ReactElement {
|
|
150
158
|
const [rows, setRows] = useState<readonly PermissionRow[]>([])
|
|
151
159
|
const [query, setQuery] = useState('')
|
|
@@ -162,22 +170,24 @@ export function PermissionPanel({ current, load, select, close }: {
|
|
|
162
170
|
const visible = useMemo(() => rows.filter(row => `${row.id} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
|
|
163
171
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
|
|
164
172
|
useInput((input, key) => {
|
|
165
|
-
if (key.escape
|
|
173
|
+
if (key.escape) return close()
|
|
174
|
+
// q closes only while the query is empty; mid-filter it is query text.
|
|
175
|
+
if (input === 'q' && query === '') return close()
|
|
166
176
|
if (input === 'r' && query === '') return refresh()
|
|
167
177
|
if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
|
|
168
178
|
if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
|
|
169
|
-
if (key.return && visible[cursor] !== undefined) return select(visible[cursor]
|
|
179
|
+
if (key.return && visible[cursor] !== undefined) return select(visible[cursor].id)
|
|
170
180
|
const next = editQuery(query, input, key)
|
|
171
181
|
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
172
182
|
})
|
|
173
183
|
return createElement(ListFrame, {
|
|
174
|
-
title:
|
|
184
|
+
title: t('panel.permission.title', { current }),
|
|
175
185
|
rows: visible.map(row => ({ key: row.id, text: `${row.id === current ? '●' : '○'} ${row.id}${row.description === undefined ? '' : ` · ${row.description}`}` })),
|
|
176
|
-
cursor, loading, error, query, footer: '
|
|
186
|
+
cursor, loading, error, query, footer: t('panel.footer.chooseSelect'),
|
|
177
187
|
})
|
|
178
188
|
}
|
|
179
189
|
|
|
180
|
-
export function PluginPanel({ load, close, initialQuery = '' }: { load()
|
|
190
|
+
export function PluginPanel({ load, close, initialQuery = '' }: { load: () => readonly PluginRow[]; close: () => void; initialQuery?: string }): ReactElement {
|
|
181
191
|
const [epoch, setEpoch] = useState(0)
|
|
182
192
|
const [query, setQuery] = useState(initialQuery)
|
|
183
193
|
const [cursor, setCursor] = useState(0)
|
|
@@ -185,7 +195,9 @@ export function PluginPanel({ load, close, initialQuery = '' }: { load(): readon
|
|
|
185
195
|
const rows = useMemo(() => load().filter(row => `${row.entryId} ${row.moduleName} ${row.phase ?? ''}`.toLowerCase().includes(query.toLowerCase())), [epoch, query])
|
|
186
196
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
|
|
187
197
|
useInput((input, key) => {
|
|
188
|
-
if (key.escape
|
|
198
|
+
if (key.escape) return close()
|
|
199
|
+
// q closes only while the query is empty; mid-filter it is query text.
|
|
200
|
+
if (input === 'q' && query === '') return close()
|
|
189
201
|
if (input === 'r' && query === '') return setEpoch(value => value + 1)
|
|
190
202
|
if (key.upArrow) return setCursor(value => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length)
|
|
191
203
|
if (key.downArrow) return setCursor(value => rows.length === 0 ? 0 : (value + 1) % rows.length)
|
|
@@ -194,12 +206,12 @@ export function PluginPanel({ load, close, initialQuery = '' }: { load(): readon
|
|
|
194
206
|
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
195
207
|
})
|
|
196
208
|
return createElement(ListFrame, {
|
|
197
|
-
title: '
|
|
209
|
+
title: t('panel.plugin.title'),
|
|
198
210
|
rows: rows.map((row, index) => ({
|
|
199
211
|
key: row.entryId,
|
|
200
212
|
disabled: !row.enabled,
|
|
201
213
|
text: `${row.enabled ? '●' : '○'} ${row.entryId} · ${row.phase ?? 'not mounted'}${expanded && index === cursor ? ` · ${row.moduleName}` : ''}`,
|
|
202
|
-
})), cursor, loading: false, query, footer: '
|
|
214
|
+
})), cursor, loading: false, query, footer: t('panel.footer.inspectDetails'),
|
|
203
215
|
})
|
|
204
216
|
}
|
|
205
217
|
|
|
@@ -245,7 +257,7 @@ const JOB_MARK: Record<JobRow['status'], string> = {
|
|
|
245
257
|
* interval dies with the panel). Cancel stays upstream-only; an absent
|
|
246
258
|
* registry renders as the plain empty state (a harmless missing service).
|
|
247
259
|
*/
|
|
248
|
-
export function JobsPanel({ load, close }: { load()
|
|
260
|
+
export function JobsPanel({ load, close }: { load: () => readonly JobRow[]; close: () => void }): ReactElement {
|
|
249
261
|
const [, setRefresh] = useState(0)
|
|
250
262
|
const [cursor, setCursor] = useState(0)
|
|
251
263
|
const [, setTick] = useState(0)
|
|
@@ -262,29 +274,29 @@ export function JobsPanel({ load, close }: { load(): readonly JobRow[]; close():
|
|
|
262
274
|
})
|
|
263
275
|
useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
|
|
264
276
|
return createElement(ListFrame, {
|
|
265
|
-
title:
|
|
277
|
+
title: t('panel.jobs.title', { count: rows.length }),
|
|
266
278
|
rows: rows.map(row => ({
|
|
267
279
|
key: row.id,
|
|
268
280
|
text: `${JOB_MARK[row.status]} ${row.id} · ${singleLineText(row.label)} · ${runClock((row.finishedAt ?? Date.now()) - row.startedAt)}${row.detail === undefined ? '' : ` · ${singleLineText(row.detail)}`}`,
|
|
269
281
|
})),
|
|
270
|
-
cursor, loading: false, query: '', searching: false, footer: '
|
|
282
|
+
cursor, loading: false, query: '', searching: false, footer: t('panel.footer.inspectRefresh'),
|
|
271
283
|
})
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
export function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }: {
|
|
275
287
|
currentCwd: string
|
|
276
|
-
load(options: SessionDirectoryOptions, signal?: AbortSignal)
|
|
277
|
-
readTranscript(id: string, signal?: AbortSignal)
|
|
278
|
-
select(row: SessionRow)
|
|
288
|
+
load: (options: SessionDirectoryOptions, signal?: AbortSignal) => Promise<readonly SessionRow[]>
|
|
289
|
+
readTranscript: (id: string, signal?: AbortSignal) => Promise<string>
|
|
290
|
+
select: (row: SessionRow) => void
|
|
279
291
|
/** Arm the composer-based delete confirm for one row (App owns the keys). */
|
|
280
|
-
requestDelete
|
|
292
|
+
requestDelete?: (row: SessionRow) => void
|
|
281
293
|
/** The row id awaiting y/n in the composer, when any (App-owned). */
|
|
282
294
|
deleteConfirmId?: string
|
|
283
295
|
/** Bump to reload the listing (e.g. after a deletion). */
|
|
284
296
|
reloadToken?: number
|
|
285
297
|
/** Opened via /delete: hint-first delete mode. */
|
|
286
298
|
deleteMode?: boolean
|
|
287
|
-
close()
|
|
299
|
+
close: () => void
|
|
288
300
|
}): ReactElement {
|
|
289
301
|
// Codex resume-picker default: the CURRENT directory's root sessions; the
|
|
290
302
|
// cwd filter widens to all only on request (the old default leaked every
|
|
@@ -351,12 +363,12 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
351
363
|
if (key.pageDown) return setCursor(value => Math.min(rows.length - 1, value + 8))
|
|
352
364
|
if (input === 'g') return setCursor(0)
|
|
353
365
|
if (input === 'G') return setCursor(Math.max(0, rows.length - 1))
|
|
354
|
-
if (input === 'd' && rows[cursor] !== undefined && requestDelete !== undefined) return requestDelete(rows[cursor]
|
|
366
|
+
if (input === 'd' && rows[cursor] !== undefined && requestDelete !== undefined) return requestDelete(rows[cursor])
|
|
355
367
|
if (input === 'e' && rows[cursor] !== undefined) {
|
|
356
|
-
return setExpanded(value => value === rows[cursor]
|
|
368
|
+
return setExpanded(value => value === rows[cursor].id ? undefined : rows[cursor].id)
|
|
357
369
|
}
|
|
358
370
|
if (input === 't' && rows[cursor] !== undefined) {
|
|
359
|
-
const row = rows[cursor]
|
|
371
|
+
const row = rows[cursor]
|
|
360
372
|
transcriptLoad.current?.abort()
|
|
361
373
|
setTranscript({ id: row.id })
|
|
362
374
|
const controller = new AbortController()
|
|
@@ -367,11 +379,11 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
367
379
|
)
|
|
368
380
|
return
|
|
369
381
|
}
|
|
370
|
-
if (key.return && rows[cursor]?.resumable === true) return select(rows[cursor]
|
|
382
|
+
if (key.return && rows[cursor]?.resumable === true) return select(rows[cursor])
|
|
371
383
|
}, { isActive: transcript === undefined })
|
|
372
384
|
if (transcript !== undefined) {
|
|
373
385
|
return createElement(DocumentPanel, {
|
|
374
|
-
title:
|
|
386
|
+
title: t('panel.document.title', { id: transcript.id }),
|
|
375
387
|
text: transcript.text,
|
|
376
388
|
error: transcript.error,
|
|
377
389
|
close: () => { transcriptLoad.current?.abort(); setTranscript(undefined) },
|
|
@@ -388,7 +400,7 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
|
|
|
388
400
|
disabled: !row.resumable,
|
|
389
401
|
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}`}` : ''}`,
|
|
390
402
|
})), cursor, loading, error, query: options.query, searching,
|
|
391
|
-
footer: '
|
|
403
|
+
footer: t('panel.footer.resume'),
|
|
392
404
|
})
|
|
393
405
|
}
|
|
394
406
|
|
|
@@ -433,7 +445,7 @@ function DocumentPanel({ title, text, error, close }: {
|
|
|
433
445
|
title: string
|
|
434
446
|
text?: string
|
|
435
447
|
error?: string
|
|
436
|
-
close()
|
|
448
|
+
close: () => void
|
|
437
449
|
}): ReactElement {
|
|
438
450
|
const stdout = useStdout().stdout
|
|
439
451
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -448,18 +460,19 @@ function DocumentPanel({ title, text, error, close }: {
|
|
|
448
460
|
if (input === 'g') return setScroll(0)
|
|
449
461
|
if (input === 'G') return setScroll(Math.max(0, lines.length - viewport.bodyRows))
|
|
450
462
|
})
|
|
451
|
-
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
463
|
+
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('document.compact'), viewport.contentColumns))
|
|
452
464
|
const body: readonly StyledLine[] = error !== undefined
|
|
453
465
|
? textLines(`error: ${singleLineText(error)}`, viewport.contentColumns, 'error')
|
|
454
466
|
: text === undefined
|
|
455
|
-
? textLines('loading
|
|
467
|
+
? textLines(t('document.loading'), viewport.contentColumns, 'dim')
|
|
456
468
|
: lines.slice(scroll, scroll + viewport.bodyRows)
|
|
469
|
+
const accent = panelAccent('kernel-transcript', getPalette().dim, getPalette().brandBright)
|
|
457
470
|
return createElement(
|
|
458
471
|
Box,
|
|
459
|
-
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(
|
|
460
|
-
createElement(Text, { color: inkColor(
|
|
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)),
|
|
461
474
|
createElement(DocumentRows, { lines: body }),
|
|
462
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(
|
|
475
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('document.footer', { from: lines.length === 0 ? 0 : scroll + 1, to: Math.min(lines.length, scroll + viewport.bodyRows), total: lines.length }), viewport.contentColumns)),
|
|
463
476
|
)
|
|
464
477
|
}
|
|
465
478
|
|
|
@@ -473,8 +486,8 @@ export function HistoryPanel({ entries, fill, close }: {
|
|
|
473
486
|
/** Newest-first recall entries (persistent + in-session, deduped). */
|
|
474
487
|
entries: readonly string[]
|
|
475
488
|
/** Accept one entry: its text plus its recall-space index (browsing resumes there). */
|
|
476
|
-
fill(text: string, index: number)
|
|
477
|
-
close()
|
|
489
|
+
fill: (text: string, index: number) => void
|
|
490
|
+
close: () => void
|
|
478
491
|
}): ReactElement {
|
|
479
492
|
const [query, setQuery] = useState('')
|
|
480
493
|
const [cursor, setCursor] = useState(0)
|
|
@@ -490,14 +503,18 @@ export function HistoryPanel({ entries, fill, close }: {
|
|
|
490
503
|
}
|
|
491
504
|
if (key.upArrow) return setCursor(value => Math.max(0, value - 1))
|
|
492
505
|
if (key.downArrow) return setCursor(value => Math.min(matches.length - 1, value + 1))
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (
|
|
506
|
+
// g/G stay vim-style jumps only on an empty query (the /mode contract):
|
|
507
|
+
// mid-filter they are query text, so filters like 'Fix' or 'grep' survive.
|
|
508
|
+
if (input === 'g' && query === '') return setCursor(0)
|
|
509
|
+
if (input === 'G' && query === '') return setCursor(matches.length - 1)
|
|
510
|
+
if (key.backspace || key.delete) {
|
|
496
511
|
setQuery(current => deleteLastGrapheme(current))
|
|
497
512
|
setCursor(0)
|
|
498
513
|
return
|
|
499
514
|
}
|
|
500
|
-
|
|
515
|
+
// Ink reports single uppercase letters and shifted symbols ('!', '@')
|
|
516
|
+
// with key.shift set; only ctrl/meta mark real command input.
|
|
517
|
+
if (input !== '' && !key.ctrl && !key.meta) {
|
|
501
518
|
const text = stripPasteMarkers(input)
|
|
502
519
|
if (text !== '') {
|
|
503
520
|
setQuery(current => (current + text).slice(0, 120))
|
|
@@ -509,21 +526,22 @@ export function HistoryPanel({ entries, fill, close }: {
|
|
|
509
526
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
510
527
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
511
528
|
const picked = matches[cursor]
|
|
512
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
529
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`/history · ${picked === undefined ? t('history.compact.none') : singleLineText(picked)} · ${t('panel.close')}`, viewport.contentColumns))
|
|
513
530
|
}
|
|
514
531
|
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
515
532
|
const offset = revealRow(0, cursor, matches.length, bodyRows)
|
|
516
533
|
const visible = matches.slice(offset, offset + bodyRows)
|
|
517
534
|
const header = query === ''
|
|
518
|
-
?
|
|
519
|
-
:
|
|
535
|
+
? t('history.title.prompts', { count: entries.length })
|
|
536
|
+
: t('history.title.match', { matches: matches.length, count: entries.length, query: truncateColumns(singleLineText(query), Math.max(6, viewport.contentColumns - 34)) })
|
|
537
|
+
const accent = panelAccent('history', getPalette().dim, getPalette().brandBright)
|
|
520
538
|
return createElement(
|
|
521
539
|
Box,
|
|
522
|
-
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(
|
|
523
|
-
createElement(Text, { color: inkColor(
|
|
524
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(
|
|
540
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
541
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(header, viewport.contentColumns)),
|
|
542
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('history.filterHint', { state: query === '' ? t('history.filterEmpty') : t('history.filterQuery', { query: singleLineText(query) }) }), viewport.contentColumns)),
|
|
525
543
|
...(visible.length === 0
|
|
526
|
-
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, truncateColumns('
|
|
544
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('history.empty'), viewport.contentColumns))]
|
|
527
545
|
: visible.map((entry, index) => {
|
|
528
546
|
const absolute = offset + index
|
|
529
547
|
const selected = absolute === cursor
|
|
@@ -537,7 +555,313 @@ export function HistoryPanel({ entries, fill, close }: {
|
|
|
537
555
|
truncateColumns((selected ? '› ' : ' ') + singleLineText(entry), viewport.contentColumns),
|
|
538
556
|
)
|
|
539
557
|
})),
|
|
540
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns('
|
|
558
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('history.footer'), viewport.contentColumns)),
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* The /review candidate picker (Codex's preset popup): bare /review opens a
|
|
564
|
+
* four-way preset — review uncommitted changes, pick a base branch, pick a
|
|
565
|
+
* recent commit, or type a custom focus. Branch and commit phases are
|
|
566
|
+
* type-to-filter lists; every selection resolves to the same /review
|
|
567
|
+
* argument string the direct command accepts.
|
|
568
|
+
*/
|
|
569
|
+
export function ReviewPickerPanel({ loadBranches, loadCommits, choose, close }: {
|
|
570
|
+
loadBranches: (signal?: AbortSignal) => Promise<readonly ReviewBranch[]>
|
|
571
|
+
loadCommits: (signal?: AbortSignal) => Promise<readonly ReviewCommit[]>
|
|
572
|
+
/** Run the review for one picker selection. */
|
|
573
|
+
choose: (selection: ReviewSelection) => void
|
|
574
|
+
close: () => void
|
|
575
|
+
}): ReactElement {
|
|
576
|
+
const [phase, setPhase] = useState<'preset' | 'branches' | 'commits' | 'custom'>('preset')
|
|
577
|
+
const [cursor, setCursor] = useState(0)
|
|
578
|
+
const [query, setQuery] = useState('')
|
|
579
|
+
const [rows, setRows] = useState<readonly (ReviewBranch | ReviewCommit)[]>([])
|
|
580
|
+
const [loading, setLoading] = useState(false)
|
|
581
|
+
const [error, setError] = useState<string>()
|
|
582
|
+
const loadRef = useRef<AbortController>()
|
|
583
|
+
const now = useMemo(() => Date.now(), [rows])
|
|
584
|
+
|
|
585
|
+
useEffect(() => {
|
|
586
|
+
if (phase !== 'branches' && phase !== 'commits') return undefined
|
|
587
|
+
loadRef.current?.abort()
|
|
588
|
+
const controller = new AbortController()
|
|
589
|
+
loadRef.current = controller
|
|
590
|
+
setLoading(true)
|
|
591
|
+
setError(undefined)
|
|
592
|
+
const load = phase === 'branches'
|
|
593
|
+
? (signal?: AbortSignal) => loadBranches(signal) as Promise<readonly (ReviewBranch | ReviewCommit)[]>
|
|
594
|
+
: (signal?: AbortSignal) => loadCommits(signal) as Promise<readonly (ReviewBranch | ReviewCommit)[]>
|
|
595
|
+
void Promise.resolve().then(() => load(controller.signal)).then(list => {
|
|
596
|
+
if (controller.signal.aborted) return
|
|
597
|
+
setLoading(false)
|
|
598
|
+
setRows(list)
|
|
599
|
+
setCursor(0)
|
|
600
|
+
}, reason => {
|
|
601
|
+
if (controller.signal.aborted) return
|
|
602
|
+
setLoading(false)
|
|
603
|
+
setRows([])
|
|
604
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
605
|
+
})
|
|
606
|
+
return () => controller.abort()
|
|
607
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
608
|
+
}, [phase])
|
|
609
|
+
|
|
610
|
+
const filtered = useMemo(() => {
|
|
611
|
+
if (phase !== 'branches' && phase !== 'commits') return []
|
|
612
|
+
const needle = query.toLowerCase()
|
|
613
|
+
if (needle === '') return rows
|
|
614
|
+
return rows.filter(row => {
|
|
615
|
+
const hay = 'name' in row ? row.name : `${row.sha} ${row.title}`
|
|
616
|
+
return hay.toLowerCase().includes(needle)
|
|
617
|
+
})
|
|
618
|
+
}, [phase, rows, query])
|
|
619
|
+
|
|
620
|
+
const submit = (selection: ReviewSelection): void => {
|
|
621
|
+
choose(selection)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
useInput((input, key) => {
|
|
625
|
+
if (key.escape || (input === 'q' && query === '' && phase !== 'custom')) {
|
|
626
|
+
if (phase !== 'preset') {
|
|
627
|
+
setPhase('preset')
|
|
628
|
+
setQuery('')
|
|
629
|
+
setCursor(0)
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
return close()
|
|
633
|
+
}
|
|
634
|
+
if (key.ctrl && input === 'c') return close()
|
|
635
|
+
if (phase === 'custom' || ((phase === 'branches' || phase === 'commits') && query !== '')) {
|
|
636
|
+
const next = editQuery(query, input, key)
|
|
637
|
+
if (next !== undefined) {
|
|
638
|
+
setQuery(next)
|
|
639
|
+
setCursor(0)
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// The row budget per phase: the preset list is fixed at four rows, the
|
|
644
|
+
// branch/commit lists clamp to their filtered length.
|
|
645
|
+
const rowCount = phase === 'preset' ? 4 : filtered.length
|
|
646
|
+
if (key.upArrow) return setCursor(value => Math.max(0, value - 1))
|
|
647
|
+
if (key.downArrow) return setCursor(value => Math.min(Math.max(0, rowCount - 1), value + 1))
|
|
648
|
+
if (key.return) {
|
|
649
|
+
if (phase === 'preset') {
|
|
650
|
+
if (cursor === 0) return submit({ kind: 'uncommitted' })
|
|
651
|
+
if (cursor === 1) return setPhase('branches')
|
|
652
|
+
if (cursor === 2) return setPhase('commits')
|
|
653
|
+
return setPhase('custom')
|
|
654
|
+
}
|
|
655
|
+
if (phase === 'branches' || phase === 'commits') {
|
|
656
|
+
const row = filtered[cursor]
|
|
657
|
+
if (row !== undefined) submit('name' in row ? { kind: 'base-branch', branch: row.name } : { kind: 'commit', sha: row.sha })
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
if (query.trim() !== '') submit({ kind: 'custom', instructions: query.trim() })
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
const stdout = useStdout().stdout
|
|
666
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
667
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
668
|
+
const state = phase === 'preset' ? 'pick a review target' : loading ? 'loading…' : error !== undefined ? `error: ${error}` : `${filtered.length} candidates`
|
|
669
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`/review · ${state} · ${t('panel.close')}`, viewport.contentColumns))
|
|
670
|
+
}
|
|
671
|
+
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
672
|
+
const offset = revealRow(0, cursor, phase === 'preset' ? 4 : filtered.length, bodyRows)
|
|
673
|
+
const header = phase === 'preset'
|
|
674
|
+
? t('review.picker.title')
|
|
675
|
+
: phase === 'branches'
|
|
676
|
+
? loading ? t('review.picker.loadingBranches') : error !== undefined ? t('search.compact.error', { message: truncateColumns(singleLineText(error), Math.max(6, viewport.contentColumns - 14)) }) : t('review.picker.branches', { filtered: filtered.length, total: rows.length })
|
|
677
|
+
: phase === 'commits'
|
|
678
|
+
? loading ? t('review.picker.loadingCommits') : error !== undefined ? t('search.compact.error', { message: truncateColumns(singleLineText(error), Math.max(6, viewport.contentColumns - 14)) }) : t('review.picker.commits', { filtered: filtered.length, total: rows.length })
|
|
679
|
+
: t('review.picker.customHint')
|
|
680
|
+
const accent = panelAccent('review-picker', getPalette().dim, getPalette().brandBright)
|
|
681
|
+
return createElement(
|
|
682
|
+
Box,
|
|
683
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
684
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(header, viewport.contentColumns)),
|
|
685
|
+
...(phase === 'preset'
|
|
686
|
+
? [
|
|
687
|
+
t('review.picker.uncommitted'),
|
|
688
|
+
t('review.picker.branch'),
|
|
689
|
+
t('review.picker.commit'),
|
|
690
|
+
t('review.picker.custom'),
|
|
691
|
+
].map((label, index) => {
|
|
692
|
+
const absolute = offset + index
|
|
693
|
+
const selected = absolute === cursor
|
|
694
|
+
return createElement(
|
|
695
|
+
Text,
|
|
696
|
+
{ key: `preset-${index}`, color: selected ? inkColor(getPalette().brandBright) : undefined, wrap: 'truncate-end' },
|
|
697
|
+
truncateColumns(`${selected ? '› ' : ' '}${label}`, viewport.contentColumns),
|
|
698
|
+
)
|
|
699
|
+
})
|
|
700
|
+
: phase === 'custom'
|
|
701
|
+
? [createElement(Text, { key: 'custom-input', dimColor: true, wrap: 'truncate-end' }, truncateColumns(` ${query === '' ? t('review.picker.customHint') : singleLineText(query)}`, viewport.contentColumns))]
|
|
702
|
+
: filtered.length === 0
|
|
703
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, truncateColumns(` ${loading ? t('review.picker.loading') : error !== undefined ? t('review.picker.loadFailed') : query === '' ? t('review.picker.empty') : t('review.picker.noMatch')}`, viewport.contentColumns))]
|
|
704
|
+
: filtered.slice(offset, offset + bodyRows).map((row, index) => {
|
|
705
|
+
const absolute = offset + index
|
|
706
|
+
const selected = absolute === cursor
|
|
707
|
+
const label = 'name' in row ? row.name : `${row.sha.slice(0, 7)} · ${formatRelativeTime(row.at, now)} · ${row.title}`
|
|
708
|
+
return createElement(
|
|
709
|
+
Text,
|
|
710
|
+
{ key: `row-${absolute}`, color: selected ? inkColor(getPalette().brandBright) : undefined, wrap: 'truncate-end' },
|
|
711
|
+
truncateColumns(`${selected ? '› ' : ' '}${singleLineText(label)}`, viewport.contentColumns),
|
|
712
|
+
)
|
|
713
|
+
})),
|
|
714
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('review.picker.footer'), viewport.contentColumns)),
|
|
715
|
+
)
|
|
716
|
+
}
|
|
717
|
+
|
|
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
|
+
/**
|
|
737
|
+
* The /search panel: full-text search over every persisted session through
|
|
738
|
+
* the in-process session-query engine (the same corpus the model's
|
|
739
|
+
* session_search tool reads). Type a query, Enter searches, Enter again
|
|
740
|
+
* resumes the hit; the query line edits like every kernel panel.
|
|
741
|
+
*/
|
|
742
|
+
export function SearchPanel({ load, select, initialQuery = '', close }: {
|
|
743
|
+
load: (query: string, signal?: AbortSignal) => Promise<readonly SearchRow[]>
|
|
744
|
+
select: (row: SearchRow) => void
|
|
745
|
+
initialQuery?: string
|
|
746
|
+
close: () => void
|
|
747
|
+
}): ReactElement {
|
|
748
|
+
const [query, setQuery] = useState(initialQuery)
|
|
749
|
+
const [rows, setRows] = useState<readonly SearchRow[]>([])
|
|
750
|
+
const [cursor, setCursor] = useState(0)
|
|
751
|
+
const [loading, setLoading] = useState(false)
|
|
752
|
+
const [error, setError] = useState<string>()
|
|
753
|
+
const [searched, setSearched] = useState('')
|
|
754
|
+
const searchRef = useRef<AbortController>()
|
|
755
|
+
const run = (next: string): void => {
|
|
756
|
+
const trimmed = next.trim()
|
|
757
|
+
if (trimmed === '') return
|
|
758
|
+
searchRef.current?.abort()
|
|
759
|
+
const controller = new AbortController()
|
|
760
|
+
searchRef.current = controller
|
|
761
|
+
setLoading(true)
|
|
762
|
+
setError(undefined)
|
|
763
|
+
void Promise.resolve().then(() => load(trimmed, controller.signal)).then(hits => {
|
|
764
|
+
if (controller.signal.aborted) return
|
|
765
|
+
setLoading(false)
|
|
766
|
+
setRows(hits)
|
|
767
|
+
setCursor(0)
|
|
768
|
+
setSearched(next)
|
|
769
|
+
}, reason => {
|
|
770
|
+
if (controller.signal.aborted) return
|
|
771
|
+
setLoading(false)
|
|
772
|
+
// Stale results must not stay interactive under an error header: a
|
|
773
|
+
// later Enter re-runs the query instead of resuming an old hit.
|
|
774
|
+
setRows([])
|
|
775
|
+
setCursor(0)
|
|
776
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
777
|
+
})
|
|
778
|
+
}
|
|
779
|
+
// An /search <query> invocation searches immediately with its argument.
|
|
780
|
+
useEffect(() => {
|
|
781
|
+
if (initialQuery.trim() !== '') run(initialQuery)
|
|
782
|
+
return () => searchRef.current?.abort()
|
|
783
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
784
|
+
}, [])
|
|
785
|
+
useInput((input, key) => {
|
|
786
|
+
if (key.escape || (input === 'q' && query === '')) return close()
|
|
787
|
+
if (key.ctrl && input === 'c') return close()
|
|
788
|
+
if (key.backspace || key.delete) {
|
|
789
|
+
setQuery(current => deleteLastGrapheme(current))
|
|
790
|
+
setCursor(0)
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
const next = editQuery(query, input, key)
|
|
794
|
+
if (next !== undefined) {
|
|
795
|
+
setQuery(next)
|
|
796
|
+
setCursor(0)
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
if (key.upArrow) return setCursor(value => Math.max(0, value - 1))
|
|
800
|
+
if (key.downArrow) return setCursor(value => Math.min(rows.length - 1, value + 1))
|
|
801
|
+
if (key.return) {
|
|
802
|
+
// A changed query searches; the SAME query re-runs when the previous
|
|
803
|
+
// pass failed or produced nothing (Enter is then the refresh key).
|
|
804
|
+
const stale = error !== undefined || rows.length === 0
|
|
805
|
+
if (query.trim() !== '' && (query.trim() !== searched.trim() || stale)) {
|
|
806
|
+
run(query)
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
// Non-resumable hits (subagent conversations) keep the panel open:
|
|
810
|
+
// Enter must not trade the visible results for a rejected switch.
|
|
811
|
+
const row = rows[cursor]
|
|
812
|
+
if (row !== undefined && row.resumable) select(row)
|
|
813
|
+
return
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
const stdout = useStdout().stdout
|
|
817
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
818
|
+
const now = useMemo(() => Date.now(), [rows, searched])
|
|
819
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
820
|
+
const state = loading ? 'searching…' : error !== undefined ? `error: ${error}` : rows.length === 0 ? 'no results yet' : `❯ ${rows[cursor]?.label ?? ''}`
|
|
821
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`/search · ${state} · ${t('panel.close')}`, viewport.contentColumns))
|
|
822
|
+
}
|
|
823
|
+
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
824
|
+
const offset = revealRow(0, cursor, rows.length, bodyRows)
|
|
825
|
+
const visible = rows.slice(offset, offset + bodyRows)
|
|
826
|
+
const header = error !== undefined
|
|
827
|
+
? t('search.compact.error', { message: truncateColumns(singleLineText(error), Math.max(6, viewport.contentColumns - 14)) })
|
|
828
|
+
: loading
|
|
829
|
+
? t('search.compact.searching')
|
|
830
|
+
: searched === ''
|
|
831
|
+
? t('search.title.type')
|
|
832
|
+
: rows.length === 1
|
|
833
|
+
? t('search.title.hits', { count: rows.length, query: truncateColumns(singleLineText(searched), Math.max(6, viewport.contentColumns - 30)) })
|
|
834
|
+
: t('search.title.hitsPlural', { count: rows.length, query: truncateColumns(singleLineText(searched), Math.max(6, viewport.contentColumns - 30)) })
|
|
835
|
+
const accent = panelAccent('search', getPalette().dim, getPalette().brandBright)
|
|
836
|
+
return createElement(
|
|
837
|
+
Box,
|
|
838
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
839
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(header, viewport.contentColumns)),
|
|
840
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('search.hint', { state: query === '' ? t('search.hintEmpty') : singleLineText(query) }), viewport.contentColumns)),
|
|
841
|
+
...(visible.length === 0
|
|
842
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, truncateColumns(searched === '' ? ` ${t('search.empty.idle')}` : ` ${t('search.empty.noHits', { loading: loading ? '…' : '' })}`, viewport.contentColumns))]
|
|
843
|
+
: visible.flatMap((row, index) => {
|
|
844
|
+
const absolute = offset + index
|
|
845
|
+
const selected = absolute === cursor
|
|
846
|
+
return [
|
|
847
|
+
createElement(
|
|
848
|
+
Text,
|
|
849
|
+
{
|
|
850
|
+
key: `search-${absolute}`,
|
|
851
|
+
color: selected ? inkColor(getPalette().brandBright) : undefined,
|
|
852
|
+
dimColor: row.subagent,
|
|
853
|
+
wrap: 'truncate-end',
|
|
854
|
+
},
|
|
855
|
+
truncateColumns(`${selected ? '› ' : ' '}${row.subagent ? '↳ ' : ''}${singleLineText(row.label)} · ${formatRelativeTime(row.updatedAt, now)}${row.detail === '' ? '' : ` · ${row.detail}`}${row.resumable ? '' : ' · read-only'}`, viewport.contentColumns),
|
|
856
|
+
),
|
|
857
|
+
createElement(
|
|
858
|
+
Text,
|
|
859
|
+
{ key: `search-snippet-${absolute}`, dimColor: true, wrap: 'truncate-end' },
|
|
860
|
+
truncateColumns(` ⎿ ${singleLineText(row.snippet)}`, viewport.contentColumns),
|
|
861
|
+
),
|
|
862
|
+
]
|
|
863
|
+
})),
|
|
864
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('search.footer'), viewport.contentColumns)),
|
|
541
865
|
)
|
|
542
866
|
}
|
|
543
867
|
|
|
@@ -549,8 +873,8 @@ export function HistoryPanel({ entries, fill, close }: {
|
|
|
549
873
|
*/
|
|
550
874
|
export function StatuslinePanel({ enabled, change, close }: {
|
|
551
875
|
enabled: readonly StatusItemId[]
|
|
552
|
-
change(items: readonly StatusItemId[])
|
|
553
|
-
close()
|
|
876
|
+
change: (items: readonly StatusItemId[]) => void
|
|
877
|
+
close: () => void
|
|
554
878
|
}): ReactElement {
|
|
555
879
|
// Working state: the full catalog in display order (enabled entries in
|
|
556
880
|
// their configured positions, disabled ones trailing canonically) plus
|
|
@@ -571,7 +895,7 @@ export function StatuslinePanel({ enabled, change, close }: {
|
|
|
571
895
|
if (target < 0 || target >= order.length) return
|
|
572
896
|
const next = [...order]
|
|
573
897
|
const [item] = next.splice(cursor, 1)
|
|
574
|
-
next.splice(target, 0, item
|
|
898
|
+
next.splice(target, 0, item)
|
|
575
899
|
commit(next, on)
|
|
576
900
|
setCursor(target)
|
|
577
901
|
}
|
|
@@ -600,16 +924,17 @@ export function StatuslinePanel({ enabled, change, close }: {
|
|
|
600
924
|
const stdout = useStdout().stdout
|
|
601
925
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
602
926
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
603
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
927
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.statusline.compact'), viewport.contentColumns))
|
|
604
928
|
}
|
|
605
929
|
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
606
930
|
const offset = revealRow(0, cursor, order.length, bodyRows)
|
|
607
931
|
const visible = order.slice(offset, offset + bodyRows)
|
|
608
932
|
const meta = new Map(STATUS_ITEMS.map(item => [item.id, item]))
|
|
933
|
+
const accent = panelAccent('statusline', getPalette().dim, getPalette().brandBright)
|
|
609
934
|
return createElement(
|
|
610
935
|
Box,
|
|
611
|
-
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(
|
|
612
|
-
createElement(Text, { color: inkColor(
|
|
936
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
937
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(t('statusline.title'), viewport.contentColumns)),
|
|
613
938
|
...visible.map((id, index) => {
|
|
614
939
|
const absolute = offset + index
|
|
615
940
|
const selected = absolute === cursor
|
|
@@ -625,7 +950,7 @@ export function StatuslinePanel({ enabled, change, close }: {
|
|
|
625
950
|
truncateColumns((selected ? '› ' : ' ') + (on.has(id) ? '● ' : '○ ') + (info?.label ?? id) + (info === undefined ? '' : ' · ' + info.description + ' · ' + info.side), viewport.contentColumns),
|
|
626
951
|
)
|
|
627
952
|
}),
|
|
628
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns('
|
|
953
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('panel.footer.statusline'), viewport.contentColumns)),
|
|
629
954
|
)
|
|
630
955
|
}
|
|
631
956
|
|
|
@@ -648,11 +973,11 @@ export function EffortPanel({ row, current, select, back, onExit }: {
|
|
|
648
973
|
/** Effective effort currently in force ('' when none), for the ● mark. */
|
|
649
974
|
current: string | undefined
|
|
650
975
|
/** Accept one advertised effort id, or '' for the provider default. */
|
|
651
|
-
select(effortId: string)
|
|
976
|
+
select: (effortId: string) => void
|
|
652
977
|
/** Return to the model list without applying. */
|
|
653
|
-
back()
|
|
978
|
+
back: () => void
|
|
654
979
|
/** Leave the whole /model flow (Ctrl+C). */
|
|
655
|
-
onExit()
|
|
980
|
+
onExit: () => void
|
|
656
981
|
}): ReactElement {
|
|
657
982
|
const advertised = row.reasoning?.efforts ?? []
|
|
658
983
|
const empty = row.reasoning === undefined || advertised.length === 0
|
|
@@ -699,21 +1024,21 @@ export function EffortPanel({ row, current, select, back, onExit }: {
|
|
|
699
1024
|
return
|
|
700
1025
|
}
|
|
701
1026
|
if (key.return && rows[cursor] !== undefined) {
|
|
702
|
-
select(rows[cursor]
|
|
1027
|
+
select(rows[cursor].id)
|
|
703
1028
|
}
|
|
704
1029
|
})
|
|
705
1030
|
return createElement(ListFrame, {
|
|
706
|
-
title:
|
|
1031
|
+
title: t('panel.effort.title', { provider: row.providerName, model: row.modelName }),
|
|
707
1032
|
rows: empty
|
|
708
|
-
? [{ key: 'empty', disabled: true, text: '
|
|
1033
|
+
? [{ key: 'empty', disabled: true, text: t('panel.effort.empty') }]
|
|
709
1034
|
: rows.map(effort => ({
|
|
710
1035
|
key: effort.id,
|
|
711
|
-
text: `${effort.id === effective ? '●' : '○'} ${effort.name}${effort.id === row.reasoning?.defaultEffort ?
|
|
1036
|
+
text: `${effort.id === effective ? '●' : '○'} ${effort.name}${effort.id === row.reasoning?.defaultEffort ? ` · ${t('panel.default')}` : ''}${effort.description === undefined ? '' : ` · ${effort.description}`}`,
|
|
712
1037
|
})),
|
|
713
1038
|
cursor,
|
|
714
1039
|
loading: false,
|
|
715
1040
|
query: '',
|
|
716
|
-
footer: empty ? '
|
|
1041
|
+
footer: empty ? t('panel.footer.effortEmpty') : t('panel.footer.effort'),
|
|
717
1042
|
})
|
|
718
1043
|
}
|
|
719
1044
|
|
|
@@ -738,10 +1063,10 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
738
1063
|
/** Live feed rows (child sessions observed this process). */
|
|
739
1064
|
live: readonly SubagentRow[]
|
|
740
1065
|
/** Load this session's persisted child sessions by lineage. */
|
|
741
|
-
load()
|
|
1066
|
+
load: () => Promise<readonly SessionRow[]>
|
|
742
1067
|
/** Read one child session's full transcript as markdown. */
|
|
743
|
-
readTranscript(id: string, signal?: AbortSignal)
|
|
744
|
-
close()
|
|
1068
|
+
readTranscript: (id: string, signal?: AbortSignal) => Promise<string>
|
|
1069
|
+
close: () => void
|
|
745
1070
|
}): ReactElement {
|
|
746
1071
|
const [dirRows, setDirRows] = useState<readonly SessionRow[] | undefined>(undefined)
|
|
747
1072
|
const [error, setError] = useState<string>()
|
|
@@ -812,7 +1137,7 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
812
1137
|
}, { isActive: transcript === undefined })
|
|
813
1138
|
if (transcript !== undefined) {
|
|
814
1139
|
return createElement(DocumentPanel, {
|
|
815
|
-
title:
|
|
1140
|
+
title: t('panel.subagent.transcriptTitle', { id: transcript.id.slice(-12) }),
|
|
816
1141
|
text: transcript.text,
|
|
817
1142
|
error: transcript.error,
|
|
818
1143
|
close: () => {
|
|
@@ -822,7 +1147,7 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
822
1147
|
})
|
|
823
1148
|
}
|
|
824
1149
|
return createElement(ListFrame, {
|
|
825
|
-
title:
|
|
1150
|
+
title: t('panel.agents.title', { live: live.length, total: rows.length }),
|
|
826
1151
|
rows: rows.map(row => ({
|
|
827
1152
|
key: row.id,
|
|
828
1153
|
text: `${row.running ? '●' : row.done ? '✓' : row.live ? '⏸' : '○'} ${row.label} · ${row.activity}${row.live ? ' · live' : ''}`,
|
|
@@ -831,7 +1156,7 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
831
1156
|
loading,
|
|
832
1157
|
...error === undefined ? {} : { error },
|
|
833
1158
|
query: '',
|
|
834
|
-
footer:
|
|
1159
|
+
footer: t('panel.footer.agents'),
|
|
835
1160
|
})
|
|
836
1161
|
}
|
|
837
1162
|
|
|
@@ -850,12 +1175,12 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
|
|
|
850
1175
|
export function SubagentPanel({ current, load, pick, inherit, close }: {
|
|
851
1176
|
/** Display label of the override in force, '' when following the current model. */
|
|
852
1177
|
current: string
|
|
853
|
-
load()
|
|
1178
|
+
load: () => Promise<ModelDirectory>
|
|
854
1179
|
/** Apply one model (with an advertised effort, when picked) as the override. */
|
|
855
|
-
pick(row: ModelRow, effortId?: string)
|
|
1180
|
+
pick: (row: ModelRow, effortId?: string) => void
|
|
856
1181
|
/** Drop the override: subagents follow the current model again. */
|
|
857
|
-
inherit()
|
|
858
|
-
close()
|
|
1182
|
+
inherit: () => void
|
|
1183
|
+
close: () => void
|
|
859
1184
|
}): ReactElement {
|
|
860
1185
|
const [directory, setDirectory] = useState<ModelDirectory | undefined>(undefined)
|
|
861
1186
|
const [error, setError] = useState<string>()
|
|
@@ -899,7 +1224,7 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
|
|
|
899
1224
|
setEffortFor(row)
|
|
900
1225
|
return
|
|
901
1226
|
}
|
|
902
|
-
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0]
|
|
1227
|
+
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : undefined
|
|
903
1228
|
pick(row, effortId)
|
|
904
1229
|
}
|
|
905
1230
|
})
|
|
@@ -913,9 +1238,9 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
|
|
|
913
1238
|
})
|
|
914
1239
|
}
|
|
915
1240
|
return createElement(ListFrame, {
|
|
916
|
-
title:
|
|
1241
|
+
title: `${t('panel.subagent.title')}${current === '' ? '' : t('panel.subagent.override', { value: current })}`,
|
|
917
1242
|
rows: [
|
|
918
|
-
{ key: '__inherit__', text: `${current === '' ? '●' : '○'} inherit
|
|
1243
|
+
{ key: '__inherit__', text: `${current === '' ? '●' : '○'} ${t('panel.subagent.inherit')}` },
|
|
919
1244
|
...rows.map(row => ({
|
|
920
1245
|
key: `${row.provider}/${row.model}`,
|
|
921
1246
|
text: `${current.startsWith(`${row.provider}/${row.model}`) ? '●' : '○'} ${row.providerName} · ${row.modelName}`,
|
|
@@ -925,6 +1250,149 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
|
|
|
925
1250
|
loading,
|
|
926
1251
|
...error === undefined ? {} : { error },
|
|
927
1252
|
query: '',
|
|
928
|
-
footer: '
|
|
1253
|
+
footer: t('panel.footer.subagent'),
|
|
1254
|
+
})
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* The /schedule panel: the read-only catalog of active reminders folded from
|
|
1259
|
+
* durable schedule/change events (the web ui-schedule contract: overdue
|
|
1260
|
+
* first, then ascending target; the model creates and cancels through its
|
|
1261
|
+
* schedule_* tools, the panel only shows state). A local second-hand keeps
|
|
1262
|
+
* the relative labels live while the panel is open.
|
|
1263
|
+
*/
|
|
1264
|
+
export interface ScheduleDisplayRow {
|
|
1265
|
+
readonly key: string
|
|
1266
|
+
readonly text: string
|
|
1267
|
+
readonly tone?: 'error'
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** Human frequency label: one-shot kinds read as Once, every rows carry the interval. */
|
|
1271
|
+
export function scheduleFrequency(row: ScheduleRow): string {
|
|
1272
|
+
if (row.kind !== 'every') return 'Once'
|
|
1273
|
+
const seconds = row.everySeconds ?? 0
|
|
1274
|
+
if (seconds >= 3600 && seconds % 3600 === 0) return `Every ${seconds / 3600}h`
|
|
1275
|
+
if (seconds >= 60 && seconds % 60 === 0) return `Every ${seconds / 60}m`
|
|
1276
|
+
return `Every ${seconds}s`
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** Relative label for the next target: in N unit, or N unit overdue. */
|
|
1280
|
+
export function scheduleRelative(targetAt: number, now: number): string {
|
|
1281
|
+
const delta = Math.max(0, Math.abs(targetAt - now))
|
|
1282
|
+
const minutes = Math.floor(delta / 60_000)
|
|
1283
|
+
const unit = minutes === 0
|
|
1284
|
+
? '<1m'
|
|
1285
|
+
: minutes >= 60
|
|
1286
|
+
? `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}`
|
|
1287
|
+
: `${minutes}m`
|
|
1288
|
+
return targetAt <= now ? `${unit} overdue` : `in ${unit}`
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** Ordered display rows: overdue first (error tone), then ascending target. */
|
|
1292
|
+
export function scheduleDisplayRows(rows: readonly ScheduleRow[], now: number): readonly ScheduleDisplayRow[] {
|
|
1293
|
+
return [...rows]
|
|
1294
|
+
.sort((left, right) => (Number(left.targetAt > now) - Number(right.targetAt > now)) || (left.targetAt - right.targetAt))
|
|
1295
|
+
.map(row => ({
|
|
1296
|
+
key: row.id,
|
|
1297
|
+
text: `${row.prompt} · ${scheduleFrequency(row)} · ${new Date(row.targetAt).toLocaleString()} (${scheduleRelative(row.targetAt, now)})`,
|
|
1298
|
+
tone: row.targetAt <= now ? 'error' as const : undefined,
|
|
1299
|
+
}))
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
export function SchedulePanel({ rows, close }: { rows: () => readonly ScheduleRow[]; close: () => void }): ReactElement {
|
|
1303
|
+
const [, setTick] = useState(0)
|
|
1304
|
+
useEffect(() => {
|
|
1305
|
+
const id = setInterval(() => setTick(value => value + 1), 1_000)
|
|
1306
|
+
return () => clearInterval(id)
|
|
1307
|
+
}, [])
|
|
1308
|
+
const display = scheduleDisplayRows(rows(), Date.now())
|
|
1309
|
+
const stdout = useStdout().stdout
|
|
1310
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
1311
|
+
useInput((input, key) => {
|
|
1312
|
+
if (key.escape || input === 'q') return close()
|
|
929
1313
|
})
|
|
1314
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1315
|
+
const summary = display.length === 0 ? t('panel.schedule.none') : singleLineText(display[0].text)
|
|
1316
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`/schedule · ${summary}`, viewport.contentColumns))
|
|
1317
|
+
}
|
|
1318
|
+
const budget = Math.max(1, viewport.bodyRows)
|
|
1319
|
+
const visible = display.slice(0, budget)
|
|
1320
|
+
const hidden = display.length - visible.length
|
|
1321
|
+
const accent = panelAccent('schedule', getPalette().dim, getPalette().brandBright)
|
|
1322
|
+
return createElement(
|
|
1323
|
+
Box,
|
|
1324
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
1325
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(display.length === 1 ? t('schedule.title', { count: display.length }) : t('schedule.titlePlural', { count: display.length }), viewport.contentColumns)),
|
|
1326
|
+
...(display.length === 0
|
|
1327
|
+
? [createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(` ${t('schedule.empty')}`, viewport.contentColumns))]
|
|
1328
|
+
: visible.map(row => createElement(Text, {
|
|
1329
|
+
key: row.key,
|
|
1330
|
+
color: row.tone === 'error' ? inkColor(getPalette().error) : undefined,
|
|
1331
|
+
wrap: 'truncate-end',
|
|
1332
|
+
}, truncateColumns(` ${singleLineText(row.text)}`, viewport.contentColumns)))),
|
|
1333
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('panel.schedule.footer', { more: hidden > 0 ? t('panel.schedule.more', { count: hidden }) : '' }), viewport.contentColumns)),
|
|
1334
|
+
)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* The /usage panel: the session's provider-reported token totals, its context
|
|
1339
|
+
* pressure and estimated composition, and the exact per-turn accounting, in
|
|
1340
|
+
* one bounded scrollable surface. Read-only — Esc or q closes it.
|
|
1341
|
+
*/
|
|
1342
|
+
export function UsagePanel({ load, close }: {
|
|
1343
|
+
/** Read the current session's usage blocks from the mounted projections. */
|
|
1344
|
+
load: () => Promise<UsageView>
|
|
1345
|
+
close: () => void
|
|
1346
|
+
}): ReactElement {
|
|
1347
|
+
const stdout = useStdout().stdout
|
|
1348
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
1349
|
+
const [scroll, setScroll] = useState(0)
|
|
1350
|
+
const [view, setView] = useState<UsageView>()
|
|
1351
|
+
const [error, setError] = useState<string>()
|
|
1352
|
+
// The panel opens on the loading row and swaps in the numbers when the
|
|
1353
|
+
// loader settles: materializing the projection units folds the whole log,
|
|
1354
|
+
// and that must not run inside the keystroke that opened the panel.
|
|
1355
|
+
useEffect(() => {
|
|
1356
|
+
let live = true
|
|
1357
|
+
Promise.resolve().then(load).then(
|
|
1358
|
+
loaded => {
|
|
1359
|
+
if (live) setView(loaded)
|
|
1360
|
+
},
|
|
1361
|
+
reason => {
|
|
1362
|
+
if (live) setError(reason instanceof Error ? reason.message : String(reason))
|
|
1363
|
+
},
|
|
1364
|
+
)
|
|
1365
|
+
return () => {
|
|
1366
|
+
live = false
|
|
1367
|
+
}
|
|
1368
|
+
}, [load])
|
|
1369
|
+
const lines = useMemo(
|
|
1370
|
+
() => view === undefined ? [] : usageLines(view, viewport.contentColumns),
|
|
1371
|
+
[view, viewport.contentColumns],
|
|
1372
|
+
)
|
|
1373
|
+
useInput((input, key) => {
|
|
1374
|
+
if (key.escape || input === 'q') return close()
|
|
1375
|
+
if (key.upArrow) return setScroll(value => Math.max(0, value - 1))
|
|
1376
|
+
if (key.downArrow) return setScroll(value => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + 1))
|
|
1377
|
+
if (key.pageUp) return setScroll(value => Math.max(0, value - Math.max(1, viewport.bodyRows - 1)))
|
|
1378
|
+
if (key.pageDown) return setScroll(value => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + Math.max(1, viewport.bodyRows - 1)))
|
|
1379
|
+
if (input === 'g') return setScroll(0)
|
|
1380
|
+
if (input === 'G') return setScroll(Math.max(0, lines.length - viewport.bodyRows))
|
|
1381
|
+
})
|
|
1382
|
+
if (viewport.compact) {
|
|
1383
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.usage.compact'), viewport.contentColumns))
|
|
1384
|
+
}
|
|
1385
|
+
const body: readonly StyledLine[] = error !== undefined
|
|
1386
|
+
? textLines(`error: ${singleLineText(error)}`, viewport.contentColumns, 'error')
|
|
1387
|
+
: view === undefined
|
|
1388
|
+
? textLines(t('panel.loading'), viewport.contentColumns, 'dim')
|
|
1389
|
+
: lines.slice(scroll, scroll + viewport.bodyRows)
|
|
1390
|
+
const accent = panelAccent('usage', getPalette().dim, getPalette().brandBright)
|
|
1391
|
+
return createElement(
|
|
1392
|
+
Box,
|
|
1393
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
1394
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(t('panel.usage.title'), viewport.contentColumns)),
|
|
1395
|
+
createElement(DocumentRows, { lines: body }),
|
|
1396
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(t('panel.usage.footer'), viewport.contentColumns)),
|
|
1397
|
+
)
|
|
930
1398
|
}
|