@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.
Files changed (152) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/session-metrics/view.mjs +10 -2
  5. package/vendor/tui/lib/app.mjs +6674 -0
  6. package/vendor/tui/lib/approval.mjs +122 -0
  7. package/vendor/tui/lib/attachments.mjs +218 -0
  8. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  9. package/vendor/tui/lib/authorization.mjs +111 -0
  10. package/vendor/tui/lib/commands.mjs +123 -0
  11. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  12. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  13. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  14. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  15. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  16. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  17. package/vendor/tui/lib/editor-keys.mjs +346 -0
  18. package/vendor/tui/lib/editor.mjs +50 -0
  19. package/vendor/tui/lib/fork.mjs +24 -0
  20. package/vendor/tui/lib/git-workflow.mjs +278 -0
  21. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  22. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  23. package/vendor/tui/lib/index.mjs +2198 -0
  24. package/vendor/tui/lib/input-split.mjs +170 -0
  25. package/vendor/tui/lib/internals.mjs +90 -0
  26. package/vendor/tui/lib/invariant.mjs +22 -0
  27. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  28. package/vendor/tui/lib/keyboard.mjs +283 -0
  29. package/vendor/tui/lib/language-panel.mjs +41 -0
  30. package/vendor/tui/lib/locales/en.mjs +495 -0
  31. package/vendor/tui/lib/locales/zh.mjs +495 -0
  32. package/vendor/tui/lib/mentions.mjs +150 -0
  33. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  34. package/vendor/tui/lib/models.mjs +215 -0
  35. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  36. package/vendor/tui/lib/permissions.mjs +45 -0
  37. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  38. package/vendor/tui/lib/presets.mjs +46 -0
  39. package/vendor/tui/lib/provider-settings.mjs +600 -0
  40. package/vendor/tui/lib/questions.mjs +116 -0
  41. package/vendor/tui/lib/rainbow.mjs +181 -0
  42. package/vendor/tui/lib/render/animations.mjs +665 -0
  43. package/vendor/tui/lib/render/editor.mjs +437 -0
  44. package/vendor/tui/lib/render/export.mjs +119 -0
  45. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  46. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  47. package/vendor/tui/lib/render/inspector.mjs +93 -0
  48. package/vendor/tui/lib/render/lines.mjs +538 -0
  49. package/vendor/tui/lib/render/markdown.mjs +592 -0
  50. package/vendor/tui/lib/render/projection.mjs +1862 -0
  51. package/vendor/tui/lib/render/status.mjs +591 -0
  52. package/vendor/tui/lib/render/text.mjs +157 -0
  53. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  54. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  55. package/vendor/tui/lib/render/usage.mjs +336 -0
  56. package/vendor/tui/lib/render/width.mjs +191 -0
  57. package/vendor/tui/lib/session-directory.mjs +329 -0
  58. package/vendor/tui/lib/session-query.mjs +184 -0
  59. package/vendor/tui/lib/session-switch.mjs +51 -0
  60. package/vendor/tui/lib/settings-file.mjs +75 -0
  61. package/vendor/tui/lib/skills.mjs +119 -0
  62. package/vendor/tui/lib/startup.mjs +111 -0
  63. package/vendor/tui/lib/store.mjs +129 -0
  64. package/vendor/tui/lib/subagents.mjs +200 -0
  65. package/vendor/tui/lib/terminal-title.mjs +186 -0
  66. package/vendor/tui/lib/theme-panel.mjs +60 -0
  67. package/vendor/tui/lib/theme.mjs +379 -0
  68. package/vendor/tui/lib/update-panel.mjs +239 -0
  69. package/vendor/tui/lib/update.mjs +101 -0
  70. package/vendor/tui/lib/version.mjs +105 -0
  71. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  72. package/vendor/tui/LICENSE +0 -21
  73. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  74. package/vendor/tui/dscode-email/cli.mjs +0 -32
  75. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  76. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  77. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  78. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  79. package/vendor/tui/dscode-email/imap.mjs +0 -136
  80. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  81. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  82. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  83. package/vendor/tui/dscode-email/store.mjs +0 -32
  84. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  85. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  86. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  87. package/vendor/tui/index.mjs +0 -44441
  88. package/vendor/tui/invariant.mjs +0 -21
  89. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  90. package/vendor/tui/session-query.mjs +0 -150
  91. package/vendor/tui/startup.mjs +0 -109
  92. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  93. package/vendor/tui/types/app.d.ts +0 -348
  94. package/vendor/tui/types/approval.d.ts +0 -59
  95. package/vendor/tui/types/attachments.d.ts +0 -52
  96. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  97. package/vendor/tui/types/authorization.d.ts +0 -36
  98. package/vendor/tui/types/commands.d.ts +0 -52
  99. package/vendor/tui/types/editor-keys.d.ts +0 -105
  100. package/vendor/tui/types/editor.d.ts +0 -6
  101. package/vendor/tui/types/fork.d.ts +0 -8
  102. package/vendor/tui/types/git-workflow.d.ts +0 -121
  103. package/vendor/tui/types/index.d.ts +0 -223
  104. package/vendor/tui/types/input-split.d.ts +0 -54
  105. package/vendor/tui/types/internals.d.ts +0 -26
  106. package/vendor/tui/types/invariant.d.ts +0 -15
  107. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  108. package/vendor/tui/types/keyboard.d.ts +0 -80
  109. package/vendor/tui/types/language-panel.d.ts +0 -12
  110. package/vendor/tui/types/locales/en.d.ts +0 -450
  111. package/vendor/tui/types/locales/zh.d.ts +0 -9
  112. package/vendor/tui/types/mentions.d.ts +0 -85
  113. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  114. package/vendor/tui/types/models.d.ts +0 -133
  115. package/vendor/tui/types/permissions.d.ts +0 -27
  116. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  117. package/vendor/tui/types/presets.d.ts +0 -22
  118. package/vendor/tui/types/provider-settings.d.ts +0 -239
  119. package/vendor/tui/types/questions.d.ts +0 -54
  120. package/vendor/tui/types/rainbow.d.ts +0 -69
  121. package/vendor/tui/types/render/animations.d.ts +0 -307
  122. package/vendor/tui/types/render/editor.d.ts +0 -163
  123. package/vendor/tui/types/render/export.d.ts +0 -9
  124. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  125. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  126. package/vendor/tui/types/render/inspector.d.ts +0 -62
  127. package/vendor/tui/types/render/lines.d.ts +0 -86
  128. package/vendor/tui/types/render/markdown.d.ts +0 -29
  129. package/vendor/tui/types/render/projection.d.ts +0 -587
  130. package/vendor/tui/types/render/status.d.ts +0 -196
  131. package/vendor/tui/types/render/text.d.ts +0 -64
  132. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  133. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  134. package/vendor/tui/types/render/usage.d.ts +0 -113
  135. package/vendor/tui/types/render/width.d.ts +0 -29
  136. package/vendor/tui/types/session-directory.d.ts +0 -190
  137. package/vendor/tui/types/session-query.d.ts +0 -92
  138. package/vendor/tui/types/session-switch.d.ts +0 -17
  139. package/vendor/tui/types/settings-file.d.ts +0 -41
  140. package/vendor/tui/types/skills.d.ts +0 -47
  141. package/vendor/tui/types/startup.d.ts +0 -65
  142. package/vendor/tui/types/store.d.ts +0 -58
  143. package/vendor/tui/types/subagents.d.ts +0 -70
  144. package/vendor/tui/types/terminal-title.d.ts +0 -66
  145. package/vendor/tui/types/theme-panel.d.ts +0 -24
  146. package/vendor/tui/types/theme.d.ts +0 -434
  147. package/vendor/tui/types/update-panel.d.ts +0 -49
  148. package/vendor/tui/types/update.d.ts +0 -75
  149. package/vendor/tui/types/version.d.ts +0 -19
  150. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  151. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  152. /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
+ }