dsh-code 0.8.0 → 0.9.1
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 +19 -3
- package/README.md +19 -3
- package/lib/index.mjs +1175 -227
- package/lib/types/app.d.ts +13 -0
- package/lib/types/approval.d.ts +3 -1
- package/lib/types/kernel-panels.d.ts +58 -8
- package/lib/types/models.d.ts +15 -1
- package/lib/types/render/animations.d.ts +14 -2
- package/lib/types/render/projection.d.ts +2 -0
- package/lib/types/render/tool-preview.d.ts +10 -0
- package/lib/types/session-directory.d.ts +46 -2
- package/lib/types/subagents.d.ts +60 -0
- package/package.json +25 -1
- package/src/app.ts +400 -103
- package/src/approval.ts +161 -135
- package/src/index.ts +175 -8
- package/src/kernel-panels.ts +310 -30
- package/src/models.ts +26 -0
- package/src/render/animations.ts +49 -5
- package/src/render/lines.ts +236 -233
- package/src/render/projection.ts +5 -1
- package/src/render/tool-preview.ts +77 -50
- package/src/session-directory.ts +128 -6
- package/src/subagents.ts +165 -0
package/src/app.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
deepseekWaveTier,
|
|
53
53
|
deepseekWaveWordHue,
|
|
54
54
|
deepseekWaveWordVisible,
|
|
55
|
+
effortAboveHigh,
|
|
55
56
|
isOfficialDeepSeekLabel,
|
|
56
57
|
pulseFrame,
|
|
57
58
|
WAVE_BASE_DARK,
|
|
@@ -66,7 +67,8 @@ import type { ProviderSettingsDirectory, ProviderTargetView } from './provider-s
|
|
|
66
67
|
import type { QuestionSnapshot, QuestionStore } from './questions.ts'
|
|
67
68
|
import type { SkillsView, SkillRow } from './skills.ts'
|
|
68
69
|
import type { MentionCandidate } from './mentions.ts'
|
|
69
|
-
import {
|
|
70
|
+
import type { SubagentFeedView, SubagentRow } from './subagents.ts'
|
|
71
|
+
import { AgentsPanel, EffortPanel, ModePanel, HistoryPanel, PermissionPanel, PluginPanel, ResumePanel, StatuslinePanel, SubagentPanel } from './kernel-panels.ts'
|
|
70
72
|
import type { PresetRow } from './presets.ts'
|
|
71
73
|
import type { PermissionRow } from './permissions.ts'
|
|
72
74
|
import type { PluginRow } from './plugin-inventory.ts'
|
|
@@ -131,6 +133,8 @@ export interface AppProps {
|
|
|
131
133
|
approval: ApprovalStore
|
|
132
134
|
/** ask_user_question store fed by the single UI provider. */
|
|
133
135
|
questions: QuestionStore
|
|
136
|
+
/** Live subagent activity feed (child sessions of the current root). */
|
|
137
|
+
subagents: SubagentFeedView
|
|
134
138
|
/** Live slash-command descriptor list (completion candidates). */
|
|
135
139
|
commands: CommandsView
|
|
136
140
|
/** Live user-invocable skill catalog (completion candidates). */
|
|
@@ -167,6 +171,14 @@ export interface AppProps {
|
|
|
167
171
|
loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
168
172
|
/** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
|
|
169
173
|
selectModel(row: ModelRow, effortId?: string): string
|
|
174
|
+
/** The /subagent override label, '' when delegated agents follow the current model. */
|
|
175
|
+
subagentModel: string
|
|
176
|
+
/** Apply one /subagent model pick; returns the override label. */
|
|
177
|
+
setSubagentModel(row: ModelRow, effortId?: string): string
|
|
178
|
+
/** Drop the /subagent override (delegated agents follow the current model). */
|
|
179
|
+
clearSubagentModel(): void
|
|
180
|
+
/** Delete one session subtree; resolves with the outcome line. */
|
|
181
|
+
deleteSession(id: string): Promise<string>
|
|
170
182
|
/** Load provider/settings/credential facts for the optional /model provider stage. */
|
|
171
183
|
loadModelProviders?(): Promise<ProviderSettingsDirectory>
|
|
172
184
|
/** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */
|
|
@@ -193,6 +205,8 @@ export interface AppProps {
|
|
|
193
205
|
createSession(mode?: string): void
|
|
194
206
|
loadSessions(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>
|
|
195
207
|
loadSessionTranscript(id: string, signal?: AbortSignal): Promise<string>
|
|
208
|
+
/** Load this session's subagent conversations (children by lineage). */
|
|
209
|
+
loadSubagents(): Promise<readonly SessionRow[]>
|
|
196
210
|
switchSession(row: SessionRow): void
|
|
197
211
|
cancelSessionSwitch(): boolean
|
|
198
212
|
loadPlugins(): readonly PluginRow[]
|
|
@@ -691,6 +705,30 @@ function todoMark(status: TodoItem['status']): string {
|
|
|
691
705
|
return status === 'completed' ? '✓' : status === 'in_progress' ? '●' : '○'
|
|
692
706
|
}
|
|
693
707
|
|
|
708
|
+
/**
|
|
709
|
+
* One-row live subagent summary (the Codex agent status feed, compressed to
|
|
710
|
+
* the transcript's budget): running count, total, and the most recently
|
|
711
|
+
* active child's current activity. One line, never more — the full view is
|
|
712
|
+
* the /agents panel.
|
|
713
|
+
*/
|
|
714
|
+
function AgentsLine({ rows }: { rows: readonly SubagentRow[] }): ReactElement | undefined {
|
|
715
|
+
if (rows.length === 0) return undefined
|
|
716
|
+
const running = rows.filter(row => row.state !== 'done').length
|
|
717
|
+
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]!
|
|
718
|
+
const mark = newest.state === 'done' ? '✓' : newest.state === 'idle' ? '⏸' : '●'
|
|
719
|
+
return createElement(
|
|
720
|
+
Box,
|
|
721
|
+
{ paddingX: 1 },
|
|
722
|
+
createElement(
|
|
723
|
+
Text,
|
|
724
|
+
{ color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' },
|
|
725
|
+
`agents ${running} live`,
|
|
726
|
+
createElement(Text, { color: inkColor(getPalette().dim) }, ` · ${rows.length} total · /agents`),
|
|
727
|
+
createElement(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`),
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
694
732
|
/** One-row todo summary: task count cannot grow the live Ink tree. */
|
|
695
733
|
function TodoPanel({ todos }: { todos: readonly TodoItem[] }): ReactElement | undefined {
|
|
696
734
|
if (todos.length === 0) return undefined
|
|
@@ -780,9 +818,10 @@ function statusToneProps(tone: StatusTone): {
|
|
|
780
818
|
|
|
781
819
|
/** Theme anchors for the one-shot composer wave, read from the active palette
|
|
782
820
|
* so the wave stays coordinated in both themes. The flash tier runs the
|
|
783
|
-
* brand blues; the deepseek
|
|
784
|
-
* richer mix
|
|
785
|
-
*
|
|
821
|
+
* brand blues; the deepseek AND unknown tiers swap in the code sky-blue for
|
|
822
|
+
* a brighter, richer mix (the unknown tier reuses the pro palette). Codex's
|
|
823
|
+
* Wave bands carry no hue index (only hues[0] tints the row), so the accent
|
|
824
|
+
* the prompt keeps is always hues[0]. */
|
|
786
825
|
function deepseekWaveHues(tier: DeepseekWaveTier): readonly [RgbTriple, RgbTriple, RgbTriple] {
|
|
787
826
|
const palette = getPalette()
|
|
788
827
|
return tier === 'flash'
|
|
@@ -877,72 +916,139 @@ function NoticeLine({ text, tone, columns }: {
|
|
|
877
916
|
)
|
|
878
917
|
}
|
|
879
918
|
|
|
880
|
-
/**
|
|
881
|
-
|
|
919
|
+
/** One selectable approval decision (Codex approval-overlay wording). */
|
|
920
|
+
interface ApprovalOption {
|
|
921
|
+
readonly key: 'allow' | 'reject-note' | 'reject'
|
|
922
|
+
readonly label: string
|
|
923
|
+
readonly hotkey: string
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** The fixed decision list; answers stay in the binary answerer vocabulary. */
|
|
927
|
+
const APPROVAL_OPTIONS: readonly ApprovalOption[] = [
|
|
928
|
+
{ key: 'allow', label: 'Yes, proceed', hotkey: 'y' },
|
|
929
|
+
{ key: 'reject-note', label: 'No, and tell it what to do differently', hotkey: 'n' },
|
|
930
|
+
{ key: 'reject', label: 'No, continue without running it', hotkey: 'd' },
|
|
931
|
+
]
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* The approval dialog (Codex ApprovalOverlay contract): a bold question
|
|
935
|
+
* header, the bounded command body with an explicit overflow marker, a
|
|
936
|
+
* numbered option list with a `›` cursor, single-key shortcuts, and digits
|
|
937
|
+
* for direct selection. Askers queue FIFO — the count rides the header.
|
|
938
|
+
* The upstream answerer vocabulary stays binary (`allowed-once` /
|
|
939
|
+
* `rejected`): "tell it what to do differently" rejects and hands the
|
|
940
|
+
* composer back with a hint notice, exactly Codex's decline-then-type flow.
|
|
941
|
+
*/
|
|
942
|
+
function ApprovalBar({ snapshot, locked, notify }: {
|
|
943
|
+
snapshot: ApprovalSnapshot
|
|
944
|
+
locked: boolean
|
|
945
|
+
notify(text: string, tone?: NoticeTone): void
|
|
946
|
+
}): ReactElement | undefined {
|
|
882
947
|
const stdout = useStdout().stdout
|
|
883
948
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
884
|
-
const [
|
|
949
|
+
const [cursor, setCursor] = useState(0)
|
|
885
950
|
const pending = snapshot.pending
|
|
886
|
-
const active = !locked &&
|
|
887
|
-
const
|
|
951
|
+
const active = !locked && pending !== undefined && !snapshot.answered
|
|
952
|
+
const body = useMemo<readonly StyledLine[]>(() => pending === undefined || pending.command === ''
|
|
888
953
|
? []
|
|
889
|
-
: [
|
|
890
|
-
...styledLines([lineSegment(pending.headline, 'warn')], viewport.contentColumns),
|
|
891
|
-
...(pending.command === '' ? [] : textLines(` ${pending.command}`, viewport.contentColumns, 'dim')),
|
|
892
|
-
], [pending, viewport.contentColumns])
|
|
893
|
-
const visibleScroll = clampScroll(scroll, content.length, viewport.bodyRows)
|
|
954
|
+
: textLines(pending.command, viewport.contentColumns, 'dim'), [pending, viewport.contentColumns])
|
|
894
955
|
|
|
895
956
|
useEffect(() => {
|
|
896
|
-
|
|
957
|
+
setCursor(0)
|
|
897
958
|
}, [pending])
|
|
898
959
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
960
|
+
const decide = (option: ApprovalOption): void => {
|
|
961
|
+
const ask = snapshot.pending
|
|
962
|
+
if (ask === undefined || snapshot.answered) return
|
|
963
|
+
if (option.key === 'allow') {
|
|
964
|
+
ask.answer('allowed-once')
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
ask.answer('rejected')
|
|
968
|
+
if (option.key === 'reject-note') {
|
|
969
|
+
notify('rejected — type below what it should do differently (it steers the next step)', 'warning')
|
|
970
|
+
}
|
|
971
|
+
}
|
|
902
972
|
|
|
903
973
|
useInput((input, key) => {
|
|
904
|
-
|
|
974
|
+
const ask = snapshot.pending
|
|
975
|
+
if (ask === undefined || snapshot.answered) return
|
|
905
976
|
if (key.upArrow) {
|
|
906
|
-
|
|
977
|
+
setCursor(current => (current + APPROVAL_OPTIONS.length - 1) % APPROVAL_OPTIONS.length)
|
|
907
978
|
return
|
|
908
979
|
}
|
|
909
980
|
if (key.downArrow) {
|
|
910
|
-
|
|
981
|
+
setCursor(current => (current + 1) % APPROVAL_OPTIONS.length)
|
|
911
982
|
return
|
|
912
983
|
}
|
|
913
|
-
if (key.
|
|
914
|
-
|
|
984
|
+
if (key.return) {
|
|
985
|
+
decide(APPROVAL_OPTIONS[cursor]!)
|
|
915
986
|
return
|
|
916
987
|
}
|
|
917
|
-
if (key.
|
|
918
|
-
|
|
988
|
+
if (key.escape) {
|
|
989
|
+
decide(APPROVAL_OPTIONS[2]!)
|
|
919
990
|
return
|
|
920
991
|
}
|
|
921
|
-
if (snapshot.answered) return
|
|
922
992
|
if (input === 'y' || input === 'Y') {
|
|
923
|
-
|
|
993
|
+
decide(APPROVAL_OPTIONS[0]!)
|
|
924
994
|
return
|
|
925
995
|
}
|
|
926
996
|
if (input === 'n' || input === 'N') {
|
|
927
|
-
|
|
997
|
+
decide(APPROVAL_OPTIONS[1]!)
|
|
998
|
+
return
|
|
999
|
+
}
|
|
1000
|
+
if (input === 'd' || input === 'D') {
|
|
1001
|
+
decide(APPROVAL_OPTIONS[2]!)
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
if (/^[1-9]$/u.test(input)) {
|
|
1005
|
+
const index = Number(input) - 1
|
|
1006
|
+
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index]!)
|
|
928
1007
|
}
|
|
929
1008
|
}, { isActive: active })
|
|
930
|
-
|
|
1009
|
+
|
|
1010
|
+
if (pending === undefined) return undefined
|
|
931
1011
|
if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
|
|
1012
|
+
const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : ''
|
|
932
1013
|
if (viewport.compact) {
|
|
933
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
1014
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`approval${queuedSuffix} · enter/y allow · esc/n reject`, viewport.contentColumns))
|
|
934
1015
|
}
|
|
935
|
-
|
|
1016
|
+
// Body budget: title + options + footer consume fixed rows; the command
|
|
1017
|
+
// preview shrinks with an explicit overflow marker (Codex's "[… N lines]").
|
|
1018
|
+
const reservedRows = 3 + APPROVAL_OPTIONS.length
|
|
1019
|
+
const bodyBudget = Math.max(1, viewport.bodyRows - reservedRows)
|
|
1020
|
+
const visibleBody = body.slice(0, bodyBudget)
|
|
1021
|
+
const overflow = body.length - visibleBody.length
|
|
936
1022
|
return createElement(
|
|
937
1023
|
Box,
|
|
938
1024
|
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().warn) },
|
|
939
|
-
createElement(
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1025
|
+
createElement(
|
|
1026
|
+
Text,
|
|
1027
|
+
{ color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' },
|
|
1028
|
+
truncateColumns(`${pending.headline}${queuedSuffix}`, viewport.contentColumns),
|
|
1029
|
+
),
|
|
1030
|
+
createElement(PanelGap, { visible: viewport.gapRows > 0 && body.length > 0 }),
|
|
1031
|
+
...visibleBody.map((line, index) => createElement(StyledRows, { key: `body-${index}`, lines: [line] })),
|
|
1032
|
+
...(overflow > 0
|
|
1033
|
+
? [createElement(Text, { key: 'overflow', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(`… +${overflow} more lines · ctrl+o shows the full call in the transcript`, viewport.contentColumns))]
|
|
1034
|
+
: []),
|
|
1035
|
+
...(body.length > 0 ? [createElement(PanelGap, { visible: viewport.gapRows > 0 })] : []),
|
|
1036
|
+
...APPROVAL_OPTIONS.map((option, index) => {
|
|
1037
|
+
const selected = !snapshot.answered && index === cursor
|
|
1038
|
+
return createElement(
|
|
1039
|
+
Text,
|
|
1040
|
+
{
|
|
1041
|
+
key: option.key,
|
|
1042
|
+
color: selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().text),
|
|
1043
|
+
bold: selected || undefined,
|
|
1044
|
+
wrap: 'truncate-end',
|
|
1045
|
+
},
|
|
1046
|
+
truncateColumns(`${selected ? '›' : ' '} ${index + 1}. ${option.label} (${option.hotkey})`, viewport.contentColumns),
|
|
1047
|
+
)
|
|
1048
|
+
}),
|
|
1049
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(snapshot.answered
|
|
944
1050
|
? 'submitted…'
|
|
945
|
-
: '
|
|
1051
|
+
: '↑↓ choose · enter confirm · y/n/d quick · esc reject', viewport.contentColumns)),
|
|
946
1052
|
)
|
|
947
1053
|
}
|
|
948
1054
|
|
|
@@ -1217,9 +1323,11 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1217
1323
|
}
|
|
1218
1324
|
|
|
1219
1325
|
/** The /model panel: a scrolling list over the advisory model directory. */
|
|
1220
|
-
function ModelPanel({ directory, error, onSelect, onProviders, onRetry, onClose }: {
|
|
1326
|
+
function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry, onClose }: {
|
|
1221
1327
|
directory: ModelDirectory | undefined
|
|
1222
1328
|
error: string | undefined
|
|
1329
|
+
/** `provider/model` label of the applied model: the cursor lands on it once. */
|
|
1330
|
+
current?: string
|
|
1223
1331
|
onSelect(row: ModelRow): void
|
|
1224
1332
|
onProviders?(): void
|
|
1225
1333
|
onRetry(): void
|
|
@@ -1229,14 +1337,27 @@ function ModelPanel({ directory, error, onSelect, onProviders, onRetry, onClose
|
|
|
1229
1337
|
const stdout = useStdout().stdout
|
|
1230
1338
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
1231
1339
|
const rows = directory?.rows ?? []
|
|
1340
|
+
const positioned = useRef(false)
|
|
1232
1341
|
|
|
1233
1342
|
useEffect(() => {
|
|
1234
|
-
|
|
1235
|
-
|
|
1343
|
+
// Open ON the applied model (Codex resumes the previous pick): the first
|
|
1344
|
+
// non-empty directory positions the cursor once, never on later refreshes.
|
|
1345
|
+
if (positioned.current || rows.length === 0 || current === undefined) {
|
|
1346
|
+
if (rows.length === 0) {
|
|
1347
|
+
if (cursor !== 0) setCursor(0)
|
|
1348
|
+
return
|
|
1349
|
+
}
|
|
1350
|
+
if (cursor >= rows.length) setCursor(rows.length - 1)
|
|
1236
1351
|
return
|
|
1237
1352
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1353
|
+
const index = rows.findIndex(row => `${row.provider}/${row.model}` === current)
|
|
1354
|
+
if (index >= 0) {
|
|
1355
|
+
positioned.current = true
|
|
1356
|
+
setCursor(index)
|
|
1357
|
+
} else if (cursor >= rows.length) {
|
|
1358
|
+
setCursor(Math.max(0, rows.length - 1))
|
|
1359
|
+
}
|
|
1360
|
+
}, [rows, cursor, current])
|
|
1240
1361
|
|
|
1241
1362
|
useInput((input, key) => {
|
|
1242
1363
|
if (key.escape || input === 'q') {
|
|
@@ -1685,6 +1806,9 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
1685
1806
|
createElement(Box, { key: 'local-statusline' }, row('/statusline', 'customize the status line items')),
|
|
1686
1807
|
createElement(Box, { key: 'local-theme' }, row('/theme', 'switch the color theme')),
|
|
1687
1808
|
createElement(Box, { key: 'local-history' }, row('/history', 'search and recall past prompts')),
|
|
1809
|
+
createElement(Box, { key: 'local-agents' }, row('/agents', 'inspect subagent sessions of this conversation')),
|
|
1810
|
+
createElement(Box, { key: 'local-subagent' }, row('/subagent', 'choose the model delegated subagents run on')),
|
|
1811
|
+
createElement(Box, { key: 'local-delete' }, row('/delete', 'delete a session and its subagent threads')),
|
|
1688
1812
|
createElement(Box, { key: 'local-clear' }, row('/clear', 'clear the screen')),
|
|
1689
1813
|
createElement(Box, { key: 'local-export' }, row('/export', 'export the transcript to markdown (/export [path])')),
|
|
1690
1814
|
createElement(Box, { key: 'local-title' }, row('/title', 'rename this session (/title <text>)')),
|
|
@@ -2021,6 +2145,9 @@ export function completionCandidates(
|
|
|
2021
2145
|
{ label: '/statusline', description: 'customize the status line', origin: 'command' },
|
|
2022
2146
|
{ label: '/theme', description: 'switch the color theme', origin: 'command' },
|
|
2023
2147
|
{ label: '/history', description: 'search and recall past prompts', origin: 'command' },
|
|
2148
|
+
{ label: '/agents', description: 'inspect subagent sessions of this conversation', origin: 'command' },
|
|
2149
|
+
{ label: '/subagent', description: 'choose the model delegated subagents run on', origin: 'command' },
|
|
2150
|
+
{ label: '/delete', description: 'delete a session and its subagent threads', origin: 'command' },
|
|
2024
2151
|
{ label: '/clear', description: 'clear the screen', origin: 'command' },
|
|
2025
2152
|
{ label: '/export', description: 'export the transcript to markdown', origin: 'command' },
|
|
2026
2153
|
{ label: '/title', description: 'rename this session', origin: 'command' },
|
|
@@ -2123,7 +2250,7 @@ function CompletionMenu({ active, mention, index, rows }: {
|
|
|
2123
2250
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
2124
2251
|
* box passes every key through untouched.
|
|
2125
2252
|
*/
|
|
2126
|
-
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }: {
|
|
2253
|
+
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, openAgents, openSubagent, openDelete, deleteConfirm, confirmDelete, cancelDelete, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }: {
|
|
2127
2254
|
active: boolean
|
|
2128
2255
|
frozen: boolean
|
|
2129
2256
|
busy: boolean
|
|
@@ -2143,6 +2270,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2143
2270
|
openStatusline(): void
|
|
2144
2271
|
openTheme(): void
|
|
2145
2272
|
openHistory(): void
|
|
2273
|
+
/** Open the /agents panel (live subagent feed + transcript entry). */
|
|
2274
|
+
openAgents(): void
|
|
2275
|
+
/** Open the /subagent model panel. */
|
|
2276
|
+
openSubagent(): void
|
|
2277
|
+
/** Open the /resume picker in delete mode, optionally pre-armed on one id. */
|
|
2278
|
+
openDelete(id?: string): void
|
|
2279
|
+
/** The row id awaiting y/n in this box, when a deletion is pending. */
|
|
2280
|
+
deleteConfirm?: string
|
|
2281
|
+
/** Confirm the pending deletion (y in the box). */
|
|
2282
|
+
confirmDelete(): void
|
|
2283
|
+
/** Cancel the pending deletion (any other key in the box). */
|
|
2284
|
+
cancelDelete(): void
|
|
2146
2285
|
createSession(mode?: string): void
|
|
2147
2286
|
cancelSessionSwitch(): boolean
|
|
2148
2287
|
notify(text: string, tone?: NoticeTone): void
|
|
@@ -2170,9 +2309,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2170
2309
|
historyFill: { text: string; index: number } | undefined
|
|
2171
2310
|
/** Marks the accepted entry consumed (called after the fill is applied). */
|
|
2172
2311
|
historyConsumed(): void
|
|
2173
|
-
/** DeepSeek easter-egg wave tier of the applied
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2312
|
+
/** DeepSeek easter-egg wave tier of the applied route (null otherwise):
|
|
2313
|
+
* official DeepSeek models drive their flash/pro tiers, non-DeepSeek
|
|
2314
|
+
* models running an effort above high drive the "Into the Unknown"
|
|
2315
|
+
* variant. Drives the persistent prompt glyph/accent and the sparkle
|
|
2316
|
+
* tier. */
|
|
2176
2317
|
waveTier: DeepseekWaveTier | null
|
|
2177
2318
|
/** The ignition style running, if any: Wave / Aurora / Pulse. */
|
|
2178
2319
|
waveStyle: DeepseekWaveStyle | null
|
|
@@ -2289,9 +2430,53 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2289
2430
|
}))
|
|
2290
2431
|
: candidates
|
|
2291
2432
|
|
|
2433
|
+
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
2434
|
+
const acceptMenuCandidate = (): void => {
|
|
2435
|
+
if (mentionActive && mentionToken !== undefined) {
|
|
2436
|
+
const row = mentionRows[completionIndex % mentionRows.length]
|
|
2437
|
+
if (row !== undefined) {
|
|
2438
|
+
// Session rows carry the canonical @[label](dsh-session:…) token;
|
|
2439
|
+
// file rows insert `@path` (directories keep their trailing slash).
|
|
2440
|
+
const insertion = row.label.startsWith('@')
|
|
2441
|
+
? row.label
|
|
2442
|
+
: `@${row.label}${row.kind === 'directory' ? '/' : ''}`
|
|
2443
|
+
setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor))
|
|
2444
|
+
setCursor(mentionToken.start + insertion.length)
|
|
2445
|
+
}
|
|
2446
|
+
} else if (pathActive) {
|
|
2447
|
+
const row = pathRows[completionIndex % Math.max(1, pathRows.length)]
|
|
2448
|
+
if (row !== undefined) {
|
|
2449
|
+
// Bare path completion replaces the typed token with the chosen
|
|
2450
|
+
// workspace path (directories keep their trailing slash).
|
|
2451
|
+
const insertion = row.kind === 'directory' ? `${row.label}/` : row.label
|
|
2452
|
+
setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor))
|
|
2453
|
+
setCursor(pathTokenStart + insertion.length)
|
|
2454
|
+
}
|
|
2455
|
+
} else {
|
|
2456
|
+
const candidate = candidates[completionIndex % candidates.length]
|
|
2457
|
+
if (candidate !== undefined) {
|
|
2458
|
+
setValue(`${candidate.label} `)
|
|
2459
|
+
setCursor(candidate.label.length + 1)
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
setCompletionIndex(0)
|
|
2463
|
+
setDismissedMenuValue(undefined)
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2292
2466
|
useInput((input, key) => {
|
|
2293
2467
|
// Modal ownership: approval/question/model dialogs consume all keys.
|
|
2294
2468
|
if (!active) return
|
|
2469
|
+
// Deletion confirm owns the box: y proceeds, anything else cancels.
|
|
2470
|
+
// Typed in the INPUT BOX (codex delete-confirm): the keystroke is echoed
|
|
2471
|
+
// as the box's own prompt, not an invisible panel keypress.
|
|
2472
|
+
if (deleteConfirm !== undefined) {
|
|
2473
|
+
if (input === 'y' || input === 'Y') {
|
|
2474
|
+
confirmDelete()
|
|
2475
|
+
} else {
|
|
2476
|
+
cancelDelete()
|
|
2477
|
+
}
|
|
2478
|
+
return
|
|
2479
|
+
}
|
|
2295
2480
|
// Shift+Tab cycles the permission preset (Claude-Code convention).
|
|
2296
2481
|
if (key.tab && key.shift) {
|
|
2297
2482
|
try {
|
|
@@ -2363,6 +2548,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2363
2548
|
setDismissedMenuValue(undefined)
|
|
2364
2549
|
return
|
|
2365
2550
|
}
|
|
2551
|
+
// Enter on an open completion menu accepts the highlighted candidate
|
|
2552
|
+
// (Codex list parity: Tab and Enter are both accept keys — many users
|
|
2553
|
+
// never discover Tab) — UNLESS the draft already spells one candidate
|
|
2554
|
+
// exactly, in which case Enter submits it (typing a full "/effort" and
|
|
2555
|
+
// pressing return must run the command, not re-accept its own text).
|
|
2556
|
+
if (menuActive) {
|
|
2557
|
+
const exactSlash = !mentionActive && !pathActive && candidates.some(candidate => candidate.label === value)
|
|
2558
|
+
if (!exactSlash) {
|
|
2559
|
+
acceptMenuCandidate()
|
|
2560
|
+
return
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2366
2563
|
const text = value.trim()
|
|
2367
2564
|
setValue('')
|
|
2368
2565
|
setCursor(0)
|
|
@@ -2461,6 +2658,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2461
2658
|
openHistory()
|
|
2462
2659
|
return
|
|
2463
2660
|
}
|
|
2661
|
+
if (text === '/agents') {
|
|
2662
|
+
openAgents()
|
|
2663
|
+
return
|
|
2664
|
+
}
|
|
2665
|
+
if (text === '/subagent') {
|
|
2666
|
+
openSubagent()
|
|
2667
|
+
return
|
|
2668
|
+
}
|
|
2669
|
+
if (text === '/delete' || text.startsWith('/delete ')) {
|
|
2670
|
+
openDelete(text.slice(7).trim())
|
|
2671
|
+
return
|
|
2672
|
+
}
|
|
2464
2673
|
if (busy && !text.startsWith('/')) {
|
|
2465
2674
|
// A running turn is steered, not blocked: the inbox delivers this
|
|
2466
2675
|
// text at the next step boundary (Esc/Ctrl+C still cancels outright).
|
|
@@ -2506,35 +2715,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2506
2715
|
return
|
|
2507
2716
|
}
|
|
2508
2717
|
if (key.tab && menuActive) {
|
|
2509
|
-
|
|
2510
|
-
const row = mentionRows[completionIndex % mentionRows.length]
|
|
2511
|
-
if (row !== undefined) {
|
|
2512
|
-
// Session rows carry the canonical @[label](dsh-session:…) token;
|
|
2513
|
-
// file rows insert `@path` (directories keep their trailing slash).
|
|
2514
|
-
const insertion = row.label.startsWith('@')
|
|
2515
|
-
? row.label
|
|
2516
|
-
: `@${row.label}${row.kind === 'directory' ? '/' : ''}`
|
|
2517
|
-
setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor))
|
|
2518
|
-
setCursor(mentionToken.start + insertion.length)
|
|
2519
|
-
}
|
|
2520
|
-
} else if (pathActive) {
|
|
2521
|
-
const row = pathRows[completionIndex % Math.max(1, pathRows.length)]
|
|
2522
|
-
if (row !== undefined) {
|
|
2523
|
-
// Bare path completion replaces the typed token with the chosen
|
|
2524
|
-
// workspace path (directories keep their trailing slash).
|
|
2525
|
-
const insertion = row.kind === 'directory' ? `${row.label}/` : row.label
|
|
2526
|
-
setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor))
|
|
2527
|
-
setCursor(pathTokenStart + insertion.length)
|
|
2528
|
-
}
|
|
2529
|
-
} else {
|
|
2530
|
-
const candidate = candidates[completionIndex % candidates.length]
|
|
2531
|
-
if (candidate !== undefined) {
|
|
2532
|
-
setValue(`${candidate.label} `)
|
|
2533
|
-
setCursor(candidate.label.length + 1)
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
setCompletionIndex(0)
|
|
2537
|
-
setDismissedMenuValue(undefined)
|
|
2718
|
+
acceptMenuCandidate()
|
|
2538
2719
|
return
|
|
2539
2720
|
}
|
|
2540
2721
|
if (key.backspace || key.delete) {
|
|
@@ -2595,7 +2776,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2595
2776
|
// starts the sweep whenever that pair changes (App picks a NEW random style
|
|
2596
2777
|
// for every replay — including effort changes on the same route — so the
|
|
2597
2778
|
// pair always differs when a new wave should run) and stops it when the
|
|
2598
|
-
//
|
|
2779
|
+
// route leaves every wave tier (tier becomes null).
|
|
2599
2780
|
const [waveTick, setWaveTick] = useState<number | null>(null)
|
|
2600
2781
|
const wavePrevious = useRef<{ tier: DeepseekWaveTier | null; style: DeepseekWaveStyle | null }>({ tier: null, style: null })
|
|
2601
2782
|
useEffect(() => {
|
|
@@ -2632,6 +2813,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2632
2813
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0])
|
|
2633
2814
|
const promptGlyph = waveTier === 'flash' ? '›' : waveTier === 'deepseek' ? '»' : '❯'
|
|
2634
2815
|
if (frozen) {
|
|
2816
|
+
// A pending deletion turns the box into the confirm prompt: the y/n is
|
|
2817
|
+
// typed HERE, with a readable warn-styled hint instead of a dim footer.
|
|
2818
|
+
if (deleteConfirm !== undefined) {
|
|
2819
|
+
return createElement(
|
|
2820
|
+
Box,
|
|
2821
|
+
{ width: Math.max(1, columns - 1), borderStyle: 'round', borderColor: inkColor(getPalette().warn), paddingX: 1 },
|
|
2822
|
+
createElement(
|
|
2823
|
+
Text,
|
|
2824
|
+
{ wrap: 'truncate-end' },
|
|
2825
|
+
createElement(Text, { color: inkColor(getPalette().warn), bold: true }, '❯ '),
|
|
2826
|
+
createElement(Text, { color: inkColor(getPalette().warn), bold: true }, 'y delete · any other key cancels'),
|
|
2827
|
+
),
|
|
2828
|
+
)
|
|
2829
|
+
}
|
|
2635
2830
|
const frozen = value === ''
|
|
2636
2831
|
? 'type a message'
|
|
2637
2832
|
: verboseLine(value, Math.max(1, columns - 6))
|
|
@@ -2713,11 +2908,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2713
2908
|
while (cells.length < contentWidth) {
|
|
2714
2909
|
cells.push({ char: ' ', backgroundColor: waveBg(cells.length) })
|
|
2715
2910
|
}
|
|
2716
|
-
// The
|
|
2717
|
-
//
|
|
2718
|
-
//
|
|
2911
|
+
// The wordmark rides the wave's middle: `deepseek` on the official
|
|
2912
|
+
// tiers, `Into the Unknown` on the non-DeepSeek high-effort variant —
|
|
2913
|
+
// in the tier's cycled hues, placed in the row's mid-section and only
|
|
2914
|
+
// over blank or placeholder cells — real draft text is never covered.
|
|
2719
2915
|
if (deepseekWaveWordVisible(waveTick!, waveTier!, style)) {
|
|
2720
|
-
const word = 'deepseek'
|
|
2916
|
+
const word = waveTier === 'unknown' ? 'Into the Unknown' : 'deepseek'
|
|
2721
2917
|
const start = Math.max(2, Math.floor((contentWidth - word.length) / 2))
|
|
2722
2918
|
let clear = true
|
|
2723
2919
|
for (let at = 0; at < word.length; at += 1) {
|
|
@@ -2734,9 +2930,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2734
2930
|
}
|
|
2735
2931
|
}
|
|
2736
2932
|
}
|
|
2737
|
-
// The tail sparkles belong to the Wave style's
|
|
2738
|
-
//
|
|
2739
|
-
|
|
2933
|
+
// The tail sparkles belong to the Wave style's pro tiers only (the
|
|
2934
|
+
// deepseek and unknown tiers share the Ultra parameters — Codex paints
|
|
2935
|
+
// spark_frame on Wave+Ultra).
|
|
2936
|
+
if ((waveTier === 'deepseek' || waveTier === 'unknown') && style === 'wave') {
|
|
2740
2937
|
const spark = deepseekWaveSpark(waveTick!)
|
|
2741
2938
|
if (spark !== null) {
|
|
2742
2939
|
const last = cells[cells.length - 1]
|
|
@@ -2950,16 +3147,18 @@ export function App(props: AppProps): ReactElement {
|
|
|
2950
3147
|
const [effortFor, setEffortFor] = useState<ModelRow | undefined>(undefined)
|
|
2951
3148
|
/** Effective reasoning effort, shown in the /model picker and switch notice. */
|
|
2952
3149
|
const [effortLabel, setEffortLabel] = useState<string | undefined>(props.effort)
|
|
2953
|
-
/** DeepSeek easter egg: switching INTO an official DeepSeek route
|
|
2954
|
-
*
|
|
2955
|
-
*
|
|
2956
|
-
*
|
|
2957
|
-
*
|
|
2958
|
-
*
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
3150
|
+
/** DeepSeek easter egg: switching INTO an official DeepSeek route — or
|
|
3151
|
+
* onto a NON-DeepSeek model running a reasoning effort strictly above
|
|
3152
|
+
* high — plays one of Codex's three ignition styles (Wave / Aurora /
|
|
3153
|
+
* Pulse, picked at random without repeating) across the composer's
|
|
3154
|
+
* padded band (33ms tick, per-style durations), then the band returns
|
|
3155
|
+
* to static while the prompt marker keeps the tier accent. The trigger
|
|
3156
|
+
* follows the applied model label (what the status bar actually shows),
|
|
3157
|
+
* never the initial paint, and the tier is derived from the label and
|
|
3158
|
+
* cached at the switch. The 33ms tick itself lives inside Input, so the
|
|
3159
|
+
* sweep re-renders only the composer row, not the whole tree, at 30fps;
|
|
3160
|
+
* App owns the rarely-changing tier/style and Input starts the sweep
|
|
3161
|
+
* whenever that pair changes. */
|
|
2963
3162
|
const [waveTier, setWaveTier] = useState<DeepseekWaveTier | null>(null)
|
|
2964
3163
|
const [waveStyle, setWaveStyle] = useState<DeepseekWaveStyle | null>(null)
|
|
2965
3164
|
const previousModel = useRef<string | undefined>(undefined)
|
|
@@ -2969,18 +3168,23 @@ export function App(props: AppProps): ReactElement {
|
|
|
2969
3168
|
const previous = previousModel.current
|
|
2970
3169
|
previousModel.current = modelLabel
|
|
2971
3170
|
// The wave replays when the applied model changes OR its effort level
|
|
2972
|
-
// changes
|
|
2973
|
-
//
|
|
3171
|
+
// changes (Codex replays the ignition on effort changes too). Official
|
|
3172
|
+
// DeepSeek routes run their flash/pro tiers; a NON-DeepSeek model
|
|
3173
|
+
// running a reasoning effort STRICTLY above high runs the "Into the
|
|
3174
|
+
// Unknown" variant — the deepseek tier's exact motion with a different
|
|
3175
|
+
// wordmark. Any other non-DeepSeek route stays static.
|
|
2974
3176
|
const effortChanged = previousEffort.current !== effortLabel
|
|
2975
3177
|
previousEffort.current = effortLabel
|
|
2976
3178
|
const modelChanged = previous !== undefined && previous !== modelLabel
|
|
2977
|
-
|
|
3179
|
+
const official = isOfficialDeepSeekLabel(modelLabel)
|
|
3180
|
+
const unknownTrigger = !official && effortAboveHigh(effortLabel)
|
|
3181
|
+
if (!official && !unknownTrigger) {
|
|
2978
3182
|
setWaveTier(null)
|
|
2979
3183
|
setWaveStyle(null)
|
|
2980
3184
|
return
|
|
2981
3185
|
}
|
|
2982
3186
|
if (modelChanged || effortChanged) {
|
|
2983
|
-
setWaveTier(deepseekWaveTier(modelLabel))
|
|
3187
|
+
setWaveTier(official ? deepseekWaveTier(modelLabel) : 'unknown')
|
|
2984
3188
|
const nextStyle = deepseekWaveStyleRandom(previousStyle.current)
|
|
2985
3189
|
previousStyle.current = nextStyle
|
|
2986
3190
|
setWaveStyle(nextStyle)
|
|
@@ -3053,6 +3257,34 @@ export function App(props: AppProps): ReactElement {
|
|
|
3053
3257
|
const [statuslineItems, setStatuslineItems] = useState<readonly StatusItemId[]>(() => parseStatuslineItems(props.statusline))
|
|
3054
3258
|
const [themeOpen, setThemeOpen] = useState(false)
|
|
3055
3259
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
3260
|
+
const [agentsOpen, setAgentsOpen] = useState(false)
|
|
3261
|
+
const [subagentOpen, setSubagentOpen] = useState(false)
|
|
3262
|
+
/** /delete state: delete-mode hint plus an optional pre-armed row id. */
|
|
3263
|
+
const [resumeDelete, setResumeDelete] = useState<{ mode: boolean; id?: string }>({ mode: false })
|
|
3264
|
+
/** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
|
|
3265
|
+
* composer takes the keys, the resume panel yields until it settles. */
|
|
3266
|
+
const [deleteConfirmId, setDeleteConfirmId] = useState<string | undefined>(undefined)
|
|
3267
|
+
/** Bumped after a deletion so the /resume listing reloads immediately. */
|
|
3268
|
+
const [deleteReloadToken, setDeleteReloadToken] = useState(0)
|
|
3269
|
+
const requestDelete = useCallback((row: SessionRow): void => {
|
|
3270
|
+
setDeleteConfirmId(row.id)
|
|
3271
|
+
}, [])
|
|
3272
|
+
const cancelDelete = useCallback((): void => {
|
|
3273
|
+
setDeleteConfirmId(undefined)
|
|
3274
|
+
}, [])
|
|
3275
|
+
const confirmDelete = useCallback((): void => {
|
|
3276
|
+
const id = deleteConfirmId
|
|
3277
|
+
if (id === undefined) return
|
|
3278
|
+
setDeleteConfirmId(undefined)
|
|
3279
|
+
void props.deleteSession(id).then(outcome => {
|
|
3280
|
+
notify(outcome)
|
|
3281
|
+
// Keep the picker open and reload: a successful deletion must vanish
|
|
3282
|
+
// from the list immediately, not look like a no-op.
|
|
3283
|
+
setDeleteReloadToken(token => token + 1)
|
|
3284
|
+
}, (reason: unknown) => {
|
|
3285
|
+
notify(`delete failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
3286
|
+
})
|
|
3287
|
+
}, [deleteConfirmId, props.deleteSession, notify])
|
|
3056
3288
|
/** The /history panel's accepted entry: text plus its recall-space index. */
|
|
3057
3289
|
const [historyFill, setHistoryFill] = useState<{ text: string; index: number } | undefined>(undefined)
|
|
3058
3290
|
/** Submissions recorded in this process (Codex local history; persistent file stays in the runner). */
|
|
@@ -3089,10 +3321,16 @@ export function App(props: AppProps): ReactElement {
|
|
|
3089
3321
|
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
|
3090
3322
|
const approvalSnapshot = useSyncExternalStore(props.approval.subscribe, props.approval.getSnapshot)
|
|
3091
3323
|
const questionSnapshot = useSyncExternalStore(props.questions.subscribe, props.questions.getSnapshot)
|
|
3324
|
+
const agentRows = useSyncExternalStore(props.subagents.subscribe, props.subagents.getSnapshot)
|
|
3092
3325
|
const approvalPending = approvalSnapshot.pending !== undefined
|
|
3093
3326
|
const questionPending = questionSnapshot.pending !== undefined
|
|
3094
3327
|
// While any modal owns the keys, the prompt box passes everything through.
|
|
3095
|
-
|
|
3328
|
+
// While a deletion waits for y/n, the composer takes the keys (the resume
|
|
3329
|
+
// panel yields): the confirm is typed IN the input box, not as an invisible
|
|
3330
|
+
// panel keypress.
|
|
3331
|
+
const inputActive = deleteConfirmId !== undefined
|
|
3332
|
+
? !approvalPending && !questionPending
|
|
3333
|
+
: !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending
|
|
3096
3334
|
|
|
3097
3335
|
// Human questions outrank local inspectors. Close the lower modal instead
|
|
3098
3336
|
// of leaving an approval/question visible but keyboard-locked behind it.
|
|
@@ -3110,6 +3348,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
3110
3348
|
setStatuslineOpen(false)
|
|
3111
3349
|
setThemeOpen(false)
|
|
3112
3350
|
setHistoryOpen(false)
|
|
3351
|
+
setAgentsOpen(false)
|
|
3352
|
+
setSubagentOpen(false)
|
|
3353
|
+
setDeleteConfirmId(undefined)
|
|
3113
3354
|
setVerboseOpen(false)
|
|
3114
3355
|
}, [approvalPending, questionPending])
|
|
3115
3356
|
|
|
@@ -3213,9 +3454,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
3213
3454
|
? Math.max(1, Math.floor(streamRows / 3))
|
|
3214
3455
|
: 1
|
|
3215
3456
|
const answerRows = view.streaming === '' ? 0 : Math.max(1, streamRows - reasoningRows)
|
|
3216
|
-
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending
|
|
3457
|
+
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending
|
|
3217
3458
|
const inspectorVisible = verboseOpen && !approvalPending && !questionPending
|
|
3218
|
-
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || inspectorVisible || approvalPending || questionPending
|
|
3459
|
+
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || inspectorVisible || approvalPending || questionPending
|
|
3219
3460
|
const closeInspector = useCallback((): void => {
|
|
3220
3461
|
setVerboseOpen(false)
|
|
3221
3462
|
}, [])
|
|
@@ -3322,6 +3563,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
3322
3563
|
})
|
|
3323
3564
|
} else if (effortFor !== undefined) {
|
|
3324
3565
|
modelSurface = createElement(EffortPanel, {
|
|
3566
|
+
// Keyed per row: switching models remounts the stage so its cursor
|
|
3567
|
+
// initializes on the new model's effective effort.
|
|
3568
|
+
key: `${effortFor.provider}/${effortFor.model}`,
|
|
3325
3569
|
row: effortFor,
|
|
3326
3570
|
current: effortLabel,
|
|
3327
3571
|
select: (effortId: string) => applyModel(effortFor, effortId),
|
|
@@ -3331,6 +3575,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
3331
3575
|
modelSurface = createElement(ModelPanel, {
|
|
3332
3576
|
directory,
|
|
3333
3577
|
error: modelError,
|
|
3578
|
+
current: modelLabel,
|
|
3334
3579
|
onSelect: (row: ModelRow) => {
|
|
3335
3580
|
// A model advertising several levels opens the effort stage first;
|
|
3336
3581
|
// one advertised level is its only option, while no capability uses
|
|
@@ -3387,8 +3632,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
3387
3632
|
)
|
|
3388
3633
|
: undefined,
|
|
3389
3634
|
transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : undefined,
|
|
3635
|
+
transcriptVisible ? createElement(AgentsLine, { rows: agentRows }) : undefined,
|
|
3390
3636
|
createElement(QuestionBar, { store: props.questions, snapshot: questionSnapshot, locked: false }),
|
|
3391
|
-
createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending }),
|
|
3637
|
+
createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending, notify }),
|
|
3392
3638
|
modelSurface,
|
|
3393
3639
|
helpOpen && !approvalPending && !questionPending
|
|
3394
3640
|
? createElement(HelpPanel, {
|
|
@@ -3441,6 +3687,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
3441
3687
|
currentCwd: props.workspaceRoot,
|
|
3442
3688
|
load: props.loadSessions,
|
|
3443
3689
|
readTranscript: props.loadSessionTranscript,
|
|
3690
|
+
requestDelete,
|
|
3691
|
+
deleteConfirmId,
|
|
3692
|
+
reloadToken: deleteReloadToken,
|
|
3693
|
+
deleteMode: resumeDelete.mode,
|
|
3444
3694
|
select: (row: SessionRow) => { props.switchSession(row); setResumeOpen(false) },
|
|
3445
3695
|
close: () => setResumeOpen(false),
|
|
3446
3696
|
})
|
|
@@ -3483,6 +3733,37 @@ export function App(props: AppProps): ReactElement {
|
|
|
3483
3733
|
close: () => setHistoryOpen(false),
|
|
3484
3734
|
})
|
|
3485
3735
|
: undefined,
|
|
3736
|
+
agentsOpen && !approvalPending && !questionPending
|
|
3737
|
+
? createElement(AgentsPanel, {
|
|
3738
|
+
live: agentRows,
|
|
3739
|
+
load: props.loadSubagents,
|
|
3740
|
+
readTranscript: props.loadSessionTranscript,
|
|
3741
|
+
close: () => setAgentsOpen(false),
|
|
3742
|
+
})
|
|
3743
|
+
: undefined,
|
|
3744
|
+
subagentOpen && !approvalPending && !questionPending
|
|
3745
|
+
? createElement(SubagentPanel, {
|
|
3746
|
+
current: props.subagentModel,
|
|
3747
|
+
load: props.loadModels,
|
|
3748
|
+
pick: (row: ModelRow, effortId?: string) => {
|
|
3749
|
+
try {
|
|
3750
|
+
// The runner's label already carries the effort suffix
|
|
3751
|
+
// (`provider/model@effort`), so no second append here.
|
|
3752
|
+
const label = props.setSubagentModel(row, effortId)
|
|
3753
|
+
notify(`subagents → ${label}`)
|
|
3754
|
+
setSubagentOpen(false)
|
|
3755
|
+
} catch (reason: unknown) {
|
|
3756
|
+
notify(`subagent model change failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
3757
|
+
}
|
|
3758
|
+
},
|
|
3759
|
+
inherit: () => {
|
|
3760
|
+
props.clearSubagentModel()
|
|
3761
|
+
notify('subagents → inherit current model')
|
|
3762
|
+
setSubagentOpen(false)
|
|
3763
|
+
},
|
|
3764
|
+
close: () => setSubagentOpen(false),
|
|
3765
|
+
})
|
|
3766
|
+
: undefined,
|
|
3486
3767
|
notice === undefined
|
|
3487
3768
|
? undefined
|
|
3488
3769
|
: createElement(NoticeLine, {
|
|
@@ -3542,7 +3823,12 @@ export function App(props: AppProps): ReactElement {
|
|
|
3542
3823
|
return
|
|
3543
3824
|
}
|
|
3544
3825
|
if (row.reasoning === undefined || row.reasoning.efforts.length === 0) {
|
|
3545
|
-
|
|
3826
|
+
// A model that advertises no levels still opens the stage: the
|
|
3827
|
+
// panel itself carries the empty state (the web effort pane's
|
|
3828
|
+
// "no levels" copy), instead of a bare notice that reads like
|
|
3829
|
+
// a failure.
|
|
3830
|
+
setEffortFor(row)
|
|
3831
|
+
setModelOpen(true)
|
|
3546
3832
|
return
|
|
3547
3833
|
}
|
|
3548
3834
|
setEffortFor(row)
|
|
@@ -3556,11 +3842,22 @@ export function App(props: AppProps): ReactElement {
|
|
|
3556
3842
|
},
|
|
3557
3843
|
openMode: () => setModeOpen(true),
|
|
3558
3844
|
openPermission: () => setPermissionOpen(true),
|
|
3559
|
-
openResume: () => setResumeOpen(true),
|
|
3845
|
+
openResume: () => { setResumeDelete({ mode: false }); setResumeOpen(true) },
|
|
3560
3846
|
openPlugin: (query = '') => { setPluginQuery(query); setPluginOpen(true) },
|
|
3561
3847
|
openStatusline: () => setStatuslineOpen(true),
|
|
3562
3848
|
openTheme: () => setThemeOpen(true),
|
|
3563
3849
|
openHistory: () => setHistoryOpen(true),
|
|
3850
|
+
openAgents: () => setAgentsOpen(true),
|
|
3851
|
+
openSubagent: () => setSubagentOpen(true),
|
|
3852
|
+
openDelete: (id?: string) => {
|
|
3853
|
+
const armed = id === undefined || id === '' ? undefined : id
|
|
3854
|
+
setResumeDelete({ mode: true, ...armed === undefined ? {} : { id: armed } })
|
|
3855
|
+
setDeleteConfirmId(armed)
|
|
3856
|
+
setResumeOpen(true)
|
|
3857
|
+
},
|
|
3858
|
+
deleteConfirm: deleteConfirmId,
|
|
3859
|
+
confirmDelete,
|
|
3860
|
+
cancelDelete,
|
|
3564
3861
|
createSession: props.createSession,
|
|
3565
3862
|
cancelSessionSwitch: props.cancelSessionSwitch,
|
|
3566
3863
|
notify,
|