dsh-code 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.en.md +10 -8
  2. package/README.md +10 -8
  3. package/lib/index.mjs +6980 -5942
  4. package/lib/session-query.mjs +18 -1
  5. package/lib/types/app.d.ts +38 -65
  6. package/lib/types/completion.d.ts +29 -0
  7. package/lib/types/composer.d.ts +150 -0
  8. package/lib/types/git-workflow.d.ts +6 -0
  9. package/lib/types/index.d.ts +6 -188
  10. package/lib/types/locales/en.d.ts +20 -8
  11. package/lib/types/{authorization-panel.d.ts → panels/authorization-panel.d.ts} +6 -1
  12. package/lib/types/panels/completion-panel.d.ts +13 -0
  13. package/lib/types/panels/interaction-bars.d.ts +38 -0
  14. package/lib/types/{kernel-panels.d.ts → panels/kernel-panels.d.ts} +16 -37
  15. package/lib/types/{language-panel.d.ts → panels/language-panel.d.ts} +1 -1
  16. package/lib/types/panels/model-panels.d.ts +86 -0
  17. package/lib/types/{theme-panel.d.ts → panels/theme-panel.d.ts} +1 -1
  18. package/lib/types/{update-panel.d.ts → panels/update-panel.d.ts} +1 -1
  19. package/lib/types/provider-settings.d.ts +11 -0
  20. package/lib/types/render/inspector.d.ts +16 -0
  21. package/lib/types/render/projection-events.d.ts +33 -0
  22. package/lib/types/render/text.d.ts +4 -0
  23. package/lib/types/render/transcript-viewport.d.ts +32 -0
  24. package/lib/types/runner/harness-gate.d.ts +83 -0
  25. package/lib/types/runner/input-history.d.ts +31 -0
  26. package/lib/types/runner/mode-cycle.d.ts +44 -0
  27. package/lib/types/runner/preferences.d.ts +40 -0
  28. package/lib/types/runner/quit.d.ts +27 -0
  29. package/lib/types/runner/search-rows.d.ts +38 -0
  30. package/lib/types/runner/session-io.d.ts +57 -0
  31. package/lib/types/runner/session-target.d.ts +42 -0
  32. package/lib/types/runner/startup-config.d.ts +33 -0
  33. package/lib/types/runner/submissions.d.ts +87 -0
  34. package/lib/types/session/attach.d.ts +39 -0
  35. package/lib/types/{store.d.ts → session/store.d.ts} +1 -1
  36. package/lib/types/{subagents.d.ts → session/subagents.d.ts} +8 -1
  37. package/lib/types/session-query.d.ts +11 -0
  38. package/lib/types/settings-file.d.ts +10 -0
  39. package/lib/types/{panel-accent.d.ts → ui/panel-accent.d.ts} +1 -1
  40. package/lib/types/ui/panel-gap.d.ts +6 -0
  41. package/lib/types/ui/query-editor.d.ts +10 -0
  42. package/lib/types/ui/styled-rows.d.ts +8 -0
  43. package/lib/types/{terminal-title.d.ts → ui/terminal-title.d.ts} +1 -1
  44. package/lib/types/ui/ui-contract.d.ts +12 -0
  45. package/lib/types/ui/use-frames.d.ts +6 -0
  46. package/lib/types/ui/use-stable-input.d.ts +7 -0
  47. package/package.json +3 -2
  48. package/src/app.ts +802 -4162
  49. package/src/completion.ts +117 -0
  50. package/src/composer.ts +1956 -0
  51. package/src/git-workflow.ts +18 -0
  52. package/src/index.ts +113 -633
  53. package/src/internals.ts +1 -1
  54. package/src/locales/en.ts +20 -8
  55. package/src/locales/zh.ts +20 -8
  56. package/src/{authorization-panel.ts → panels/authorization-panel.ts} +25 -6
  57. package/src/panels/completion-panel.ts +79 -0
  58. package/src/panels/interaction-bars.ts +567 -0
  59. package/src/{kernel-panels.ts → panels/kernel-panels.ts} +84 -65
  60. package/src/{language-panel.ts → panels/language-panel.ts} +4 -4
  61. package/src/panels/model-panels.ts +1021 -0
  62. package/src/{theme-panel.ts → panels/theme-panel.ts} +5 -5
  63. package/src/{update-panel.ts → panels/update-panel.ts} +6 -6
  64. package/src/provider-settings.ts +38 -0
  65. package/src/render/inspector.ts +35 -0
  66. package/src/render/lines.ts +45 -18
  67. package/src/render/projection-events.ts +109 -0
  68. package/src/render/projection.ts +11 -2
  69. package/src/render/text.ts +8 -0
  70. package/src/render/tool-preview.ts +35 -2
  71. package/src/render/transcript-viewport.ts +61 -0
  72. package/src/render/usage.ts +1 -2
  73. package/src/runner/harness-gate.ts +168 -0
  74. package/src/runner/input-history.ts +77 -0
  75. package/src/runner/mode-cycle.ts +49 -0
  76. package/src/runner/preferences.ts +67 -0
  77. package/src/runner/quit.ts +53 -0
  78. package/src/runner/search-rows.ts +55 -0
  79. package/src/runner/session-io.ts +245 -0
  80. package/src/runner/session-target.ts +81 -0
  81. package/src/runner/startup-config.ts +54 -0
  82. package/src/runner/submissions.ts +157 -0
  83. package/src/session/attach.ts +87 -0
  84. package/src/{session-directory.ts → session/session-directory.ts} +1 -1
  85. package/src/{store.ts → session/store.ts} +1 -1
  86. package/src/{subagents.ts → session/subagents.ts} +12 -1
  87. package/src/session-query.ts +22 -0
  88. package/src/settings-file.ts +19 -1
  89. package/src/{panel-accent.ts → ui/panel-accent.ts} +1 -1
  90. package/src/ui/panel-gap.ts +9 -0
  91. package/src/ui/query-editor.ts +16 -0
  92. package/src/ui/styled-rows.ts +124 -0
  93. package/src/{terminal-title.ts → ui/terminal-title.ts} +1 -1
  94. package/src/ui/ui-contract.ts +10 -0
  95. package/src/ui/use-frames.ts +23 -0
  96. package/src/ui/use-stable-input.ts +17 -0
  97. /package/lib/types/{fork.d.ts → session/fork.d.ts} +0 -0
  98. /package/lib/types/{history.d.ts → session/history.d.ts} +0 -0
  99. /package/lib/types/{session-directory.d.ts → session/session-directory.d.ts} +0 -0
  100. /package/lib/types/{session-switch.d.ts → session/session-switch.d.ts} +0 -0
  101. /package/src/{fork.ts → session/fork.ts} +0 -0
  102. /package/src/{history.ts → session/history.ts} +0 -0
  103. /package/src/{session-switch.ts → session/session-switch.ts} +0 -0
