dsh-code 1.0.5 → 1.0.7

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.
Files changed (47) hide show
  1. package/README.en.md +338 -286
  2. package/README.md +68 -16
  3. package/bin/deepseek.mjs +204 -4
  4. package/cordis.patch.yml +105 -7
  5. package/lib/index.mjs +2148 -439
  6. package/lib/session-query.mjs +149 -0
  7. package/lib/types/app.d.ts +34 -8
  8. package/lib/types/attachments.d.ts +36 -4
  9. package/lib/types/index.d.ts +38 -2
  10. package/lib/types/kernel-panels.d.ts +23 -0
  11. package/lib/types/provider-settings.d.ts +6 -11
  12. package/lib/types/render/animations.d.ts +74 -7
  13. package/lib/types/render/editor.d.ts +4 -3
  14. package/lib/types/render/export.d.ts +0 -6
  15. package/lib/types/render/fuzzy.d.ts +21 -0
  16. package/lib/types/render/ime-cursor.d.ts +60 -0
  17. package/lib/types/render/projection.d.ts +80 -4
  18. package/lib/types/render/status.d.ts +1 -1
  19. package/lib/types/session-directory.d.ts +48 -13
  20. package/lib/types/session-query.d.ts +92 -0
  21. package/lib/types/store.d.ts +3 -0
  22. package/lib/types/terminal-title.d.ts +58 -0
  23. package/lib/types/update-panel.d.ts +49 -0
  24. package/lib/types/update.d.ts +66 -0
  25. package/package.json +307 -162
  26. package/src/app.ts +730 -266
  27. package/src/attachments.ts +110 -11
  28. package/src/commands.ts +35 -5
  29. package/src/index.ts +1986 -1779
  30. package/src/internals.ts +66 -40
  31. package/src/kernel-panels.ts +89 -3
  32. package/src/provider-settings.ts +12 -12
  33. package/src/render/animations.ts +606 -403
  34. package/src/render/editor.ts +5 -4
  35. package/src/render/export.ts +13 -3
  36. package/src/render/fuzzy.ts +83 -0
  37. package/src/render/ime-cursor.ts +147 -0
  38. package/src/render/projection.ts +1974 -1621
  39. package/src/render/status.ts +18 -4
  40. package/src/session-directory.ts +94 -16
  41. package/src/session-query.ts +235 -0
  42. package/src/skills.ts +23 -9
  43. package/src/store.ts +39 -1
  44. package/src/subagents.ts +26 -3
  45. package/src/terminal-title.ts +173 -0
  46. package/src/update-panel.ts +246 -0
  47. package/src/update.ts +110 -0
package/src/internals.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  shouldEnableKeyboardEnhancement,
20
20
  } from './keyboard.ts'
21
21
  import { createSplitStdin } from './input-split.ts'
22
+ import { ensureVsCodeTabTitleSetting } from './terminal-title.ts'
22
23
 
23
24
  /** A mounted terminal app instance; the runner owns unmount ordering. */
