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.
- package/README.en.md +338 -286
- package/README.md +68 -16
- package/bin/deepseek.mjs +204 -4
- package/cordis.patch.yml +105 -7
- package/lib/index.mjs +2148 -439
- package/lib/session-query.mjs +149 -0
- package/lib/types/app.d.ts +34 -8
- package/lib/types/attachments.d.ts +36 -4
- package/lib/types/index.d.ts +38 -2
- package/lib/types/kernel-panels.d.ts +23 -0
- package/lib/types/provider-settings.d.ts +6 -11
- package/lib/types/render/animations.d.ts +74 -7
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/export.d.ts +0 -6
- package/lib/types/render/fuzzy.d.ts +21 -0
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/projection.d.ts +80 -4
- package/lib/types/render/status.d.ts +1 -1
- package/lib/types/session-directory.d.ts +48 -13
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/store.d.ts +3 -0
- package/lib/types/terminal-title.d.ts +58 -0
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +66 -0
- package/package.json +307 -162
- package/src/app.ts +730 -266
- package/src/attachments.ts +110 -11
- package/src/commands.ts +35 -5
- package/src/index.ts +1986 -1779
- package/src/internals.ts +66 -40
- package/src/kernel-panels.ts +89 -3
- package/src/provider-settings.ts +12 -12
- package/src/render/animations.ts +606 -403
- package/src/render/editor.ts +5 -4
- package/src/render/export.ts +13 -3
- package/src/render/fuzzy.ts +83 -0
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/projection.ts +1974 -1621
- package/src/render/status.ts +18 -4
- package/src/session-directory.ts +94 -16
- package/src/session-query.ts +235 -0
- package/src/skills.ts +23 -9
- package/src/store.ts +39 -1
- package/src/subagents.ts +26 -3
- package/src/terminal-title.ts +173 -0
- package/src/update-panel.ts +246 -0
- 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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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,
|
package/src/kernel-panels.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/provider-settings.ts
CHANGED
|
@@ -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]))]
|