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.
@@ -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) return createElement(Box, { display: 'none' })
51
- if (viewport.compact) {
52
- return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(`${props.title} · esc close`), viewport.contentColumns))
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
- if (key.return && visible[cursor]?.broken === undefined) return select(visible[cursor]!.id)
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
- return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('/history · esc close', viewport.contentColumns))
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
- * Remove bracketed paste markers from one input chunk. Panel drafts accept raw
80
- * `input` text, where an unhandled paste would otherwise persist the literal
81
- * "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
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+))?(?:[:;](\\d+))?u'
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
- // Kitty disambiguate mode reports the six legacy functional keys as CSI u
118
- // codes 1-6 (Home, Insert, Delete, End, PageUp, PageDown). Ink 5 cannot
119
- // parse these forms and would insert literal "[3u" text into the draft, so
120
- // rewrite them to the legacy sequences the input layer already annotates.
121
- // The modifier parameter passes through: kitty and xterm share the same
122
- // 1+bit-field encoding (shift 2, alt 3, ctrl 5, ...). Lock-key bits are
123
- // dropped because the legacy sequences cannot express them.
124
- if (key.code >= 1 && key.code <= 6) {
125
- const mask = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0)
126
- const mods = mask === 0 ? '' : `;${mask + 1}`
127
- if (key.code === 1) return mods === '' ? '\x1b[H' : `\x1b[1${mods}H`
128
- if (key.code === 4) return mods === '' ? '\x1b[F' : `\x1b[1${mods}F`
129
- return `\x1b[${key.code}${mods}~`
130
- }
131
- if (key.code >= 97 && key.code <= 122) {
132
- const letter = String.fromCodePoint(key.code)
133
- if (ctrl) return String.fromCodePoint(key.code - 96)
134
- if (alt) return '\x1b' + letter
135
- if (shift) return String.fromCodePoint(key.alternate ?? key.code - 32)
136
- return letter
137
- }
138
- if (key.code >= 65 && key.code <= 90) {
139
- if (ctrl) return String.fromCodePoint(key.code + 32 - 96)
140
- if (alt) return '\x1b' + String.fromCodePoint(key.code + 32)
141
- return String.fromCodePoint(key.code)
142
- }
143
- if (key.code >= 32 && key.code <= 126 && key.alternate !== undefined) {
144
- const base = key.alternate >= 97 && key.alternate <= 122 ? key.alternate : key.code
145
- if (ctrl && base - 96 >= 1 && base - 96 <= 26) return String.fromCodePoint(base - 96)
146
- if (alt) return '\x1b' + String.fromCodePoint(key.alternate)
147
- return String.fromCodePoint(key.alternate)
148
- }
149
- return undefined
150
- }
151
-
152
- /**
153
- * Rewrite every decodable kitty CSI-u sequence in one stdin chunk to the
154
- * legacy form the input layer already handles. Undecodable or non-key
155
- * sequences pass through untouched, so terminals without the protocol are
156
- * unaffected.
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
- alternate: third !== undefined && third !== '' ? Number.parseInt(third, 10) : undefined,
166
- })
167
- return legacy ?? whole
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 ??= new WorkspaceFileSearch(cwd, {
126
- maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
127
- maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
128
- excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES],
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(() => [] as readonly ServiceFileCandidate[])
156
+ .catch((error: unknown) => {
157
+ fileFailure = error
158
+ return [] as readonly ServiceFileCandidate[]
159
+ })
141
160
  : agent === undefined
142
- ? preSessionFiles(needle, signal).catch(() => [] as readonly ServiceFileCandidate[])
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(() => [] as readonly SessionReferenceCandidate[])
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
- return [...fileRows, ...sessionRows]
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)