@@ -224,6 +224,28 @@ export async function observeStableWithSkip(
224
224
  throw new SessionQueryError('session-search persistence observation did not stabilize after one retry', 'SESSION_QUERY_PERSISTENCE_FAILED')
225
225
  }
226
226
 
227
+ /**
228
+ * Assert the private override seam this compatibility engine relies on.
229
+ * Harness keeps `_observeStable` private, so TypeScript cannot protect this
230
+ * subclass when upstream renames the method or changes its call signature.
231
+ * The exact-version launcher gate prevents unsupported hosts; this import-time
232
+ * check additionally prevents a newly built bundle from silently mounting an
233
+ * override the installed base will never call.
234
+ *
235
+ * @internal Exported for the compatibility regression.
236
+ */
237
+ export function assertSessionQueryOverrideContract(candidate: unknown): void {
238
+ if (typeof candidate !== 'function') throw new TypeError('session-query sqlite engine must be a constructor')
239
+ const prototype = (candidate as { prototype?: object }).prototype
240
+ const descriptor = prototype === undefined ? undefined : Object.getOwnPropertyDescriptor(prototype, '_observeStable')
241
+ const method: unknown = descriptor?.value
242
+ if (typeof method !== 'function' || method.length !== 2) {
243
+ throw new Error('incompatible @deepseek-ai/dsh-session-query-sqlite: expected _observeStable(indexed, signal)')
244
+ }
245
+ }
246
+
247
+ assertSessionQueryOverrideContract(SqliteSessionQueryEngine)
248
+
227
249
  // The opaque-base cast keeps the private `_observeStable` override legal in
