dsh-ssh-tui 0.3.6 → 0.3.8
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/README.en.md +34 -18
- package/README.md +23 -8
- package/cordis.patch.yml +17 -3
- package/lib/index.js +58 -8
- package/lib/index.js.map +1 -1
- package/lib/route-memory.js +23 -4
- package/lib/route-memory.js.map +1 -1
- package/lib/subagent-model.js +37 -12
- package/lib/subagent-model.js.map +1 -1
- package/lib/supergrok-token.js +128 -0
- package/lib/supergrok-token.js.map +1 -0
- package/lib/tui.js +253 -105
- package/lib/tui.js.map +1 -1
- package/lib/types/route-memory.d.ts +10 -0
- package/lib/types/subagent-model.d.ts +4 -2
- package/lib/types/supergrok-token.d.ts +22 -0
- package/lib/types/tui.d.ts +24 -8
- package/package.json +29 -29
package/lib/tui.js
CHANGED
|
@@ -25,9 +25,30 @@ import { formatSessionTime, listResumableSessions } from './session-list.js';
|
|
|
25
25
|
import { defaultReasoningEffort } from './reasoning.js';
|
|
26
26
|
import { checkForPluginUpdate } from './update-check.js';
|
|
27
27
|
import { ROUTE_MEMORY_NAMESPACE, parseRouteMemory, rememberedRouteFor, upsertRememberedRoute, } from './route-memory.js';
|
|
28
|
+
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model';
|
|
28
29
|
import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, subagentModelMatchesProvider, subagentSettingsValue, } from './subagent-model.js';
|
|
29
|
-
|
|
30
|
+
import { resolveFreshSuperGrokToken } from './supergrok-token.js';
|
|
30
31
|
import { UserQuestionError, } from '@deepseek-ai/dsh-user-questions';
|
|
32
|
+
function discoverProviderModels(llm, request, signal) {
|
|
33
|
+
return llm.discoverModels(settingsNamespace('llm-pi-ai'), { ...request, signal }, signal);
|
|
34
|
+
}
|
|
35
|
+
function installUserQuestionAnswerer(ctx, questions, ask) {
|
|
36
|
+
const provider = questions;
|
|
37
|
+
if (typeof provider.registerProvider === 'function') {
|
|
38
|
+
return provider.registerProvider({ ask });
|
|
39
|
+
}
|
|
40
|
+
return ctx.on('user-questions/request', async (request, next) => {
|
|
41
|
+
try {
|
|
42
|
+
return await ask(request);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof UserQuestionError && error.code === 'ASK_ABORTED')
|
|
46
|
+
throw error;
|
|
47
|
+
return await next();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const ROUTE_MEMORY_NS = ROUTE_MEMORY_NAMESPACE;
|
|
31
52
|
const PROVIDER_TEMPLATES = {
|
|
32
53
|
official: {
|
|
33
54
|
label: 'DeepSeek 官方',
|
|
@@ -492,7 +513,7 @@ const LOCAL_COMMANDS = [
|
|
|
492
513
|
{ name: 'provider', description: 'switch provider, then model and reasoning effort' },
|
|
493
514
|
{ name: 'submodel', description: `select subagent model (default ${DEFAULT_SUBAGENT_MODEL}, same provider as parent)` },
|
|
494
515
|
{ name: 'subeffort', description: 'select subagent reasoning effort (default follows provider)' },
|
|
495
|
-
{ name: 'mode', description: 'switch agent mode / preset (standard, minimal,
|
|
516
|
+
{ name: 'mode', description: 'switch agent mode / preset (standard, minimal, ptc, cordis, routing-suite, ...)' },
|
|
496
517
|
{ name: 'quit', description: 'exit the TUI' },
|
|
497
518
|
{ name: 'exit', description: 'exit the TUI' },
|
|
498
519
|
{ name: 'clear', description: 'clear the transcript view' },
|
|
@@ -684,6 +705,62 @@ function wrap(text, width) {
|
|
|
684
705
|
}
|
|
685
706
|
return lines;
|
|
686
707
|
}
|
|
708
|
+
/** Wrap plain text and report each output line's char range in the source. */
|
|
709
|
+
function wrapTracked(text, width) {
|
|
710
|
+
const limit = Math.max(1, width);
|
|
711
|
+
const out = [];
|
|
712
|
+
let base = 0;
|
|
713
|
+
for (const sourceLine of text.split('\n')) {
|
|
714
|
+
if (sourceLine === '') {
|
|
715
|
+
out.push({ line: '', start: base, end: base });
|
|
716
|
+
base += 1;
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
let rest = sourceLine;
|
|
720
|
+
let cursor = base;
|
|
721
|
+
while (displayWidth(rest) > limit) {
|
|
722
|
+
let cut = 0;
|
|
723
|
+
let used = 0;
|
|
724
|
+
for (const char of rest) {
|
|
725
|
+
const charWidth = displayWidth(char);
|
|
726
|
+
if (charWidth > 0 && used + charWidth > limit)
|
|
727
|
+
break;
|
|
728
|
+
used += charWidth;
|
|
729
|
+
cut += char.length;
|
|
730
|
+
}
|
|
731
|
+
if (cut === 0)
|
|
732
|
+
cut = firstCodePointLength(rest);
|
|
733
|
+
out.push({ line: rest.slice(0, cut), start: cursor, end: cursor + cut });
|
|
734
|
+
rest = rest.slice(cut);
|
|
735
|
+
cursor += cut;
|
|
736
|
+
}
|
|
737
|
+
out.push({ line: rest, start: cursor, end: cursor + rest.length });
|
|
738
|
+
base += sourceLine.length + 1;
|
|
739
|
+
}
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
/** Paint one already-wrapped output line by the segments overlapping its range. */
|
|
743
|
+
function paintSegmentedLine(line, start, end, segments) {
|
|
744
|
+
if (segments.length === 0)
|
|
745
|
+
return line;
|
|
746
|
+
let out = '';
|
|
747
|
+
for (const seg of segments) {
|
|
748
|
+
if (seg.end <= start)
|
|
749
|
+
continue;
|
|
750
|
+
if (seg.start >= end)
|
|
751
|
+
break;
|
|
752
|
+
const from = Math.max(seg.start, start);
|
|
753
|
+
const to = Math.min(seg.end, end);
|
|
754
|
+
if (to <= from)
|
|
755
|
+
continue;
|
|
756
|
+
out += `\x1b[${seg.sgr}m${line.slice(from - start, to - start)}\x1b[0m`;
|
|
757
|
+
}
|
|
758
|
+
return out === '' ? line : out;
|
|
759
|
+
}
|
|
760
|
+
/** Wrap `text` and color each output line by overlapping `segments`. */
|
|
761
|
+
function wrapSegmented(text, width, segments) {
|
|
762
|
+
return wrapTracked(text, width).map(({ line, start, end }) => paintSegmentedLine(line, start, end, segments));
|
|
763
|
+
}
|
|
687
764
|
function truncate(text, maxLines) {
|
|
688
765
|
const lines = text.split('\n');
|
|
689
766
|
if (maxLines <= 0)
|
|
@@ -1197,9 +1274,14 @@ export function remainingPercentFromUsed(usedPercent) {
|
|
|
1197
1274
|
return 100;
|
|
1198
1275
|
return Math.max(0, Math.min(100, Math.round((100 - usedPercent) * 10) / 10));
|
|
1199
1276
|
}
|
|
1200
|
-
/** Cross a remaining-percent threshold from above (50 / 25 / 10 / 5).
|
|
1277
|
+
/** Cross a remaining-percent threshold from above (50 / 25 / 10 / 5).
|
|
1278
|
+
* Only the tightest (lowest) crossed threshold is returned, so one drop
|
|
1279
|
+
* never paints 50/25/10 as three identical warnings. */
|
|
1201
1280
|
export function crossedQuotaThresholds(previousRemaining, remaining) {
|
|
1202
|
-
|
|
1281
|
+
const crossed = QUOTA_ALERT_THRESHOLDS.filter(threshold => remaining <= threshold && (previousRemaining === undefined || previousRemaining > threshold));
|
|
1282
|
+
if (crossed.length === 0)
|
|
1283
|
+
return [];
|
|
1284
|
+
return [crossed[crossed.length - 1]];
|
|
1203
1285
|
}
|
|
1204
1286
|
export function quotaAlertText(snapshot, window) {
|
|
1205
1287
|
const reset = window.resetsAt === undefined ? '' : `(${formatQuotaReset(window.resetsAt)})`;
|
|
@@ -1207,11 +1289,13 @@ export function quotaAlertText(snapshot, window) {
|
|
|
1207
1289
|
}
|
|
1208
1290
|
/**
|
|
1209
1291
|
* How often to re-fetch quota, based on the tightest window.
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1292
|
+
* Counted in model steps (not conversation turns): a turn with several
|
|
1293
|
+
* tool/LLM steps should refresh more often because it spends more quota.
|
|
1294
|
+
* Hourly/5h: every 10 steps, every 4 when near a threshold.
|
|
1295
|
+
* Weekly: every 50 steps, every 10 when near.
|
|
1296
|
+
* Monthly: every 80 steps, every 20 when near.
|
|
1213
1297
|
*/
|
|
1214
|
-
export function
|
|
1298
|
+
export function quotaRefreshEverySteps(window) {
|
|
1215
1299
|
if (window === undefined)
|
|
1216
1300
|
return 10;
|
|
1217
1301
|
const near = window.remainingPercent <= QUOTA_NEAR_THRESHOLD_PERCENT;
|
|
@@ -1223,6 +1307,8 @@ export function quotaRefreshEveryTurns(window) {
|
|
|
1223
1307
|
return near ? 20 : 80;
|
|
1224
1308
|
return near ? 10 : 50;
|
|
1225
1309
|
}
|
|
1310
|
+
/** @deprecated Same cadence as {@link quotaRefreshEverySteps}; the name predates step accounting. */
|
|
1311
|
+
export const quotaRefreshEveryTurns = quotaRefreshEverySteps;
|
|
1226
1312
|
function quotaPeriodLabel(period) {
|
|
1227
1313
|
if (period === 'hourly')
|
|
1228
1314
|
return '5 小时';
|
|
@@ -1588,6 +1674,24 @@ function friendlyArgsSummary(name, args) {
|
|
|
1588
1674
|
const SHELL_TOOL_NAMES = new Set(['bash', 'pwsh']);
|
|
1589
1675
|
const DIFF_TOOL_NAMES = new Set(['edit', 'write', 'str_replace_editor']);
|
|
1590
1676
|
const SUBAGENT_TOOL_NAMES = new Set(['subagent', 'subagent_fork', 'task']);
|
|
1677
|
+
/** Localized card titles for tool names without a dedicated branch. */
|
|
1678
|
+
const TOOL_TITLE_MAP = {
|
|
1679
|
+
edit: '编辑',
|
|
1680
|
+
write: '写入',
|
|
1681
|
+
str_replace_editor: '替换',
|
|
1682
|
+
fetch: '抓取网页',
|
|
1683
|
+
list_files: '列出文件',
|
|
1684
|
+
list: '列出文件',
|
|
1685
|
+
ls: '列出文件',
|
|
1686
|
+
find: '搜索文件',
|
|
1687
|
+
search: '网页搜索',
|
|
1688
|
+
delete: '删除文件',
|
|
1689
|
+
rm: '删除文件',
|
|
1690
|
+
rename: '重命名文件',
|
|
1691
|
+
mv: '重命名文件',
|
|
1692
|
+
mkdir: '创建目录',
|
|
1693
|
+
skills: '技能',
|
|
1694
|
+
};
|
|
1591
1695
|
const MAX_SUBAGENT_LOGS = 80;
|
|
1592
1696
|
const TODO_STATUS_MARK = {
|
|
1593
1697
|
pending: '○',
|
|
@@ -1926,7 +2030,7 @@ export function presentToolCall(name, args) {
|
|
|
1926
2030
|
const diff = diffHunksFromArgs(name, args);
|
|
1927
2031
|
const path = diff?.[0]?.path;
|
|
1928
2032
|
return {
|
|
1929
|
-
title: name,
|
|
2033
|
+
title: TOOL_TITLE_MAP[name] ?? name,
|
|
1930
2034
|
summary: path ?? friendlyArgsSummary(name, args),
|
|
1931
2035
|
...diff === null || diff === undefined ? {} : { diff },
|
|
1932
2036
|
};
|
|
@@ -1974,7 +2078,7 @@ export function presentToolCall(name, args) {
|
|
|
1974
2078
|
const url = typeof parsed?.url === 'string' ? parsed.url : '';
|
|
1975
2079
|
return { title: '抓取网页', summary: url || friendlyArgsSummary(name, args) };
|
|
1976
2080
|
}
|
|
1977
|
-
return { title: name, summary: friendlyArgsSummary(name, args) };
|
|
2081
|
+
return { title: TOOL_TITLE_MAP[name] ?? name, summary: friendlyArgsSummary(name, args) };
|
|
1978
2082
|
}
|
|
1979
2083
|
/** Validate a tool/result meta payload's structured diff, mirroring the web card. */
|
|
1980
2084
|
export function diffMetaDiffs(meta) {
|
|
@@ -2353,6 +2457,7 @@ export class SshTui {
|
|
|
2353
2457
|
lastPaintWidth = 0;
|
|
2354
2458
|
lastPaintHeight = 0;
|
|
2355
2459
|
lastChromeStart = 0;
|
|
2460
|
+
lastTranscriptStart = -1;
|
|
2356
2461
|
paintIntervalMs;
|
|
2357
2462
|
paintLink = 'local';
|
|
2358
2463
|
paintProbed = false;
|
|
@@ -2361,7 +2466,7 @@ export class SshTui {
|
|
|
2361
2466
|
llmRetry;
|
|
2362
2467
|
quotaSnapshot;
|
|
2363
2468
|
quotaAlerted = new Set();
|
|
2364
|
-
|
|
2469
|
+
quotaStepsSinceRefresh = 0;
|
|
2365
2470
|
quotaRefreshInFlight = false;
|
|
2366
2471
|
searchHits = [];
|
|
2367
2472
|
searchIndex = -1;
|
|
@@ -2405,7 +2510,7 @@ export class SshTui {
|
|
|
2405
2510
|
this.disposers.push(this.ctx.on('session/event', this.handleSessionEvent), this.ctx.on('agent/status', this.handleStatus), this.ctx.on('agent/error', this.handleError), this.ctx.on('agent/disposed', this.handleDisposed), this.ctx.on('agent/inbox/claimed', this.handleInboxClaimed), this.ctx.on('agent/inbox/discarded', this.handleInboxDiscarded), this.ctx.on('agent/request', this.handleAgentRequest), this.ctx.on('subagent/start', this.handleSubagentStart), this.ctx.on('subagent/end', this.handleSubagentEnd), this.ctx.on('approval/request', this.handleApproval));
|
|
2406
2511
|
const questions = this.ctx.get('userQuestions');
|
|
2407
2512
|
if (questions !== undefined) {
|
|
2408
|
-
this.userQuestionDisposer =
|
|
2513
|
+
this.userQuestionDisposer = installUserQuestionAnswerer(this.ctx, questions, this.handleUserQuestions);
|
|
2409
2514
|
}
|
|
2410
2515
|
this.write(`${this.useAlternateScreen ? '\x1b[?1049h' : ''}\x1b[?1000h\x1b[?1006h\x1b[?2004h\x1b[?25l`);
|
|
2411
2516
|
this.render();
|
|
@@ -3068,22 +3173,11 @@ export class SshTui {
|
|
|
3068
3173
|
if (row.kind === 'tool') {
|
|
3069
3174
|
const running = row.status === undefined || row.status === 'running';
|
|
3070
3175
|
const ok = row.status === 'ok';
|
|
3071
|
-
//
|
|
3072
|
-
//
|
|
3073
|
-
|
|
3074
|
-
const dotColor = !this.color ? undefined : running ? '33' : ok ? '32' : '31';
|
|
3075
|
-
const styleToolHeader = (line) => {
|
|
3076
|
-
const safe = sanitizeTerminalText(line);
|
|
3077
|
-
if (!this.color)
|
|
3078
|
-
return safe;
|
|
3079
|
-
const dotIndex = safe.indexOf('●');
|
|
3080
|
-
if (dotColor === undefined || dotIndex === -1)
|
|
3081
|
-
return this.styleLine('tool', safe);
|
|
3082
|
-
return `\x1b[33m${safe.slice(0, dotIndex)}\x1b[${dotColor}m●\x1b[33m${safe.slice(dotIndex + 1)}\x1b[0m`;
|
|
3083
|
-
};
|
|
3176
|
+
// Header text follows the execution state; the shell command itself
|
|
3177
|
+
// stays dim so it reads like a command, not a status.
|
|
3178
|
+
const stateCode = !this.color ? undefined : running ? '33' : ok ? '32' : '31';
|
|
3084
3179
|
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3085
3180
|
const state = running ? 'running…' : ok ? 'ok' : 'error';
|
|
3086
|
-
const summary = row.summary === '' ? '' : ` ${row.summary}`;
|
|
3087
3181
|
const exit = !running && row.command !== undefined
|
|
3088
3182
|
? row.signal !== undefined
|
|
3089
3183
|
? ` [信号 ${row.signal}]`
|
|
@@ -3093,22 +3187,49 @@ export class SshTui {
|
|
|
3093
3187
|
: '';
|
|
3094
3188
|
const focused = this.focusedRow === row;
|
|
3095
3189
|
const marker = row.expanded ? '▾' : '▸';
|
|
3096
|
-
const
|
|
3190
|
+
const lead = `${focused ? '▶ ' : ' '}${marker} ● ${row.title}`;
|
|
3191
|
+
// Summary carries the operand (command / path / pattern), not a status:
|
|
3192
|
+
// keep it dim so the title stays the colored, readable part.
|
|
3193
|
+
const summaryText = row.summary === '' ? '' : ` ${row.summary}`;
|
|
3194
|
+
const tail = ` [${state}]${exit}${spinner}`;
|
|
3195
|
+
const plainHeader = `${lead}${summaryText}${tail}`;
|
|
3196
|
+
const headerSegments = stateCode === undefined
|
|
3197
|
+
? []
|
|
3198
|
+
: [
|
|
3199
|
+
{ start: 0, end: lead.length, sgr: stateCode },
|
|
3200
|
+
{ start: lead.length, end: lead.length + summaryText.length, sgr: '90' },
|
|
3201
|
+
{ start: lead.length + summaryText.length, end: plainHeader.length, sgr: stateCode },
|
|
3202
|
+
];
|
|
3097
3203
|
if (!row.expanded) {
|
|
3098
|
-
const collapsed = truncateToWidth(
|
|
3099
|
-
const styled =
|
|
3204
|
+
const collapsed = truncateToWidth(plainHeader, Math.max(1, width - 2));
|
|
3205
|
+
const styled = stateCode === undefined
|
|
3206
|
+
? collapsed
|
|
3207
|
+
: paintSegmentedLine(collapsed, 0, collapsed.length, headerSegments);
|
|
3100
3208
|
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
3101
3209
|
continue;
|
|
3102
3210
|
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3211
|
+
const expandedHeaderLines = headerSegments.length === 0
|
|
3212
|
+
? wrap(plainHeader, width)
|
|
3213
|
+
: wrapSegmented(plainHeader, Math.max(1, width), headerSegments);
|
|
3214
|
+
for (const wrapped of expandedHeaderLines) {
|
|
3215
|
+
addDisplay(wrapped, row);
|
|
3105
3216
|
}
|
|
3217
|
+
const bodyCode = !this.color ? undefined : running ? '33' : ok ? '32' : '31';
|
|
3106
3218
|
for (const line of toolBodyLines(row, this.maxToolOutputLines)) {
|
|
3107
3219
|
const inner = Math.max(1, width - 2);
|
|
3108
3220
|
const fillRow = line.kind === 'diff-add' || line.kind === 'diff-del';
|
|
3109
3221
|
for (const wrapped of wrap(line.text, inner)) {
|
|
3110
3222
|
const body = fillRow ? padToWidth(` ${wrapped}`, width) : ` ${wrapped}`;
|
|
3111
|
-
|
|
3223
|
+
// Diffs and todo marks keep their dedicated colors; ordinary
|
|
3224
|
+
// results follow the tool's execution state (yellow running /
|
|
3225
|
+
// green ok / red error).
|
|
3226
|
+
const kind = line.kind;
|
|
3227
|
+
const style = kind === 'diff-add' || kind === 'diff-del' || kind === 'diff-path'
|
|
3228
|
+
? this.styleLine(kind, body)
|
|
3229
|
+
: kind === 'todo-done' || kind === 'todo-active' || kind === 'todo-pending'
|
|
3230
|
+
? this.styleLine(kind, body)
|
|
3231
|
+
: this.styleStatusText(bodyCode ?? (kind === 'error' ? '31' : '37'), body);
|
|
3232
|
+
addDisplay(style, row);
|
|
3112
3233
|
}
|
|
3113
3234
|
}
|
|
3114
3235
|
continue;
|
|
@@ -3598,7 +3719,8 @@ export class SshTui {
|
|
|
3598
3719
|
String(chromeStart),
|
|
3599
3720
|
].join('\x1f');
|
|
3600
3721
|
const chromeChanged = chromeKey !== this.lastChromeKey || chromeStart !== this.lastChromeStart;
|
|
3601
|
-
const
|
|
3722
|
+
const transcriptScrolled = start !== this.lastTranscriptStart;
|
|
3723
|
+
const sizeChanged = width !== this.lastPaintWidth || height !== this.lastPaintHeight || transcriptScrolled;
|
|
3602
3724
|
// One stdout write per frame: dirty rows only, so jump-host SSH sees a
|
|
3603
3725
|
// single packet instead of one write per line. Clip/pad so leftover
|
|
3604
3726
|
// wide glyphs cannot wrap into the input box.
|
|
@@ -3621,6 +3743,7 @@ export class SshTui {
|
|
|
3621
3743
|
this.lastPaintWidth = width;
|
|
3622
3744
|
this.lastPaintHeight = height;
|
|
3623
3745
|
this.lastChromeStart = chromeStart;
|
|
3746
|
+
this.lastTranscriptStart = start;
|
|
3624
3747
|
};
|
|
3625
3748
|
buildSuggestions() {
|
|
3626
3749
|
const input = this.input;
|
|
@@ -3760,6 +3883,13 @@ export class SshTui {
|
|
|
3760
3883
|
this.dirty = false;
|
|
3761
3884
|
this.paint();
|
|
3762
3885
|
};
|
|
3886
|
+
/** Color one line with an explicit SGR code (tool state colors, etc.). */
|
|
3887
|
+
styleStatusText(code, text) {
|
|
3888
|
+
const safe = sanitizeTerminalText(text);
|
|
3889
|
+
if (!this.color)
|
|
3890
|
+
return safe;
|
|
3891
|
+
return `\x1b[${code}m${safe}\x1b[0m`;
|
|
3892
|
+
}
|
|
3763
3893
|
styleLine(kind, text) {
|
|
3764
3894
|
const safe = sanitizeTerminalText(text);
|
|
3765
3895
|
if (!this.color)
|
|
@@ -3769,8 +3899,9 @@ export class SshTui {
|
|
|
3769
3899
|
kind === 'reasoning' ? '2;3' :
|
|
3770
3900
|
kind === 'brand' ? '1;38;2;77;107;253' :
|
|
3771
3901
|
kind === 'tool' || kind === 'tool-result' ? '33' :
|
|
3772
|
-
|
|
3773
|
-
|
|
3902
|
+
// Codex-like: muted add/del that blend into the terminal background.
|
|
3903
|
+
kind === 'diff-add' ? '38;2;122;168;116;48;2;18;42;24' :
|
|
3904
|
+
kind === 'diff-del' ? '38;2;196;122;122;48;2;48;20;20' :
|
|
3774
3905
|
kind === 'diff-path' ? '1;36' :
|
|
3775
3906
|
kind === 'todo-done' ? '2;32' :
|
|
3776
3907
|
kind === 'todo-active' ? '1;36' :
|
|
@@ -3796,7 +3927,7 @@ export class SshTui {
|
|
|
3796
3927
|
const provider = selection.provider ?? parentProvider;
|
|
3797
3928
|
const model = subagentModelMatchesProvider(provider, selection.model)
|
|
3798
3929
|
? selection.model
|
|
3799
|
-
: defaultSubagentModelForProvider(provider);
|
|
3930
|
+
: defaultSubagentModelForProvider(provider, [], this.selectionRef?.current?.model);
|
|
3800
3931
|
return {
|
|
3801
3932
|
...resolved,
|
|
3802
3933
|
provider,
|
|
@@ -4006,6 +4137,16 @@ export class SshTui {
|
|
|
4006
4137
|
// Usage accounting is complete for this step; the map only exists to
|
|
4007
4138
|
// deduplicate repeated usage reports during the step.
|
|
4008
4139
|
this.usageByStep.delete(`${event.data.turn}:${event.data.step}`);
|
|
4140
|
+
if (!this.replaying) {
|
|
4141
|
+
this.quotaStepsSinceRefresh += 1;
|
|
4142
|
+
const every = quotaRefreshEverySteps(this.quotaSnapshot === undefined
|
|
4143
|
+
? undefined
|
|
4144
|
+
: tightestQuotaWindow(this.quotaSnapshot));
|
|
4145
|
+
if (this.quotaStepsSinceRefresh >= every) {
|
|
4146
|
+
this.quotaStepsSinceRefresh = 0;
|
|
4147
|
+
void this.refreshQuota({ reason: 'step', announce: false }).catch(() => { });
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4009
4150
|
this.markDirty();
|
|
4010
4151
|
break;
|
|
4011
4152
|
}
|
|
@@ -4016,14 +4157,6 @@ export class SshTui {
|
|
|
4016
4157
|
this.markDirty();
|
|
4017
4158
|
break;
|
|
4018
4159
|
case 'turn/end': {
|
|
4019
|
-
this.quotaTurnsSinceRefresh += 1;
|
|
4020
|
-
const every = quotaRefreshEveryTurns(this.quotaSnapshot === undefined
|
|
4021
|
-
? undefined
|
|
4022
|
-
: tightestQuotaWindow(this.quotaSnapshot));
|
|
4023
|
-
if (this.quotaTurnsSinceRefresh >= every) {
|
|
4024
|
-
this.quotaTurnsSinceRefresh = 0;
|
|
4025
|
-
void this.refreshQuota({ reason: 'turn', announce: false }).catch(() => { });
|
|
4026
|
-
}
|
|
4027
4160
|
const reason = event.data.reason;
|
|
4028
4161
|
this.openToolCalls.clear();
|
|
4029
4162
|
this.pendingToolTimes.clear();
|
|
@@ -4062,11 +4195,6 @@ export class SshTui {
|
|
|
4062
4195
|
this.markDirty();
|
|
4063
4196
|
break;
|
|
4064
4197
|
}
|
|
4065
|
-
case 'todo/write': {
|
|
4066
|
-
this.upsertPlanRow({ todos: parsePlanTodos(event.data.todos) });
|
|
4067
|
-
this.markDirty();
|
|
4068
|
-
break;
|
|
4069
|
-
}
|
|
4070
4198
|
default:
|
|
4071
4199
|
this.handleExtensionEvent(event);
|
|
4072
4200
|
break;
|
|
@@ -4132,6 +4260,11 @@ export class SshTui {
|
|
|
4132
4260
|
this.markDirty();
|
|
4133
4261
|
return;
|
|
4134
4262
|
}
|
|
4263
|
+
if (type === 'todo/write') {
|
|
4264
|
+
this.upsertPlanRow({ todos: parsePlanTodos(data?.todos) });
|
|
4265
|
+
this.markDirty();
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4135
4268
|
if (type === 'command/run') {
|
|
4136
4269
|
this.handleCommandRun(data);
|
|
4137
4270
|
return;
|
|
@@ -4707,12 +4840,11 @@ export class SshTui {
|
|
|
4707
4840
|
const llm = this.ctx.get('llm');
|
|
4708
4841
|
if (llm === undefined)
|
|
4709
4842
|
return [];
|
|
4710
|
-
const discovered = await
|
|
4843
|
+
const discovered = await discoverProviderModels(llm, {
|
|
4711
4844
|
baseURL,
|
|
4712
4845
|
...(api === undefined ? {} : { api }),
|
|
4713
4846
|
...(apiKey === undefined ? {} : { apiKey }),
|
|
4714
|
-
|
|
4715
|
-
});
|
|
4847
|
+
}, AbortSignal.timeout(15_000));
|
|
4716
4848
|
return discovered.map(model => ({ id: model.id, label: model.name || model.id }));
|
|
4717
4849
|
}
|
|
4718
4850
|
/** Add one endpoint-listed model to the stored provider profile when needed. */
|
|
@@ -5018,7 +5150,7 @@ export class SshTui {
|
|
|
5018
5150
|
if (this.selectionRef !== undefined)
|
|
5019
5151
|
this.selectionRef.current = next;
|
|
5020
5152
|
this.onSelectionChanged?.(next);
|
|
5021
|
-
await this.
|
|
5153
|
+
await this.persistDefaultSelection(next);
|
|
5022
5154
|
await this.rememberRoute(next);
|
|
5023
5155
|
const kind = describeProviderRoute(provider);
|
|
5024
5156
|
this.pushRow({
|
|
@@ -5029,7 +5161,6 @@ export class SshTui {
|
|
|
5029
5161
|
const previousProvider = current?.provider ?? this.agent.options.provider ?? this.providerName;
|
|
5030
5162
|
if (previousProvider !== provider) {
|
|
5031
5163
|
await this.syncSubagentToProvider(provider, listedIds, true);
|
|
5032
|
-
await this.promptSubagentAfterProviderSwitch(provider);
|
|
5033
5164
|
this.clearQuotaForProvider(provider);
|
|
5034
5165
|
void this.refreshQuota({ reason: 'command', announce: false }).catch(() => { });
|
|
5035
5166
|
}
|
|
@@ -5038,6 +5169,47 @@ export class SshTui {
|
|
|
5038
5169
|
}
|
|
5039
5170
|
this.markDirty();
|
|
5040
5171
|
}
|
|
5172
|
+
/**
|
|
5173
|
+
* Persist the default provider/model selection. `agentDefaultModel` may be
|
|
5174
|
+
* unavailable or its settings namespace may not be registered in this
|
|
5175
|
+
* process, so a failed `saveSelection` falls back to writing the
|
|
5176
|
+
* `agent-default-model` settings section directly and surfaces a warning
|
|
5177
|
+
* when neither path sticks.
|
|
5178
|
+
*/
|
|
5179
|
+
async persistDefaultSelection(next) {
|
|
5180
|
+
const settings = this.ctx.get('settings');
|
|
5181
|
+
const defaultModel = this.ctx.get('agentDefaultModel');
|
|
5182
|
+
if (defaultModel !== undefined) {
|
|
5183
|
+
try {
|
|
5184
|
+
await defaultModel.saveSelection(next);
|
|
5185
|
+
return true;
|
|
5186
|
+
}
|
|
5187
|
+
catch {
|
|
5188
|
+
// Fall through to the direct settings write.
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
if (settings !== undefined) {
|
|
5192
|
+
try {
|
|
5193
|
+
await settings.replace(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, {
|
|
5194
|
+
provider: next.provider,
|
|
5195
|
+
model: next.model,
|
|
5196
|
+
...(next.reasoningEffort === undefined ? {} : { reasoningEffort: String(next.reasoningEffort) }),
|
|
5197
|
+
});
|
|
5198
|
+
return true;
|
|
5199
|
+
}
|
|
5200
|
+
catch (error) {
|
|
5201
|
+
this.pushRow({
|
|
5202
|
+
kind: 'error',
|
|
5203
|
+
text: `默认选择未能固化:agentDefaultModel 不可用且 settings 写入失败(${errorChain(error)})。本次切换仅当前会话生效,重启会回退到保存过的提供商。`,
|
|
5204
|
+
});
|
|
5205
|
+
this.markDirty();
|
|
5206
|
+
return false;
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
this.pushRow({ kind: 'error', text: '默认选择未能固化:settings 服务不可用。本次切换仅当前会话生效。' });
|
|
5210
|
+
this.markDirty();
|
|
5211
|
+
return false;
|
|
5212
|
+
}
|
|
5041
5213
|
/** Provider route the next subagent request should use. */
|
|
5042
5214
|
effectiveSubagentProvider() {
|
|
5043
5215
|
return this.subagentSelection.current.provider
|
|
@@ -5066,7 +5238,8 @@ export class SshTui {
|
|
|
5066
5238
|
catalog = [];
|
|
5067
5239
|
}
|
|
5068
5240
|
}
|
|
5069
|
-
const
|
|
5241
|
+
const parentModel = this.selectionRef?.current?.model ?? this.agent.options.model;
|
|
5242
|
+
const nextModel = defaultSubagentModelForProvider(provider, catalog, parentModel);
|
|
5070
5243
|
if (!force && nextModel === current.model && current.provider === undefined)
|
|
5071
5244
|
return;
|
|
5072
5245
|
const persisted = await this.saveSubagentSelection({
|
|
@@ -5078,41 +5251,12 @@ export class SshTui {
|
|
|
5078
5251
|
text: `子代理已跟随提供商 ${provider},模型改为 ${nextModel}${persisted ? '' : '(仅当前会话)'}。`,
|
|
5079
5252
|
});
|
|
5080
5253
|
}
|
|
5081
|
-
async promptSubagentAfterProviderSwitch(provider) {
|
|
5082
|
-
const current = this.subagentSelection.current;
|
|
5083
|
-
try {
|
|
5084
|
-
const { options, source } = await this.subagentModelOptions(provider);
|
|
5085
|
-
const listed = options.length === 0
|
|
5086
|
-
? [{ id: current.model, label: current.model }]
|
|
5087
|
-
: options;
|
|
5088
|
-
if (!listed.some(option => option.id === current.model)) {
|
|
5089
|
-
listed.unshift({ id: current.model, label: current.model });
|
|
5090
|
-
}
|
|
5091
|
-
const selected = await this.pickModelOption(listed, provider, source, current.model);
|
|
5092
|
-
if (selected === undefined || selected.id === current.model)
|
|
5093
|
-
return;
|
|
5094
|
-
if (!(await this.ensureProviderModelConfigured(provider, selected.id)))
|
|
5095
|
-
return;
|
|
5096
|
-
const persisted = await this.saveSubagentSelection({ model: selected.id });
|
|
5097
|
-
this.pushRow({
|
|
5098
|
-
kind: 'system',
|
|
5099
|
-
text: `子代理模型已设为 ${selected.id}(提供方跟随 ${provider})${persisted ? '' : '(仅当前会话)'}。`,
|
|
5100
|
-
});
|
|
5101
|
-
}
|
|
5102
|
-
catch (error) {
|
|
5103
|
-
if (error instanceof UserQuestionError) {
|
|
5104
|
-
this.pushRow({ kind: 'system', text: `子代理沿用 ${current.model}。之后可用 /submodel 再改。` });
|
|
5105
|
-
return;
|
|
5106
|
-
}
|
|
5107
|
-
this.pushRow({ kind: 'error', text: `选择子代理模型失败:${errorChain(error)}` });
|
|
5108
|
-
}
|
|
5109
|
-
}
|
|
5110
5254
|
clearQuotaForProvider(provider) {
|
|
5111
5255
|
if (this.quotaSnapshot !== undefined && this.quotaSnapshot.provider === provider)
|
|
5112
5256
|
return;
|
|
5113
5257
|
this.quotaSnapshot = undefined;
|
|
5114
5258
|
this.quotaAlerted.clear();
|
|
5115
|
-
this.
|
|
5259
|
+
this.quotaStepsSinceRefresh = 0;
|
|
5116
5260
|
this.markDirty();
|
|
5117
5261
|
}
|
|
5118
5262
|
/** Persist one subagent selection and publish it to the live request waterfall. */
|
|
@@ -5254,7 +5398,7 @@ export class SshTui {
|
|
|
5254
5398
|
});
|
|
5255
5399
|
this.markDirty();
|
|
5256
5400
|
}
|
|
5257
|
-
/** /mode: pick an agent preset (standard / minimal /
|
|
5401
|
+
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
5258
5402
|
async runModeCommand() {
|
|
5259
5403
|
const agentPresets = this.ctx.get('agentPresets');
|
|
5260
5404
|
if (agentPresets === undefined) {
|
|
@@ -5540,13 +5684,29 @@ export class SshTui {
|
|
|
5540
5684
|
const token = await this.resolveSuperGrokToken();
|
|
5541
5685
|
if (token === undefined)
|
|
5542
5686
|
throw new Error('未找到 SuperGrok OAuth token(~/.grok-bridge/auth.json)');
|
|
5543
|
-
const
|
|
5687
|
+
const headers = {
|
|
5544
5688
|
authorization: `Bearer ${token}`,
|
|
5545
5689
|
accept: 'application/json',
|
|
5546
5690
|
'x-grok-client-mode': 'cli',
|
|
5547
5691
|
'x-grok-client-version': '1.0.0',
|
|
5548
|
-
}
|
|
5549
|
-
|
|
5692
|
+
};
|
|
5693
|
+
try {
|
|
5694
|
+
const payload = await this.fetchJson(SUPERGROK_BILLING_URL, headers, 'SuperGrok');
|
|
5695
|
+
return parseSuperGrokBilling(payload);
|
|
5696
|
+
}
|
|
5697
|
+
catch (error) {
|
|
5698
|
+
const message = errorChain(error);
|
|
5699
|
+
if (!message.includes('HTTP 401') && !message.includes('HTTP 403'))
|
|
5700
|
+
throw error;
|
|
5701
|
+
const retried = await this.resolveSuperGrokToken({ force: true });
|
|
5702
|
+
if (retried === undefined || retried === token)
|
|
5703
|
+
throw error;
|
|
5704
|
+
const payload = await this.fetchJson(SUPERGROK_BILLING_URL, {
|
|
5705
|
+
...headers,
|
|
5706
|
+
authorization: `Bearer ${retried}`,
|
|
5707
|
+
}, 'SuperGrok');
|
|
5708
|
+
return parseSuperGrokBilling(payload);
|
|
5709
|
+
}
|
|
5550
5710
|
}
|
|
5551
5711
|
const llmPiAi = this.ctx.get('settings')?.get(settingsNamespace('llm-pi-ai'));
|
|
5552
5712
|
const source = openCodeSourceFor(provider, llmPiAi);
|
|
@@ -5558,19 +5718,8 @@ export class SshTui {
|
|
|
5558
5718
|
const payload = await this.fetchOpenCodeGoUsage(apiKey);
|
|
5559
5719
|
return parseOpenCodeGoQuota(payload, source.provider);
|
|
5560
5720
|
}
|
|
5561
|
-
async resolveSuperGrokToken() {
|
|
5562
|
-
|
|
5563
|
-
try {
|
|
5564
|
-
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
5565
|
-
const token = typeof parsed.access_token === 'string' ? parsed.access_token : parsed.accessToken;
|
|
5566
|
-
return typeof token === 'string' && token.trim() !== '' ? token.trim() : undefined;
|
|
5567
|
-
}
|
|
5568
|
-
catch {
|
|
5569
|
-
return undefined;
|
|
5570
|
-
}
|
|
5571
|
-
};
|
|
5572
|
-
return await fromFile(join(homedir(), '.grok-bridge', 'auth.json'))
|
|
5573
|
-
?? await fromFile(join(homedir(), '.grok', 'auth.json'));
|
|
5721
|
+
async resolveSuperGrokToken(options = {}) {
|
|
5722
|
+
return resolveFreshSuperGrokToken(options);
|
|
5574
5723
|
}
|
|
5575
5724
|
async fetchJson(url, headers, label) {
|
|
5576
5725
|
let response;
|
|
@@ -6149,12 +6298,11 @@ export class SshTui {
|
|
|
6149
6298
|
const llm = this.ctx.get('llm');
|
|
6150
6299
|
if (llm === undefined)
|
|
6151
6300
|
throw new Error('llm 服务不可用');
|
|
6152
|
-
const discovered = await
|
|
6301
|
+
const discovered = await discoverProviderModels(llm, {
|
|
6153
6302
|
baseURL,
|
|
6154
6303
|
...(template.api === undefined ? {} : { api: template.api }),
|
|
6155
6304
|
...(key === '' ? {} : { apiKey: key }),
|
|
6156
|
-
|
|
6157
|
-
});
|
|
6305
|
+
}, AbortSignal.timeout(15_000));
|
|
6158
6306
|
// Apply only if the wizard is still on the same draft the fetch started
|
|
6159
6307
|
// from, so a stale reply cannot overwrite a newer edit or a reset.
|
|
6160
6308
|
const stillCurrent = this.onboarding === state
|