@toddzheng024/dscode-bundle 0.7.21 → 0.7.23
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/package.json +4 -4
- package/plugins/dscode/index.mjs +10 -0
- package/plugins/i18n/messages.d.mts +21 -0
- package/plugins/i18n/messages.mjs +18 -6
- 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/tui-tools/workspace-discovery.mjs +11 -4
- package/presets/dscode/agent.cordis.yml +3 -2
- 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
|
@@ -4,7 +4,7 @@ import { estimateCost, peakEmoji } from './pricing.mjs';
|
|
|
4
4
|
import { balanceNow, trustedNow } from './balance.mjs';
|
|
5
5
|
import { grokSubscriptionNow } from '../grok/billing.mjs';
|
|
6
6
|
import { sessionAverageTps } from './rate.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { attributeCostByTurn, evaluateBudget, parseBudget } from './turns.mjs';
|
|
8
8
|
let source;
|
|
9
9
|
export function setMetricSource(next) { source = next; return () => { if (source === next) source = undefined; }; }
|
|
10
10
|
export function summarize(rows, events = [], corrupt = false) {
|
|
@@ -43,7 +43,29 @@ export function summarize(rows, events = [], corrupt = false) {
|
|
|
43
43
|
// OpenRouter reports cache reads only when there are some: a missing count is zero, not unknown.
|
|
44
44
|
input += total; hit += u.cacheReadTokens ?? 0;
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
// Per-turn attribution needs the ledger's own rows (the backfilled history has
|
|
47
|
+
// no turn boundary), so it reads them directly instead of the merged map.
|
|
48
|
+
const attribution = attributeCostByTurn(rows, events);
|
|
49
|
+
return {
|
|
50
|
+
cost, unknown, calls, pending,
|
|
51
|
+
cache: input > 0 && !cacheUnknown ? Math.min(100, hit / input * 100) : null,
|
|
52
|
+
turns: attribution.turns,
|
|
53
|
+
lastTurn: attribution.lastTurn,
|
|
54
|
+
unattributed: attribution.unattributed,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Per-turn cost for one live session, straight from its ledger, keyed by the
|
|
60
|
+
* durable turn number. The /usage panel prices its turns with this: the token
|
|
61
|
+
* meter says what each turn billed, this says what it cost.
|
|
62
|
+
* @param id - the session id.
|
|
63
|
+
* @param events - the session's durable events, for the turn windows.
|
|
64
|
+
* @returns `{ turn, cost, calls, unknown }` per completed or running turn.
|
|
65
|
+
*/
|
|
66
|
+
export function turnCostsFor(id, events = []) {
|
|
67
|
+
const entries = id && process.env.DSH_HOME ? readMetrics(process.env.DSH_HOME, id).rows : [];
|
|
68
|
+
return attributeCostByTurn(entries, events).turns;
|
|
47
69
|
}
|
|
48
70
|
/**
|
|
49
71
|
* Terminal columns of a string: East Asian wide characters (such as the cache label's
|
|
@@ -113,50 +135,77 @@ function figure(text, width) {
|
|
|
113
135
|
return padding > 0 ? ' '.repeat(padding) + text : text;
|
|
114
136
|
}
|
|
115
137
|
|
|
116
|
-
export function formatFooter(metrics, context, columns = 80, rates, locale = 'en',
|
|
138
|
+
export function formatFooter(metrics, context, columns = 80, rates, locale = 'en', provider = 'deepseek-official') {
|
|
117
139
|
const label = key => t(locale, key);
|
|
118
140
|
const ctx = figure(Number.isFinite(context) ? `${Math.round(context)}%` : '--', FIGURE.percent);
|
|
119
141
|
const cache = figure(metrics.cache === null ? '--' : `${metrics.cache.toFixed(1)}%`, FIGURE.share);
|
|
120
|
-
// The balance belongs to the provider the
|
|
142
|
+
// The balance belongs to the provider serving the route; only DeepSeek's official one bills by a peak window.
|
|
121
143
|
const balance = balanceNow(provider);
|
|
122
144
|
const spend = metrics.unknown && metrics.cost === 0 ? '--' : `$${metrics.cost.toFixed(2)}${metrics.unknown ? '+' : ''}${metrics.pending ? '…' : ''}`;
|
|
123
|
-
// The money
|
|
124
|
-
// column a handful of times per session, and reserving
|
|
125
|
-
//
|
|
145
|
+
// The money is the one live figure whose digits are not reserved: they cross a
|
|
146
|
+
// column a handful of times per session, and reserving them would evict a
|
|
147
|
+
// per-second figure at the widths the footer actually runs at. A balance the
|
|
148
|
+
// provider cannot report is left out instead of parked as `$--`.
|
|
126
149
|
const dollars = provider === 'grok' ? grokFooterFact(grokSubscriptionNow(), locale)
|
|
127
|
-
:
|
|
150
|
+
: spend + (balance === null ? '' : ' / $' + balance.toFixed(2)) + (provider === 'deepseek-official' ? ' ' + peakEmoji(trustedNow()) : '');
|
|
151
|
+
// The budget slot rides the money figure: it answers "how much of the session's
|
|
152
|
+
// limit is spent", which is a fact about the cost, not a second cost. A session
|
|
153
|
+
// with no limit renders exactly as before, so the footer only grows by choice.
|
|
154
|
+
const budget = metrics.budget;
|
|
155
|
+
const budgeted = budget === undefined || budget.limit === null ? ''
|
|
156
|
+
: `/${budget.limit.toFixed(2)}${budget.state === 'over' ? '⚠!' : budget.state === 'warn' ? '⚠' : ''}`;
|
|
157
|
+
const money = dollars + budgeted;
|
|
158
|
+
// The last completed turn is the one figure the whole-session total cannot
|
|
159
|
+
// give: `$1.23 / $9.86 · #12 $0.04` reads "the session so far, of which the
|
|
160
|
+
// last turn cost this". `+` marks a turn whose settled calls were not all
|
|
161
|
+
// priceable, the same mark the total uses.
|
|
162
|
+
const turn = metrics.lastTurn === undefined || !Number.isFinite(metrics.lastTurn.cost) ? ''
|
|
163
|
+
: `#${metrics.lastTurn.turn} $${metrics.lastTurn.cost.toFixed(2)}${metrics.lastTurn.unknown ? '+' : ''}`;
|
|
164
|
+
// Every figure reads value first and carries its own short qualifier, so the
|
|
165
|
+
// cluster stays scannable without a `label:` prefix in front of each number.
|
|
128
166
|
const base = rates ? [
|
|
129
|
-
`${
|
|
130
|
-
`${
|
|
131
|
-
`${label('footer.context')}
|
|
132
|
-
] : [`${label('footer.context')}
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
.filter((part, index) => part !== '' && !omit.has(index))
|
|
144
|
-
.join(' | ');
|
|
167
|
+
`${figure(Number.isFinite(rates.current) ? '~' + rates.current.toFixed(1) : '--', FIGURE.rate)} tps`,
|
|
168
|
+
`${figure(Number.isFinite(rates.average) ? rates.average.toFixed(1) : '--', FIGURE.rate)} tps ${label('footer.average')}`,
|
|
169
|
+
`${ctx} ${label('footer.context')}`, money, `${cache} ${label('footer.cache')}`,
|
|
170
|
+
] : [`${ctx} ${label('footer.context')}`, money, `${cache} ${label('footer.cache')}`];
|
|
171
|
+
// The slot is reserved even when no turn has cost anything yet: the drop
|
|
172
|
+
// ladder's indices are fixed against this array, and an unfilled slot renders
|
|
173
|
+
// as '' (skipped by `render`) exactly like the absent figure would.
|
|
174
|
+
base.splice(rates ? 3 : 1, 0, turn);
|
|
175
|
+
// Narrow terminals shed the quietest figures first: the last turn, then
|
|
176
|
+
// average, current and context, and only then the cache. The running cost is
|
|
177
|
+
// the last thing standing, and its slot is index 4 (or 2 without rates) now
|
|
178
|
+
// that the turn slot holds index 3 (or 1) open.
|
|
179
|
+
const drops = rates ? [3, 1, 0, 2, 5, 4] : [1, 0, 3, 2];
|
|
180
|
+
const render = omit => base.filter((part, index) => part !== '' && !omit.has(index)).join(' · ');
|
|
145
181
|
for (let dropped = 0; dropped <= drops.length; dropped++) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
const value = render({ omit, head });
|
|
149
|
-
if (displayWidth(value) <= columns) return value;
|
|
150
|
-
}
|
|
182
|
+
const value = render(new Set(drops.slice(0, dropped)));
|
|
183
|
+
if (displayWidth(value) <= columns) return value;
|
|
151
184
|
}
|
|
152
|
-
const floor = render(
|
|
185
|
+
const floor = render(new Set(drops));
|
|
153
186
|
let clipped = '';
|
|
154
187
|
for (const char of floor) { if (displayWidth(clipped + char) > columns) break; clipped += char; }
|
|
155
188
|
return clipped;
|
|
156
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* The session's recorded spend and whether any of it is unpriceable, for a host
|
|
192
|
+
* that must decide something (the budget gate) rather than only render it. Same
|
|
193
|
+
* ledger and same rules as {@link footerFor}, so the figure the gate compares
|
|
194
|
+
* and the figure the footer shows can never disagree.
|
|
195
|
+
*/
|
|
196
|
+
export function sessionSpend(id) {
|
|
197
|
+
try {
|
|
198
|
+
const ledger = id && process.env.DSH_HOME ? readMetrics(process.env.DSH_HOME, id) : { rows: [], corrupt: false };
|
|
199
|
+
const data = id ? source?.(id) : undefined;
|
|
200
|
+
const summary = summarize(ledger.rows, data?.events ?? [], ledger.corrupt);
|
|
201
|
+
return { cost: summary.cost, unknown: summary.unknown, pending: summary.pending };
|
|
202
|
+
} catch { return { cost: 0, unknown: true, pending: 0 }; }
|
|
203
|
+
}
|
|
204
|
+
|
|
157
205
|
/** Per-events memo: the status line renders up to once a second, and summarize/average are O(events). */
|
|
158
206
|
const footerCache = new WeakMap();
|
|
159
|
-
export function footerFor(id, stats, columns,
|
|
207
|
+
export function footerFor(id, stats, columns, provider = 'deepseek-official', locale = 'en') {
|
|
208
|
+
const limit = parseBudget(process.env.DSCODE_SESSION_BUDGET_USD);
|
|
160
209
|
try {
|
|
161
210
|
const data = id ? source?.(id) : undefined;
|
|
162
211
|
const ledger = id && process.env.DSH_HOME ? readMetrics(process.env.DSH_HOME, id) : { rows: [], corrupt: false };
|
|
@@ -170,6 +219,9 @@ export function footerFor(id, stats, columns, header = '', locale = 'en') {
|
|
|
170
219
|
const capacity = data?.capacity ?? stats.contextWindow;
|
|
171
220
|
const average = fresh ? hit.average : sessionAverageTps(events);
|
|
172
221
|
if (events.length > 0 && !fresh) footerCache.set(events, { key: ledger.rows, length: events.length, tail, summary, average });
|
|
173
|
-
|
|
174
|
-
|
|
222
|
+
// The budget comes from the environment for this process only: it is a
|
|
223
|
+
// per-machine spending guard, not a session property worth persisting.
|
|
224
|
+
const budget = limit === null ? undefined : evaluateBudget(summary.cost, limit);
|
|
225
|
+
return formatFooter({ ...summary, ...(budget === undefined ? {} : { budget }) }, Number.isFinite(used) && capacity > 0 ? used / capacity * 100 : undefined, columns, { current: data?.currentTps, average }, locale, provider);
|
|
226
|
+
} catch { return formatFooter({ cost: 0, unknown: true, cache: null }, undefined, columns, { current: null, average: null }, locale, provider); }
|
|
175
227
|
}
|
|
@@ -10,11 +10,15 @@ export const instructionFileCandidates = Object.freeze(['AGENTS.md', 'CLAUDE.md'
|
|
|
10
10
|
export const projectRootMarkers = Object.freeze(['.git']);
|
|
11
11
|
|
|
12
12
|
export const ENABLED_VALUES = Object.freeze(['1', 'true', 'on', 'yes']);
|
|
13
|
-
// Fail closed: only the documented enable spellings
|
|
14
|
-
// writing "no", "none" or "disable" cannot accidentally
|
|
15
|
-
//
|
|
13
|
+
// Fail closed: only the documented enable spellings read as on, so an operator
|
|
14
|
+
// writing "no", "none" or "disable" cannot accidentally arm a feature that reads
|
|
15
|
+
// untrusted project content.
|
|
16
16
|
export const enabledFlag = (value) => ENABLED_VALUES.includes(String(value ?? '').trim().toLowerCase());
|
|
17
|
-
|
|
17
|
+
// Ancestor skill discovery defaults to on, so its switch is the inverse: only an
|
|
18
|
+
// explicit off spelling disables it and an unrecognised value keeps the default.
|
|
19
|
+
export const DISABLED_VALUES = Object.freeze(['0', 'false', 'off', 'no', 'none', 'disable']);
|
|
20
|
+
export const disabledFlag = (value) => DISABLED_VALUES.includes(String(value ?? '').trim().toLowerCase());
|
|
21
|
+
export const skillAncestorsEnabled = (env = process.env) => !disabledFlag(env.DSCODE_SKILL_ANCESTORS);
|
|
18
22
|
|
|
19
23
|
// A symlinked $HOME (or a /Volumes mount) must not break the boundary test: the
|
|
20
24
|
// launcher passes an already-resolved session directory while os.homedir() returns
|
|
@@ -52,6 +56,9 @@ export function projectRootOf({ cwd, markers = projectRootMarkers }) {
|
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// Nearest first, which is also precedence: the provider scans `customSkillDirs` in
|
|
60
|
+
// order and the first definition of a duplicate name wins. The provider's own rank
|
|
61
|
+
// 100/200 roots at the project root still outrank everything registered here.
|
|
55
62
|
export function ancestorSkillDirs({ cwd, home, env = process.env }) {
|
|
56
63
|
if (!skillAncestorsEnabled(env)) return [];
|
|
57
64
|
const projectRoot = projectRootOf({ cwd });
|
|
@@ -135,8 +135,9 @@
|
|
|
135
135
|
- id: skill-filesystem
|
|
136
136
|
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
137
137
|
config:
|
|
138
|
-
#
|
|
139
|
-
#
|
|
138
|
+
# Ancestor skill discovery is on by default: the skill roots of directories
|
|
139
|
+
# between the working directory and home register at rank 300, nearest first,
|
|
140
|
+
# so headers enter the catalog and bodies stay lazy. =0 turns it off.
|
|
140
141
|
customSkillDirs: !!js 'JSON.parse(process.env.DSCODE_SKILL_ANCESTOR_DIRS ?? "[]")'
|
|
141
142
|
|
|
142
143
|
- id: tool-skill
|
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,
|