dsh-code 0.7.0 → 0.8.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.
- package/README.en.md +20 -6
- package/README.md +20 -6
- package/lib/index.mjs +2685 -622
- package/lib/types/app.d.ts +77 -1
- package/lib/types/history.d.ts +15 -4
- package/lib/types/index.d.ts +48 -0
- package/lib/types/kernel-panels.d.ts +7 -0
- package/lib/types/permissions.d.ts +37 -0
- package/lib/types/presets.d.ts +2 -0
- package/lib/types/provider-settings.d.ts +144 -0
- package/lib/types/questions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +8 -6
- package/lib/types/render/lines.d.ts +6 -0
- package/lib/types/render/markdown.d.ts +3 -3
- package/lib/types/render/projection.d.ts +95 -3
- package/lib/types/render/status.d.ts +26 -36
- package/lib/types/render/text.d.ts +14 -7
- package/lib/types/render/tool-detail.d.ts +3 -1
- package/lib/types/render/tool-preview.d.ts +4 -1
- package/lib/types/session-directory.d.ts +15 -0
- package/lib/types/store.d.ts +13 -2
- package/lib/types/version.d.ts +5 -0
- package/package.json +1 -1
- package/src/app.ts +847 -150
- package/src/approval.ts +11 -2
- package/src/history.ts +20 -5
- package/src/index.ts +402 -159
- package/src/kernel-panels.ts +45 -8
- package/src/permissions.ts +85 -0
- package/src/presets.ts +12 -0
- package/src/provider-settings.ts +520 -0
- package/src/questions.ts +15 -5
- package/src/render/animations.ts +32 -18
- package/src/render/lines.ts +21 -6
- package/src/render/markdown.ts +302 -4
- package/src/render/projection.ts +665 -10
- package/src/render/status.ts +68 -162
- package/src/render/text.ts +28 -9
- package/src/render/tool-detail.ts +81 -40
- package/src/render/tool-preview.ts +18 -2
- package/src/session-directory.ts +44 -5
- package/src/skills.ts +8 -4
- package/src/store.ts +26 -8
- package/src/version.ts +16 -0
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Bounded preview line for a tool invocation's raw JSON arguments: the first
|
|
3
3
|
* human-meaningful string among the well-known keys (command, path, query, …)
|
|
4
4
|
* with a fallback to the bounded raw JSON. Shared by the tool card in the
|
|
5
|
-
* transcript and the approval bar's command preview.
|
|
5
|
+
* transcript and the approval bar's command preview. Arguments longer than
|
|
6
|
+
* {@link MAX_PARSE_CHARS} are never parsed: the preview is a display concern,
|
|
7
|
+
* and a synchronous `JSON.parse` plus string copies of an unbounded model
|
|
8
|
+
* payload must not run on the approval or projection paths.
|
|
6
9
|
*
|
|
7
10
|
* @module @deepseek-ai/dsh-code/render/tool-preview
|
|
8
11
|
*/
|
|
@@ -10,6 +13,18 @@
|
|
|
10
13
|
/** Keys searched in declaration order when building a preview. */
|
|
11
14
|
const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query'] as const
|
|
12
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Raw arguments longer than this are skipped without parsing and fall back
|
|
18
|
+
* to the bounded raw preview. Well above any realistic command/path/query
|
|
19
|
+
* string while keeping the synchronous parse cost negligible.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_PARSE_CHARS = 4096
|
|
22
|
+
|
|
23
|
+
/** Bounded raw-arguments fallback shared by the skip-parse and parse-failure paths. */
|
|
24
|
+
function boundedRawPreview(args: string): string {
|
|
25
|
+
return args.length > 80 ? `${args.slice(0, 77)}...` : args
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
/**
|
|
14
29
|
* Resolve one bounded preview for raw tool arguments.
|
|
15
30
|
* @param args - raw JSON arguments string as the model produced it.
|
|
@@ -18,6 +33,7 @@ const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query
|
|
|
18
33
|
*/
|
|
19
34
|
export function toolArgumentsPreview(args: string, toolName: string): string {
|
|
20
35
|
if (args === '') return toolName
|
|
36
|
+
if (args.length > MAX_PARSE_CHARS) return boundedRawPreview(args)
|
|
21
37
|
try {
|
|
22
38
|
const parsed: unknown = JSON.parse(args)
|
|
23
39
|
if (parsed !== null && typeof parsed === 'object') {
|
|
@@ -30,5 +46,5 @@ export function toolArgumentsPreview(args: string, toolName: string): string {
|
|
|
30
46
|
} catch {
|
|
31
47
|
// Raw JSON parse failed: fall through to the bounded raw arguments.
|
|
32
48
|
}
|
|
33
|
-
return args
|
|
49
|
+
return boundedRawPreview(args)
|
|
34
50
|
}
|
package/src/session-directory.ts
CHANGED
|
@@ -53,21 +53,60 @@ export interface SessionRow {
|
|
|
53
53
|
readonly title?: string
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
|
|
57
|
+
const CASE_INSENSITIVE_FS = process.platform === 'win32' || process.platform === 'darwin'
|
|
58
|
+
|
|
59
|
+
/** True when the header describes a subagent conversation (durable lineage). */
|
|
60
|
+
export function isSubagentSession(header: SessionHeader): boolean {
|
|
61
|
+
return header.origin === 'subagent' || header.parentSession !== undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function comparablePath(value: string): string {
|
|
65
|
+
const resolved = resolve(value)
|
|
66
|
+
return CASE_INSENSITIVE_FS ? resolved.toLowerCase() : resolved
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Platform-consistent path equality for session cwd comparisons. */
|
|
70
|
+
export function samePath(left: string | undefined, right: string): boolean {
|
|
57
71
|
if (left === undefined) return false
|
|
58
|
-
return
|
|
72
|
+
return comparablePath(left) === comparablePath(right)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Unique header match by exact id or unique id prefix (root and subagent
|
|
77
|
+
* headers alike); the caller applies any lineage gate.
|
|
78
|
+
* @param headers - the persisted headers.
|
|
79
|
+
* @param wanted - the id or id prefix.
|
|
80
|
+
* @returns the uniquely matched header.
|
|
81
|
+
* @throws when nothing matches or the prefix is ambiguous.
|
|
82
|
+
*/
|
|
83
|
+
export function matchSessionId(headers: readonly SessionHeader[], wanted: string): SessionHeader {
|
|
84
|
+
const exact = headers.filter(header => header.id === wanted)
|
|
85
|
+
const matches = exact.length > 0 ? exact : headers.filter(header => header.id.startsWith(wanted))
|
|
86
|
+
if (matches.length === 0) throw new Error(`no persisted session matches "${wanted}"`)
|
|
87
|
+
if (matches.length > 1) {
|
|
88
|
+
throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`)
|
|
89
|
+
}
|
|
90
|
+
return matches[0]!
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The newest persisted ROOT session pinned to this cwd, or undefined. */
|
|
94
|
+
export function newestRootForCwd(headers: readonly SessionHeader[], cwd: string): SessionHeader | undefined {
|
|
95
|
+
const local = headers
|
|
96
|
+
.filter(header => !isSubagentSession(header) && samePath(header.cwd, cwd))
|
|
97
|
+
.sort((left, right) => right.createdAt - left.createdAt)
|
|
98
|
+
return local[0]
|
|
59
99
|
}
|
|
60
100
|
|
|
61
101
|
/** Filter/sort header-only records. No session log is loaded here. */
|
|
62
102
|
export function projectSessionRows(records: readonly SessionRecord[], options: SessionDirectoryOptions): SessionRow[] {
|
|
63
103
|
const needle = options.query.trim().toLowerCase()
|
|
64
104
|
return records
|
|
65
|
-
.filter(record => options.sessions === 'all'
|
|
66
|
-
|| (record.header.parentSession === undefined && record.header.origin !== 'subagent'))
|
|
105
|
+
.filter(record => options.sessions === 'all' || !isSubagentSession(record.header))
|
|
67
106
|
.filter(record => options.cwd === 'all' || samePath(record.header.cwd, options.currentCwd))
|
|
68
107
|
.map(record => {
|
|
69
108
|
const cwd = record.header.cwd ?? ''
|
|
70
|
-
const subagent = record.header
|
|
109
|
+
const subagent = isSubagentSession(record.header)
|
|
71
110
|
return {
|
|
72
111
|
id: record.header.id,
|
|
73
112
|
createdAt: record.header.createdAt,
|
package/src/skills.ts
CHANGED
|
@@ -69,12 +69,15 @@ export function watchSkills(ctx: Context): SkillsWatch {
|
|
|
69
69
|
const listeners = new Set<() => void>()
|
|
70
70
|
|
|
71
71
|
const reload = (): void => {
|
|
72
|
-
const
|
|
73
|
-
if (skills === undefined ||
|
|
72
|
+
const target = agent
|
|
73
|
+
if (skills === undefined || target === undefined) return
|
|
74
74
|
Promise.resolve().then(() => skills.list({
|
|
75
|
-
cwd:
|
|
76
|
-
scope:
|
|
75
|
+
cwd: target.session.header.cwd,
|
|
76
|
+
scope: target,
|
|
77
77
|
})).then((summaries: readonly SkillSummary[]) => {
|
|
78
|
+
// A retarget landed while this catalog was loading: the rows belong to
|
|
79
|
+
// another agent's workspace and must never overwrite the current view.
|
|
80
|
+
if (agent !== target) return
|
|
78
81
|
const next = toRows(summaries)
|
|
79
82
|
const unchanged = next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name)
|
|
80
83
|
rows = next
|
|
@@ -83,6 +86,7 @@ export function watchSkills(ctx: Context): SkillsWatch {
|
|
|
83
86
|
if (unchanged && !recovered) return
|
|
84
87
|
for (const listener of listeners) listener()
|
|
85
88
|
}).catch((cause: unknown) => {
|
|
89
|
+
if (agent !== target) return
|
|
86
90
|
// Discovery failure keeps the last good rows; the next skills/change
|
|
87
91
|
// notification is the retry surface (mirrors the web directory).
|
|
88
92
|
rows = [...rows]
|
package/src/store.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Observable transcript store: folds session events into the projection view
|
|
3
3
|
* and notifies subscribers. The renderer subscribes through
|
|
4
|
-
* `useSyncExternalStore`; the runner owns event feeding.
|
|
5
|
-
*
|
|
4
|
+
* `useSyncExternalStore`; the runner owns event feeding.
|
|
5
|
+
*
|
|
6
|
+
* Notification coalescing: the fold stays synchronous — `getView()` always
|
|
7
|
+
* returns the latest state the moment `apply` returns — but listener
|
|
8
|
+
* notification is scheduled on a microtask and deduplicated, so N events
|
|
9
|
+
* delivered inside one synchronous drain (the zai/GLM adapter drains its
|
|
10
|
+
* token buffer in sub-millisecond bursts) produce ONE React re-render.
|
|
11
|
+
* Synchronous per-event notification instead cascades one
|
|
12
|
+
* `useSyncExternalStore` force-update per token inside a single flush; the
|
|
13
|
+
* reconciler counts those as nested passive updates and floods React's
|
|
14
|
+
* "Maximum update depth exceeded" warning past 50 events, besides rendering
|
|
15
|
+
* the whole live tree once per token. A microtask keeps latency within the
|
|
16
|
+
* same macrotask, before Ink's throttled paint.
|
|
6
17
|
*
|
|
7
18
|
* @module @deepseek-ai/dsh-tui/store
|
|
8
19
|
*/
|
|
@@ -35,6 +46,17 @@ export interface TranscriptStore {
|
|
|
35
46
|
export function createTranscriptStore(replay?: readonly SessionEvent[]): TranscriptStore {
|
|
36
47
|
let view = replay === undefined ? createTranscriptView() : projectEvents(replay)
|
|
37
48
|
const listeners = new Set<() => void>()
|
|
49
|
+
let scheduled = false
|
|
50
|
+
const notify = (): void => {
|
|
51
|
+
if (scheduled) return
|
|
52
|
+
scheduled = true
|
|
53
|
+
queueMicrotask(() => {
|
|
54
|
+
scheduled = false
|
|
55
|
+
for (const listener of listeners) {
|
|
56
|
+
listener()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
38
60
|
return {
|
|
39
61
|
getView: () => view,
|
|
40
62
|
subscribe(listener: () => void): () => void {
|
|
@@ -47,15 +69,11 @@ export function createTranscriptStore(replay?: readonly SessionEvent[]): Transcr
|
|
|
47
69
|
const next = projectEvent(view, event)
|
|
48
70
|
if (next === view) return
|
|
49
71
|
view = next
|
|
50
|
-
|
|
51
|
-
listener()
|
|
52
|
-
}
|
|
72
|
+
notify()
|
|
53
73
|
},
|
|
54
74
|
reset(): void {
|
|
55
75
|
view = createTranscriptView()
|
|
56
|
-
|
|
57
|
-
listener()
|
|
58
|
-
}
|
|
76
|
+
notify()
|
|
59
77
|
},
|
|
60
78
|
}
|
|
61
79
|
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Installed dsh-code version exposed by the terminal header. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
/** Read one package manifest version without making terminal startup depend on it. */
|
|
6
|
+
export function readPackageVersion(manifest = new URL('../package.json', import.meta.url)): string {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(readFileSync(manifest, 'utf8')) as { version?: unknown }
|
|
9
|
+
return typeof parsed.version === 'string' && parsed.version.length > 0 ? parsed.version : '0.0.0'
|
|
10
|
+
} catch {
|
|
11
|
+
return '0.0.0'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Version of the installed dsh-code package. */
|
|
16
|
+
export const DSH_CODE_VERSION = readPackageVersion()
|