24
25
  export interface TuiMount {
@@ -44,46 +45,71 @@ export const internals: {
44
45
  // the keyboard protocol on terminals that can safely own those key events.
45
46
  const keyboardEnhanced = shouldEnableKeyboardEnhancement()
46
47
  const focusReporting = isVsCodeTerminalEnv()
47
- process.stdout.write(
48
- (keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : '')
49
- + BRACKETED_PASTE_ENABLE
50
- + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ''),
51
- )
52
- // App owns Ctrl+C's deliberate three-state contract (interrupt, clear
53
- // draft, quit). Ink's default `exitOnCtrlC: true` would intercept the
54
- // normalized control byte first, unmount only its renderer, and leave the
55
- // Harness runner plus the pushed keyboard protocol alive.
56
- // stdin travels through the keypress splitter: Ink parses one chunk as
57
- // one keypress, so a coalesced space-then-enter would drop both keys.
58
- const tuiStdin = createSplitStdin(process.stdin)
59
- // Ink only touches isTTY/setRawMode/ref/read on stdin; the object-mode
60
- // proxy satisfies that contract without the full ReadStream surface.
61
- const instance = render(element, {
62
- exitOnCtrlC: false,
63
- stdin: tuiStdin.stdin as unknown as NodeJS.ReadStream,
64
- stdout: process.stdout,
65
- })
66
- return {
67
- rerender(element: ReactElement): void {
68
- instance.rerender(element)
69
- },
70
- unmount(): void {
71
- // The cleanup below must run even when Ink's unmount throws (a
72
- // render-teardown failure): a stdin tap or pushed terminal-protocol
73
- // stack outliving the app wedges the terminal for whatever runs
74
- // next, and a stray exception here must not skip the exit sequence.
75
- try {
76
- instance.unmount()
77
- } finally {
78
- tuiStdin.dispose()
79
- // Pop only a stack this mount pushed, then disable bracketed paste.
80
- process.stdout.write(
81
- (keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : '')
82
- + BRACKETED_PASTE_DISABLE
83
- + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ''),
84
- )
85
- }
86
- },
48
+ // Enter raw mode BEFORE pushing any protocol: xterm.js answers `?1004h`
49
+ // with an immediate focus report (ESC[I), and while the tty still carries
50
+ // the shell's cooked+ECHO settings that report is echoed to the screen as
51
+ // a literal `^[[I`. Ink only takes raw mode after its first commit, so
52
+ // this mount owns the window; the call is idempotent with Ink's later one.
53
+ if (process.stdin.isTTY === true) process.stdin.setRawMode?.(true)
54
+ try {
55
+ process.stdout.write(
56
+ (keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : '')
57
+ + BRACKETED_PASTE_ENABLE
58
+ + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ''),
59
+ )
60
+ // Cosmetic best effort: inside a VS Code integrated terminal the tab
61
+ // shows the process name ("node") unless the user settings map it to
62
+ // the sequence title; align them once. Total function, never throws.
63
+ ensureVsCodeTabTitleSetting()
64
+ // App owns Ctrl+C's deliberate three-state contract (interrupt, clear
65
+ // draft, quit). Ink's default `exitOnCtrlC: true` would intercept the
66
+ // normalized control byte first, unmount only its renderer, and leave the
67
+ // Harness runner plus the pushed keyboard protocol alive.
68
+ // stdin travels through the keypress splitter: Ink parses one chunk as
69
+ // one keypress, so a coalesced space-then-enter would drop both keys.
70
+ const tuiStdin = createSplitStdin(process.stdin)
71
+ // Ink only touches isTTY/setRawMode/ref/read on stdin; the object-mode
72
+ // proxy satisfies that contract without the full ReadStream surface.
73
+ const instance = render(element, {
74
+ exitOnCtrlC: false,
75
+ stdin: tuiStdin.stdin as unknown as NodeJS.ReadStream,
76
+ stdout: process.stdout,
77
+ })
78
+ return {
79
+ rerender(element: ReactElement): void {
80
+ instance.rerender(element)
81
+ },
82
+ unmount(): void {
83
+ // The cleanup below must run even when Ink's unmount throws (a
84
+ // render-teardown failure): a stdin tap or pushed terminal-protocol
85
+ // stack outliving the app wedges the terminal for whatever runs
86
+ // next, and a stray exception here must not skip the exit sequence.
87
+ // Pop the stack while raw mode still hides echo — xterm.js keeps
88
+ // reporting focus changes until `?1004l` lands, and one arriving
89
+ // after Ink restores the cooked tty would print as `^[[I`.
90
+ try {
91
+ process.stdout.write(
92
+ (keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : '')
93
+ + BRACKETED_PASTE_DISABLE
94
+ + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ''),
95
+ )
96
+ } finally {
97
+ try {
98
+ instance.unmount()
99
+ } finally {
100
+ tuiStdin.dispose()
101
+ // Belt and braces: give the tty back its cooked mode even when
102
+ // Ink never took raw mode over (idempotent at the termios level).
103
+ if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false)
104
+ }
105
+ }
106
+ },
107
+ }
108
+ } catch (error) {
109
+ // The synchronous mount path failed before Ink could own the terminal:
110
+ // undo the raw mode entered above so the shell keeps its echo.
111
+ if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false)
112
+ throw error
87
113
  }
88
114
  },
89
115
  stderr: process.stderr,
