dsh-code 0.6.1 → 0.8.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 +20 -6
- package/README.md +20 -6
- package/lib/index.mjs +3952 -1315
- package/lib/startup.mjs +21 -9
- package/lib/theme-BEi4i_aN.mjs +624 -0
- package/lib/types/app.d.ts +108 -4
- package/lib/types/history.d.ts +15 -4
- package/lib/types/index.d.ts +49 -0
- package/lib/types/kernel-panels.d.ts +28 -0
- package/lib/types/mentions.d.ts +29 -12
- package/lib/types/models.d.ts +66 -0
- package/lib/types/permissions.d.ts +37 -0
- package/lib/types/presets.d.ts +2 -0
- package/lib/types/provider-settings.d.ts +144 -0
- package/lib/types/questions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +177 -2
- package/lib/types/render/lines.d.ts +6 -0
- package/lib/types/render/markdown.d.ts +3 -3
- package/lib/types/render/projection.d.ts +123 -3
- package/lib/types/render/status.d.ts +35 -24
- package/lib/types/render/text.d.ts +14 -7
- package/lib/types/render/tool-detail.d.ts +3 -1
- package/lib/types/render/tool-preview.d.ts +4 -1
- package/lib/types/session-directory.d.ts +15 -0
- package/lib/types/startup.d.ts +12 -4
- package/lib/types/store.d.ts +13 -2
- package/lib/types/theme-panel.d.ts +24 -0
- package/lib/types/theme.d.ts +158 -2
- package/lib/types/version.d.ts +5 -0
- package/package.json +1 -1
- package/src/app.ts +1283 -206
- package/src/approval.ts +11 -2
- package/src/history.ts +20 -5
- package/src/index.ts +1207 -905
- package/src/kernel-panels.ts +518 -419
- package/src/mentions.ts +57 -27
- package/src/models.ts +200 -66
- package/src/permissions.ts +85 -0
- package/src/presets.ts +12 -0
- package/src/provider-settings.ts +520 -0
- package/src/questions.ts +15 -5
- package/src/render/animations.ts +373 -2
- package/src/render/lines.ts +21 -6
- package/src/render/markdown.ts +302 -4
- package/src/render/projection.ts +1419 -659
- package/src/render/status.ts +650 -603
- package/src/render/text.ts +28 -9
- package/src/render/tool-detail.ts +81 -40
- package/src/render/tool-preview.ts +18 -2
- package/src/session-directory.ts +44 -5
- package/src/skills.ts +8 -4
- package/src/startup.ts +119 -109
- package/src/store.ts +26 -8
- package/src/theme-panel.ts +72 -0
- package/src/theme.ts +206 -70
- package/src/version.ts +16 -0
package/src/kernel-panels.ts
CHANGED
|
@@ -1,419 +1,518 @@
|
|
|
1
|
-
/** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
|
|
2
|
-
|
|
3
|
-
import { createElement, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
|
4
|
-
import { Box, Text, useInput, useStdout } from 'ink'
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
readonly
|
|
20
|
-
readonly
|
|
21
|
-
readonly
|
|
22
|
-
readonly
|
|
23
|
-
readonly
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const [
|
|
77
|
-
const [
|
|
78
|
-
const [
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
useEffect(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (key.
|
|
92
|
-
if (
|
|
93
|
-
if (key.
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const [
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const [
|
|
142
|
-
const [
|
|
143
|
-
const [cursor, setCursor] = useState(0)
|
|
144
|
-
const [
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
1
|
+
/** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
|
|
2
|
+
|
|
3
|
+
import { createElement, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
|
4
|
+
import { Box, Text, useInput, useStdout } from 'ink'
|
|
5
|
+
import type { ModelRow } from './models.ts'
|
|
6
|
+
import type { PermissionRow } from './permissions.ts'
|
|
7
|
+
import type { PresetRow } from './presets.ts'
|
|
8
|
+
import type { PluginRow } from './plugin-inventory.ts'
|
|
9
|
+
import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
|
|
10
|
+
import { panelViewport, revealRow } from './render/inspector.ts'
|
|
11
|
+
import { textLines } from './render/lines.ts'
|
|
12
|
+
import { DEFAULT_STATUSLINE_ITEMS, STATUS_ITEMS, type StatusItemId } from './render/status.ts'
|
|
13
|
+
import { singleLineText, truncateColumns } from './render/text.ts'
|
|
14
|
+
import { getPalette, inkColor } from './theme.ts'
|
|
15
|
+
|
|
16
|
+
interface ListFrameProps {
|
|
17
|
+
readonly title: string
|
|
18
|
+
readonly rows: readonly { readonly key: string; readonly text: string; readonly disabled?: boolean }[]
|
|
19
|
+
readonly cursor: number
|
|
20
|
+
readonly loading: boolean
|
|
21
|
+
readonly error?: string
|
|
22
|
+
readonly query: string
|
|
23
|
+
readonly footer: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ListFrame(props: ListFrameProps): ReactElement {
|
|
27
|
+
const stdout = useStdout().stdout
|
|
28
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
29
|
+
if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
|
|
30
|
+
if (viewport.compact) {
|
|
31
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(`${props.title} · esc close`), viewport.contentColumns))
|
|
32
|
+
}
|
|
33
|
+
const stateRows = props.loading
|
|
34
|
+
? [{ key: 'loading', text: ' loading…' }]
|
|
35
|
+
: props.error !== undefined
|
|
36
|
+
? [{ key: 'error', text: ` ${singleLineText(props.error)}` }]
|
|
37
|
+
: props.rows.length === 0
|
|
38
|
+
? [{ key: 'empty', text: ' no matching entries' }]
|
|
39
|
+
: props.rows
|
|
40
|
+
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
41
|
+
const offset = revealRow(0, props.cursor, stateRows.length, bodyRows)
|
|
42
|
+
const visible = stateRows.slice(offset, offset + bodyRows)
|
|
43
|
+
return createElement(
|
|
44
|
+
Box,
|
|
45
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
|
|
46
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(singleLineText(props.title), viewport.contentColumns)),
|
|
47
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(`search: ${props.query === '' ? 'type to filter' : props.query}`), viewport.contentColumns)),
|
|
48
|
+
...visible.map((row, index) => {
|
|
49
|
+
const absolute = offset + index
|
|
50
|
+
const selected = !props.loading && props.error === undefined && props.rows.length > 0 && absolute === props.cursor
|
|
51
|
+
return createElement(Text, {
|
|
52
|
+
key: row.key,
|
|
53
|
+
color: selected ? inkColor(getPalette().brandBright) : row.disabled ? inkColor(getPalette().dim) : undefined,
|
|
54
|
+
dimColor: row.disabled,
|
|
55
|
+
wrap: 'truncate-end',
|
|
56
|
+
}, truncateColumns(`${selected ? '› ' : ' '}${singleLineText(row.text)}`, viewport.contentColumns))
|
|
57
|
+
}),
|
|
58
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(props.footer), viewport.contentColumns)),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function editQuery(query: string, input: string, key: { backspace?: boolean; delete?: boolean }): string | undefined {
|
|
63
|
+
if (key.backspace || key.delete) return query.slice(0, -1)
|
|
64
|
+
if (input.length === 1 && input >= ' ' && input !== '\x7f') return query + input
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function ModePanel({ current, load, select, close }: {
|
|
69
|
+
current: string
|
|
70
|
+
load(): Promise<readonly PresetRow[]>
|
|
71
|
+
select(id: string): void
|
|
72
|
+
close(): void
|
|
73
|
+
}): ReactElement {
|
|
74
|
+
const [rows, setRows] = useState<readonly PresetRow[]>([])
|
|
75
|
+
const [query, setQuery] = useState('')
|
|
76
|
+
const [cursor, setCursor] = useState(0)
|
|
77
|
+
const [loading, setLoading] = useState(true)
|
|
78
|
+
const [error, setError] = useState<string>()
|
|
79
|
+
const refresh = (): void => {
|
|
80
|
+
setLoading(true); setError(undefined)
|
|
81
|
+
Promise.resolve().then(load).then(value => { setRows(value); setLoading(false) }, reason => {
|
|
82
|
+
setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
useEffect(refresh, [])
|
|
86
|
+
const visible = useMemo(() => rows.filter(row => `${row.id} ${row.name ?? ''} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
|
|
87
|
+
useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
|
|
88
|
+
useInput((input, key) => {
|
|
89
|
+
if (key.escape || input === 'q') return close()
|
|
90
|
+
if (input === 'r' && query === '') return refresh()
|
|
91
|
+
if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
|
|
92
|
+
if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
|
|
93
|
+
if (key.return && visible[cursor]?.broken === undefined) return select(visible[cursor]!.id)
|
|
94
|
+
const next = editQuery(query, input, key)
|
|
95
|
+
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
96
|
+
})
|
|
97
|
+
return createElement(ListFrame, {
|
|
98
|
+
title: `/mode · current ${current}`,
|
|
99
|
+
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}`}` })),
|
|
100
|
+
cursor, loading, error, query, footer: '↑↓ choose · enter switch · r refresh · esc close',
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function PermissionPanel({ current, load, select, close }: {
|
|
105
|
+
current: string
|
|
106
|
+
load(): Promise<readonly PermissionRow[]>
|
|
107
|
+
select(id: string): void
|
|
108
|
+
close(): void
|
|
109
|
+
}): ReactElement {
|
|
110
|
+
const [rows, setRows] = useState<readonly PermissionRow[]>([])
|
|
111
|
+
const [query, setQuery] = useState('')
|
|
112
|
+
const [cursor, setCursor] = useState(0)
|
|
113
|
+
const [loading, setLoading] = useState(true)
|
|
114
|
+
const [error, setError] = useState<string>()
|
|
115
|
+
const refresh = (): void => {
|
|
116
|
+
setLoading(true); setError(undefined)
|
|
117
|
+
Promise.resolve().then(load).then(value => { setRows(value); setLoading(false) }, reason => {
|
|
118
|
+
setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
useEffect(refresh, [])
|
|
122
|
+
const visible = useMemo(() => rows.filter(row => `${row.id} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
|
|
123
|
+
useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
|
|
124
|
+
useInput((input, key) => {
|
|
125
|
+
if (key.escape || input === 'q') return close()
|
|
126
|
+
if (input === 'r' && query === '') return refresh()
|
|
127
|
+
if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
|
|
128
|
+
if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
|
|
129
|
+
if (key.return && visible[cursor] !== undefined) return select(visible[cursor]!.id)
|
|
130
|
+
const next = editQuery(query, input, key)
|
|
131
|
+
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
132
|
+
})
|
|
133
|
+
return createElement(ListFrame, {
|
|
134
|
+
title: `/permission · current ${current}`,
|
|
135
|
+
rows: visible.map(row => ({ key: row.id, text: `${row.id === current ? '●' : '○'} ${row.id}${row.description === undefined ? '' : ` · ${row.description}`}` })),
|
|
136
|
+
cursor, loading, error, query, footer: '↑↓ choose · enter select · r refresh · esc close',
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function PluginPanel({ load, close, initialQuery = '' }: { load(): readonly PluginRow[]; close(): void; initialQuery?: string }): ReactElement {
|
|
141
|
+
const [epoch, setEpoch] = useState(0)
|
|
142
|
+
const [query, setQuery] = useState(initialQuery)
|
|
143
|
+
const [cursor, setCursor] = useState(0)
|
|
144
|
+
const [expanded, setExpanded] = useState(false)
|
|
145
|
+
const rows = useMemo(() => load().filter(row => `${row.entryId} ${row.moduleName} ${row.phase ?? ''}`.toLowerCase().includes(query.toLowerCase())), [epoch, query])
|
|
146
|
+
useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
|
|
147
|
+
useInput((input, key) => {
|
|
148
|
+
if (key.escape || input === 'q') return close()
|
|
149
|
+
if (input === 'r' && query === '') return setEpoch(value => value + 1)
|
|
150
|
+
if (key.upArrow) return setCursor(value => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length)
|
|
151
|
+
if (key.downArrow) return setCursor(value => rows.length === 0 ? 0 : (value + 1) % rows.length)
|
|
152
|
+
if (key.return) return setExpanded(value => !value)
|
|
153
|
+
const next = editQuery(query, input, key)
|
|
154
|
+
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
155
|
+
})
|
|
156
|
+
return createElement(ListFrame, {
|
|
157
|
+
title: '/plugin · loader inspector',
|
|
158
|
+
rows: rows.map((row, index) => ({
|
|
159
|
+
key: row.entryId,
|
|
160
|
+
disabled: !row.enabled,
|
|
161
|
+
text: `${row.enabled ? '●' : '○'} ${row.entryId} · ${row.phase ?? 'not mounted'}${expanded && index === cursor ? ` · ${row.moduleName}` : ''}`,
|
|
162
|
+
})), cursor, loading: false, query, footer: '↑↓ inspect · enter details · r refresh · esc close',
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function ResumePanel({ currentCwd, load, readTranscript, select, close }: {
|
|
167
|
+
currentCwd: string
|
|
168
|
+
load(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>
|
|
169
|
+
readTranscript(id: string, signal?: AbortSignal): Promise<string>
|
|
170
|
+
select(row: SessionRow): void
|
|
171
|
+
close(): void
|
|
172
|
+
}): ReactElement {
|
|
173
|
+
const [options, setOptions] = useState<SessionDirectoryOptions>({ sessions: 'roots', cwd: 'all', sort: 'newest', currentCwd, query: '' })
|
|
174
|
+
const [focus, setFocus] = useState(0)
|
|
175
|
+
const [density, setDensity] = useState<'comfortable' | 'dense'>('comfortable')
|
|
176
|
+
const [rows, setRows] = useState<readonly SessionRow[]>([])
|
|
177
|
+
const [cursor, setCursor] = useState(0)
|
|
178
|
+
const [loading, setLoading] = useState(true)
|
|
179
|
+
const [error, setError] = useState<string>()
|
|
180
|
+
const [expanded, setExpanded] = useState<string>()
|
|
181
|
+
const [transcript, setTranscript] = useState<{ id: string; text?: string; error?: string }>()
|
|
182
|
+
const transcriptLoad = useRef<AbortController>()
|
|
183
|
+
useEffect(() => () => transcriptLoad.current?.abort(), [])
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const controller = new AbortController()
|
|
186
|
+
setLoading(true); setError(undefined)
|
|
187
|
+
Promise.resolve().then(() => load(options, controller.signal)).then(value => {
|
|
188
|
+
if (!controller.signal.aborted) { setRows(value); setLoading(false) }
|
|
189
|
+
}, reason => {
|
|
190
|
+
if (!controller.signal.aborted) { setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false) }
|
|
191
|
+
})
|
|
192
|
+
return () => controller.abort()
|
|
193
|
+
}, [options])
|
|
194
|
+
useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
|
|
195
|
+
const cycle = (): void => {
|
|
196
|
+
if (focus === 3) {
|
|
197
|
+
setDensity(current => current === 'comfortable' ? 'dense' : 'comfortable')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
setOptions(value => {
|
|
201
|
+
if (focus === 0) return { ...value, sessions: value.sessions === 'roots' ? 'all' : 'roots' }
|
|
202
|
+
if (focus === 1) return { ...value, cwd: value.cwd === 'all' ? 'current' : 'all' }
|
|
203
|
+
return { ...value, sort: value.sort === 'newest' ? 'oldest' : 'newest' }
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
useInput((input, key) => {
|
|
207
|
+
if (key.escape || input === 'q') return close()
|
|
208
|
+
if (key.tab) return setFocus(value => (value + (key.shift ? 3 : 1)) % 4)
|
|
209
|
+
if (key.leftArrow) return cycle()
|
|
210
|
+
if (key.rightArrow) return cycle()
|
|
211
|
+
if (key.upArrow) return setCursor(value => rows.length === 0 ? 0 : Math.max(0, value - 1))
|
|
212
|
+
if (key.downArrow) return setCursor(value => rows.length === 0 ? 0 : Math.min(rows.length - 1, value + 1))
|
|
213
|
+
if (key.pageUp) return setCursor(value => Math.max(0, value - 8))
|
|
214
|
+
if (key.pageDown) return setCursor(value => Math.min(rows.length - 1, value + 8))
|
|
215
|
+
if (input === 'g') return setCursor(0)
|
|
216
|
+
if (input === 'G') return setCursor(Math.max(0, rows.length - 1))
|
|
217
|
+
if (input === 'd') return setDensity(value => value === 'comfortable' ? 'dense' : 'comfortable')
|
|
218
|
+
if (input === 'e' && rows[cursor] !== undefined) {
|
|
219
|
+
return setExpanded(value => value === rows[cursor]!.id ? undefined : rows[cursor]!.id)
|
|
220
|
+
}
|
|
221
|
+
if (input === 't' && rows[cursor] !== undefined) {
|
|
222
|
+
const row = rows[cursor]!
|
|
223
|
+
transcriptLoad.current?.abort()
|
|
224
|
+
setTranscript({ id: row.id })
|
|
225
|
+
const controller = new AbortController()
|
|
226
|
+
transcriptLoad.current = controller
|
|
227
|
+
Promise.resolve().then(() => readTranscript(row.id, controller.signal)).then(
|
|
228
|
+
text => { if (!controller.signal.aborted) setTranscript({ id: row.id, text }) },
|
|
229
|
+
reason => { if (!controller.signal.aborted) setTranscript({ id: row.id, error: reason instanceof Error ? reason.message : String(reason) }) },
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
if (key.return && rows[cursor]?.resumable === true) return select(rows[cursor]!)
|
|
234
|
+
const next = editQuery(options.query, input, key)
|
|
235
|
+
if (next !== undefined) { setOptions(value => ({ ...value, query: next })); setCursor(0) }
|
|
236
|
+
}, { isActive: transcript === undefined })
|
|
237
|
+
if (transcript !== undefined) {
|
|
238
|
+
return createElement(DocumentPanel, {
|
|
239
|
+
title: `transcript · ${transcript.id}`,
|
|
240
|
+
text: transcript.text,
|
|
241
|
+
error: transcript.error,
|
|
242
|
+
close: () => { transcriptLoad.current?.abort(); setTranscript(undefined) },
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
const toolbar = `[${focus === 0 ? '>' : ''}${options.sessions}] [${focus === 1 ? '>' : ''}${options.cwd} cwd] [${focus === 2 ? '>' : ''}${options.sort}] [${focus === 3 ? '>' : ''}${density}]`
|
|
246
|
+
return createElement(ListFrame, {
|
|
247
|
+
title: `/resume · ${toolbar}`,
|
|
248
|
+
rows: rows.map(row => ({
|
|
249
|
+
key: row.id,
|
|
250
|
+
disabled: !row.resumable,
|
|
251
|
+
text: `${row.subagent ? '↳' : '○'} ${row.title ?? row.id.slice(-12)}${density === 'comfortable' ? ` · ${row.workspace} · ${row.preset}` : ''}${row.live ? ' · live' : ''}${expanded === row.id ? ` · ${row.id} · ${row.cwd}${row.parent === undefined ? '' : ` · parent ${row.parent}`}` : ''}`,
|
|
252
|
+
})), cursor, loading, error, query: options.query,
|
|
253
|
+
footer: 'type search · tab/←→ filters · ↑↓/pg navigate · e details · t transcript · enter resume',
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function DocumentPanel({ title, text, error, close }: {
|
|
258
|
+
title: string
|
|
259
|
+
text?: string
|
|
260
|
+
error?: string
|
|
261
|
+
close(): void
|
|
262
|
+
}): ReactElement {
|
|
263
|
+
const stdout = useStdout().stdout
|
|
264
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
265
|
+
const [scroll, setScroll] = useState(0)
|
|
266
|
+
const lines = useMemo(() => text === undefined ? [] : textLines(text, viewport.contentColumns).map(line => line.segments.map(segment => segment.text).join('')), [text, viewport.contentColumns])
|
|
267
|
+
useInput((input, key) => {
|
|
268
|
+
if (key.escape || input === 'q' || input === 't') return close()
|
|
269
|
+
if (key.upArrow) return setScroll(value => Math.max(0, value - 1))
|
|
270
|
+
if (key.downArrow) return setScroll(value => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + 1))
|
|
271
|
+
if (key.pageUp) return setScroll(value => Math.max(0, value - Math.max(1, viewport.bodyRows - 1)))
|
|
272
|
+
if (key.pageDown) return setScroll(value => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + Math.max(1, viewport.bodyRows - 1)))
|
|
273
|
+
if (input === 'g') return setScroll(0)
|
|
274
|
+
if (input === 'G') return setScroll(Math.max(0, lines.length - viewport.bodyRows))
|
|
275
|
+
})
|
|
276
|
+
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('transcript · esc close', viewport.contentColumns))
|
|
277
|
+
const body = error !== undefined
|
|
278
|
+
? [`error: ${singleLineText(error)}`]
|
|
279
|
+
: text === undefined
|
|
280
|
+
? ['loading transcript…']
|
|
281
|
+
: lines.slice(scroll, scroll + viewport.bodyRows)
|
|
282
|
+
return createElement(
|
|
283
|
+
Box,
|
|
284
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
|
|
285
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(singleLineText(title), viewport.contentColumns)),
|
|
286
|
+
...body.map((line, index) => createElement(Text, { key: `${scroll}-${index}`, wrap: 'truncate-end' }, truncateColumns(line, viewport.contentColumns))),
|
|
287
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(`lines ${lines.length === 0 ? 0 : scroll + 1}-${Math.min(lines.length, scroll + viewport.bodyRows)}/${lines.length} · ↑↓/pg/g/G · t/esc close`, viewport.contentColumns)),
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The /history recall panel (Codex composer-history search, bounded): one
|
|
293
|
+
* query line over the newest-first recall space, filtered by substring, with
|
|
294
|
+
* arrow selection and enter to fill the composer. Editing the query restarts
|
|
295
|
+
* from the newest match; Esc closes without touching the draft.
|
|
296
|
+
*/
|
|
297
|
+
export function HistoryPanel({ entries, fill, close }: {
|
|
298
|
+
/** Newest-first recall entries (persistent + in-session, deduped). */
|
|
299
|
+
entries: readonly string[]
|
|
300
|
+
/** Accept one entry: its text plus its recall-space index (browsing resumes there). */
|
|
301
|
+
fill(text: string, index: number): void
|
|
302
|
+
close(): void
|
|
303
|
+
}): ReactElement {
|
|
304
|
+
const [query, setQuery] = useState('')
|
|
305
|
+
const [cursor, setCursor] = useState(0)
|
|
306
|
+
const matches = query === ''
|
|
307
|
+
? entries
|
|
308
|
+
: entries.filter(entry => entry.toLowerCase().includes(query.toLowerCase()))
|
|
309
|
+
useInput((input, key) => {
|
|
310
|
+
if (key.escape) return close()
|
|
311
|
+
if (key.return) {
|
|
312
|
+
const entry = matches[cursor]
|
|
313
|
+
if (entry !== undefined) fill(entry, entries.indexOf(entry))
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (key.upArrow) return setCursor(value => Math.max(0, value - 1))
|
|
317
|
+
if (key.downArrow) return setCursor(value => Math.min(matches.length - 1, value + 1))
|
|
318
|
+
if (input === 'g') return setCursor(0)
|
|
319
|
+
if (input === 'G') return setCursor(matches.length - 1)
|
|
320
|
+
if (key.backspace) {
|
|
321
|
+
setQuery(current => current.slice(0, -1))
|
|
322
|
+
setCursor(0)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
if (input !== '' && !key.ctrl && !key.meta && !key.shift) {
|
|
326
|
+
setQuery(current => (current + input).slice(0, 120))
|
|
327
|
+
setCursor(0)
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
const stdout = useStdout().stdout
|
|
331
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
332
|
+
if (viewport.compact) {
|
|
333
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('/history · esc close', viewport.contentColumns))
|
|
334
|
+
}
|
|
335
|
+
if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
|
|
336
|
+
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
337
|
+
const offset = revealRow(0, cursor, matches.length, bodyRows)
|
|
338
|
+
const visible = matches.slice(offset, offset + bodyRows)
|
|
339
|
+
const header = query === ''
|
|
340
|
+
? `/history · ${entries.length} prompts · type to filter`
|
|
341
|
+
: `/history · ${matches.length} of ${entries.length} match '${truncateColumns(singleLineText(query), viewport.contentColumns - 30)}'`
|
|
342
|
+
return createElement(
|
|
343
|
+
Box,
|
|
344
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
|
|
345
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(header, viewport.contentColumns)),
|
|
346
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(` filter ${query === '' ? '· type to search prompts' : '· ' + singleLineText(query)}, enter fills the composer`, viewport.contentColumns)),
|
|
347
|
+
...(visible.length === 0
|
|
348
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, truncateColumns(' no matching prompts', viewport.contentColumns))]
|
|
349
|
+
: visible.map((entry, index) => {
|
|
350
|
+
const absolute = offset + index
|
|
351
|
+
const selected = absolute === cursor
|
|
352
|
+
return createElement(
|
|
353
|
+
Text,
|
|
354
|
+
{
|
|
355
|
+
key: `history-${absolute}`,
|
|
356
|
+
color: selected ? inkColor(getPalette().brandBright) : undefined,
|
|
357
|
+
wrap: 'truncate-end',
|
|
358
|
+
},
|
|
359
|
+
truncateColumns((selected ? '› ' : ' ') + singleLineText(entry), viewport.contentColumns),
|
|
360
|
+
)
|
|
361
|
+
})),
|
|
362
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns('↑↓ move · g/G ends · enter fill · esc close', viewport.contentColumns)),
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The /statusline picker (the Codex setup-view contract): one bounded list
|
|
368
|
+
* of every status item with its enabled mark, arrow reordering, and a
|
|
369
|
+
* live preview — the real status line under the composer updates as you
|
|
370
|
+
* edit, so the panel itself carries no duplicate preview row.
|
|
371
|
+
*/
|
|
372
|
+
export function StatuslinePanel({ enabled, change, close }: {
|
|
373
|
+
enabled: readonly StatusItemId[]
|
|
374
|
+
change(items: readonly StatusItemId[]): void
|
|
375
|
+
close(): void
|
|
376
|
+
}): ReactElement {
|
|
377
|
+
// Working state: the full catalog in display order (enabled entries in
|
|
378
|
+
// their configured positions, disabled ones trailing canonically) plus
|
|
379
|
+
// the enabled set. Persisted shape is the enabled subsequence only.
|
|
380
|
+
const [order, setOrder] = useState<readonly StatusItemId[]>(() => {
|
|
381
|
+
const seen = new Set(enabled)
|
|
382
|
+
return [...enabled, ...DEFAULT_STATUSLINE_ITEMS.filter(id => !seen.has(id))]
|
|
383
|
+
})
|
|
384
|
+
const [on, setOn] = useState<ReadonlySet<StatusItemId>>(() => new Set(enabled))
|
|
385
|
+
const [cursor, setCursor] = useState(0)
|
|
386
|
+
const commit = (nextOrder: readonly StatusItemId[], nextOn: ReadonlySet<StatusItemId>): void => {
|
|
387
|
+
setOrder(nextOrder)
|
|
388
|
+
setOn(nextOn)
|
|
389
|
+
change(nextOrder.filter(id => nextOn.has(id)))
|
|
390
|
+
}
|
|
391
|
+
const move = (offset: number): void => {
|
|
392
|
+
const target = cursor + offset
|
|
393
|
+
if (target < 0 || target >= order.length) return
|
|
394
|
+
const next = [...order]
|
|
395
|
+
const [item] = next.splice(cursor, 1)
|
|
396
|
+
next.splice(target, 0, item!)
|
|
397
|
+
commit(next, on)
|
|
398
|
+
setCursor(target)
|
|
399
|
+
}
|
|
400
|
+
useInput((input, key) => {
|
|
401
|
+
if (key.escape || input === 'q' || key.return) return close()
|
|
402
|
+
if (key.upArrow) return setCursor(value => Math.max(0, value - 1))
|
|
403
|
+
if (key.downArrow) return setCursor(value => Math.min(order.length - 1, value + 1))
|
|
404
|
+
if (key.leftArrow) return move(-1)
|
|
405
|
+
if (key.rightArrow) return move(1)
|
|
406
|
+
if (input === 'g') return setCursor(0)
|
|
407
|
+
if (input === 'G') return setCursor(order.length - 1)
|
|
408
|
+
if (input === 'd') {
|
|
409
|
+
commit([...DEFAULT_STATUSLINE_ITEMS], new Set(DEFAULT_STATUSLINE_ITEMS))
|
|
410
|
+
setCursor(0)
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
if (input === ' ') {
|
|
414
|
+
const item = order[cursor]
|
|
415
|
+
if (item === undefined) return
|
|
416
|
+
const next = new Set(on)
|
|
417
|
+
if (next.has(item)) next.delete(item)
|
|
418
|
+
else next.add(item)
|
|
419
|
+
commit(order, next)
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
const stdout = useStdout().stdout
|
|
423
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
424
|
+
if (viewport.compact) {
|
|
425
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('/statusline · esc close', viewport.contentColumns))
|
|
426
|
+
}
|
|
427
|
+
if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
|
|
428
|
+
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
429
|
+
const offset = revealRow(0, cursor, order.length, bodyRows)
|
|
430
|
+
const visible = order.slice(offset, offset + bodyRows)
|
|
431
|
+
const meta = new Map(STATUS_ITEMS.map(item => [item.id, item]))
|
|
432
|
+
return createElement(
|
|
433
|
+
Box,
|
|
434
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
|
|
435
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns('/statusline · items apply to the live status line below', viewport.contentColumns)),
|
|
436
|
+
...visible.map((id, index) => {
|
|
437
|
+
const absolute = offset + index
|
|
438
|
+
const selected = absolute === cursor
|
|
439
|
+
const info = meta.get(id)
|
|
440
|
+
return createElement(
|
|
441
|
+
Text,
|
|
442
|
+
{
|
|
443
|
+
key: id,
|
|
444
|
+
color: selected ? inkColor(getPalette().brandBright) : undefined,
|
|
445
|
+
dimColor: !on.has(id) || undefined,
|
|
446
|
+
wrap: 'truncate-end',
|
|
447
|
+
},
|
|
448
|
+
truncateColumns((selected ? '› ' : ' ') + (on.has(id) ? '● ' : '○ ') + (info?.label ?? id) + (info === undefined ? '' : ' · ' + info.description + ' · ' + info.side), viewport.contentColumns),
|
|
449
|
+
)
|
|
450
|
+
}),
|
|
451
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns('↑↓ move · space toggle · ←→ reorder · d default · esc close', viewport.contentColumns)),
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The `/model` reasoning-effort stage (the Codex model → reasoning popup
|
|
457
|
+
* contract): one bounded list over the selected model's adapter-advertised
|
|
458
|
+
* effort levels, with the effective effort and the model default marked.
|
|
459
|
+
* A model WITHOUT an adapter-declared default leads with a "Default"
|
|
460
|
+
* (provider-default) row — the web effort pane's first entry — so the user
|
|
461
|
+
* can clear a picked level back to provider behavior instead of being forced
|
|
462
|
+
* to choose an advertised one. Enter applies one level; Esc returns to the
|
|
463
|
+
* model list without applying.
|
|
464
|
+
*/
|
|
465
|
+
export function EffortPanel({ row, current, select, back }: {
|
|
466
|
+
/** The model row whose advertised levels this stage lists. */
|
|
467
|
+
row: ModelRow
|
|
468
|
+
/** Effective effort currently in force ('' when none), for the ● mark. */
|
|
469
|
+
current: string | undefined
|
|
470
|
+
/** Accept one advertised effort id, or '' for the provider default. */
|
|
471
|
+
select(effortId: string): void
|
|
472
|
+
/** Return to the model list without applying. */
|
|
473
|
+
back(): void
|
|
474
|
+
}): ReactElement {
|
|
475
|
+
const [cursor, setCursor] = useState(0)
|
|
476
|
+
const efforts = row.reasoning?.efforts ?? []
|
|
477
|
+
// The provider-default row only exists when the adapter declares no default
|
|
478
|
+
// effort: with one, the default is an advertised level already in the list.
|
|
479
|
+
const hasDefaultRow = row.reasoning !== undefined && row.reasoning.defaultEffort === undefined
|
|
480
|
+
const rows = hasDefaultRow
|
|
481
|
+
? [{ id: '', name: 'Default' }, ...efforts]
|
|
482
|
+
: efforts
|
|
483
|
+
// An absent or cleared effort is the Default row's current state.
|
|
484
|
+
const effective = current === undefined || current === '' ? '' : current
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
if (rows.length === 0) {
|
|
487
|
+
if (cursor !== 0) setCursor(0)
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
if (cursor >= rows.length) setCursor(rows.length - 1)
|
|
491
|
+
}, [rows.length, cursor])
|
|
492
|
+
useInput((input, key) => {
|
|
493
|
+
if (key.escape || input === 'q') return back()
|
|
494
|
+
if (rows.length === 0) return
|
|
495
|
+
if (key.upArrow) {
|
|
496
|
+
setCursor(cursor > 0 ? cursor - 1 : rows.length - 1)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
if (key.downArrow) {
|
|
500
|
+
setCursor(cursor < rows.length - 1 ? cursor + 1 : 0)
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
if (key.return && rows[cursor] !== undefined) {
|
|
504
|
+
select(rows[cursor]!.id)
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
return createElement(ListFrame, {
|
|
508
|
+
title: `/model — effort for ${row.providerName} · ${row.modelName}`,
|
|
509
|
+
rows: rows.map(effort => ({
|
|
510
|
+
key: effort.id,
|
|
511
|
+
text: `${effort.id === effective ? '●' : '○'} ${effort.name}${effort.id === row.reasoning?.defaultEffort ? ' · default' : ''}${effort.description === undefined ? '' : ` · ${effort.description}`}`,
|
|
512
|
+
})),
|
|
513
|
+
cursor,
|
|
514
|
+
loading: false,
|
|
515
|
+
query: '',
|
|
516
|
+
footer: '↑↓ choose · enter apply · esc/q back',
|
|
517
|
+
})
|
|
518
|
+
}
|