dsh-ssh-tui 0.3.7 → 0.3.9
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 +49 -28
- package/README.md +29 -14
- package/lib/i18n/en.js +429 -0
- package/lib/i18n/en.js.map +1 -0
- package/lib/i18n/index.js +94 -0
- package/lib/i18n/index.js.map +1 -0
- package/lib/i18n/zh.js +429 -0
- package/lib/i18n/zh.js.map +1 -0
- package/lib/index.js +71 -8
- package/lib/index.js.map +1 -1
- package/lib/picker.js +5 -4
- package/lib/picker.js.map +1 -1
- package/lib/route-memory.js +23 -4
- package/lib/route-memory.js.map +1 -1
- package/lib/session-list.js +46 -0
- package/lib/session-list.js.map +1 -1
- package/lib/session-lock.js +3 -6
- package/lib/session-lock.js.map +1 -1
- package/lib/subagent-model.js +81 -14
- package/lib/subagent-model.js.map +1 -1
- package/lib/supergrok-token.js +132 -0
- package/lib/supergrok-token.js.map +1 -0
- package/lib/tui.js +950 -346
- package/lib/tui.js.map +1 -1
- package/lib/types/i18n/en.d.ts +2 -0
- package/lib/types/i18n/index.d.ts +28 -0
- package/lib/types/i18n/zh.d.ts +2 -0
- package/lib/types/route-memory.d.ts +10 -0
- package/lib/types/session-list.d.ts +16 -0
- package/lib/types/subagent-model.d.ts +28 -2
- package/lib/types/supergrok-token.d.ts +22 -0
- package/lib/types/tui.d.ts +123 -3
- package/lib/update-check.js +2 -1
- package/lib/update-check.js.map +1 -1
- package/package.json +1 -1
package/lib/tui.js
CHANGED
|
@@ -21,11 +21,14 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
|
21
21
|
import { createUserMessage, errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
22
22
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
23
23
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
24
|
-
import { formatSessionTime, listResumableSessions } from './session-list.js';
|
|
24
|
+
import { formatFooterCwd, formatSessionTime, listResumableSessions } from './session-list.js';
|
|
25
|
+
import { applySavedLocale, getLocale, localeDisplayName, localeFromTag, setLocale, t, UI_LOCALE_NAMESPACE, } from './i18n/index.js';
|
|
25
26
|
import { defaultReasoningEffort } from './reasoning.js';
|
|
26
27
|
import { checkForPluginUpdate } from './update-check.js';
|
|
27
28
|
import { ROUTE_MEMORY_NAMESPACE, parseRouteMemory, rememberedRouteFor, upsertRememberedRoute, } from './route-memory.js';
|
|
28
|
-
import {
|
|
29
|
+
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model';
|
|
30
|
+
import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, describeSubagentFit, subagentModelMatchesProvider, subagentSettingsValue, } from './subagent-model.js';
|
|
31
|
+
import { resolveFreshSuperGrokToken } from './supergrok-token.js';
|
|
29
32
|
import { UserQuestionError, } from '@deepseek-ai/dsh-user-questions';
|
|
30
33
|
function discoverProviderModels(llm, request, signal) {
|
|
31
34
|
return llm.discoverModels(settingsNamespace('llm-pi-ai'), { ...request, signal }, signal);
|
|
@@ -47,42 +50,44 @@ function installUserQuestionAnswerer(ctx, questions, ask) {
|
|
|
47
50
|
});
|
|
48
51
|
}
|
|
49
52
|
const ROUTE_MEMORY_NS = ROUTE_MEMORY_NAMESPACE;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
53
|
+
function providerTemplates() {
|
|
54
|
+
return {
|
|
55
|
+
official: {
|
|
56
|
+
label: t('route.deepseek'),
|
|
57
|
+
defaultId: 'deepseek-official',
|
|
58
|
+
defaultBaseUrl: 'https://api.deepseek.com',
|
|
59
|
+
defaultModels: ['deepseek-v4-pro', 'deepseek-v4-flash'],
|
|
60
|
+
},
|
|
61
|
+
'opencode-go': {
|
|
62
|
+
label: t('onboard.providerGo'),
|
|
63
|
+
defaultId: 'opencode-go',
|
|
64
|
+
defaultBaseUrl: 'https://opencode.ai/zen/go/v1',
|
|
65
|
+
api: 'openai-responses',
|
|
66
|
+
defaultModels: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
|
67
|
+
},
|
|
68
|
+
'openai-completions': {
|
|
69
|
+
label: t('onboard.providerCompletions'),
|
|
70
|
+
defaultId: 'my-gateway',
|
|
71
|
+
defaultBaseUrl: '',
|
|
72
|
+
api: 'openai-completions',
|
|
73
|
+
defaultModels: ['deepseek-v4-flash'],
|
|
74
|
+
},
|
|
75
|
+
'openai-responses': {
|
|
76
|
+
label: t('onboard.providerResponses'),
|
|
77
|
+
defaultId: 'my-responses',
|
|
78
|
+
defaultBaseUrl: '',
|
|
79
|
+
api: 'openai-responses',
|
|
80
|
+
defaultModels: ['deepseek-v4-flash'],
|
|
81
|
+
},
|
|
82
|
+
'anthropic-messages': {
|
|
83
|
+
label: t('onboard.providerAnthropic'),
|
|
84
|
+
defaultId: 'my-anthropic',
|
|
85
|
+
defaultBaseUrl: '',
|
|
86
|
+
api: 'anthropic-messages',
|
|
87
|
+
defaultModels: ['deepseek-v4-flash'],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
86
91
|
const RENDER_INTERVAL_MS = 160;
|
|
87
92
|
const LOCAL_PAINT_INTERVAL_MS = 80;
|
|
88
93
|
const WAIT_INDICATOR_MS = 8000;
|
|
@@ -141,8 +146,8 @@ export function paintIntervalForRtt(rttMs) {
|
|
|
141
146
|
}
|
|
142
147
|
export function paintLinkLabel(kind, intervalMs, probed) {
|
|
143
148
|
if (kind === 'local')
|
|
144
|
-
return
|
|
145
|
-
return probed ?
|
|
149
|
+
return t('paint.localMs', { ms: intervalMs });
|
|
150
|
+
return probed ? t('paint.sshMs', { ms: intervalMs }) : t('paint.sshMsUnprobed', { ms: intervalMs });
|
|
146
151
|
}
|
|
147
152
|
/** Signal-bar quality from a measured SSH round-trip, or local TTY. */
|
|
148
153
|
export function linkQualityOf(kind, rttMs) {
|
|
@@ -186,16 +191,16 @@ export function formatLinkQualityChip(kind, intervalMs, rttMs, probed, color = f
|
|
|
186
191
|
? `\x1b[${LINK_PIP_COLOR[filled] ?? '90'}m${pips}\x1b[0m`
|
|
187
192
|
: pips;
|
|
188
193
|
if (kind === 'local')
|
|
189
|
-
return
|
|
194
|
+
return t('paint.localChip', { pips: colored });
|
|
190
195
|
const delay = probed && rttMs !== undefined && Number.isFinite(rttMs)
|
|
191
196
|
? `${Math.round(rttMs)}ms`
|
|
192
197
|
: `${intervalMs}ms`;
|
|
193
|
-
return
|
|
198
|
+
return t('paint.sshChip', { pips: colored, delay });
|
|
194
199
|
}
|
|
195
200
|
export function providerShortCode(provider) {
|
|
196
201
|
const id = provider.trim();
|
|
197
202
|
if (id === 'deepseek-official' || id === 'deepseek')
|
|
198
|
-
return '
|
|
203
|
+
return t('route.deepseek');
|
|
199
204
|
if (id === 'xai' || id === 'grok' || id.startsWith('xai-'))
|
|
200
205
|
return 'SuperGrok';
|
|
201
206
|
if (id === 'opencode-go')
|
|
@@ -208,29 +213,29 @@ export function providerShortCode(provider) {
|
|
|
208
213
|
export function footerStatsGroups(stats) {
|
|
209
214
|
const groups = [];
|
|
210
215
|
if (stats.steps > 0)
|
|
211
|
-
groups.push(
|
|
216
|
+
groups.push(t('footer.turnsSteps', { turns: stats.turns, steps: stats.steps }));
|
|
212
217
|
const billedInput = stats.inputTokens + stats.cacheReadTokens + stats.cacheWriteTokens;
|
|
213
218
|
if (billedInput > 0 || stats.outputTokens > 0) {
|
|
214
|
-
groups.push(
|
|
219
|
+
groups.push(t('footer.tokens', { input: formatTokens(billedInput), output: formatTokens(stats.outputTokens) }));
|
|
215
220
|
}
|
|
216
221
|
const speeds = [];
|
|
217
222
|
if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
|
|
218
223
|
speeds.push(formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)));
|
|
219
224
|
}
|
|
220
225
|
else if (stats.ttftSteps > 0) {
|
|
221
|
-
speeds.push(
|
|
226
|
+
speeds.push(t('footer.ttft', { duration: formatDuration(stats.ttftMs / stats.ttftSteps) }));
|
|
222
227
|
}
|
|
223
228
|
if (speeds.length > 0)
|
|
224
229
|
groups.push(speeds.join(' '));
|
|
225
230
|
const durations = [];
|
|
226
231
|
if (stats.llmMs > 0)
|
|
227
|
-
durations.push(
|
|
232
|
+
durations.push(t('footer.llmMs', { duration: formatDuration(stats.llmMs) }));
|
|
228
233
|
if (stats.toolMs > 0)
|
|
229
|
-
durations.push(
|
|
234
|
+
durations.push(t('footer.toolMs', { duration: formatDuration(stats.toolMs) }));
|
|
230
235
|
if (durations.length > 0)
|
|
231
236
|
groups.push(durations.join(' '));
|
|
232
237
|
if (billedInput > 0)
|
|
233
|
-
groups.push(
|
|
238
|
+
groups.push(t('footer.cacheHit', { percent: Math.round(stats.cacheReadTokens / billedInput * 100) }));
|
|
234
239
|
return groups;
|
|
235
240
|
}
|
|
236
241
|
export function fitFooterStatsLine(chip, groups, width) {
|
|
@@ -242,36 +247,36 @@ export function fitFooterStatsLine(chip, groups, width) {
|
|
|
242
247
|
}
|
|
243
248
|
export function footerActivity(input) {
|
|
244
249
|
if (input.planReview)
|
|
245
|
-
return { kind: 'plan-review', text: '
|
|
250
|
+
return { kind: 'plan-review', text: t('footer.planReview') };
|
|
246
251
|
if (input.waitingQuestion)
|
|
247
|
-
return { kind: 'waiting', text: '
|
|
252
|
+
return { kind: 'waiting', text: t('footer.waiting') };
|
|
248
253
|
if (input.compacting)
|
|
249
|
-
return { kind: 'compacting', text: '
|
|
254
|
+
return { kind: 'compacting', text: t('footer.compacting') };
|
|
250
255
|
if (input.retry !== undefined) {
|
|
251
|
-
return { kind: 'retry', text:
|
|
256
|
+
return { kind: 'retry', text: t('footer.retry', { retry: input.retry.retry, max: input.retry.maxRetries }) };
|
|
252
257
|
}
|
|
253
258
|
if (input.subagents > 0)
|
|
254
|
-
return { kind: 'subagents', text:
|
|
259
|
+
return { kind: 'subagents', text: t('footer.subagents', { count: input.subagents }) };
|
|
255
260
|
if (input.running && input.tools > 0)
|
|
256
|
-
return { kind: 'tools', text:
|
|
261
|
+
return { kind: 'tools', text: t('footer.tools', { count: input.tools }) };
|
|
257
262
|
if (input.planLeftOpen)
|
|
258
|
-
return { kind: 'plan-open', text: '
|
|
263
|
+
return { kind: 'plan-open', text: t('footer.planOpen') };
|
|
259
264
|
if (input.planPending)
|
|
260
|
-
return { kind: 'plan-pending', text: '
|
|
265
|
+
return { kind: 'plan-pending', text: t('footer.planSwitching') };
|
|
261
266
|
if (input.planActive)
|
|
262
|
-
return { kind: 'plan-pending', text: '
|
|
267
|
+
return { kind: 'plan-pending', text: t('footer.planMode') };
|
|
263
268
|
if (input.goalPhase === 'active')
|
|
264
|
-
return { kind: 'goal', text: '
|
|
269
|
+
return { kind: 'goal', text: t('footer.goalActive') };
|
|
265
270
|
if (input.goalPhase === 'paused')
|
|
266
|
-
return { kind: 'goal', text: '
|
|
271
|
+
return { kind: 'goal', text: t('footer.goalPaused') };
|
|
267
272
|
if (input.goalPhase === 'blocked')
|
|
268
|
-
return { kind: 'goal', text: '
|
|
273
|
+
return { kind: 'goal', text: t('footer.goalBlocked') };
|
|
269
274
|
if (input.running && input.idleMs > WAIT_INDICATOR_MS) {
|
|
270
|
-
return { kind: 'waiting-llm', text:
|
|
275
|
+
return { kind: 'waiting-llm', text: t('footer.waitSeconds', { seconds: Math.floor(input.idleMs / 1000) }) };
|
|
271
276
|
}
|
|
272
277
|
if (input.running)
|
|
273
|
-
return { kind: 'idle', text: '
|
|
274
|
-
return { kind: 'idle', text: '
|
|
278
|
+
return { kind: 'idle', text: t('footer.running') };
|
|
279
|
+
return { kind: 'idle', text: t('footer.idle') };
|
|
275
280
|
}
|
|
276
281
|
/** Short remaining-quota bar: 8 pips, filled from the left. */
|
|
277
282
|
export function formatQuotaBar(remainingPercent, width = 8) {
|
|
@@ -283,27 +288,53 @@ export function footerIdentityParts(input) {
|
|
|
283
288
|
const parts = [];
|
|
284
289
|
if (input.preset !== undefined && input.preset !== '')
|
|
285
290
|
parts.push(`[${input.preset}]`);
|
|
291
|
+
if (input.cwdLabel !== undefined && input.cwdLabel !== '')
|
|
292
|
+
parts.push(input.cwdLabel);
|
|
286
293
|
const model = input.effort === undefined ? input.model : `${input.model} ${input.effort}`;
|
|
287
294
|
if (model !== '')
|
|
288
295
|
parts.push(model);
|
|
289
296
|
if (input.subDiffers)
|
|
290
297
|
parts.push(`sub:${input.subModel}`);
|
|
291
|
-
if (input.
|
|
292
|
-
parts.push(
|
|
298
|
+
if (input.quotaPercent !== undefined) {
|
|
299
|
+
parts.push(formatFooterQuota(input.quotaPercent, input.quotaCode));
|
|
293
300
|
}
|
|
294
301
|
if (input.search !== undefined)
|
|
295
|
-
parts.push(
|
|
302
|
+
parts.push(t('footer.search', { index: input.search.index + 1, total: input.search.total }));
|
|
296
303
|
if (input.foldedInput)
|
|
297
|
-
parts.push('
|
|
304
|
+
parts.push(t('footer.inputFolded'));
|
|
298
305
|
else if (input.multiLineInput)
|
|
299
|
-
parts.push('
|
|
306
|
+
parts.push(t('footer.multiLine'));
|
|
300
307
|
if (input.queued > 0)
|
|
301
|
-
parts.push(
|
|
308
|
+
parts.push(t('footer.queued', { count: input.queued }));
|
|
302
309
|
return parts;
|
|
303
310
|
}
|
|
311
|
+
/** `SuperGrok ███████░ 82%`, or just the bar + percent when `code` is omitted. */
|
|
312
|
+
export function formatFooterQuota(percent, code) {
|
|
313
|
+
const bar = `${formatQuotaBar(percent)} ${percent.toFixed(0)}%`;
|
|
314
|
+
return code !== undefined && code.trim() !== '' ? `${code.trim()} ${bar}` : bar;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Drop the Go / SuperGrok plan name from a quota identity part, keeping the
|
|
318
|
+
* remaining-percent bar. Returns true when a part was rewritten.
|
|
319
|
+
*/
|
|
320
|
+
export function dropFooterQuotaPlanName(parts) {
|
|
321
|
+
for (let index = 0; index < parts.length; index++) {
|
|
322
|
+
const part = parts[index];
|
|
323
|
+
if (part === undefined)
|
|
324
|
+
continue;
|
|
325
|
+
const barAt = part.search(/ [█░]+ \d+%$/);
|
|
326
|
+
if (barAt <= 0)
|
|
327
|
+
continue;
|
|
328
|
+
parts[index] = part.slice(barAt + 1);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
304
333
|
export function fitFooterStatusLine(activity, identity, width) {
|
|
305
334
|
const kept = [...identity];
|
|
306
335
|
const render = () => kept.length === 0 ? activity : `${activity} ${kept.join(' · ')}`;
|
|
336
|
+
if (displayWidth(render()) > width)
|
|
337
|
+
dropFooterQuotaPlanName(kept);
|
|
307
338
|
while (kept.length > 0 && displayWidth(render()) > width)
|
|
308
339
|
kept.pop();
|
|
309
340
|
return truncateToWidth(render(), Math.max(1, width));
|
|
@@ -355,7 +386,7 @@ const PLUGIN_VERSION = (() => {
|
|
|
355
386
|
const STALL_WARNING_MS = 60000;
|
|
356
387
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
357
388
|
const QUESTION_OPTION_KEYS = '123456789abcdefghijklmnopqrstuvwxyz';
|
|
358
|
-
const SUBAGENT_DEFAULT_EFFORT_LABEL = '
|
|
389
|
+
const SUBAGENT_DEFAULT_EFFORT_LABEL = () => t('footer.effortDefault');
|
|
359
390
|
const RESERVED_BOTTOM_LINES = 3; // input line + stats line + status line
|
|
360
391
|
const MAX_TRANSCRIPT_ROWS = 5000;
|
|
361
392
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -485,20 +516,46 @@ const DEEPSEEK_LOGO_VARIANTS = [
|
|
|
485
516
|
],
|
|
486
517
|
},
|
|
487
518
|
];
|
|
519
|
+
/** Lines printed by `/status` — SSH first-boot diagnostics, no extra command. */
|
|
520
|
+
export function formatStatusReport(input) {
|
|
521
|
+
const route = describeProviderRoute(input.provider);
|
|
522
|
+
const effort = input.effort === undefined ? '' : ` (${input.effort})`;
|
|
523
|
+
const fit = describeSubagentFit({
|
|
524
|
+
parentProvider: input.provider,
|
|
525
|
+
parentModel: input.parentModel,
|
|
526
|
+
subProvider: input.subProvider,
|
|
527
|
+
subModel: input.subModel,
|
|
528
|
+
});
|
|
529
|
+
return [
|
|
530
|
+
`session: ${input.sessionId}`,
|
|
531
|
+
`plugin: dsh-ssh-tui ${input.pluginVersion}`,
|
|
532
|
+
`cwd: ${input.cwd ?? ''}`,
|
|
533
|
+
`route: ${input.provider}/${input.model}${effort}`,
|
|
534
|
+
`provider: ${route.kind}`,
|
|
535
|
+
`status: ${input.agentStatus}`,
|
|
536
|
+
`preset: ${input.preset}`,
|
|
537
|
+
`subagents: ${input.activeSubagents}`,
|
|
538
|
+
fit.line,
|
|
539
|
+
`plan: ${input.plan}`,
|
|
540
|
+
formatQuotaStatusLine(input.quota),
|
|
541
|
+
`paint: ${input.paint}`,
|
|
542
|
+
input.waitingQuestions > 0 ? `questions: waiting ${input.waitingQuestions}` : 'questions: none',
|
|
543
|
+
];
|
|
544
|
+
}
|
|
488
545
|
/** Human-facing kind for a live LLM route. */
|
|
489
546
|
export function describeProviderRoute(provider) {
|
|
490
547
|
const id = provider.trim();
|
|
491
548
|
if (id === 'deepseek-official' || id === 'deepseek') {
|
|
492
|
-
return { kind: '
|
|
549
|
+
return { kind: t('route.deepseek'), short: t('route.deepseek') };
|
|
493
550
|
}
|
|
494
551
|
if (id === 'xai' || id === 'grok' || id.startsWith('xai-')) {
|
|
495
|
-
return { kind: '
|
|
552
|
+
return { kind: t('route.supergrokKind'), short: t('route.supergrokShort') };
|
|
496
553
|
}
|
|
497
554
|
if (id === 'opencode-go')
|
|
498
|
-
return { kind: '
|
|
555
|
+
return { kind: t('route.go'), short: t('route.go') };
|
|
499
556
|
if (id === 'opencode')
|
|
500
|
-
return { kind: '
|
|
501
|
-
return { kind: '
|
|
557
|
+
return { kind: t('route.zen'), short: t('route.zen') };
|
|
558
|
+
return { kind: t('route.registered'), short: id };
|
|
502
559
|
}
|
|
503
560
|
/** Routes that authenticate without a harness API-key credential. */
|
|
504
561
|
export function providerUsesLocalOAuth(provider) {
|
|
@@ -515,15 +572,22 @@ const LOCAL_COMMANDS = [
|
|
|
515
572
|
{ name: 'quit', description: 'exit the TUI' },
|
|
516
573
|
{ name: 'exit', description: 'exit the TUI' },
|
|
517
574
|
{ name: 'clear', description: 'clear the transcript view' },
|
|
518
|
-
{ name: 'status', description: 'show session,
|
|
575
|
+
{ name: 'status', description: 'show session, route, quota window, subagent fit, paint, and plugin version' },
|
|
519
576
|
{ name: 'usage', description: 'show remaining quota or account balance for the current provider' },
|
|
520
577
|
{ name: 'balance', description: 'alias of /usage: DeepSeek / OpenAI-compatible balance, or subscription quota' },
|
|
521
578
|
{ name: 'subagents', description: 'list active subagents; kill <id> to stop one' },
|
|
522
579
|
{ name: 'resume', description: 'resume a past session (empty = session picker)' },
|
|
523
580
|
{ name: 'setup', description: 'add or update an API-key provider without wiping other saved routes' },
|
|
524
581
|
{ name: 'find', description: 'search thinking / plan / subagent / reply cards' },
|
|
582
|
+
{ name: 'language', description: 'switch UI language (zh / en); empty opens a picker' },
|
|
583
|
+
{ name: 'lang', description: 'alias of /language' },
|
|
525
584
|
{ name: 'dialog-test', description: 'verify the question dialog' },
|
|
526
585
|
];
|
|
586
|
+
function localizedCommands() {
|
|
587
|
+
return LOCAL_COMMANDS.map(command => (command.name === 'language' || command.name === 'lang'
|
|
588
|
+
? { name: command.name, description: t('lang.cmd') }
|
|
589
|
+
: command));
|
|
590
|
+
}
|
|
527
591
|
/**
|
|
528
592
|
* Terminal cell width for one string.
|
|
529
593
|
*
|
|
@@ -703,6 +767,62 @@ function wrap(text, width) {
|
|
|
703
767
|
}
|
|
704
768
|
return lines;
|
|
705
769
|
}
|
|
770
|
+
/** Wrap plain text and report each output line's char range in the source. */
|
|
771
|
+
function wrapTracked(text, width) {
|
|
772
|
+
const limit = Math.max(1, width);
|
|
773
|
+
const out = [];
|
|
774
|
+
let base = 0;
|
|
775
|
+
for (const sourceLine of text.split('\n')) {
|
|
776
|
+
if (sourceLine === '') {
|
|
777
|
+
out.push({ line: '', start: base, end: base });
|
|
778
|
+
base += 1;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
let rest = sourceLine;
|
|
782
|
+
let cursor = base;
|
|
783
|
+
while (displayWidth(rest) > limit) {
|
|
784
|
+
let cut = 0;
|
|
785
|
+
let used = 0;
|
|
786
|
+
for (const char of rest) {
|
|
787
|
+
const charWidth = displayWidth(char);
|
|
788
|
+
if (charWidth > 0 && used + charWidth > limit)
|
|
789
|
+
break;
|
|
790
|
+
used += charWidth;
|
|
791
|
+
cut += char.length;
|
|
792
|
+
}
|
|
793
|
+
if (cut === 0)
|
|
794
|
+
cut = firstCodePointLength(rest);
|
|
795
|
+
out.push({ line: rest.slice(0, cut), start: cursor, end: cursor + cut });
|
|
796
|
+
rest = rest.slice(cut);
|
|
797
|
+
cursor += cut;
|
|
798
|
+
}
|
|
799
|
+
out.push({ line: rest, start: cursor, end: cursor + rest.length });
|
|
800
|
+
base += sourceLine.length + 1;
|
|
801
|
+
}
|
|
802
|
+
return out;
|
|
803
|
+
}
|
|
804
|
+
/** Paint one already-wrapped output line by the segments overlapping its range. */
|
|
805
|
+
function paintSegmentedLine(line, start, end, segments) {
|
|
806
|
+
if (segments.length === 0)
|
|
807
|
+
return line;
|
|
808
|
+
let out = '';
|
|
809
|
+
for (const seg of segments) {
|
|
810
|
+
if (seg.end <= start)
|
|
811
|
+
continue;
|
|
812
|
+
if (seg.start >= end)
|
|
813
|
+
break;
|
|
814
|
+
const from = Math.max(seg.start, start);
|
|
815
|
+
const to = Math.min(seg.end, end);
|
|
816
|
+
if (to <= from)
|
|
817
|
+
continue;
|
|
818
|
+
out += `\x1b[${seg.sgr}m${line.slice(from - start, to - start)}\x1b[0m`;
|
|
819
|
+
}
|
|
820
|
+
return out === '' ? line : out;
|
|
821
|
+
}
|
|
822
|
+
/** Wrap `text` and color each output line by overlapping `segments`. */
|
|
823
|
+
function wrapSegmented(text, width, segments) {
|
|
824
|
+
return wrapTracked(text, width).map(({ line, start, end }) => paintSegmentedLine(line, start, end, segments));
|
|
825
|
+
}
|
|
706
826
|
function truncate(text, maxLines) {
|
|
707
827
|
const lines = text.split('\n');
|
|
708
828
|
if (maxLines <= 0)
|
|
@@ -1227,7 +1347,12 @@ export function crossedQuotaThresholds(previousRemaining, remaining) {
|
|
|
1227
1347
|
}
|
|
1228
1348
|
export function quotaAlertText(snapshot, window) {
|
|
1229
1349
|
const reset = window.resetsAt === undefined ? '' : `(${formatQuotaReset(window.resetsAt)})`;
|
|
1230
|
-
return
|
|
1350
|
+
return t('quota.alert', {
|
|
1351
|
+
plan: snapshot.plan,
|
|
1352
|
+
period: quotaPeriodLabel(window.period),
|
|
1353
|
+
percent: window.remainingPercent.toFixed(0),
|
|
1354
|
+
reset,
|
|
1355
|
+
});
|
|
1231
1356
|
}
|
|
1232
1357
|
/**
|
|
1233
1358
|
* How often to re-fetch quota, based on the tightest window.
|
|
@@ -1253,12 +1378,12 @@ export function quotaRefreshEverySteps(window) {
|
|
|
1253
1378
|
export const quotaRefreshEveryTurns = quotaRefreshEverySteps;
|
|
1254
1379
|
function quotaPeriodLabel(period) {
|
|
1255
1380
|
if (period === 'hourly')
|
|
1256
|
-
return '
|
|
1381
|
+
return t('quota.periodHourly');
|
|
1257
1382
|
if (period === 'weekly')
|
|
1258
|
-
return '
|
|
1383
|
+
return t('quota.periodWeekly');
|
|
1259
1384
|
if (period === 'monthly')
|
|
1260
|
-
return '
|
|
1261
|
-
return '
|
|
1385
|
+
return t('quota.periodMonthly');
|
|
1386
|
+
return t('quota.periodUnknown');
|
|
1262
1387
|
}
|
|
1263
1388
|
function formatQuotaReset(iso) {
|
|
1264
1389
|
const reset = new Date(iso);
|
|
@@ -1341,6 +1466,20 @@ export function formatQuotaSnapshot(snapshot) {
|
|
|
1341
1466
|
}
|
|
1342
1467
|
return lines.join('\n');
|
|
1343
1468
|
}
|
|
1469
|
+
/** Compact `/status` quota line: tightest window first, then the rest. */
|
|
1470
|
+
export function formatQuotaStatusLine(snapshot) {
|
|
1471
|
+
if (snapshot === undefined || snapshot.windows.length === 0)
|
|
1472
|
+
return 'quota: none';
|
|
1473
|
+
const tightest = tightestQuotaWindow(snapshot);
|
|
1474
|
+
const ordered = tightest === undefined
|
|
1475
|
+
? snapshot.windows
|
|
1476
|
+
: [tightest, ...snapshot.windows.filter(window => window !== tightest)];
|
|
1477
|
+
const parts = ordered.map(window => {
|
|
1478
|
+
const remaining = Math.max(0, Math.min(100, window.remainingPercent));
|
|
1479
|
+
return `${window.label} ${remaining.toFixed(0)}%`;
|
|
1480
|
+
});
|
|
1481
|
+
return `quota: ${snapshot.plan} ${parts.join(' · ')}`;
|
|
1482
|
+
}
|
|
1344
1483
|
/** Tightest remaining window — used for threshold alerts. */
|
|
1345
1484
|
export function tightestQuotaWindow(snapshot) {
|
|
1346
1485
|
return snapshot.windows.reduce((best, window) => {
|
|
@@ -1616,6 +1755,22 @@ function friendlyArgsSummary(name, args) {
|
|
|
1616
1755
|
const SHELL_TOOL_NAMES = new Set(['bash', 'pwsh']);
|
|
1617
1756
|
const DIFF_TOOL_NAMES = new Set(['edit', 'write', 'str_replace_editor']);
|
|
1618
1757
|
const SUBAGENT_TOOL_NAMES = new Set(['subagent', 'subagent_fork', 'task']);
|
|
1758
|
+
/**
|
|
1759
|
+
* Tool calls that already have a dedicated transcript card (goal/change,
|
|
1760
|
+
* plan dock, question dialog). Showing them again as raw `get_goal` cards
|
|
1761
|
+
* just duplicates chrome.
|
|
1762
|
+
*/
|
|
1763
|
+
const HIDDEN_TOOL_NAMES = new Set(['get_goal']);
|
|
1764
|
+
const TOOL_TITLE_KEYS = [
|
|
1765
|
+
'edit', 'write', 'str_replace_editor', 'fetch', 'list_files', 'list', 'ls',
|
|
1766
|
+
'find', 'search', 'delete', 'rm', 'rename', 'mv', 'mkdir', 'skills',
|
|
1767
|
+
'create_goal', 'update_goal', 'complete_goal', 'clear_goal', 'pause_goal',
|
|
1768
|
+
'resume_goal', 'todo_write', 'todo', 'compact', 'glob', 'grep', 'read',
|
|
1769
|
+
'web_search', 'web_fetch',
|
|
1770
|
+
];
|
|
1771
|
+
function toolTitle(name) {
|
|
1772
|
+
return t(`toolTitle.${name}`, undefined, name);
|
|
1773
|
+
}
|
|
1619
1774
|
const MAX_SUBAGENT_LOGS = 80;
|
|
1620
1775
|
const TODO_STATUS_MARK = {
|
|
1621
1776
|
pending: '○',
|
|
@@ -1670,19 +1825,15 @@ export function cardCategoryOf(row) {
|
|
|
1670
1825
|
return 'question';
|
|
1671
1826
|
if (row.kind === 'goal')
|
|
1672
1827
|
return 'goal';
|
|
1828
|
+
if (row.kind === 'prompt')
|
|
1829
|
+
return 'prompt';
|
|
1673
1830
|
if (row.kind === 'compaction')
|
|
1674
1831
|
return 'tool';
|
|
1675
1832
|
return undefined;
|
|
1676
1833
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
subagent: '子代理',
|
|
1681
|
-
reply: '回复',
|
|
1682
|
-
tool: '工具',
|
|
1683
|
-
question: '提问',
|
|
1684
|
-
goal: '目标',
|
|
1685
|
-
};
|
|
1834
|
+
function cardCategoryLabel(category) {
|
|
1835
|
+
return t(`card.${category}`);
|
|
1836
|
+
}
|
|
1686
1837
|
const SEARCHABLE_CATEGORIES = ['thinking', 'plan', 'subagent', 'reply'];
|
|
1687
1838
|
function parseCardCategoryToken(token) {
|
|
1688
1839
|
const id = token.trim().toLowerCase();
|
|
@@ -1700,6 +1851,8 @@ function parseCardCategoryToken(token) {
|
|
|
1700
1851
|
return 'question';
|
|
1701
1852
|
if (id === 'goal' || id === '目标')
|
|
1702
1853
|
return 'goal';
|
|
1854
|
+
if (id === 'prompt' || id === '提示词' || id === '注入')
|
|
1855
|
+
return 'prompt';
|
|
1703
1856
|
return undefined;
|
|
1704
1857
|
}
|
|
1705
1858
|
/** Split `/find thinking padAnsi` into an optional category and a query. */
|
|
@@ -1736,21 +1889,81 @@ function rowSearchHaystack(row) {
|
|
|
1736
1889
|
return `${row.objective} ${row.blockedReason ?? ''}`;
|
|
1737
1890
|
case 'compaction':
|
|
1738
1891
|
return `${row.summary ?? ''} ${row.error ?? ''}`;
|
|
1892
|
+
case 'prompt':
|
|
1893
|
+
return `${row.sources.join(' ')} ${row.text}`;
|
|
1739
1894
|
default:
|
|
1740
1895
|
return '';
|
|
1741
1896
|
}
|
|
1742
1897
|
}
|
|
1898
|
+
const PROMPT_SOURCE_PATTERNS = [
|
|
1899
|
+
{ id: 'AGENTS.MD', pattern: /\bAGENTS\.md\b/iu },
|
|
1900
|
+
{ id: 'CLAUDE.MD', pattern: /\bCLAUDE\.md\b/iu },
|
|
1901
|
+
{ id: 'GEMINI.MD', pattern: /\bGEMINI\.md\b/iu },
|
|
1902
|
+
{ id: 'CURSOR.MD', pattern: /\b(?:\.?cursor(?:\/rules)?|CURSOR\.md)\b/iu },
|
|
1903
|
+
{ id: 'COPILOT.MD', pattern: /\b(?:COPILOT\.md|\.github\/copilot-instructions)\b/iu },
|
|
1904
|
+
{ id: 'WINDSURF.MD', pattern: /\bWINDSURF\.md\b/iu },
|
|
1905
|
+
];
|
|
1906
|
+
const SYSTEM_PRESET_HINT = /you are an ai agent powered by deepseek harness|powered by DeepSeek Harness|harness identity|deployment persona|system prompt/iu;
|
|
1907
|
+
const SYSTEM_PRESET_LABEL = () => t('prompt.systemPreset');
|
|
1908
|
+
const CONTEXT_LABEL = () => t('prompt.context');
|
|
1909
|
+
/** Classify one injected prompt blob into display sources. */
|
|
1910
|
+
export function promptInjectionSources(text, plugin) {
|
|
1911
|
+
const found = [];
|
|
1912
|
+
const seen = new Set();
|
|
1913
|
+
const add = (id) => {
|
|
1914
|
+
if (seen.has(id))
|
|
1915
|
+
return;
|
|
1916
|
+
seen.add(id);
|
|
1917
|
+
found.push(id);
|
|
1918
|
+
};
|
|
1919
|
+
for (const { id, pattern } of PROMPT_SOURCE_PATTERNS) {
|
|
1920
|
+
if (pattern.test(text))
|
|
1921
|
+
add(id);
|
|
1922
|
+
}
|
|
1923
|
+
const fromTags = text.matchAll(/Additional instructions from:\s*([^\n<]+)/giu);
|
|
1924
|
+
for (const match of fromTags) {
|
|
1925
|
+
const raw = (match[1] ?? '').trim();
|
|
1926
|
+
const file = raw.split(/[\\/]/u).filter(Boolean).at(-1);
|
|
1927
|
+
if (file !== undefined && /\.md$/iu.test(file))
|
|
1928
|
+
add(file.toUpperCase());
|
|
1929
|
+
}
|
|
1930
|
+
const looksSystem = SYSTEM_PRESET_HINT.test(text)
|
|
1931
|
+
|| plugin === 'system-prompt'
|
|
1932
|
+
|| plugin === 'dsh-system-prompt';
|
|
1933
|
+
if (looksSystem)
|
|
1934
|
+
add(SYSTEM_PRESET_LABEL());
|
|
1935
|
+
if (found.length === 0)
|
|
1936
|
+
add(CONTEXT_LABEL());
|
|
1937
|
+
const systemIndex = found.indexOf(SYSTEM_PRESET_LABEL());
|
|
1938
|
+
if (systemIndex > 0) {
|
|
1939
|
+
found.splice(systemIndex, 1);
|
|
1940
|
+
found.unshift(SYSTEM_PRESET_LABEL());
|
|
1941
|
+
}
|
|
1942
|
+
return found;
|
|
1943
|
+
}
|
|
1944
|
+
export function promptInjectionTitle(sources) {
|
|
1945
|
+
return sources.length === 0 ? t('prompt.inject') : t('prompt.injectWith', { sources: sources.join(' ') });
|
|
1946
|
+
}
|
|
1947
|
+
export function isPromptInjectionMessage(sourceKind, text, plugin) {
|
|
1948
|
+
if (sourceKind === 'user')
|
|
1949
|
+
return false;
|
|
1950
|
+
if (sourceKind === 'plugin')
|
|
1951
|
+
return true;
|
|
1952
|
+
return /<system-reminder\b/iu.test(text)
|
|
1953
|
+
|| SYSTEM_PRESET_HINT.test(text)
|
|
1954
|
+
|| promptInjectionSources(text, plugin).some(id => id !== SYSTEM_PRESET_LABEL());
|
|
1955
|
+
}
|
|
1743
1956
|
export function compactionHeaderText(row) {
|
|
1744
1957
|
const recovered = row.prunedTokens > 0
|
|
1745
|
-
?
|
|
1958
|
+
? t('compact.recoverTokens', { tokens: formatTokens(row.prunedTokens) })
|
|
1746
1959
|
: row.pruneCount > 0
|
|
1747
|
-
?
|
|
1748
|
-
: '
|
|
1960
|
+
? t('compact.pruneChunks', { count: row.pruneCount })
|
|
1961
|
+
: t('compact.prepare');
|
|
1749
1962
|
if (row.status === 'running')
|
|
1750
|
-
return
|
|
1963
|
+
return t('compact.running', { detail: recovered });
|
|
1751
1964
|
if (row.status === 'error')
|
|
1752
|
-
return
|
|
1753
|
-
return
|
|
1965
|
+
return t('compact.failed', { error: row.error ?? t('quota.unknown') });
|
|
1966
|
+
return t('compact.done', { detail: recovered });
|
|
1754
1967
|
}
|
|
1755
1968
|
/** Transcript rows matching a `/find` query, newest last. */
|
|
1756
1969
|
export function matchTranscriptRows(rows, raw) {
|
|
@@ -1773,20 +1986,20 @@ export function planDockNote(plan) {
|
|
|
1773
1986
|
const allDone = plan.todos.length > 0 && plan.todos.every(item => item.status === 'completed');
|
|
1774
1987
|
const leftover = plan.todos.filter(item => item.status !== 'completed').length;
|
|
1775
1988
|
if (plan.turnLeftOpen === true && leftover > 0) {
|
|
1776
|
-
return
|
|
1989
|
+
return t('plan.leftOpen', { count: leftover });
|
|
1777
1990
|
}
|
|
1778
1991
|
if (plan.pending)
|
|
1779
|
-
return '
|
|
1992
|
+
return t('plan.pendingNext');
|
|
1780
1993
|
if (plan.active)
|
|
1781
|
-
return '
|
|
1994
|
+
return t('plan.planningOnly');
|
|
1782
1995
|
if (running)
|
|
1783
|
-
return '
|
|
1996
|
+
return t('plan.executing');
|
|
1784
1997
|
if (allDone)
|
|
1785
|
-
return '
|
|
1998
|
+
return t('plan.allDone');
|
|
1786
1999
|
if (plan.todos.length > 0 || (plan.planMarkdown !== undefined && plan.planMarkdown !== '')) {
|
|
1787
|
-
return '
|
|
2000
|
+
return t('plan.stillOpen');
|
|
1788
2001
|
}
|
|
1789
|
-
return '
|
|
2002
|
+
return t('plan.closed');
|
|
1790
2003
|
}
|
|
1791
2004
|
/** Compact per-status counts matching the web plan strip. */
|
|
1792
2005
|
export function todoProgressLabel(todos) {
|
|
@@ -1795,11 +2008,11 @@ export function todoProgressLabel(todos) {
|
|
|
1795
2008
|
const pending = todos.length - done - active;
|
|
1796
2009
|
const parts = [];
|
|
1797
2010
|
if (done > 0)
|
|
1798
|
-
parts.push(
|
|
2011
|
+
parts.push(t('plan.todoDone', { count: done }));
|
|
1799
2012
|
if (active > 0)
|
|
1800
|
-
parts.push(
|
|
2013
|
+
parts.push(t('plan.todoActive', { count: active }));
|
|
1801
2014
|
if (pending > 0)
|
|
1802
|
-
parts.push(
|
|
2015
|
+
parts.push(t('plan.todoPending', { count: pending }));
|
|
1803
2016
|
return parts.join(' · ');
|
|
1804
2017
|
}
|
|
1805
2018
|
function todoItemKind(status) {
|
|
@@ -1954,7 +2167,7 @@ export function presentToolCall(name, args) {
|
|
|
1954
2167
|
const diff = diffHunksFromArgs(name, args);
|
|
1955
2168
|
const path = diff?.[0]?.path;
|
|
1956
2169
|
return {
|
|
1957
|
-
title: name,
|
|
2170
|
+
title: toolTitle(name),
|
|
1958
2171
|
summary: path ?? friendlyArgsSummary(name, args),
|
|
1959
2172
|
...diff === null || diff === undefined ? {} : { diff },
|
|
1960
2173
|
};
|
|
@@ -1962,47 +2175,62 @@ export function presentToolCall(name, args) {
|
|
|
1962
2175
|
if (SUBAGENT_TOOL_NAMES.has(name)) {
|
|
1963
2176
|
const description = typeof parsed?.description === 'string' ? parsed.description.trim() : '';
|
|
1964
2177
|
return {
|
|
1965
|
-
title: name === 'subagent_fork' ? '
|
|
2178
|
+
title: toolTitle(name === 'subagent_fork' ? 'subagent_fork' : 'subagent'),
|
|
1966
2179
|
summary: description === '' ? friendlyArgsSummary(name, args) : description,
|
|
1967
2180
|
};
|
|
1968
2181
|
}
|
|
1969
2182
|
if (name === 'todo_write' || name === 'todo') {
|
|
1970
|
-
return { title: '
|
|
2183
|
+
return { title: toolTitle('todo_write'), summary: todoSummary(parsed) };
|
|
1971
2184
|
}
|
|
1972
2185
|
if (name === 'ask_user_question') {
|
|
1973
|
-
return { title: '
|
|
2186
|
+
return { title: toolTitle('ask_user_question'), summary: askSummary(parsed) };
|
|
1974
2187
|
}
|
|
1975
2188
|
if (name === 'exit_plan_mode') {
|
|
1976
2189
|
const plan = typeof parsed?.plan === 'string' ? parsed.plan : '';
|
|
1977
|
-
return { title: '
|
|
2190
|
+
return { title: toolTitle('exit_plan_mode'), summary: planTitleFromMarkdown(plan) ?? t('plan.waitConfirm') };
|
|
2191
|
+
}
|
|
2192
|
+
if (name === 'update_goal' || name === 'create_goal') {
|
|
2193
|
+
const action = typeof parsed?.action === 'string' ? parsed.action.trim() : '';
|
|
2194
|
+
const objective = typeof parsed?.objective === 'string' ? parsed.objective.trim() : '';
|
|
2195
|
+
const titleKey = name === 'create_goal' || action === 'create' || action === 'set'
|
|
2196
|
+
? 'create_goal'
|
|
2197
|
+
: action === 'pause' ? 'pause_goal'
|
|
2198
|
+
: action === 'resume' ? 'resume_goal'
|
|
2199
|
+
: action === 'clear' ? 'clear_goal'
|
|
2200
|
+
: action === 'complete' ? 'complete_goal'
|
|
2201
|
+
: 'update_goal';
|
|
2202
|
+
return { title: toolTitle(titleKey), summary: objective || action || friendlyArgsSummary(name, args) };
|
|
2203
|
+
}
|
|
2204
|
+
if (name === 'get_goal') {
|
|
2205
|
+
return { title: toolTitle('get_goal'), summary: friendlyArgsSummary(name, args) };
|
|
1978
2206
|
}
|
|
1979
2207
|
if (name === 'read') {
|
|
1980
2208
|
const path = typeof parsed?.path === 'string' ? parsed.path
|
|
1981
2209
|
: typeof parsed?.file_path === 'string' ? parsed.file_path
|
|
1982
2210
|
: typeof parsed?.url === 'string' ? parsed.url
|
|
1983
2211
|
: '';
|
|
1984
|
-
return { title: '
|
|
2212
|
+
return { title: toolTitle('read'), summary: path || friendlyArgsSummary(name, args) };
|
|
1985
2213
|
}
|
|
1986
2214
|
if (name === 'grep') {
|
|
1987
2215
|
const pattern = typeof parsed?.pattern === 'string' ? parsed.pattern : '';
|
|
1988
2216
|
const path = typeof parsed?.path === 'string' ? parsed.path : '';
|
|
1989
|
-
return { title: '
|
|
2217
|
+
return { title: toolTitle('grep'), summary: [pattern, path].filter(Boolean).join(' ') || friendlyArgsSummary(name, args) };
|
|
1990
2218
|
}
|
|
1991
2219
|
if (name === 'glob') {
|
|
1992
2220
|
const pattern = typeof parsed?.pattern === 'string' ? parsed.pattern
|
|
1993
2221
|
: typeof parsed?.glob_pattern === 'string' ? parsed.glob_pattern
|
|
1994
2222
|
: '';
|
|
1995
|
-
return { title: '
|
|
2223
|
+
return { title: toolTitle('glob'), summary: pattern || friendlyArgsSummary(name, args) };
|
|
1996
2224
|
}
|
|
1997
2225
|
if (name === 'web_search') {
|
|
1998
2226
|
const query = typeof parsed?.query === 'string' ? parsed.query : typeof parsed?.q === 'string' ? parsed.q : '';
|
|
1999
|
-
return { title: '
|
|
2227
|
+
return { title: toolTitle('web_search'), summary: query || friendlyArgsSummary(name, args) };
|
|
2000
2228
|
}
|
|
2001
2229
|
if (name === 'web_fetch') {
|
|
2002
2230
|
const url = typeof parsed?.url === 'string' ? parsed.url : '';
|
|
2003
|
-
return { title: '
|
|
2231
|
+
return { title: toolTitle('web_fetch'), summary: url || friendlyArgsSummary(name, args) };
|
|
2004
2232
|
}
|
|
2005
|
-
return { title: name, summary: friendlyArgsSummary(name, args) };
|
|
2233
|
+
return { title: toolTitle(name), summary: friendlyArgsSummary(name, args) };
|
|
2006
2234
|
}
|
|
2007
2235
|
/** Validate a tool/result meta payload's structured diff, mirroring the web card. */
|
|
2008
2236
|
export function diffMetaDiffs(meta) {
|
|
@@ -2042,6 +2270,76 @@ function capDisplayLines(lines, maxLines) {
|
|
|
2042
2270
|
return [marker];
|
|
2043
2271
|
return [...lines.slice(0, budget - 2), marker, ...lines.slice(-1)];
|
|
2044
2272
|
}
|
|
2273
|
+
/** Running / ok / error → ANSI color for the status dot and status word only. */
|
|
2274
|
+
export function toolStateColor(status) {
|
|
2275
|
+
if (status === 'ok')
|
|
2276
|
+
return '32';
|
|
2277
|
+
if (status === 'error')
|
|
2278
|
+
return '31';
|
|
2279
|
+
return '33';
|
|
2280
|
+
}
|
|
2281
|
+
export function toolStateLabel(status) {
|
|
2282
|
+
if (status === 'ok')
|
|
2283
|
+
return 'ok';
|
|
2284
|
+
if (status === 'error')
|
|
2285
|
+
return 'error';
|
|
2286
|
+
return 'running…';
|
|
2287
|
+
}
|
|
2288
|
+
/** Header + SGR spans: default title, dim operand, colored ● and [ok]/[error]. */
|
|
2289
|
+
export function buildToolHeader(input) {
|
|
2290
|
+
const running = input.status === undefined || input.status === 'running';
|
|
2291
|
+
const state = toolStateLabel(input.status);
|
|
2292
|
+
const exit = !running && input.command !== undefined
|
|
2293
|
+
? input.signal !== undefined
|
|
2294
|
+
? ` [信号 ${input.signal}]`
|
|
2295
|
+
: (input.exitCode ?? 0) !== 0
|
|
2296
|
+
? ` [退出码 ${input.exitCode}]`
|
|
2297
|
+
: ''
|
|
2298
|
+
: '';
|
|
2299
|
+
const spinner = input.spinner ?? '';
|
|
2300
|
+
const prefix = input.focused ? '▶ ' : ' ';
|
|
2301
|
+
const marker = input.expanded ? '▾' : '▸';
|
|
2302
|
+
const lead = `${prefix}${marker} ● ${input.title}`;
|
|
2303
|
+
const summaryText = input.summary === '' ? '' : ` ${input.summary}`;
|
|
2304
|
+
const stateToken = `[${state}]`;
|
|
2305
|
+
const tail = ` ${stateToken}${exit}${spinner}`;
|
|
2306
|
+
const plain = `${lead}${summaryText}${tail}`;
|
|
2307
|
+
const stateCode = toolStateColor(input.status);
|
|
2308
|
+
const dotIndex = lead.indexOf('●');
|
|
2309
|
+
const stateIndex = lead.length + summaryText.length + 2;
|
|
2310
|
+
const segments = [];
|
|
2311
|
+
if (dotIndex >= 0)
|
|
2312
|
+
segments.push({ start: dotIndex, end: dotIndex + '●'.length, sgr: stateCode });
|
|
2313
|
+
if (summaryText.length > 0) {
|
|
2314
|
+
segments.push({ start: lead.length, end: lead.length + summaryText.length, sgr: '90' });
|
|
2315
|
+
}
|
|
2316
|
+
segments.push({ start: stateIndex, end: stateIndex + stateToken.length + exit.length, sgr: stateCode });
|
|
2317
|
+
if (spinner !== '') {
|
|
2318
|
+
segments.push({
|
|
2319
|
+
start: stateIndex + stateToken.length + exit.length,
|
|
2320
|
+
end: plain.length,
|
|
2321
|
+
sgr: '90',
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
return { plain, segments: segments.filter(segment => segment.end > segment.start) };
|
|
2325
|
+
}
|
|
2326
|
+
/** How many terminal rows a tool body occupies after wrapping. */
|
|
2327
|
+
export function wrappedToolBodyLineCount(lines, width) {
|
|
2328
|
+
const inner = Math.max(1, width - 2);
|
|
2329
|
+
let count = 0;
|
|
2330
|
+
for (const line of lines) {
|
|
2331
|
+
count += Math.max(1, wrap(line.text, inner).length);
|
|
2332
|
+
}
|
|
2333
|
+
return count;
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* True when the full tool body plus a one-line header fits in the workspace
|
|
2337
|
+
* (the rows between the title bar and the input chrome). Oversized bodies
|
|
2338
|
+
* open a dedicated inspect overlay instead of dumping into the transcript.
|
|
2339
|
+
*/
|
|
2340
|
+
export function toolBodyFitsWorkspace(bodyLines, workspaceRows) {
|
|
2341
|
+
return bodyLines + 1 <= Math.max(1, workspaceRows);
|
|
2342
|
+
}
|
|
2045
2343
|
/** Flatten hunks into git-style `-`/`+` lines plus the web-compatible footer. */
|
|
2046
2344
|
export function renderToolDiff(diffs, maxLines) {
|
|
2047
2345
|
const rows = [];
|
|
@@ -2163,17 +2461,19 @@ function parseJsonBody(text) {
|
|
|
2163
2461
|
* converted into readable indented content instead of raw JSON text.
|
|
2164
2462
|
*/
|
|
2165
2463
|
export function toolBodyLines(row, maxLines) {
|
|
2464
|
+
const unlimited = !Number.isFinite(maxLines) || maxLines >= Number.MAX_SAFE_INTEGER;
|
|
2166
2465
|
if (row.diff !== undefined && row.diff.length > 0) {
|
|
2167
|
-
// File-edit diffs are never truncated: omitting hunks would
|
|
2168
|
-
// exact code change the model applied. `maxLines` only governs
|
|
2169
|
-
// generic JSON output bodies.
|
|
2170
|
-
return renderToolDiff(row.diff, Number.MAX_SAFE_INTEGER);
|
|
2466
|
+
// File-edit diffs are never truncated in the card: omitting hunks would
|
|
2467
|
+
// hide the exact code change the model applied. `maxLines` only governs
|
|
2468
|
+
// shell and generic JSON output bodies (and the inspect overlay).
|
|
2469
|
+
return renderToolDiff(row.diff, unlimited ? Number.MAX_SAFE_INTEGER : maxLines);
|
|
2171
2470
|
}
|
|
2172
2471
|
if (row.command !== undefined) {
|
|
2173
2472
|
const out = [];
|
|
2174
2473
|
if (row.output !== '') {
|
|
2175
|
-
|
|
2176
|
-
|
|
2474
|
+
const text = unlimited ? row.output : truncate(row.output, maxLines);
|
|
2475
|
+
for (const line of text.split('\n')) {
|
|
2476
|
+
out.push({ kind: 'tool-result', text: line });
|
|
2177
2477
|
}
|
|
2178
2478
|
}
|
|
2179
2479
|
else if (row.status !== 'running' && row.status !== undefined) {
|
|
@@ -2181,9 +2481,10 @@ export function toolBodyLines(row, maxLines) {
|
|
|
2181
2481
|
}
|
|
2182
2482
|
return out;
|
|
2183
2483
|
}
|
|
2184
|
-
const specialized = specializedToolBody(row);
|
|
2185
|
-
if (specialized !== null)
|
|
2186
|
-
return capDisplayLines(specialized, maxLines);
|
|
2484
|
+
const specialized = specializedToolBody(row, unlimited ? Number.MAX_SAFE_INTEGER : maxLines);
|
|
2485
|
+
if (specialized !== null) {
|
|
2486
|
+
return unlimited ? specialized : capDisplayLines(specialized, maxLines);
|
|
2487
|
+
}
|
|
2187
2488
|
const out = [];
|
|
2188
2489
|
const args = parseJsonArgs(row.args);
|
|
2189
2490
|
if (args !== null && Object.keys(args).length > 0) {
|
|
@@ -2201,12 +2502,13 @@ export function toolBodyLines(row, maxLines) {
|
|
|
2201
2502
|
}
|
|
2202
2503
|
}
|
|
2203
2504
|
else {
|
|
2204
|
-
|
|
2505
|
+
const text = unlimited ? row.output : truncate(row.output, maxLines);
|
|
2506
|
+
for (const line of text.split('\n')) {
|
|
2205
2507
|
out.push({ kind: 'tool-result', text: line });
|
|
2206
2508
|
}
|
|
2207
2509
|
}
|
|
2208
2510
|
}
|
|
2209
|
-
return capDisplayLines(out, maxLines);
|
|
2511
|
+
return unlimited ? out : capDisplayLines(out, maxLines);
|
|
2210
2512
|
}
|
|
2211
2513
|
function firstString(record, keys) {
|
|
2212
2514
|
for (const key of keys) {
|
|
@@ -2216,9 +2518,11 @@ function firstString(record, keys) {
|
|
|
2216
2518
|
}
|
|
2217
2519
|
return '';
|
|
2218
2520
|
}
|
|
2219
|
-
function specializedToolBody(row) {
|
|
2521
|
+
function specializedToolBody(row, maxLines = Number.MAX_SAFE_INTEGER) {
|
|
2220
2522
|
const name = row.name ?? '';
|
|
2221
2523
|
const args = parseJsonArgs(row.args);
|
|
2524
|
+
const unlimited = !Number.isFinite(maxLines) || maxLines >= Number.MAX_SAFE_INTEGER;
|
|
2525
|
+
const take = (text, fallback) => unlimited ? text : truncate(text, Math.min(maxLines, fallback));
|
|
2222
2526
|
if (name === 'todo_write' || name === 'todo') {
|
|
2223
2527
|
const todos = parsePlanTodos(args ?? row.args);
|
|
2224
2528
|
const out = [{ kind: 'diff-path', text: todoProgressLabel(todos) || '待办列表' }];
|
|
@@ -2256,7 +2560,7 @@ function specializedToolBody(row) {
|
|
|
2256
2560
|
out.push({ kind: 'tool-result', text: `offset ${offset ?? 1}${limit === undefined ? '' : ` · limit ${limit}`}` });
|
|
2257
2561
|
}
|
|
2258
2562
|
if (row.output !== '') {
|
|
2259
|
-
for (const line of
|
|
2563
|
+
for (const line of take(row.output, 40).split('\n')) {
|
|
2260
2564
|
out.push({ kind: 'tool-result', text: line });
|
|
2261
2565
|
}
|
|
2262
2566
|
}
|
|
@@ -2270,7 +2574,7 @@ function specializedToolBody(row) {
|
|
|
2270
2574
|
const path = firstString(args, ['path', 'glob']);
|
|
2271
2575
|
const out = [{ kind: 'diff-path', text: [pattern, path].filter(Boolean).join(' ') || name }];
|
|
2272
2576
|
if (row.output !== '') {
|
|
2273
|
-
for (const line of
|
|
2577
|
+
for (const line of take(row.output, 30).split('\n')) {
|
|
2274
2578
|
out.push({ kind: 'tool-result', text: line });
|
|
2275
2579
|
}
|
|
2276
2580
|
}
|
|
@@ -2280,12 +2584,27 @@ function specializedToolBody(row) {
|
|
|
2280
2584
|
const query = firstString(args, ['query', 'q', 'url']);
|
|
2281
2585
|
const out = [{ kind: 'diff-path', text: query || name }];
|
|
2282
2586
|
if (row.output !== '') {
|
|
2283
|
-
for (const line of
|
|
2587
|
+
for (const line of take(row.output, 24).split('\n')) {
|
|
2284
2588
|
out.push({ kind: 'assistant', text: line });
|
|
2285
2589
|
}
|
|
2286
2590
|
}
|
|
2287
2591
|
return out;
|
|
2288
2592
|
}
|
|
2593
|
+
if (name === 'update_goal' || name === 'create_goal' || name === 'get_goal') {
|
|
2594
|
+
const objective = args === null ? '' : firstString(args, ['objective', 'goal']);
|
|
2595
|
+
const action = args === null ? '' : firstString(args, ['action']);
|
|
2596
|
+
const out = [];
|
|
2597
|
+
if (action !== '')
|
|
2598
|
+
out.push({ kind: 'diff-path', text: action });
|
|
2599
|
+
if (objective !== '')
|
|
2600
|
+
out.push({ kind: 'assistant', text: objective });
|
|
2601
|
+
if (row.output !== '') {
|
|
2602
|
+
for (const line of take(row.output, 12).split('\n')) {
|
|
2603
|
+
out.push({ kind: 'tool-result', text: line });
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
return out.length > 0 ? out : null;
|
|
2607
|
+
}
|
|
2289
2608
|
return null;
|
|
2290
2609
|
}
|
|
2291
2610
|
/** Recover the shell tools' exit marker, mirroring @deepseek-ai/dsh-shell/render. */
|
|
@@ -2382,6 +2701,10 @@ export class SshTui {
|
|
|
2382
2701
|
lastPaintHeight = 0;
|
|
2383
2702
|
lastChromeStart = 0;
|
|
2384
2703
|
lastTranscriptStart = -1;
|
|
2704
|
+
/** 1-based screen row of the footer `目录:` chip, when painted. */
|
|
2705
|
+
cwdChipRow;
|
|
2706
|
+
/** Set when a card expand/collapse moves chrome; next paint full-redraws. */
|
|
2707
|
+
forceFullPaint = false;
|
|
2385
2708
|
paintIntervalMs;
|
|
2386
2709
|
paintLink = 'local';
|
|
2387
2710
|
paintProbed = false;
|
|
@@ -2423,7 +2746,10 @@ export class SshTui {
|
|
|
2423
2746
|
});
|
|
2424
2747
|
this.pushRow({ kind: 'brand-logo' });
|
|
2425
2748
|
this.pushRow({ kind: 'system', text: 'DeepSeek Harness — SSH TUI' });
|
|
2426
|
-
this.pushRow({ kind: 'system', text: '
|
|
2749
|
+
this.pushRow({ kind: 'system', text: t('boot.help') });
|
|
2750
|
+
if (config.cwdNotice !== undefined && config.cwdNotice !== '') {
|
|
2751
|
+
this.pushRow({ kind: /进入|Entered/u.test(config.cwdNotice) ? 'system' : 'error', text: config.cwdNotice });
|
|
2752
|
+
}
|
|
2427
2753
|
}
|
|
2428
2754
|
/** Enter raw mode, switch to the alternate screen, and start listening. */
|
|
2429
2755
|
start() {
|
|
@@ -2754,7 +3080,8 @@ export class SshTui {
|
|
|
2754
3080
|
|| row.kind === 'plan'
|
|
2755
3081
|
|| row.kind === 'question'
|
|
2756
3082
|
|| row.kind === 'goal'
|
|
2757
|
-
|| row.kind === 'compaction'
|
|
3083
|
+
|| row.kind === 'compaction'
|
|
3084
|
+
|| row.kind === 'prompt');
|
|
2758
3085
|
if (this.streaming !== undefined && this.streaming.reasoning !== '') {
|
|
2759
3086
|
this.streamingReasoning ??= { kind: 'streaming-reasoning', expanded: false };
|
|
2760
3087
|
rows.push(this.streamingReasoning);
|
|
@@ -2867,7 +3194,7 @@ export class SshTui {
|
|
|
2867
3194
|
const summary = title ?? (counts === '' ? '还没有任务' : counts);
|
|
2868
3195
|
const marker = plan.expanded ? '▾' : '▸';
|
|
2869
3196
|
const focused = this.focusedRow === plan ? '▶ ' : ' ';
|
|
2870
|
-
const header = `${focused}${marker} ${mode}${spinner} · ${summary}${plan.expanded || yieldBottom ? '' : '
|
|
3197
|
+
const header = `${focused}${marker} ${mode}${spinner} · ${summary}${plan.expanded || yieldBottom ? '' : t('card.expand')}`;
|
|
2871
3198
|
const lines = [this.styleLine('plan-dock', padToWidth(header, width))];
|
|
2872
3199
|
if (yieldBottom || !plan.expanded)
|
|
2873
3200
|
return lines;
|
|
@@ -2899,6 +3226,104 @@ export class SshTui {
|
|
|
2899
3226
|
}
|
|
2900
3227
|
return lines;
|
|
2901
3228
|
}
|
|
3229
|
+
paintToolBodyLine(addDisplay, row, line, width) {
|
|
3230
|
+
const inner = Math.max(1, width - 2);
|
|
3231
|
+
const fillRow = line.kind === 'diff-add' || line.kind === 'diff-del';
|
|
3232
|
+
for (const wrapped of wrap(line.text, inner)) {
|
|
3233
|
+
const body = fillRow ? padToWidth(` ${wrapped}`, width) : ` ${wrapped}`;
|
|
3234
|
+
const kind = line.kind;
|
|
3235
|
+
const style = kind === 'diff-add' || kind === 'diff-del' || kind === 'diff-path'
|
|
3236
|
+
? this.styleLine(kind, body)
|
|
3237
|
+
: kind === 'todo-done' || kind === 'todo-active' || kind === 'todo-pending'
|
|
3238
|
+
? this.styleLine(kind, body)
|
|
3239
|
+
: kind === 'error'
|
|
3240
|
+
? this.styleLine('error', body)
|
|
3241
|
+
: kind === 'assistant'
|
|
3242
|
+
? this.styleLine('assistant', body)
|
|
3243
|
+
: this.styleLine('tool-result', body);
|
|
3244
|
+
addDisplay(style, row);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
workspaceRowsFor(_width, height) {
|
|
3248
|
+
const header = 2;
|
|
3249
|
+
const chrome = RESERVED_BOTTOM_LINES + 1;
|
|
3250
|
+
return Math.max(1, height - header - chrome);
|
|
3251
|
+
}
|
|
3252
|
+
paintInspectOverlay(width, height) {
|
|
3253
|
+
const dialog = this.dialog;
|
|
3254
|
+
if (dialog === undefined || dialog.kind !== 'inspect')
|
|
3255
|
+
return;
|
|
3256
|
+
const header = this.styleLine('system', truncateToWidth(`工具全文 · ${dialog.title}`, width));
|
|
3257
|
+
const hint = this.styleLine('system', truncateToWidth('PgUp/PgDn/滚轮滚动 · Esc 返回会话', width));
|
|
3258
|
+
const divider = this.styleLine('system', repeatToWidth('─', width));
|
|
3259
|
+
const bodyBudget = Math.max(1, height - 4);
|
|
3260
|
+
const rendered = [];
|
|
3261
|
+
for (const line of dialog.lines) {
|
|
3262
|
+
const inner = Math.max(1, width - 2);
|
|
3263
|
+
const fillRow = line.kind === 'diff-add' || line.kind === 'diff-del';
|
|
3264
|
+
for (const wrapped of wrap(line.text, inner)) {
|
|
3265
|
+
const body = fillRow ? padToWidth(` ${wrapped}`, width) : ` ${wrapped}`;
|
|
3266
|
+
const kind = line.kind;
|
|
3267
|
+
rendered.push(kind === 'diff-add' || kind === 'diff-del' || kind === 'diff-path'
|
|
3268
|
+
? this.styleLine(kind, body)
|
|
3269
|
+
: kind === 'todo-done' || kind === 'todo-active' || kind === 'todo-pending'
|
|
3270
|
+
? this.styleLine(kind, body)
|
|
3271
|
+
: kind === 'error'
|
|
3272
|
+
? this.styleLine('error', body)
|
|
3273
|
+
: kind === 'assistant'
|
|
3274
|
+
? this.styleLine('assistant', body)
|
|
3275
|
+
: this.styleLine('tool-result', body));
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
const maxOffset = Math.max(0, rendered.length - bodyBudget);
|
|
3279
|
+
if (dialog.offset > maxOffset)
|
|
3280
|
+
dialog.offset = maxOffset;
|
|
3281
|
+
if (dialog.offset < 0)
|
|
3282
|
+
dialog.offset = 0;
|
|
3283
|
+
const slice = rendered.slice(dialog.offset, dialog.offset + bodyBudget);
|
|
3284
|
+
while (slice.length < bodyBudget)
|
|
3285
|
+
slice.push('');
|
|
3286
|
+
const pos = rendered.length === 0
|
|
3287
|
+
? '0/0'
|
|
3288
|
+
: `${dialog.offset + 1}–${Math.min(rendered.length, dialog.offset + bodyBudget)}/${rendered.length}`;
|
|
3289
|
+
const footer = this.styleLine('system', truncateToWidth(`全文 ${pos} · Esc 返回`, width));
|
|
3290
|
+
const paintRows = [header, divider, ...slice, hint, footer];
|
|
3291
|
+
this.write(composePaintOutput({
|
|
3292
|
+
width,
|
|
3293
|
+
height,
|
|
3294
|
+
paintRows,
|
|
3295
|
+
previousRows: this.lastPaintRows,
|
|
3296
|
+
sizeChanged: true,
|
|
3297
|
+
chromeChanged: true,
|
|
3298
|
+
chromeStart: 0,
|
|
3299
|
+
previousChromeStart: 0,
|
|
3300
|
+
cursorRow: height,
|
|
3301
|
+
cursorColumn: 1,
|
|
3302
|
+
}));
|
|
3303
|
+
this.lastPaintRows = paintRows.length > height ? paintRows.slice(0, height) : paintRows;
|
|
3304
|
+
this.lastChromeKey = `inspect:${dialog.offset}:${width}x${height}`;
|
|
3305
|
+
this.lastPaintWidth = width;
|
|
3306
|
+
this.lastPaintHeight = height;
|
|
3307
|
+
this.lastChromeStart = 0;
|
|
3308
|
+
this.lastTranscriptStart = -1;
|
|
3309
|
+
}
|
|
3310
|
+
openToolInspect(row) {
|
|
3311
|
+
const lines = toolBodyLines(row, Number.MAX_SAFE_INTEGER);
|
|
3312
|
+
this.openDialog({
|
|
3313
|
+
kind: 'inspect',
|
|
3314
|
+
title: `${row.title}${row.summary === '' ? '' : ` ${row.summary}`}`,
|
|
3315
|
+
lines,
|
|
3316
|
+
offset: 0,
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
closeInspect() {
|
|
3320
|
+
if (this.dialog?.kind !== 'inspect')
|
|
3321
|
+
return;
|
|
3322
|
+
this.dialog = undefined;
|
|
3323
|
+
this.forceFullPaint = true;
|
|
3324
|
+
this.markDirty();
|
|
3325
|
+
this.showNextDialog();
|
|
3326
|
+
}
|
|
2902
3327
|
paintCollapsibleHeader(addDisplay, row, kind, header, width, colorize) {
|
|
2903
3328
|
const focused = this.focusedRow === row;
|
|
2904
3329
|
const marker = row.expanded ? '▾' : '▸';
|
|
@@ -2945,8 +3370,23 @@ export class SshTui {
|
|
|
2945
3370
|
const target = focused ?? rows[rows.length - 1];
|
|
2946
3371
|
if (target === undefined)
|
|
2947
3372
|
return;
|
|
3373
|
+
this.toggleCard(target);
|
|
3374
|
+
}
|
|
3375
|
+
toggleCard(target) {
|
|
3376
|
+
if (target.kind === 'tool' && !target.expanded) {
|
|
3377
|
+
const width = Math.max(10, process.stdout.columns || 80);
|
|
3378
|
+
const height = Math.max(6, process.stdout.rows || 24);
|
|
3379
|
+
const body = toolBodyLines(target, Number.MAX_SAFE_INTEGER);
|
|
3380
|
+
const bodyRows = wrappedToolBodyLineCount(body, width);
|
|
3381
|
+
if (!toolBodyFitsWorkspace(bodyRows, this.workspaceRowsFor(width, height))) {
|
|
3382
|
+
this.focusedRow = target;
|
|
3383
|
+
this.openToolInspect(target);
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
2948
3387
|
target.expanded = !target.expanded;
|
|
2949
3388
|
this.focusedRow = target;
|
|
3389
|
+
this.forceFullPaint = true;
|
|
2950
3390
|
this.markDirty();
|
|
2951
3391
|
}
|
|
2952
3392
|
/** Expand all collapsible blocks, or collapse them again when all are open. */
|
|
@@ -2955,9 +3395,26 @@ export class SshTui {
|
|
|
2955
3395
|
if (rows.length === 0)
|
|
2956
3396
|
return;
|
|
2957
3397
|
const allExpanded = rows.every(row => row.expanded);
|
|
2958
|
-
|
|
2959
|
-
row
|
|
2960
|
-
|
|
3398
|
+
if (allExpanded) {
|
|
3399
|
+
for (const row of rows)
|
|
3400
|
+
row.expanded = false;
|
|
3401
|
+
this.focusedRow = null;
|
|
3402
|
+
}
|
|
3403
|
+
else {
|
|
3404
|
+
const width = Math.max(10, process.stdout.columns || 80);
|
|
3405
|
+
const height = Math.max(6, process.stdout.rows || 24);
|
|
3406
|
+
const workspace = this.workspaceRowsFor(width, height);
|
|
3407
|
+
for (const row of rows) {
|
|
3408
|
+
if (row.kind === 'tool') {
|
|
3409
|
+
const bodyRows = wrappedToolBodyLineCount(toolBodyLines(row, Number.MAX_SAFE_INTEGER), width);
|
|
3410
|
+
if (!toolBodyFitsWorkspace(bodyRows, workspace))
|
|
3411
|
+
continue;
|
|
3412
|
+
}
|
|
3413
|
+
row.expanded = true;
|
|
3414
|
+
}
|
|
3415
|
+
this.focusedRow = rows[rows.length - 1] ?? null;
|
|
3416
|
+
}
|
|
3417
|
+
this.forceFullPaint = true;
|
|
2961
3418
|
this.markDirty();
|
|
2962
3419
|
}
|
|
2963
3420
|
highlightSearchLine(line) {
|
|
@@ -2968,9 +3425,21 @@ export class SshTui {
|
|
|
2968
3425
|
revealRow(row) {
|
|
2969
3426
|
if (row === undefined)
|
|
2970
3427
|
return;
|
|
3428
|
+
if (row.kind === 'tool') {
|
|
3429
|
+
const width = Math.max(10, process.stdout.columns || 80);
|
|
3430
|
+
const height = Math.max(6, process.stdout.rows || 24);
|
|
3431
|
+
const body = toolBodyLines(row, Number.MAX_SAFE_INTEGER);
|
|
3432
|
+
const bodyRows = wrappedToolBodyLineCount(body, width);
|
|
3433
|
+
if (!toolBodyFitsWorkspace(bodyRows, this.workspaceRowsFor(width, height))) {
|
|
3434
|
+
this.focusedRow = row;
|
|
3435
|
+
this.openToolInspect(row);
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
2971
3439
|
if (row.kind !== 'assistant' && 'expanded' in row) {
|
|
2972
3440
|
row.expanded = true;
|
|
2973
3441
|
this.focusedRow = row;
|
|
3442
|
+
this.forceFullPaint = true;
|
|
2974
3443
|
}
|
|
2975
3444
|
else {
|
|
2976
3445
|
this.focusedRow = null;
|
|
@@ -2987,18 +3456,18 @@ export class SshTui {
|
|
|
2987
3456
|
const live = this.findLivePlanRow();
|
|
2988
3457
|
if (live !== undefined) {
|
|
2989
3458
|
this.focusCard(live);
|
|
2990
|
-
this.pushRow({ kind: 'system', text:
|
|
3459
|
+
this.pushRow({ kind: 'system', text: t('jump.planDock', { category: cardCategoryLabel(category) }) });
|
|
2991
3460
|
this.revealRow(live);
|
|
2992
3461
|
return;
|
|
2993
3462
|
}
|
|
2994
3463
|
}
|
|
2995
3464
|
const target = this.rows.findLast(row => cardCategoryOf(row) === category);
|
|
2996
3465
|
if (target === undefined) {
|
|
2997
|
-
this.pushRow({ kind: 'system', text:
|
|
3466
|
+
this.pushRow({ kind: 'system', text: t('jump.missing', { category: cardCategoryLabel(category) }) });
|
|
2998
3467
|
this.markDirty();
|
|
2999
3468
|
return;
|
|
3000
3469
|
}
|
|
3001
|
-
this.pushRow({ kind: 'system', text:
|
|
3470
|
+
this.pushRow({ kind: 'system', text: t('jump.latest', { category: cardCategoryLabel(category) }) });
|
|
3002
3471
|
this.revealRow(target);
|
|
3003
3472
|
}
|
|
3004
3473
|
applySearchHits(query, hits) {
|
|
@@ -3012,7 +3481,7 @@ export class SshTui {
|
|
|
3012
3481
|
}
|
|
3013
3482
|
this.searchIndex = hits.length - 1;
|
|
3014
3483
|
const hit = hits[this.searchIndex];
|
|
3015
|
-
const where = hit === undefined ? '' :
|
|
3484
|
+
const where = hit === undefined ? '' : cardCategoryLabel(cardCategoryOf(hit) ?? 'reply');
|
|
3016
3485
|
this.pushRow({
|
|
3017
3486
|
kind: 'system',
|
|
3018
3487
|
text: `找到 ${hits.length} 条${query === '' ? '' : `「${query}」`} · 第 ${hits.length}/${hits.length} 条(${where})。Ctrl+G / Alt+N 下一条,Alt+P 上一条。`,
|
|
@@ -3021,7 +3490,7 @@ export class SshTui {
|
|
|
3021
3490
|
}
|
|
3022
3491
|
runFindCommand(arg) {
|
|
3023
3492
|
const parsed = parseFindQuery(arg);
|
|
3024
|
-
const label = parsed.category === undefined ? '' : `${
|
|
3493
|
+
const label = parsed.category === undefined ? '' : `${cardCategoryLabel(parsed.category)} `;
|
|
3025
3494
|
const hits = matchTranscriptRows(this.rows, arg);
|
|
3026
3495
|
this.applySearchHits(`${label}${parsed.query}`.trim(), hits);
|
|
3027
3496
|
}
|
|
@@ -3034,7 +3503,7 @@ export class SshTui {
|
|
|
3034
3503
|
const count = this.searchHits.length;
|
|
3035
3504
|
this.searchIndex = (this.searchIndex + delta + count) % count;
|
|
3036
3505
|
const hit = this.searchHits[this.searchIndex];
|
|
3037
|
-
const where = hit === undefined ? '' :
|
|
3506
|
+
const where = hit === undefined ? '' : cardCategoryLabel(cardCategoryOf(hit) ?? 'reply');
|
|
3038
3507
|
this.pushRow({
|
|
3039
3508
|
kind: 'system',
|
|
3040
3509
|
text: `搜索「${this.searchQuery}」· 第 ${this.searchIndex + 1}/${count} 条(${where})。`,
|
|
@@ -3046,6 +3515,10 @@ export class SshTui {
|
|
|
3046
3515
|
return;
|
|
3047
3516
|
const width = Math.max(10, process.stdout.columns || 80);
|
|
3048
3517
|
const height = Math.max(6, process.stdout.rows || 24);
|
|
3518
|
+
if (this.dialog?.kind === 'inspect') {
|
|
3519
|
+
this.paintInspectOverlay(width, height);
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3049
3522
|
const display = [];
|
|
3050
3523
|
const displayRefs = [];
|
|
3051
3524
|
const searchHit = this.searchHits[this.searchIndex];
|
|
@@ -3083,7 +3556,7 @@ export class SshTui {
|
|
|
3083
3556
|
const focused = this.focusedRow === row;
|
|
3084
3557
|
const marker = row.expanded ? '▾' : '▸';
|
|
3085
3558
|
const lines = row.text.split('\n').length;
|
|
3086
|
-
const header = `${marker} 已思考 · ${lines} 行${row.expanded ? '' : '
|
|
3559
|
+
const header = `${marker} 已思考 · ${lines} 行${row.expanded ? '' : t('card.expand')}`;
|
|
3087
3560
|
const line = `${focused ? '▶ ' : ' '}${header}`;
|
|
3088
3561
|
const styled = this.styleLine('reasoning', line);
|
|
3089
3562
|
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
@@ -3096,75 +3569,62 @@ export class SshTui {
|
|
|
3096
3569
|
}
|
|
3097
3570
|
if (row.kind === 'tool') {
|
|
3098
3571
|
const running = row.status === undefined || row.status === 'running';
|
|
3099
|
-
const ok = row.status === 'ok';
|
|
3100
|
-
// The status dot carries its own ANSI color. `styleLine` sanitizes its
|
|
3101
|
-
// input, so embedding the escape sequence there would leave literal
|
|
3102
|
-
// "[33m" text on screen; color the dot between two sanitized halves.
|
|
3103
|
-
const dotColor = !this.color ? undefined : running ? '33' : ok ? '32' : '31';
|
|
3104
|
-
const styleToolHeader = (line) => {
|
|
3105
|
-
const safe = sanitizeTerminalText(line);
|
|
3106
|
-
if (!this.color)
|
|
3107
|
-
return safe;
|
|
3108
|
-
const dotIndex = safe.indexOf('●');
|
|
3109
|
-
if (dotColor === undefined || dotIndex === -1)
|
|
3110
|
-
return this.styleLine('tool', safe);
|
|
3111
|
-
return `\x1b[33m${safe.slice(0, dotIndex)}\x1b[${dotColor}m●\x1b[33m${safe.slice(dotIndex + 1)}\x1b[0m`;
|
|
3112
|
-
};
|
|
3113
|
-
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3114
|
-
const state = running ? 'running…' : ok ? 'ok' : 'error';
|
|
3115
|
-
const summary = row.summary === '' ? '' : ` ${row.summary}`;
|
|
3116
|
-
const exit = !running && row.command !== undefined
|
|
3117
|
-
? row.signal !== undefined
|
|
3118
|
-
? ` [信号 ${row.signal}]`
|
|
3119
|
-
: (row.exitCode ?? 0) !== 0
|
|
3120
|
-
? ` [退出码 ${row.exitCode}]`
|
|
3121
|
-
: ''
|
|
3122
|
-
: '';
|
|
3123
3572
|
const focused = this.focusedRow === row;
|
|
3124
|
-
const
|
|
3125
|
-
|
|
3573
|
+
const header = buildToolHeader({
|
|
3574
|
+
focused,
|
|
3575
|
+
expanded: row.expanded,
|
|
3576
|
+
title: row.title,
|
|
3577
|
+
summary: row.summary,
|
|
3578
|
+
status: row.status,
|
|
3579
|
+
command: row.command,
|
|
3580
|
+
signal: row.signal,
|
|
3581
|
+
exitCode: row.exitCode,
|
|
3582
|
+
spinner: running ? ` ${this.spinnerFrame()}` : '',
|
|
3583
|
+
});
|
|
3584
|
+
const headerSegments = this.color ? header.segments : [];
|
|
3126
3585
|
if (!row.expanded) {
|
|
3127
|
-
const collapsed = truncateToWidth(
|
|
3128
|
-
const styled =
|
|
3586
|
+
const collapsed = truncateToWidth(header.plain, Math.max(1, width - 2));
|
|
3587
|
+
const styled = headerSegments.length === 0
|
|
3588
|
+
? collapsed
|
|
3589
|
+
: paintSegmentedLine(collapsed, 0, collapsed.length, headerSegments);
|
|
3129
3590
|
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
3130
3591
|
continue;
|
|
3131
3592
|
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3593
|
+
const expandedHeaderLines = headerSegments.length === 0
|
|
3594
|
+
? wrap(header.plain, width)
|
|
3595
|
+
: wrapSegmented(header.plain, Math.max(1, width), headerSegments);
|
|
3596
|
+
for (const wrapped of expandedHeaderLines) {
|
|
3597
|
+
addDisplay(wrapped, row);
|
|
3134
3598
|
}
|
|
3135
|
-
for (const line of toolBodyLines(row,
|
|
3136
|
-
|
|
3137
|
-
const fillRow = line.kind === 'diff-add' || line.kind === 'diff-del';
|
|
3138
|
-
for (const wrapped of wrap(line.text, inner)) {
|
|
3139
|
-
const body = fillRow ? padToWidth(` ${wrapped}`, width) : ` ${wrapped}`;
|
|
3140
|
-
addDisplay(this.styleLine(line.kind, body), row);
|
|
3141
|
-
}
|
|
3599
|
+
for (const line of toolBodyLines(row, Number.MAX_SAFE_INTEGER)) {
|
|
3600
|
+
this.paintToolBodyLine(addDisplay, row, line, width);
|
|
3142
3601
|
}
|
|
3143
3602
|
continue;
|
|
3144
3603
|
}
|
|
3145
3604
|
if (row.kind === 'subagent') {
|
|
3146
3605
|
const running = row.status === 'running';
|
|
3147
3606
|
const ok = row.status === 'ok';
|
|
3148
|
-
const
|
|
3607
|
+
const aborted = row.status === 'aborted';
|
|
3608
|
+
const dotColor = !this.color ? undefined : running ? '33' : ok ? '32' : aborted ? '33' : '31';
|
|
3149
3609
|
const styleHeader = (line) => {
|
|
3150
3610
|
const safe = sanitizeTerminalText(line);
|
|
3151
3611
|
if (!this.color)
|
|
3152
3612
|
return safe;
|
|
3153
3613
|
const dotIndex = safe.indexOf('●');
|
|
3154
3614
|
if (dotColor === undefined || dotIndex === -1)
|
|
3155
|
-
return
|
|
3156
|
-
return
|
|
3615
|
+
return safe;
|
|
3616
|
+
return `${safe.slice(0, dotIndex)}\x1b[${dotColor}m●\x1b[0m${safe.slice(dotIndex + 1)}`;
|
|
3157
3617
|
};
|
|
3158
3618
|
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3159
|
-
const header = `● ${subagentHeaderText(row)}${spinner}${row.expanded ? '' : '
|
|
3160
|
-
this.paintCollapsibleHeader(addDisplay, row, '
|
|
3619
|
+
const header = `● ${subagentHeaderText(row)}${spinner}${row.expanded ? '' : t('card.expand')}`;
|
|
3620
|
+
this.paintCollapsibleHeader(addDisplay, row, 'system', header, width, styleHeader);
|
|
3161
3621
|
if (row.expanded) {
|
|
3162
|
-
addDisplay(this.styleLine('
|
|
3622
|
+
addDisplay(this.styleLine('system', ` 会话 ${row.sessionId} · ${row.provider}${row.local ? '' : ' · 外部进程'}`), row);
|
|
3163
3623
|
if (row.stopReason !== undefined) {
|
|
3164
|
-
addDisplay(this.styleLine('
|
|
3624
|
+
addDisplay(this.styleLine('system', ` 结束原因:${row.stopReason}`), row);
|
|
3165
3625
|
}
|
|
3166
3626
|
if (row.logs.length === 0) {
|
|
3167
|
-
addDisplay(this.styleLine('
|
|
3627
|
+
addDisplay(this.styleLine('system', running ? ' 等待子代理输出…' : ' 没有可见输出'), row);
|
|
3168
3628
|
}
|
|
3169
3629
|
else {
|
|
3170
3630
|
for (const entry of row.logs) {
|
|
@@ -3172,7 +3632,7 @@ export class SshTui {
|
|
|
3172
3632
|
? 'assistant'
|
|
3173
3633
|
: entry.kind === 'result' && row.status === 'error'
|
|
3174
3634
|
? 'error'
|
|
3175
|
-
: '
|
|
3635
|
+
: 'system';
|
|
3176
3636
|
for (const wrapped of wrap(entry.text, Math.max(1, width - 2))) {
|
|
3177
3637
|
addDisplay(this.styleLine(kind, ` ${wrapped}`), row);
|
|
3178
3638
|
}
|
|
@@ -3187,7 +3647,7 @@ export class SshTui {
|
|
|
3187
3647
|
const counts = todoProgressLabel(row.todos);
|
|
3188
3648
|
const title = planTitleFromMarkdown(row.planMarkdown ?? '');
|
|
3189
3649
|
const summary = title ?? (counts === '' ? '已归档' : counts);
|
|
3190
|
-
const header = `计划 · ${summary}${row.expanded ? '' : '
|
|
3650
|
+
const header = `计划 · ${summary}${row.expanded ? '' : t('card.expand')}`;
|
|
3191
3651
|
this.paintCollapsibleHeader(addDisplay, row, 'plan-dock', header, width);
|
|
3192
3652
|
if (row.expanded) {
|
|
3193
3653
|
addDisplay(this.styleLine('plan-dock', ` ${planDockNote({ ...row, active: false, pending: false })}`), row);
|
|
@@ -3205,12 +3665,22 @@ export class SshTui {
|
|
|
3205
3665
|
}
|
|
3206
3666
|
continue;
|
|
3207
3667
|
}
|
|
3668
|
+
if (row.kind === 'prompt') {
|
|
3669
|
+
const header = `● ${promptInjectionTitle(row.sources)}${row.expanded ? '' : t('card.expand')}`;
|
|
3670
|
+
this.paintCollapsibleHeader(addDisplay, row, 'system', header, width);
|
|
3671
|
+
if (row.expanded) {
|
|
3672
|
+
for (const wrapped of wrap(row.text, Math.max(1, width - 2))) {
|
|
3673
|
+
addDisplay(this.styleLine('system', ` ${wrapped}`), row);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
continue;
|
|
3677
|
+
}
|
|
3208
3678
|
if (row.kind === 'question') {
|
|
3209
3679
|
const waiting = row.status === 'waiting';
|
|
3210
3680
|
const spinner = waiting ? ` ${this.spinnerFrame()}` : '';
|
|
3211
3681
|
const state = waiting ? '等待回答' : row.status === 'answered' ? '已回答' : '已取消';
|
|
3212
3682
|
const title = row.intent === 'plan-review' ? '计划待审' : '提问用户';
|
|
3213
|
-
const header = `● ${title}${spinner} · ${state} · ${row.summary}${row.expanded ? '' : '
|
|
3683
|
+
const header = `● ${title}${spinner} · ${state} · ${row.summary}${row.expanded ? '' : t('card.expand')}`;
|
|
3214
3684
|
this.paintCollapsibleHeader(addDisplay, row, waiting ? 'tool' : 'system', header, width);
|
|
3215
3685
|
if (row.expanded) {
|
|
3216
3686
|
if (row.header !== undefined)
|
|
@@ -3244,7 +3714,7 @@ export class SshTui {
|
|
|
3244
3714
|
: row.phase === 'blocked' ? '受阻'
|
|
3245
3715
|
: row.phase === 'complete' ? '已完成'
|
|
3246
3716
|
: '已清除';
|
|
3247
|
-
const header = `● 目标${spinner} · ${phase} · ${row.objective}${row.expanded ? '' : '
|
|
3717
|
+
const header = `● 目标${spinner} · ${phase} · ${row.objective}${row.expanded ? '' : t('card.expand')}`;
|
|
3248
3718
|
this.paintCollapsibleHeader(addDisplay, row, live ? 'tool' : 'system', header, width);
|
|
3249
3719
|
if (row.expanded) {
|
|
3250
3720
|
addDisplay(this.styleLine('system', ' 用 /goal 查看、暂停、恢复或清除当前目标。'), row);
|
|
@@ -3260,7 +3730,7 @@ export class SshTui {
|
|
|
3260
3730
|
const running = row.status === 'running';
|
|
3261
3731
|
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3262
3732
|
const elapsed = Math.max(0, Math.floor(((row.endedAt ?? Date.now()) - row.startedAt) / 1000));
|
|
3263
|
-
const header = `● ${compactionHeaderText(row)}${spinner} · ${elapsed}s${row.expanded ? '' : '
|
|
3733
|
+
const header = `● ${compactionHeaderText(row)}${spinner} · ${elapsed}s${row.expanded ? '' : t('card.expand')}`;
|
|
3264
3734
|
this.paintCollapsibleHeader(addDisplay, row, running ? 'tool' : row.status === 'error' ? 'error' : 'system', header, width);
|
|
3265
3735
|
if (row.expanded) {
|
|
3266
3736
|
addDisplay(this.styleLine('system', running
|
|
@@ -3324,53 +3794,57 @@ export class SshTui {
|
|
|
3324
3794
|
else if (this.dialog.kind === 'onboarding') {
|
|
3325
3795
|
const ob = this.onboarding;
|
|
3326
3796
|
if (ob !== undefined) {
|
|
3327
|
-
const template =
|
|
3797
|
+
const template = providerTemplates()[ob.providerType];
|
|
3328
3798
|
const providerLabel = `${template.label}${template.defaultBaseUrl === '' ? '' : `(${template.defaultBaseUrl})`}`;
|
|
3329
3799
|
switch (ob.step) {
|
|
3330
3800
|
case 'provider':
|
|
3331
|
-
addDialog('
|
|
3332
|
-
addDialog('
|
|
3333
|
-
addDialog('
|
|
3334
|
-
addDialog('
|
|
3335
|
-
addDialog('
|
|
3336
|
-
addDialog('
|
|
3337
|
-
addDialog('
|
|
3801
|
+
addDialog(t('onboard.title'));
|
|
3802
|
+
addDialog(t('onboard.opt1'));
|
|
3803
|
+
addDialog(t('onboard.opt2'));
|
|
3804
|
+
addDialog(t('onboard.opt3'));
|
|
3805
|
+
addDialog(t('onboard.opt4'));
|
|
3806
|
+
addDialog(t('onboard.opt5'));
|
|
3807
|
+
addDialog(t('onboard.pickHint'));
|
|
3338
3808
|
break;
|
|
3339
3809
|
case 'id':
|
|
3340
|
-
addDialog(
|
|
3341
|
-
addDialog('
|
|
3342
|
-
addDialog(
|
|
3343
|
-
addDialog('
|
|
3810
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3811
|
+
addDialog(t('onboard.idPrompt'));
|
|
3812
|
+
addDialog(t('onboard.default', { value: template.defaultId }));
|
|
3813
|
+
addDialog(t('onboard.enterEsc'));
|
|
3344
3814
|
break;
|
|
3345
3815
|
case 'key':
|
|
3346
|
-
addDialog(
|
|
3347
|
-
addDialog('
|
|
3348
|
-
addDialog('
|
|
3816
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3817
|
+
addDialog(t('onboard.keyPrompt'));
|
|
3818
|
+
addDialog(t('onboard.enterEsc'));
|
|
3349
3819
|
break;
|
|
3350
3820
|
case 'base-url':
|
|
3351
|
-
addDialog(
|
|
3352
|
-
addDialog(
|
|
3353
|
-
addDialog('
|
|
3821
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3822
|
+
addDialog(t('onboard.basePrompt', { fallback: template.defaultBaseUrl || t('onboard.baseFallback') }));
|
|
3823
|
+
addDialog(t('onboard.enterEsc'));
|
|
3354
3824
|
break;
|
|
3355
3825
|
case 'models':
|
|
3356
|
-
addDialog(
|
|
3357
|
-
addDialog('
|
|
3826
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3827
|
+
addDialog(t('onboard.modelsPrompt'));
|
|
3358
3828
|
addDialog(ob.models.length > 0
|
|
3359
|
-
?
|
|
3360
|
-
:
|
|
3829
|
+
? t('onboard.modelsFetched', { count: ob.models.length, list: formatModelList(ob.models, 6) })
|
|
3830
|
+
: t('onboard.default', { value: template.defaultModels.join(', ') }));
|
|
3361
3831
|
if (template.api !== undefined)
|
|
3362
|
-
addDialog('
|
|
3363
|
-
addDialog('
|
|
3832
|
+
addDialog(t('onboard.ctrlF'));
|
|
3833
|
+
addDialog(t('onboard.enterEsc'));
|
|
3364
3834
|
break;
|
|
3365
3835
|
case 'confirm':
|
|
3366
|
-
addDialog('
|
|
3367
|
-
addDialog(
|
|
3836
|
+
addDialog(t('onboard.confirmTitle'));
|
|
3837
|
+
addDialog(t('onboard.confirmProvider', { label: providerLabel }));
|
|
3368
3838
|
addDialog(` Provider ID: ${ob.providerId}`);
|
|
3369
|
-
addDialog(
|
|
3370
|
-
addDialog(
|
|
3371
|
-
addDialog(
|
|
3372
|
-
addDialog(
|
|
3373
|
-
|
|
3839
|
+
addDialog(t('onboard.confirmBase', { url: ob.baseUrl === '' ? (template.defaultBaseUrl || t('onboard.defaultParen')) : ob.baseUrl }));
|
|
3840
|
+
addDialog(t('onboard.confirmApi', { api: template.api ?? 'deepseek-official' }));
|
|
3841
|
+
addDialog(t('onboard.confirmModels', { list: formatModelList(ob.models, 8) }));
|
|
3842
|
+
addDialog(t('onboard.confirmKey', {
|
|
3843
|
+
head: sliceCodePoints(ob.key, 6),
|
|
3844
|
+
tail: lastCodePoints(ob.key, 4),
|
|
3845
|
+
length: ob.key.length,
|
|
3846
|
+
}));
|
|
3847
|
+
addDialog(t('onboard.confirmHint'));
|
|
3374
3848
|
break;
|
|
3375
3849
|
}
|
|
3376
3850
|
}
|
|
@@ -3582,6 +4056,7 @@ export class SshTui {
|
|
|
3582
4056
|
foldedInput: inputView.folded,
|
|
3583
4057
|
multiLineInput: inputRows > 1,
|
|
3584
4058
|
queued: this.pendingMessages.size,
|
|
4059
|
+
cwdLabel: formatFooterCwd(this.workspaceCwd()),
|
|
3585
4060
|
};
|
|
3586
4061
|
const activity = footerActivity(footer);
|
|
3587
4062
|
const activityText = activity.kind === 'compacting'
|
|
@@ -3589,7 +4064,8 @@ export class SshTui {
|
|
|
3589
4064
|
: activity.kind === 'subagents'
|
|
3590
4065
|
? `${this.spinnerFrame(160)} ${activity.text}`
|
|
3591
4066
|
: activity.text;
|
|
3592
|
-
const
|
|
4067
|
+
const identity = footerIdentityParts(footer);
|
|
4068
|
+
const statusText = fitFooterStatusLine(activityText, identity, Math.max(1, width));
|
|
3593
4069
|
const statusLine = this.styleLine('system', statusText);
|
|
3594
4070
|
const paintRows = [
|
|
3595
4071
|
...headerLines,
|
|
@@ -3628,7 +4104,11 @@ export class SshTui {
|
|
|
3628
4104
|
].join('\x1f');
|
|
3629
4105
|
const chromeChanged = chromeKey !== this.lastChromeKey || chromeStart !== this.lastChromeStart;
|
|
3630
4106
|
const transcriptScrolled = start !== this.lastTranscriptStart;
|
|
3631
|
-
const sizeChanged =
|
|
4107
|
+
const sizeChanged = this.forceFullPaint
|
|
4108
|
+
|| width !== this.lastPaintWidth
|
|
4109
|
+
|| height !== this.lastPaintHeight
|
|
4110
|
+
|| transcriptScrolled;
|
|
4111
|
+
this.forceFullPaint = false;
|
|
3632
4112
|
// One stdout write per frame: dirty rows only, so jump-host SSH sees a
|
|
3633
4113
|
// single packet instead of one write per line. Clip/pad so leftover
|
|
3634
4114
|
// wide glyphs cannot wrap into the input box.
|
|
@@ -3652,7 +4132,19 @@ export class SshTui {
|
|
|
3652
4132
|
this.lastPaintHeight = height;
|
|
3653
4133
|
this.lastChromeStart = chromeStart;
|
|
3654
4134
|
this.lastTranscriptStart = start;
|
|
4135
|
+
const cwdChip = formatFooterCwd(this.workspaceCwd());
|
|
4136
|
+
this.cwdChipRow = cwdChip !== '' && statusText.includes(cwdChip)
|
|
4137
|
+
? Math.min(height, paintRows.length)
|
|
4138
|
+
: undefined;
|
|
3655
4139
|
};
|
|
4140
|
+
workspaceCwd() {
|
|
4141
|
+
return this.agent.session.header?.cwd ?? process.cwd();
|
|
4142
|
+
}
|
|
4143
|
+
announceWorkspaceCwd() {
|
|
4144
|
+
const cwd = this.workspaceCwd();
|
|
4145
|
+
this.pushRow({ kind: 'system', text: t('cwd.full', { cwd }) });
|
|
4146
|
+
this.markDirty();
|
|
4147
|
+
}
|
|
3656
4148
|
buildSuggestions() {
|
|
3657
4149
|
const input = this.input;
|
|
3658
4150
|
if (!input.startsWith('/'))
|
|
@@ -3664,7 +4156,7 @@ export class SshTui {
|
|
|
3664
4156
|
local: false,
|
|
3665
4157
|
}));
|
|
3666
4158
|
const all = [
|
|
3667
|
-
...
|
|
4159
|
+
...localizedCommands().map(command => ({ name: command.name, description: command.description, local: true })),
|
|
3668
4160
|
...dsh,
|
|
3669
4161
|
];
|
|
3670
4162
|
const filtered = prefix === ''
|
|
@@ -3799,7 +4291,7 @@ export class SshTui {
|
|
|
3799
4291
|
kind === 'assistant' ? '1;37' :
|
|
3800
4292
|
kind === 'reasoning' ? '2;3' :
|
|
3801
4293
|
kind === 'brand' ? '1;38;2;77;107;253' :
|
|
3802
|
-
kind === 'tool' || kind === 'tool-result' ? '
|
|
4294
|
+
kind === 'tool' || kind === 'tool-result' ? '37' :
|
|
3803
4295
|
// Codex-like: muted add/del that blend into the terminal background.
|
|
3804
4296
|
kind === 'diff-add' ? '38;2;122;168;116;48;2;18;42;24' :
|
|
3805
4297
|
kind === 'diff-del' ? '38;2;196;122;122;48;2;48;20;20' :
|
|
@@ -3828,7 +4320,7 @@ export class SshTui {
|
|
|
3828
4320
|
const provider = selection.provider ?? parentProvider;
|
|
3829
4321
|
const model = subagentModelMatchesProvider(provider, selection.model)
|
|
3830
4322
|
? selection.model
|
|
3831
|
-
: defaultSubagentModelForProvider(provider);
|
|
4323
|
+
: defaultSubagentModelForProvider(provider, [], this.selectionRef?.current?.model);
|
|
3832
4324
|
return {
|
|
3833
4325
|
...resolved,
|
|
3834
4326
|
provider,
|
|
@@ -3850,11 +4342,15 @@ export class SshTui {
|
|
|
3850
4342
|
.map(block => block.text)
|
|
3851
4343
|
.join('');
|
|
3852
4344
|
if (text !== '') {
|
|
3853
|
-
const
|
|
4345
|
+
const source = event.data.source;
|
|
4346
|
+
const sourceKind = source.kind ?? '';
|
|
3854
4347
|
if (sourceKind === 'user') {
|
|
3855
4348
|
this.pushRow({ kind: 'user', text: `❯ ${text}` });
|
|
3856
4349
|
}
|
|
3857
|
-
else if (sourceKind
|
|
4350
|
+
else if (isPromptInjectionMessage(sourceKind, text, source.plugin)) {
|
|
4351
|
+
this.pushPromptInjection(text, source.plugin);
|
|
4352
|
+
}
|
|
4353
|
+
else if (sourceKind === 'plugin' && source.form === 'snapshot') {
|
|
3858
4354
|
this.pushRow({ kind: 'system', text: text });
|
|
3859
4355
|
}
|
|
3860
4356
|
else {
|
|
@@ -3943,22 +4439,24 @@ export class SshTui {
|
|
|
3943
4439
|
case 'tool/call': {
|
|
3944
4440
|
this.openToolCalls.set(String(event.data.callId), event.data.name);
|
|
3945
4441
|
this.pendingToolTimes.set(String(event.data.callId), event.time);
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
4442
|
+
if (!HIDDEN_TOOL_NAMES.has(event.data.name)) {
|
|
4443
|
+
const present = presentToolCall(event.data.name, event.data.arguments);
|
|
4444
|
+
const row = {
|
|
4445
|
+
kind: 'tool',
|
|
4446
|
+
callId: event.data.callId,
|
|
4447
|
+
name: event.data.name,
|
|
4448
|
+
args: event.data.arguments,
|
|
4449
|
+
status: 'running',
|
|
4450
|
+
output: '',
|
|
4451
|
+
title: present.title,
|
|
4452
|
+
summary: present.summary,
|
|
4453
|
+
...present.command === undefined ? {} : { command: present.command },
|
|
4454
|
+
...present.cwd === undefined ? {} : { cwd: present.cwd },
|
|
4455
|
+
...present.diff === undefined ? {} : { diff: present.diff },
|
|
4456
|
+
expanded: false,
|
|
4457
|
+
};
|
|
4458
|
+
this.pushRow(row);
|
|
4459
|
+
}
|
|
3962
4460
|
if (event.data.name === 'exit_plan_mode') {
|
|
3963
4461
|
const markdown = planMarkdownFromArgs(event.data.arguments);
|
|
3964
4462
|
if (markdown !== undefined)
|
|
@@ -3981,8 +4479,6 @@ export class SshTui {
|
|
|
3981
4479
|
const metaDiffs = diffMetaDiffs(event.data.meta);
|
|
3982
4480
|
if (metaDiffs !== null) {
|
|
3983
4481
|
row.diff = metaDiffs;
|
|
3984
|
-
if (DIFF_TOOL_NAMES.has(row.name))
|
|
3985
|
-
row.expanded = true;
|
|
3986
4482
|
}
|
|
3987
4483
|
const isShell = SHELL_TOOL_NAMES.has(row.name);
|
|
3988
4484
|
if (isShell) {
|
|
@@ -4382,6 +4878,16 @@ export class SshTui {
|
|
|
4382
4878
|
this.pushRow({ kind: 'system', text: `${notice}:${objective}` });
|
|
4383
4879
|
this.markDirty();
|
|
4384
4880
|
}
|
|
4881
|
+
pushPromptInjection(text, plugin) {
|
|
4882
|
+
const sources = promptInjectionSources(text, plugin);
|
|
4883
|
+
this.pushRow({
|
|
4884
|
+
kind: 'prompt',
|
|
4885
|
+
sources,
|
|
4886
|
+
text,
|
|
4887
|
+
...(plugin === undefined ? {} : { plugin }),
|
|
4888
|
+
expanded: false,
|
|
4889
|
+
});
|
|
4890
|
+
}
|
|
4385
4891
|
handleSubagentExtensionEvent(row, event) {
|
|
4386
4892
|
const type = String(event.type);
|
|
4387
4893
|
const data = event.data;
|
|
@@ -4417,6 +4923,8 @@ export class SshTui {
|
|
|
4417
4923
|
break;
|
|
4418
4924
|
}
|
|
4419
4925
|
case 'tool/call': {
|
|
4926
|
+
if (HIDDEN_TOOL_NAMES.has(event.data.name))
|
|
4927
|
+
break;
|
|
4420
4928
|
const present = presentToolCall(event.data.name, event.data.arguments);
|
|
4421
4929
|
appendSubagentLog(row, { kind: 'tool', text: `▶ ${present.title} ${present.summary}` });
|
|
4422
4930
|
break;
|
|
@@ -4713,7 +5221,7 @@ export class SshTui {
|
|
|
4713
5221
|
/** Default listing endpoint for a built-in OpenCode route with no stored base URL. */
|
|
4714
5222
|
openCodeListingBaseURL(provider) {
|
|
4715
5223
|
if (provider === 'opencode-go')
|
|
4716
|
-
return
|
|
5224
|
+
return providerTemplates()['opencode-go'].defaultBaseUrl;
|
|
4717
5225
|
if (provider === 'opencode')
|
|
4718
5226
|
return OPENCODE_ZEN_BASE_URL;
|
|
4719
5227
|
return undefined;
|
|
@@ -5051,7 +5559,7 @@ export class SshTui {
|
|
|
5051
5559
|
if (this.selectionRef !== undefined)
|
|
5052
5560
|
this.selectionRef.current = next;
|
|
5053
5561
|
this.onSelectionChanged?.(next);
|
|
5054
|
-
await this.
|
|
5562
|
+
await this.persistDefaultSelection(next);
|
|
5055
5563
|
await this.rememberRoute(next);
|
|
5056
5564
|
const kind = describeProviderRoute(provider);
|
|
5057
5565
|
this.pushRow({
|
|
@@ -5062,7 +5570,6 @@ export class SshTui {
|
|
|
5062
5570
|
const previousProvider = current?.provider ?? this.agent.options.provider ?? this.providerName;
|
|
5063
5571
|
if (previousProvider !== provider) {
|
|
5064
5572
|
await this.syncSubagentToProvider(provider, listedIds, true);
|
|
5065
|
-
await this.promptSubagentAfterProviderSwitch(provider);
|
|
5066
5573
|
this.clearQuotaForProvider(provider);
|
|
5067
5574
|
void this.refreshQuota({ reason: 'command', announce: false }).catch(() => { });
|
|
5068
5575
|
}
|
|
@@ -5071,6 +5578,47 @@ export class SshTui {
|
|
|
5071
5578
|
}
|
|
5072
5579
|
this.markDirty();
|
|
5073
5580
|
}
|
|
5581
|
+
/**
|
|
5582
|
+
* Persist the default provider/model selection. `agentDefaultModel` may be
|
|
5583
|
+
* unavailable or its settings namespace may not be registered in this
|
|
5584
|
+
* process, so a failed `saveSelection` falls back to writing the
|
|
5585
|
+
* `agent-default-model` settings section directly and surfaces a warning
|
|
5586
|
+
* when neither path sticks.
|
|
5587
|
+
*/
|
|
5588
|
+
async persistDefaultSelection(next) {
|
|
5589
|
+
const settings = this.ctx.get('settings');
|
|
5590
|
+
const defaultModel = this.ctx.get('agentDefaultModel');
|
|
5591
|
+
if (defaultModel !== undefined) {
|
|
5592
|
+
try {
|
|
5593
|
+
await defaultModel.saveSelection(next);
|
|
5594
|
+
return true;
|
|
5595
|
+
}
|
|
5596
|
+
catch {
|
|
5597
|
+
// Fall through to the direct settings write.
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
if (settings !== undefined) {
|
|
5601
|
+
try {
|
|
5602
|
+
await settings.replace(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, {
|
|
5603
|
+
provider: next.provider,
|
|
5604
|
+
model: next.model,
|
|
5605
|
+
...(next.reasoningEffort === undefined ? {} : { reasoningEffort: String(next.reasoningEffort) }),
|
|
5606
|
+
});
|
|
5607
|
+
return true;
|
|
5608
|
+
}
|
|
5609
|
+
catch (error) {
|
|
5610
|
+
this.pushRow({
|
|
5611
|
+
kind: 'error',
|
|
5612
|
+
text: `默认选择未能固化:agentDefaultModel 不可用且 settings 写入失败(${errorChain(error)})。本次切换仅当前会话生效,重启会回退到保存过的提供商。`,
|
|
5613
|
+
});
|
|
5614
|
+
this.markDirty();
|
|
5615
|
+
return false;
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
this.pushRow({ kind: 'error', text: '默认选择未能固化:settings 服务不可用。本次切换仅当前会话生效。' });
|
|
5619
|
+
this.markDirty();
|
|
5620
|
+
return false;
|
|
5621
|
+
}
|
|
5074
5622
|
/** Provider route the next subagent request should use. */
|
|
5075
5623
|
effectiveSubagentProvider() {
|
|
5076
5624
|
return this.subagentSelection.current.provider
|
|
@@ -5099,7 +5647,8 @@ export class SshTui {
|
|
|
5099
5647
|
catalog = [];
|
|
5100
5648
|
}
|
|
5101
5649
|
}
|
|
5102
|
-
const
|
|
5650
|
+
const parentModel = this.selectionRef?.current?.model ?? this.agent.options.model;
|
|
5651
|
+
const nextModel = defaultSubagentModelForProvider(provider, catalog, parentModel);
|
|
5103
5652
|
if (!force && nextModel === current.model && current.provider === undefined)
|
|
5104
5653
|
return;
|
|
5105
5654
|
const persisted = await this.saveSubagentSelection({
|
|
@@ -5111,35 +5660,6 @@ export class SshTui {
|
|
|
5111
5660
|
text: `子代理已跟随提供商 ${provider},模型改为 ${nextModel}${persisted ? '' : '(仅当前会话)'}。`,
|
|
5112
5661
|
});
|
|
5113
5662
|
}
|
|
5114
|
-
async promptSubagentAfterProviderSwitch(provider) {
|
|
5115
|
-
const current = this.subagentSelection.current;
|
|
5116
|
-
try {
|
|
5117
|
-
const { options, source } = await this.subagentModelOptions(provider);
|
|
5118
|
-
const listed = options.length === 0
|
|
5119
|
-
? [{ id: current.model, label: current.model }]
|
|
5120
|
-
: options;
|
|
5121
|
-
if (!listed.some(option => option.id === current.model)) {
|
|
5122
|
-
listed.unshift({ id: current.model, label: current.model });
|
|
5123
|
-
}
|
|
5124
|
-
const selected = await this.pickModelOption(listed, provider, source, current.model);
|
|
5125
|
-
if (selected === undefined || selected.id === current.model)
|
|
5126
|
-
return;
|
|
5127
|
-
if (!(await this.ensureProviderModelConfigured(provider, selected.id)))
|
|
5128
|
-
return;
|
|
5129
|
-
const persisted = await this.saveSubagentSelection({ model: selected.id });
|
|
5130
|
-
this.pushRow({
|
|
5131
|
-
kind: 'system',
|
|
5132
|
-
text: `子代理模型已设为 ${selected.id}(提供方跟随 ${provider})${persisted ? '' : '(仅当前会话)'}。`,
|
|
5133
|
-
});
|
|
5134
|
-
}
|
|
5135
|
-
catch (error) {
|
|
5136
|
-
if (error instanceof UserQuestionError) {
|
|
5137
|
-
this.pushRow({ kind: 'system', text: `子代理沿用 ${current.model}。之后可用 /submodel 再改。` });
|
|
5138
|
-
return;
|
|
5139
|
-
}
|
|
5140
|
-
this.pushRow({ kind: 'error', text: `选择子代理模型失败:${errorChain(error)}` });
|
|
5141
|
-
}
|
|
5142
|
-
}
|
|
5143
5663
|
clearQuotaForProvider(provider) {
|
|
5144
5664
|
if (this.quotaSnapshot !== undefined && this.quotaSnapshot.provider === provider)
|
|
5145
5665
|
return;
|
|
@@ -5254,7 +5774,7 @@ export class SshTui {
|
|
|
5254
5774
|
return;
|
|
5255
5775
|
}
|
|
5256
5776
|
const choices = [
|
|
5257
|
-
{ id: undefined, label: SUBAGENT_DEFAULT_EFFORT_LABEL },
|
|
5777
|
+
{ id: undefined, label: SUBAGENT_DEFAULT_EFFORT_LABEL() },
|
|
5258
5778
|
...effortOptions.map(option => ({ id: option.id, label: option.label })),
|
|
5259
5779
|
];
|
|
5260
5780
|
const answer = await this.askQuestion({
|
|
@@ -5287,6 +5807,41 @@ export class SshTui {
|
|
|
5287
5807
|
});
|
|
5288
5808
|
this.markDirty();
|
|
5289
5809
|
}
|
|
5810
|
+
/** /language or /lang: persist zh/en and repaint chrome immediately. */
|
|
5811
|
+
async runLanguageCommand(arg) {
|
|
5812
|
+
const direct = localeFromTag(arg);
|
|
5813
|
+
let next = direct;
|
|
5814
|
+
if (next === undefined && arg.trim() !== '') {
|
|
5815
|
+
this.pushRow({ kind: 'error', text: t('lang.unknown', { id: arg.trim() }) });
|
|
5816
|
+
this.markDirty();
|
|
5817
|
+
return;
|
|
5818
|
+
}
|
|
5819
|
+
if (next === undefined) {
|
|
5820
|
+
const current = getLocale();
|
|
5821
|
+
const answer = await this.askQuestion({
|
|
5822
|
+
id: 'language-pick',
|
|
5823
|
+
question: t('lang.pick'),
|
|
5824
|
+
options: [
|
|
5825
|
+
{ label: t('lang.zh'), description: current === 'zh' ? t('lang.current') : t('lang.zhDesc') },
|
|
5826
|
+
{ label: t('lang.en'), description: current === 'en' ? t('lang.current') : t('lang.enDesc') },
|
|
5827
|
+
],
|
|
5828
|
+
}, 0, 1, current === 'en' ? 1 : 0);
|
|
5829
|
+
const picked = answer.selected[0];
|
|
5830
|
+
next = picked === t('lang.en') ? 'en' : 'zh';
|
|
5831
|
+
}
|
|
5832
|
+
setLocale(next);
|
|
5833
|
+
const settings = this.ctx.get('settings');
|
|
5834
|
+
if (settings === undefined) {
|
|
5835
|
+
this.pushRow({ kind: 'error', text: t('lang.settingsMissing') });
|
|
5836
|
+
}
|
|
5837
|
+
else {
|
|
5838
|
+
await settings.replace(UI_LOCALE_NAMESPACE, { language: next });
|
|
5839
|
+
applySavedLocale({ language: next });
|
|
5840
|
+
}
|
|
5841
|
+
this.forceFullPaint = true;
|
|
5842
|
+
this.pushRow({ kind: 'system', text: t('lang.switched', { name: localeDisplayName(next) }) });
|
|
5843
|
+
this.markDirty();
|
|
5844
|
+
}
|
|
5290
5845
|
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
5291
5846
|
async runModeCommand() {
|
|
5292
5847
|
const agentPresets = this.ctx.get('agentPresets');
|
|
@@ -5573,13 +6128,29 @@ export class SshTui {
|
|
|
5573
6128
|
const token = await this.resolveSuperGrokToken();
|
|
5574
6129
|
if (token === undefined)
|
|
5575
6130
|
throw new Error('未找到 SuperGrok OAuth token(~/.grok-bridge/auth.json)');
|
|
5576
|
-
const
|
|
6131
|
+
const headers = {
|
|
5577
6132
|
authorization: `Bearer ${token}`,
|
|
5578
6133
|
accept: 'application/json',
|
|
5579
6134
|
'x-grok-client-mode': 'cli',
|
|
5580
6135
|
'x-grok-client-version': '1.0.0',
|
|
5581
|
-
}
|
|
5582
|
-
|
|
6136
|
+
};
|
|
6137
|
+
try {
|
|
6138
|
+
const payload = await this.fetchJson(SUPERGROK_BILLING_URL, headers, 'SuperGrok');
|
|
6139
|
+
return parseSuperGrokBilling(payload);
|
|
6140
|
+
}
|
|
6141
|
+
catch (error) {
|
|
6142
|
+
const message = errorChain(error);
|
|
6143
|
+
if (!message.includes('HTTP 401') && !message.includes('HTTP 403'))
|
|
6144
|
+
throw error;
|
|
6145
|
+
const retried = await this.resolveSuperGrokToken({ force: true });
|
|
6146
|
+
if (retried === undefined || retried === token)
|
|
6147
|
+
throw error;
|
|
6148
|
+
const payload = await this.fetchJson(SUPERGROK_BILLING_URL, {
|
|
6149
|
+
...headers,
|
|
6150
|
+
authorization: `Bearer ${retried}`,
|
|
6151
|
+
}, 'SuperGrok');
|
|
6152
|
+
return parseSuperGrokBilling(payload);
|
|
6153
|
+
}
|
|
5583
6154
|
}
|
|
5584
6155
|
const llmPiAi = this.ctx.get('settings')?.get(settingsNamespace('llm-pi-ai'));
|
|
5585
6156
|
const source = openCodeSourceFor(provider, llmPiAi);
|
|
@@ -5591,19 +6162,8 @@ export class SshTui {
|
|
|
5591
6162
|
const payload = await this.fetchOpenCodeGoUsage(apiKey);
|
|
5592
6163
|
return parseOpenCodeGoQuota(payload, source.provider);
|
|
5593
6164
|
}
|
|
5594
|
-
async resolveSuperGrokToken() {
|
|
5595
|
-
|
|
5596
|
-
try {
|
|
5597
|
-
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
5598
|
-
const token = typeof parsed.access_token === 'string' ? parsed.access_token : parsed.accessToken;
|
|
5599
|
-
return typeof token === 'string' && token.trim() !== '' ? token.trim() : undefined;
|
|
5600
|
-
}
|
|
5601
|
-
catch {
|
|
5602
|
-
return undefined;
|
|
5603
|
-
}
|
|
5604
|
-
};
|
|
5605
|
-
return await fromFile(join(homedir(), '.grok-bridge', 'auth.json'))
|
|
5606
|
-
?? await fromFile(join(homedir(), '.grok', 'auth.json'));
|
|
6165
|
+
async resolveSuperGrokToken(options = {}) {
|
|
6166
|
+
return resolveFreshSuperGrokToken(options);
|
|
5607
6167
|
}
|
|
5608
6168
|
async fetchJson(url, headers, label) {
|
|
5609
6169
|
let response;
|
|
@@ -5646,7 +6206,10 @@ export class SshTui {
|
|
|
5646
6206
|
if (match !== null) {
|
|
5647
6207
|
switch (match[1]) {
|
|
5648
6208
|
case 'A':
|
|
5649
|
-
if (this.
|
|
6209
|
+
if (this.dialog?.kind === 'inspect') {
|
|
6210
|
+
this.scrollInspectOrTranscript(-1);
|
|
6211
|
+
}
|
|
6212
|
+
else if (this.suggestionsVisible()) {
|
|
5650
6213
|
this.suggestionIndex = Math.max(0, this.suggestionIndex - 1);
|
|
5651
6214
|
this.markDirty();
|
|
5652
6215
|
}
|
|
@@ -5658,7 +6221,10 @@ export class SshTui {
|
|
|
5658
6221
|
}
|
|
5659
6222
|
return;
|
|
5660
6223
|
case 'B':
|
|
5661
|
-
if (this.
|
|
6224
|
+
if (this.dialog?.kind === 'inspect') {
|
|
6225
|
+
this.scrollInspectOrTranscript(1);
|
|
6226
|
+
}
|
|
6227
|
+
else if (this.suggestionsVisible()) {
|
|
5662
6228
|
this.suggestionIndex = Math.min(this.commandSuggestions.length - 1, this.suggestionIndex + 1);
|
|
5663
6229
|
this.markDirty();
|
|
5664
6230
|
}
|
|
@@ -5683,13 +6249,11 @@ export class SshTui {
|
|
|
5683
6249
|
const y = Number(sgrMouse[3]);
|
|
5684
6250
|
if (sgrMouse[4] === 'M') {
|
|
5685
6251
|
if (button === 64) {
|
|
5686
|
-
this.
|
|
5687
|
-
this.markDirty();
|
|
6252
|
+
this.scrollInspectOrTranscript(3);
|
|
5688
6253
|
return;
|
|
5689
6254
|
}
|
|
5690
6255
|
if (button === 65) {
|
|
5691
|
-
this.
|
|
5692
|
-
this.markDirty();
|
|
6256
|
+
this.scrollInspectOrTranscript(-3);
|
|
5693
6257
|
return;
|
|
5694
6258
|
}
|
|
5695
6259
|
if (button === 0) {
|
|
@@ -5700,13 +6264,11 @@ export class SshTui {
|
|
|
5700
6264
|
return;
|
|
5701
6265
|
}
|
|
5702
6266
|
if (combined === '\x1b[5~') {
|
|
5703
|
-
this.
|
|
5704
|
-
this.markDirty();
|
|
6267
|
+
this.scrollInspectOrTranscript(Math.max(3, Math.floor((process.stdout.rows || 24) / 2)));
|
|
5705
6268
|
return;
|
|
5706
6269
|
}
|
|
5707
6270
|
if (combined === '\x1b[6~') {
|
|
5708
|
-
this.
|
|
5709
|
-
this.markDirty();
|
|
6271
|
+
this.scrollInspectOrTranscript(-Math.max(3, Math.floor((process.stdout.rows || 24) / 2)));
|
|
5710
6272
|
return;
|
|
5711
6273
|
}
|
|
5712
6274
|
if (parseCursorPositionReply(combined) !== undefined)
|
|
@@ -5973,6 +6535,12 @@ export class SshTui {
|
|
|
5973
6535
|
const dialog = this.dialog;
|
|
5974
6536
|
if (dialog === undefined)
|
|
5975
6537
|
return;
|
|
6538
|
+
if (dialog.kind === 'inspect') {
|
|
6539
|
+
if (text === '\x1b' || text === '\x03' || text === 'q' || text === 'Q' || text === '\r' || text === '\n') {
|
|
6540
|
+
this.closeInspect();
|
|
6541
|
+
}
|
|
6542
|
+
return;
|
|
6543
|
+
}
|
|
5976
6544
|
if (dialog.kind === 'onboarding') {
|
|
5977
6545
|
this.handleOnboardingChar(text);
|
|
5978
6546
|
return;
|
|
@@ -6068,7 +6636,7 @@ export class SshTui {
|
|
|
6068
6636
|
if (text === '\r' || text === '\n') {
|
|
6069
6637
|
const value = this.input.trim();
|
|
6070
6638
|
if (state.step === 'id') {
|
|
6071
|
-
const template =
|
|
6639
|
+
const template = providerTemplates()[state.providerType];
|
|
6072
6640
|
const id = value === '' ? template.defaultId : value;
|
|
6073
6641
|
if (!/^[a-z0-9][a-z0-9-]*$/u.test(id)) {
|
|
6074
6642
|
this.pushRow({ kind: 'error', text: 'Provider ID 只能包含小写字母、数字和连字符,且不能以连字符开头。' });
|
|
@@ -6086,7 +6654,7 @@ export class SshTui {
|
|
|
6086
6654
|
state.key = value;
|
|
6087
6655
|
}
|
|
6088
6656
|
else if (state.step === 'models') {
|
|
6089
|
-
const template =
|
|
6657
|
+
const template = providerTemplates()[state.providerType];
|
|
6090
6658
|
const parsed = value === ''
|
|
6091
6659
|
? template.defaultModels
|
|
6092
6660
|
: value.split(/[\s,,]+/u).filter(Boolean);
|
|
@@ -6165,7 +6733,7 @@ export class SshTui {
|
|
|
6165
6733
|
const state = this.onboarding;
|
|
6166
6734
|
if (state === undefined || state.step !== 'models')
|
|
6167
6735
|
return;
|
|
6168
|
-
const template =
|
|
6736
|
+
const template = providerTemplates()[state.providerType];
|
|
6169
6737
|
const providerType = state.providerType;
|
|
6170
6738
|
const baseUrl = state.baseUrl;
|
|
6171
6739
|
const key = state.key;
|
|
@@ -6223,7 +6791,7 @@ export class SshTui {
|
|
|
6223
6791
|
try {
|
|
6224
6792
|
const credentials = this.ctx.get('credentials');
|
|
6225
6793
|
const settings = this.ctx.get('settings');
|
|
6226
|
-
const template =
|
|
6794
|
+
const template = providerTemplates()[state.providerType];
|
|
6227
6795
|
if (state.providerType === 'official') {
|
|
6228
6796
|
const envRef = 'DEEPSEEK_API_KEY';
|
|
6229
6797
|
await this.saveCredential(credentials, envRef, state.key);
|
|
@@ -6461,6 +7029,10 @@ export class SshTui {
|
|
|
6461
7029
|
}
|
|
6462
7030
|
handleEscape() {
|
|
6463
7031
|
if (this.dialog !== undefined) {
|
|
7032
|
+
if (this.dialog.kind === 'inspect') {
|
|
7033
|
+
this.closeInspect();
|
|
7034
|
+
return;
|
|
7035
|
+
}
|
|
6464
7036
|
if (this.dialog.kind === 'confirm')
|
|
6465
7037
|
this.closeConfirm('cancel');
|
|
6466
7038
|
else if (this.dialog.kind === 'onboarding')
|
|
@@ -6496,11 +7068,22 @@ export class SshTui {
|
|
|
6496
7068
|
handleMouseClick(y) {
|
|
6497
7069
|
if (this.dialog !== undefined)
|
|
6498
7070
|
return;
|
|
7071
|
+
if (this.cwdChipRow !== undefined && y === this.cwdChipRow) {
|
|
7072
|
+
this.announceWorkspaceCwd();
|
|
7073
|
+
return;
|
|
7074
|
+
}
|
|
6499
7075
|
const row = this.clickableRows.get(y);
|
|
6500
7076
|
if (row === undefined)
|
|
6501
7077
|
return;
|
|
6502
|
-
this.
|
|
6503
|
-
|
|
7078
|
+
this.toggleCard(row);
|
|
7079
|
+
}
|
|
7080
|
+
scrollInspectOrTranscript(delta) {
|
|
7081
|
+
if (this.dialog?.kind === 'inspect') {
|
|
7082
|
+
this.dialog.offset = Math.max(0, this.dialog.offset + delta);
|
|
7083
|
+
this.markDirty();
|
|
7084
|
+
return;
|
|
7085
|
+
}
|
|
7086
|
+
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
6504
7087
|
this.markDirty();
|
|
6505
7088
|
}
|
|
6506
7089
|
handleCtrlC() {
|
|
@@ -6567,7 +7150,7 @@ export class SshTui {
|
|
|
6567
7150
|
const arg = rest.join(' ');
|
|
6568
7151
|
switch (command) {
|
|
6569
7152
|
case 'help': {
|
|
6570
|
-
const local =
|
|
7153
|
+
const local = localizedCommands()
|
|
6571
7154
|
.filter(item => item.name !== 'help' && item.name !== 'exit')
|
|
6572
7155
|
.map(item => `/${item.name.padEnd(12)} ${item.description}`);
|
|
6573
7156
|
const dsh = (this.ctx.get('commands')?.list(this.agent) ?? [])
|
|
@@ -6578,13 +7161,13 @@ export class SshTui {
|
|
|
6578
7161
|
...local,
|
|
6579
7162
|
...dsh,
|
|
6580
7163
|
'',
|
|
6581
|
-
'
|
|
6582
|
-
'
|
|
6583
|
-
'
|
|
6584
|
-
'
|
|
6585
|
-
'
|
|
6586
|
-
'
|
|
6587
|
-
'
|
|
7164
|
+
t('help.intro1'),
|
|
7165
|
+
t('help.intro2'),
|
|
7166
|
+
t('help.intro3'),
|
|
7167
|
+
t('help.intro4'),
|
|
7168
|
+
t('help.intro5'),
|
|
7169
|
+
t('help.intro6'),
|
|
7170
|
+
t('help.intro7'),
|
|
6588
7171
|
].join('\n'),
|
|
6589
7172
|
});
|
|
6590
7173
|
break;
|
|
@@ -6596,7 +7179,7 @@ export class SshTui {
|
|
|
6596
7179
|
case 'model':
|
|
6597
7180
|
void this.runModelCommand().catch((error) => {
|
|
6598
7181
|
if (error instanceof UserQuestionError) {
|
|
6599
|
-
this.pushRow({ kind: 'system', text: '
|
|
7182
|
+
this.pushRow({ kind: 'system', text: t('help.modelCancel') });
|
|
6600
7183
|
}
|
|
6601
7184
|
else {
|
|
6602
7185
|
this.pushRow({ kind: 'error', text: `/model failed: ${errorChain(error)}` });
|
|
@@ -6607,7 +7190,7 @@ export class SshTui {
|
|
|
6607
7190
|
case 'provider':
|
|
6608
7191
|
void this.runProviderCommand().catch((error) => {
|
|
6609
7192
|
if (error instanceof UserQuestionError) {
|
|
6610
|
-
this.pushRow({ kind: 'system', text: '
|
|
7193
|
+
this.pushRow({ kind: 'system', text: t('help.providerCancel') });
|
|
6611
7194
|
}
|
|
6612
7195
|
else {
|
|
6613
7196
|
this.pushRow({ kind: 'error', text: `/provider failed: ${errorChain(error)}` });
|
|
@@ -6618,7 +7201,7 @@ export class SshTui {
|
|
|
6618
7201
|
case 'submodel':
|
|
6619
7202
|
void this.runSubmodelCommand(arg).catch((error) => {
|
|
6620
7203
|
if (error instanceof UserQuestionError) {
|
|
6621
|
-
this.pushRow({ kind: 'system', text: '
|
|
7204
|
+
this.pushRow({ kind: 'system', text: t('help.submodelCancel') });
|
|
6622
7205
|
}
|
|
6623
7206
|
else {
|
|
6624
7207
|
this.pushRow({ kind: 'error', text: `/submodel failed: ${errorChain(error)}` });
|
|
@@ -6629,7 +7212,7 @@ export class SshTui {
|
|
|
6629
7212
|
case 'subeffort':
|
|
6630
7213
|
void this.runSubeffortCommand().catch((error) => {
|
|
6631
7214
|
if (error instanceof UserQuestionError) {
|
|
6632
|
-
this.pushRow({ kind: 'system', text: '
|
|
7215
|
+
this.pushRow({ kind: 'system', text: t('help.subeffortCancel') });
|
|
6633
7216
|
}
|
|
6634
7217
|
else {
|
|
6635
7218
|
this.pushRow({ kind: 'error', text: `/subeffort failed: ${errorChain(error)}` });
|
|
@@ -6640,7 +7223,7 @@ export class SshTui {
|
|
|
6640
7223
|
case 'mode':
|
|
6641
7224
|
void this.runModeCommand().catch((error) => {
|
|
6642
7225
|
if (error instanceof UserQuestionError) {
|
|
6643
|
-
this.pushRow({ kind: 'system', text: '
|
|
7226
|
+
this.pushRow({ kind: 'system', text: t('help.modeCancel') });
|
|
6644
7227
|
}
|
|
6645
7228
|
else {
|
|
6646
7229
|
this.pushRow({ kind: 'error', text: `/mode failed: ${errorChain(error)}` });
|
|
@@ -6648,6 +7231,18 @@ export class SshTui {
|
|
|
6648
7231
|
this.markDirty();
|
|
6649
7232
|
});
|
|
6650
7233
|
break;
|
|
7234
|
+
case 'language':
|
|
7235
|
+
case 'lang':
|
|
7236
|
+
void this.runLanguageCommand(arg).catch((error) => {
|
|
7237
|
+
if (error instanceof UserQuestionError) {
|
|
7238
|
+
this.pushRow({ kind: 'system', text: t('help.modeCancel') });
|
|
7239
|
+
}
|
|
7240
|
+
else {
|
|
7241
|
+
this.pushRow({ kind: 'error', text: `/language failed: ${errorChain(error)}` });
|
|
7242
|
+
}
|
|
7243
|
+
this.markDirty();
|
|
7244
|
+
});
|
|
7245
|
+
break;
|
|
6651
7246
|
case 'find':
|
|
6652
7247
|
this.runFindCommand(arg);
|
|
6653
7248
|
break;
|
|
@@ -6662,28 +7257,37 @@ export class SshTui {
|
|
|
6662
7257
|
this.searchQuery = '';
|
|
6663
7258
|
this.planNudgePending = false;
|
|
6664
7259
|
this.pendingReveal = undefined;
|
|
6665
|
-
this.pushRow({ kind: 'system', text: '
|
|
7260
|
+
this.pushRow({ kind: 'system', text: t('clear.transcript') });
|
|
6666
7261
|
break;
|
|
6667
7262
|
case 'status':
|
|
6668
7263
|
{
|
|
6669
7264
|
const plan = this.findLivePlanRow();
|
|
6670
7265
|
const waiting = this.rows.filter(row => row.kind === 'question' && row.status === 'waiting').length;
|
|
6671
7266
|
const provider = this.currentProviderId();
|
|
6672
|
-
const route = describeProviderRoute(provider);
|
|
6673
7267
|
const model = this.selectionRef?.current?.model ?? this.agent.options.model ?? 'default';
|
|
6674
7268
|
const effort = this.selectionRef?.current?.reasoningEffort;
|
|
6675
|
-
const
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
7269
|
+
const sub = this.subagentSelection.current;
|
|
7270
|
+
const quota = this.quotaSnapshot !== undefined && this.quotaSnapshot.provider === provider
|
|
7271
|
+
? this.quotaSnapshot
|
|
7272
|
+
: undefined;
|
|
7273
|
+
const lines = formatStatusReport({
|
|
7274
|
+
sessionId: this.agent.id,
|
|
7275
|
+
pluginVersion: PLUGIN_VERSION,
|
|
7276
|
+
provider,
|
|
7277
|
+
model,
|
|
7278
|
+
...(effort === undefined ? {} : { effort }),
|
|
7279
|
+
agentStatus: this.agent.status,
|
|
7280
|
+
preset: this.presetName,
|
|
7281
|
+
activeSubagents: this.activeSubagents.size,
|
|
7282
|
+
plan: plan === undefined ? 'off' : plan.pending ? 'pending' : plan.active ? 'on' : 'off',
|
|
7283
|
+
paint: formatLinkQualityChip(this.paintLink, this.paintIntervalMs, this.paintRttMs, this.paintProbed),
|
|
7284
|
+
waitingQuestions: waiting,
|
|
7285
|
+
...(quota === undefined ? {} : { quota }),
|
|
7286
|
+
parentModel: model,
|
|
7287
|
+
...(sub.provider === undefined ? {} : { subProvider: sub.provider }),
|
|
7288
|
+
subModel: sub.model,
|
|
7289
|
+
cwd: this.workspaceCwd(),
|
|
7290
|
+
});
|
|
6687
7291
|
this.pushRow({ kind: 'system', text: lines.join('\n') });
|
|
6688
7292
|
}
|
|
6689
7293
|
break;
|
|
@@ -6732,7 +7336,7 @@ export class SshTui {
|
|
|
6732
7336
|
const activity = card?.lastActivity ? ` · ${card.lastActivity}` : '';
|
|
6733
7337
|
return `▶ ${label} ${sub.id}(${sub.provider})运行 ${Math.floor((Date.now() - sub.startedAt) / 1000)}s [${runId.slice(0, 8)}]${activity}`;
|
|
6734
7338
|
});
|
|
6735
|
-
this.pushRow({ kind: 'system', text:
|
|
7339
|
+
this.pushRow({ kind: 'system', text: t('sub.listHint', { lines: lines.join('\n') }) });
|
|
6736
7340
|
}
|
|
6737
7341
|
break;
|
|
6738
7342
|
}
|