@@ -4,6 +4,7 @@ import { createElement, useEffect, useMemo, useRef, useState, type ReactElement
4
4
  import { Box, Text, useInput, useStdout } from 'ink'
5
5
  import type { ModelDirectory, ModelRow } from './models.ts'
6
6
  import type { SubagentRow } from './subagents.ts'
7
+ import type { ScheduleRow } from './render/projection.ts'
7
8
  import type { PermissionRow } from './permissions.ts'
8
9
  import type { PresetRow } from './presets.ts'
9
10
  import type { PluginRow } from './plugin-inventory.ts'
@@ -123,7 +124,9 @@ export function ModePanel({ current, load, select, close }: {
123
124
  const visible = useMemo(() => rows.filter(row => `${row.id} ${row.name ?? ''} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
124
125
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
125
126
  useInput((input, key) => {
126
- if (key.escape || input === 'q') return close()
127
+ if (key.escape) return close()
128
+ // q closes only while the query is empty; mid-filter it is query text.
129
+ if (input === 'q' && query === '') return close()
127
130
  if (input === 'r' && query === '') return refresh()
128
131
  if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
129
132
  if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
@@ -162,7 +165,9 @@ export function PermissionPanel({ current, load, select, close }: {
162
165
  const visible = useMemo(() => rows.filter(row => `${row.id} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
163
166
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
164
167
  useInput((input, key) => {
165
- if (key.escape || input === 'q') return close()
168
+ if (key.escape) return close()
169
+ // q closes only while the query is empty; mid-filter it is query text.
170
+ if (input === 'q' && query === '') return close()
166
171
  if (input === 'r' && query === '') return refresh()
167
172
  if (key.upArrow) return setCursor(value => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length)
168
173
  if (key.downArrow) return setCursor(value => visible.length === 0 ? 0 : (value + 1) % visible.length)
@@ -185,7 +190,9 @@ export function PluginPanel({ load, close, initialQuery = '' }: { load(): readon
185
190
  const rows = useMemo(() => load().filter(row => `${row.entryId} ${row.moduleName} ${row.phase ?? ''}`.toLowerCase().includes(query.toLowerCase())), [epoch, query])
186
191
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
187
192
  useInput((input, key) => {
188
- if (key.escape || input === 'q') return close()
193
+ if (key.escape) return close()
194
+ // q closes only while the query is empty; mid-filter it is query text.
195
+ if (input === 'q' && query === '') return close()
189
196
  if (input === 'r' && query === '') return setEpoch(value => value + 1)
190
197
  if (key.upArrow) return setCursor(value => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length)
191
198
  if (key.downArrow) return setCursor(value => rows.length === 0 ? 0 : (value + 1) % rows.length)
@@ -928,3 +935,82 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
928
935
  footer: '↑↓ choose · enter apply · r refresh · esc close',
929
936
  })
930
937
  }
938
+
939
+ /**
940
+ * The /schedule panel: the read-only catalog of active reminders folded from
941
+ * durable schedule/change events (the web ui-schedule contract: overdue
942
+ * first, then ascending target; the model creates and cancels through its
943
+ * schedule_* tools, the panel only shows state). A local second-hand keeps
944
+ * the relative labels live while the panel is open.
945
+ */
946
+ export interface ScheduleDisplayRow {
947
+ readonly key: string
948
+ readonly text: string
949
+ readonly tone?: 'error'
950
+ }
951
+
952
+ /** Human frequency label: one-shot kinds read as Once, every rows carry the interval. */
953
+ export function scheduleFrequency(row: ScheduleRow): string {
954
+ if (row.kind !== 'every') return 'Once'
955
+ const seconds = row.everySeconds ?? 0
956
+ if (seconds >= 3600 && seconds % 3600 === 0) return `Every ${seconds / 3600}h`
957
+ if (seconds >= 60 && seconds % 60 === 0) return `Every ${seconds / 60}m`
958
+ return `Every ${seconds}s`
959
+ }
960
+
961
+ /** Relative label for the next target: in N unit, or N unit overdue. */
962
+ export function scheduleRelative(targetAt: number, now: number): string {
963
+ const delta = Math.max(0, Math.abs(targetAt - now))
964
+ const minutes = Math.floor(delta / 60_000)
965
+ const unit = minutes === 0
966
+ ? '<1m'
967
+ : minutes >= 60
968
+ ? `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}`
969
+ : `${minutes}m`
970
+ return targetAt <= now ? `${unit} overdue` : `in ${unit}`
971
+ }
972
+
973
+ /** Ordered display rows: overdue first (error tone), then ascending target. */
974
+ export function scheduleDisplayRows(rows: readonly ScheduleRow[], now: number): readonly ScheduleDisplayRow[] {
975
+ return [...rows]
976
+ .sort((left, right) => (Number(left.targetAt > now) - Number(right.targetAt > now)) || (left.targetAt - right.targetAt))
977
+ .map(row => ({
978
+ key: row.id,
979
+ text: `${row.prompt} · ${scheduleFrequency(row)} · ${new Date(row.targetAt).toLocaleString()} (${scheduleRelative(row.targetAt, now)})`,
980
+ tone: row.targetAt <= now ? 'error' as const : undefined,
981
+ }))
982
+ }
983
+
984
+ export function SchedulePanel({ rows, close }: { rows(): readonly ScheduleRow[]; close(): void }): ReactElement {
985
+ const [, setTick] = useState(0)
986
+ useEffect(() => {
987
+ const id = setInterval(() => setTick(value => value + 1), 1_000)
988
+ return () => clearInterval(id)
989
+ }, [])
990
+ const display = scheduleDisplayRows(rows(), Date.now())
991
+ const stdout = useStdout().stdout
992
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
993
+ useInput((input, key) => {
994
+ if (key.escape || input === 'q') return close()
995
+ })
996
+ if (viewport.maxHeight === 0 || viewport.compact) {
997
+ const summary = display.length === 0 ? 'no active reminders' : singleLineText(display[0]!.text)
998
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(`/schedule · ${summary}`, viewport.contentColumns))
999
+ }
1000
+ const budget = Math.max(1, viewport.bodyRows)
1001
+ const visible = display.slice(0, budget)
1002
+ const hidden = display.length - visible.length
1003
+ return createElement(
1004
+ Box,
1005
+ { width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
1006
+ createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(`/schedule · ${display.length} active reminder${display.length === 1 ? '' : 's'}`, viewport.contentColumns)),
1007
+ ...(display.length === 0
1008
+ ? [createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(' no active reminders — the model creates them with schedule_create', viewport.contentColumns))]
1009
+ : visible.map(row => createElement(Text, {
1010
+ key: row.key,
1011
+ color: row.tone === 'error' ? inkColor(getPalette().error) : undefined,
1012
+ wrap: 'truncate-end',
1013
+ }, truncateColumns(` ${singleLineText(row.text)}`, viewport.contentColumns)))),
1014
+ createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(`esc/q close${hidden > 0 ? ` · +${hidden} more` : ''} · the model schedules via schedule_create`, viewport.contentColumns)),
1015
+ )
1016
+ }
@@ -33,6 +33,8 @@ interface LlmFace {
33
33
  readonly settingsNs: string
34
34
  readonly settingsPath: readonly string[]
35
35
  readonly declared?: boolean
36
+ /** Configuration diagnostic for repair; unaffected models may remain serviceable. */
37
+ readonly error?: string
36
38
  }[]
37
39
  /**
38
40
  * Registered endpoint model discovery; absent on an older service. The
@@ -256,18 +258,6 @@ export interface DiscoveredModelView {
256
258
  readonly maxTokens?: number
257
259
  }
258
260
 
259
- /** One model an endpoint reported about itself (mirrors LlmDiscoveredModel). */
260
- export interface DiscoveredModelView {
261
- /** Model id the endpoint accepts. */
262
- readonly id: string
263
- /** Human-readable name when the endpoint supplies one. */
264
- readonly name?: string
265
- /** Context window when disclosed; adoption still owes it if absent. */
266
- readonly contextWindow?: number
267
- /** Output cap when disclosed. */
268
- readonly maxTokens?: number
269
- }
270
-
271
261
  /**
272
262
  * The seven canonical reasoning levels a reasoningEfforts key may name -
273
263
  * pi-ai's THINKING_LEVELS. A pi-ai upgrade that adds or removes one fails
@@ -362,6 +352,12 @@ export interface ProviderTargetView {
362
352
  readonly configuration: ProviderConfiguration
363
353
  /** The owning adapter reports this route as hand-declared (absent when it draws no distinction). */
364
354
  readonly declared?: boolean
355
+ /**
356
+ * Configuration diagnostic the adapter reported for this route (catalog or
357
+ * profile damage): the row stays listed and repairable instead of the whole
358
+ * provider vanishing; absent when the route reads clean.
359
+ */
360
+ readonly diagnostic?: string
365
361
  }
366
362
 
367
363
  /** The resolved provider/settings/credential join. */
@@ -431,6 +427,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
431
427
  settingsNs: string
432
428
  settingsPath: readonly string[]
433
429
  declared?: boolean
430
+ error?: string
434
431
  }> = []
435
432
  if (llm.listConfigurableProviders !== undefined) {
436
433
  try {
@@ -462,6 +459,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
462
459
  settingsNs: string
463
460
  settingsPath: readonly string[]
464
461
  declared?: boolean
462
+ error?: string
465
463
  }> = [
466
464
  ...directoryEntries.map(entry => ({
467
465
  provider: entry.provider,
@@ -470,6 +468,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
470
468
  settingsNs: entry.settingsNs,
471
469
  settingsPath: entry.settingsPath,
472
470
  ...entry.declared === undefined ? {} : { declared: entry.declared },
471
+ ...entry.error === undefined ? {} : { error: singleLine(entry.error) },
473
472
  })),
474
473
  ...registered
475
474
  .filter(provider => !declared.has(provider.id))
@@ -508,6 +507,7 @@ export async function loadProviderSettings(ctx: Context): Promise<ProviderSettin
508
507
  ...credentialRef === undefined ? {} : { credentialRef },
509
508
  suggestedRef: deriveCredentialRef(base.provider),
510
509
  ...base.declared === undefined ? {} : { declared: base.declared },
510
+ ...base.error === undefined ? {} : { diagnostic: base.error },
511
511
  }
512
512
  })
513
513
  const refs = [...new Set(rows.flatMap(row => row.credentialRef === undefined ? [] : [row.credentialRef]))]