@toddzheng024/dscode-bundle 0.7.22 → 0.7.24

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 (43) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/cordis.patch.yml +2 -0
  3. package/package.json +4 -2
  4. package/plugins/dscode/index.mjs +1 -1
  5. package/plugins/i18n/messages.d.mts +21 -0
  6. package/plugins/i18n/messages.mjs +18 -6
  7. package/plugins/openrouter/adapter.mjs +23 -2
  8. package/plugins/openrouter/wire.mjs +2 -1
  9. package/plugins/session-bridge/index.mjs +6 -0
  10. package/plugins/session-bridge/tasks.mjs +227 -0
  11. package/plugins/session-metrics/turns.mjs +134 -0
  12. package/plugins/session-metrics/view.mjs +85 -33
  13. package/plugins/triggers/cli.mjs +439 -0
  14. package/plugins/triggers/config.mjs +245 -0
  15. package/plugins/triggers/host.mjs +138 -0
  16. package/plugins/triggers/index.mjs +57 -0
  17. package/plugins/triggers/launchd.mjs +101 -0
  18. package/plugins/triggers/log.mjs +104 -0
  19. package/plugins/triggers/options.mjs +109 -0
  20. package/plugins/triggers/overlay.mjs +7 -0
  21. package/plugins/triggers/poll.mjs +57 -0
  22. package/plugins/triggers/run.mjs +156 -0
  23. package/plugins/triggers/spool.mjs +128 -0
  24. package/plugins/tui-tools/workspace-discovery.mjs +11 -4
  25. package/presets/dscode/agent.cordis.yml +4 -3
  26. package/vendor/command-goal/LICENSE +21 -0
  27. package/vendor/command-goal/index.js +208 -0
  28. package/vendor/command-goal/types/index.d.ts +10 -0
  29. package/vendor/tui/lib/app.mjs +124 -36
  30. package/vendor/tui/lib/communication.mjs +262 -0
  31. package/vendor/tui/lib/dscode/chat.mjs +34 -0
  32. package/vendor/tui/lib/dscode/model-search.mjs +2 -2
  33. package/vendor/tui/lib/dscode/palette.mjs +100 -0
  34. package/vendor/tui/lib/dscode/telemetry.mjs +18 -11
  35. package/vendor/tui/lib/index.mjs +140 -6
  36. package/vendor/tui/lib/locales/en.mjs +3 -0
  37. package/vendor/tui/lib/locales/zh.mjs +3 -0
  38. package/vendor/tui/lib/mentions.mjs +1 -1
  39. package/vendor/tui/lib/render/status.mjs +56 -35
  40. package/vendor/tui/lib/render/text.mjs +33 -0
  41. package/vendor/tui/lib/render/usage.mjs +11 -2
  42. package/vendor/tui/lib/session-directory.mjs +25 -0
  43. package/vendor/tui/lib/skills.mjs +19 -7
