dsh-code 1.0.3 → 1.0.4
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.md +291 -285
- package/bin/deepseek.mjs +26 -3
- package/lib/index.mjs +2749 -1783
- package/lib/types/app.d.ts +11 -2
- package/lib/types/commands.d.ts +13 -0
- package/lib/types/index.d.ts +28 -0
- package/lib/types/input-split.d.ts +54 -0
- package/lib/types/kernel-panels.d.ts +3 -1
- package/lib/types/keyboard.d.ts +8 -0
- package/lib/types/provider-settings.d.ts +77 -0
- package/lib/types/render/projection.d.ts +7 -1
- package/lib/types/render/status.d.ts +22 -15
- package/lib/types/skills.d.ts +1 -1
- package/package.json +1 -1
- package/src/app.ts +5459 -4900
- package/src/approval.ts +8 -3
- package/src/authorization-panel.ts +2 -4
- package/src/commands.ts +27 -3
- package/src/index.ts +153 -38
- package/src/input-split.ts +191 -0
- package/src/internals.ts +26 -8
- package/src/kernel-panels.ts +26 -10
- package/src/keyboard.ts +123 -88
- package/src/mentions.ts +42 -9
- package/src/provider-settings.ts +204 -0
- package/src/questions.ts +20 -0
- package/src/render/lines.ts +24 -12
- package/src/render/markdown.ts +15 -13
- package/src/render/projection.ts +99 -12
- package/src/render/status.ts +76 -71
- package/src/render/text.ts +9 -3
- package/src/skills.ts +19 -6
- package/src/theme-panel.ts +79 -72
package/src/kernel-panels.ts
CHANGED
|
@@ -47,9 +47,19 @@ function searchLine(searching: boolean | undefined, query: string): string {
|
|
|
47
47
|
function ListFrame(props: ListFrameProps): ReactElement {
|
|
48
48
|
const stdout = useStdout().stdout
|
|
49
49
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
50
|
-
if (viewport.maxHeight === 0
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
51
|
+
// One visible row instead of a hidden panel: the current selection (or
|
|
52
|
+
// load state) is shown so Enter/arrows are never blind keys acting on
|
|
53
|
+
// invisible state. The selection leads and `esc close` follows it, so a
|
|
54
|
+
// narrow terminal truncates the panel title — never the actionable facts.
|
|
55
|
+
const body = props.loading
|
|
56
|
+
? `${props.title} · loading…`
|
|
57
|
+
: props.error !== undefined
|
|
58
|
+
? `${props.title} · load failed`
|
|
59
|
+
: props.rows.length === 0
|
|
60
|
+
? `${props.title} · no matching entries`
|
|
61
|
+
: `❯ ${singleLineText(props.rows[props.cursor]?.text ?? '')}`
|
|
62
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(`${body} · esc close`), viewport.contentColumns))
|
|
53
63
|
}
|
|
54
64
|
const stateRows = props.loading
|
|
55
65
|
? [{ key: 'loading', text: ' loading…' }]
|
|
@@ -117,7 +127,10 @@ export function ModePanel({ current, load, select, close }: {
|
|
|
117
127
|
if (input === 'r' && query === '') return refresh()
|
|
118
128
|
if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
|
|
119
129
|
if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
|
|
120
|
-
|
|
130
|
+
// Empty/loading/filtered-out lists have no row at the cursor: a bare
|
|
131
|
+
// `?.broken === undefined` check passes on undefined and crashes the
|
|
132
|
+
// process on the `!.id` access (PermissionPanel guards this correctly).
|
|
133
|
+
if (key.return && visible[cursor] !== undefined && visible[cursor]!.broken === undefined) return select(visible[cursor]!.id)
|
|
121
134
|
const next = editQuery(query, input, key)
|
|
122
135
|
if (next !== undefined) { setQuery(next); setCursor(0) }
|
|
123
136
|
})
|
|
@@ -494,10 +507,10 @@ export function HistoryPanel({ entries, fill, close }: {
|
|
|
494
507
|
})
|
|
495
508
|
const stdout = useStdout().stdout
|
|
496
509
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
497
|
-
if (viewport.compact) {
|
|
498
|
-
|
|
510
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
511
|
+
const picked = matches[cursor]
|
|
512
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('/history · ' + (picked === undefined ? 'no matching prompts' : singleLineText(picked)) + ' · esc close', viewport.contentColumns))
|
|
499
513
|
}
|
|
500
|
-
if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
|
|
501
514
|
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
502
515
|
const offset = revealRow(0, cursor, matches.length, bodyRows)
|
|
503
516
|
const visible = matches.slice(offset, offset + bodyRows)
|
|
@@ -586,10 +599,9 @@ export function StatuslinePanel({ enabled, change, close }: {
|
|
|
586
599
|
})
|
|
587
600
|
const stdout = useStdout().stdout
|
|
588
601
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
589
|
-
if (viewport.compact) {
|
|
602
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
590
603
|
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('/statusline · esc close', viewport.contentColumns))
|
|
591
604
|
}
|
|
592
|
-
if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
|
|
593
605
|
const bodyRows = Math.max(1, viewport.bodyRows - 1)
|
|
594
606
|
const offset = revealRow(0, cursor, order.length, bodyRows)
|
|
595
607
|
const visible = order.slice(offset, offset + bodyRows)
|
|
@@ -630,7 +642,7 @@ export function StatuslinePanel({ enabled, change, close }: {
|
|
|
630
642
|
* instead of a bare failure notice. Enter applies one level; Esc returns to
|
|
631
643
|
* the model list without applying.
|
|
632
644
|
*/
|
|
633
|
-
export function EffortPanel({ row, current, select, back }: {
|
|
645
|
+
export function EffortPanel({ row, current, select, back, onExit }: {
|
|
634
646
|
/** The model row whose advertised levels this stage lists. */
|
|
635
647
|
row: ModelRow
|
|
636
648
|
/** Effective effort currently in force ('' when none), for the ● mark. */
|
|
@@ -639,6 +651,8 @@ export function EffortPanel({ row, current, select, back }: {
|
|
|
639
651
|
select(effortId: string): void
|
|
640
652
|
/** Return to the model list without applying. */
|
|
641
653
|
back(): void
|
|
654
|
+
/** Leave the whole /model flow (Ctrl+C). */
|
|
655
|
+
onExit(): void
|
|
642
656
|
}): ReactElement {
|
|
643
657
|
const advertised = row.reasoning?.efforts ?? []
|
|
644
658
|
const empty = row.reasoning === undefined || advertised.length === 0
|
|
@@ -666,6 +680,7 @@ export function EffortPanel({ row, current, select, back }: {
|
|
|
666
680
|
}, [rows.length, cursor])
|
|
667
681
|
useInput((input, key) => {
|
|
668
682
|
if (key.escape || input === 'q') return back()
|
|
683
|
+
if (key.ctrl && input === 'c') return onExit()
|
|
669
684
|
if (empty) return
|
|
670
685
|
if (input === 'g') {
|
|
671
686
|
setCursor(0)
|
|
@@ -894,6 +909,7 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
|
|
|
894
909
|
current: current === '' ? undefined : current.split('@')[1],
|
|
895
910
|
select: effortId => pick(effortFor, effortId),
|
|
896
911
|
back: () => setEffortFor(undefined),
|
|
912
|
+
onExit: close,
|
|
897
913
|
})
|
|
898
914
|
}
|
|
899
915
|
return createElement(ListFrame, {
|
package/src/keyboard.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
|
|
3
|
-
* kitty CSI-u normalization layer.
|
|
4
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
|
|
3
|
+
* kitty CSI-u normalization layer.
|
|
4
|
+
*
|
|
5
5
|
* The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
|
|
6
6
|
* and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
|
|
7
7
|
* deliberately NOT requested: Ink 5's parser cannot decode the
|
|
8
8
|
* `:event-type` suffix, and repeat/release reporting buys this surface
|
|
9
9
|
* nothing.
|
|
10
|
-
*
|
|
11
|
-
* Ink 5 also cannot parse most CSI-u forms at all — they fall through its
|
|
12
|
-
* regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
|
|
13
|
-
* stdin read patch therefore rewrites every CSI-u form it can decode back
|
|
14
|
-
* to the legacy byte or canonical sequence the existing key handling
|
|
10
|
+
*
|
|
11
|
+
* Ink 5 also cannot parse most CSI-u forms at all — they fall through its
|
|
12
|
+
* regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
|
|
13
|
+
* stdin read patch therefore rewrites every CSI-u form it can decode back
|
|
14
|
+
* to the legacy byte or canonical sequence the existing key handling
|
|
15
15
|
* already understands, before Ink ever parses the chunk.
|
|
16
16
|
* @module @deepseek-ai/dsh-code/keyboard
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
|
|
20
20
|
export const KEYBOARD_ENHANCE_ENABLE = '\x1b[>4;0m\x1b[>5u'
|
|
21
|
-
|
|
22
|
-
/** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
|
|
21
|
+
|
|
22
|
+
/** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
|
|
23
23
|
export const KEYBOARD_ENHANCE_DISABLE = '\x1b[<u\x1b[>4;0m'
|
|
24
24
|
|
|
25
25
|
/** Explicit environment overrides for terminal keyboard enhancement. */
|
|
@@ -47,7 +47,7 @@ export function shouldEnableKeyboardEnhancement(env: NodeJS.ProcessEnv = process
|
|
|
47
47
|
if (explicitEnable !== undefined) return explicitEnable
|
|
48
48
|
return !isVsCodeTerminalEnv(env)
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
/** Enable bracketed paste reporting. */
|
|
52
52
|
export const BRACKETED_PASTE_ENABLE = '\x1b[?2004h'
|
|
53
53
|
|
|
@@ -70,16 +70,25 @@ export function stripTerminalFocusEvents(chunk: string, onFocus: (focused: boole
|
|
|
70
70
|
return ''
|
|
71
71
|
})
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
/** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
|
|
75
|
-
export const PASTE_START_MARKER = '[200~'
|
|
76
|
-
export const PASTE_END_MARKER = '[201~'
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
73
|
+
|
|
74
|
+
/** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
|
|
75
|
+
export const PASTE_START_MARKER = '[200~'
|
|
76
|
+
export const PASTE_END_MARKER = '[201~'
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* How long an unterminated bracketed-paste block may hold buffered bytes
|
|
80
|
+
* before the input splitter strips its start marker and releases them: a
|
|
81
|
+
* terminal that loses the end marker must never take the whole keyboard
|
|
82
|
+
* hostage (Esc/Ctrl+C included). Shared by the splitter and the composer's
|
|
83
|
+
* lost-paste safety net so both use one window.
|
|
84
|
+
*/
|
|
85
|
+
export const PASTE_BRACKET_TIMEOUT_MS = 1_000
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove bracketed paste markers from one input chunk. Panel drafts accept raw
|
|
89
|
+
* `input` text, where an unhandled paste would otherwise persist the literal
|
|
90
|
+
* "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
|
|
91
|
+
*/
|
|
83
92
|
export function stripPasteMarkers(text: string): string {
|
|
84
93
|
return text
|
|
85
94
|
.replaceAll(`\x1b${PASTE_START_MARKER}`, '')
|
|
@@ -87,22 +96,37 @@ export function stripPasteMarkers(text: string): string {
|
|
|
87
96
|
.replaceAll(PASTE_START_MARKER, '')
|
|
88
97
|
.replaceAll(PASTE_END_MARKER, '')
|
|
89
98
|
}
|
|
90
|
-
|
|
91
|
-
/** One decoded CSI-u keypress: key code, 1-based modifier param, alternate code. */
|
|
92
|
-
interface CsiUKey {
|
|
93
|
-
code: number
|
|
94
|
-
modifiers: number
|
|
95
|
-
alternate?: number
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
|
|
99
|
-
const CSI_U_SOURCE = '\x1b\\[(\\d+)(?:;(\\d+))?(?:
|
|
99
|
+
|
|
100
|
+
/** One decoded CSI-u keypress: key code, 1-based modifier param, alternate code. */
|
|
101
|
+
interface CsiUKey {
|
|
102
|
+
code: number
|
|
103
|
+
modifiers: number
|
|
104
|
+
alternate?: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
|
|
108
|
+
const CSI_U_SOURCE = '\x1b\\[(\\d+)(?:;(\\d+))?(?:(:|;)(\\d+))?u'
|
|
109
|
+
|
|
110
|
+
/** Kitty private-use keycodes for the numeric keypad and keypad Enter. */
|
|
111
|
+
const KITTY_KEYPAD_CODES: Readonly<Record<number, string>> = {
|
|
112
|
+
57399: '0',
|
|
113
|
+
57400: '1',
|
|
114
|
+
57401: '2',
|
|
115
|
+
57402: '3',
|
|
116
|
+
57403: '4',
|
|
117
|
+
57404: '5',
|
|
118
|
+
57405: '6',
|
|
119
|
+
57406: '7',
|
|
120
|
+
57407: '8',
|
|
121
|
+
57408: '9',
|
|
122
|
+
57414: '\r',
|
|
123
|
+
}
|
|
100
124
|
|
|
101
125
|
/** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
|
|
102
126
|
function legacyForKey(key: CsiUKey): string | undefined {
|
|
103
|
-
const bits = Math.max(0, key.modifiers - 1)
|
|
104
|
-
const shift = (bits & 1) !== 0
|
|
105
|
-
const alt = (bits & 2) !== 0
|
|
127
|
+
const bits = Math.max(0, key.modifiers - 1)
|
|
128
|
+
const shift = (bits & 1) !== 0
|
|
129
|
+
const alt = (bits & 2) !== 0
|
|
106
130
|
const ctrl = (bits & 4) !== 0
|
|
107
131
|
if (key.code === 13) {
|
|
108
132
|
// Modified Enter has no dedicated composer behavior. Preserve the legacy
|
|
@@ -110,62 +134,73 @@ function legacyForKey(key: CsiUKey): string | undefined {
|
|
|
110
134
|
if (ctrl) return '\n'
|
|
111
135
|
if (alt) return '\x1b\r'
|
|
112
136
|
return '\r'
|
|
113
|
-
}
|
|
114
|
-
if (key.code === 27) return '\x1b'
|
|
115
|
-
if (key.code === 9) return shift ? '\x1b[Z' : '\t'
|
|
116
|
-
if (key.code === 127) return alt || ctrl ? '\x1b\x7f' : '\x7f'
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
if (key.code >=
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
137
|
+
}
|
|
138
|
+
if (key.code === 27) return '\x1b'
|
|
139
|
+
if (key.code === 9) return shift ? '\x1b[Z' : '\t'
|
|
140
|
+
if (key.code === 127) return alt || ctrl ? '\x1b\x7f' : '\x7f'
|
|
141
|
+
const keypad = KITTY_KEYPAD_CODES[key.code]
|
|
142
|
+
if (keypad !== undefined) {
|
|
143
|
+
if (alt) return '\x1b' + keypad
|
|
144
|
+
return keypad
|
|
145
|
+
}
|
|
146
|
+
// Kitty disambiguate mode reports the six legacy functional keys as CSI u
|
|
147
|
+
// codes 1-6 (Home, Insert, Delete, End, PageUp, PageDown). Ink 5 cannot
|
|
148
|
+
// parse these forms and would insert literal "[3u" text into the draft, so
|
|
149
|
+
// rewrite them to the legacy sequences the input layer already annotates.
|
|
150
|
+
// The modifier parameter passes through: kitty and xterm share the same
|
|
151
|
+
// 1+bit-field encoding (shift 2, alt 3, ctrl 5, ...). Lock-key bits are
|
|
152
|
+
// dropped because the legacy sequences cannot express them.
|
|
153
|
+
if (key.code >= 1 && key.code <= 6) {
|
|
154
|
+
const mask = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0)
|
|
155
|
+
const mods = mask === 0 ? '' : `;${mask + 1}`
|
|
156
|
+
if (key.code === 1) return mods === '' ? '\x1b[H' : `\x1b[1${mods}H`
|
|
157
|
+
if (key.code === 4) return mods === '' ? '\x1b[F' : `\x1b[1${mods}F`
|
|
158
|
+
return `\x1b[${key.code}${mods}~`
|
|
159
|
+
}
|
|
160
|
+
if (key.code >= 97 && key.code <= 122) {
|
|
161
|
+
const letter = String.fromCodePoint(key.code)
|
|
162
|
+
if (ctrl) return String.fromCodePoint(key.code - 96)
|
|
163
|
+
if (alt) return '\x1b' + letter
|
|
164
|
+
if (shift) return String.fromCodePoint(key.alternate ?? key.code - 32)
|
|
165
|
+
return letter
|
|
166
|
+
}
|
|
167
|
+
if (key.code >= 65 && key.code <= 90) {
|
|
168
|
+
if (ctrl) return String.fromCodePoint(key.code + 32 - 96)
|
|
169
|
+
if (alt) return '\x1b' + String.fromCodePoint(key.code + 32)
|
|
170
|
+
return String.fromCodePoint(key.code)
|
|
171
|
+
}
|
|
172
|
+
if (key.code >= 32 && key.code <= 126) {
|
|
173
|
+
// Kitty reports ordinary printable keys as CSI-u with no alternate code
|
|
174
|
+
// (for example space is `ESC[32u` and `1` is `ESC[49u`). Keep the
|
|
175
|
+
// alternate form when present, but never leave a plain printable key as
|
|
176
|
+
// an unknown escape sequence for Ink to swallow.
|
|
177
|
+
const base = key.alternate ?? key.code
|
|
178
|
+
if (ctrl && base - 96 >= 1 && base - 96 <= 26) return String.fromCodePoint(base - 96)
|
|
179
|
+
if (alt) return '\x1b' + String.fromCodePoint(base)
|
|
180
|
+
return String.fromCodePoint(base)
|
|
181
|
+
}
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Rewrite every decodable kitty CSI-u sequence in one stdin chunk to the
|
|
187
|
+
* legacy form the input layer already handles. Undecodable or non-key
|
|
188
|
+
* sequences pass through untouched, so terminals without the protocol are
|
|
189
|
+
* unaffected.
|
|
190
|
+
*/
|
|
158
191
|
export function normalizeKeyboardChunk(chunk: string): string {
|
|
159
192
|
if (!chunk.includes('\x1b[') || !chunk.includes('u')) return chunk
|
|
160
193
|
const pattern = new RegExp(CSI_U_SOURCE, 'g')
|
|
161
|
-
return chunk.replace(pattern, (whole, code: string, mods?: string, third?: string) => {
|
|
162
|
-
const legacy = legacyForKey({
|
|
163
|
-
code: Number.parseInt(code, 10),
|
|
164
|
-
modifiers: mods === undefined || mods === '' ? 1 : Math.max(1, Number.parseInt(mods, 10)),
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
return chunk.replace(pattern, (whole, code: string, mods?: string, separator?: ':' | ';', third?: string) => {
|
|
195
|
+
const legacy = legacyForKey({
|
|
196
|
+
code: Number.parseInt(code, 10),
|
|
197
|
+
modifiers: mods === undefined || mods === '' ? 1 : Math.max(1, Number.parseInt(mods, 10)),
|
|
198
|
+
// A colon introduces Kitty's optional event type (`:1` = press), not
|
|
199
|
+
// an alternate key code. Semicolon introduces the alternate code.
|
|
200
|
+
alternate: separator === ';' && third !== undefined && third !== '' ? Number.parseInt(third, 10) : undefined,
|
|
201
|
+
})
|
|
202
|
+
return legacy ?? whole
|
|
203
|
+
})
|
|
169
204
|
}
|
|
170
205
|
|
|
171
206
|
/** Editor actions Ink cannot distinguish reliably when terminal bytes batch. */
|
package/src/mentions.ts
CHANGED
|
@@ -120,29 +120,54 @@ export function createMentions(ctx: Context, agent: Agent | undefined, cwd: stri
|
|
|
120
120
|
const sessionCapable = agent !== undefined && resolver !== undefined
|
|
121
121
|
// Pre-session fallback: one lazily built search over the launch cwd with
|
|
122
122
|
// the official defaults — pure in-memory index, no handles to release.
|
|
123
|
+
// The mounted service invalidates its per-agent index after every tool
|
|
124
|
+
// result; nothing drives that for this bare instance, so a time window
|
|
125
|
+
// refreshes it instead — without it, files created or deleted before the
|
|
126
|
+
// first session never appear in (or never leave) the fuzzy @ menu.
|
|
127
|
+
const PRE_SESSION_INDEX_TTL_MS = 30_000
|
|
123
128
|
let preSessionSearch: WorkspaceFileSearch | undefined
|
|
129
|
+
let preSessionIndexedAt = 0
|
|
124
130
|
const preSessionFiles = (query: string, signal?: AbortSignal): Promise<readonly ServiceFileCandidate[]> => {
|
|
125
|
-
preSessionSearch
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
if (preSessionSearch === undefined || Date.now() - preSessionIndexedAt > PRE_SESSION_INDEX_TTL_MS) {
|
|
132
|
+
preSessionSearch?.invalidate()
|
|
133
|
+
preSessionSearch = new WorkspaceFileSearch(cwd, {
|
|
134
|
+
maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
135
|
+
maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
136
|
+
excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES],
|
|
137
|
+
})
|
|
138
|
+
preSessionIndexedAt = Date.now()
|
|
139
|
+
}
|
|
130
140
|
return preSessionSearch.list(query, signal ?? new AbortController().signal)
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
return {
|
|
134
144
|
async candidates(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]> {
|
|
135
145
|
const needle = query.trim()
|
|
146
|
+
// A failed discovery half is remembered, never masked: when NOTHING came
|
|
147
|
+
// back the rejection tells the menu "search unavailable" instead of a
|
|
148
|
+
// silent empty list that reads as "no matches". A half that still
|
|
149
|
+
// returned rows shows them — partial results beat an error wall.
|
|
150
|
+
let fileFailure: unknown
|
|
151
|
+
let sessionFailure: unknown
|
|
136
152
|
const [files, sessions] = await Promise.all([
|
|
137
153
|
agent !== undefined && fileReferences !== undefined
|
|
138
154
|
? fileReferences
|
|
139
155
|
.list(agent, needle, signal ?? new AbortController().signal)
|
|
140
|
-
.catch(() =>
|
|
156
|
+
.catch((error: unknown) => {
|
|
157
|
+
fileFailure = error
|
|
158
|
+
return [] as readonly ServiceFileCandidate[]
|
|
159
|
+
})
|
|
141
160
|
: agent === undefined
|
|
142
|
-
? preSessionFiles(needle, signal).catch(() =>
|
|
161
|
+
? preSessionFiles(needle, signal).catch((error: unknown) => {
|
|
162
|
+
fileFailure = error
|
|
163
|
+
return [] as readonly ServiceFileCandidate[]
|
|
164
|
+
})
|
|
143
165
|
: Promise.resolve([] as readonly ServiceFileCandidate[]),
|
|
144
166
|
sessionCapable && needle !== '' && !isPathLikeMentionQuery(needle) && agent !== undefined
|
|
145
|
-
? resolver!.listCandidates(agent, needle, 10, signal).catch(() =>
|
|
167
|
+
? resolver!.listCandidates(agent, needle, 10, signal).catch((error: unknown) => {
|
|
168
|
+
sessionFailure = error
|
|
169
|
+
return [] as readonly SessionReferenceCandidate[]
|
|
170
|
+
})
|
|
146
171
|
: Promise.resolve([] as readonly SessionReferenceCandidate[]),
|
|
147
172
|
])
|
|
148
173
|
// The service owns ranking (and the bare-@ default rows); the menu caps
|
|
@@ -161,7 +186,15 @@ export function createMentions(ctx: Context, agent: Agent | undefined, cwd: stri
|
|
|
161
186
|
description: `Session · ${candidate.cwd ?? '(no cwd)'}`,
|
|
162
187
|
kind: 'session',
|
|
163
188
|
}))
|
|
164
|
-
|
|
189
|
+
const rows = [...fileRows, ...sessionRows]
|
|
190
|
+
if (rows.length === 0 && (fileFailure !== undefined || sessionFailure !== undefined)) {
|
|
191
|
+
throw fileFailure instanceof Error
|
|
192
|
+
? fileFailure
|
|
193
|
+
: sessionFailure instanceof Error
|
|
194
|
+
? sessionFailure
|
|
195
|
+
: new Error(String(fileFailure ?? sessionFailure))
|
|
196
|
+
}
|
|
197
|
+
return rows
|
|
165
198
|
},
|
|
166
199
|
parse(text: string): ParsedSessionReferenceText {
|
|
167
200
|
return parseSessionReferenceText(text)
|