dsh-code 1.0.1 → 1.0.2
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 -4
- package/README.md +4 -4
- package/lib/index.mjs +754 -380
- package/lib/types/app.d.ts +2 -13
- package/lib/types/attachments.d.ts +1 -1
- package/lib/types/keyboard.d.ts +31 -0
- package/lib/types/mentions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +27 -11
- package/lib/types/render/editor.d.ts +32 -7
- package/lib/types/render/lines.d.ts +1 -1
- package/package.json +1 -1
- package/src/app.ts +596 -503
- package/src/attachments.ts +7 -0
- package/src/index.ts +1 -1
- package/src/internals.ts +26 -9
- package/src/keyboard.ts +131 -7
- package/src/mentions.ts +6 -1
- package/src/render/animations.ts +64 -17
- package/src/render/editor.ts +125 -25
- package/src/render/lines.ts +16 -2
package/src/app.ts
CHANGED
|
@@ -40,16 +40,20 @@ import type { TranscriptStore } from './store.ts'
|
|
|
40
40
|
import { settledEntryCount, type TranscriptEntry } from './render/projection.ts'
|
|
41
41
|
import { type MdSegment, visibleColumns } from './render/markdown.ts'
|
|
42
42
|
import {
|
|
43
|
-
busyChaseFrame,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
busyChaseFrame,
|
|
44
|
+
BUSY_CHASE_TICK_MS,
|
|
45
|
+
caretVisible,
|
|
46
|
+
DEEP_DIVING_SHIMMER_TICK_MS,
|
|
47
|
+
DEEPSEEK_WAVE_TICK_MS,
|
|
48
|
+
deepseekWaveColumnBg,
|
|
49
|
+
deepseekWaveDuration,
|
|
50
|
+
deepseekWaveSpark,
|
|
51
|
+
deepseekWaveStyleRandom,
|
|
52
|
+
deepseekWaveTier,
|
|
53
|
+
deepseekWaveWordHue,
|
|
54
|
+
deepseekWaveWordVisible,
|
|
55
|
+
deepDivingGradientColor,
|
|
56
|
+
deepDivingSparkColor,
|
|
53
57
|
effortAboveHigh,
|
|
54
58
|
isOfficialDeepSeekLabel,
|
|
55
59
|
type DeepseekWaveStyle,
|
|
@@ -61,7 +65,7 @@ import type { ModelDirectory, ModelRow } from './models.ts'
|
|
|
61
65
|
import type { ProviderConfiguration, ProviderSettingsDirectory, ProviderTargetView } from './provider-settings.ts'
|
|
62
66
|
import type { QuestionSnapshot, QuestionStore } from './questions.ts'
|
|
63
67
|
import type { SkillsView, SkillRow } from './skills.ts'
|
|
64
|
-
import type
|
|
68
|
+
import { isPathLikeMentionQuery, type MentionCandidate } from './mentions.ts'
|
|
65
69
|
import type { SubagentFeedView, SubagentRow } from './subagents.ts'
|
|
66
70
|
import { AgentsPanel, EffortPanel, HistoryPanel, JobsPanel, ModePanel, PermissionPanel, PluginPanel, ResumePanel, StatuslinePanel, runClock, SubagentPanel, type JobRow } from './kernel-panels.ts'
|
|
67
71
|
import type { PresetRow } from './presets.ts'
|
|
@@ -140,7 +144,16 @@ import {
|
|
|
140
144
|
type StatusTone,
|
|
141
145
|
} from './render/status.ts'
|
|
142
146
|
import { displayTail, displayText, singleLineText, truncateColumns } from './render/text.ts'
|
|
143
|
-
import {
|
|
147
|
+
import {
|
|
148
|
+
isVsCodeTerminalEnv,
|
|
149
|
+
normalizeKeyboardChunk,
|
|
150
|
+
PASTE_END_MARKER,
|
|
151
|
+
PASTE_START_MARKER,
|
|
152
|
+
stripPasteMarkers,
|
|
153
|
+
stripTerminalFocusEvents,
|
|
154
|
+
tokenizeRawEditorChunk,
|
|
155
|
+
type RawEditorToken,
|
|
156
|
+
} from './keyboard.ts'
|
|
144
157
|
import {
|
|
145
158
|
clampScroll,
|
|
146
159
|
followInspectorCursor,
|
|
@@ -168,18 +181,22 @@ import {
|
|
|
168
181
|
deleteBackward,
|
|
169
182
|
deleteForward,
|
|
170
183
|
deleteLastGrapheme,
|
|
171
|
-
deleteWordBackward,
|
|
172
|
-
deleteWordForward,
|
|
173
|
-
editorModel,
|
|
184
|
+
deleteWordBackward,
|
|
185
|
+
deleteWordForward,
|
|
186
|
+
editorModel,
|
|
187
|
+
editorRowParts,
|
|
174
188
|
insertText,
|
|
175
189
|
type EditResult,
|
|
176
190
|
killToLineEnd,
|
|
177
191
|
killToLineStart,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
192
|
+
moveCursorBy,
|
|
193
|
+
moveCursorVertically,
|
|
194
|
+
moveToLineEnd,
|
|
195
|
+
moveToLineStart,
|
|
196
|
+
moveWordLeft,
|
|
197
|
+
moveWordRight,
|
|
198
|
+
remapStableRange,
|
|
199
|
+
replaceRangePreservingCursor,
|
|
183
200
|
sanitizeDraftText,
|
|
184
201
|
shouldRecallNavigate,
|
|
185
202
|
splitGraphemes,
|
|
@@ -265,7 +282,7 @@ export interface AppProps {
|
|
|
265
282
|
/** Validate draft image paths without committing attachment objects. */
|
|
266
283
|
inspectImages(paths: readonly string[]): Promise<readonly ImagePathInspection[]>
|
|
267
284
|
/** Validate, normalize and persist images immediately before submission. */
|
|
268
|
-
prepareImages(paths: readonly string[]): Promise<readonly ImageBlock[]>
|
|
285
|
+
prepareImages(paths: readonly string[], signal?: AbortSignal): Promise<readonly ImageBlock[]>
|
|
269
286
|
/** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
|
|
270
287
|
selectModel(row: ModelRow, effortId?: string): string
|
|
271
288
|
/** The /subagent override label, '' when delegated agents follow the current model. */
|
|
@@ -355,14 +372,15 @@ function padColumns(text: string, width: number): string {
|
|
|
355
372
|
}
|
|
356
373
|
|
|
357
374
|
/** Interval-driven frame counter for one self-contained animated leaf. */
|
|
358
|
-
function useFrames(intervalMs: number): number {
|
|
359
|
-
const [tick, setTick] = useState(0)
|
|
360
|
-
useEffect(() => {
|
|
361
|
-
|
|
375
|
+
function useFrames(intervalMs: number, active = true): number {
|
|
376
|
+
const [tick, setTick] = useState(0)
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
if (!active) return
|
|
379
|
+
const id = setInterval(() => setTick(current => current + 1), intervalMs)
|
|
362
380
|
return () => {
|
|
363
381
|
clearInterval(id)
|
|
364
382
|
}
|
|
365
|
-
}, [intervalMs])
|
|
383
|
+
}, [active, intervalMs])
|
|
366
384
|
return tick
|
|
367
385
|
}
|
|
368
386
|
|
|
@@ -381,16 +399,11 @@ function useStableInput(handler: (input: string, key: Key) => void, active: bool
|
|
|
381
399
|
useInput(stableHandler, { isActive: active })
|
|
382
400
|
}
|
|
383
401
|
|
|
384
|
-
/**
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
*/
|
|
390
|
-
function BusyChase(): ReactElement {
|
|
391
|
-
const tick = useFrames(125)
|
|
392
|
-
return createElement(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + ' ')
|
|
393
|
-
}
|
|
402
|
+
/** The original web StateDot chase used by the busy composer marker. */
|
|
403
|
+
function BusyChase(): ReactElement {
|
|
404
|
+
const tick = useFrames(BUSY_CHASE_TICK_MS)
|
|
405
|
+
return createElement(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + ' ')
|
|
406
|
+
}
|
|
394
407
|
|
|
395
408
|
/** Blinking block caret appended to streaming text. */
|
|
396
409
|
function Caret(): ReactElement {
|
|
@@ -398,32 +411,54 @@ function Caret(): ReactElement {
|
|
|
398
411
|
return createElement(Text, null, caretVisible(tick) ? '▍' : ' ')
|
|
399
412
|
}
|
|
400
413
|
|
|
401
|
-
/**
|
|
402
|
-
function
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
414
|
+
/** One resettable input-caret phase shared by the entire composer. */
|
|
415
|
+
function useCursorBlink(active: boolean): { visible: boolean; reset(): void } {
|
|
416
|
+
const [epoch, setEpoch] = useState(0)
|
|
417
|
+
const [visible, setVisible] = useState(true)
|
|
418
|
+
useEffect(() => {
|
|
419
|
+
setVisible(true)
|
|
420
|
+
if (!active) return
|
|
421
|
+
const id = setInterval(() => setVisible(current => !current), 530)
|
|
422
|
+
return () => {
|
|
423
|
+
clearInterval(id)
|
|
424
|
+
}
|
|
425
|
+
}, [active, epoch])
|
|
426
|
+
const reset = useCallback((): void => {
|
|
427
|
+
setVisible(true)
|
|
428
|
+
setEpoch(current => current + 1)
|
|
429
|
+
}, [])
|
|
430
|
+
return { visible, reset }
|
|
431
|
+
}
|
|
406
432
|
|
|
407
433
|
/**
|
|
408
|
-
* The busy line, web TurnStatus contract:
|
|
409
|
-
* `Deep diving...` label, with the elapsed clock appended
|
|
410
|
-
* has clearly been running (15s) — anchored to `turn/start`
|
|
411
|
-
* mid-turn keeps the real time.
|
|
412
|
-
*/
|
|
413
|
-
function DeepDivingLine({ since }: { since: number }): ReactElement {
|
|
414
|
-
useFrames(
|
|
415
|
-
const elapsed = since === 0 ? 0 : Date.now() - since
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
434
|
+
* The busy line, web TurnStatus contract: a continuously moving blue gradient
|
|
435
|
+
* paints the complete `Deep diving...` label, with the elapsed clock appended
|
|
436
|
+
* only once the turn has clearly been running (15s) — anchored to `turn/start`
|
|
437
|
+
* so a resumed mid-turn keeps the real time.
|
|
438
|
+
*/
|
|
439
|
+
function DeepDivingLine({ since }: { since: number }): ReactElement {
|
|
440
|
+
const tick = useFrames(DEEP_DIVING_SHIMMER_TICK_MS)
|
|
441
|
+
const elapsed = since === 0 ? 0 : Date.now() - since
|
|
442
|
+
const text = elapsed >= 15_000 ? `✻ Deep diving... ${runClock(elapsed)}` : '✻ Deep diving...'
|
|
443
|
+
const palette = getPalette()
|
|
444
|
+
const graphemes = splitGraphemes(text)
|
|
445
|
+
return createElement(
|
|
446
|
+
Text,
|
|
447
|
+
{ wrap: 'truncate-end' },
|
|
448
|
+
...graphemes.map((grapheme, index) => {
|
|
449
|
+
const sparkle = grapheme.text === '✻'
|
|
450
|
+
return createElement(
|
|
451
|
+
Text,
|
|
452
|
+
{
|
|
453
|
+
key: `${grapheme.start}-${grapheme.end}`,
|
|
454
|
+
color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
|
|
455
|
+
bold: sparkle || undefined,
|
|
456
|
+
},
|
|
457
|
+
grapheme.text,
|
|
458
|
+
)
|
|
459
|
+
}),
|
|
460
|
+
)
|
|
461
|
+
}
|
|
427
462
|
|
|
428
463
|
/**
|
|
429
464
|
* The streaming buffer rendered with a hard size cap: the live region must
|
|
@@ -2054,83 +2089,13 @@ function verboseLine(text: string, columns: number): string {
|
|
|
2054
2089
|
return truncateColumns(displayText(text).replace(/\n/gu, ' ↵ ').replace(/\t/gu, ' '), Math.max(1, columns))
|
|
2055
2090
|
}
|
|
2056
2091
|
|
|
2057
|
-
/**
|
|
2058
|
-
|
|
2059
|
-
* arrive with `input === ''` and no flag, and Backspace vs Delete both
|
|
2060
|
-
* collapse onto `key.delete`. The composer patches `stdin.read` — the one
|
|
2061
|
-
* choke point every Ink input chunk already passes through — and annotates
|
|
2062
|
-
* the exact sequences the editor must own; Ink's own view of the same chunk
|
|
2063
|
-
* is a no-op for every one of them.
|
|
2064
|
-
*/
|
|
2065
|
-
type RawKeyAnnotation =
|
|
2066
|
-
| 'home'
|
|
2067
|
-
| 'end'
|
|
2068
|
-
| 'delete-backward'
|
|
2069
|
-
| 'delete-word-backward'
|
|
2070
|
-
| 'delete-forward'
|
|
2071
|
-
| 'delete-word-forward'
|
|
2072
|
-
| undefined
|
|
2073
|
-
|
|
2074
|
-
/** Identify one whole-chunk key sequence Ink drops or blurs. */
|
|
2075
|
-
function annotateRawKey(chunk: string): RawKeyAnnotation {
|
|
2076
|
-
switch (chunk) {
|
|
2077
|
-
case '':
|
|
2078
|
-
return 'delete-backward'
|
|
2079
|
-
case '':
|
|
2080
|
-
case '':
|
|
2081
|
-
return 'delete-word-backward'
|
|
2082
|
-
case '[3~':
|
|
2083
|
-
case '[3;2~':
|
|
2084
|
-
return 'delete-forward'
|
|
2085
|
-
case '[3;3~':
|
|
2086
|
-
case '[3;5~':
|
|
2087
|
-
return 'delete-word-forward'
|
|
2088
|
-
case '[H':
|
|
2089
|
-
case '[1~':
|
|
2090
|
-
case '[7~':
|
|
2091
|
-
case 'OH':
|
|
2092
|
-
return 'home'
|
|
2093
|
-
case '[F':
|
|
2094
|
-
case '[4~':
|
|
2095
|
-
case '[8~':
|
|
2096
|
-
case 'OF':
|
|
2097
|
-
return 'end'
|
|
2098
|
-
default:
|
|
2099
|
-
return undefined
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
/**
|
|
2104
|
-
* One-row editor window keeping the logical cursor visible in long drafts.
|
|
2105
|
-
* The caret and its surroundings slice at grapheme boundaries: splitting a
|
|
2106
|
-
* star-plane surrogate pair would render an isolated half under the block
|
|
2107
|
-
* caret with a width the terminal never draws.
|
|
2108
|
-
*/
|
|
2109
|
-
export function editorWindow(value: string, cursor: number, columns: number): { before: string; caret: string; after: string } {
|
|
2110
|
-
const width = Math.max(1, columns)
|
|
2111
|
-
const normalize = (text: string): string => displayText(text).replace(/\n/gu, '↵').replace(/\t/gu, ' ')
|
|
2112
|
-
const site = clampCursor(value, cursor)
|
|
2113
|
-
const caretSpan = splitGraphemes(value).find(span => span.start === site)
|
|
2114
|
-
const caret = caretSpan === undefined ? ' ' : normalize(caretSpan.text)
|
|
2115
|
-
const rest = value.slice(caretSpan === undefined ? site : caretSpan.end)
|
|
2116
|
-
const remaining = Math.max(0, width - visibleColumns(caret))
|
|
2117
|
-
const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(rest)))
|
|
2118
|
-
const beforeBudget = Math.max(0, remaining - afterBudget)
|
|
2119
|
-
const before = beforeBudget === 0
|
|
2120
|
-
? ''
|
|
2121
|
-
: displayTail(normalize(value.slice(0, site)), beforeBudget, 1).text
|
|
2122
|
-
const after = afterBudget === 0
|
|
2123
|
-
? ''
|
|
2124
|
-
: truncateColumns(normalize(rest), afterBudget)
|
|
2125
|
-
return { before, caret, after }
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
2129
|
-
const COMPOSER_PLACEHOLDER = 'type a message · / commands · @ mentions'
|
|
2092
|
+
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
2093
|
+
const COMPOSER_PLACEHOLDER = 'type a message · / commands · @ mentions'
|
|
2130
2094
|
|
|
2131
2095
|
/** One physical cell of the wave-painted composer row: a char plus styles. */
|
|
2132
|
-
interface ComposerCell {
|
|
2133
|
-
char: string
|
|
2096
|
+
interface ComposerCell {
|
|
2097
|
+
char: string
|
|
2098
|
+
width?: number
|
|
2134
2099
|
color?: string
|
|
2135
2100
|
backgroundColor?: string
|
|
2136
2101
|
bold?: boolean
|
|
@@ -2515,7 +2480,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2515
2480
|
refresh(): void
|
|
2516
2481
|
loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
2517
2482
|
inspectImages(paths: readonly string[]): Promise<readonly ImagePathInspection[]>
|
|
2518
|
-
prepareImages(paths: readonly string[]): Promise<readonly ImageBlock[]>
|
|
2483
|
+
prepareImages(paths: readonly string[], signal?: AbortSignal): Promise<readonly ImageBlock[]>
|
|
2519
2484
|
cyclePermission(): string
|
|
2520
2485
|
exportTranscript(argument: string): Promise<void>
|
|
2521
2486
|
renameTitle(argument: string): string
|
|
@@ -2546,9 +2511,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2546
2511
|
maxRows: number
|
|
2547
2512
|
/** Reports the editor's current physical row count so the live budget stays exact. */
|
|
2548
2513
|
onEditorRows(rows: number): void
|
|
2549
|
-
}): ReactElement {
|
|
2550
|
-
const columns = useStdout().stdout?.columns ?? 80
|
|
2514
|
+
}): ReactElement {
|
|
2515
|
+
const columns = useStdout().stdout?.columns ?? 80
|
|
2516
|
+
const editorColumns = Math.max(1, columns - 6)
|
|
2551
2517
|
const stdin = useStdin().stdin
|
|
2518
|
+
const focusReporting = isVsCodeTerminalEnv()
|
|
2552
2519
|
const [value, setValue] = useState('')
|
|
2553
2520
|
const [cursor, setCursor] = useState(0)
|
|
2554
2521
|
const valueRef = useRef(value)
|
|
@@ -2559,6 +2526,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2559
2526
|
const draftImagesRef = useRef(draftImages)
|
|
2560
2527
|
draftImagesRef.current = draftImages
|
|
2561
2528
|
const [preparingImages, setPreparingImages] = useState(false)
|
|
2529
|
+
const prepareAbortRef = useRef<AbortController | undefined>(undefined)
|
|
2530
|
+
const prepareEpochRef = useRef(0)
|
|
2531
|
+
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages)
|
|
2532
|
+
useEffect(() => () => {
|
|
2533
|
+
prepareEpochRef.current += 1
|
|
2534
|
+
prepareAbortRef.current?.abort()
|
|
2535
|
+
}, [])
|
|
2562
2536
|
// Codex textarea editing state: a single-entry kill buffer, the vertical
|
|
2563
2537
|
// move's preferred display column, the editor's scroll window, and the
|
|
2564
2538
|
// bracketed-paste marker state. All of it is editor-local; nothing here
|
|
@@ -2569,11 +2543,17 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2569
2543
|
const pasteBracketRef = useRef(false)
|
|
2570
2544
|
/** Cancels the pending lost-paste safety timer (undefined when disarmed). */
|
|
2571
2545
|
const pasteBracketCancelRef = useRef<(() => void) | undefined>(undefined)
|
|
2572
|
-
/**
|
|
2573
|
-
const
|
|
2546
|
+
/** Ordered editor tokens from the stdin chunk Ink is about to deliver. */
|
|
2547
|
+
const rawEditorTokens = useRef<readonly RawEditorToken[] | undefined>(undefined)
|
|
2548
|
+
/** VS Code focus state from xterm focus-report events; starts focused. */
|
|
2549
|
+
const terminalFocusedRef = useRef(true)
|
|
2574
2550
|
// Codex shell-style recall: the navigation cursor, the saved draft restored
|
|
2575
2551
|
// on Down past the newest entry, and the boundary-gate anchor.
|
|
2576
|
-
const recall = useRef<RecallState>(beginRecall([], ''))
|
|
2552
|
+
const recall = useRef<RecallState>(beginRecall([], ''))
|
|
2553
|
+
|
|
2554
|
+
useEffect(() => {
|
|
2555
|
+
preferredColumnRef.current = null
|
|
2556
|
+
}, [editorColumns])
|
|
2577
2557
|
|
|
2578
2558
|
// A /history panel acceptance lands as a fill: place the sanitized text at
|
|
2579
2559
|
// the end of the composer and resume recall from that entry.
|
|
@@ -2582,8 +2562,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2582
2562
|
const safe = sanitizeDraftText(historyFill.text)
|
|
2583
2563
|
draftImagesRef.current = []
|
|
2584
2564
|
setDraftImages([])
|
|
2585
|
-
|
|
2586
|
-
|
|
2565
|
+
valueRef.current = safe
|
|
2566
|
+
cursorRef.current = safe.length
|
|
2567
|
+
setValue(safe)
|
|
2568
|
+
setCursor(safe.length)
|
|
2569
|
+
resetCursorBlink()
|
|
2587
2570
|
preferredColumnRef.current = null
|
|
2588
2571
|
setDismissedMenuValue(undefined)
|
|
2589
2572
|
recall.current = {
|
|
@@ -2593,7 +2576,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2593
2576
|
lastRecalled: safe,
|
|
2594
2577
|
}
|
|
2595
2578
|
historyConsumed()
|
|
2596
|
-
}, [historyFill, recallSpace, historyConsumed])
|
|
2579
|
+
}, [historyFill, recallSpace, historyConsumed, resetCursorBlink])
|
|
2597
2580
|
|
|
2598
2581
|
useEffect(() => {
|
|
2599
2582
|
setDraftImages((current) => {
|
|
@@ -2607,26 +2590,30 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2607
2590
|
// as distinct keys, and kitty CSI-u forms parse as unnamed junk Ink would
|
|
2608
2591
|
// insert as draft text.
|
|
2609
2592
|
// Patch stdin.read — the single choke point Ink's input loop pulls every
|
|
2610
|
-
// chunk through — to first rewrite decodable CSI-u sequences to their
|
|
2611
|
-
// legacy bytes, then
|
|
2612
|
-
// matching
|
|
2613
|
-
//
|
|
2614
|
-
// parses the normalized string; every other byte passes through untouched.
|
|
2593
|
+
// chunk through — to first rewrite decodable CSI-u sequences to their
|
|
2594
|
+
// legacy bytes, then tokenize editor-only sequences before Ink emits the
|
|
2595
|
+
// matching input event. Batched Home/End/Delete/Backspace actions remain
|
|
2596
|
+
// ordered even though Ink invokes useInput only once for the whole chunk.
|
|
2615
2597
|
useEffect(() => {
|
|
2616
2598
|
if (stdin === undefined) return
|
|
2617
2599
|
const originalRead = stdin.read.bind(stdin)
|
|
2618
2600
|
const patchedRead = function patchedRead(this: typeof stdin, ...args: Parameters<typeof originalRead>) {
|
|
2619
|
-
const chunk = originalRead(...args)
|
|
2620
|
-
if (chunk === null) return chunk
|
|
2621
|
-
const normalized = normalizeKeyboardChunk(typeof chunk === 'string' ? chunk : String(chunk))
|
|
2622
|
-
|
|
2623
|
-
|
|
2601
|
+
const chunk = originalRead(...args)
|
|
2602
|
+
if (chunk === null) return chunk
|
|
2603
|
+
const normalized = normalizeKeyboardChunk(typeof chunk === 'string' ? chunk : String(chunk))
|
|
2604
|
+
const input = focusReporting
|
|
2605
|
+
? stripTerminalFocusEvents(normalized, focused => {
|
|
2606
|
+
terminalFocusedRef.current = focused
|
|
2607
|
+
})
|
|
2608
|
+
: normalized
|
|
2609
|
+
rawEditorTokens.current = tokenizeRawEditorChunk(input)
|
|
2610
|
+
return input
|
|
2624
2611
|
} as typeof stdin.read
|
|
2625
2612
|
stdin.read = patchedRead
|
|
2626
2613
|
return () => {
|
|
2627
2614
|
stdin.read = originalRead as typeof stdin.read
|
|
2628
2615
|
}
|
|
2629
|
-
}, [stdin])
|
|
2616
|
+
}, [focusReporting, stdin])
|
|
2630
2617
|
|
|
2631
2618
|
// Keep the navigation's recall space fresh while browsing state survives
|
|
2632
2619
|
// (new local submissions extend the space; the index stays valid unless
|
|
@@ -2651,6 +2638,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2651
2638
|
: { start: beforeCursor.length - lastLine.length + (tokenMatch.index ?? 0) + (tokenMatch[1]?.length ?? 0), query: tokenMatch[2] ?? '' }
|
|
2652
2639
|
const mentionActive = mentionToken !== undefined
|
|
2653
2640
|
const [mentionRows, setMentionRows] = useState<readonly MentionCandidate[]>([])
|
|
2641
|
+
const mentionRequestRef = useRef(0)
|
|
2654
2642
|
|
|
2655
2643
|
const sameImagePath = (left: string, right: string): boolean => (
|
|
2656
2644
|
process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right
|
|
@@ -2680,6 +2668,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2680
2668
|
}
|
|
2681
2669
|
|
|
2682
2670
|
const insertDroppedImages = (paths: readonly string[]): void => {
|
|
2671
|
+
const originalValue = valueRef.current
|
|
2672
|
+
const originalCursor = cursorRef.current
|
|
2683
2673
|
notify(`checking ${paths.length} image${paths.length === 1 ? '' : 's'}…`)
|
|
2684
2674
|
void inspectImages(paths).then((inspected) => {
|
|
2685
2675
|
const additions: DraftImage[] = []
|
|
@@ -2694,14 +2684,23 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2694
2684
|
notify('those images are already attached', 'warning')
|
|
2695
2685
|
return
|
|
2696
2686
|
}
|
|
2697
|
-
const at = cursorRef.current
|
|
2698
2687
|
const current = valueRef.current
|
|
2688
|
+
const anchor = remapStableRange(originalValue, current, { start: originalCursor, end: originalCursor })
|
|
2689
|
+
if (anchor === undefined) {
|
|
2690
|
+
notify('draft changed at the image drop point; drop the images again', 'warning')
|
|
2691
|
+
return
|
|
2692
|
+
}
|
|
2693
|
+
const at = anchor.start
|
|
2699
2694
|
const insertion = `${at > 0 && !/\s$/u.test(current.slice(0, at)) ? ' ' : ''}${markers.join(' ')}${current.slice(at) === '' ? '' : ' '}`
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2695
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, insertion)
|
|
2696
|
+
const nextCursor = current === originalValue && cursorRef.current === originalCursor
|
|
2697
|
+
? at + insertion.length
|
|
2698
|
+
: edit.cursor
|
|
2699
|
+
valueRef.current = edit.value
|
|
2700
|
+
cursorRef.current = nextCursor
|
|
2701
|
+
setValue(edit.value)
|
|
2702
|
+
setCursor(nextCursor)
|
|
2703
|
+
resetCursorBlink()
|
|
2705
2704
|
const nextImages = [...draftImagesRef.current, ...additions]
|
|
2706
2705
|
draftImagesRef.current = nextImages
|
|
2707
2706
|
setDraftImages(nextImages)
|
|
@@ -2711,28 +2710,38 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2711
2710
|
})
|
|
2712
2711
|
}
|
|
2713
2712
|
|
|
2714
|
-
useEffect(() => {
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2713
|
+
useEffect(() => {
|
|
2714
|
+
const requestId = mentionRequestRef.current + 1
|
|
2715
|
+
mentionRequestRef.current = requestId
|
|
2716
|
+
if (!active || !mentionActive) {
|
|
2717
|
+
setMentionRows([])
|
|
2718
|
+
return
|
|
2719
|
+
}
|
|
2720
|
+
const controller = new AbortController()
|
|
2721
|
+
const query = mentionToken.query
|
|
2722
|
+
const timer = setTimeout(() => {
|
|
2723
|
+
void loadMentions(query, controller.signal).then(
|
|
2724
|
+
rows => {
|
|
2725
|
+
if (!controller.signal.aborted && mentionRequestRef.current === requestId) setMentionRows(rows)
|
|
2726
|
+
},
|
|
2727
|
+
() => {},
|
|
2728
|
+
)
|
|
2729
|
+
}, 50)
|
|
2730
|
+
return () => {
|
|
2731
|
+
clearTimeout(timer)
|
|
2732
|
+
controller.abort()
|
|
2733
|
+
}
|
|
2734
|
+
}, [active, mentionActive, mentionToken?.query])
|
|
2729
2735
|
|
|
2730
2736
|
// Codex routes keys to the topmost surface first. Completion therefore
|
|
2731
2737
|
// remains available while a turn runs, and Esc dismisses it before the
|
|
2732
|
-
// same key is allowed to interrupt the turn.
|
|
2733
|
-
const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value
|
|
2734
|
-
const
|
|
2735
|
-
? mentionRows.
|
|
2738
|
+
// same key is allowed to interrupt the turn.
|
|
2739
|
+
const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value
|
|
2740
|
+
const visibleMentionRows = mentionToken !== undefined && isPathLikeMentionQuery(mentionToken.query)
|
|
2741
|
+
? mentionRows.filter(row => row.kind !== 'session')
|
|
2742
|
+
: mentionRows
|
|
2743
|
+
const menuRows: readonly CompletionCandidate[] = mentionActive
|
|
2744
|
+
? visibleMentionRows.map(row => ({
|
|
2736
2745
|
label: row.label.startsWith('@')
|
|
2737
2746
|
? row.label
|
|
2738
2747
|
: `@${row.label}${row.kind === 'directory' ? '/' : ''}`,
|
|
@@ -2741,37 +2750,45 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2741
2750
|
}))
|
|
2742
2751
|
: candidates
|
|
2743
2752
|
|
|
2744
|
-
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
2745
|
-
const acceptMenuCandidate = (): void => {
|
|
2746
|
-
if (mentionActive && mentionToken !== undefined) {
|
|
2747
|
-
|
|
2753
|
+
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
2754
|
+
const acceptMenuCandidate = (): void => {
|
|
2755
|
+
if (mentionActive && mentionToken !== undefined) {
|
|
2756
|
+
if (visibleMentionRows.length === 0) return
|
|
2757
|
+
const row = visibleMentionRows[completionIndex % visibleMentionRows.length]
|
|
2748
2758
|
if (row !== undefined) {
|
|
2749
2759
|
if (row.kind === 'file' && row.path !== undefined && looksLikeImagePath(row.path)) {
|
|
2750
2760
|
const tokenText = value.slice(mentionToken.start, cursor)
|
|
2751
2761
|
const start = mentionToken.start
|
|
2762
|
+
const originalValue = value
|
|
2752
2763
|
notify(`checking image ${basename(row.path)}…`)
|
|
2753
2764
|
void inspectImages([row.path]).then((inspected) => {
|
|
2754
2765
|
const inspection = inspected[0]
|
|
2755
2766
|
if (inspection === undefined) return
|
|
2756
2767
|
const current = valueRef.current
|
|
2757
|
-
|
|
2768
|
+
const anchor = remapStableRange(originalValue, current, { start, end: start + tokenText.length })
|
|
2769
|
+
if (anchor === undefined || current.slice(anchor.start, anchor.end) !== tokenText) {
|
|
2770
|
+
notify('draft changed around the image mention; select it again', 'warning')
|
|
2771
|
+
return
|
|
2772
|
+
}
|
|
2758
2773
|
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
2759
|
-
const
|
|
2760
|
-
valueRef.current =
|
|
2761
|
-
cursorRef.current =
|
|
2762
|
-
setValue(
|
|
2763
|
-
setCursor(
|
|
2764
|
-
|
|
2774
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, '')
|
|
2775
|
+
valueRef.current = edit.value
|
|
2776
|
+
cursorRef.current = edit.cursor
|
|
2777
|
+
setValue(edit.value)
|
|
2778
|
+
setCursor(edit.cursor)
|
|
2779
|
+
resetCursorBlink()
|
|
2780
|
+
setDismissedMenuValue(edit.value)
|
|
2765
2781
|
notify(`${inspection.name} is already attached`, 'warning')
|
|
2766
2782
|
return
|
|
2767
2783
|
}
|
|
2768
2784
|
const marker = uniqueImageMarker(inspection.name, 'mention')
|
|
2769
|
-
const
|
|
2770
|
-
valueRef.current =
|
|
2771
|
-
cursorRef.current =
|
|
2772
|
-
setValue(
|
|
2773
|
-
setCursor(
|
|
2774
|
-
|
|
2785
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, marker)
|
|
2786
|
+
valueRef.current = edit.value
|
|
2787
|
+
cursorRef.current = edit.cursor
|
|
2788
|
+
setValue(edit.value)
|
|
2789
|
+
setCursor(edit.cursor)
|
|
2790
|
+
resetCursorBlink()
|
|
2791
|
+
setDismissedMenuValue(edit.value)
|
|
2775
2792
|
registerDraftImage(inspection, marker)
|
|
2776
2793
|
notify(`${inspection.name} ready for the next message`)
|
|
2777
2794
|
}, (reason: unknown) => {
|
|
@@ -2783,44 +2800,149 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2783
2800
|
}
|
|
2784
2801
|
// Session rows carry the canonical @[label](dsh-session:…) token;
|
|
2785
2802
|
// file rows insert `@path` (directories keep their trailing slash).
|
|
2786
|
-
const insertion = row.label.startsWith('@')
|
|
2787
|
-
? row.label
|
|
2788
|
-
: `@${row.label}${row.kind === 'directory' ? '/' : ''}`
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2803
|
+
const insertion = row.label.startsWith('@')
|
|
2804
|
+
? row.label
|
|
2805
|
+
: `@${row.label}${row.kind === 'directory' ? '/' : ''}`
|
|
2806
|
+
const nextValue = value.slice(0, mentionToken.start) + insertion + value.slice(cursor)
|
|
2807
|
+
const nextCursor = mentionToken.start + insertion.length
|
|
2808
|
+
valueRef.current = nextValue
|
|
2809
|
+
cursorRef.current = nextCursor
|
|
2810
|
+
setValue(nextValue)
|
|
2811
|
+
setCursor(nextCursor)
|
|
2812
|
+
resetCursorBlink()
|
|
2813
|
+
}
|
|
2814
|
+
} else {
|
|
2815
|
+
if (candidates.length === 0) return
|
|
2816
|
+
const candidate = candidates[completionIndex % candidates.length]
|
|
2817
|
+
if (candidate !== undefined) {
|
|
2818
|
+
const nextValue = `${candidate.label} `
|
|
2819
|
+
const nextCursor = candidate.label.length + 1
|
|
2820
|
+
valueRef.current = nextValue
|
|
2821
|
+
cursorRef.current = nextCursor
|
|
2822
|
+
setValue(nextValue)
|
|
2823
|
+
setCursor(nextCursor)
|
|
2824
|
+
resetCursorBlink()
|
|
2825
|
+
}
|
|
2798
2826
|
}
|
|
2799
2827
|
setCompletionIndex(0)
|
|
2800
2828
|
setDismissedMenuValue(undefined)
|
|
2801
2829
|
}
|
|
2802
2830
|
|
|
2803
2831
|
/** Apply one editor edit: draft, cursor, kill buffer, menu reset. */
|
|
2804
|
-
const applyEdit = (edit: EditResult): void => {
|
|
2805
|
-
if (edit.killed !== undefined && edit.killed !== '') killRef.current = edit.killed
|
|
2806
|
-
|
|
2807
|
-
|
|
2832
|
+
const applyEdit = (edit: EditResult): void => {
|
|
2833
|
+
if (edit.killed !== undefined && edit.killed !== '') killRef.current = edit.killed
|
|
2834
|
+
valueRef.current = edit.value
|
|
2835
|
+
cursorRef.current = edit.cursor
|
|
2836
|
+
setValue(edit.value)
|
|
2837
|
+
setCursor(edit.cursor)
|
|
2838
|
+
resetCursorBlink()
|
|
2808
2839
|
preferredColumnRef.current = null
|
|
2809
2840
|
setCompletionIndex(0)
|
|
2810
2841
|
setDismissedMenuValue(undefined)
|
|
2811
2842
|
}
|
|
2812
2843
|
|
|
2813
2844
|
/** Move the cursor without editing; horizontal moves clear the column preference. */
|
|
2814
|
-
const moveCursorTo = (next: number): void => {
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2845
|
+
const moveCursorTo = (next: number): void => {
|
|
2846
|
+
resetCursorBlink()
|
|
2847
|
+
if (next === cursorRef.current) return
|
|
2848
|
+
cursorRef.current = next
|
|
2849
|
+
setCursor(next)
|
|
2850
|
+
preferredColumnRef.current = null
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/** Apply an ordered raw-key batch against one current draft snapshot. */
|
|
2854
|
+
const applyRawEditorTokens = (tokens: readonly RawEditorToken[]): void => {
|
|
2855
|
+
let nextValue = valueRef.current
|
|
2856
|
+
let nextCursor = cursorRef.current
|
|
2857
|
+
for (const token of tokens) {
|
|
2858
|
+
if (token.kind === 'text') {
|
|
2859
|
+
const edit = insertText(nextValue, nextCursor, token.text)
|
|
2860
|
+
nextValue = edit.value
|
|
2861
|
+
nextCursor = edit.cursor
|
|
2862
|
+
continue
|
|
2863
|
+
}
|
|
2864
|
+
if (token.kind === 'home') {
|
|
2865
|
+
nextCursor = moveToLineStart(nextValue, nextCursor, false)
|
|
2866
|
+
continue
|
|
2867
|
+
}
|
|
2868
|
+
if (token.kind === 'end') {
|
|
2869
|
+
nextCursor = moveToLineEnd(nextValue, nextCursor, false)
|
|
2870
|
+
continue
|
|
2871
|
+
}
|
|
2872
|
+
const edit = token.kind === 'delete-backward'
|
|
2873
|
+
? deleteBackward(nextValue, nextCursor)
|
|
2874
|
+
: token.kind === 'delete-word-backward'
|
|
2875
|
+
? deleteWordBackward(nextValue, nextCursor)
|
|
2876
|
+
: token.kind === 'delete-forward'
|
|
2877
|
+
? deleteForward(nextValue, nextCursor)
|
|
2878
|
+
: deleteWordForward(nextValue, nextCursor)
|
|
2879
|
+
if (edit.killed !== undefined && edit.killed !== '') killRef.current = edit.killed
|
|
2880
|
+
nextValue = edit.value
|
|
2881
|
+
nextCursor = edit.cursor
|
|
2882
|
+
}
|
|
2883
|
+
valueRef.current = nextValue
|
|
2884
|
+
cursorRef.current = nextCursor
|
|
2885
|
+
setValue(nextValue)
|
|
2886
|
+
setCursor(nextCursor)
|
|
2887
|
+
resetCursorBlink()
|
|
2888
|
+
preferredColumnRef.current = null
|
|
2889
|
+
setCompletionIndex(0)
|
|
2890
|
+
setDismissedMenuValue(undefined)
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
const cancelImageSubmission = (): void => {
|
|
2894
|
+
prepareEpochRef.current += 1
|
|
2895
|
+
prepareAbortRef.current?.abort()
|
|
2896
|
+
prepareAbortRef.current = undefined
|
|
2897
|
+
setPreparingImages(false)
|
|
2898
|
+
dismissNotice()
|
|
2899
|
+
notify('image submission cancelled', 'warning')
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
/** Move through visual rows first, then cross history at the true edge. */
|
|
2903
|
+
const navigateVertical = (direction: -1 | 1): void => {
|
|
2904
|
+
const currentValue = valueRef.current
|
|
2905
|
+
const currentCursor = cursorRef.current
|
|
2906
|
+
const model = editorModel(currentValue, editorColumns)
|
|
2907
|
+
const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column
|
|
2908
|
+
const next = moveCursorVertically(model, currentCursor, preferred, direction)
|
|
2909
|
+
if (next !== currentCursor) {
|
|
2910
|
+
cursorRef.current = next
|
|
2911
|
+
setCursor(next)
|
|
2912
|
+
resetCursorBlink()
|
|
2913
|
+
preferredColumnRef.current = preferred
|
|
2914
|
+
return
|
|
2915
|
+
}
|
|
2916
|
+
if (recall.current.entries.length > 0
|
|
2917
|
+
&& shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
|
|
2918
|
+
const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current)
|
|
2919
|
+
recall.current = step.state
|
|
2920
|
+
if (step.entry !== undefined) {
|
|
2921
|
+
const safe = sanitizeDraftText(step.entry)
|
|
2922
|
+
valueRef.current = safe
|
|
2923
|
+
cursorRef.current = safe.length
|
|
2924
|
+
setValue(safe)
|
|
2925
|
+
setCursor(safe.length)
|
|
2926
|
+
preferredColumnRef.current = null
|
|
2927
|
+
setDismissedMenuValue(undefined)
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
resetCursorBlink()
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
useStableInput((input, key) => {
|
|
2821
2934
|
// Modal ownership: approval/question/model dialogs consume all keys.
|
|
2822
2935
|
if (!active) return
|
|
2823
|
-
|
|
2936
|
+
// React may not have committed the previous Tab completion render before
|
|
2937
|
+
// the next terminal byte arrives. Read the synchronous editor refs so a
|
|
2938
|
+
// completion followed immediately by text edits never uses stale closure
|
|
2939
|
+
// state.
|
|
2940
|
+
const liveValue = valueRef.current
|
|
2941
|
+
const liveCursor = cursorRef.current
|
|
2942
|
+
if (preparingImages) {
|
|
2943
|
+
if (key.escape || (key.ctrl && input === 'c')) cancelImageSubmission()
|
|
2944
|
+
return
|
|
2945
|
+
}
|
|
2824
2946
|
// Deletion confirm owns the box: y proceeds, anything else cancels.
|
|
2825
2947
|
// Typed in the INPUT BOX (codex delete-confirm): the keystroke is echoed
|
|
2826
2948
|
// as the box's own prompt, not an invisible panel keypress.
|
|
@@ -2842,10 +2964,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2842
2964
|
}
|
|
2843
2965
|
return
|
|
2844
2966
|
}
|
|
2845
|
-
// Ctrl+R toggles the thinking display (Claude-Code reasoning fold).
|
|
2846
|
-
if (key.ctrl && input === 'r') {
|
|
2847
|
-
|
|
2848
|
-
|
|
2967
|
+
// Ctrl+R toggles the thinking display (Claude-Code reasoning fold).
|
|
2968
|
+
if (key.ctrl && input === 'r') {
|
|
2969
|
+
if (focusReporting && !terminalFocusedRef.current) return
|
|
2970
|
+
toggleReasoning()
|
|
2971
|
+
return
|
|
2849
2972
|
}
|
|
2850
2973
|
// Ctrl+O opens the bounded transcript inspector (Claude-Code convention,
|
|
2851
2974
|
// adapted to append-only static rows): one history entry at a time with
|
|
@@ -2858,11 +2981,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2858
2981
|
// cancelled, a non-empty draft is cleared, and only an idle empty input
|
|
2859
2982
|
// exits. Ctrl+D always means exit but refuses mid-turn.
|
|
2860
2983
|
if (key.ctrl && input === 'c') {
|
|
2861
|
-
if (busy) {
|
|
2862
|
-
interrupt()
|
|
2863
|
-
} else if (
|
|
2984
|
+
if (busy) {
|
|
2985
|
+
interrupt()
|
|
2986
|
+
} else if (liveValue !== '') {
|
|
2987
|
+
valueRef.current = ''
|
|
2988
|
+
cursorRef.current = 0
|
|
2864
2989
|
setValue('')
|
|
2865
2990
|
setCursor(0)
|
|
2991
|
+
resetCursorBlink()
|
|
2866
2992
|
draftImagesRef.current = []
|
|
2867
2993
|
setDraftImages([])
|
|
2868
2994
|
setCompletionIndex(0)
|
|
@@ -2875,8 +3001,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2875
3001
|
if (key.ctrl && input === 'd') {
|
|
2876
3002
|
// Codex: Ctrl+D deletes forward while a draft exists; the app-level
|
|
2877
3003
|
// exit only fires from an empty composer.
|
|
2878
|
-
if (
|
|
2879
|
-
applyEdit(deleteForward(
|
|
3004
|
+
if (liveValue !== '') {
|
|
3005
|
+
applyEdit(deleteForward(liveValue, liveCursor))
|
|
2880
3006
|
return
|
|
2881
3007
|
}
|
|
2882
3008
|
if (busy) notify('cancel the running turn before exiting (Esc or Ctrl+C)', 'warning')
|
|
@@ -2885,7 +3011,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2885
3011
|
}
|
|
2886
3012
|
if (key.escape) {
|
|
2887
3013
|
if (menuActive) {
|
|
2888
|
-
setDismissedMenuValue(
|
|
3014
|
+
setDismissedMenuValue(liveValue)
|
|
2889
3015
|
return
|
|
2890
3016
|
}
|
|
2891
3017
|
if (hasNotice) {
|
|
@@ -2897,14 +3023,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2897
3023
|
}
|
|
2898
3024
|
// Delete on the empty composer cancels the newest queued message (the
|
|
2899
3025
|
// web queue-mirror contract: the durable splice drops the pending row).
|
|
2900
|
-
if (key.delete &&
|
|
3026
|
+
if (key.delete && liveValue === '' && queued.length > 0) {
|
|
2901
3027
|
cancelQueued(queued[queued.length - 1]!.messageId)
|
|
2902
3028
|
return
|
|
2903
3029
|
}
|
|
2904
3030
|
if (key.return) {
|
|
2905
3031
|
// A newline inside an open bracketed paste inserts; it never submits.
|
|
2906
3032
|
if (pasteBracketRef.current) {
|
|
2907
|
-
applyEdit(insertText(
|
|
3033
|
+
applyEdit(insertText(liveValue, liveCursor, '\n'))
|
|
2908
3034
|
return
|
|
2909
3035
|
}
|
|
2910
3036
|
// Enter on an open completion menu accepts the highlighted candidate
|
|
@@ -2913,18 +3039,24 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2913
3039
|
// exactly, in which case Enter submits it (typing a full "/effort" and
|
|
2914
3040
|
// pressing return must run the command, not re-accept its own text).
|
|
2915
3041
|
if (menuActive) {
|
|
2916
|
-
const exactSlash = !mentionActive && candidates.some(candidate => candidate.label ===
|
|
3042
|
+
const exactSlash = !mentionActive && candidates.some(candidate => candidate.label === liveValue)
|
|
2917
3043
|
if (!exactSlash) {
|
|
2918
3044
|
acceptMenuCandidate()
|
|
2919
3045
|
return
|
|
2920
3046
|
}
|
|
2921
3047
|
}
|
|
2922
|
-
const text =
|
|
3048
|
+
const text = liveValue.trim()
|
|
2923
3049
|
if (draftImagesRef.current.length > 0) {
|
|
3050
|
+
const controller = new AbortController()
|
|
3051
|
+
const epoch = prepareEpochRef.current + 1
|
|
3052
|
+
prepareEpochRef.current = epoch
|
|
3053
|
+
prepareAbortRef.current = controller
|
|
2924
3054
|
setPreparingImages(true)
|
|
2925
3055
|
notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? '' : 's'}…`)
|
|
2926
3056
|
const snapshot = draftImagesRef.current
|
|
2927
|
-
void prepareImages(snapshot.map(image => image.path)).then((images) => {
|
|
3057
|
+
void prepareImages(snapshot.map(image => image.path), controller.signal).then((images) => {
|
|
3058
|
+
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return
|
|
3059
|
+
prepareAbortRef.current = undefined
|
|
2928
3060
|
setPreparingImages(false)
|
|
2929
3061
|
valueRef.current = ''
|
|
2930
3062
|
cursorRef.current = 0
|
|
@@ -2943,14 +3075,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2943
3075
|
if (busy) steer(text, images)
|
|
2944
3076
|
else dispatch(text, images)
|
|
2945
3077
|
}, (reason: unknown) => {
|
|
3078
|
+
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return
|
|
3079
|
+
prepareAbortRef.current = undefined
|
|
2946
3080
|
setPreparingImages(false)
|
|
2947
3081
|
notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
2948
3082
|
})
|
|
2949
3083
|
return
|
|
2950
3084
|
}
|
|
3085
|
+
valueRef.current = ''
|
|
3086
|
+
cursorRef.current = 0
|
|
2951
3087
|
setValue('')
|
|
2952
|
-
setCursor(0)
|
|
2953
|
-
|
|
3088
|
+
setCursor(0)
|
|
3089
|
+
resetCursorBlink()
|
|
3090
|
+
setCompletionIndex(0)
|
|
2954
3091
|
setDismissedMenuValue(undefined)
|
|
2955
3092
|
if (text === '') return
|
|
2956
3093
|
dismissNotice()
|
|
@@ -3094,10 +3231,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3094
3231
|
dispatch(text)
|
|
3095
3232
|
return
|
|
3096
3233
|
}
|
|
3097
|
-
// Ink exposes Ctrl+J as a bare LF and Alt+Enter as a bare CR after
|
|
3098
|
-
// stripping the leading escape. Neither is a multiline shortcut.
|
|
3099
|
-
if (input === '\n' || input === '\r') return
|
|
3100
|
-
|
|
3234
|
+
// Ink exposes Ctrl+J as a bare LF and Alt+Enter as a bare CR after
|
|
3235
|
+
// stripping the leading escape. Neither is a multiline shortcut.
|
|
3236
|
+
if (input === '\n' || input === '\r') return
|
|
3237
|
+
// A fast Tab followed by text can arrive as one readable chunk in an
|
|
3238
|
+
// integrated terminal. Accept the candidate first, then apply the
|
|
3239
|
+
// remaining characters against the synchronously updated editor refs.
|
|
3240
|
+
if (menuActive && (key.tab || input.startsWith('\t'))) {
|
|
3241
|
+
const remainder = key.tab ? '' : input.slice(1)
|
|
3242
|
+
acceptMenuCandidate()
|
|
3243
|
+
if (remainder !== '') applyEdit(insertText(valueRef.current, cursorRef.current, remainder))
|
|
3244
|
+
return
|
|
3245
|
+
}
|
|
3246
|
+
if (menuActive && key.upArrow) {
|
|
3101
3247
|
setCompletionIndex(index => (index + menuRows.length - 1) % menuRows.length)
|
|
3102
3248
|
return
|
|
3103
3249
|
}
|
|
@@ -3105,135 +3251,86 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3105
3251
|
setCompletionIndex(index => (index + 1) % menuRows.length)
|
|
3106
3252
|
return
|
|
3107
3253
|
}
|
|
3108
|
-
//
|
|
3109
|
-
//
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
if (
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
}
|
|
3121
|
-
if (key.upArrow || key.downArrow) {
|
|
3122
|
-
// Codex boundary gate: shell recall runs from an empty draft, or from
|
|
3123
|
-
// a boundary of a draft that still matches the last recalled entry;
|
|
3124
|
-
// every interior Up/Down moves the caret across the multiline draft.
|
|
3125
|
-
if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
|
|
3126
|
-
const step = key.upArrow ? recallOlder(recall.current, value) : recallNewer(recall.current)
|
|
3127
|
-
recall.current = step.state
|
|
3128
|
-
if (step.entry !== undefined) {
|
|
3129
|
-
const safe = sanitizeDraftText(step.entry)
|
|
3130
|
-
setValue(safe)
|
|
3131
|
-
setCursor(safe.length)
|
|
3132
|
-
preferredColumnRef.current = null
|
|
3133
|
-
setDismissedMenuValue(undefined)
|
|
3134
|
-
}
|
|
3135
|
-
return
|
|
3136
|
-
}
|
|
3137
|
-
const model = editorModel(value, Math.max(1, columns - 6))
|
|
3138
|
-
const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column
|
|
3139
|
-
const next = moveCursorVertically(model, cursor, preferred, key.upArrow ? -1 : 1)
|
|
3140
|
-
if (next !== cursor) {
|
|
3141
|
-
setCursor(next)
|
|
3142
|
-
preferredColumnRef.current = preferred
|
|
3143
|
-
}
|
|
3144
|
-
return
|
|
3145
|
-
}
|
|
3254
|
+
// Batched Home/End/Delete/Backspace sequences bypass Ink's one-key parser
|
|
3255
|
+
// and reduce against one current editor snapshot in their original order.
|
|
3256
|
+
const rawTokens = rawEditorTokens.current
|
|
3257
|
+
rawEditorTokens.current = undefined
|
|
3258
|
+
if (rawTokens !== undefined) {
|
|
3259
|
+
applyRawEditorTokens(rawTokens)
|
|
3260
|
+
return
|
|
3261
|
+
}
|
|
3262
|
+
if (key.upArrow || key.downArrow) {
|
|
3263
|
+
navigateVertical(key.upArrow ? -1 : 1)
|
|
3264
|
+
return
|
|
3265
|
+
}
|
|
3146
3266
|
// Ctrl+P / Ctrl+N share the Up/Down contract (Codex binds them to
|
|
3147
3267
|
// move_up/move_down, so the history gate applies first).
|
|
3148
|
-
if (key.ctrl && (input === 'p' || input === 'n')) {
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
const step = up ? recallOlder(recall.current, value) : recallNewer(recall.current)
|
|
3152
|
-
recall.current = step.state
|
|
3153
|
-
if (step.entry !== undefined) {
|
|
3154
|
-
const safe = sanitizeDraftText(step.entry)
|
|
3155
|
-
setValue(safe)
|
|
3156
|
-
setCursor(safe.length)
|
|
3157
|
-
preferredColumnRef.current = null
|
|
3158
|
-
setDismissedMenuValue(undefined)
|
|
3159
|
-
}
|
|
3160
|
-
return
|
|
3161
|
-
}
|
|
3162
|
-
const model = editorModel(value, Math.max(1, columns - 6))
|
|
3163
|
-
const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column
|
|
3164
|
-
const next = moveCursorVertically(model, cursor, preferred, up ? -1 : 1)
|
|
3165
|
-
if (next !== cursor) {
|
|
3166
|
-
setCursor(next)
|
|
3167
|
-
preferredColumnRef.current = preferred
|
|
3168
|
-
}
|
|
3169
|
-
return
|
|
3170
|
-
}
|
|
3171
|
-
if (key.tab && menuActive) {
|
|
3172
|
-
acceptMenuCandidate()
|
|
3173
|
-
return
|
|
3174
|
-
}
|
|
3175
|
-
// Codex editor keymap: Alt/Ctrl+arrows and Alt+B/F move by word pieces;
|
|
3176
|
-
// plain arrows and Ctrl+B/F move by grapheme.
|
|
3177
|
-
if (key.leftArrow) {
|
|
3178
|
-
moveCursorTo(key.meta || key.ctrl ? moveWordLeft(value, cursor) : moveCursorBy(value, cursor, -1))
|
|
3179
|
-
return
|
|
3180
|
-
}
|
|
3181
|
-
if (key.rightArrow) {
|
|
3182
|
-
moveCursorTo(key.meta || key.ctrl ? moveWordRight(value, cursor) : moveCursorBy(value, cursor, 1))
|
|
3183
|
-
return
|
|
3184
|
-
}
|
|
3185
|
-
if (key.meta && input === 'b') {
|
|
3186
|
-
moveCursorTo(moveWordLeft(value, cursor))
|
|
3187
|
-
return
|
|
3188
|
-
}
|
|
3189
|
-
if (key.meta && input === 'f') {
|
|
3190
|
-
moveCursorTo(moveWordRight(value, cursor))
|
|
3191
|
-
return
|
|
3192
|
-
}
|
|
3193
|
-
if (key.ctrl && input === 'b') {
|
|
3194
|
-
moveCursorTo(moveCursorBy(value, cursor, -1))
|
|
3195
|
-
return
|
|
3196
|
-
}
|
|
3197
|
-
if (key.ctrl && input === 'f') {
|
|
3198
|
-
moveCursorTo(moveCursorBy(value, cursor, 1))
|
|
3199
|
-
return
|
|
3268
|
+
if (key.ctrl && (input === 'p' || input === 'n')) {
|
|
3269
|
+
navigateVertical(input === 'p' ? -1 : 1)
|
|
3270
|
+
return
|
|
3200
3271
|
}
|
|
3272
|
+
// Codex editor keymap: Alt/Ctrl+arrows and Alt+B/F move by word pieces;
|
|
3273
|
+
// plain arrows and Ctrl+B/F move by grapheme.
|
|
3274
|
+
if (key.leftArrow) {
|
|
3275
|
+
moveCursorTo(key.meta || key.ctrl ? moveWordLeft(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, -1))
|
|
3276
|
+
return
|
|
3277
|
+
}
|
|
3278
|
+
if (key.rightArrow) {
|
|
3279
|
+
moveCursorTo(key.meta || key.ctrl ? moveWordRight(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, 1))
|
|
3280
|
+
return
|
|
3281
|
+
}
|
|
3282
|
+
if (key.meta && input === 'b') {
|
|
3283
|
+
moveCursorTo(moveWordLeft(liveValue, liveCursor))
|
|
3284
|
+
return
|
|
3285
|
+
}
|
|
3286
|
+
if (key.meta && input === 'f') {
|
|
3287
|
+
moveCursorTo(moveWordRight(liveValue, liveCursor))
|
|
3288
|
+
return
|
|
3289
|
+
}
|
|
3290
|
+
if (key.ctrl && input === 'b') {
|
|
3291
|
+
moveCursorTo(moveCursorBy(liveValue, liveCursor, -1))
|
|
3292
|
+
return
|
|
3293
|
+
}
|
|
3294
|
+
if (key.ctrl && input === 'f') {
|
|
3295
|
+
moveCursorTo(moveCursorBy(liveValue, liveCursor, 1))
|
|
3296
|
+
return
|
|
3297
|
+
}
|
|
3201
3298
|
// Ctrl+W and Alt+Backspace delete the previous word piece into the kill
|
|
3202
|
-
// buffer; Alt+D and the raw Ctrl/Alt+Delete variants kill forward.
|
|
3203
|
-
if (key.ctrl && input === 'w') {
|
|
3204
|
-
applyEdit(deleteWordBackward(
|
|
3205
|
-
return
|
|
3206
|
-
}
|
|
3207
|
-
if (key.meta && input === 'd') {
|
|
3208
|
-
applyEdit(deleteWordForward(
|
|
3209
|
-
return
|
|
3299
|
+
// buffer; Alt+D and the raw Ctrl/Alt+Delete variants kill forward.
|
|
3300
|
+
if (key.ctrl && input === 'w') {
|
|
3301
|
+
applyEdit(deleteWordBackward(liveValue, liveCursor))
|
|
3302
|
+
return
|
|
3303
|
+
}
|
|
3304
|
+
if (key.meta && input === 'd') {
|
|
3305
|
+
applyEdit(deleteWordForward(liveValue, liveCursor))
|
|
3306
|
+
return
|
|
3210
3307
|
}
|
|
3211
3308
|
// Un-annotated backspace/delete (Ink maps both and here):
|
|
3212
|
-
// delete the grapheme before the cursor.
|
|
3213
|
-
if (key.backspace || key.delete) {
|
|
3214
|
-
applyEdit(deleteBackward(
|
|
3215
|
-
return
|
|
3309
|
+
// delete the grapheme before the cursor.
|
|
3310
|
+
if (key.backspace || key.delete) {
|
|
3311
|
+
applyEdit(deleteBackward(liveValue, liveCursor))
|
|
3312
|
+
return
|
|
3216
3313
|
}
|
|
3217
3314
|
// Readline parity over the LOGICAL line: A/E to its ends, U/K kill to
|
|
3218
|
-
// them (filling the single kill buffer), Y yanks it back.
|
|
3219
|
-
if (key.ctrl && input === 'a') {
|
|
3220
|
-
moveCursorTo(
|
|
3221
|
-
return
|
|
3222
|
-
}
|
|
3223
|
-
if (key.ctrl && input === 'e') {
|
|
3224
|
-
moveCursorTo(
|
|
3225
|
-
return
|
|
3226
|
-
}
|
|
3227
|
-
if (key.ctrl && input === 'u') {
|
|
3228
|
-
applyEdit(killToLineStart(
|
|
3229
|
-
return
|
|
3230
|
-
}
|
|
3231
|
-
if (key.ctrl && input === 'k') {
|
|
3232
|
-
applyEdit(killToLineEnd(
|
|
3233
|
-
return
|
|
3234
|
-
}
|
|
3235
|
-
if (key.ctrl && input === 'y') {
|
|
3236
|
-
if (killRef.current !== '') applyEdit(insertText(
|
|
3315
|
+
// them (filling the single kill buffer), Y yanks it back.
|
|
3316
|
+
if (key.ctrl && input === 'a') {
|
|
3317
|
+
moveCursorTo(moveToLineStart(liveValue, liveCursor, true))
|
|
3318
|
+
return
|
|
3319
|
+
}
|
|
3320
|
+
if (key.ctrl && input === 'e') {
|
|
3321
|
+
moveCursorTo(moveToLineEnd(liveValue, liveCursor, true))
|
|
3322
|
+
return
|
|
3323
|
+
}
|
|
3324
|
+
if (key.ctrl && input === 'u') {
|
|
3325
|
+
applyEdit(killToLineStart(liveValue, liveCursor))
|
|
3326
|
+
return
|
|
3327
|
+
}
|
|
3328
|
+
if (key.ctrl && input === 'k') {
|
|
3329
|
+
applyEdit(killToLineEnd(liveValue, liveCursor))
|
|
3330
|
+
return
|
|
3331
|
+
}
|
|
3332
|
+
if (key.ctrl && input === 'y') {
|
|
3333
|
+
if (killRef.current !== '') applyEdit(insertText(liveValue, liveCursor, killRef.current))
|
|
3237
3334
|
return
|
|
3238
3335
|
}
|
|
3239
3336
|
// Ctrl+L refreshes the screen (readline convention): raw ANSI clear
|
|
@@ -3275,11 +3372,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3275
3372
|
insertDroppedImages(droppedPaths)
|
|
3276
3373
|
return
|
|
3277
3374
|
}
|
|
3278
|
-
applyEdit(insertText(
|
|
3375
|
+
applyEdit(insertText(valueRef.current, cursorRef.current, text))
|
|
3279
3376
|
}
|
|
3280
|
-
})
|
|
3281
|
-
|
|
3282
|
-
// The DeepSeek easter-egg wave owns its 33ms tick HERE instead of in App:
|
|
3377
|
+
}, active)
|
|
3378
|
+
|
|
3379
|
+
// The DeepSeek easter-egg wave owns its 33ms tick HERE instead of in App:
|
|
3283
3380
|
// the interval re-renders only the composer band at 30fps, never the whole
|
|
3284
3381
|
// tree. App drives the tier/style pair on a model switch; this local effect
|
|
3285
3382
|
// starts the sweep whenever that pair changes (App picks a NEW random style
|
|
@@ -3299,7 +3396,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3299
3396
|
setWaveTick(0)
|
|
3300
3397
|
}
|
|
3301
3398
|
}, [waveTier, waveStyle])
|
|
3302
|
-
const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null
|
|
3399
|
+
const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null
|
|
3303
3400
|
&& waveTick * DEEPSEEK_WAVE_TICK_MS < deepseekWaveDuration(waveTier, waveStyle)
|
|
3304
3401
|
useEffect(() => {
|
|
3305
3402
|
if (!waveActive) return
|
|
@@ -3325,8 +3422,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3325
3422
|
// column-safe physical rows, with the caret mapped to its exact row and
|
|
3326
3423
|
// column. Computed before the frozen path so the row report below runs
|
|
3327
3424
|
// unconditionally.
|
|
3328
|
-
const
|
|
3329
|
-
const editorViewModel = editorModel(value, editorColumns)
|
|
3425
|
+
const editorViewModel = editorModel(value, editorColumns)
|
|
3330
3426
|
const clampedCursor = clampCursor(value, cursor)
|
|
3331
3427
|
const caret = caretSite(editorViewModel, clampedCursor)
|
|
3332
3428
|
const editorWindowRows = Math.min(editorViewModel.rows.length, Math.max(1, maxRows))
|
|
@@ -3352,7 +3448,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3352
3448
|
const bandWidth = Math.max(1, columns - 1)
|
|
3353
3449
|
const bandBg = inkColor(getPalette().composerBand)
|
|
3354
3450
|
const bandFill = (consumed: number): string => ' '.repeat(Math.max(0, bandWidth - consumed))
|
|
3355
|
-
const band = (content: ReactElement): ReactElement => createElement(
|
|
3451
|
+
const band = (content: ReactElement): ReactElement => createElement(
|
|
3356
3452
|
Box,
|
|
3357
3453
|
{ flexDirection: 'column', width: bandWidth },
|
|
3358
3454
|
createElement(Text, { backgroundColor: bandBg }, ' '.repeat(bandWidth)),
|
|
@@ -3390,141 +3486,138 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3390
3486
|
rows: menuRows,
|
|
3391
3487
|
})
|
|
3392
3488
|
|
|
3393
|
-
//
|
|
3394
|
-
//
|
|
3395
|
-
//
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
? createElement(BusyChase)
|
|
3421
|
-
: createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, `${promptGlyph} `)
|
|
3422
|
-
: ' ',
|
|
3423
|
-
before,
|
|
3424
|
-
createElement(CursorBlock, { key: 'caret', char: caretChar }),
|
|
3425
|
-
placeholder
|
|
3426
|
-
? createElement(Text, { dimColor: true }, COMPOSER_PLACEHOLDER)
|
|
3427
|
-
: after,
|
|
3489
|
+
// Every state reuses this exact multiline editor window. Only the caret row
|
|
3490
|
+
// owns an inverse block; non-caret rows render their text without a hidden
|
|
3491
|
+
// spacer or a second blink timer.
|
|
3492
|
+
const editorRows: ReactElement[] = []
|
|
3493
|
+
for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
|
|
3494
|
+
const row = editorViewModel.rows[index]!
|
|
3495
|
+
const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages)
|
|
3496
|
+
const placeholder = index === 0 && value === '' && !busy && !preparingImages
|
|
3497
|
+
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after
|
|
3498
|
+
const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail)
|
|
3499
|
+
editorRows.push(createElement(
|
|
3500
|
+
Text,
|
|
3501
|
+
{ key: index, backgroundColor: bandBg, wrap: 'truncate-end' },
|
|
3502
|
+
index === 0
|
|
3503
|
+
? preparingImages
|
|
3504
|
+
? createElement(Text, { color: inkColor(getPalette().warn), bold: true }, '… ')
|
|
3505
|
+
: busy
|
|
3506
|
+
? createElement(BusyChase)
|
|
3507
|
+
: createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, `${promptGlyph} `)
|
|
3508
|
+
: ' ',
|
|
3509
|
+
parts.before,
|
|
3510
|
+
parts.hasCaret
|
|
3511
|
+
? createElement(Text, { key: 'caret', inverse: cursorVisible || undefined }, parts.caret)
|
|
3512
|
+
: null,
|
|
3513
|
+
placeholder
|
|
3514
|
+
? createElement(Text, { dimColor: true }, COMPOSER_PLACEHOLDER)
|
|
3515
|
+
: parts.after,
|
|
3428
3516
|
bandFill(consumed),
|
|
3429
3517
|
))
|
|
3430
|
-
}
|
|
3431
|
-
const staticEditor = createElement(Box, { flexDirection: 'column' }, ...editorRows)
|
|
3432
|
-
|
|
3433
|
-
//
|
|
3434
|
-
//
|
|
3435
|
-
//
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
const
|
|
3441
|
-
const
|
|
3442
|
-
const
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
const cells: ComposerCell[]
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
const
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3518
|
+
}
|
|
3519
|
+
const staticEditor = createElement(Box, { flexDirection: 'column' }, ...editorRows)
|
|
3520
|
+
|
|
3521
|
+
// The wave paints the SAME visible rows and caret site as the static path.
|
|
3522
|
+
// Graphemes remain atomic and every background sample advances by terminal
|
|
3523
|
+
// display columns, so CJK and emoji cannot move the caret or wrap the band.
|
|
3524
|
+
const waveRow = (): ReactElement => {
|
|
3525
|
+
const hues = deepseekWaveHues(waveTier!)
|
|
3526
|
+
const style = waveStyle!
|
|
3527
|
+
const bandRgb = getPalette().composerBand
|
|
3528
|
+
const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows)
|
|
3529
|
+
const totalBandRows = visibleRows.length + 2
|
|
3530
|
+
const waveBg = (row: number, column: number): string => {
|
|
3531
|
+
const rgb = deepseekWaveColumnBg(waveTick!, column, bandWidth, waveTier!, style, hues, bandRgb, row, totalBandRows)
|
|
3532
|
+
return rgb === null ? bandBg : inkColor(rgb)
|
|
3533
|
+
}
|
|
3534
|
+
const blankBandRow = (row: number): ReactElement => {
|
|
3535
|
+
const blanks: ComposerCell[] = []
|
|
3536
|
+
for (let column = 0; column < bandWidth; column += 1) {
|
|
3537
|
+
blanks.push({ char: ' ', width: 1, backgroundColor: waveBg(row, column) })
|
|
3538
|
+
}
|
|
3539
|
+
return createElement(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks))
|
|
3540
|
+
}
|
|
3541
|
+
const cellIndexAtColumn = (cells: readonly ComposerCell[], target: number): number | undefined => {
|
|
3542
|
+
let column = 0
|
|
3543
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
3544
|
+
if (column === target) return index
|
|
3545
|
+
column += cells[index]!.width ?? visibleColumns(cells[index]!.char)
|
|
3546
|
+
if (column > target) return undefined
|
|
3547
|
+
}
|
|
3548
|
+
return undefined
|
|
3549
|
+
}
|
|
3550
|
+
const editorWaveRows = visibleRows.map((row, visibleIndex) => {
|
|
3551
|
+
const sourceIndex = editorWindowStart + visibleIndex
|
|
3552
|
+
const bandRow = visibleIndex + 1
|
|
3553
|
+
const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor)
|
|
3554
|
+
const placeholder = sourceIndex === 0 && value === '' && !busy
|
|
3555
|
+
const cells: ComposerCell[] = []
|
|
3556
|
+
let usedColumns = 0
|
|
3557
|
+
const push = (char: string, extra: Omit<ComposerCell, 'char' | 'width' | 'backgroundColor'> = {}): void => {
|
|
3558
|
+
const width = visibleColumns(char)
|
|
3559
|
+
cells.push({ char, width, backgroundColor: waveBg(bandRow, usedColumns), ...extra })
|
|
3560
|
+
usedColumns += width
|
|
3561
|
+
}
|
|
3562
|
+
if (sourceIndex === 0) {
|
|
3563
|
+
push(promptGlyph, { color: promptColor, bold: true })
|
|
3564
|
+
push(' ', { color: promptColor })
|
|
3565
|
+
} else {
|
|
3566
|
+
push(' ')
|
|
3567
|
+
push(' ')
|
|
3568
|
+
}
|
|
3569
|
+
for (const span of splitGraphemes(parts.before)) push(span.text)
|
|
3570
|
+
if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible })
|
|
3571
|
+
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after
|
|
3572
|
+
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {})
|
|
3573
|
+
while (usedColumns < bandWidth) push(' ')
|
|
3574
|
+
|
|
3575
|
+
const middleBandRow = Math.floor(totalBandRows / 2)
|
|
3576
|
+
if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick!, waveTier!, style)) {
|
|
3577
|
+
const word = waveTier === 'unknown' ? 'Into the Unknown' : 'deepseek'
|
|
3578
|
+
const start = Math.max(2, Math.floor((bandWidth - word.length) / 2))
|
|
3579
|
+
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at))
|
|
3580
|
+
if (indices.every(index => index !== undefined && (cells[index]!.char === ' ' || cells[index]!.dim === true))) {
|
|
3581
|
+
for (let at = 0; at < word.length; at += 1) {
|
|
3582
|
+
const cell = cells[indices[at]!]!
|
|
3583
|
+
cell.char = word[at]!
|
|
3584
|
+
cell.width = 1
|
|
3585
|
+
cell.color = inkColor(deepseekWaveWordHue(at, hues))
|
|
3586
|
+
cell.bold = true
|
|
3587
|
+
cell.dim = false
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
if (bandRow === middleBandRow && (waveTier === 'deepseek' || waveTier === 'unknown') && style === 'wave') {
|
|
3592
|
+
const spark = deepseekWaveSpark(waveTick!)
|
|
3593
|
+
const lastIndex = cellIndexAtColumn(cells, bandWidth - 1)
|
|
3594
|
+
if (spark !== null && lastIndex !== undefined && cells[lastIndex]!.char === ' ') {
|
|
3595
|
+
cells[lastIndex]!.char = spark
|
|
3596
|
+
cells[lastIndex]!.color = promptColor
|
|
3597
|
+
cells[lastIndex]!.bold = true
|
|
3598
|
+
cells[lastIndex]!.dim = false
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
return createElement(Text, { key: `editor-${sourceIndex}`, wrap: 'truncate-end' }, ...waveRowSpans(cells))
|
|
3602
|
+
})
|
|
3603
|
+
return createElement(
|
|
3604
|
+
Box,
|
|
3605
|
+
{ flexDirection: 'column', width: bandWidth },
|
|
3606
|
+
blankBandRow(0),
|
|
3607
|
+
...editorWaveRows,
|
|
3608
|
+
blankBandRow(totalBandRows - 1),
|
|
3609
|
+
)
|
|
3610
|
+
}
|
|
3520
3611
|
|
|
3521
|
-
return createElement(
|
|
3522
|
-
Box,
|
|
3523
|
-
{ flexDirection: 'column' },
|
|
3524
|
-
menu,
|
|
3525
|
-
waveTick !== null && waveTier !== null && !busy
|
|
3526
|
-
|
|
3527
|
-
|
|
3612
|
+
return createElement(
|
|
3613
|
+
Box,
|
|
3614
|
+
{ flexDirection: 'column' },
|
|
3615
|
+
menu,
|
|
3616
|
+
waveTick !== null && waveTier !== null && waveStyle !== null && !busy && !preparingImages
|
|
3617
|
+
? waveRow()
|
|
3618
|
+
: band(staticEditor),
|
|
3619
|
+
)
|
|
3620
|
+
}
|
|
3528
3621
|
|
|
3529
3622
|
/** One cached settled row: the row Box plus its roomy-prompt spacers. */
|
|
3530
3623
|
interface SettledRowRecord {
|