@@ -0,0 +1,208 @@
1
+ // dscode-goal-cap-v1
2
+ import { GoalError } from "@deepseek-ai/dsh-goal";
3
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ //#region lib/types/index.js
5
+ /**
6
+ * Human-facing `/goal` command over the persisted same-session goal domain.
7
+ * @module @deepseek-ai/dsh-command-goal
8
+ */
9
+ const name = "command-goal";
10
+ const inject = ["commands", "goals"];
11
+ const USAGE = "Usage: /goal [<objective>|clear|edit <objective>|pause|resume], /goal[<rounds>] set the round cap";
12
+ /** Fail loudly if a locally closed union gains an unhandled member. */
13
+ /* v8 ignore start -- closed-union backstop is unreachable without violating the TypeScript contract */
14
+ function assertNever(value, label) {
15
+ throw new TypeError(`unknown ${label}: ${String(value)}`);
16
+ }
17
+ /* v8 ignore stop */
18
+ /** Parse only the grammar owned by `/goal`; arbitrary other input is an objective. */
19
+ function parseGoalCommand(rawInput) {
20
+ const input = rawInput.trim();
21
+ const capMatch = /^\[(\d+)\]\s*(.*)$/su.exec(input);
22
+ if (capMatch !== null) {
23
+ const maxGoalRounds = Number(capMatch[1]);
24
+ const rest = capMatch[2].trim();
25
+ if (maxGoalRounds < 1 || ["clear", "pause", "resume", "edit"].includes(rest.toLowerCase())) return { kind: "invalid-cap" };
26
+ return rest === "" ? { kind: "cap", maxGoalRounds } : { kind: "create", objective: rest, maxGoalRounds };
27
+ }
28
+ if (input.length === 0) return { kind: "show" };
29
+ const control = input.toLowerCase();
30
+ if (control === "clear") return { kind: "clear" };
31
+ if (control === "pause") return { kind: "pause" };
32
+ if (control === "resume") return { kind: "resume" };
33
+ if (control === "edit") return { kind: "invalid-edit" };
34
+ if (/^edit(?=\s)/iu.test(input)) return {
35
+ kind: "edit",
36
+ objective: input.slice(4).trim()
37
+ };
38
+ return {
39
+ kind: "create",
40
+ objective: input
41
+ };
42
+ }
43
+ /** Human label for one durable goal phase. */
44
+ function phaseLabel(phase) {
45
+ switch (phase) {
46
+ case "active": return "active";
47
+ case "paused": return "paused";
48
+ case "blocked": return "blocked";
49
+ case "complete": return "complete";
50
+ /* v8 ignore next 2 -- GoalPhase is closed and every member is handled above */
51
+ default: return assertNever(phase, "goal phase");
52
+ }
53
+ }
54
+ /** Commands that are meaningful from one exact live state. */
55
+ function commandHint(goal) {
56
+ if (goal.phase === "active") return goal.activation === "armed" ? "/goal edit <objective>, /goal pause, /goal clear" : "/goal edit <objective>, /goal resume, /goal clear";
57
+ switch (goal.phase) {
58
+ case "paused":
59
+ case "blocked": return "/goal edit <objective>, /goal resume, /goal clear";
60
+ case "complete": return "/goal <objective>, /goal clear";
61
+ /* v8 ignore next 2 -- the active branch and every non-active phase are handled above */
62
+ default: return assertNever(goal.phase, "goal phase");
63
+ }
64
+ }
65
+ /** Render direct UI output without exposing compare-and-set internals. */
66
+ function renderGoal(title, goal) {
67
+ const reason = goal.phase === "blocked" ? goal.blockedReason : void 0;
68
+ /* v8 ignore next -- durable replay guarantees every blocked goal carries its validated reason */
69
+ if (goal.phase === "blocked" && reason === void 0) throw new TypeError("blocked goal is missing its reason");
70
+ const blocker = reason === void 0 ? [] : [`Blocker: ${reason.code}: ${reason.message}`];
71
+ return {
72
+ kind: "success",
73
+ text: [
74
+ title,
75
+ `Status: ${phaseLabel(goal.phase)}`,
76
+ ...blocker,
77
+ `Objective: ${goal.objective}`,
78
+ `Rounds: ${goal.roundsStarted}/${goal.maxGoalRounds}`,
79
+ `Activation: ${goal.activation}`,
80
+ "",
81
+ `Commands: ${commandHint(goal)}`
82
+ ].join("\n")
83
+ };
84
+ }
85
+ /** Exact current compare-and-set ref. */
86
+ function goalRef(goal) {
87
+ return {
88
+ id: goal.id,
89
+ revision: goal.revision
90
+ };
91
+ }
92
+ /** dscode: the create/edit request, carrying the `[N]` cap when the human set one. */
93
+ function goalRequest(command) {
94
+ return { objective: command.objective, ...command.maxGoalRounds === void 0 ? {} : { maxGoalRounds: command.maxGoalRounds } };
95
+ }
96
+ /** Direct error for an operation that requires a current goal. */
97
+ function missingGoal(action) {
98
+ return {
99
+ kind: "error",
100
+ text: `No goal is currently set; /goal ${action} requires one. ${USAGE}`
101
+ };
102
+ }
103
+ /**
104
+ * Submit the invocation's admitted composer attachments as one model-visible user
105
+ * message ahead of the goal's next round. The attachments precede a fixed text
106
+ * block naming their role, so a later goal round reads them from ordinary
107
+ * session history without the goal domain storing attachment state.
108
+ */
109
+ function submitObjectiveAttachments(invocation) {
110
+ if (invocation.attachments.length === 0) return;
111
+ invocation.agent.followup(createUserMessage({
112
+ content: [...invocation.attachments, {
113
+ type: "text",
114
+ text: "Reference attachments for the goal objective."
115
+ }],
116
+ source: { kind: "user" }
117
+ }));
118
+ }
119
+ /** Execute one parsed human command through the domain that owns persistence. */
120
+ function executeGoalCommand(ctx, invocation) {
121
+ const command = parseGoalCommand(invocation.rawInput);
122
+ if (invocation.attachments.length > 0 && command.kind !== "create" && command.kind !== "edit") return {
123
+ kind: "error",
124
+ text: "Attachments only accompany a goal objective: /goal <objective> or /goal edit <objective>."
125
+ };
126
+ try {
127
+ const current = ctx.goals.get(invocation.agent);
128
+ switch (command.kind) {
129
+ case "show": return current === void 0 ? {
130
+ kind: "success",
131
+ text: `No goal is currently set.\n${USAGE}`
132
+ } : renderGoal("Goal", current);
133
+ case "invalid-edit": return {
134
+ kind: "error",
135
+ text: `Goal editing requires a replacement objective.\n${USAGE}`
136
+ };
137
+ case "create": {
138
+ if (current !== void 0 && current.phase !== "complete") return {
139
+ kind: "error",
140
+ text: `A goal is already ${phaseLabel(current.phase)}. Use /goal edit <objective> to change it or /goal clear before replacing it.`
141
+ };
142
+ const created = ctx.goals.create(invocation.agent, goalRequest(command));
143
+ submitObjectiveAttachments(invocation);
144
+ return renderGoal("Goal created", created);
145
+ }
146
+ case "edit": {
147
+ if (current === void 0) return missingGoal("edit");
148
+ if (current.phase === "complete") {
149
+ const replaced = ctx.goals.create(invocation.agent, goalRequest(command));
150
+ submitObjectiveAttachments(invocation);
151
+ return renderGoal("Goal created", replaced);
152
+ }
153
+ const edited = ctx.goals.edit(invocation.agent, goalRef(current), goalRequest(command));
154
+ submitObjectiveAttachments(invocation);
155
+ return renderGoal("Goal updated", edited);
156
+ }
157
+ case "pause":
158
+ if (current === void 0) return missingGoal("pause");
159
+ return renderGoal("Goal paused", ctx.goals.pause(invocation.agent, goalRef(current)));
160
+ case "resume":
161
+ if (current === void 0) return missingGoal("resume");
162
+ return renderGoal("Goal resumed", ctx.goals.resume(invocation.agent, goalRef(current)));
163
+ case "clear":
164
+ if (current === void 0) return {
165
+ kind: "success",
166
+ text: "No goal to clear."
167
+ };
168
+ ctx.goals.clear(invocation.agent, goalRef(current));
169
+ return {
170
+ kind: "success",
171
+ text: "Goal cleared."
172
+ };
173
+ case "cap": {
174
+ if (current === void 0) return {
175
+ kind: "error",
176
+ text: `No goal is currently set; /goal[<rounds>] changes the round cap of an existing goal. ${USAGE}`
177
+ };
178
+ return renderGoal("Goal updated", ctx.goals.edit(invocation.agent, goalRef(current), { maxGoalRounds: command.maxGoalRounds }));
179
+ }
180
+ case "invalid-cap": return {
181
+ kind: "error",
182
+ text: `The round cap must be a positive whole number and cannot accompany a control word.\n${USAGE}`
183
+ };
184
+ /* v8 ignore next 2 -- GoalCommand is closed and every member is handled above */
185
+ default: return assertNever(command, "goal command");
186
+ }
187
+ } catch (error) {
188
+ if (error instanceof GoalError) return {
189
+ kind: "error",
190
+ text: "The goal command is not valid for the current state. Run /goal to view available commands."
191
+ };
192
+ throw error;
193
+ }
194
+ }
195
+ /** Register the Codex-shaped `/goal` command for every composed command adapter. */
196
+ function apply(ctx) {
197
+ ctx.commands.register({
198
+ name: "goal",
199
+ description: "set or view the goal for a long-running task",
200
+ input: {
201
+ hint: "[<objective>|clear|edit <objective>|pause|resume], [<rounds>]",
202
+ attachments: true
203
+ },
204
+ handler: (invocation) => executeGoalCommand(ctx, invocation)
205
+ });
206
+ }
207
+ //#endregion
208
+ export { apply, inject, name };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Human-facing `/goal` command over the persisted same-session goal domain.
3
+ * @module @deepseek-ai/dsh-command-goal
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ export declare const name = "command-goal";
7
+ export declare const inject: string[];
8
+ /** Register the Codex-shaped `/goal` command for every composed command adapter. */
9
+ export declare function apply(ctx: Context): void;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -28,13 +28,12 @@ import { DEFAULT_TERMINAL_TITLE, sanitizeTerminalTitle, terminalTitleSequence, u
28
28
  import { settledEntryCount } from './render/projection.mjs';
29
29
  import { imeCursorRowsUp, useImeCursorAnchor } from './render/ime-cursor.mjs';
30
30
  import { readClipboardImage } from './dscode/clipboard-image/index.mjs';
31
- import { dscodeChatLines } from './dscode/chat.mjs';
31
+ import { createChatLinesCache, dscodeChatLines } from './dscode/chat.mjs';
32
32
  import { readFileSync } from 'node:fs';
33
33
  import { FOOTER_FIGURE_RESERVE, footerFor as dscodeFooterFor } from '../../../plugins/session-metrics/view.mjs';
34
34
  import { newerVersion as dscodeNewerVersion } from '../../../plugins/tui-tools/update.mjs';
35
35
  import { languageName as dscodeLanguageName, normalizeLanguage as dscodeNormalizeLanguage, t as dscodeMessage } from '../../../plugins/i18n/messages.mjs';
36
36
  import { dscodeTelemetryNodes } from './dscode/telemetry.mjs';
37
- import { dscodeFooterHeader } from './render/status.mjs';
38
37
  import { dscodePadEnd, welcomeArtRows, welcomePath, WELCOME_ART, WELCOME_ART_SMALL } from './dscode/welcome.mjs';
39
38
  import { TETRIS_TICK_MS as DSCODE_TETRIS_TICK_MS, TETRIS_WIDTH as DSCODE_TETRIS_WIDTH, tetrisFrame as dscodeTetrisFrame } from '../../../plugins/compaction/tetris.mjs';
40
39
  import { PROVIDERS as DSCODE_PROVIDERS, providerSpec as dscodeProviderSpec, providerArgument as dscodeProviderArgument, providerOfLabel as dscodeProviderOfLabel, pickModel as dscodePickModel, credentialState as dscodeCredentialState, waitForModels as dscodeWaitForModels, } from '../../../plugins/providers/catalog.mjs';
@@ -128,8 +127,8 @@ const RESIZE_REFLOW_CLEAR = '\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[3J\x1b[H';
128
127
  const SYNCHRONIZED_UPDATE_BEGIN = '\x1b[?2026h';
129
128
  /** Release the held frame after Ink has replayed the source-backed Static rows. */
130
129
  const SYNCHRONIZED_UPDATE_END = '\x1b[?2026l';
131
- import { layoutStatusBar, padValue, parseStatuslineItems, statusCycleHint, STATUS_GROUP_SEPARATOR, STATUS_ITEM_SEPARATOR, STATUS_ROW2_INDENT, } from './render/status.mjs';
132
- import { displayTail, displayText, singleLineText, truncateColumns } from './render/text.mjs';
130
+ import { layoutStatusBar, parseStatuslineItems, statusCycleHint, STATUS_GROUP_SEPARATOR, STATUS_ITEM_SEPARATOR, STATUS_ROW2_INDENT, } from './render/status.mjs';
131
+ import { displayTail, displayText, formatTokens, singleLineText, truncateColumns, wrapText } from './render/text.mjs';
133
132
  import { isVsCodeTerminalEnv, normalizeKeyboardChunk, PASTE_BRACKET_TIMEOUT_MS, PASTE_END_MARKER, PASTE_START_MARKER, stripPasteMarkers, stripTerminalFocusEvents, tokenizeRawEditorChunk, } from './keyboard.mjs';
134
133
  import { clampScroll, followInspectorCursor, inspectorViewport, layoutGutterRows, liveRegionBudget, moveScroll, panelViewport, revealRow, selectionWindow, } from './render/inspector.mjs';
135
134
  import { clampLiveAllocation, diffLineStyle, fillDiffLineBars, lineSegment, markdownLines, styledLines, textLines, transcriptEntryLines, } from './render/lines.mjs';
@@ -214,9 +213,10 @@ function useFrames(intervalMs, active = true) {
214
213
  * Ink re-subscribes its input effect whenever the handler identity changes.
215
214
  * Keep terminal input ownership stable while a local surface updates cursor,
216
215
  * scroll, or draft state; otherwise every key toggles raw mode and can make
217
- * Ink repeatedly repaint the live region.
216
+ * Ink repeatedly repaint the live region. `active` defaults to true: a panel
217
+ * that only mounts while it owns the keyboard omits the argument.
218
218
  */
219
- function useStableInput(handler, active) {
219
+ function useStableInput(handler, active = true) {
220
220
  const handlerRef = useRef(handler);
221
221
  handlerRef.current = handler;
222
222
  const stableHandler = useCallback((input, key) => {
@@ -349,6 +349,25 @@ function dscodeActivity(entries, streaming) {
349
349
  return dscodeT('activity.running') + ' · ' + singleLineText(tool.name) + (running.length > 1 ? ' +' + (running.length - 1) : '')
350
350
  + (description ? ' · ' + truncateColumns(singleLineText(description), 56) : '');
351
351
  }
352
+ /**
353
+ * dscode: what cross-session communication is doing, or undefined when none is.
354
+ * A send in flight and a request still awaiting its answer outrank the ordinary
355
+ * tool activity, because the turn is blocked on another session either way.
356
+ */
357
+ function dscodeCommunicationActivity(view) {
358
+ const waiting = view.waiting.at(-1);
359
+ if (waiting !== undefined)
360
+ return '⇄ ' + dscodeT('communication.waiting', { peer: shortPeer(waiting.peer) });
361
+ const sending = [...view.rows].reverse().find(row => row.direction === 'sent' && row.pending);
362
+ if (sending !== undefined)
363
+ return '⇄ ' + dscodeT('communication.sending', { peer: shortPeer(sending.peer) });
364
+ return undefined;
365
+ }
366
+ /** Bounded peer label: a full session id is too long for one activity row. */
367
+ function shortPeer(peer) {
368
+ const label = peer.startsWith('session:') ? `session ${peer.slice('session:'.length, 'session:'.length + 8)}` : peer;
369
+ return label.length > 32 ? `${label.slice(0, 31)}…` : label;
370
+ }
352
371
  // dscode: the compaction indicator — a small scripted Tetris bot that lines each
353
372
  // piece up, drops it and clears full rows, the way compaction clears older history.
354
373
  function dscodeTetrisCells(row, palette, key) {
@@ -374,7 +393,7 @@ export function DscodeCompactionLine({ since, rows, animated = true }) {
374
393
  }
375
394
  return createElement(Box, { flexDirection: 'column', paddingX: 2 }, ...board.map((row, y) => createElement(Box, { key: y }, wall('left'), ...dscodeTetrisCells(row, palette, 'cell'), wall('right'), y === 1 ? label(dscodeT('compaction.running'), palette.brandBright) : y === 2 ? label(clock, palette.dim) : undefined)), createElement(Text, { color: inkColor(palette.dim) }, '+' + '-'.repeat(DSCODE_TETRIS_WIDTH * 2) + '+'));
376
395
  }
377
- export function DscodeActivityLine({ entries, streaming, since, animated = true }) {
396
+ export function DscodeActivityLine({ entries, streaming, since, animated = true, communication }) {
378
397
  const columns = useStdout().stdout?.columns ?? 80;
379
398
  const tick = useFrames(animated ? 100 : 1000);
380
399
  const elapsed = since > 0 ? Math.max(0, Date.now() - since) : 0;
@@ -385,8 +404,9 @@ export function DscodeActivityLine({ entries, streaming, since, animated = true
385
404
  const spinner = animated
386
405
  ? dscodeSpinnerCells(tick, palette)
387
406
  : { left: { text: ' ', color: palette.brandMid }, flake: palette.brandBright, right: { text: ' ', color: palette.brandMid } };
388
- const label = truncateColumns(dscodeActivity(entries, streaming), Math.max(1, columns - 9 - visibleColumns(suffix)));
389
- return createElement(Box, { paddingX: 2 }, createElement(Text, { wrap: 'truncate-end' }, createElement(Text, { color: inkColor(spinner.left.color) }, spinner.left.text), createElement(Text, { color: inkColor(spinner.flake) }, '❄'), createElement(Text, { color: inkColor(spinner.right.color) }, spinner.right.text), createElement(Text, { color: inkColor(getPalette().brandBright) }, ' ' + label), createElement(Text, { color: inkColor(getPalette().dim) }, suffix)));
407
+ const crossSession = communication === undefined ? undefined : dscodeCommunicationActivity(communication);
408
+ const label = truncateColumns(crossSession ?? dscodeActivity(entries, streaming), Math.max(1, columns - 9 - visibleColumns(suffix)));
409
+ return createElement(Box, { paddingX: 2 }, createElement(Text, { wrap: 'truncate-end' }, createElement(Text, { color: inkColor(spinner.left.color) }, spinner.left.text), createElement(Text, { color: inkColor(spinner.flake) }, '❄'), createElement(Text, { color: inkColor(spinner.right.color) }, spinner.right.text), createElement(Text, { color: inkColor(crossSession === undefined ? getPalette().brandBright : getPalette().steered) }, ' ' + label), createElement(Text, { color: inkColor(getPalette().dim) }, suffix)));
390
410
  }
391
411
  function DeepDivingLine({ since, animated = true }) {
392
412
  const elapsed = since === 0 ? 0 : Date.now() - since;
@@ -1018,15 +1038,18 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1018
1038
  const timer = setInterval(() => dscodeRefreshMetrics(n => n + 1), 1000);
1019
1039
  return () => clearInterval(timer);
1020
1040
  }, []);
1021
- // dscode: the telemetry segment carries the header and only appears once the
1022
- // terminal can seat it; below that the identity row keeps the model. Its
1023
- // slot is padded to the width it was laid out for, so the live figures
1024
- // decide the text the row shows without ever changing the row's geometry.
1041
+ // dscode: the live figures close row 2 as one left-hand cluster and only appear
1042
+ // once the terminal can seat them; row 1 names the model itself. The cluster is
1043
+ // laid out against the same width budget as before, and its own ladder decides
1044
+ // which figures fit.
1025
1045
  const telemetryWidth = Math.max(1, Math.min(columns - 8, Math.max(40, Math.floor(columns * 0.8) - 4) + FOOTER_FIGURE_RESERVE));
1046
+ // The figures belong to the provider serving the route (`provider/model`).
1047
+ const slash = facts.model.indexOf('/');
1048
+ const provider = slash > 0 ? facts.model.slice(0, slash) : 'deepseek-official';
1026
1049
  const telemetry = columns >= 48
1027
- ? dscodeFooterFor(facts.fullSessionId, stats, telemetryWidth, dscodeFooterHeader(facts, stats), getLanguage())
1050
+ ? dscodeFooterFor(facts.fullSessionId, stats, telemetryWidth, provider, getLanguage())
1028
1051
  : '';
1029
- facts = { ...facts, telemetry: telemetry === '' ? '' : padValue(telemetry, telemetryWidth) };
1052
+ facts = { ...facts, telemetry };
1030
1053
  // Flowing-theme busy flow: the identity cluster's live dot cycles the
1031
1054
  // anchor walk while a turn runs; static themes never start the timer.
1032
1055
  const flow = themeFlow();
@@ -1055,6 +1078,7 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1055
1078
  facts.goal?.phase,
1056
1079
  facts.goal?.rounds,
1057
1080
  facts.goal?.max,
1081
+ facts.skills,
1058
1082
  stats,
1059
1083
  busy,
1060
1084
  columns,
@@ -1075,16 +1099,13 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1075
1099
  if (groupIndex > 0) {
1076
1100
  leftParts.push(createElement(Text, { key: key + 'gs' + groupIndex, color: inkColor(getPalette().dim) }, STATUS_GROUP_SEPARATOR));
1077
1101
  }
1102
+ const telemetryGroup = group.id === 'telemetry';
1078
1103
  group.spans.forEach((span, spanIndex) => {
1079
- leftParts.push(createElement(Text, { key: key + 'g' + groupIndex + 's' + spanIndex, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, span.text));
1104
+ const spanKey = key + 'g' + groupIndex + 's' + spanIndex;
1105
+ leftParts.push(createElement(Text, { key: spanKey, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, telemetryGroup ? dscodeTelemetryNodes(span.text, spanKey) : span.text));
1080
1106
  });
1081
1107
  });
1082
1108
  const rightParts = [];
1083
- // dscode: row 2 joins its left figures to the right-pinned telemetry with a
1084
- // quieter rule than the cluster separator.
1085
- if (key === 's2' && row.left.length > 0 && row.right.length > 0) {
1086
- rightParts.push(createElement(Text, { key: key + 'divider', color: inkColor(getPalette().dim) }, '| '));
1087
- }
1088
1109
  // dscode: the cycle hint rides LEFT of the right cluster, so the
1089
1110
  // right-anchored badge holds its columns whether or not the hint is
1090
1111
  // painted; layoutStatusBar reserves the hint's width in both states.
@@ -1098,7 +1119,7 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1098
1119
  if (index > 0) {
1099
1120
  rightParts.push(createElement(Text, { key: key + 'rs' + index, color: inkColor(getPalette().dim) }, STATUS_ITEM_SEPARATOR));
1100
1121
  }
1101
- rightParts.push(createElement(Text, { key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, key === 's2' && index === 0 ? dscodeTelemetryNodes(span.text, key + 'r' + index) : span.text));
1122
+ rightParts.push(createElement(Text, { key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, span.text));
1102
1123
  });
1103
1124
  // Each row already fits the column budget; truncate-end stays as the
1104
1125
  // terminal-measurement backstop so a drifting cell count clips instead
@@ -1119,12 +1140,17 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1119
1140
  * above the composer.
1120
1141
  */
1121
1142
  function NoticeLine({ text, tone, columns }) {
1143
+ // dscode: `relay` is cross-session traffic. It takes the violet the interface
1144
+ // reserves for steered/queued interactive input plus its own glyph, so it is
1145
+ // never mistaken for the session's own tool or shell output.
1122
1146
  const color = tone === 'error'
1123
1147
  ? getPalette().error
1124
1148
  : tone === 'warning'
1125
1149
  ? getPalette().warn
1126
- : getPalette().brandBright;
1127
- const mark = tone === 'error' ? '⨯' : tone === 'warning' ? '!' : '•';
1150
+ : tone === 'relay'
1151
+ ? getPalette().steered
1152
+ : getPalette().brandBright;
1153
+ const mark = tone === 'error' ? '⨯' : tone === 'warning' ? '!' : tone === 'relay' ? '⇄' : '•';
1128
1154
  return createElement(Box, { paddingLeft: 2 }, createElement(Text, { color: inkColor(color), wrap: 'truncate-end' }, truncateColumns(`${mark} ${singleLineText(text)}`, Math.max(1, columns - 2))));
1129
1155
  }
1130
1156
  /** The fixed decision list; answers stay in the binary answerer vocabulary. */
@@ -1987,7 +2013,7 @@ export function DscodeEmailPanel({ columns, rows, pick, close, gmail, imap }) {
1987
2013
  const listWidth = wide ? Math.max(26, Math.floor(columns * 0.4)) : columns;
1988
2014
  const previewWidth = wide ? Math.max(1, columns - listWidth - 1) : columns;
1989
2015
  const clean = value => dscodeEmailText(value).replace(/\n/g, ' ');
1990
- const bodyLines = mail ? wrapText(dscodeEmailText(mail.body), Math.max(1, previewWidth - 2), 'wrap').split('\n') : [];
2016
+ const bodyLines = mail ? wrapText(dscodeEmailText(mail.body), Math.max(1, previewWidth - 2)) : [];
1991
2017
  useStableInput((input, key) => {
1992
2018
  if (setup)
1993
2019
  return;
@@ -2878,6 +2904,40 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
2878
2904
  return createElement(Text, { key: model.id, color: added ? inkColor(getPalette().dim) : active ? inkColor(getPalette().brandBright) : inkColor(getPalette().text), wrap: 'truncate-end' }, truncateColumns(label, viewport.contentColumns));
2879
2905
  }), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.footer'), viewport.contentColumns)));
2880
2906
  }
2907
+ /**
2908
+ * dscode: the budget gate. The runner checks the session's recorded spend
2909
+ * against `DSCODE_SESSION_BUDGET_USD` before it delivers a prompt and hands the
2910
+ * submission here instead of sending it; the user decides whether the turn
2911
+ * still runs. It deliberately mirrors the compaction confirmation (`y`/`n`/Esc)
2912
+ * so the two stops share one reflex, and it never sends the prompt itself — the
2913
+ * runner owns delivery, this panel only answers.
2914
+ */
2915
+ export function DscodeBudgetConfirmPanel({ decision, confirm, back }) {
2916
+ const stdout = useStdout().stdout;
2917
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
2918
+ useStableInput((input, key) => {
2919
+ if (key.escape || input === 'n') {
2920
+ back();
2921
+ return;
2922
+ }
2923
+ if (input === 'y')
2924
+ confirm();
2925
+ }, true);
2926
+ const palette = getPalette();
2927
+ const values = {
2928
+ spent: '$' + decision.spent.toFixed(2),
2929
+ limit: '$' + decision.limit.toFixed(2),
2930
+ percent: String(Math.round(decision.percent)) + '%',
2931
+ };
2932
+ const title = dscodeT('budget.confirm.title', values);
2933
+ const hint = dscodeT('budget.confirm.hint');
2934
+ if (viewport.maxHeight === 0 || viewport.compact)
2935
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(title + ' · ' + hint, viewport.contentColumns));
2936
+ const body = [dscodeT('budget.confirm.spent', values), dscodeT('budget.confirm.next', values)]
2937
+ .map((text, index) => createElement(Text, { key: 'body' + index, color: index === 0 ? void 0 : inkColor(palette.dim), wrap: 'truncate-end' }, truncateColumns(' ' + text, viewport.contentColumns)))
2938
+ .slice(0, viewport.bodyRows);
2939
+ return createElement(Box, { flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(palette.warn) }, createElement(Text, { color: inkColor(palette.warn), bold: true, wrap: 'truncate-end' }, truncateColumns(title, viewport.contentColumns)), createElement(PanelGap, { visible: viewport.gapRows > 0 }), ...body, createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Text, { color: inkColor(palette.dim), wrap: 'truncate-end' }, truncateColumns(hint, viewport.contentColumns)));
2940
+ }
2881
2941
  /** Bounded destructive-action confirmation for credential or provider removal. */
2882
2942
  export function DscodeCompactionConfirmPanel({ preview, confirm, back }) {
2883
2943
  const stdout = useStdout().stdout;
@@ -3993,8 +4053,8 @@ function Input({ effortSurface, ultraPulse, active, frozen, frozenHint, busy, de
3993
4053
  // path-like query keeps the upstream order entirely.
3994
4054
  let rankedMentionRows = visibleMentionRows;
3995
4055
  if (mentionToken !== undefined && !isPathLikeMentionQuery(mentionToken.query) && mentionToken.query !== '') {
3996
- const hits = rankByName(visibleMentionRows.map(row => ({ name: row.label.replace(/^@/u, ''), row })), mentionToken.query)
3997
- .map(entry => entry.row);
4056
+ const mentionMatches = visibleMentionRows.map(row => ({ name: row.label.replace(/^@/u, ''), row }));
4057
+ const hits = rankByName(mentionMatches, mentionToken.query).map(entry => entry.row);
3998
4058
  const hitSet = new Set(hits);
3999
4059
  rankedMentionRows = [...hits, ...visibleMentionRows.filter(row => !hitSet.has(row))];
4000
4060
  }
@@ -4329,7 +4389,7 @@ function Input({ effortSurface, ultraPulse, active, frozen, frozenHint, busy, de
4329
4389
  }
4330
4390
  if (liveValue.startsWith('!')) {
4331
4391
  // Dropping the bang shifts every character left: keep the caret on the same letter.
4332
- applyEdit({ value: liveValue.slice(1), cursor: Math.max(0, liveCursor - 1) });
4392
+ applyEdit({ value: liveValue.slice(1), cursor: Math.max(0, liveCursor - 1), killed: undefined });
4333
4393
  return;
4334
4394
  }
4335
4395
  if (hasNotice) {
@@ -5195,7 +5255,7 @@ export function computeSettledRows(previous, entries, settled, showReasoning, re
5195
5255
  if (record.before !== undefined)
5196
5256
  window.unshift(record.before);
5197
5257
  }
5198
- const header = createElement(Header, { key: 'header', ...headerFacts, resumed });
5258
+ const header = createElement(Header, { key: 'header', ...headerFacts });
5199
5259
  const flat = droppedEntries > 0
5200
5260
  ? [header, settledTrimHint(droppedEntries, columns), ...window]
5201
5261
  : [header, ...window];
@@ -5270,6 +5330,12 @@ export function App(props) {
5270
5330
  // dscode: the email inbox owns the surface while it is open; a picked message
5271
5331
  // steers into the running turn (or starts one) and closes the panel.
5272
5332
  const [emailOpen, setEmailOpen] = useState(false);
5333
+ /**
5334
+ * dscode: a submission the budget gate stopped. The runner parks it here
5335
+ * instead of delivering it; the panel's `y` hands the exact line back, and
5336
+ * `n`/Esc clears it without sending. Only one prompt waits at a time.
5337
+ */
5338
+ const [budgetPending, setBudgetPending] = useState(undefined);
5273
5339
  const gmail = useMemo(() => dscodeCreateGmailConnector(), []);
5274
5340
  const imap = useMemo(() => dscodeCreateImapConnector(), []);
5275
5341
  useEffect(() => {
@@ -5291,6 +5357,9 @@ export function App(props) {
5291
5357
  setEmailOpen(false);
5292
5358
  setBtwOpen(false);
5293
5359
  setBtwSelected(undefined);
5360
+ // A parked prompt belongs to the session that was over budget; carrying it
5361
+ // across a switch would deliver the old session's text into the new one.
5362
+ setBudgetPending(undefined);
5294
5363
  }, [props.sessionKey]);
5295
5364
  // The stores are closure-backed singletons whose methods never touch `this`,
5296
5365
  // but a bare method reference still detaches it from its receiver. One stable
@@ -5320,10 +5389,12 @@ export function App(props) {
5320
5389
  // process-stable, so one callback per view identity is enough.
5321
5390
  const readDescriptors = useCallback(() => props.commands.descriptors, [props.commands]);
5322
5391
  const readSkills = useCallback(() => props.skills.rows, [props.skills]);
5392
+ const readSkillCount = useCallback(() => props.skills.count, [props.skills]);
5323
5393
  const subscribeCommands = useCallback((listener) => props.commands.subscribe(listener), [props.commands]);
5324
5394
  const subscribeSkills = useCallback((listener) => props.skills.subscribe(listener), [props.skills]);
5325
5395
  const descriptors = useSyncExternalStore(subscribeCommands, readDescriptors);
5326
5396
  const skills = useSyncExternalStore(subscribeSkills, readSkills);
5397
+ const skillCount = useSyncExternalStore(subscribeSkills, readSkillCount);
5327
5398
  const [modelLabel, setModelLabel] = useState(props.model);
5328
5399
  const [modelOpen, setModelOpen] = useState(false);
5329
5400
  /** Nested /model stages; only one owns terminal input at a time. */
@@ -5419,8 +5490,8 @@ export function App(props) {
5419
5490
  // `/latest` dist-tag endpoint, which silently suppressed this notice.
5420
5491
  fetch(DSCODE_REGISTRY_URL, { headers: { accept: 'application/json' }, signal: controller.signal })
5421
5492
  .then(response => (response.ok ? response.json() : undefined))
5422
- .then(data => {
5423
- const latest = data?.version;
5493
+ .then((data) => {
5494
+ const latest = data !== null && typeof data === 'object' ? data.version : undefined;
5424
5495
  if (typeof latest === 'string' && dscodeNewerVersion(latest, DSCODE_VERSION))
5425
5496
  notify(t('update.available', { version: latest }), 'warning');
5426
5497
  })
@@ -5432,7 +5503,7 @@ export function App(props) {
5432
5503
  };
5433
5504
  }, []);
5434
5505
  useEffect(() => {
5435
- props.onBridgeReady({ notify });
5506
+ props.onBridgeReady({ notify, confirmBudget: setBudgetPending });
5436
5507
  }, []);
5437
5508
  useEffect(() => {
5438
5509
  if (!modelOpen)
@@ -5616,8 +5687,8 @@ export function App(props) {
5616
5687
  // panel keypress.
5617
5688
  const inputActive = deleteConfirmId !== undefined
5618
5689
  ? !approvalPending && !questionPending
5619
- : !emailOpen && !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5620
- const transcriptVisible = !btwOpen && !emailOpen && !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5690
+ : !emailOpen && !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && budgetPending === undefined && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5691
+ const transcriptVisible = !btwOpen && !emailOpen && !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && budgetPending === undefined && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5621
5692
  // Human questions outrank local inspectors. Close the lower modal instead
5622
5693
  // of leaving an approval/question visible but keyboard-locked behind it.
5623
5694
  useEffect(() => {
@@ -5774,10 +5845,15 @@ export function App(props) {
5774
5845
  // session title; cleared on unmount so the host shell regains its default.
5775
5846
  const tabTitle = view.title === '' ? DEFAULT_TERMINAL_TITLE : view.title;
5776
5847
  useTerminalTitle(tabTitle);
5848
+ // dscode: the live region is re-rendered for every event, but an entry that
5849
+ // did not change keeps its object identity, so its wrapped lines are reused
5850
+ // instead of re-flowed per frame. Only the streaming text (view.streaming)
5851
+ // changes between those frames, and it is rendered outside this list.
5852
+ const chatLines = useMemo(() => createChatLinesCache(), []);
5777
5853
  const allLiveLines = useMemo(() => view.entries.slice(settled).flatMap(
5778
5854
  // Width shrinks with the real terminal (no 10-column floor: on a
5779
5855
  // narrower terminal the floor silently overflowed every row).
5780
- entry => dscodeChatLines(entry, Math.max(1, terminalColumns - 2), showReasoning)), [view.entries, settled, terminalColumns, showReasoning]);
5856
+ entry => chatLines(entry, Math.max(1, terminalColumns - 2), showReasoning)), [view.entries, settled, terminalColumns, showReasoning, chatLines]);
5781
5857
  // Reserve the same stream slice from the moment a turn becomes busy. This
5782
5858
  // keeps the first thinking frame from changing the dynamic-tree geometry
5783
5859
  // underneath Ink's cursor ledger and avoids a start-of-thinking flash.
@@ -5820,7 +5896,7 @@ export function App(props) {
5820
5896
  const auditedReasoningRows = liveAudit.allocation.reasoning;
5821
5897
  const auditedAnswerRows = liveAudit.allocation.answer;
5822
5898
  const inspectorVisible = verboseOpen && !approvalPending && !questionPending;
5823
- const modalVisible = emailOpen || modelOpen || helpOpen && modeOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || languageOpen || historyOpen || queueOpen || agentsOpen || subagentOpen || todosOpen || usageOpen || inspectorVisible || diffView !== undefined || reviewPickerOpen || approvalPending || questionPending;
5899
+ const modalVisible = budgetPending !== undefined || emailOpen || modelOpen || helpOpen && modeOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || languageOpen || historyOpen || queueOpen || agentsOpen || subagentOpen || todosOpen || usageOpen || inspectorVisible || diffView !== undefined || reviewPickerOpen || approvalPending || questionPending;
5824
5900
  // The surface that currently owns the keyboard, named in the frozen band:
5825
5901
  // an empty composer under a panel must not advertise typing it cannot
5826
5902
  // accept — every key actually feeds the panel (which may or may not
@@ -6385,6 +6461,17 @@ export function App(props) {
6385
6461
  update: props.updateQueued,
6386
6462
  onClose: () => setQueueOpen(false),
6387
6463
  })
6464
+ : undefined, budgetPending !== undefined
6465
+ ? createElement(DscodeBudgetConfirmPanel, {
6466
+ decision: budgetPending.decision,
6467
+ confirm: () => {
6468
+ const parked = budgetPending;
6469
+ setBudgetPending(undefined);
6470
+ props.dscodeAcknowledgeBudget?.(props.sessionKey);
6471
+ props.dispatch(parked.text, parked.images, props.sessionKey);
6472
+ },
6473
+ back: () => setBudgetPending(undefined),
6474
+ })
6388
6475
  : undefined, createElement(QuestionBar, { store: props.questions, snapshot: questionSnapshot, locked: false }), createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending, notify, interrupt: props.interrupt, summarize: questionPending }), effortFor !== undefined ? undefined : modelSurface, helpOpen && !approvalPending && !questionPending
6389
6476
  ? createElement(HelpPanel, {
6390
6477
  descriptors,
@@ -6801,6 +6888,7 @@ export function App(props) {
6801
6888
  permission: view.permission !== '' ? view.permission : props.permission,
6802
6889
  sandbox: view.sandbox,
6803
6890
  goal: view.goal === undefined ? undefined : { phase: view.goal.phase, rounds: view.goal.rounds, max: view.goal.max },
6891
+ skills: skillCount,
6804
6892
  },
6805
6893
  stats: view.stats,
6806
6894
  busy,