dsh-code 0.6.0 → 0.7.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 +4 -2
- package/README.md +4 -2
- package/bin/deepseek.mjs +38 -3
- package/lib/index.mjs +1485 -906
- package/lib/startup.mjs +21 -9
- package/lib/theme-BEi4i_aN.mjs +624 -0
- package/lib/types/app.d.ts +31 -3
- package/lib/types/index.d.ts +1 -0
- package/lib/types/kernel-panels.d.ts +21 -0
- package/lib/types/mentions.d.ts +29 -12
- package/lib/types/models.d.ts +66 -0
- package/lib/types/render/animations.d.ts +175 -2
- package/lib/types/render/projection.d.ts +38 -7
- package/lib/types/render/status.d.ts +34 -13
- package/lib/types/startup.d.ts +12 -4
- package/lib/types/theme-panel.d.ts +24 -0
- package/lib/types/theme.d.ts +158 -2
- package/package.json +1 -1
- package/src/app.ts +510 -130
- package/src/index.ts +964 -900
- package/src/kernel-panels.ts +481 -419
- package/src/mentions.ts +57 -27
- package/src/models.ts +200 -66
- package/src/render/animations.ts +359 -2
- package/src/render/projection.ts +764 -655
- package/src/render/status.ts +744 -603
- package/src/startup.ts +119 -109
- package/src/theme-panel.ts +72 -0
- package/src/theme.ts +206 -70
package/src/app.ts
CHANGED
|
@@ -22,20 +22,49 @@ import { assertNever } from '@deepseek-ai/dsh-llm'
|
|
|
22
22
|
import type { CommandDescriptor } from '@deepseek-ai/dsh-commands'
|
|
23
23
|
import type { TodoItem } from '@deepseek-ai/dsh-session'
|
|
24
24
|
import type { AskUserQuestionAnswerItem } from '@deepseek-ai/dsh-user-questions'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
brand,
|
|
27
|
+
dim,
|
|
28
|
+
error as paintError,
|
|
29
|
+
getPalette,
|
|
30
|
+
getTheme,
|
|
31
|
+
inkColor,
|
|
32
|
+
setTheme,
|
|
33
|
+
type RgbTriple,
|
|
34
|
+
type ThemeName,
|
|
35
|
+
} from './theme.ts'
|
|
36
|
+
import { ThemePanel } from './theme-panel.ts'
|
|
26
37
|
import { WHALE_GLYPH, WHALE_GLYPH_COLUMNS } from './whale-glyph.ts'
|
|
27
38
|
import type { TranscriptStore } from './store.ts'
|
|
28
39
|
import { settledEntryCount, type TranscriptEntry } from './render/projection.ts'
|
|
29
40
|
import { renderMarkdown, type MdSegment, visibleColumns } from './render/markdown.ts'
|
|
30
41
|
import type { ToolDetail } from './render/tool-detail.ts'
|
|
31
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
busyChaseFrame,
|
|
44
|
+
caretVisible,
|
|
45
|
+
DEEPSEEK_WAVE_TICK_MS,
|
|
46
|
+
deepseekWaveBorderColor,
|
|
47
|
+
deepseekWaveColumnBg,
|
|
48
|
+
deepseekWaveDuration,
|
|
49
|
+
deepseekWaveSpark,
|
|
50
|
+
deepseekWaveStyleRandom,
|
|
51
|
+
deepseekWaveTier,
|
|
52
|
+
deepseekWaveWordHue,
|
|
53
|
+
deepseekWaveWordVisible,
|
|
54
|
+
isOfficialDeepSeekLabel,
|
|
55
|
+
pulseFrame,
|
|
56
|
+
WAVE_BASE_DARK,
|
|
57
|
+
WAVE_BASE_LIGHT,
|
|
58
|
+
type DeepseekWaveStyle,
|
|
59
|
+
type DeepseekWaveTier,
|
|
60
|
+
} from './render/animations.ts'
|
|
32
61
|
import type { ApprovalSnapshot, ApprovalStore } from './approval.ts'
|
|
33
62
|
import type { CommandsView } from './commands.ts'
|
|
34
63
|
import type { ModelDirectory, ModelRow } from './models.ts'
|
|
35
64
|
import type { QuestionSnapshot, QuestionStore } from './questions.ts'
|
|
36
65
|
import type { SkillsView, SkillRow } from './skills.ts'
|
|
37
66
|
import type { MentionCandidate } from './mentions.ts'
|
|
38
|
-
import { ModePanel, HistoryPanel, PluginPanel, ResumePanel, StatuslinePanel } from './kernel-panels.ts'
|
|
67
|
+
import { EffortPanel, ModePanel, HistoryPanel, PluginPanel, ResumePanel, StatuslinePanel } from './kernel-panels.ts'
|
|
39
68
|
import type { PresetRow } from './presets.ts'
|
|
40
69
|
import type { PluginRow } from './plugin-inventory.ts'
|
|
41
70
|
import {
|
|
@@ -103,6 +132,8 @@ export interface AppProps {
|
|
|
103
132
|
skills: SkillsView
|
|
104
133
|
/** `provider/model` selection serving this session (updated on /model). */
|
|
105
134
|
model: string
|
|
135
|
+
/** Effective reasoning effort in force ('' when none), for the /model picker mark. */
|
|
136
|
+
effort?: string
|
|
106
137
|
/** Working-directory basename the session serves. */
|
|
107
138
|
cwd: string
|
|
108
139
|
/** Absolute working directory used by session filters and references. */
|
|
@@ -127,8 +158,8 @@ export interface AppProps {
|
|
|
127
158
|
loadModels(): Promise<ModelDirectory>
|
|
128
159
|
/** Load @mention candidates for the typed query (files + sessions). */
|
|
129
160
|
loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
130
|
-
/** Apply one /model selection; returns the display label. */
|
|
131
|
-
selectModel(row: ModelRow): string
|
|
161
|
+
/** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
|
|
162
|
+
selectModel(row: ModelRow, effortId?: string): string
|
|
132
163
|
/** Cycle to the next permission preset (Shift+Tab); returns the new label. */
|
|
133
164
|
cyclePermission(): string
|
|
134
165
|
/** Export the transcript to a markdown file (/export [path]); reports via notices. */
|
|
@@ -150,6 +181,8 @@ export interface AppProps {
|
|
|
150
181
|
statusline: readonly string[]
|
|
151
182
|
/** Persist a new statusline item set; the runner surfaces IO failures as notices. */
|
|
152
183
|
saveStatusline(items: readonly string[]): void
|
|
184
|
+
/** Apply and persist one /theme selection; the runner owns the theme.json file. */
|
|
185
|
+
saveTheme?(name: ThemeName): void
|
|
153
186
|
/** Persistent cross-session input history (oldest first); the runner owns the file. */
|
|
154
187
|
history: readonly string[]
|
|
155
188
|
/** Persist one submitted prompt to the global history file. */
|
|
@@ -158,11 +191,6 @@ export interface AppProps {
|
|
|
158
191
|
cancelQueued(messageId: string): void
|
|
159
192
|
}
|
|
160
193
|
|
|
161
|
-
/** Ink `color` string for one palette triple. */
|
|
162
|
-
function inkColor(triple: readonly [number, number, number]): string {
|
|
163
|
-
return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`
|
|
164
|
-
}
|
|
165
|
-
|
|
166
194
|
/** Pad text with spaces to a visible-column target (menu name column). */
|
|
167
195
|
function padColumns(text: string, width: number): string {
|
|
168
196
|
const clipped = truncateColumns(singleLineText(text), width)
|
|
@@ -199,7 +227,7 @@ function useStableInput(handler: (input: string, key: Key) => void, active: bool
|
|
|
199
227
|
/** Single-cell stepped pulse: the web's 125ms flat-hold brightness steps over 1s. */
|
|
200
228
|
function Pulse(): ReactElement {
|
|
201
229
|
const tick = useFrames(125)
|
|
202
|
-
return createElement(Text, { color: inkColor(
|
|
230
|
+
return createElement(Text, { color: inkColor(getPalette().brandBright) }, pulseFrame(tick))
|
|
203
231
|
}
|
|
204
232
|
|
|
205
233
|
/**
|
|
@@ -210,7 +238,7 @@ function Pulse(): ReactElement {
|
|
|
210
238
|
*/
|
|
211
239
|
function BusyChase(): ReactElement {
|
|
212
240
|
const tick = useFrames(125)
|
|
213
|
-
return createElement(Text, { color: inkColor(
|
|
241
|
+
return createElement(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + ' ')
|
|
214
242
|
}
|
|
215
243
|
|
|
216
244
|
/** Blinking block caret appended to streaming text. */
|
|
@@ -298,11 +326,11 @@ function segmentProps(style: MdSegment['style']): {
|
|
|
298
326
|
} {
|
|
299
327
|
switch (style) {
|
|
300
328
|
case 'accent':
|
|
301
|
-
return { color: inkColor(
|
|
329
|
+
return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined }
|
|
302
330
|
case 'code':
|
|
303
|
-
return { color: inkColor(
|
|
331
|
+
return { color: inkColor(getPalette().code), bold: undefined, italic: undefined, strikethrough: undefined }
|
|
304
332
|
case 'dim':
|
|
305
|
-
return { color: inkColor(
|
|
333
|
+
return { color: inkColor(getPalette().dim), bold: undefined, italic: undefined, strikethrough: undefined }
|
|
306
334
|
case 'bold':
|
|
307
335
|
return { color: undefined, bold: true, italic: undefined, strikethrough: undefined }
|
|
308
336
|
case 'italic':
|
|
@@ -310,7 +338,7 @@ function segmentProps(style: MdSegment['style']): {
|
|
|
310
338
|
case 'boldItalic':
|
|
311
339
|
return { color: undefined, bold: true, italic: true, strikethrough: undefined }
|
|
312
340
|
case 'strike':
|
|
313
|
-
return { color: inkColor(
|
|
341
|
+
return { color: inkColor(getPalette().dim), bold: undefined, italic: undefined, strikethrough: true }
|
|
314
342
|
default:
|
|
315
343
|
return { color: undefined, bold: undefined, italic: undefined, strikethrough: undefined }
|
|
316
344
|
}
|
|
@@ -326,13 +354,13 @@ function lineStyleProps(style: LineStyle): {
|
|
|
326
354
|
} {
|
|
327
355
|
switch (style) {
|
|
328
356
|
case 'brand':
|
|
329
|
-
return { color: inkColor(
|
|
357
|
+
return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
330
358
|
case 'success':
|
|
331
|
-
return { color: inkColor(
|
|
359
|
+
return { color: inkColor(getPalette().success), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
332
360
|
case 'error':
|
|
333
|
-
return { color: inkColor(
|
|
361
|
+
return { color: inkColor(getPalette().error), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
334
362
|
case 'warn':
|
|
335
|
-
return { color: inkColor(
|
|
363
|
+
return { color: inkColor(getPalette().warn), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
336
364
|
case 'dimItalic':
|
|
337
365
|
return { color: undefined, bold: undefined, italic: true, strikethrough: undefined, dimColor: true }
|
|
338
366
|
default:
|
|
@@ -407,7 +435,7 @@ function ToolDetailBody({ detail }: { detail: ToolDetail }): ReactElement {
|
|
|
407
435
|
Text,
|
|
408
436
|
{
|
|
409
437
|
key: at,
|
|
410
|
-
color: line.mark === '+' ? inkColor(
|
|
438
|
+
color: line.mark === '+' ? inkColor(getPalette().success) : line.mark === '-' ? inkColor(getPalette().error) : inkColor(getPalette().dim),
|
|
411
439
|
wrap: 'truncate-end',
|
|
412
440
|
},
|
|
413
441
|
` ${line.mark}${displayText(line.text)}`,
|
|
@@ -482,8 +510,8 @@ function EntryLine({ entry, showReasoning, verbose }: { entry: TranscriptEntry;
|
|
|
482
510
|
const mark = entry.state === 'running'
|
|
483
511
|
? createElement(Pulse)
|
|
484
512
|
: entry.state === 'error'
|
|
485
|
-
? createElement(Text, { color: inkColor(
|
|
486
|
-
: createElement(Text, { color: inkColor(
|
|
513
|
+
? createElement(Text, { color: inkColor(getPalette().error) }, '⨯')
|
|
514
|
+
: createElement(Text, { color: inkColor(getPalette().success) }, '⏺')
|
|
487
515
|
return createElement(
|
|
488
516
|
Box,
|
|
489
517
|
{ flexDirection: 'column' },
|
|
@@ -499,7 +527,7 @@ function EntryLine({ entry, showReasoning, verbose }: { entry: TranscriptEntry;
|
|
|
499
527
|
? undefined
|
|
500
528
|
: createElement(
|
|
501
529
|
Text,
|
|
502
|
-
{ color: entry.state === 'error' ? inkColor(
|
|
530
|
+
{ color: entry.state === 'error' ? inkColor(getPalette().error) : inkColor(getPalette().dim), wrap: verbose ? 'truncate-end' : undefined },
|
|
503
531
|
` ⎿ ${displayText(entry.summary)}`,
|
|
504
532
|
),
|
|
505
533
|
verbose && entry.detail !== undefined
|
|
@@ -511,8 +539,8 @@ function EntryLine({ entry, showReasoning, verbose }: { entry: TranscriptEntry;
|
|
|
511
539
|
const mark = entry.state === 'running'
|
|
512
540
|
? createElement(Pulse)
|
|
513
541
|
: entry.state === 'error'
|
|
514
|
-
? createElement(Text, { color: inkColor(
|
|
515
|
-
: createElement(Text, { color: inkColor(
|
|
542
|
+
? createElement(Text, { color: inkColor(getPalette().error) }, '⨯')
|
|
543
|
+
: createElement(Text, { color: inkColor(getPalette().success) }, '⏺')
|
|
516
544
|
return createElement(
|
|
517
545
|
Box,
|
|
518
546
|
{ flexDirection: 'column' },
|
|
@@ -526,7 +554,7 @@ function EntryLine({ entry, showReasoning, verbose }: { entry: TranscriptEntry;
|
|
|
526
554
|
),
|
|
527
555
|
entry.summary === ''
|
|
528
556
|
? undefined
|
|
529
|
-
: createElement(Text, { color: inkColor(
|
|
557
|
+
: createElement(Text, { color: inkColor(getPalette().dim), wrap: verbose ? 'truncate-end' : undefined }, ` ⎿ ${displayText(entry.summary)}`),
|
|
530
558
|
)
|
|
531
559
|
}
|
|
532
560
|
case 'turn-marker':
|
|
@@ -546,7 +574,7 @@ function EntryLine({ entry, showReasoning, verbose }: { entry: TranscriptEntry;
|
|
|
546
574
|
// next attempt is underway.
|
|
547
575
|
return createElement(
|
|
548
576
|
Text,
|
|
549
|
-
{ color: entry.state === 'running' ? inkColor(
|
|
577
|
+
{ color: entry.state === 'running' ? inkColor(getPalette().warn) : inkColor(getPalette().dim), wrap: verbose ? 'truncate-end' : undefined },
|
|
550
578
|
` ↻ retry ${entry.attempt}/${entry.max} · ${displayText(entry.code)} · ${Math.round(entry.delayMs / 100) / 10}s`,
|
|
551
579
|
)
|
|
552
580
|
case 'files': {
|
|
@@ -584,8 +612,8 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
584
612
|
if (rows < 20) {
|
|
585
613
|
return createElement(
|
|
586
614
|
Box,
|
|
587
|
-
{ flexDirection: 'row', gap: 1, borderStyle: 'round', borderColor: inkColor(
|
|
588
|
-
createElement(Text, { color: inkColor(
|
|
615
|
+
{ flexDirection: 'row', gap: 1, borderStyle: 'round', borderColor: inkColor(getPalette().brand), paddingX: 1, alignSelf: 'flex-start' },
|
|
616
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), bold: true }, 'DeepSeek Harness'),
|
|
589
617
|
createElement(Text, { dimColor: true }, hint),
|
|
590
618
|
)
|
|
591
619
|
}
|
|
@@ -594,16 +622,16 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
594
622
|
// alignSelf shrinks the border to the whale-plus-wordmark content instead
|
|
595
623
|
// of stretching across the terminal and stranding empty space on the right
|
|
596
624
|
// (the compact-banner treatment the Claude Code welcome uses).
|
|
597
|
-
{ flexDirection: 'row', gap: 2, borderStyle: 'round', borderColor: inkColor(
|
|
625
|
+
{ flexDirection: 'row', gap: 2, borderStyle: 'round', borderColor: inkColor(getPalette().brand), paddingX: 1, alignSelf: 'flex-start' },
|
|
598
626
|
createElement(
|
|
599
627
|
Box,
|
|
600
628
|
{ flexDirection: 'column', width: WHALE_GLYPH_COLUMNS, justifyContent: 'center' },
|
|
601
|
-
...WHALE_GLYPH.map((row, index) => createElement(Text, { key: index, color: inkColor(
|
|
629
|
+
...WHALE_GLYPH.map((row, index) => createElement(Text, { key: index, color: inkColor(getPalette().brand) }, row)),
|
|
602
630
|
),
|
|
603
631
|
createElement(
|
|
604
632
|
Box,
|
|
605
633
|
{ flexDirection: 'column', justifyContent: 'center' },
|
|
606
|
-
createElement(Text, { color: inkColor(
|
|
634
|
+
createElement(Text, { color: inkColor(getPalette().brandBright), bold: true }, 'DeepSeek Harness'),
|
|
607
635
|
createElement(Text, { dimColor: true }, hint),
|
|
608
636
|
),
|
|
609
637
|
)
|
|
@@ -626,10 +654,10 @@ function TodoPanel({ todos }: { todos: readonly TodoItem[] }): ReactElement | un
|
|
|
626
654
|
{ paddingX: 1 },
|
|
627
655
|
createElement(
|
|
628
656
|
Text,
|
|
629
|
-
{ color: inkColor(
|
|
657
|
+
{ color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' },
|
|
630
658
|
`todos ${completed}/${todos.length}`,
|
|
631
659
|
createElement(Text, { dimColor: true }, ` · ${inProgress} active · ${pending} pending`),
|
|
632
|
-
current === undefined ? '' : createElement(Text, { color: inkColor(
|
|
660
|
+
current === undefined ? '' : createElement(Text, { color: inkColor(getPalette().brandBright) }, ` · ${todoMark(current.status)} ${displayText(current.content)}`),
|
|
633
661
|
),
|
|
634
662
|
)
|
|
635
663
|
}
|
|
@@ -637,7 +665,7 @@ function TodoPanel({ todos }: { todos: readonly TodoItem[] }): ReactElement | un
|
|
|
637
665
|
/**
|
|
638
666
|
* Ink props for one status tone: the Codex status-line accent mapping over
|
|
639
667
|
* the DeepSeek palette, all blue by design — the status bar speaks only in
|
|
640
|
-
* degrees of blue (deep accent, primary figures,
|
|
668
|
+
* degrees of blue (deep accent, primary figures, model identity, sky
|
|
641
669
|
* paths and done states), with amber/red reserved for warnings and errors.
|
|
642
670
|
*/
|
|
643
671
|
function statusToneProps(tone: StatusTone): {
|
|
@@ -647,28 +675,47 @@ function statusToneProps(tone: StatusTone): {
|
|
|
647
675
|
} {
|
|
648
676
|
switch (tone) {
|
|
649
677
|
case 'model':
|
|
650
|
-
|
|
678
|
+
// Same tone as the working-directory segment: the model name reads as
|
|
679
|
+
// a path fact, not a brand accent.
|
|
680
|
+
return { color: inkColor(getPalette().code), bold: true, dimColor: undefined }
|
|
651
681
|
case 'live':
|
|
652
|
-
return { color: inkColor(
|
|
682
|
+
return { color: inkColor(getPalette().brandBright), bold: undefined, dimColor: undefined }
|
|
653
683
|
case 'path':
|
|
654
|
-
return { color: inkColor(
|
|
684
|
+
return { color: inkColor(getPalette().code), bold: undefined, dimColor: undefined }
|
|
655
685
|
case 'branch':
|
|
656
|
-
return { color: inkColor(
|
|
686
|
+
return { color: inkColor(getPalette().text), bold: undefined, dimColor: undefined }
|
|
657
687
|
case 'value':
|
|
658
|
-
return { color: inkColor(
|
|
688
|
+
return { color: inkColor(getPalette().brand), bold: undefined, dimColor: undefined }
|
|
659
689
|
case 'label':
|
|
660
690
|
case 'meta':
|
|
661
|
-
|
|
691
|
+
// Explicit RGB gray, not SGR dim: Ink's token stream inherits an
|
|
692
|
+
// unclosed `dim` into the next span (the model name after the busy dot
|
|
693
|
+
// rendered dim+bold and looked gray), and a concrete color closes
|
|
694
|
+
// cleanly on the style transition. Theme-aware via the palette.
|
|
695
|
+
return { color: inkColor(getPalette().dim), bold: undefined, dimColor: undefined }
|
|
662
696
|
case 'accent':
|
|
663
|
-
return { color: inkColor(
|
|
697
|
+
return { color: inkColor(getPalette().brandDeep), bold: undefined, dimColor: undefined }
|
|
698
|
+
// Context-bar segment shades, dark → light across the DeepSeek blues:
|
|
699
|
+
// system/prompt/assistant/thinking/tools. The tools segment borrows the
|
|
700
|
+
// code sky-blue as the fifth shade (no new theme token).
|
|
701
|
+
case 'ctxSystem':
|
|
702
|
+
return { color: inkColor(getPalette().brandDeep), bold: undefined, dimColor: undefined }
|
|
703
|
+
case 'ctxPrompt':
|
|
704
|
+
return { color: inkColor(getPalette().brand), bold: undefined, dimColor: undefined }
|
|
705
|
+
case 'ctxAssistant':
|
|
706
|
+
return { color: inkColor(getPalette().brandMid), bold: undefined, dimColor: undefined }
|
|
707
|
+
case 'ctxThinking':
|
|
708
|
+
return { color: inkColor(getPalette().brandBright), bold: undefined, dimColor: undefined }
|
|
709
|
+
case 'ctxTools':
|
|
710
|
+
return { color: inkColor(getPalette().code), bold: undefined, dimColor: undefined }
|
|
664
711
|
case 'success':
|
|
665
|
-
return { color: inkColor(
|
|
712
|
+
return { color: inkColor(getPalette().code), bold: true, dimColor: undefined }
|
|
666
713
|
case 'warn':
|
|
667
|
-
return { color: inkColor(
|
|
714
|
+
return { color: inkColor(getPalette().warn), bold: true, dimColor: undefined }
|
|
668
715
|
case 'error':
|
|
669
|
-
return { color: inkColor(
|
|
716
|
+
return { color: inkColor(getPalette().error), bold: true, dimColor: undefined }
|
|
670
717
|
default:
|
|
671
|
-
return { color:
|
|
718
|
+
return { color: inkColor(getPalette().dim), bold: undefined, dimColor: undefined }
|
|
672
719
|
}
|
|
673
720
|
}
|
|
674
721
|
|
|
@@ -681,7 +728,28 @@ function statusToneProps(tone: StatusTone): {
|
|
|
681
728
|
* content, so the footer degrades to a single row on narrow terminals. Both
|
|
682
729
|
* layouts arrive pre-measured from the pure reducer, so Ink only paints;
|
|
683
730
|
* truncation degrades groups, it never wraps a row.
|
|
731
|
+
*
|
|
732
|
+
* The DeepSeek easter egg: when the model label *switches* to an official
|
|
733
|
+
* DeepSeek route, the composer's INPUT ROW (not the frame) plays Codex's
|
|
734
|
+
* effort-ignition "Wave" — a blue crest sweeping the content row column by
|
|
735
|
+
* column, with the `· ✦ ✧` sparkles on the deepseek tier — and the prompt
|
|
736
|
+
* marker keeps the tier accent afterwards. The border stays a constant
|
|
737
|
+
* static dim; only the row's per-column background tints during the wave,
|
|
738
|
+
* so the row and column budget is untouched throughout.
|
|
684
739
|
*/
|
|
740
|
+
|
|
741
|
+
/** Theme anchors for the one-shot composer wave, read from the active palette
|
|
742
|
+
* so the wave stays coordinated in both themes. The flash tier runs the
|
|
743
|
+
* brand blues; the deepseek tier swaps in the code sky-blue for a brighter,
|
|
744
|
+
* richer mix. Codex's Wave bands carry no hue index (only hues[0] tints the
|
|
745
|
+
* row), so the accent the prompt keeps is always hues[0]. */
|
|
746
|
+
function deepseekWaveHues(tier: DeepseekWaveTier): readonly [RgbTriple, RgbTriple, RgbTriple] {
|
|
747
|
+
const palette = getPalette()
|
|
748
|
+
return tier === 'flash'
|
|
749
|
+
? [palette.brandBright, palette.brand, palette.brandMid]
|
|
750
|
+
: [palette.brandBright, palette.code, palette.brandMid]
|
|
751
|
+
}
|
|
752
|
+
|
|
685
753
|
function StatusLine({ facts, stats, busy, columns, items }: {
|
|
686
754
|
facts: StatusFacts
|
|
687
755
|
stats: Parameters<typeof layoutStatusBar>[1]
|
|
@@ -690,11 +758,12 @@ function StatusLine({ facts, stats, busy, columns, items }: {
|
|
|
690
758
|
items: readonly string[]
|
|
691
759
|
}): ReactElement {
|
|
692
760
|
const layout = layoutStatusBar(facts, stats, Math.max(8, columns - 2), { busy, items })
|
|
761
|
+
|
|
693
762
|
const renderRow = (row: { left: readonly StatusGroup[]; right: readonly StatusSpan[]; hint: boolean }, key: string): ReactElement => {
|
|
694
763
|
const leftParts: ReactElement[] = []
|
|
695
764
|
row.left.forEach((group, groupIndex) => {
|
|
696
765
|
if (groupIndex > 0) {
|
|
697
|
-
leftParts.push(createElement(Text, { key: key + 'gs' + groupIndex,
|
|
766
|
+
leftParts.push(createElement(Text, { key: key + 'gs' + groupIndex, color: inkColor(getPalette().dim) }, STATUS_GROUP_SEPARATOR))
|
|
698
767
|
}
|
|
699
768
|
group.spans.forEach((span, spanIndex) => {
|
|
700
769
|
leftParts.push(createElement(
|
|
@@ -707,7 +776,7 @@ function StatusLine({ facts, stats, busy, columns, items }: {
|
|
|
707
776
|
const rightParts: ReactElement[] = []
|
|
708
777
|
row.right.forEach((span, index) => {
|
|
709
778
|
if (index > 0) {
|
|
710
|
-
rightParts.push(createElement(Text, { key: key + 'rs' + index,
|
|
779
|
+
rightParts.push(createElement(Text, { key: key + 'rs' + index, color: inkColor(getPalette().dim) }, STATUS_ITEM_SEPARATOR))
|
|
711
780
|
}
|
|
712
781
|
rightParts.push(createElement(
|
|
713
782
|
Text,
|
|
@@ -716,7 +785,7 @@ function StatusLine({ facts, stats, busy, columns, items }: {
|
|
|
716
785
|
))
|
|
717
786
|
})
|
|
718
787
|
if (row.hint) {
|
|
719
|
-
rightParts.push(createElement(Text, { key: key + 'hint',
|
|
788
|
+
rightParts.push(createElement(Text, { key: key + 'hint', color: inkColor(getPalette().dim) }, STATUS_CYCLE_HINT))
|
|
720
789
|
}
|
|
721
790
|
// Each row already fits the column budget; truncate-end stays as the
|
|
722
791
|
// terminal-measurement backstop so a drifting cell count clips instead
|
|
@@ -751,10 +820,10 @@ function NoticeLine({ text, tone, columns }: {
|
|
|
751
820
|
columns: number
|
|
752
821
|
}): ReactElement {
|
|
753
822
|
const color = tone === 'error'
|
|
754
|
-
?
|
|
823
|
+
? getPalette().error
|
|
755
824
|
: tone === 'warning'
|
|
756
|
-
?
|
|
757
|
-
:
|
|
825
|
+
? getPalette().warn
|
|
826
|
+
: getPalette().brandBright
|
|
758
827
|
const mark = tone === 'error' ? '⨯' : tone === 'warning' ? '!' : '•'
|
|
759
828
|
return createElement(
|
|
760
829
|
Box,
|
|
@@ -825,8 +894,8 @@ function ApprovalBar({ snapshot, locked }: { snapshot: ApprovalSnapshot; locked:
|
|
|
825
894
|
const { answered } = snapshot
|
|
826
895
|
return createElement(
|
|
827
896
|
Box,
|
|
828
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
829
|
-
createElement(Text, { color: inkColor(
|
|
897
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().warn) },
|
|
898
|
+
createElement(Text, { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' }, truncateColumns(`⏸ waiting for approval · lines ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)),
|
|
830
899
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
831
900
|
createElement(StyledRows, { lines: content.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
|
|
832
901
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
@@ -1093,10 +1162,10 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1093
1162
|
: '↑↓ choose · pgup/pgdn scroll · enter submit · c custom · esc interrupt'
|
|
1094
1163
|
return createElement(
|
|
1095
1164
|
Box,
|
|
1096
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(isPlan ?
|
|
1165
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep) },
|
|
1097
1166
|
createElement(
|
|
1098
1167
|
Text,
|
|
1099
|
-
{ color: inkColor(isPlan ?
|
|
1168
|
+
{ color: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep), bold: true, wrap: 'truncate-end' },
|
|
1100
1169
|
truncateColumns(`${isPlan ? '📋 plan review' : '❓ question'} ${index + 1}/${pending.request.questions.length} · lines ${rendered.lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(rendered.lines.length, visibleScroll + viewport.bodyRows)}/${rendered.lines.length}`, viewport.contentColumns),
|
|
1101
1170
|
),
|
|
1102
1171
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
@@ -1176,7 +1245,7 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }: {
|
|
|
1176
1245
|
: error !== undefined
|
|
1177
1246
|
? [createElement(
|
|
1178
1247
|
Text,
|
|
1179
|
-
{ key: 'error', color: inkColor(
|
|
1248
|
+
{ key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' },
|
|
1180
1249
|
truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns),
|
|
1181
1250
|
)]
|
|
1182
1251
|
: [
|
|
@@ -1184,7 +1253,7 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }: {
|
|
|
1184
1253
|
? []
|
|
1185
1254
|
: [createElement(
|
|
1186
1255
|
Text,
|
|
1187
|
-
{ key: 'failures', color: inkColor(
|
|
1256
|
+
{ key: 'failures', color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1188
1257
|
truncateColumns(` unavailable providers: ${directory?.failures.join(', ')}`, viewport.contentColumns),
|
|
1189
1258
|
)]),
|
|
1190
1259
|
...(rows.length === 0
|
|
@@ -1199,8 +1268,8 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }: {
|
|
|
1199
1268
|
const visible = rowBudget === 0 ? [] : rows.slice(first, first + rowBudget)
|
|
1200
1269
|
return createElement(
|
|
1201
1270
|
Box,
|
|
1202
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
1203
|
-
createElement(Text, { color: inkColor(
|
|
1271
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().brand) },
|
|
1272
|
+
createElement(Text, { color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' }, truncateColumns(`/model — select model${rows.length === 0 ? '' : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)),
|
|
1204
1273
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1205
1274
|
...stateRows,
|
|
1206
1275
|
...visible.map((row) => {
|
|
@@ -1210,7 +1279,7 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }: {
|
|
|
1210
1279
|
Text,
|
|
1211
1280
|
{
|
|
1212
1281
|
key: `${row.provider}/${row.model}`,
|
|
1213
|
-
color: index === cursor ? inkColor(
|
|
1282
|
+
color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
1214
1283
|
wrap: 'truncate-end',
|
|
1215
1284
|
},
|
|
1216
1285
|
truncateColumns(`${index === cursor ? '❯ ' : ' '}${label}`, viewport.contentColumns),
|
|
@@ -1258,16 +1327,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
1258
1327
|
? []
|
|
1259
1328
|
: [createElement(
|
|
1260
1329
|
Text,
|
|
1261
|
-
{ key: 'commands-error', color: inkColor(
|
|
1330
|
+
{ key: 'commands-error', color: inkColor(getPalette().error), wrap: 'truncate-end' },
|
|
1262
1331
|
truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns),
|
|
1263
1332
|
)]),
|
|
1264
1333
|
createElement(Box, { key: 'local-help' }, row('/help', 'show this overlay')),
|
|
1265
1334
|
createElement(Box, { key: 'local-model' }, row('/model', 'switch the model')),
|
|
1335
|
+
createElement(Box, { key: 'local-effort' }, row('/effort', 'adjust reasoning effort for the current model')),
|
|
1266
1336
|
createElement(Box, { key: 'local-mode' }, row('/mode', 'inspect or select the agent preset (/mode [preset])')),
|
|
1267
1337
|
createElement(Box, { key: 'local-new' }, row('/new', 'create and switch to a fresh session (/new [preset])')),
|
|
1268
1338
|
createElement(Box, { key: 'local-resume' }, row('/resume', 'browse or switch root sessions (/resume [id|prefix])')),
|
|
1269
1339
|
createElement(Box, { key: 'local-plugin' }, row('/plugin', 'inspect the live plugin composition')),
|
|
1270
1340
|
createElement(Box, { key: 'local-statusline' }, row('/statusline', 'customize the status line items')),
|
|
1341
|
+
createElement(Box, { key: 'local-theme' }, row('/theme', 'switch the color theme')),
|
|
1271
1342
|
createElement(Box, { key: 'local-history' }, row('/history', 'search and recall past prompts')),
|
|
1272
1343
|
createElement(Box, { key: 'local-clear' }, row('/clear', 'clear the screen')),
|
|
1273
1344
|
createElement(Box, { key: 'local-export' }, row('/export', 'export the transcript to markdown (/export [path])')),
|
|
@@ -1288,7 +1359,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
1288
1359
|
? []
|
|
1289
1360
|
: [createElement(
|
|
1290
1361
|
Text,
|
|
1291
|
-
{ key: 'skills-error', color: inkColor(
|
|
1362
|
+
{ key: 'skills-error', color: inkColor(getPalette().error), wrap: 'truncate-end' },
|
|
1292
1363
|
truncateColumns(` skill catalog unavailable: ${singleLineText(skillError)}`, viewport.contentColumns),
|
|
1293
1364
|
)]),
|
|
1294
1365
|
...skills.map(skill => createElement(
|
|
@@ -1326,8 +1397,8 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
1326
1397
|
|
|
1327
1398
|
return createElement(
|
|
1328
1399
|
Box,
|
|
1329
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
1330
|
-
createElement(Text, { color: inkColor(
|
|
1400
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().brand) },
|
|
1401
|
+
createElement(Text, { color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' }, truncateColumns(`/help — keys and commands · rows ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)),
|
|
1331
1402
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1332
1403
|
...content.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
1333
1404
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
@@ -1358,6 +1429,59 @@ function editorWindow(value: string, cursor: number, columns: number): { before:
|
|
|
1358
1429
|
return { before, caret, after }
|
|
1359
1430
|
}
|
|
1360
1431
|
|
|
1432
|
+
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
1433
|
+
const COMPOSER_PLACEHOLDER = 'type a message · / commands · @ mentions'
|
|
1434
|
+
|
|
1435
|
+
/** One physical cell of the wave-painted composer row: a char plus styles. */
|
|
1436
|
+
interface ComposerCell {
|
|
1437
|
+
char: string
|
|
1438
|
+
color?: string
|
|
1439
|
+
backgroundColor?: string
|
|
1440
|
+
bold?: boolean
|
|
1441
|
+
inverse?: boolean
|
|
1442
|
+
dim?: boolean
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/** Adjacent cells with identical styling merge into one styled Text span. */
|
|
1446
|
+
function sameCellStyle(a: ComposerCell, b: ComposerCell): boolean {
|
|
1447
|
+
return a.color === b.color
|
|
1448
|
+
&& a.backgroundColor === b.backgroundColor
|
|
1449
|
+
&& a.bold === b.bold
|
|
1450
|
+
&& a.inverse === b.inverse
|
|
1451
|
+
&& a.dim === b.dim
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Render the wave row as one Text whose cells carry per-column
|
|
1456
|
+
* `backgroundColor` runs: the Codex Wave crest paints a smooth gradient
|
|
1457
|
+
* (one SGR run per sampled column) over the prompt, draft, cursor,
|
|
1458
|
+
* placeholder, and the trailing blank fill — the draft stays readable
|
|
1459
|
+
* because the tint blends at ≤ 0.55 toward the theme's blank-cell base.
|
|
1460
|
+
*/
|
|
1461
|
+
function waveRowSpans(cells: readonly ComposerCell[]): ReactElement[] {
|
|
1462
|
+
const spans: ReactElement[] = []
|
|
1463
|
+
let start = 0
|
|
1464
|
+
while (start < cells.length) {
|
|
1465
|
+
const cell = cells[start]!
|
|
1466
|
+
let end = start + 1
|
|
1467
|
+
while (end < cells.length && sameCellStyle(cells[end]!, cell)) end += 1
|
|
1468
|
+
spans.push(createElement(
|
|
1469
|
+
Text,
|
|
1470
|
+
{
|
|
1471
|
+
key: start,
|
|
1472
|
+
color: cell.color,
|
|
1473
|
+
backgroundColor: cell.backgroundColor,
|
|
1474
|
+
bold: cell.bold,
|
|
1475
|
+
inverse: cell.inverse,
|
|
1476
|
+
dimColor: cell.dim,
|
|
1477
|
+
},
|
|
1478
|
+
cells.slice(start, end).map(c => c.char).join(''),
|
|
1479
|
+
))
|
|
1480
|
+
start = end
|
|
1481
|
+
}
|
|
1482
|
+
return spans
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1361
1485
|
/**
|
|
1362
1486
|
* The Ctrl+O transcript inspector: one selected durable entry at a time,
|
|
1363
1487
|
* with independent history selection and content scrolling. The complete
|
|
@@ -1474,11 +1598,11 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
1474
1598
|
width: viewport.outerColumns,
|
|
1475
1599
|
paddingX: 1,
|
|
1476
1600
|
borderStyle: 'round',
|
|
1477
|
-
borderColor: inkColor(
|
|
1601
|
+
borderColor: inkColor(getPalette().brand),
|
|
1478
1602
|
},
|
|
1479
1603
|
createElement(
|
|
1480
1604
|
Text,
|
|
1481
|
-
{ color: inkColor(
|
|
1605
|
+
{ color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' },
|
|
1482
1606
|
truncateColumns(title, viewport.contentColumns),
|
|
1483
1607
|
),
|
|
1484
1608
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
@@ -1526,9 +1650,14 @@ interface CompletionCandidate {
|
|
|
1526
1650
|
* Resolve completion candidates for the current input: TUI-local commands,
|
|
1527
1651
|
* the live registry descriptors, and user-invocable skills, filtered by the
|
|
1528
1652
|
* typed prefix. Command names win collisions (the dispatch tries the
|
|
1529
|
-
* registry first and only then falls through to the skill gesture)
|
|
1653
|
+
* registry first and only then falls through to the skill gesture), and a
|
|
1654
|
+
* later duplicate name never renders twice.
|
|
1655
|
+
*
|
|
1656
|
+
* A bare `/` returns the FULL merged list — Codex's command popup shows every
|
|
1657
|
+
* command inside a scroll window on an empty filter, and the menu's own
|
|
1658
|
+
* selection window bounds the visible rows, so no slice cap is needed.
|
|
1530
1659
|
*/
|
|
1531
|
-
function completionCandidates(
|
|
1660
|
+
export function completionCandidates(
|
|
1532
1661
|
value: string,
|
|
1533
1662
|
descriptors: readonly CommandDescriptor[],
|
|
1534
1663
|
skills: readonly SkillRow[],
|
|
@@ -1538,11 +1667,13 @@ function completionCandidates(
|
|
|
1538
1667
|
const local: CompletionCandidate[] = [
|
|
1539
1668
|
{ label: '/help', description: 'show commands', origin: 'command' },
|
|
1540
1669
|
{ label: '/model', description: 'switch the model', origin: 'command' },
|
|
1670
|
+
{ label: '/effort', description: 'adjust reasoning effort for the current model', origin: 'command' },
|
|
1541
1671
|
{ label: '/mode', description: 'select the agent preset', origin: 'command' },
|
|
1542
1672
|
{ label: '/new', description: 'start a fresh session', origin: 'command' },
|
|
1543
1673
|
{ label: '/resume', description: 'browse or switch sessions', origin: 'command' },
|
|
1544
1674
|
{ label: '/plugin', description: 'inspect the plugin composition', origin: 'command' },
|
|
1545
1675
|
{ label: '/statusline', description: 'customize the status line', origin: 'command' },
|
|
1676
|
+
{ label: '/theme', description: 'switch the color theme', origin: 'command' },
|
|
1546
1677
|
{ label: '/history', description: 'search and recall past prompts', origin: 'command' },
|
|
1547
1678
|
{ label: '/clear', description: 'clear the screen', origin: 'command' },
|
|
1548
1679
|
{ label: '/export', description: 'export the transcript to markdown', origin: 'command' },
|
|
@@ -1568,12 +1699,19 @@ function completionCandidates(
|
|
|
1568
1699
|
description: skill.modelInvocable ? `skill · ${skill.description}` : `skill (user only) · ${skill.description}`,
|
|
1569
1700
|
origin: 'skill',
|
|
1570
1701
|
}))
|
|
1571
|
-
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1702
|
+
// One row per name, first occurrence wins: local before registry before
|
|
1703
|
+
// skills, which is exactly the shadowing precedence above (defensive
|
|
1704
|
+
// against duplicate registry names across scopes).
|
|
1705
|
+
const seen = new Set<string>()
|
|
1706
|
+
const all: CompletionCandidate[] = []
|
|
1707
|
+
for (const candidate of [...local, ...registry, ...skillRows]) {
|
|
1708
|
+
const name = candidate.label.slice(1)
|
|
1709
|
+
if (seen.has(name)) continue
|
|
1710
|
+
seen.add(name)
|
|
1711
|
+
all.push(candidate)
|
|
1712
|
+
}
|
|
1713
|
+
if (prefix === '') return all
|
|
1714
|
+
return all.filter(candidate => candidate.label.slice(1).startsWith(prefix))
|
|
1577
1715
|
}
|
|
1578
1716
|
|
|
1579
1717
|
/**
|
|
@@ -1607,6 +1745,7 @@ function CompletionMenu({ active, mention, index, rows }: {
|
|
|
1607
1745
|
const selected = rows.length === 0 ? 0 : index % rows.length
|
|
1608
1746
|
const first = selectionWindow(selected, rows.length, limit)
|
|
1609
1747
|
const visible = rows.slice(first, first + limit)
|
|
1748
|
+
const hidden = rows.length - visible.length
|
|
1610
1749
|
return createElement(
|
|
1611
1750
|
Box,
|
|
1612
1751
|
{ flexDirection: 'column', marginLeft: 2, paddingY: verticalPadding },
|
|
@@ -1618,13 +1757,17 @@ function CompletionMenu({ active, mention, index, rows }: {
|
|
|
1618
1757
|
Text,
|
|
1619
1758
|
{
|
|
1620
1759
|
key: candidate.label,
|
|
1621
|
-
color: absolute === selected ? inkColor(
|
|
1760
|
+
color: absolute === selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
1622
1761
|
wrap: 'truncate-end',
|
|
1623
1762
|
},
|
|
1624
1763
|
`${absolute === selected ? '❯ ' : ' '}${padColumns(candidate.label, nameWidth)}${dim(truncateColumns(displayText(candidate.description), descBudget))}`,
|
|
1625
1764
|
)
|
|
1626
1765
|
})),
|
|
1627
|
-
|
|
1766
|
+
// Scroll affordance: with the full merged catalog (commands + registry +
|
|
1767
|
+
// skills) the six-row window rarely shows the tail — count and hint keep
|
|
1768
|
+
// the rest discoverable without inflating the menu budget.
|
|
1769
|
+
hidden > 0 ? createElement(Text, { key: 'more', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, dim(` … +${hidden} more`)) : undefined,
|
|
1770
|
+
showFooter ? createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, dim(mention ? `↑↓ choose · ${rows.length} items · tab insert` : `↑↓ choose · ${rows.length} items · tab complete`)) : undefined,
|
|
1628
1771
|
)
|
|
1629
1772
|
}
|
|
1630
1773
|
|
|
@@ -1634,7 +1777,7 @@ function CompletionMenu({ active, mention, index, rows }: {
|
|
|
1634
1777
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
1635
1778
|
* box passes every key through untouched.
|
|
1636
1779
|
*/
|
|
1637
|
-
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openHelp, openMode, openResume, openPlugin, openStatusline, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed }: {
|
|
1780
|
+
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, 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, waveTick, waveTier, waveStyle }: {
|
|
1638
1781
|
active: boolean
|
|
1639
1782
|
frozen: boolean
|
|
1640
1783
|
busy: boolean
|
|
@@ -1645,11 +1788,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
1645
1788
|
interrupt(): boolean
|
|
1646
1789
|
quit(): void
|
|
1647
1790
|
openModel(): void
|
|
1791
|
+
openEffort(): void
|
|
1648
1792
|
openHelp(): void
|
|
1649
1793
|
openMode(): void
|
|
1650
1794
|
openResume(): void
|
|
1651
1795
|
openPlugin(query?: string): void
|
|
1652
1796
|
openStatusline(): void
|
|
1797
|
+
openTheme(): void
|
|
1653
1798
|
openHistory(): void
|
|
1654
1799
|
createSession(mode?: string): void
|
|
1655
1800
|
cancelSessionSwitch(): boolean
|
|
@@ -1678,6 +1823,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
1678
1823
|
historyFill: { text: string; index: number } | undefined
|
|
1679
1824
|
/** Marks the accepted entry consumed (called after the fill is applied). */
|
|
1680
1825
|
historyConsumed(): void
|
|
1826
|
+
/** DeepSeek easter-egg wave frame (null when static): the composer's input
|
|
1827
|
+
* row rides Codex's Wave sweep (33ms tick, 1.0s flash / 1.3s deepseek). */
|
|
1828
|
+
waveTick: number | null
|
|
1829
|
+
/** The wave tier of the applied official DeepSeek model (null otherwise):
|
|
1830
|
+
* drives the persistent prompt glyph/accent and the sparkle tier. */
|
|
1831
|
+
waveTier: DeepseekWaveTier | null
|
|
1832
|
+
/** The ignition style running, if any: Wave / Aurora / Pulse. */
|
|
1833
|
+
waveStyle: DeepseekWaveStyle | null
|
|
1681
1834
|
}): ReactElement {
|
|
1682
1835
|
const columns = useStdout().stdout?.columns ?? 80
|
|
1683
1836
|
const [value, setValue] = useState('')
|
|
@@ -1915,6 +2068,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
1915
2068
|
openModel()
|
|
1916
2069
|
return
|
|
1917
2070
|
}
|
|
2071
|
+
if (text === '/effort' || text.startsWith('/effort ')) {
|
|
2072
|
+
openEffort()
|
|
2073
|
+
return
|
|
2074
|
+
}
|
|
1918
2075
|
if (text === '/mode' || text.startsWith('/mode ')) {
|
|
1919
2076
|
const mode = text.slice(5).trim()
|
|
1920
2077
|
if (mode === '') openMode()
|
|
@@ -1943,6 +2100,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
1943
2100
|
openStatusline()
|
|
1944
2101
|
return
|
|
1945
2102
|
}
|
|
2103
|
+
if (text === '/theme') {
|
|
2104
|
+
openTheme()
|
|
2105
|
+
return
|
|
2106
|
+
}
|
|
1946
2107
|
if (text === '/history') {
|
|
1947
2108
|
openHistory()
|
|
1948
2109
|
return
|
|
@@ -2077,55 +2238,136 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2077
2238
|
|
|
2078
2239
|
// Every exclusive panel keeps the composer as a stable visual anchor, but
|
|
2079
2240
|
// freezes it to one row: no menu, multiline wrap, or animation.
|
|
2241
|
+
const tierActive = waveTier !== null
|
|
2242
|
+
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier)
|
|
2243
|
+
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0])
|
|
2244
|
+
const promptGlyph = waveTier === 'flash' ? '›' : waveTier === 'deepseek' ? '»' : '❯'
|
|
2080
2245
|
if (frozen) {
|
|
2081
2246
|
const frozen = value === ''
|
|
2082
2247
|
? 'type a message'
|
|
2083
2248
|
: verboseLine(value, Math.max(1, columns - 6))
|
|
2084
2249
|
return createElement(
|
|
2085
2250
|
Box,
|
|
2086
|
-
{ width: Math.max(1, columns - 1), borderStyle: 'round', borderColor: inkColor(
|
|
2251
|
+
{ width: Math.max(1, columns - 1), borderStyle: 'round', borderColor: inkColor(getPalette().dim), paddingX: 1 },
|
|
2087
2252
|
createElement(
|
|
2088
2253
|
Text,
|
|
2089
2254
|
{ wrap: 'truncate-end' },
|
|
2090
|
-
createElement(Text, { color:
|
|
2255
|
+
createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, busy ? '… ' : `${promptGlyph} `),
|
|
2091
2256
|
frozen,
|
|
2092
2257
|
),
|
|
2093
2258
|
)
|
|
2094
2259
|
}
|
|
2095
2260
|
|
|
2261
|
+
// The bordered frame: static dim at rest; while the wave runs the border
|
|
2262
|
+
// breathes with the sweep (dim blends toward the tier accent and back), so
|
|
2263
|
+
// the frame glows up while the crest crosses the row.
|
|
2264
|
+
const frame = (row: ReactElement, borderRgb?: RgbTriple): ReactElement => createElement(
|
|
2265
|
+
Box,
|
|
2266
|
+
{ width: Math.max(1, columns - 1), borderStyle: 'round', borderColor: inkColor(borderRgb ?? getPalette().dim), paddingX: 1 },
|
|
2267
|
+
row,
|
|
2268
|
+
)
|
|
2269
|
+
const menu = createElement(CompletionMenu, {
|
|
2270
|
+
active: menuActive,
|
|
2271
|
+
mention: mentionActive,
|
|
2272
|
+
index: completionIndex,
|
|
2273
|
+
rows: menuRows,
|
|
2274
|
+
})
|
|
2275
|
+
|
|
2276
|
+
// Static row (idle, busy, or after the wave): prompt + editor window. The
|
|
2277
|
+
// prompt marker keeps the tier accent while an official DeepSeek model is
|
|
2278
|
+
// applied, restoring the static brand ❯ on any other route.
|
|
2096
2279
|
const editor = editorWindow(value, cursor, Math.max(1, columns - 6))
|
|
2280
|
+
const staticRow = createElement(
|
|
2281
|
+
Text,
|
|
2282
|
+
{ wrap: 'truncate-end' },
|
|
2283
|
+
busy
|
|
2284
|
+
? createElement(BusyChase)
|
|
2285
|
+
: createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, `${promptGlyph} `),
|
|
2286
|
+
value === '' ? undefined : editor.before,
|
|
2287
|
+
createElement(CursorBlock, { char: editor.caret }),
|
|
2288
|
+
value === '' && !busy
|
|
2289
|
+
? createElement(Text, { dimColor: true }, COMPOSER_PLACEHOLDER)
|
|
2290
|
+
: editor.after,
|
|
2291
|
+
)
|
|
2292
|
+
|
|
2293
|
+
// Wave row: the input row assembled column by column, each cell carrying
|
|
2294
|
+
// the sampled wave `backgroundColor` (null outside the crest → transparent),
|
|
2295
|
+
// so the crest sweeps the FULL content row — prompt, draft, cursor,
|
|
2296
|
+
// placeholder, and the trailing blank fill. The deepseek tier drops the
|
|
2297
|
+
// `· ✦ ✧` sparkles into the rightmost blank cell from 900ms on.
|
|
2298
|
+
const waveRow = (): ReactElement => {
|
|
2299
|
+
const contentWidth = Math.max(1, columns - 5)
|
|
2300
|
+
const waveEditor = editorWindow(value, cursor, Math.max(1, contentWidth - 2))
|
|
2301
|
+
const hues = deepseekWaveHues(waveTier!)
|
|
2302
|
+
const style = waveStyle!
|
|
2303
|
+
const base = getTheme() === 'light' ? WAVE_BASE_LIGHT : WAVE_BASE_DARK
|
|
2304
|
+
const waveBg = (column: number): string | undefined => {
|
|
2305
|
+
const rgb = deepseekWaveColumnBg(waveTick!, column, contentWidth, waveTier!, style, hues, base)
|
|
2306
|
+
return rgb === null ? undefined : inkColor(rgb)
|
|
2307
|
+
}
|
|
2308
|
+
const cells: ComposerCell[] = []
|
|
2309
|
+
cells.push({ char: promptGlyph, color: promptColor, bold: true, backgroundColor: waveBg(0) })
|
|
2310
|
+
cells.push({ char: ' ', color: promptColor, backgroundColor: waveBg(1) })
|
|
2311
|
+
for (const char of waveEditor.before) {
|
|
2312
|
+
cells.push({ char, backgroundColor: waveBg(cells.length) })
|
|
2313
|
+
}
|
|
2314
|
+
cells.push({ char: waveEditor.caret, inverse: true, backgroundColor: waveBg(cells.length) })
|
|
2315
|
+
if (value === '' && !busy) {
|
|
2316
|
+
for (let at = 0; at < COMPOSER_PLACEHOLDER.length; at += 1) {
|
|
2317
|
+
cells.push({ char: COMPOSER_PLACEHOLDER[at]!, dim: true, backgroundColor: waveBg(cells.length) })
|
|
2318
|
+
}
|
|
2319
|
+
} else {
|
|
2320
|
+
for (const char of waveEditor.after) {
|
|
2321
|
+
cells.push({ char, backgroundColor: waveBg(cells.length) })
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
while (cells.length < contentWidth) {
|
|
2325
|
+
cells.push({ char: ' ', backgroundColor: waveBg(cells.length) })
|
|
2326
|
+
}
|
|
2327
|
+
// The brand wordmark rides the wave's middle: `deepseek` in the tier's
|
|
2328
|
+
// cycled hues, placed in the row's mid-section and only over blank or
|
|
2329
|
+
// placeholder cells — real draft text is never covered.
|
|
2330
|
+
if (deepseekWaveWordVisible(waveTick!, waveTier!, style)) {
|
|
2331
|
+
const word = 'deepseek'
|
|
2332
|
+
const start = Math.max(2, Math.floor((contentWidth - word.length) / 2))
|
|
2333
|
+
let clear = true
|
|
2334
|
+
for (let at = 0; at < word.length; at += 1) {
|
|
2335
|
+
const cell = cells[start + at]
|
|
2336
|
+
if (cell === undefined || (cell.char !== ' ' && cell.dim !== true)) { clear = false; break }
|
|
2337
|
+
}
|
|
2338
|
+
if (clear) {
|
|
2339
|
+
for (let at = 0; at < word.length; at += 1) {
|
|
2340
|
+
const cell = cells[start + at]!
|
|
2341
|
+
cell.char = word[at]!
|
|
2342
|
+
cell.color = inkColor(deepseekWaveWordHue(at, hues))
|
|
2343
|
+
cell.bold = true
|
|
2344
|
+
cell.dim = false
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
// The tail sparkles belong to the Wave style's deepseek tier only
|
|
2349
|
+
// (Codex paints spark_frame on Wave+Ultra).
|
|
2350
|
+
if (waveTier === 'deepseek' && style === 'wave') {
|
|
2351
|
+
const spark = deepseekWaveSpark(waveTick!)
|
|
2352
|
+
if (spark !== null) {
|
|
2353
|
+
const last = cells[cells.length - 1]
|
|
2354
|
+
if (last !== undefined && last.char === ' ') {
|
|
2355
|
+
last.char = spark
|
|
2356
|
+
last.color = promptColor
|
|
2357
|
+
last.bold = true
|
|
2358
|
+
last.dim = false
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const borderRgb = deepseekWaveBorderColor(waveTick!, waveTier!, style, hues, getPalette().dim)
|
|
2363
|
+
return frame(createElement(Text, { wrap: 'truncate-end' }, ...waveRowSpans(cells)), borderRgb)
|
|
2364
|
+
}
|
|
2097
2365
|
|
|
2098
2366
|
return createElement(
|
|
2099
2367
|
Box,
|
|
2100
2368
|
{ flexDirection: 'column' },
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
createElement(CompletionMenu, {
|
|
2104
|
-
active: menuActive,
|
|
2105
|
-
mention: mentionActive,
|
|
2106
|
-
index: completionIndex,
|
|
2107
|
-
rows: menuRows,
|
|
2108
|
-
}),
|
|
2109
|
-
// The framed input box: a visible boundary so the prompt never blends
|
|
2110
|
-
// into the transcript above it; the cursor block sits immediately after
|
|
2111
|
-
// the prompt marker (leftmost), with the dim placeholder trailing it —
|
|
2112
|
-
// no extra space, so the empty state reads `❯ ▮type a message…`.
|
|
2113
|
-
createElement(
|
|
2114
|
-
Box,
|
|
2115
|
-
{ width: Math.max(1, columns - 1), borderStyle: 'round', borderColor: inkColor(TUI_RGB.dim), paddingX: 1 },
|
|
2116
|
-
createElement(
|
|
2117
|
-
Text,
|
|
2118
|
-
{ wrap: 'truncate-end' },
|
|
2119
|
-
busy
|
|
2120
|
-
? createElement(BusyChase)
|
|
2121
|
-
: createElement(Text, { color: inkColor(TUI_RGB.brand) }, '❯ '),
|
|
2122
|
-
value === '' ? undefined : editor.before,
|
|
2123
|
-
createElement(CursorBlock, { char: editor.caret }),
|
|
2124
|
-
value === '' && !busy
|
|
2125
|
-
? createElement(Text, { dimColor: true }, 'type a message · / commands · @ mentions')
|
|
2126
|
-
: editor.after,
|
|
2127
|
-
),
|
|
2128
|
-
),
|
|
2369
|
+
menu,
|
|
2370
|
+
waveTick !== null && waveTier !== null && !busy ? waveRow() : frame(staticRow),
|
|
2129
2371
|
)
|
|
2130
2372
|
}
|
|
2131
2373
|
|
|
@@ -2136,6 +2378,61 @@ export function App(props: AppProps): ReactElement {
|
|
|
2136
2378
|
const skills = useSyncExternalStore(props.skills.subscribe, () => props.skills.rows)
|
|
2137
2379
|
const [modelLabel, setModelLabel] = useState(props.model)
|
|
2138
2380
|
const [modelOpen, setModelOpen] = useState(false)
|
|
2381
|
+
/** The model row whose effort levels the /model stage lists; undefined shows the model list. */
|
|
2382
|
+
const [effortFor, setEffortFor] = useState<ModelRow | undefined>(undefined)
|
|
2383
|
+
/** Effective reasoning effort, shown in the /model picker and switch notice. */
|
|
2384
|
+
const [effortLabel, setEffortLabel] = useState<string | undefined>(props.effort)
|
|
2385
|
+
/** DeepSeek easter egg: switching INTO an official DeepSeek route plays
|
|
2386
|
+
* one of Codex's three ignition styles (Wave / Aurora / Pulse, picked at
|
|
2387
|
+
* random without repeating) across the composer's padded band (33ms tick,
|
|
2388
|
+
* per-style durations), then the band returns to static while the prompt
|
|
2389
|
+
* marker keeps the tier accent. The trigger follows the applied model
|
|
2390
|
+
* label (what the status bar actually shows), never the initial paint,
|
|
2391
|
+
* and the tier is derived from the label and cached at the switch. */
|
|
2392
|
+
const [waveTick, setWaveTick] = useState<number | null>(null)
|
|
2393
|
+
const [waveTier, setWaveTier] = useState<DeepseekWaveTier | null>(null)
|
|
2394
|
+
const [waveStyle, setWaveStyle] = useState<DeepseekWaveStyle | null>(null)
|
|
2395
|
+
const previousModel = useRef<string | undefined>(undefined)
|
|
2396
|
+
const previousEffort = useRef<string | undefined>(props.effort)
|
|
2397
|
+
const previousStyle = useRef<DeepseekWaveStyle | undefined>(undefined)
|
|
2398
|
+
useEffect(() => {
|
|
2399
|
+
const previous = previousModel.current
|
|
2400
|
+
previousModel.current = modelLabel
|
|
2401
|
+
// The wave replays when the applied model changes OR its effort level
|
|
2402
|
+
// changes on the same official DeepSeek route (Codex replays the
|
|
2403
|
+
// ignition on effort changes too).
|
|
2404
|
+
const effortChanged = previousEffort.current !== effortLabel
|
|
2405
|
+
previousEffort.current = effortLabel
|
|
2406
|
+
const modelChanged = previous !== undefined && previous !== modelLabel
|
|
2407
|
+
if (!isOfficialDeepSeekLabel(modelLabel)) {
|
|
2408
|
+
setWaveTier(null)
|
|
2409
|
+
setWaveStyle(null)
|
|
2410
|
+
setWaveTick(null)
|
|
2411
|
+
return
|
|
2412
|
+
}
|
|
2413
|
+
if (modelChanged || effortChanged) {
|
|
2414
|
+
setWaveTier(deepseekWaveTier(modelLabel))
|
|
2415
|
+
const nextStyle = deepseekWaveStyleRandom(previousStyle.current)
|
|
2416
|
+
previousStyle.current = nextStyle
|
|
2417
|
+
setWaveStyle(nextStyle)
|
|
2418
|
+
setWaveTick(0)
|
|
2419
|
+
}
|
|
2420
|
+
}, [modelLabel, effortLabel])
|
|
2421
|
+
const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null
|
|
2422
|
+
&& waveTick * DEEPSEEK_WAVE_TICK_MS < deepseekWaveDuration(waveTier, waveStyle)
|
|
2423
|
+
useEffect(() => {
|
|
2424
|
+
if (!waveActive) return
|
|
2425
|
+
const id = setInterval(() => {
|
|
2426
|
+
setWaveTick(current => (current === null ? 0 : current + 1))
|
|
2427
|
+
}, DEEPSEEK_WAVE_TICK_MS)
|
|
2428
|
+
return () => {
|
|
2429
|
+
clearInterval(id)
|
|
2430
|
+
}
|
|
2431
|
+
}, [waveActive])
|
|
2432
|
+
useEffect(() => {
|
|
2433
|
+
if (waveTick !== null && waveTier !== null && waveStyle !== null
|
|
2434
|
+
&& waveTick * DEEPSEEK_WAVE_TICK_MS >= deepseekWaveDuration(waveTier, waveStyle)) setWaveTick(null)
|
|
2435
|
+
}, [waveTick, waveTier, waveStyle])
|
|
2139
2436
|
const [directory, setDirectory] = useState<ModelDirectory | undefined>(undefined)
|
|
2140
2437
|
const [modelError, setModelError] = useState<string | undefined>(undefined)
|
|
2141
2438
|
const [modelLoadEpoch, setModelLoadEpoch] = useState(0)
|
|
@@ -2175,6 +2472,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
2175
2472
|
const [pluginQuery, setPluginQuery] = useState('')
|
|
2176
2473
|
const [statuslineOpen, setStatuslineOpen] = useState(false)
|
|
2177
2474
|
const [statuslineItems, setStatuslineItems] = useState<readonly StatusItemId[]>(() => parseStatuslineItems(props.statusline))
|
|
2475
|
+
const [themeOpen, setThemeOpen] = useState(false)
|
|
2178
2476
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
2179
2477
|
/** The /history panel's accepted entry: text plus its recall-space index. */
|
|
2180
2478
|
const [historyFill, setHistoryFill] = useState<{ text: string; index: number } | undefined>(undefined)
|
|
@@ -2202,18 +2500,20 @@ export function App(props: AppProps): ReactElement {
|
|
|
2202
2500
|
const approvalPending = approvalSnapshot.pending !== undefined
|
|
2203
2501
|
const questionPending = questionSnapshot.pending !== undefined
|
|
2204
2502
|
// While any modal owns the keys, the prompt box passes everything through.
|
|
2205
|
-
const inputActive = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending
|
|
2503
|
+
const inputActive = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending
|
|
2206
2504
|
|
|
2207
2505
|
// Human questions outrank local inspectors. Close the lower modal instead
|
|
2208
2506
|
// of leaving an approval/question visible but keyboard-locked behind it.
|
|
2209
2507
|
useEffect(() => {
|
|
2210
2508
|
if (!approvalPending && !questionPending) return
|
|
2211
2509
|
setModelOpen(false)
|
|
2510
|
+
setEffortFor(undefined)
|
|
2212
2511
|
setHelpOpen(false)
|
|
2213
2512
|
setModeOpen(false)
|
|
2214
2513
|
setResumeOpen(false)
|
|
2215
2514
|
setPluginOpen(false)
|
|
2216
2515
|
setStatuslineOpen(false)
|
|
2516
|
+
setThemeOpen(false)
|
|
2217
2517
|
setHistoryOpen(false)
|
|
2218
2518
|
setVerboseOpen(false)
|
|
2219
2519
|
}, [approvalPending, questionPending])
|
|
@@ -2319,9 +2619,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
2319
2619
|
? Math.max(1, Math.floor(streamRows / 3))
|
|
2320
2620
|
: 1
|
|
2321
2621
|
const answerRows = view.streaming === '' ? 0 : Math.max(1, streamRows - reasoningRows)
|
|
2322
|
-
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending
|
|
2622
|
+
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending
|
|
2323
2623
|
const inspectorVisible = verboseOpen && !approvalPending && !questionPending
|
|
2324
|
-
const modalVisible = modelOpen || helpOpen || modeOpen || resumeOpen || pluginOpen || statuslineOpen || historyOpen || inspectorVisible || approvalPending || questionPending
|
|
2624
|
+
const modalVisible = modelOpen || helpOpen || modeOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || inspectorVisible || approvalPending || questionPending
|
|
2325
2625
|
const closeInspector = useCallback((): void => {
|
|
2326
2626
|
setVerboseOpen(false)
|
|
2327
2627
|
}, [])
|
|
@@ -2330,6 +2630,20 @@ export function App(props: AppProps): ReactElement {
|
|
|
2330
2630
|
setRefreshEpoch(epoch => epoch + 1)
|
|
2331
2631
|
}
|
|
2332
2632
|
|
|
2633
|
+
/** Apply one /model pick: record the selection, close the panel, report via notice. */
|
|
2634
|
+
const applyModel = (row: ModelRow, effortId: string | undefined): void => {
|
|
2635
|
+
try {
|
|
2636
|
+
const label = props.selectModel(row, effortId)
|
|
2637
|
+
setModelLabel(label)
|
|
2638
|
+
setEffortLabel(effortId)
|
|
2639
|
+
notify(`model → next step uses ${label}${effortId === undefined || effortId === '' ? '' : `@${effortId}`}`)
|
|
2640
|
+
setModelOpen(false)
|
|
2641
|
+
setEffortFor(undefined)
|
|
2642
|
+
} catch (error: unknown) {
|
|
2643
|
+
notify(`model switch failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2333
2647
|
return createElement(
|
|
2334
2648
|
Box,
|
|
2335
2649
|
{ flexDirection: 'column' },
|
|
@@ -2368,25 +2682,36 @@ export function App(props: AppProps): ReactElement {
|
|
|
2368
2682
|
createElement(QuestionBar, { store: props.questions, snapshot: questionSnapshot, locked: false }),
|
|
2369
2683
|
createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending }),
|
|
2370
2684
|
modelOpen && !approvalPending && !questionPending
|
|
2371
|
-
?
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2685
|
+
? effortFor === undefined
|
|
2686
|
+
? createElement(ModelPanel, {
|
|
2687
|
+
directory,
|
|
2688
|
+
error: modelError,
|
|
2689
|
+
onSelect: (row: ModelRow) => {
|
|
2690
|
+
// A model advertising several levels opens the effort stage first;
|
|
2691
|
+
// one advertised level is its only option (Codex's
|
|
2692
|
+
// single-supported-effort shortcut), and no capability applies
|
|
2693
|
+
// the model default exactly as before.
|
|
2694
|
+
if (row.reasoning !== undefined && row.reasoning.efforts.length > 1) {
|
|
2695
|
+
setEffortFor(row)
|
|
2696
|
+
return
|
|
2697
|
+
}
|
|
2698
|
+
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0]!.id : undefined
|
|
2699
|
+
applyModel(row, effortId)
|
|
2700
|
+
},
|
|
2701
|
+
onRetry: () => {
|
|
2702
|
+
setModelLoadEpoch(epoch => epoch + 1)
|
|
2703
|
+
},
|
|
2704
|
+
onClose: () => {
|
|
2378
2705
|
setModelOpen(false)
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
},
|
|
2389
|
-
})
|
|
2706
|
+
setEffortFor(undefined)
|
|
2707
|
+
},
|
|
2708
|
+
})
|
|
2709
|
+
: createElement(EffortPanel, {
|
|
2710
|
+
row: effortFor,
|
|
2711
|
+
current: effortLabel,
|
|
2712
|
+
select: (effortId: string) => applyModel(effortFor, effortId),
|
|
2713
|
+
back: () => setEffortFor(undefined),
|
|
2714
|
+
})
|
|
2390
2715
|
: undefined,
|
|
2391
2716
|
helpOpen && !approvalPending && !questionPending
|
|
2392
2717
|
? createElement(HelpPanel, {
|
|
@@ -2440,6 +2765,21 @@ export function App(props: AppProps): ReactElement {
|
|
|
2440
2765
|
close: () => setStatuslineOpen(false),
|
|
2441
2766
|
})
|
|
2442
2767
|
: undefined,
|
|
2768
|
+
themeOpen && !approvalPending && !questionPending
|
|
2769
|
+
? createElement(ThemePanel, {
|
|
2770
|
+
current: getTheme(),
|
|
2771
|
+
select: (name: ThemeName) => {
|
|
2772
|
+
// Apply immediately (module-level palette), persist through the
|
|
2773
|
+
// runner, then close: the close re-render paints with the new
|
|
2774
|
+
// palette. `auto` stores as requested; detection is a later step.
|
|
2775
|
+
setTheme(name)
|
|
2776
|
+
props.saveTheme?.(name)
|
|
2777
|
+
notify(`theme → ${name}`)
|
|
2778
|
+
setThemeOpen(false)
|
|
2779
|
+
},
|
|
2780
|
+
close: () => setThemeOpen(false),
|
|
2781
|
+
})
|
|
2782
|
+
: undefined,
|
|
2443
2783
|
historyOpen && !approvalPending && !questionPending
|
|
2444
2784
|
? createElement(HistoryPanel, {
|
|
2445
2785
|
entries: recallSpace,
|
|
@@ -2476,8 +2816,44 @@ export function App(props: AppProps): ReactElement {
|
|
|
2476
2816
|
openModel: () => {
|
|
2477
2817
|
setDirectory(undefined)
|
|
2478
2818
|
setModelError(undefined)
|
|
2819
|
+
setEffortFor(undefined)
|
|
2479
2820
|
setModelOpen(true)
|
|
2480
2821
|
},
|
|
2822
|
+
openEffort: () => {
|
|
2823
|
+
// /effort adjusts the CURRENT model's reasoning: resolve it from
|
|
2824
|
+
// the live catalog, then open the same effort stage the /model
|
|
2825
|
+
// picker would. Match on the applied label (what the status bar
|
|
2826
|
+
// shows) — `props.model` may still carry the deployment default
|
|
2827
|
+
// until the next request header lands. The model-id fallback
|
|
2828
|
+
// prefers a reasoning-capable row (several routes may serve the
|
|
2829
|
+
// same id), and a capability-lookup failure reads as "retry",
|
|
2830
|
+
// never as "the model has no efforts" — the adapter advertises
|
|
2831
|
+
// levels for every deepseek model, so "no efforts" is almost
|
|
2832
|
+
// always a failed resolveModelInfo, not a fact.
|
|
2833
|
+
void props.loadModels().then((loaded) => {
|
|
2834
|
+
const [provider, model] = modelLabel.split('/')
|
|
2835
|
+
const row = loaded.rows.find(candidate => candidate.provider === provider && candidate.model === model)
|
|
2836
|
+
?? loaded.rows.find(candidate => candidate.model === model && candidate.reasoning !== undefined)
|
|
2837
|
+
?? loaded.rows.find(candidate => candidate.model === model)
|
|
2838
|
+
if (row === undefined) {
|
|
2839
|
+
notify('current model is not in the catalog', 'warning')
|
|
2840
|
+
return
|
|
2841
|
+
}
|
|
2842
|
+
const rowTag = `${row.provider}/${row.model}`
|
|
2843
|
+
if (loaded.reasoningFailures?.includes(rowTag) === true) {
|
|
2844
|
+
notify('reasoning levels temporarily unavailable (capability lookup failed) — try again', 'warning')
|
|
2845
|
+
return
|
|
2846
|
+
}
|
|
2847
|
+
if (row.reasoning === undefined || row.reasoning.efforts.length === 0) {
|
|
2848
|
+
notify('current model does not expose reasoning efforts', 'warning')
|
|
2849
|
+
return
|
|
2850
|
+
}
|
|
2851
|
+
setEffortFor(row)
|
|
2852
|
+
setModelOpen(true)
|
|
2853
|
+
}, (error: unknown) => {
|
|
2854
|
+
notify(`model lookup failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
2855
|
+
})
|
|
2856
|
+
},
|
|
2481
2857
|
openHelp: () => {
|
|
2482
2858
|
setHelpOpen(true)
|
|
2483
2859
|
},
|
|
@@ -2485,6 +2861,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
2485
2861
|
openResume: () => setResumeOpen(true),
|
|
2486
2862
|
openPlugin: (query = '') => { setPluginQuery(query); setPluginOpen(true) },
|
|
2487
2863
|
openStatusline: () => setStatuslineOpen(true),
|
|
2864
|
+
openTheme: () => setThemeOpen(true),
|
|
2488
2865
|
openHistory: () => setHistoryOpen(true),
|
|
2489
2866
|
createSession: props.createSession,
|
|
2490
2867
|
cancelSessionSwitch: props.cancelSessionSwitch,
|
|
@@ -2519,6 +2896,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
2519
2896
|
cancelQueued: props.cancelQueued,
|
|
2520
2897
|
historyFill,
|
|
2521
2898
|
historyConsumed,
|
|
2899
|
+
waveTick,
|
|
2900
|
+
waveTier,
|
|
2901
|
+
waveStyle,
|
|
2522
2902
|
}),
|
|
2523
2903
|
createElement(StatusLine, {
|
|
2524
2904
|
facts: {
|