@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.
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/cordis.patch.yml +2 -0
- package/package.json +4 -2
- package/plugins/dscode/index.mjs +1 -1
- package/plugins/i18n/messages.d.mts +21 -0
- package/plugins/i18n/messages.mjs +18 -6
- package/plugins/openrouter/adapter.mjs +23 -2
- package/plugins/openrouter/wire.mjs +2 -1
- package/plugins/session-bridge/index.mjs +6 -0
- package/plugins/session-bridge/tasks.mjs +227 -0
- package/plugins/session-metrics/turns.mjs +134 -0
- package/plugins/session-metrics/view.mjs +85 -33
- package/plugins/triggers/cli.mjs +439 -0
- package/plugins/triggers/config.mjs +245 -0
- package/plugins/triggers/host.mjs +138 -0
- package/plugins/triggers/index.mjs +57 -0
- package/plugins/triggers/launchd.mjs +101 -0
- package/plugins/triggers/log.mjs +104 -0
- package/plugins/triggers/options.mjs +109 -0
- package/plugins/triggers/overlay.mjs +7 -0
- package/plugins/triggers/poll.mjs +57 -0
- package/plugins/triggers/run.mjs +156 -0
- package/plugins/triggers/spool.mjs +128 -0
- package/plugins/tui-tools/workspace-discovery.mjs +11 -4
- package/presets/dscode/agent.cordis.yml +4 -3
- package/vendor/command-goal/LICENSE +21 -0
- package/vendor/command-goal/index.js +208 -0
- package/vendor/command-goal/types/index.d.ts +10 -0
- package/vendor/tui/lib/app.mjs +124 -36
- package/vendor/tui/lib/communication.mjs +262 -0
- package/vendor/tui/lib/dscode/chat.mjs +34 -0
- package/vendor/tui/lib/dscode/model-search.mjs +2 -2
- package/vendor/tui/lib/dscode/palette.mjs +100 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +18 -11
- package/vendor/tui/lib/index.mjs +140 -6
- package/vendor/tui/lib/locales/en.mjs +3 -0
- package/vendor/tui/lib/locales/zh.mjs +3 -0
- package/vendor/tui/lib/mentions.mjs +1 -1
- package/vendor/tui/lib/render/status.mjs +56 -35
- package/vendor/tui/lib/render/text.mjs +33 -0
- package/vendor/tui/lib/render/usage.mjs +11 -2
- package/vendor/tui/lib/session-directory.mjs +25 -0
- 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
|
package/vendor/tui/lib/app.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
389
|
-
|
|
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
|
|
1022
|
-
// terminal can seat
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
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,
|
|
1050
|
+
? dscodeFooterFor(facts.fullSessionId, stats, telemetryWidth, provider, getLanguage())
|
|
1028
1051
|
: '';
|
|
1029
|
-
facts = { ...facts, telemetry
|
|
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
|
-
|
|
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) },
|
|
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
|
-
:
|
|
1127
|
-
|
|
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)
|
|
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
|
|
3997
|
-
|
|
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
|
|
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
|
|
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 =>
|
|
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,
|