228
250
  // TypeScript while inheriting every runtime static (inject, Config, Service
229
251
  // metadata) from the real engine class.
@@ -17,8 +17,9 @@
17
17
  */
18
18
 
19
19
  import { randomUUID } from 'node:crypto'
20
+ import { readFileSync } from 'node:fs'
20
21
  import { mkdir, rename, writeFile } from 'node:fs/promises'
21
- import { dirname } from 'node:path'
22
+ import { basename, dirname } from 'node:path'
22
23
 
23
24
  /**
24
25
  * Run one file operation with a bounded retry: one initial try plus at
@@ -86,3 +87,20 @@ export function createUserSettingsPersistence(): UserSettingsPersistence {
86
87
  },
87
88
  }
88
89
  }
90
+
91
+ /**
92
+ * Read one user-level settings file as a plain object. The callers all treat a
93
+ * missing file as "unset" and a corrupt one as "warn and fall back", so this
94
+ * helper owns the one distinction they share: readable JSON that is not an
95
+ * object is corruption, not an absent preference, and must not surface as a
96
+ * cryptic property access on `null`.
97
+ * @param path - absolute path of the settings file.
98
+ * @returns the parsed object; the caller narrows each field itself.
99
+ */
100
+ export function readSettingsObject(path: string): Record<string, unknown> {
101
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
102
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
103
+ throw new Error(`${basename(path)} must contain a JSON object`)
104
+ }
105
+ return parsed as Record<string, unknown>
106
+ }
@@ -11,7 +11,7 @@
11
11
  * @module @deepseek-ai/dsh-tui/panel-accent
12
12
  */
13
13
 
14
- import { surfaceAccent, type RgbTriple } from './theme.ts'
14
+ import { surfaceAccent, type RgbTriple } from '../theme.ts'
15
15
 
16
16
  /** First-seen slot per panel id; stable for the process lifetime. */
17
17
  const panelSlots = new Map<string, number>()
