@toddzheng024/dscode-bundle 0.7.12 → 0.7.13
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/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +1 -1
- package/package.json +5 -4
- package/plugins/session-metrics/view.mjs +10 -2
- package/vendor/tui/lib/app.mjs +6674 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +64 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2198 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1862 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44441
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-invocable skill watch for the `/` completion menu: the in-process
|
|
3
|
+
* equivalent of the web ui-skill trigger source. Skills are NOT commands —
|
|
4
|
+
* picking one lands the literal `/name ` text in the input, and submitting
|
|
5
|
+
* it as a normal prompt lets the host's tool-skill pre-step inject the body
|
|
6
|
+
* (the only entry point for model-disabled skills). Command descriptors win
|
|
7
|
+
* on a name collision; see the runner's dispatch.
|
|
8
|
+
*
|
|
9
|
+
* @module @deepseek-ai/dsh-code/skills
|
|
10
|
+
*/
|
|
11
|
+
import { isUserInvocable } from '@deepseek-ai/dsh-skill';
|
|
12
|
+
function toRows(skills) {
|
|
13
|
+
return skills
|
|
14
|
+
.filter(skill => isUserInvocable(skill))
|
|
15
|
+
.map(skill => ({
|
|
16
|
+
name: skill.name,
|
|
17
|
+
description: skill.description,
|
|
18
|
+
modelInvocable: skill.invocation.modelInvocable === true,
|
|
19
|
+
}))
|
|
20
|
+
.sort((left, right) => left.name < right.name ? -1 : 1);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Watch the user-invocable skill catalog for one agent's workspace. The first
|
|
24
|
+
* load starts when the owning agent is known (`setAgent`); `skills/change`
|
|
25
|
+
* and agent retargets re-read. Read failures keep the last good rows (the
|
|
26
|
+
* next change notification is the retry surface) — a missing `skills`
|
|
27
|
+
* service leaves the view permanently empty.
|
|
28
|
+
* @param ctx - context carrying the `skills` service (optional).
|
|
29
|
+
* @returns the view the completion menu subscribes to.
|
|
30
|
+
*/
|
|
31
|
+
export function watchSkills(ctx, fallbackCwd) {
|
|
32
|
+
const skills = ctx.get('skills');
|
|
33
|
+
let agent;
|
|
34
|
+
let rows = [];
|
|
35
|
+
let error;
|
|
36
|
+
// The agent whose workspace the current rows were last successfully read
|
|
37
|
+
// from: a failure for an agent that never loaded must clear the rows, not
|
|
38
|
+
// keep another workspace's catalog answerable in this session.
|
|
39
|
+
let loadedFor;
|
|
40
|
+
const listeners = new Set();
|
|
41
|
+
const reload = () => {
|
|
42
|
+
const target = agent;
|
|
43
|
+
if (skills === undefined)
|
|
44
|
+
return;
|
|
45
|
+
Promise.resolve().then(() => skills.list(target === undefined
|
|
46
|
+
// No session exists yet (a bare launch keeps the agent unset until the
|
|
47
|
+
// first message): read the global skill layer for the working directory.
|
|
48
|
+
// The upstream contract makes `scope` optional — omitted reads the
|
|
49
|
+
// global layer alone — so the menu offers skills before a session does.
|
|
50
|
+
? { cwd: fallbackCwd }
|
|
51
|
+
: {
|
|
52
|
+
cwd: target.session.header.cwd ?? fallbackCwd,
|
|
53
|
+
scope: target,
|
|
54
|
+
})).then((summaries) => {
|
|
55
|
+
// A retarget landed while this catalog was loading: the rows belong to
|
|
56
|
+
// another agent's workspace and must never overwrite the current view.
|
|
57
|
+
if (agent !== target)
|
|
58
|
+
return;
|
|
59
|
+
const next = toRows(summaries);
|
|
60
|
+
// Description and invocation-flag edits must surface too: a name-only
|
|
61
|
+
// comparison silently dropped those change notifications.
|
|
62
|
+
const unchanged = next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name
|
|
63
|
+
&& row.description === rows[index]?.description
|
|
64
|
+
&& row.modelInvocable === rows[index]?.modelInvocable);
|
|
65
|
+
rows = next;
|
|
66
|
+
loadedFor = target;
|
|
67
|
+
const recovered = error !== undefined;
|
|
68
|
+
error = undefined;
|
|
69
|
+
if (unchanged && !recovered)
|
|
70
|
+
return;
|
|
71
|
+
for (const listener of listeners)
|
|
72
|
+
listener();
|
|
73
|
+
}).catch((cause) => {
|
|
74
|
+
if (agent !== target)
|
|
75
|
+
return;
|
|
76
|
+
// Discovery failure keeps the last good rows for the SAME target (the
|
|
77
|
+
// next skills/change notification is the retry surface, mirroring the
|
|
78
|
+
// web directory); a target that never loaded starts from empty rows —
|
|
79
|
+
// stale rows from a previous workspace must not keep completing here.
|
|
80
|
+
// The rows array keeps its identity unless the failure text itself
|
|
81
|
+
// changed: a repeated identical error on the 0.1.5 event storm must not
|
|
82
|
+
// churn fresh identities into React's update chain.
|
|
83
|
+
const nextError = cause instanceof Error ? cause.message : String(cause);
|
|
84
|
+
if (loadedFor !== target)
|
|
85
|
+
rows = [];
|
|
86
|
+
const errorChanged = nextError !== error;
|
|
87
|
+
error = nextError;
|
|
88
|
+
if (!errorChanged)
|
|
89
|
+
return;
|
|
90
|
+
for (const listener of listeners)
|
|
91
|
+
listener();
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
if (skills !== undefined) {
|
|
95
|
+
ctx.on('skills/change', reload);
|
|
96
|
+
// Read the global layer immediately: a bare launch has no agent yet, and
|
|
97
|
+
// waiting for the first skills/change would leave the menu empty.
|
|
98
|
+
reload();
|
|
99
|
+
}
|
|
100
|
+
const view = {
|
|
101
|
+
get rows() {
|
|
102
|
+
return rows;
|
|
103
|
+
},
|
|
104
|
+
get error() {
|
|
105
|
+
return error;
|
|
106
|
+
},
|
|
107
|
+
subscribe(listener) {
|
|
108
|
+
listeners.add(listener);
|
|
109
|
+
return () => {
|
|
110
|
+
listeners.delete(listener);
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
setAgent(next) {
|
|
114
|
+
agent = next;
|
|
115
|
+
reload();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
return view;
|
|
119
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interactive terminal app's command-line provider: parses `--resume`,
|
|
3
|
+
* `--continue`, `--session`, `--mode`, `--theme`, `--image`, an optional
|
|
4
|
+
* initial prompt, and `--help`, then
|
|
5
|
+
* publishes {@link TUI_STARTUP_SERVICE} for the runner to consume lazily.
|
|
6
|
+
* Follows the headless bundle's startup shape (a commander action publishing
|
|
7
|
+
* a service through {@link parseCmdline}).
|
|
8
|
+
*
|
|
9
|
+
* Semantics:
|
|
10
|
+
* - `--resume <id|prefix>` — continue the persisted session whose id or unique
|
|
11
|
+
* id-prefix matches; the TUI replays its transcript and appends to the same
|
|
12
|
+
* durable log.
|
|
13
|
+
* - `--continue` / `-c` — resume the most recently modified persisted session
|
|
14
|
+
* whose project directory matches the current working directory.
|
|
15
|
+
* - `--session <id>` — create a new session under an explicit identity (the
|
|
16
|
+
* id must not exist yet).
|
|
17
|
+
* - `--theme <dark|light|prismatic|rainbow|auto>` — the color palette; auto follows the
|
|
18
|
+
* terminal (dark fallback until OSC-11 detection lands).
|
|
19
|
+
* - no flags — a fresh session with a minted id.
|
|
20
|
+
*
|
|
21
|
+
* @module @deepseek-ai/dsh-tui/startup
|
|
22
|
+
*/
|
|
23
|
+
import { Command } from 'commander';
|
|
24
|
+
import { parseCmdline } from '@deepseek-ai/dsh-cmdline';
|
|
25
|
+
import { THEME_NAMES } from './theme.mjs';
|
|
26
|
+
/** Stable Cordis plugin name. */
|
|
27
|
+
export const name = 'tui-startup';
|
|
28
|
+
/** Services required before the invocation can be resolved. */
|
|
29
|
+
export const inject = ['cmdlineArgs'];
|
|
30
|
+
/** Service provided by this plugin and injected by the terminal runner. */
|
|
31
|
+
const TUI_STARTUP_SERVICE = 'tuiStartup';
|
|
32
|
+
/** Pure option policy shared by Commander and tests. */
|
|
33
|
+
export function resolveTuiStartup(options) {
|
|
34
|
+
const selected = [options.resume !== undefined, options.continue === true, options.session !== undefined];
|
|
35
|
+
if (selected.filter(Boolean).length > 1)
|
|
36
|
+
throw new Error('--resume, --continue, and --session are mutually exclusive');
|
|
37
|
+
if (options.session === '')
|
|
38
|
+
throw new Error('--session needs an id');
|
|
39
|
+
if (options.resume === '')
|
|
40
|
+
throw new Error('--resume needs a session id or id prefix');
|
|
41
|
+
if (options.mode === '')
|
|
42
|
+
throw new Error('--mode needs a preset id');
|
|
43
|
+
if (options.mode !== undefined && (options.resume !== undefined || options.continue === true)) {
|
|
44
|
+
throw new Error('--mode applies only to a new session; it cannot be combined with --resume or --continue');
|
|
45
|
+
}
|
|
46
|
+
if (options.theme !== undefined && !THEME_NAMES.includes(options.theme)) {
|
|
47
|
+
throw new Error(`--theme must be one of: ${THEME_NAMES.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
const input = {
|
|
50
|
+
...(options.theme === undefined ? {} : { theme: options.theme }),
|
|
51
|
+
...(options.prompt === undefined || options.prompt.trim() === '' ? {} : { prompt: options.prompt.trim() }),
|
|
52
|
+
...(options.images === undefined || options.images.length === 0 ? {} : { images: [...options.images] }),
|
|
53
|
+
};
|
|
54
|
+
return options.resume !== undefined
|
|
55
|
+
? { kind: 'resume', sessionId: options.resume, ...input }
|
|
56
|
+
: options.continue === true
|
|
57
|
+
? { kind: 'latest', ...input }
|
|
58
|
+
: options.session !== undefined
|
|
59
|
+
? { kind: 'named', sessionId: options.session, ...options.mode === undefined ? {} : { mode: options.mode }, ...input }
|
|
60
|
+
: { kind: 'fresh', ...options.mode === undefined ? {} : { mode: options.mode }, ...input };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* This app's command: the launcher's flags this app owns, its description,
|
|
64
|
+
* and its help text.
|
|
65
|
+
* @returns a fresh program, so one process can parse more than once (tests).
|
|
66
|
+
*/
|
|
67
|
+
function tuiCommand() {
|
|
68
|
+
return new Command()
|
|
69
|
+
.name('dsh --profile cli')
|
|
70
|
+
.description('DeepSeek Harness CLI core: the interactive coding terminal.')
|
|
71
|
+
.helpOption('-h, --help', 'show this help')
|
|
72
|
+
.option('-r, --resume <session>', 'resume the persisted session with this id (or unique id prefix)')
|
|
73
|
+
.option('-c, --continue', 'resume the most recent persisted session for this working directory')
|
|
74
|
+
.option('--session <id>', 'create a new session under this explicit id')
|
|
75
|
+
.option('--mode <preset>', 'agent preset for a newly created session')
|
|
76
|
+
.option('--theme <name>', 'color theme: dark (default), light, prismatic, rainbow, or auto')
|
|
77
|
+
.option('-i, --image <path>', 'attach an image to the initial prompt (repeatable)', (path, paths) => [...paths, path], [])
|
|
78
|
+
.argument('[prompt...]', 'initial prompt; sends immediately after startup')
|
|
79
|
+
.addHelpText('after', `
|
|
80
|
+
Examples:
|
|
81
|
+
dsh --profile cli fresh session, minted id
|
|
82
|
+
dsh --profile cli --resume abc123 resume session by id prefix
|
|
83
|
+
dsh --profile cli --continue resume the latest local session
|
|
84
|
+
dsh --profile cli --mode minimal fresh session using the minimal preset
|
|
85
|
+
dsh --profile cli --theme light light palette for bright terminals
|
|
86
|
+
dsh --profile cli "explain this repo" start and send an initial prompt
|
|
87
|
+
dsh --profile cli -i diagram.png "review this diagram"
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse the invocation and publish the startup service. Mutual exclusions are
|
|
92
|
+
* usage errors rejected from the action before anything is provided.
|
|
93
|
+
* @param ctx - plugin context carrying the command line and exit request.
|
|
94
|
+
*/
|
|
95
|
+
export function apply(ctx) {
|
|
96
|
+
const program = tuiCommand();
|
|
97
|
+
program.action((prompt) => {
|
|
98
|
+
const options = { ...program.opts(), prompt: prompt.join(' ') };
|
|
99
|
+
let startup;
|
|
100
|
+
try {
|
|
101
|
+
startup = resolveTuiStartup(options);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
program.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
105
|
+
}
|
|
106
|
+
if (startup === undefined)
|
|
107
|
+
return;
|
|
108
|
+
ctx.provide(TUI_STARTUP_SERVICE, { startup });
|
|
109
|
+
});
|
|
110
|
+
parseCmdline(ctx, program);
|
|
111
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observable transcript store: folds session events into the projection view
|
|
3
|
+
* and notifies subscribers. The renderer subscribes through
|
|
4
|
+
* `useSyncExternalStore`; the runner owns event feeding.
|
|
5
|
+
*
|
|
6
|
+
* Folding runs on the same mutable replay accumulator the persisted-log
|
|
7
|
+
* path uses (`replayProjectEvent`: id-indexed row updates, in-place
|
|
8
|
+
* appends), so a live structural event costs O(1) entry work regardless of
|
|
9
|
+
* transcript length — the copy-on-write fold rebuilt the whole entries
|
|
10
|
+
* array per event, making a growing session quadratic. An immutable
|
|
11
|
+
* `TranscriptView` snapshot is materialized only when a changed view is
|
|
12
|
+
* READ (once per rendered frame under the notification throttle, never per
|
|
13
|
+
* event), and every snapshot copies its arrays, so a view already handed
|
|
14
|
+
* out never observes later folds.
|
|
15
|
+
*
|
|
16
|
+
* Notification coalescing: the fold stays synchronous — `getView()` always
|
|
17
|
+
* returns the latest state the moment `apply` returns — but listener
|
|
18
|
+
* notification is frame-throttled (~16ms) and deduplicated. The zai/GLM
|
|
19
|
+
* adapter delivers tokens as a sustained stream of sub-millisecond,
|
|
20
|
+
* microtask-spaced bursts: per-burst notification renders at microtask
|
|
21
|
+
* cadence, which chained SyncLane `useSyncExternalStore` rerenders past
|
|
22
|
+
* React's nested-update limit ("Maximum update depth exceeded"), while a
|
|
23
|
+
* bare `setImmediate` merges a whole macrotask turn's bursts into one
|
|
24
|
+
* chunky repaint (streaming text visibly staggers). The frame budget gives
|
|
25
|
+
* both: an event ≥16ms after the last paint notifies via `setImmediate`
|
|
26
|
+
* (sub-millisecond latency for sparse/first tokens), and anything denser
|
|
27
|
+
* defers to the next 16ms boundary — a 60fps render cap that also breaks
|
|
28
|
+
* the nesting chain by construction.
|
|
29
|
+
*
|
|
30
|
+
* @module @deepseek-ai/dsh-tui/store
|
|
31
|
+
*/
|
|
32
|
+
import { applyAssistantStreamChunk, clearAssistantStream, createReplayAccumulator, replayProjectEvent, snapshotReplayView, } from './render/projection.mjs';
|
|
33
|
+
/** Render frame budget: the notification cadence's upper bound. */
|
|
34
|
+
const NOTIFY_FRAME_MS = 16;
|
|
35
|
+
/**
|
|
36
|
+
* Create one transcript store, optionally seeded with replayed history. The
|
|
37
|
+
* seed folds synchronously BEFORE the first render, so a resumed session
|
|
38
|
+
* paints its full transcript on mount (no live `session/event` fires for
|
|
39
|
+
* constructor seeds — the store's `session/event` feed only carries new
|
|
40
|
+
* appends).
|
|
41
|
+
* @param replay - persisted events in `seq` order (e.g. a resumed session's
|
|
42
|
+
* constructor seed); folded once and never re-notified.
|
|
43
|
+
* @returns the store the runner feeds and the renderer subscribes to.
|
|
44
|
+
*/
|
|
45
|
+
export function createTranscriptStore(replay) {
|
|
46
|
+
let acc = createReplayAccumulator();
|
|
47
|
+
for (const event of replay ?? [])
|
|
48
|
+
replayProjectEvent(acc, event);
|
|
49
|
+
let view = snapshotReplayView(acc);
|
|
50
|
+
let dirty = false;
|
|
51
|
+
const listeners = new Set();
|
|
52
|
+
let scheduled = false;
|
|
53
|
+
let lastNotifyAt = 0;
|
|
54
|
+
// Live attempt → `turn:step` key: chunk frames name only their attempt, so
|
|
55
|
+
// the start frame's turn/step anchor is remembered until the end frame
|
|
56
|
+
// retires the attempt. A replacement attempt (new start frame) overwrites
|
|
57
|
+
// the entry; committed settlements already cleared the tails it replaces.
|
|
58
|
+
const attemptKeys = new Map();
|
|
59
|
+
const notify = () => {
|
|
60
|
+
if (scheduled)
|
|
61
|
+
return;
|
|
62
|
+
scheduled = true;
|
|
63
|
+
const wait = NOTIFY_FRAME_MS - (Date.now() - lastNotifyAt);
|
|
64
|
+
const dispatch = () => {
|
|
65
|
+
scheduled = false;
|
|
66
|
+
lastNotifyAt = Date.now();
|
|
67
|
+
for (const listener of listeners) {
|
|
68
|
+
listener();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// Sparse streams paint with setImmediate latency; a denser burst defers
|
|
72
|
+
// to the next frame boundary instead of repainting per microtask batch.
|
|
73
|
+
if (wait <= 0)
|
|
74
|
+
setImmediate(dispatch);
|
|
75
|
+
else
|
|
76
|
+
setTimeout(dispatch, wait);
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
getView: () => {
|
|
80
|
+
if (dirty) {
|
|
81
|
+
view = snapshotReplayView(acc);
|
|
82
|
+
dirty = false;
|
|
83
|
+
}
|
|
84
|
+
return view;
|
|
85
|
+
},
|
|
86
|
+
subscribe(listener) {
|
|
87
|
+
listeners.add(listener);
|
|
88
|
+
return () => {
|
|
89
|
+
listeners.delete(listener);
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
apply(event) {
|
|
93
|
+
if (!replayProjectEvent(acc, event))
|
|
94
|
+
return;
|
|
95
|
+
dirty = true;
|
|
96
|
+
notify();
|
|
97
|
+
},
|
|
98
|
+
applyStreamFrame(frame) {
|
|
99
|
+
if (frame.type === 'start') {
|
|
100
|
+
attemptKeys.set(frame.attemptId, `${frame.turn}:${frame.step}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (frame.type === 'chunk') {
|
|
104
|
+
const key = attemptKeys.get(frame.attemptId);
|
|
105
|
+
if (key === undefined)
|
|
106
|
+
return;
|
|
107
|
+
if (!applyAssistantStreamChunk(acc, key, frame.time, frame.chunk))
|
|
108
|
+
return;
|
|
109
|
+
dirty = true;
|
|
110
|
+
notify();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// End frame: committed settlements arrive as durable events before
|
|
114
|
+
// their end frame and already cleared the tails; an abandoned attempt
|
|
115
|
+
// has no settlement, so its partial tail is dropped here.
|
|
116
|
+
attemptKeys.delete(frame.attemptId);
|
|
117
|
+
if (frame.outcome.kind === 'abandoned' && clearAssistantStream(acc)) {
|
|
118
|
+
dirty = true;
|
|
119
|
+
notify();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
reset() {
|
|
123
|
+
acc = createReplayAccumulator();
|
|
124
|
+
attemptKeys.clear();
|
|
125
|
+
dirty = true;
|
|
126
|
+
notify();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live subagent activity feed: a bounded, display-only projection of CHILD
|
|
3
|
+
* session events. The transcript store folds only the root session (the
|
|
4
|
+
* durable truth this TUI renders); subagent conversations are their own
|
|
5
|
+
* sessions, and before this module their events were dropped entirely —
|
|
6
|
+
* a running subagent was invisible until its parent tool call settled.
|
|
7
|
+
*
|
|
8
|
+
* This is NOT a second transcript: each child folds to ONE row (label,
|
|
9
|
+
* running state, bounded last-activity text), capped at
|
|
10
|
+
* {@link MAX_SUBAGENT_ROWS}. The cap is a display budget, not a fan-out
|
|
11
|
+
* limit: a new running child evicts the OLDEST settled row when one
|
|
12
|
+
* exists, and while every row is busy the newcomer waits off-screen — but
|
|
13
|
+
* the observed-session total (getTotalSeen) keeps counting, so status
|
|
14
|
+
* totals never under-report the fan-out. Rows are advisory display
|
|
15
|
+
* state, rebuilt from live events; nothing here persists or replays.
|
|
16
|
+
* Notification is coalesced
|
|
17
|
+
* by the same ~16ms frame throttle as the transcript store (per-burst
|
|
18
|
+
* microtask notify chained SyncLane rerenders past React's nested update
|
|
19
|
+
* limit; a bare macrotask merge repaints a whole turn's bursts at once).
|
|
20
|
+
*
|
|
21
|
+
* @module @deepseek-ai/dsh-code/subagents
|
|
22
|
+
*/
|
|
23
|
+
/** Hard row cap: overflow evicts the oldest settled row; a fully busy feed waits. */
|
|
24
|
+
export const MAX_SUBAGENT_ROWS = 8;
|
|
25
|
+
/** Render frame budget: the notification cadence's upper bound. */
|
|
26
|
+
const NOTIFY_FRAME_MS = 16;
|
|
27
|
+
/** Bounded last-activity text (plain characters, display-sliced later). */
|
|
28
|
+
const MAX_ACTIVITY_CHARS = 80;
|
|
29
|
+
/** Single-line bounded preview of an assembled message's text content. */
|
|
30
|
+
function messagePreview(content) {
|
|
31
|
+
if (!Array.isArray(content))
|
|
32
|
+
return 'replied';
|
|
33
|
+
const texts = [];
|
|
34
|
+
for (const block of content) {
|
|
35
|
+
if (texts.join(' ').length >= MAX_ACTIVITY_CHARS)
|
|
36
|
+
break;
|
|
37
|
+
if (typeof block === 'object' && block !== null) {
|
|
38
|
+
const { type, text } = block;
|
|
39
|
+
if (type === 'text' && typeof text === 'string' && text !== '')
|
|
40
|
+
texts.push(text);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const joined = texts.join(' ').replace(/\s+/gu, ' ').trim();
|
|
44
|
+
return joined === '' ? 'replied' : bound(joined);
|
|
45
|
+
}
|
|
46
|
+
/** Bound one activity string to the display budget. */
|
|
47
|
+
function bound(text) {
|
|
48
|
+
const flat = text.replace(/\s+/gu, ' ').trim();
|
|
49
|
+
return flat.length > MAX_ACTIVITY_CHARS ? `${flat.slice(0, MAX_ACTIVITY_CHARS - 1)}…` : flat;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fold one child-session event into its feed row (pure).
|
|
53
|
+
* Unknown event kinds leave the row untouched.
|
|
54
|
+
* @param previous - the row's current state, when any.
|
|
55
|
+
* @param sessionId - the child session id.
|
|
56
|
+
* @param event - the child session event.
|
|
57
|
+
* @returns the next row state.
|
|
58
|
+
*/
|
|
59
|
+
export function foldSubagentRow(previous, sessionId, event) {
|
|
60
|
+
const base = previous ?? {
|
|
61
|
+
id: sessionId,
|
|
62
|
+
label: `agent ${sessionId.slice(-6)}`,
|
|
63
|
+
state: 'running',
|
|
64
|
+
activity: 'starting…',
|
|
65
|
+
updatedAt: event.time,
|
|
66
|
+
};
|
|
67
|
+
const data = event.data;
|
|
68
|
+
switch (event.type) {
|
|
69
|
+
case 'session/title': {
|
|
70
|
+
const title = data['title'];
|
|
71
|
+
const text = typeof title === 'string' && title.trim() !== '' ? title : undefined;
|
|
72
|
+
return text === undefined || text === base.label ? base : { ...base, label: bound(text), updatedAt: event.time };
|
|
73
|
+
}
|
|
74
|
+
case 'request/header':
|
|
75
|
+
return { ...base, state: 'running', activity: 'working…', updatedAt: event.time };
|
|
76
|
+
case 'user/message':
|
|
77
|
+
return { ...base, state: 'running', activity: 'prompted', updatedAt: event.time };
|
|
78
|
+
case 'assistant/attempt':
|
|
79
|
+
// Durable logs are settlement-only since session-log v2; an attempt
|
|
80
|
+
// landing without a surface message means the model is retrying or
|
|
81
|
+
// recovered from a stream error, so the child stays running.
|
|
82
|
+
return { ...base, state: 'running', activity: 'thinking…', updatedAt: event.time };
|
|
83
|
+
case 'subagent/catalog': {
|
|
84
|
+
// Parent-owned durable discovery fact (0.1.5): the catalog names the
|
|
85
|
+
// child's mode (one-shot vs continuable) and its authored label — the
|
|
86
|
+
// most semantic label the row can carry. It is a discovery fact, not a
|
|
87
|
+
// lifecycle signal: a fresh row starts idle, but a late delivery never
|
|
88
|
+
// regresses a row that already ran or finished. An unchanged fact keeps
|
|
89
|
+
// the row's identity (the no-op discipline of the default branch), so
|
|
90
|
+
// repeated deliveries never churn the snapshot array.
|
|
91
|
+
const mode = event.data.mode === 'continuable' ? 'continuable' : 'one-shot';
|
|
92
|
+
const label = event.data.label !== undefined && event.data.label.trim() !== '' ? bound(event.data.label) : undefined;
|
|
93
|
+
const nextLabel = label === undefined ? base.label : label;
|
|
94
|
+
const activity = label === undefined ? `catalog · ${mode}` : `catalog · ${mode} · ${label}`;
|
|
95
|
+
const state = previous === undefined ? 'idle' : base.state;
|
|
96
|
+
if (nextLabel === base.label && state === base.state && activity === base.activity)
|
|
97
|
+
return base;
|
|
98
|
+
return { ...base, state, label: nextLabel, activity, updatedAt: event.time };
|
|
99
|
+
}
|
|
100
|
+
case 'assistant/message':
|
|
101
|
+
return { ...base, state: 'idle', activity: messagePreview(data['message'] === undefined ? undefined : data['message'].content), updatedAt: event.time };
|
|
102
|
+
case 'tool/call': {
|
|
103
|
+
const name = typeof data['name'] === 'string' ? data['name'] : 'tool';
|
|
104
|
+
return { ...base, state: 'running', activity: `tool ${name}`, updatedAt: event.time };
|
|
105
|
+
}
|
|
106
|
+
case 'tool/result':
|
|
107
|
+
return { ...base, state: 'running', activity: 'tool done', updatedAt: event.time };
|
|
108
|
+
case 'turn/start':
|
|
109
|
+
return { ...base, state: 'running', activity: base.activity === 'starting…' ? 'working…' : base.activity, updatedAt: event.time };
|
|
110
|
+
case 'turn/end':
|
|
111
|
+
return { ...base, state: 'done', activity: 'finished', updatedAt: event.time };
|
|
112
|
+
default:
|
|
113
|
+
// Unknown kinds leave the row untouched (identity-stable: a no-op
|
|
114
|
+
// fold must not churn the snapshot array).
|
|
115
|
+
return base;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Create one subagent feed. `apply` folds a child event (the caller gates
|
|
120
|
+
* which sessions are children); `reset` clears on a session switch. Row
|
|
121
|
+
* order is first-seen; the snapshot array is frozen and only replaced when
|
|
122
|
+
* a row actually changed.
|
|
123
|
+
* @returns the mutable feed handle plus its `SubagentFeedView`.
|
|
124
|
+
*/
|
|
125
|
+
export function createSubagentFeed() {
|
|
126
|
+
let rows = Object.freeze([]);
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const listeners = new Set();
|
|
129
|
+
let scheduled = false;
|
|
130
|
+
let lastNotifyAt = 0;
|
|
131
|
+
const notify = () => {
|
|
132
|
+
if (scheduled)
|
|
133
|
+
return;
|
|
134
|
+
scheduled = true;
|
|
135
|
+
const wait = NOTIFY_FRAME_MS - (Date.now() - lastNotifyAt);
|
|
136
|
+
const dispatch = () => {
|
|
137
|
+
scheduled = false;
|
|
138
|
+
lastNotifyAt = Date.now();
|
|
139
|
+
for (const listener of listeners)
|
|
140
|
+
listener();
|
|
141
|
+
};
|
|
142
|
+
if (wait <= 0)
|
|
143
|
+
setImmediate(dispatch);
|
|
144
|
+
else
|
|
145
|
+
setTimeout(dispatch, wait);
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
apply(sessionId, event) {
|
|
149
|
+
const index = rows.findIndex(row => row.id === sessionId);
|
|
150
|
+
const previous = index === -1 ? undefined : rows[index];
|
|
151
|
+
const next = foldSubagentRow(previous, sessionId, event);
|
|
152
|
+
if (index !== -1) {
|
|
153
|
+
if (next === previous)
|
|
154
|
+
return;
|
|
155
|
+
rows = Object.freeze(rows.map((row, at) => at === index ? next : row));
|
|
156
|
+
notify();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// A child this feed has not shown yet: the honest total grows even
|
|
160
|
+
// when every row is busy; admission then prefers evicting the OLDEST
|
|
161
|
+
// settled row (idle or done — both are non-running) so a new running
|
|
162
|
+
// agent never waits on one that already settled.
|
|
163
|
+
const counted = !seen.has(sessionId);
|
|
164
|
+
if (counted)
|
|
165
|
+
seen.add(sessionId);
|
|
166
|
+
if (rows.length >= MAX_SUBAGENT_ROWS) {
|
|
167
|
+
const evict = rows.findIndex(row => row.state !== 'running');
|
|
168
|
+
if (evict === -1) {
|
|
169
|
+
if (counted)
|
|
170
|
+
notify();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
rows = Object.freeze([...rows.slice(0, evict), next, ...rows.slice(evict + 1)]);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
rows = Object.freeze([...rows, next]);
|
|
177
|
+
}
|
|
178
|
+
notify();
|
|
179
|
+
},
|
|
180
|
+
reset() {
|
|
181
|
+
seen.clear();
|
|
182
|
+
if (rows.length === 0)
|
|
183
|
+
return;
|
|
184
|
+
rows = Object.freeze([]);
|
|
185
|
+
notify();
|
|
186
|
+
},
|
|
187
|
+
subscribe(listener) {
|
|
188
|
+
listeners.add(listener);
|
|
189
|
+
return () => {
|
|
190
|
+
listeners.delete(listener);
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
getSnapshot() {
|
|
194
|
+
return rows;
|
|
195
|
+
},
|
|
196
|
+
getTotalSeen() {
|
|
197
|
+
return seen.size;
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|