@@ -0,0 +1,9 @@
1
+ /** Shared optional spacer row used by bounded panel layouts. */
2
+
3
+ import { createElement, type ReactElement } from 'react'
4
+ import { Text } from 'ink'
5
+
6
+ /** Codex-style panel rhythm that still participates in the row budget. */
7
+ export function PanelGap({ visible }: { visible: boolean }): ReactElement | undefined {
8
+ return visible ? createElement(Text, null, ' ') : undefined
9
+ }
@@ -0,0 +1,16 @@
1
+ /** Shared one-line query editor for keyboard-owned filtering panels. */
2
+
3
+ import { stripPasteMarkers } from '../keyboard.ts'
4
+ import { deleteLastGrapheme } from '../render/editor.ts'
5
+
6
+ /**
7
+ * Apply one keystroke to a panel search query. IME commits arrive as one
8
+ * multi-character chunk, so the whole printable run is appended; paste
9
+ * markers are stripped and control-laden chunks are ignored.
10
+ */
11
+ export function editQuery(query: string, input: string, key: { backspace?: boolean; delete?: boolean }): string | undefined {
12
+ if (key.backspace || key.delete) return deleteLastGrapheme(query)
13
+ const text = stripPasteMarkers(input)
14
+ if (text !== '' && !/[\u0000-\u001f\u007f]/u.test(text)) return query + text
15
+ return undefined
16
+ }
@@ -0,0 +1,124 @@
1
+ /** Ink renderer for width-safe styled terminal rows. */
2
+
3
+ import { createElement, type ReactElement } from 'react'
4
+ import { Box, Text } from 'ink'
5
+ import type { MdSegment } from '../render/markdown.ts'
6
+ import type { LineStyle, StyledLine } from '../render/lines.ts'
7
+ import {
8
+ diffBackground,
9
+ getPalette,
10
+ inkColor,
11
+ promptRowTokens,
12
+ rowBackground,
13
+ } from '../theme.ts'
14
+
15
+ function segmentProps(style: MdSegment['style']): {
16
+ color: string | undefined
17
+ bold: boolean | undefined
18
+ italic: boolean | undefined
19
+ strikethrough: boolean | undefined
20
+ } {
21
+ switch (style) {
22
+ case 'accent':
23
+ return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined }
24
+ case 'accentBold':
25
+ return { color: inkColor(getPalette().brandBright), bold: true, italic: undefined, strikethrough: undefined }
26
+ case 'code':
27
+ return { color: inkColor(getPalette().code), bold: undefined, italic: undefined, strikethrough: undefined }
28
+ case 'dim':
29
+ return { color: inkColor(getPalette().dim), bold: undefined, italic: undefined, strikethrough: undefined }
30
+ case 'bold':
31
+ return { color: undefined, bold: true, italic: undefined, strikethrough: undefined }
32
+ case 'italic':
33
+ return { color: undefined, bold: undefined, italic: true, strikethrough: undefined }
34
+ case 'boldItalic':
35
+ return { color: undefined, bold: true, italic: true, strikethrough: undefined }
36
+ case 'strike':
37
+ return { color: inkColor(getPalette().dim), bold: undefined, italic: undefined, strikethrough: true }
38
+ case 'diffAdd':
39
+ case 'diffDel':
40
+ // Inline-markdown twin of lineStyleProps' diff cases: tinted rows for
41
+ // ```diff fences rendered through the markdown span path. The diff
42
+ // foreground tokens stay AA-legible both on the row tints (when the
43
+ // background rides along) and on the plain terminal background.
44
+ return {
45
+ color: inkColor(style === 'diffAdd' ? getPalette().diffAddFg : getPalette().diffDelFg),
46
+ bold: undefined,
47
+ italic: undefined,
48
+ strikethrough: undefined,
49
+ }
50
+ default:
51
+ return { color: undefined, bold: undefined, italic: undefined, strikethrough: undefined }
52
+ }
53
+ }
54
+
55
+ /** Ink props for the richer line model used by bounded scrolling panels. */
56
+ function lineStyleProps(style: LineStyle): {
57
+ color: string | undefined
58
+ bold: boolean | undefined
59
+ italic: boolean | undefined
60
+ strikethrough: boolean | undefined
61
+ dimColor: boolean | undefined
62
+ backgroundColor: string | undefined
63
+ } {
64
+ switch (style) {
65
+ case 'brand':
66
+ return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
67
+ case 'success':
68
+ return { color: inkColor(getPalette().success), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
69
+ case 'error':
70
+ return { color: inkColor(getPalette().error), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
71
+ case 'warn':
72
+ return { color: inkColor(getPalette().warn), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
73
+ case 'dimItalic':
74
+ return { color: inkColor(getPalette().dim), bold: undefined, italic: true, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
75
+ // Codex diff rendering: added/removed lines carry a theme tint behind
76
+ // the sign and text, with the AA-tuned diff foreground tokens on top; the
77
+ // depth gate turns this into plain foreground styling on 16-color
78
+ // terminals.
79
+ case 'diffAdd':
80
+ return { color: inkColor(getPalette().diffAddFg), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: diffBackground('diffAdd') }
81
+ case 'diffDel':
82
+ return { color: inkColor(getPalette().diffDelFg), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: diffBackground('diffDel') }
83
+ // Prompt rows: one full-width bar per delivery kind. The tint comes from
84
+ // the row token and the foreground from its AA-tuned twin; the depth gate
85
+ // inside rowBackground degrades this to foreground-only on 16-color
86
+ // terminals, matching the diff rows.
87
+ case 'promptRow':
88
+ case 'promptQueuedRow':
89
+ case 'promptSteeredRow': {
90
+ const tokens = promptRowTokens(style === 'promptQueuedRow' ? 'queued' : style === 'promptSteeredRow' ? 'steered' : undefined)
91
+ return {
92
+ color: inkColor(getPalette()[tokens.fg]),
93
+ bold: undefined,
94
+ italic: undefined,
95
+ strikethrough: undefined,
96
+ dimColor: undefined,
97
+ backgroundColor: rowBackground(tokens.fg),
98
+ }
99
+ }
100
+ default:
101
+ return { ...segmentProps(style), dimColor: undefined, backgroundColor: undefined }
102
+ }
103
+ }
104
+
105
+ /** Render width-safe rows; every child is exactly one terminal row. */
106
+ export function StyledRows({ lines }: { lines: readonly StyledLine[] }): ReactElement {
107
+ return createElement(
108
+ Box,
109
+ { flexDirection: 'column' },
110
+ ...lines.map((line, index) => createElement(
111
+ Text,
112
+ { key: index, wrap: 'truncate-end' },
113
+ line.segments.length === 0
114
+ ? ' '
115
+ : line.segments.map((segment, at) => createElement(
116
+ Text,
117
+ { key: at, ...lineStyleProps(segment.style) },
118
+ segment.text,
119
+ )),
120
+ )),
121
+ )
122
+ }
123
+
124
+ /** File-oriented, color-coded unified diff viewport. */
@@ -10,7 +10,7 @@
10
10
  * an empty OSC payload and the terminal falls back to its own default; the
11
11
  * previously set title is not portable to read back and is never restored.
12
12
  *
13
- * @module @deepseek-ai/dsh-code/terminal-title
13
+ * @module @deepseek-ai/dsh-code/ui/terminal-title
14
14
  */
15
15
 
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
@@ -0,0 +1,10 @@
1
+ /** Shared terminal UI contracts used by the runner, App, and composer. */
2
+
3
+ /** Visual priority for one bounded local notice. */
4
+ export type NoticeTone = 'info' | 'warning' | 'error'
5
+
6
+ /** One mutation the terminal may request for a pending next-turn inbox item. */
7
+ export type QueueMutation =
8
+ | { readonly kind: 'remove' }
9
+ | { readonly kind: 'edit'; readonly text: string }
10
+ | { readonly kind: 'steer' }
@@ -0,0 +1,23 @@
1
+ /** Wall-clock animation frame hook shared by independent terminal leaves. */
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ /**
6
+ * Derive ticks from elapsed wall time so a stretched interval skips ahead
7
+ * instead of slowing the animation under a busy event loop or remote shell.
8
+ */
9
+ export function useFrames(intervalMs: number, active = true): number {
10
+ const [tick, setTick] = useState(0)
11
+ useEffect(() => {
12
+ if (!active) return
13
+ const startedAt = Date.now()
14
+ setTick(0)
15
+ const id = setInterval(() => {
16
+ setTick(Math.max(0, Math.floor((Date.now() - startedAt) / intervalMs)))
17
+ }, intervalMs)
18
+ return () => {
19
+ clearInterval(id)
20
+ }
21
+ }, [active, intervalMs])
22
+ return tick
23
+ }
@@ -0,0 +1,17 @@
1
+ /** Stable Ink input subscription for stateful keyboard-owned surfaces. */
2
+
3
+ import { useCallback, useRef } from 'react'
4
+ import { useInput, type Key } from 'ink'
5
+
6
+ /**
7
+ * Ink re-subscribes its input effect whenever the handler identity changes.
8
+ * Keep ownership stable while a surface updates cursor, scroll, or draft state.
9
+ */
10
+ export function useStableInput(handler: (input: string, key: Key) => void, active: boolean): void {
11
+ const handlerRef = useRef(handler)
12
+ handlerRef.current = handler
13
+ const stableHandler = useCallback((input: string, key: Key): void => {
14
+ handlerRef.current(input, key)
15
+ }, [])
16
+ useInput(stableHandler, { isActive: active })
17
+ }
File without changes
File without changes
File without changes