dsh-ssh-tui 0.3.8 → 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 +31 -22
- package/README.md +15 -9
- 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 +13 -0
- package/lib/index.js.map +1 -1
- package/lib/picker.js +5 -4
- package/lib/picker.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 +44 -2
- package/lib/subagent-model.js.map +1 -1
- package/lib/supergrok-token.js +7 -3
- package/lib/supergrok-token.js.map +1 -1
- package/lib/tui.js +821 -333
- 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/session-list.d.ts +16 -0
- package/lib/types/subagent-model.d.ts +24 -0
- package/lib/types/tui.d.ts +115 -4
- 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,12 +21,13 @@ 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
29
|
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model';
|
|
29
|
-
import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, subagentModelMatchesProvider, subagentSettingsValue, } from './subagent-model.js';
|
|
30
|
+
import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, describeSubagentFit, subagentModelMatchesProvider, subagentSettingsValue, } from './subagent-model.js';
|
|
30
31
|
import { resolveFreshSuperGrokToken } from './supergrok-token.js';
|
|
31
32
|
import { UserQuestionError, } from '@deepseek-ai/dsh-user-questions';
|
|
32
33
|
function discoverProviderModels(llm, request, signal) {
|
|
@@ -49,42 +50,44 @@ function installUserQuestionAnswerer(ctx, questions, ask) {
|
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
const ROUTE_MEMORY_NS = ROUTE_MEMORY_NAMESPACE;
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
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
|
+
}
|
|
88
91
|
const RENDER_INTERVAL_MS = 160;
|
|
89
92
|
const LOCAL_PAINT_INTERVAL_MS = 80;
|
|
90
93
|
const WAIT_INDICATOR_MS = 8000;
|
|
@@ -143,8 +146,8 @@ export function paintIntervalForRtt(rttMs) {
|
|
|
143
146
|
}
|
|
144
147
|
export function paintLinkLabel(kind, intervalMs, probed) {
|
|
145
148
|
if (kind === 'local')
|
|
146
|
-
return
|
|
147
|
-
return probed ?
|
|
149
|
+
return t('paint.localMs', { ms: intervalMs });
|
|
150
|
+
return probed ? t('paint.sshMs', { ms: intervalMs }) : t('paint.sshMsUnprobed', { ms: intervalMs });
|
|
148
151
|
}
|
|
149
152
|
/** Signal-bar quality from a measured SSH round-trip, or local TTY. */
|
|
150
153
|
export function linkQualityOf(kind, rttMs) {
|
|
@@ -188,16 +191,16 @@ export function formatLinkQualityChip(kind, intervalMs, rttMs, probed, color = f
|
|
|
188
191
|
? `\x1b[${LINK_PIP_COLOR[filled] ?? '90'}m${pips}\x1b[0m`
|
|
189
192
|
: pips;
|
|
190
193
|
if (kind === 'local')
|
|
191
|
-
return
|
|
194
|
+
return t('paint.localChip', { pips: colored });
|
|
192
195
|
const delay = probed && rttMs !== undefined && Number.isFinite(rttMs)
|
|
193
196
|
? `${Math.round(rttMs)}ms`
|
|
194
197
|
: `${intervalMs}ms`;
|
|
195
|
-
return
|
|
198
|
+
return t('paint.sshChip', { pips: colored, delay });
|
|
196
199
|
}
|
|
197
200
|
export function providerShortCode(provider) {
|
|
198
201
|
const id = provider.trim();
|
|
199
202
|
if (id === 'deepseek-official' || id === 'deepseek')
|
|
200
|
-
return '
|
|
203
|
+
return t('route.deepseek');
|
|
201
204
|
if (id === 'xai' || id === 'grok' || id.startsWith('xai-'))
|
|
202
205
|
return 'SuperGrok';
|
|
203
206
|
if (id === 'opencode-go')
|
|
@@ -210,29 +213,29 @@ export function providerShortCode(provider) {
|
|
|
210
213
|
export function footerStatsGroups(stats) {
|
|
211
214
|
const groups = [];
|
|
212
215
|
if (stats.steps > 0)
|
|
213
|
-
groups.push(
|
|
216
|
+
groups.push(t('footer.turnsSteps', { turns: stats.turns, steps: stats.steps }));
|
|
214
217
|
const billedInput = stats.inputTokens + stats.cacheReadTokens + stats.cacheWriteTokens;
|
|
215
218
|
if (billedInput > 0 || stats.outputTokens > 0) {
|
|
216
|
-
groups.push(
|
|
219
|
+
groups.push(t('footer.tokens', { input: formatTokens(billedInput), output: formatTokens(stats.outputTokens) }));
|
|
217
220
|
}
|
|
218
221
|
const speeds = [];
|
|
219
222
|
if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
|
|
220
223
|
speeds.push(formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)));
|
|
221
224
|
}
|
|
222
225
|
else if (stats.ttftSteps > 0) {
|
|
223
|
-
speeds.push(
|
|
226
|
+
speeds.push(t('footer.ttft', { duration: formatDuration(stats.ttftMs / stats.ttftSteps) }));
|
|
224
227
|
}
|
|
225
228
|
if (speeds.length > 0)
|
|
226
229
|
groups.push(speeds.join(' '));
|
|
227
230
|
const durations = [];
|
|
228
231
|
if (stats.llmMs > 0)
|
|
229
|
-
durations.push(
|
|
232
|
+
durations.push(t('footer.llmMs', { duration: formatDuration(stats.llmMs) }));
|
|
230
233
|
if (stats.toolMs > 0)
|
|
231
|
-
durations.push(
|
|
234
|
+
durations.push(t('footer.toolMs', { duration: formatDuration(stats.toolMs) }));
|
|
232
235
|
if (durations.length > 0)
|
|
233
236
|
groups.push(durations.join(' '));
|
|
234
237
|
if (billedInput > 0)
|
|
235
|
-
groups.push(
|
|
238
|
+
groups.push(t('footer.cacheHit', { percent: Math.round(stats.cacheReadTokens / billedInput * 100) }));
|
|
236
239
|
return groups;
|
|
237
240
|
}
|
|
238
241
|
export function fitFooterStatsLine(chip, groups, width) {
|
|
@@ -244,36 +247,36 @@ export function fitFooterStatsLine(chip, groups, width) {
|
|
|
244
247
|
}
|
|
245
248
|
export function footerActivity(input) {
|
|
246
249
|
if (input.planReview)
|
|
247
|
-
return { kind: 'plan-review', text: '
|
|
250
|
+
return { kind: 'plan-review', text: t('footer.planReview') };
|
|
248
251
|
if (input.waitingQuestion)
|
|
249
|
-
return { kind: 'waiting', text: '
|
|
252
|
+
return { kind: 'waiting', text: t('footer.waiting') };
|
|
250
253
|
if (input.compacting)
|
|
251
|
-
return { kind: 'compacting', text: '
|
|
254
|
+
return { kind: 'compacting', text: t('footer.compacting') };
|
|
252
255
|
if (input.retry !== undefined) {
|
|
253
|
-
return { kind: 'retry', text:
|
|
256
|
+
return { kind: 'retry', text: t('footer.retry', { retry: input.retry.retry, max: input.retry.maxRetries }) };
|
|
254
257
|
}
|
|
255
258
|
if (input.subagents > 0)
|
|
256
|
-
return { kind: 'subagents', text:
|
|
259
|
+
return { kind: 'subagents', text: t('footer.subagents', { count: input.subagents }) };
|
|
257
260
|
if (input.running && input.tools > 0)
|
|
258
|
-
return { kind: 'tools', text:
|
|
261
|
+
return { kind: 'tools', text: t('footer.tools', { count: input.tools }) };
|
|
259
262
|
if (input.planLeftOpen)
|
|
260
|
-
return { kind: 'plan-open', text: '
|
|
263
|
+
return { kind: 'plan-open', text: t('footer.planOpen') };
|
|
261
264
|
if (input.planPending)
|
|
262
|
-
return { kind: 'plan-pending', text: '
|
|
265
|
+
return { kind: 'plan-pending', text: t('footer.planSwitching') };
|
|
263
266
|
if (input.planActive)
|
|
264
|
-
return { kind: 'plan-pending', text: '
|
|
267
|
+
return { kind: 'plan-pending', text: t('footer.planMode') };
|
|
265
268
|
if (input.goalPhase === 'active')
|
|
266
|
-
return { kind: 'goal', text: '
|
|
269
|
+
return { kind: 'goal', text: t('footer.goalActive') };
|
|
267
270
|
if (input.goalPhase === 'paused')
|
|
268
|
-
return { kind: 'goal', text: '
|
|
271
|
+
return { kind: 'goal', text: t('footer.goalPaused') };
|
|
269
272
|
if (input.goalPhase === 'blocked')
|
|
270
|
-
return { kind: 'goal', text: '
|
|
273
|
+
return { kind: 'goal', text: t('footer.goalBlocked') };
|
|
271
274
|
if (input.running && input.idleMs > WAIT_INDICATOR_MS) {
|
|
272
|
-
return { kind: 'waiting-llm', text:
|
|
275
|
+
return { kind: 'waiting-llm', text: t('footer.waitSeconds', { seconds: Math.floor(input.idleMs / 1000) }) };
|
|
273
276
|
}
|
|
274
277
|
if (input.running)
|
|
275
|
-
return { kind: 'idle', text: '
|
|
276
|
-
return { kind: 'idle', text: '
|
|
278
|
+
return { kind: 'idle', text: t('footer.running') };
|
|
279
|
+
return { kind: 'idle', text: t('footer.idle') };
|
|
277
280
|
}
|
|
278
281
|
/** Short remaining-quota bar: 8 pips, filled from the left. */
|
|
279
282
|
export function formatQuotaBar(remainingPercent, width = 8) {
|
|
@@ -285,27 +288,53 @@ export function footerIdentityParts(input) {
|
|
|
285
288
|
const parts = [];
|
|
286
289
|
if (input.preset !== undefined && input.preset !== '')
|
|
287
290
|
parts.push(`[${input.preset}]`);
|
|
291
|
+
if (input.cwdLabel !== undefined && input.cwdLabel !== '')
|
|
292
|
+
parts.push(input.cwdLabel);
|
|
288
293
|
const model = input.effort === undefined ? input.model : `${input.model} ${input.effort}`;
|
|
289
294
|
if (model !== '')
|
|
290
295
|
parts.push(model);
|
|
291
296
|
if (input.subDiffers)
|
|
292
297
|
parts.push(`sub:${input.subModel}`);
|
|
293
|
-
if (input.
|
|
294
|
-
parts.push(
|
|
298
|
+
if (input.quotaPercent !== undefined) {
|
|
299
|
+
parts.push(formatFooterQuota(input.quotaPercent, input.quotaCode));
|
|
295
300
|
}
|
|
296
301
|
if (input.search !== undefined)
|
|
297
|
-
parts.push(
|
|
302
|
+
parts.push(t('footer.search', { index: input.search.index + 1, total: input.search.total }));
|
|
298
303
|
if (input.foldedInput)
|
|
299
|
-
parts.push('
|
|
304
|
+
parts.push(t('footer.inputFolded'));
|
|
300
305
|
else if (input.multiLineInput)
|
|
301
|
-
parts.push('
|
|
306
|
+
parts.push(t('footer.multiLine'));
|
|
302
307
|
if (input.queued > 0)
|
|
303
|
-
parts.push(
|
|
308
|
+
parts.push(t('footer.queued', { count: input.queued }));
|
|
304
309
|
return parts;
|
|
305
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
|
+
}
|
|
306
333
|
export function fitFooterStatusLine(activity, identity, width) {
|
|
307
334
|
const kept = [...identity];
|
|
308
335
|
const render = () => kept.length === 0 ? activity : `${activity} ${kept.join(' · ')}`;
|
|
336
|
+
if (displayWidth(render()) > width)
|
|
337
|
+
dropFooterQuotaPlanName(kept);
|
|
309
338
|
while (kept.length > 0 && displayWidth(render()) > width)
|
|
310
339
|
kept.pop();
|
|
311
340
|
return truncateToWidth(render(), Math.max(1, width));
|
|
@@ -357,7 +386,7 @@ const PLUGIN_VERSION = (() => {
|
|
|
357
386
|
const STALL_WARNING_MS = 60000;
|
|
358
387
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
359
388
|
const QUESTION_OPTION_KEYS = '123456789abcdefghijklmnopqrstuvwxyz';
|
|
360
|
-
const SUBAGENT_DEFAULT_EFFORT_LABEL = '
|
|
389
|
+
const SUBAGENT_DEFAULT_EFFORT_LABEL = () => t('footer.effortDefault');
|
|
361
390
|
const RESERVED_BOTTOM_LINES = 3; // input line + stats line + status line
|
|
362
391
|
const MAX_TRANSCRIPT_ROWS = 5000;
|
|
363
392
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -487,20 +516,46 @@ const DEEPSEEK_LOGO_VARIANTS = [
|
|
|
487
516
|
],
|
|
488
517
|
},
|
|
489
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
|
+
}
|
|
490
545
|
/** Human-facing kind for a live LLM route. */
|
|
491
546
|
export function describeProviderRoute(provider) {
|
|
492
547
|
const id = provider.trim();
|
|
493
548
|
if (id === 'deepseek-official' || id === 'deepseek') {
|
|
494
|
-
return { kind: '
|
|
549
|
+
return { kind: t('route.deepseek'), short: t('route.deepseek') };
|
|
495
550
|
}
|
|
496
551
|
if (id === 'xai' || id === 'grok' || id.startsWith('xai-')) {
|
|
497
|
-
return { kind: '
|
|
552
|
+
return { kind: t('route.supergrokKind'), short: t('route.supergrokShort') };
|
|
498
553
|
}
|
|
499
554
|
if (id === 'opencode-go')
|
|
500
|
-
return { kind: '
|
|
555
|
+
return { kind: t('route.go'), short: t('route.go') };
|
|
501
556
|
if (id === 'opencode')
|
|
502
|
-
return { kind: '
|
|
503
|
-
return { kind: '
|
|
557
|
+
return { kind: t('route.zen'), short: t('route.zen') };
|
|
558
|
+
return { kind: t('route.registered'), short: id };
|
|
504
559
|
}
|
|
505
560
|
/** Routes that authenticate without a harness API-key credential. */
|
|
506
561
|
export function providerUsesLocalOAuth(provider) {
|
|
@@ -517,15 +572,22 @@ const LOCAL_COMMANDS = [
|
|
|
517
572
|
{ name: 'quit', description: 'exit the TUI' },
|
|
518
573
|
{ name: 'exit', description: 'exit the TUI' },
|
|
519
574
|
{ name: 'clear', description: 'clear the transcript view' },
|
|
520
|
-
{ name: 'status', description: 'show session,
|
|
575
|
+
{ name: 'status', description: 'show session, route, quota window, subagent fit, paint, and plugin version' },
|
|
521
576
|
{ name: 'usage', description: 'show remaining quota or account balance for the current provider' },
|
|
522
577
|
{ name: 'balance', description: 'alias of /usage: DeepSeek / OpenAI-compatible balance, or subscription quota' },
|
|
523
578
|
{ name: 'subagents', description: 'list active subagents; kill <id> to stop one' },
|
|
524
579
|
{ name: 'resume', description: 'resume a past session (empty = session picker)' },
|
|
525
580
|
{ name: 'setup', description: 'add or update an API-key provider without wiping other saved routes' },
|
|
526
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' },
|
|
527
584
|
{ name: 'dialog-test', description: 'verify the question dialog' },
|
|
528
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
|
+
}
|
|
529
591
|
/**
|
|
530
592
|
* Terminal cell width for one string.
|
|
531
593
|
*
|
|
@@ -1285,7 +1347,12 @@ export function crossedQuotaThresholds(previousRemaining, remaining) {
|
|
|
1285
1347
|
}
|
|
1286
1348
|
export function quotaAlertText(snapshot, window) {
|
|
1287
1349
|
const reset = window.resetsAt === undefined ? '' : `(${formatQuotaReset(window.resetsAt)})`;
|
|
1288
|
-
return
|
|
1350
|
+
return t('quota.alert', {
|
|
1351
|
+
plan: snapshot.plan,
|
|
1352
|
+
period: quotaPeriodLabel(window.period),
|
|
1353
|
+
percent: window.remainingPercent.toFixed(0),
|
|
1354
|
+
reset,
|
|
1355
|
+
});
|
|
1289
1356
|
}
|
|
1290
1357
|
/**
|
|
1291
1358
|
* How often to re-fetch quota, based on the tightest window.
|
|
@@ -1311,12 +1378,12 @@ export function quotaRefreshEverySteps(window) {
|
|
|
1311
1378
|
export const quotaRefreshEveryTurns = quotaRefreshEverySteps;
|
|
1312
1379
|
function quotaPeriodLabel(period) {
|
|
1313
1380
|
if (period === 'hourly')
|
|
1314
|
-
return '
|
|
1381
|
+
return t('quota.periodHourly');
|
|
1315
1382
|
if (period === 'weekly')
|
|
1316
|
-
return '
|
|
1383
|
+
return t('quota.periodWeekly');
|
|
1317
1384
|
if (period === 'monthly')
|
|
1318
|
-
return '
|
|
1319
|
-
return '
|
|
1385
|
+
return t('quota.periodMonthly');
|
|
1386
|
+
return t('quota.periodUnknown');
|
|
1320
1387
|
}
|
|
1321
1388
|
function formatQuotaReset(iso) {
|
|
1322
1389
|
const reset = new Date(iso);
|
|
@@ -1399,6 +1466,20 @@ export function formatQuotaSnapshot(snapshot) {
|
|
|
1399
1466
|
}
|
|
1400
1467
|
return lines.join('\n');
|
|
1401
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
|
+
}
|
|
1402
1483
|
/** Tightest remaining window — used for threshold alerts. */
|
|
1403
1484
|
export function tightestQuotaWindow(snapshot) {
|
|
1404
1485
|
return snapshot.windows.reduce((best, window) => {
|
|
@@ -1674,24 +1755,22 @@ function friendlyArgsSummary(name, args) {
|
|
|
1674
1755
|
const SHELL_TOOL_NAMES = new Set(['bash', 'pwsh']);
|
|
1675
1756
|
const DIFF_TOOL_NAMES = new Set(['edit', 'write', 'str_replace_editor']);
|
|
1676
1757
|
const SUBAGENT_TOOL_NAMES = new Set(['subagent', 'subagent_fork', 'task']);
|
|
1677
|
-
/**
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
list
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
skills: '技能',
|
|
1694
|
-
};
|
|
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
|
+
}
|
|
1695
1774
|
const MAX_SUBAGENT_LOGS = 80;
|
|
1696
1775
|
const TODO_STATUS_MARK = {
|
|
1697
1776
|
pending: '○',
|
|
@@ -1746,19 +1825,15 @@ export function cardCategoryOf(row) {
|
|
|
1746
1825
|
return 'question';
|
|
1747
1826
|
if (row.kind === 'goal')
|
|
1748
1827
|
return 'goal';
|
|
1828
|
+
if (row.kind === 'prompt')
|
|
1829
|
+
return 'prompt';
|
|
1749
1830
|
if (row.kind === 'compaction')
|
|
1750
1831
|
return 'tool';
|
|
1751
1832
|
return undefined;
|
|
1752
1833
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
subagent: '子代理',
|
|
1757
|
-
reply: '回复',
|
|
1758
|
-
tool: '工具',
|
|
1759
|
-
question: '提问',
|
|
1760
|
-
goal: '目标',
|
|
1761
|
-
};
|
|
1834
|
+
function cardCategoryLabel(category) {
|
|
1835
|
+
return t(`card.${category}`);
|
|
1836
|
+
}
|
|
1762
1837
|
const SEARCHABLE_CATEGORIES = ['thinking', 'plan', 'subagent', 'reply'];
|
|
1763
1838
|
function parseCardCategoryToken(token) {
|
|
1764
1839
|
const id = token.trim().toLowerCase();
|
|
@@ -1776,6 +1851,8 @@ function parseCardCategoryToken(token) {
|
|
|
1776
1851
|
return 'question';
|
|
1777
1852
|
if (id === 'goal' || id === '目标')
|
|
1778
1853
|
return 'goal';
|
|
1854
|
+
if (id === 'prompt' || id === '提示词' || id === '注入')
|
|
1855
|
+
return 'prompt';
|
|
1779
1856
|
return undefined;
|
|
1780
1857
|
}
|
|
1781
1858
|
/** Split `/find thinking padAnsi` into an optional category and a query. */
|
|
@@ -1812,21 +1889,81 @@ function rowSearchHaystack(row) {
|
|
|
1812
1889
|
return `${row.objective} ${row.blockedReason ?? ''}`;
|
|
1813
1890
|
case 'compaction':
|
|
1814
1891
|
return `${row.summary ?? ''} ${row.error ?? ''}`;
|
|
1892
|
+
case 'prompt':
|
|
1893
|
+
return `${row.sources.join(' ')} ${row.text}`;
|
|
1815
1894
|
default:
|
|
1816
1895
|
return '';
|
|
1817
1896
|
}
|
|
1818
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
|
+
}
|
|
1819
1956
|
export function compactionHeaderText(row) {
|
|
1820
1957
|
const recovered = row.prunedTokens > 0
|
|
1821
|
-
?
|
|
1958
|
+
? t('compact.recoverTokens', { tokens: formatTokens(row.prunedTokens) })
|
|
1822
1959
|
: row.pruneCount > 0
|
|
1823
|
-
?
|
|
1824
|
-
: '
|
|
1960
|
+
? t('compact.pruneChunks', { count: row.pruneCount })
|
|
1961
|
+
: t('compact.prepare');
|
|
1825
1962
|
if (row.status === 'running')
|
|
1826
|
-
return
|
|
1963
|
+
return t('compact.running', { detail: recovered });
|
|
1827
1964
|
if (row.status === 'error')
|
|
1828
|
-
return
|
|
1829
|
-
return
|
|
1965
|
+
return t('compact.failed', { error: row.error ?? t('quota.unknown') });
|
|
1966
|
+
return t('compact.done', { detail: recovered });
|
|
1830
1967
|
}
|
|
1831
1968
|
/** Transcript rows matching a `/find` query, newest last. */
|
|
1832
1969
|
export function matchTranscriptRows(rows, raw) {
|
|
@@ -1849,20 +1986,20 @@ export function planDockNote(plan) {
|
|
|
1849
1986
|
const allDone = plan.todos.length > 0 && plan.todos.every(item => item.status === 'completed');
|
|
1850
1987
|
const leftover = plan.todos.filter(item => item.status !== 'completed').length;
|
|
1851
1988
|
if (plan.turnLeftOpen === true && leftover > 0) {
|
|
1852
|
-
return
|
|
1989
|
+
return t('plan.leftOpen', { count: leftover });
|
|
1853
1990
|
}
|
|
1854
1991
|
if (plan.pending)
|
|
1855
|
-
return '
|
|
1992
|
+
return t('plan.pendingNext');
|
|
1856
1993
|
if (plan.active)
|
|
1857
|
-
return '
|
|
1994
|
+
return t('plan.planningOnly');
|
|
1858
1995
|
if (running)
|
|
1859
|
-
return '
|
|
1996
|
+
return t('plan.executing');
|
|
1860
1997
|
if (allDone)
|
|
1861
|
-
return '
|
|
1998
|
+
return t('plan.allDone');
|
|
1862
1999
|
if (plan.todos.length > 0 || (plan.planMarkdown !== undefined && plan.planMarkdown !== '')) {
|
|
1863
|
-
return '
|
|
2000
|
+
return t('plan.stillOpen');
|
|
1864
2001
|
}
|
|
1865
|
-
return '
|
|
2002
|
+
return t('plan.closed');
|
|
1866
2003
|
}
|
|
1867
2004
|
/** Compact per-status counts matching the web plan strip. */
|
|
1868
2005
|
export function todoProgressLabel(todos) {
|
|
@@ -1871,11 +2008,11 @@ export function todoProgressLabel(todos) {
|
|
|
1871
2008
|
const pending = todos.length - done - active;
|
|
1872
2009
|
const parts = [];
|
|
1873
2010
|
if (done > 0)
|
|
1874
|
-
parts.push(
|
|
2011
|
+
parts.push(t('plan.todoDone', { count: done }));
|
|
1875
2012
|
if (active > 0)
|
|
1876
|
-
parts.push(
|
|
2013
|
+
parts.push(t('plan.todoActive', { count: active }));
|
|
1877
2014
|
if (pending > 0)
|
|
1878
|
-
parts.push(
|
|
2015
|
+
parts.push(t('plan.todoPending', { count: pending }));
|
|
1879
2016
|
return parts.join(' · ');
|
|
1880
2017
|
}
|
|
1881
2018
|
function todoItemKind(status) {
|
|
@@ -2030,7 +2167,7 @@ export function presentToolCall(name, args) {
|
|
|
2030
2167
|
const diff = diffHunksFromArgs(name, args);
|
|
2031
2168
|
const path = diff?.[0]?.path;
|
|
2032
2169
|
return {
|
|
2033
|
-
title:
|
|
2170
|
+
title: toolTitle(name),
|
|
2034
2171
|
summary: path ?? friendlyArgsSummary(name, args),
|
|
2035
2172
|
...diff === null || diff === undefined ? {} : { diff },
|
|
2036
2173
|
};
|
|
@@ -2038,47 +2175,62 @@ export function presentToolCall(name, args) {
|
|
|
2038
2175
|
if (SUBAGENT_TOOL_NAMES.has(name)) {
|
|
2039
2176
|
const description = typeof parsed?.description === 'string' ? parsed.description.trim() : '';
|
|
2040
2177
|
return {
|
|
2041
|
-
title: name === 'subagent_fork' ? '
|
|
2178
|
+
title: toolTitle(name === 'subagent_fork' ? 'subagent_fork' : 'subagent'),
|
|
2042
2179
|
summary: description === '' ? friendlyArgsSummary(name, args) : description,
|
|
2043
2180
|
};
|
|
2044
2181
|
}
|
|
2045
2182
|
if (name === 'todo_write' || name === 'todo') {
|
|
2046
|
-
return { title: '
|
|
2183
|
+
return { title: toolTitle('todo_write'), summary: todoSummary(parsed) };
|
|
2047
2184
|
}
|
|
2048
2185
|
if (name === 'ask_user_question') {
|
|
2049
|
-
return { title: '
|
|
2186
|
+
return { title: toolTitle('ask_user_question'), summary: askSummary(parsed) };
|
|
2050
2187
|
}
|
|
2051
2188
|
if (name === 'exit_plan_mode') {
|
|
2052
2189
|
const plan = typeof parsed?.plan === 'string' ? parsed.plan : '';
|
|
2053
|
-
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) };
|
|
2054
2206
|
}
|
|
2055
2207
|
if (name === 'read') {
|
|
2056
2208
|
const path = typeof parsed?.path === 'string' ? parsed.path
|
|
2057
2209
|
: typeof parsed?.file_path === 'string' ? parsed.file_path
|
|
2058
2210
|
: typeof parsed?.url === 'string' ? parsed.url
|
|
2059
2211
|
: '';
|
|
2060
|
-
return { title: '
|
|
2212
|
+
return { title: toolTitle('read'), summary: path || friendlyArgsSummary(name, args) };
|
|
2061
2213
|
}
|
|
2062
2214
|
if (name === 'grep') {
|
|
2063
2215
|
const pattern = typeof parsed?.pattern === 'string' ? parsed.pattern : '';
|
|
2064
2216
|
const path = typeof parsed?.path === 'string' ? parsed.path : '';
|
|
2065
|
-
return { title: '
|
|
2217
|
+
return { title: toolTitle('grep'), summary: [pattern, path].filter(Boolean).join(' ') || friendlyArgsSummary(name, args) };
|
|
2066
2218
|
}
|
|
2067
2219
|
if (name === 'glob') {
|
|
2068
2220
|
const pattern = typeof parsed?.pattern === 'string' ? parsed.pattern
|
|
2069
2221
|
: typeof parsed?.glob_pattern === 'string' ? parsed.glob_pattern
|
|
2070
2222
|
: '';
|
|
2071
|
-
return { title: '
|
|
2223
|
+
return { title: toolTitle('glob'), summary: pattern || friendlyArgsSummary(name, args) };
|
|
2072
2224
|
}
|
|
2073
2225
|
if (name === 'web_search') {
|
|
2074
2226
|
const query = typeof parsed?.query === 'string' ? parsed.query : typeof parsed?.q === 'string' ? parsed.q : '';
|
|
2075
|
-
return { title: '
|
|
2227
|
+
return { title: toolTitle('web_search'), summary: query || friendlyArgsSummary(name, args) };
|
|
2076
2228
|
}
|
|
2077
2229
|
if (name === 'web_fetch') {
|
|
2078
2230
|
const url = typeof parsed?.url === 'string' ? parsed.url : '';
|
|
2079
|
-
return { title: '
|
|
2231
|
+
return { title: toolTitle('web_fetch'), summary: url || friendlyArgsSummary(name, args) };
|
|
2080
2232
|
}
|
|
2081
|
-
return { title:
|
|
2233
|
+
return { title: toolTitle(name), summary: friendlyArgsSummary(name, args) };
|
|
2082
2234
|
}
|
|
2083
2235
|
/** Validate a tool/result meta payload's structured diff, mirroring the web card. */
|
|
2084
2236
|
export function diffMetaDiffs(meta) {
|
|
@@ -2118,6 +2270,76 @@ function capDisplayLines(lines, maxLines) {
|
|
|
2118
2270
|
return [marker];
|
|
2119
2271
|
return [...lines.slice(0, budget - 2), marker, ...lines.slice(-1)];
|
|
2120
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
|
+
}
|
|
2121
2343
|
/** Flatten hunks into git-style `-`/`+` lines plus the web-compatible footer. */
|
|
2122
2344
|
export function renderToolDiff(diffs, maxLines) {
|
|
2123
2345
|
const rows = [];
|
|
@@ -2239,17 +2461,19 @@ function parseJsonBody(text) {
|
|
|
2239
2461
|
* converted into readable indented content instead of raw JSON text.
|
|
2240
2462
|
*/
|
|
2241
2463
|
export function toolBodyLines(row, maxLines) {
|
|
2464
|
+
const unlimited = !Number.isFinite(maxLines) || maxLines >= Number.MAX_SAFE_INTEGER;
|
|
2242
2465
|
if (row.diff !== undefined && row.diff.length > 0) {
|
|
2243
|
-
// File-edit diffs are never truncated: omitting hunks would
|
|
2244
|
-
// exact code change the model applied. `maxLines` only governs
|
|
2245
|
-
// generic JSON output bodies.
|
|
2246
|
-
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);
|
|
2247
2470
|
}
|
|
2248
2471
|
if (row.command !== undefined) {
|
|
2249
2472
|
const out = [];
|
|
2250
2473
|
if (row.output !== '') {
|
|
2251
|
-
|
|
2252
|
-
|
|
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 });
|
|
2253
2477
|
}
|
|
2254
2478
|
}
|
|
2255
2479
|
else if (row.status !== 'running' && row.status !== undefined) {
|
|
@@ -2257,9 +2481,10 @@ export function toolBodyLines(row, maxLines) {
|
|
|
2257
2481
|
}
|
|
2258
2482
|
return out;
|
|
2259
2483
|
}
|
|
2260
|
-
const specialized = specializedToolBody(row);
|
|
2261
|
-
if (specialized !== null)
|
|
2262
|
-
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
|
+
}
|
|
2263
2488
|
const out = [];
|
|
2264
2489
|
const args = parseJsonArgs(row.args);
|
|
2265
2490
|
if (args !== null && Object.keys(args).length > 0) {
|
|
@@ -2277,12 +2502,13 @@ export function toolBodyLines(row, maxLines) {
|
|
|
2277
2502
|
}
|
|
2278
2503
|
}
|
|
2279
2504
|
else {
|
|
2280
|
-
|
|
2505
|
+
const text = unlimited ? row.output : truncate(row.output, maxLines);
|
|
2506
|
+
for (const line of text.split('\n')) {
|
|
2281
2507
|
out.push({ kind: 'tool-result', text: line });
|
|
2282
2508
|
}
|
|
2283
2509
|
}
|
|
2284
2510
|
}
|
|
2285
|
-
return capDisplayLines(out, maxLines);
|
|
2511
|
+
return unlimited ? out : capDisplayLines(out, maxLines);
|
|
2286
2512
|
}
|
|
2287
2513
|
function firstString(record, keys) {
|
|
2288
2514
|
for (const key of keys) {
|
|
@@ -2292,9 +2518,11 @@ function firstString(record, keys) {
|
|
|
2292
2518
|
}
|
|
2293
2519
|
return '';
|
|
2294
2520
|
}
|
|
2295
|
-
function specializedToolBody(row) {
|
|
2521
|
+
function specializedToolBody(row, maxLines = Number.MAX_SAFE_INTEGER) {
|
|
2296
2522
|
const name = row.name ?? '';
|
|
2297
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));
|
|
2298
2526
|
if (name === 'todo_write' || name === 'todo') {
|
|
2299
2527
|
const todos = parsePlanTodos(args ?? row.args);
|
|
2300
2528
|
const out = [{ kind: 'diff-path', text: todoProgressLabel(todos) || '待办列表' }];
|
|
@@ -2332,7 +2560,7 @@ function specializedToolBody(row) {
|
|
|
2332
2560
|
out.push({ kind: 'tool-result', text: `offset ${offset ?? 1}${limit === undefined ? '' : ` · limit ${limit}`}` });
|
|
2333
2561
|
}
|
|
2334
2562
|
if (row.output !== '') {
|
|
2335
|
-
for (const line of
|
|
2563
|
+
for (const line of take(row.output, 40).split('\n')) {
|
|
2336
2564
|
out.push({ kind: 'tool-result', text: line });
|
|
2337
2565
|
}
|
|
2338
2566
|
}
|
|
@@ -2346,7 +2574,7 @@ function specializedToolBody(row) {
|
|
|
2346
2574
|
const path = firstString(args, ['path', 'glob']);
|
|
2347
2575
|
const out = [{ kind: 'diff-path', text: [pattern, path].filter(Boolean).join(' ') || name }];
|
|
2348
2576
|
if (row.output !== '') {
|
|
2349
|
-
for (const line of
|
|
2577
|
+
for (const line of take(row.output, 30).split('\n')) {
|
|
2350
2578
|
out.push({ kind: 'tool-result', text: line });
|
|
2351
2579
|
}
|
|
2352
2580
|
}
|
|
@@ -2356,12 +2584,27 @@ function specializedToolBody(row) {
|
|
|
2356
2584
|
const query = firstString(args, ['query', 'q', 'url']);
|
|
2357
2585
|
const out = [{ kind: 'diff-path', text: query || name }];
|
|
2358
2586
|
if (row.output !== '') {
|
|
2359
|
-
for (const line of
|
|
2587
|
+
for (const line of take(row.output, 24).split('\n')) {
|
|
2360
2588
|
out.push({ kind: 'assistant', text: line });
|
|
2361
2589
|
}
|
|
2362
2590
|
}
|
|
2363
2591
|
return out;
|
|
2364
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
|
+
}
|
|
2365
2608
|
return null;
|
|
2366
2609
|
}
|
|
2367
2610
|
/** Recover the shell tools' exit marker, mirroring @deepseek-ai/dsh-shell/render. */
|
|
@@ -2458,6 +2701,10 @@ export class SshTui {
|
|
|
2458
2701
|
lastPaintHeight = 0;
|
|
2459
2702
|
lastChromeStart = 0;
|
|
2460
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;
|
|
2461
2708
|
paintIntervalMs;
|
|
2462
2709
|
paintLink = 'local';
|
|
2463
2710
|
paintProbed = false;
|
|
@@ -2499,7 +2746,10 @@ export class SshTui {
|
|
|
2499
2746
|
});
|
|
2500
2747
|
this.pushRow({ kind: 'brand-logo' });
|
|
2501
2748
|
this.pushRow({ kind: 'system', text: 'DeepSeek Harness — SSH TUI' });
|
|
2502
|
-
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
|
+
}
|
|
2503
2753
|
}
|
|
2504
2754
|
/** Enter raw mode, switch to the alternate screen, and start listening. */
|
|
2505
2755
|
start() {
|
|
@@ -2830,7 +3080,8 @@ export class SshTui {
|
|
|
2830
3080
|
|| row.kind === 'plan'
|
|
2831
3081
|
|| row.kind === 'question'
|
|
2832
3082
|
|| row.kind === 'goal'
|
|
2833
|
-
|| row.kind === 'compaction'
|
|
3083
|
+
|| row.kind === 'compaction'
|
|
3084
|
+
|| row.kind === 'prompt');
|
|
2834
3085
|
if (this.streaming !== undefined && this.streaming.reasoning !== '') {
|
|
2835
3086
|
this.streamingReasoning ??= { kind: 'streaming-reasoning', expanded: false };
|
|
2836
3087
|
rows.push(this.streamingReasoning);
|
|
@@ -2943,7 +3194,7 @@ export class SshTui {
|
|
|
2943
3194
|
const summary = title ?? (counts === '' ? '还没有任务' : counts);
|
|
2944
3195
|
const marker = plan.expanded ? '▾' : '▸';
|
|
2945
3196
|
const focused = this.focusedRow === plan ? '▶ ' : ' ';
|
|
2946
|
-
const header = `${focused}${marker} ${mode}${spinner} · ${summary}${plan.expanded || yieldBottom ? '' : '
|
|
3197
|
+
const header = `${focused}${marker} ${mode}${spinner} · ${summary}${plan.expanded || yieldBottom ? '' : t('card.expand')}`;
|
|
2947
3198
|
const lines = [this.styleLine('plan-dock', padToWidth(header, width))];
|
|
2948
3199
|
if (yieldBottom || !plan.expanded)
|
|
2949
3200
|
return lines;
|
|
@@ -2975,6 +3226,104 @@ export class SshTui {
|
|
|
2975
3226
|
}
|
|
2976
3227
|
return lines;
|
|
2977
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
|
+
}
|
|
2978
3327
|
paintCollapsibleHeader(addDisplay, row, kind, header, width, colorize) {
|
|
2979
3328
|
const focused = this.focusedRow === row;
|
|
2980
3329
|
const marker = row.expanded ? '▾' : '▸';
|
|
@@ -3021,8 +3370,23 @@ export class SshTui {
|
|
|
3021
3370
|
const target = focused ?? rows[rows.length - 1];
|
|
3022
3371
|
if (target === undefined)
|
|
3023
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
|
+
}
|
|
3024
3387
|
target.expanded = !target.expanded;
|
|
3025
3388
|
this.focusedRow = target;
|
|
3389
|
+
this.forceFullPaint = true;
|
|
3026
3390
|
this.markDirty();
|
|
3027
3391
|
}
|
|
3028
3392
|
/** Expand all collapsible blocks, or collapse them again when all are open. */
|
|
@@ -3031,9 +3395,26 @@ export class SshTui {
|
|
|
3031
3395
|
if (rows.length === 0)
|
|
3032
3396
|
return;
|
|
3033
3397
|
const allExpanded = rows.every(row => row.expanded);
|
|
3034
|
-
|
|
3035
|
-
row
|
|
3036
|
-
|
|
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;
|
|
3037
3418
|
this.markDirty();
|
|
3038
3419
|
}
|
|
3039
3420
|
highlightSearchLine(line) {
|
|
@@ -3044,9 +3425,21 @@ export class SshTui {
|
|
|
3044
3425
|
revealRow(row) {
|
|
3045
3426
|
if (row === undefined)
|
|
3046
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
|
+
}
|
|
3047
3439
|
if (row.kind !== 'assistant' && 'expanded' in row) {
|
|
3048
3440
|
row.expanded = true;
|
|
3049
3441
|
this.focusedRow = row;
|
|
3442
|
+
this.forceFullPaint = true;
|
|
3050
3443
|
}
|
|
3051
3444
|
else {
|
|
3052
3445
|
this.focusedRow = null;
|
|
@@ -3063,18 +3456,18 @@ export class SshTui {
|
|
|
3063
3456
|
const live = this.findLivePlanRow();
|
|
3064
3457
|
if (live !== undefined) {
|
|
3065
3458
|
this.focusCard(live);
|
|
3066
|
-
this.pushRow({ kind: 'system', text:
|
|
3459
|
+
this.pushRow({ kind: 'system', text: t('jump.planDock', { category: cardCategoryLabel(category) }) });
|
|
3067
3460
|
this.revealRow(live);
|
|
3068
3461
|
return;
|
|
3069
3462
|
}
|
|
3070
3463
|
}
|
|
3071
3464
|
const target = this.rows.findLast(row => cardCategoryOf(row) === category);
|
|
3072
3465
|
if (target === undefined) {
|
|
3073
|
-
this.pushRow({ kind: 'system', text:
|
|
3466
|
+
this.pushRow({ kind: 'system', text: t('jump.missing', { category: cardCategoryLabel(category) }) });
|
|
3074
3467
|
this.markDirty();
|
|
3075
3468
|
return;
|
|
3076
3469
|
}
|
|
3077
|
-
this.pushRow({ kind: 'system', text:
|
|
3470
|
+
this.pushRow({ kind: 'system', text: t('jump.latest', { category: cardCategoryLabel(category) }) });
|
|
3078
3471
|
this.revealRow(target);
|
|
3079
3472
|
}
|
|
3080
3473
|
applySearchHits(query, hits) {
|
|
@@ -3088,7 +3481,7 @@ export class SshTui {
|
|
|
3088
3481
|
}
|
|
3089
3482
|
this.searchIndex = hits.length - 1;
|
|
3090
3483
|
const hit = hits[this.searchIndex];
|
|
3091
|
-
const where = hit === undefined ? '' :
|
|
3484
|
+
const where = hit === undefined ? '' : cardCategoryLabel(cardCategoryOf(hit) ?? 'reply');
|
|
3092
3485
|
this.pushRow({
|
|
3093
3486
|
kind: 'system',
|
|
3094
3487
|
text: `找到 ${hits.length} 条${query === '' ? '' : `「${query}」`} · 第 ${hits.length}/${hits.length} 条(${where})。Ctrl+G / Alt+N 下一条,Alt+P 上一条。`,
|
|
@@ -3097,7 +3490,7 @@ export class SshTui {
|
|
|
3097
3490
|
}
|
|
3098
3491
|
runFindCommand(arg) {
|
|
3099
3492
|
const parsed = parseFindQuery(arg);
|
|
3100
|
-
const label = parsed.category === undefined ? '' : `${
|
|
3493
|
+
const label = parsed.category === undefined ? '' : `${cardCategoryLabel(parsed.category)} `;
|
|
3101
3494
|
const hits = matchTranscriptRows(this.rows, arg);
|
|
3102
3495
|
this.applySearchHits(`${label}${parsed.query}`.trim(), hits);
|
|
3103
3496
|
}
|
|
@@ -3110,7 +3503,7 @@ export class SshTui {
|
|
|
3110
3503
|
const count = this.searchHits.length;
|
|
3111
3504
|
this.searchIndex = (this.searchIndex + delta + count) % count;
|
|
3112
3505
|
const hit = this.searchHits[this.searchIndex];
|
|
3113
|
-
const where = hit === undefined ? '' :
|
|
3506
|
+
const where = hit === undefined ? '' : cardCategoryLabel(cardCategoryOf(hit) ?? 'reply');
|
|
3114
3507
|
this.pushRow({
|
|
3115
3508
|
kind: 'system',
|
|
3116
3509
|
text: `搜索「${this.searchQuery}」· 第 ${this.searchIndex + 1}/${count} 条(${where})。`,
|
|
@@ -3122,6 +3515,10 @@ export class SshTui {
|
|
|
3122
3515
|
return;
|
|
3123
3516
|
const width = Math.max(10, process.stdout.columns || 80);
|
|
3124
3517
|
const height = Math.max(6, process.stdout.rows || 24);
|
|
3518
|
+
if (this.dialog?.kind === 'inspect') {
|
|
3519
|
+
this.paintInspectOverlay(width, height);
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3125
3522
|
const display = [];
|
|
3126
3523
|
const displayRefs = [];
|
|
3127
3524
|
const searchHit = this.searchHits[this.searchIndex];
|
|
@@ -3159,7 +3556,7 @@ export class SshTui {
|
|
|
3159
3556
|
const focused = this.focusedRow === row;
|
|
3160
3557
|
const marker = row.expanded ? '▾' : '▸';
|
|
3161
3558
|
const lines = row.text.split('\n').length;
|
|
3162
|
-
const header = `${marker} 已思考 · ${lines} 行${row.expanded ? '' : '
|
|
3559
|
+
const header = `${marker} 已思考 · ${lines} 行${row.expanded ? '' : t('card.expand')}`;
|
|
3163
3560
|
const line = `${focused ? '▶ ' : ' '}${header}`;
|
|
3164
3561
|
const styled = this.styleLine('reasoning', line);
|
|
3165
3562
|
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
@@ -3172,91 +3569,62 @@ export class SshTui {
|
|
|
3172
3569
|
}
|
|
3173
3570
|
if (row.kind === 'tool') {
|
|
3174
3571
|
const running = row.status === undefined || row.status === 'running';
|
|
3175
|
-
const ok = row.status === 'ok';
|
|
3176
|
-
// Header text follows the execution state; the shell command itself
|
|
3177
|
-
// stays dim so it reads like a command, not a status.
|
|
3178
|
-
const stateCode = !this.color ? undefined : running ? '33' : ok ? '32' : '31';
|
|
3179
|
-
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3180
|
-
const state = running ? 'running…' : ok ? 'ok' : 'error';
|
|
3181
|
-
const exit = !running && row.command !== undefined
|
|
3182
|
-
? row.signal !== undefined
|
|
3183
|
-
? ` [信号 ${row.signal}]`
|
|
3184
|
-
: (row.exitCode ?? 0) !== 0
|
|
3185
|
-
? ` [退出码 ${row.exitCode}]`
|
|
3186
|
-
: ''
|
|
3187
|
-
: '';
|
|
3188
3572
|
const focused = this.focusedRow === row;
|
|
3189
|
-
const
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
:
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
{ start: lead.length + summaryText.length, end: plainHeader.length, sgr: stateCode },
|
|
3202
|
-
];
|
|
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 : [];
|
|
3203
3585
|
if (!row.expanded) {
|
|
3204
|
-
const collapsed = truncateToWidth(
|
|
3205
|
-
const styled =
|
|
3586
|
+
const collapsed = truncateToWidth(header.plain, Math.max(1, width - 2));
|
|
3587
|
+
const styled = headerSegments.length === 0
|
|
3206
3588
|
? collapsed
|
|
3207
3589
|
: paintSegmentedLine(collapsed, 0, collapsed.length, headerSegments);
|
|
3208
3590
|
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
3209
3591
|
continue;
|
|
3210
3592
|
}
|
|
3211
3593
|
const expandedHeaderLines = headerSegments.length === 0
|
|
3212
|
-
? wrap(
|
|
3213
|
-
: wrapSegmented(
|
|
3594
|
+
? wrap(header.plain, width)
|
|
3595
|
+
: wrapSegmented(header.plain, Math.max(1, width), headerSegments);
|
|
3214
3596
|
for (const wrapped of expandedHeaderLines) {
|
|
3215
3597
|
addDisplay(wrapped, row);
|
|
3216
3598
|
}
|
|
3217
|
-
const
|
|
3218
|
-
|
|
3219
|
-
const inner = Math.max(1, width - 2);
|
|
3220
|
-
const fillRow = line.kind === 'diff-add' || line.kind === 'diff-del';
|
|
3221
|
-
for (const wrapped of wrap(line.text, inner)) {
|
|
3222
|
-
const body = fillRow ? padToWidth(` ${wrapped}`, width) : ` ${wrapped}`;
|
|
3223
|
-
// Diffs and todo marks keep their dedicated colors; ordinary
|
|
3224
|
-
// results follow the tool's execution state (yellow running /
|
|
3225
|
-
// green ok / red error).
|
|
3226
|
-
const kind = line.kind;
|
|
3227
|
-
const style = kind === 'diff-add' || kind === 'diff-del' || kind === 'diff-path'
|
|
3228
|
-
? this.styleLine(kind, body)
|
|
3229
|
-
: kind === 'todo-done' || kind === 'todo-active' || kind === 'todo-pending'
|
|
3230
|
-
? this.styleLine(kind, body)
|
|
3231
|
-
: this.styleStatusText(bodyCode ?? (kind === 'error' ? '31' : '37'), body);
|
|
3232
|
-
addDisplay(style, row);
|
|
3233
|
-
}
|
|
3599
|
+
for (const line of toolBodyLines(row, Number.MAX_SAFE_INTEGER)) {
|
|
3600
|
+
this.paintToolBodyLine(addDisplay, row, line, width);
|
|
3234
3601
|
}
|
|
3235
3602
|
continue;
|
|
3236
3603
|
}
|
|
3237
3604
|
if (row.kind === 'subagent') {
|
|
3238
3605
|
const running = row.status === 'running';
|
|
3239
3606
|
const ok = row.status === 'ok';
|
|
3240
|
-
const
|
|
3607
|
+
const aborted = row.status === 'aborted';
|
|
3608
|
+
const dotColor = !this.color ? undefined : running ? '33' : ok ? '32' : aborted ? '33' : '31';
|
|
3241
3609
|
const styleHeader = (line) => {
|
|
3242
3610
|
const safe = sanitizeTerminalText(line);
|
|
3243
3611
|
if (!this.color)
|
|
3244
3612
|
return safe;
|
|
3245
3613
|
const dotIndex = safe.indexOf('●');
|
|
3246
3614
|
if (dotColor === undefined || dotIndex === -1)
|
|
3247
|
-
return
|
|
3248
|
-
return
|
|
3615
|
+
return safe;
|
|
3616
|
+
return `${safe.slice(0, dotIndex)}\x1b[${dotColor}m●\x1b[0m${safe.slice(dotIndex + 1)}`;
|
|
3249
3617
|
};
|
|
3250
3618
|
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3251
|
-
const header = `● ${subagentHeaderText(row)}${spinner}${row.expanded ? '' : '
|
|
3252
|
-
this.paintCollapsibleHeader(addDisplay, row, '
|
|
3619
|
+
const header = `● ${subagentHeaderText(row)}${spinner}${row.expanded ? '' : t('card.expand')}`;
|
|
3620
|
+
this.paintCollapsibleHeader(addDisplay, row, 'system', header, width, styleHeader);
|
|
3253
3621
|
if (row.expanded) {
|
|
3254
|
-
addDisplay(this.styleLine('
|
|
3622
|
+
addDisplay(this.styleLine('system', ` 会话 ${row.sessionId} · ${row.provider}${row.local ? '' : ' · 外部进程'}`), row);
|
|
3255
3623
|
if (row.stopReason !== undefined) {
|
|
3256
|
-
addDisplay(this.styleLine('
|
|
3624
|
+
addDisplay(this.styleLine('system', ` 结束原因:${row.stopReason}`), row);
|
|
3257
3625
|
}
|
|
3258
3626
|
if (row.logs.length === 0) {
|
|
3259
|
-
addDisplay(this.styleLine('
|
|
3627
|
+
addDisplay(this.styleLine('system', running ? ' 等待子代理输出…' : ' 没有可见输出'), row);
|
|
3260
3628
|
}
|
|
3261
3629
|
else {
|
|
3262
3630
|
for (const entry of row.logs) {
|
|
@@ -3264,7 +3632,7 @@ export class SshTui {
|
|
|
3264
3632
|
? 'assistant'
|
|
3265
3633
|
: entry.kind === 'result' && row.status === 'error'
|
|
3266
3634
|
? 'error'
|
|
3267
|
-
: '
|
|
3635
|
+
: 'system';
|
|
3268
3636
|
for (const wrapped of wrap(entry.text, Math.max(1, width - 2))) {
|
|
3269
3637
|
addDisplay(this.styleLine(kind, ` ${wrapped}`), row);
|
|
3270
3638
|
}
|
|
@@ -3279,7 +3647,7 @@ export class SshTui {
|
|
|
3279
3647
|
const counts = todoProgressLabel(row.todos);
|
|
3280
3648
|
const title = planTitleFromMarkdown(row.planMarkdown ?? '');
|
|
3281
3649
|
const summary = title ?? (counts === '' ? '已归档' : counts);
|
|
3282
|
-
const header = `计划 · ${summary}${row.expanded ? '' : '
|
|
3650
|
+
const header = `计划 · ${summary}${row.expanded ? '' : t('card.expand')}`;
|
|
3283
3651
|
this.paintCollapsibleHeader(addDisplay, row, 'plan-dock', header, width);
|
|
3284
3652
|
if (row.expanded) {
|
|
3285
3653
|
addDisplay(this.styleLine('plan-dock', ` ${planDockNote({ ...row, active: false, pending: false })}`), row);
|
|
@@ -3297,12 +3665,22 @@ export class SshTui {
|
|
|
3297
3665
|
}
|
|
3298
3666
|
continue;
|
|
3299
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
|
+
}
|
|
3300
3678
|
if (row.kind === 'question') {
|
|
3301
3679
|
const waiting = row.status === 'waiting';
|
|
3302
3680
|
const spinner = waiting ? ` ${this.spinnerFrame()}` : '';
|
|
3303
3681
|
const state = waiting ? '等待回答' : row.status === 'answered' ? '已回答' : '已取消';
|
|
3304
3682
|
const title = row.intent === 'plan-review' ? '计划待审' : '提问用户';
|
|
3305
|
-
const header = `● ${title}${spinner} · ${state} · ${row.summary}${row.expanded ? '' : '
|
|
3683
|
+
const header = `● ${title}${spinner} · ${state} · ${row.summary}${row.expanded ? '' : t('card.expand')}`;
|
|
3306
3684
|
this.paintCollapsibleHeader(addDisplay, row, waiting ? 'tool' : 'system', header, width);
|
|
3307
3685
|
if (row.expanded) {
|
|
3308
3686
|
if (row.header !== undefined)
|
|
@@ -3336,7 +3714,7 @@ export class SshTui {
|
|
|
3336
3714
|
: row.phase === 'blocked' ? '受阻'
|
|
3337
3715
|
: row.phase === 'complete' ? '已完成'
|
|
3338
3716
|
: '已清除';
|
|
3339
|
-
const header = `● 目标${spinner} · ${phase} · ${row.objective}${row.expanded ? '' : '
|
|
3717
|
+
const header = `● 目标${spinner} · ${phase} · ${row.objective}${row.expanded ? '' : t('card.expand')}`;
|
|
3340
3718
|
this.paintCollapsibleHeader(addDisplay, row, live ? 'tool' : 'system', header, width);
|
|
3341
3719
|
if (row.expanded) {
|
|
3342
3720
|
addDisplay(this.styleLine('system', ' 用 /goal 查看、暂停、恢复或清除当前目标。'), row);
|
|
@@ -3352,7 +3730,7 @@ export class SshTui {
|
|
|
3352
3730
|
const running = row.status === 'running';
|
|
3353
3731
|
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3354
3732
|
const elapsed = Math.max(0, Math.floor(((row.endedAt ?? Date.now()) - row.startedAt) / 1000));
|
|
3355
|
-
const header = `● ${compactionHeaderText(row)}${spinner} · ${elapsed}s${row.expanded ? '' : '
|
|
3733
|
+
const header = `● ${compactionHeaderText(row)}${spinner} · ${elapsed}s${row.expanded ? '' : t('card.expand')}`;
|
|
3356
3734
|
this.paintCollapsibleHeader(addDisplay, row, running ? 'tool' : row.status === 'error' ? 'error' : 'system', header, width);
|
|
3357
3735
|
if (row.expanded) {
|
|
3358
3736
|
addDisplay(this.styleLine('system', running
|
|
@@ -3416,53 +3794,57 @@ export class SshTui {
|
|
|
3416
3794
|
else if (this.dialog.kind === 'onboarding') {
|
|
3417
3795
|
const ob = this.onboarding;
|
|
3418
3796
|
if (ob !== undefined) {
|
|
3419
|
-
const template =
|
|
3797
|
+
const template = providerTemplates()[ob.providerType];
|
|
3420
3798
|
const providerLabel = `${template.label}${template.defaultBaseUrl === '' ? '' : `(${template.defaultBaseUrl})`}`;
|
|
3421
3799
|
switch (ob.step) {
|
|
3422
3800
|
case 'provider':
|
|
3423
|
-
addDialog('
|
|
3424
|
-
addDialog('
|
|
3425
|
-
addDialog('
|
|
3426
|
-
addDialog('
|
|
3427
|
-
addDialog('
|
|
3428
|
-
addDialog('
|
|
3429
|
-
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'));
|
|
3430
3808
|
break;
|
|
3431
3809
|
case 'id':
|
|
3432
|
-
addDialog(
|
|
3433
|
-
addDialog('
|
|
3434
|
-
addDialog(
|
|
3435
|
-
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'));
|
|
3436
3814
|
break;
|
|
3437
3815
|
case 'key':
|
|
3438
|
-
addDialog(
|
|
3439
|
-
addDialog('
|
|
3440
|
-
addDialog('
|
|
3816
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3817
|
+
addDialog(t('onboard.keyPrompt'));
|
|
3818
|
+
addDialog(t('onboard.enterEsc'));
|
|
3441
3819
|
break;
|
|
3442
3820
|
case 'base-url':
|
|
3443
|
-
addDialog(
|
|
3444
|
-
addDialog(
|
|
3445
|
-
addDialog('
|
|
3821
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3822
|
+
addDialog(t('onboard.basePrompt', { fallback: template.defaultBaseUrl || t('onboard.baseFallback') }));
|
|
3823
|
+
addDialog(t('onboard.enterEsc'));
|
|
3446
3824
|
break;
|
|
3447
3825
|
case 'models':
|
|
3448
|
-
addDialog(
|
|
3449
|
-
addDialog('
|
|
3826
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3827
|
+
addDialog(t('onboard.modelsPrompt'));
|
|
3450
3828
|
addDialog(ob.models.length > 0
|
|
3451
|
-
?
|
|
3452
|
-
:
|
|
3829
|
+
? t('onboard.modelsFetched', { count: ob.models.length, list: formatModelList(ob.models, 6) })
|
|
3830
|
+
: t('onboard.default', { value: template.defaultModels.join(', ') }));
|
|
3453
3831
|
if (template.api !== undefined)
|
|
3454
|
-
addDialog('
|
|
3455
|
-
addDialog('
|
|
3832
|
+
addDialog(t('onboard.ctrlF'));
|
|
3833
|
+
addDialog(t('onboard.enterEsc'));
|
|
3456
3834
|
break;
|
|
3457
3835
|
case 'confirm':
|
|
3458
|
-
addDialog('
|
|
3459
|
-
addDialog(
|
|
3836
|
+
addDialog(t('onboard.confirmTitle'));
|
|
3837
|
+
addDialog(t('onboard.confirmProvider', { label: providerLabel }));
|
|
3460
3838
|
addDialog(` Provider ID: ${ob.providerId}`);
|
|
3461
|
-
addDialog(
|
|
3462
|
-
addDialog(
|
|
3463
|
-
addDialog(
|
|
3464
|
-
addDialog(
|
|
3465
|
-
|
|
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'));
|
|
3466
3848
|
break;
|
|
3467
3849
|
}
|
|
3468
3850
|
}
|
|
@@ -3674,6 +4056,7 @@ export class SshTui {
|
|
|
3674
4056
|
foldedInput: inputView.folded,
|
|
3675
4057
|
multiLineInput: inputRows > 1,
|
|
3676
4058
|
queued: this.pendingMessages.size,
|
|
4059
|
+
cwdLabel: formatFooterCwd(this.workspaceCwd()),
|
|
3677
4060
|
};
|
|
3678
4061
|
const activity = footerActivity(footer);
|
|
3679
4062
|
const activityText = activity.kind === 'compacting'
|
|
@@ -3681,7 +4064,8 @@ export class SshTui {
|
|
|
3681
4064
|
: activity.kind === 'subagents'
|
|
3682
4065
|
? `${this.spinnerFrame(160)} ${activity.text}`
|
|
3683
4066
|
: activity.text;
|
|
3684
|
-
const
|
|
4067
|
+
const identity = footerIdentityParts(footer);
|
|
4068
|
+
const statusText = fitFooterStatusLine(activityText, identity, Math.max(1, width));
|
|
3685
4069
|
const statusLine = this.styleLine('system', statusText);
|
|
3686
4070
|
const paintRows = [
|
|
3687
4071
|
...headerLines,
|
|
@@ -3720,7 +4104,11 @@ export class SshTui {
|
|
|
3720
4104
|
].join('\x1f');
|
|
3721
4105
|
const chromeChanged = chromeKey !== this.lastChromeKey || chromeStart !== this.lastChromeStart;
|
|
3722
4106
|
const transcriptScrolled = start !== this.lastTranscriptStart;
|
|
3723
|
-
const sizeChanged =
|
|
4107
|
+
const sizeChanged = this.forceFullPaint
|
|
4108
|
+
|| width !== this.lastPaintWidth
|
|
4109
|
+
|| height !== this.lastPaintHeight
|
|
4110
|
+
|| transcriptScrolled;
|
|
4111
|
+
this.forceFullPaint = false;
|
|
3724
4112
|
// One stdout write per frame: dirty rows only, so jump-host SSH sees a
|
|
3725
4113
|
// single packet instead of one write per line. Clip/pad so leftover
|
|
3726
4114
|
// wide glyphs cannot wrap into the input box.
|
|
@@ -3744,7 +4132,19 @@ export class SshTui {
|
|
|
3744
4132
|
this.lastPaintHeight = height;
|
|
3745
4133
|
this.lastChromeStart = chromeStart;
|
|
3746
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;
|
|
3747
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
|
+
}
|
|
3748
4148
|
buildSuggestions() {
|
|
3749
4149
|
const input = this.input;
|
|
3750
4150
|
if (!input.startsWith('/'))
|
|
@@ -3756,7 +4156,7 @@ export class SshTui {
|
|
|
3756
4156
|
local: false,
|
|
3757
4157
|
}));
|
|
3758
4158
|
const all = [
|
|
3759
|
-
...
|
|
4159
|
+
...localizedCommands().map(command => ({ name: command.name, description: command.description, local: true })),
|
|
3760
4160
|
...dsh,
|
|
3761
4161
|
];
|
|
3762
4162
|
const filtered = prefix === ''
|
|
@@ -3883,13 +4283,6 @@ export class SshTui {
|
|
|
3883
4283
|
this.dirty = false;
|
|
3884
4284
|
this.paint();
|
|
3885
4285
|
};
|
|
3886
|
-
/** Color one line with an explicit SGR code (tool state colors, etc.). */
|
|
3887
|
-
styleStatusText(code, text) {
|
|
3888
|
-
const safe = sanitizeTerminalText(text);
|
|
3889
|
-
if (!this.color)
|
|
3890
|
-
return safe;
|
|
3891
|
-
return `\x1b[${code}m${safe}\x1b[0m`;
|
|
3892
|
-
}
|
|
3893
4286
|
styleLine(kind, text) {
|
|
3894
4287
|
const safe = sanitizeTerminalText(text);
|
|
3895
4288
|
if (!this.color)
|
|
@@ -3898,7 +4291,7 @@ export class SshTui {
|
|
|
3898
4291
|
kind === 'assistant' ? '1;37' :
|
|
3899
4292
|
kind === 'reasoning' ? '2;3' :
|
|
3900
4293
|
kind === 'brand' ? '1;38;2;77;107;253' :
|
|
3901
|
-
kind === 'tool' || kind === 'tool-result' ? '
|
|
4294
|
+
kind === 'tool' || kind === 'tool-result' ? '37' :
|
|
3902
4295
|
// Codex-like: muted add/del that blend into the terminal background.
|
|
3903
4296
|
kind === 'diff-add' ? '38;2;122;168;116;48;2;18;42;24' :
|
|
3904
4297
|
kind === 'diff-del' ? '38;2;196;122;122;48;2;48;20;20' :
|
|
@@ -3949,11 +4342,15 @@ export class SshTui {
|
|
|
3949
4342
|
.map(block => block.text)
|
|
3950
4343
|
.join('');
|
|
3951
4344
|
if (text !== '') {
|
|
3952
|
-
const
|
|
4345
|
+
const source = event.data.source;
|
|
4346
|
+
const sourceKind = source.kind ?? '';
|
|
3953
4347
|
if (sourceKind === 'user') {
|
|
3954
4348
|
this.pushRow({ kind: 'user', text: `❯ ${text}` });
|
|
3955
4349
|
}
|
|
3956
|
-
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') {
|
|
3957
4354
|
this.pushRow({ kind: 'system', text: text });
|
|
3958
4355
|
}
|
|
3959
4356
|
else {
|
|
@@ -4042,22 +4439,24 @@ export class SshTui {
|
|
|
4042
4439
|
case 'tool/call': {
|
|
4043
4440
|
this.openToolCalls.set(String(event.data.callId), event.data.name);
|
|
4044
4441
|
this.pendingToolTimes.set(String(event.data.callId), event.time);
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
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
|
+
}
|
|
4061
4460
|
if (event.data.name === 'exit_plan_mode') {
|
|
4062
4461
|
const markdown = planMarkdownFromArgs(event.data.arguments);
|
|
4063
4462
|
if (markdown !== undefined)
|
|
@@ -4080,8 +4479,6 @@ export class SshTui {
|
|
|
4080
4479
|
const metaDiffs = diffMetaDiffs(event.data.meta);
|
|
4081
4480
|
if (metaDiffs !== null) {
|
|
4082
4481
|
row.diff = metaDiffs;
|
|
4083
|
-
if (DIFF_TOOL_NAMES.has(row.name))
|
|
4084
|
-
row.expanded = true;
|
|
4085
4482
|
}
|
|
4086
4483
|
const isShell = SHELL_TOOL_NAMES.has(row.name);
|
|
4087
4484
|
if (isShell) {
|
|
@@ -4481,6 +4878,16 @@ export class SshTui {
|
|
|
4481
4878
|
this.pushRow({ kind: 'system', text: `${notice}:${objective}` });
|
|
4482
4879
|
this.markDirty();
|
|
4483
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
|
+
}
|
|
4484
4891
|
handleSubagentExtensionEvent(row, event) {
|
|
4485
4892
|
const type = String(event.type);
|
|
4486
4893
|
const data = event.data;
|
|
@@ -4516,6 +4923,8 @@ export class SshTui {
|
|
|
4516
4923
|
break;
|
|
4517
4924
|
}
|
|
4518
4925
|
case 'tool/call': {
|
|
4926
|
+
if (HIDDEN_TOOL_NAMES.has(event.data.name))
|
|
4927
|
+
break;
|
|
4519
4928
|
const present = presentToolCall(event.data.name, event.data.arguments);
|
|
4520
4929
|
appendSubagentLog(row, { kind: 'tool', text: `▶ ${present.title} ${present.summary}` });
|
|
4521
4930
|
break;
|
|
@@ -4812,7 +5221,7 @@ export class SshTui {
|
|
|
4812
5221
|
/** Default listing endpoint for a built-in OpenCode route with no stored base URL. */
|
|
4813
5222
|
openCodeListingBaseURL(provider) {
|
|
4814
5223
|
if (provider === 'opencode-go')
|
|
4815
|
-
return
|
|
5224
|
+
return providerTemplates()['opencode-go'].defaultBaseUrl;
|
|
4816
5225
|
if (provider === 'opencode')
|
|
4817
5226
|
return OPENCODE_ZEN_BASE_URL;
|
|
4818
5227
|
return undefined;
|
|
@@ -5365,7 +5774,7 @@ export class SshTui {
|
|
|
5365
5774
|
return;
|
|
5366
5775
|
}
|
|
5367
5776
|
const choices = [
|
|
5368
|
-
{ id: undefined, label: SUBAGENT_DEFAULT_EFFORT_LABEL },
|
|
5777
|
+
{ id: undefined, label: SUBAGENT_DEFAULT_EFFORT_LABEL() },
|
|
5369
5778
|
...effortOptions.map(option => ({ id: option.id, label: option.label })),
|
|
5370
5779
|
];
|
|
5371
5780
|
const answer = await this.askQuestion({
|
|
@@ -5398,6 +5807,41 @@ export class SshTui {
|
|
|
5398
5807
|
});
|
|
5399
5808
|
this.markDirty();
|
|
5400
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
|
+
}
|
|
5401
5845
|
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
5402
5846
|
async runModeCommand() {
|
|
5403
5847
|
const agentPresets = this.ctx.get('agentPresets');
|
|
@@ -5762,7 +6206,10 @@ export class SshTui {
|
|
|
5762
6206
|
if (match !== null) {
|
|
5763
6207
|
switch (match[1]) {
|
|
5764
6208
|
case 'A':
|
|
5765
|
-
if (this.
|
|
6209
|
+
if (this.dialog?.kind === 'inspect') {
|
|
6210
|
+
this.scrollInspectOrTranscript(-1);
|
|
6211
|
+
}
|
|
6212
|
+
else if (this.suggestionsVisible()) {
|
|
5766
6213
|
this.suggestionIndex = Math.max(0, this.suggestionIndex - 1);
|
|
5767
6214
|
this.markDirty();
|
|
5768
6215
|
}
|
|
@@ -5774,7 +6221,10 @@ export class SshTui {
|
|
|
5774
6221
|
}
|
|
5775
6222
|
return;
|
|
5776
6223
|
case 'B':
|
|
5777
|
-
if (this.
|
|
6224
|
+
if (this.dialog?.kind === 'inspect') {
|
|
6225
|
+
this.scrollInspectOrTranscript(1);
|
|
6226
|
+
}
|
|
6227
|
+
else if (this.suggestionsVisible()) {
|
|
5778
6228
|
this.suggestionIndex = Math.min(this.commandSuggestions.length - 1, this.suggestionIndex + 1);
|
|
5779
6229
|
this.markDirty();
|
|
5780
6230
|
}
|
|
@@ -5799,13 +6249,11 @@ export class SshTui {
|
|
|
5799
6249
|
const y = Number(sgrMouse[3]);
|
|
5800
6250
|
if (sgrMouse[4] === 'M') {
|
|
5801
6251
|
if (button === 64) {
|
|
5802
|
-
this.
|
|
5803
|
-
this.markDirty();
|
|
6252
|
+
this.scrollInspectOrTranscript(3);
|
|
5804
6253
|
return;
|
|
5805
6254
|
}
|
|
5806
6255
|
if (button === 65) {
|
|
5807
|
-
this.
|
|
5808
|
-
this.markDirty();
|
|
6256
|
+
this.scrollInspectOrTranscript(-3);
|
|
5809
6257
|
return;
|
|
5810
6258
|
}
|
|
5811
6259
|
if (button === 0) {
|
|
@@ -5816,13 +6264,11 @@ export class SshTui {
|
|
|
5816
6264
|
return;
|
|
5817
6265
|
}
|
|
5818
6266
|
if (combined === '\x1b[5~') {
|
|
5819
|
-
this.
|
|
5820
|
-
this.markDirty();
|
|
6267
|
+
this.scrollInspectOrTranscript(Math.max(3, Math.floor((process.stdout.rows || 24) / 2)));
|
|
5821
6268
|
return;
|
|
5822
6269
|
}
|
|
5823
6270
|
if (combined === '\x1b[6~') {
|
|
5824
|
-
this.
|
|
5825
|
-
this.markDirty();
|
|
6271
|
+
this.scrollInspectOrTranscript(-Math.max(3, Math.floor((process.stdout.rows || 24) / 2)));
|
|
5826
6272
|
return;
|
|
5827
6273
|
}
|
|
5828
6274
|
if (parseCursorPositionReply(combined) !== undefined)
|
|
@@ -6089,6 +6535,12 @@ export class SshTui {
|
|
|
6089
6535
|
const dialog = this.dialog;
|
|
6090
6536
|
if (dialog === undefined)
|
|
6091
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
|
+
}
|
|
6092
6544
|
if (dialog.kind === 'onboarding') {
|
|
6093
6545
|
this.handleOnboardingChar(text);
|
|
6094
6546
|
return;
|
|
@@ -6184,7 +6636,7 @@ export class SshTui {
|
|
|
6184
6636
|
if (text === '\r' || text === '\n') {
|
|
6185
6637
|
const value = this.input.trim();
|
|
6186
6638
|
if (state.step === 'id') {
|
|
6187
|
-
const template =
|
|
6639
|
+
const template = providerTemplates()[state.providerType];
|
|
6188
6640
|
const id = value === '' ? template.defaultId : value;
|
|
6189
6641
|
if (!/^[a-z0-9][a-z0-9-]*$/u.test(id)) {
|
|
6190
6642
|
this.pushRow({ kind: 'error', text: 'Provider ID 只能包含小写字母、数字和连字符,且不能以连字符开头。' });
|
|
@@ -6202,7 +6654,7 @@ export class SshTui {
|
|
|
6202
6654
|
state.key = value;
|
|
6203
6655
|
}
|
|
6204
6656
|
else if (state.step === 'models') {
|
|
6205
|
-
const template =
|
|
6657
|
+
const template = providerTemplates()[state.providerType];
|
|
6206
6658
|
const parsed = value === ''
|
|
6207
6659
|
? template.defaultModels
|
|
6208
6660
|
: value.split(/[\s,,]+/u).filter(Boolean);
|
|
@@ -6281,7 +6733,7 @@ export class SshTui {
|
|
|
6281
6733
|
const state = this.onboarding;
|
|
6282
6734
|
if (state === undefined || state.step !== 'models')
|
|
6283
6735
|
return;
|
|
6284
|
-
const template =
|
|
6736
|
+
const template = providerTemplates()[state.providerType];
|
|
6285
6737
|
const providerType = state.providerType;
|
|
6286
6738
|
const baseUrl = state.baseUrl;
|
|
6287
6739
|
const key = state.key;
|
|
@@ -6339,7 +6791,7 @@ export class SshTui {
|
|
|
6339
6791
|
try {
|
|
6340
6792
|
const credentials = this.ctx.get('credentials');
|
|
6341
6793
|
const settings = this.ctx.get('settings');
|
|
6342
|
-
const template =
|
|
6794
|
+
const template = providerTemplates()[state.providerType];
|
|
6343
6795
|
if (state.providerType === 'official') {
|
|
6344
6796
|
const envRef = 'DEEPSEEK_API_KEY';
|
|
6345
6797
|
await this.saveCredential(credentials, envRef, state.key);
|
|
@@ -6577,6 +7029,10 @@ export class SshTui {
|
|
|
6577
7029
|
}
|
|
6578
7030
|
handleEscape() {
|
|
6579
7031
|
if (this.dialog !== undefined) {
|
|
7032
|
+
if (this.dialog.kind === 'inspect') {
|
|
7033
|
+
this.closeInspect();
|
|
7034
|
+
return;
|
|
7035
|
+
}
|
|
6580
7036
|
if (this.dialog.kind === 'confirm')
|
|
6581
7037
|
this.closeConfirm('cancel');
|
|
6582
7038
|
else if (this.dialog.kind === 'onboarding')
|
|
@@ -6612,11 +7068,22 @@ export class SshTui {
|
|
|
6612
7068
|
handleMouseClick(y) {
|
|
6613
7069
|
if (this.dialog !== undefined)
|
|
6614
7070
|
return;
|
|
7071
|
+
if (this.cwdChipRow !== undefined && y === this.cwdChipRow) {
|
|
7072
|
+
this.announceWorkspaceCwd();
|
|
7073
|
+
return;
|
|
7074
|
+
}
|
|
6615
7075
|
const row = this.clickableRows.get(y);
|
|
6616
7076
|
if (row === undefined)
|
|
6617
7077
|
return;
|
|
6618
|
-
this.
|
|
6619
|
-
|
|
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);
|
|
6620
7087
|
this.markDirty();
|
|
6621
7088
|
}
|
|
6622
7089
|
handleCtrlC() {
|
|
@@ -6683,7 +7150,7 @@ export class SshTui {
|
|
|
6683
7150
|
const arg = rest.join(' ');
|
|
6684
7151
|
switch (command) {
|
|
6685
7152
|
case 'help': {
|
|
6686
|
-
const local =
|
|
7153
|
+
const local = localizedCommands()
|
|
6687
7154
|
.filter(item => item.name !== 'help' && item.name !== 'exit')
|
|
6688
7155
|
.map(item => `/${item.name.padEnd(12)} ${item.description}`);
|
|
6689
7156
|
const dsh = (this.ctx.get('commands')?.list(this.agent) ?? [])
|
|
@@ -6694,13 +7161,13 @@ export class SshTui {
|
|
|
6694
7161
|
...local,
|
|
6695
7162
|
...dsh,
|
|
6696
7163
|
'',
|
|
6697
|
-
'
|
|
6698
|
-
'
|
|
6699
|
-
'
|
|
6700
|
-
'
|
|
6701
|
-
'
|
|
6702
|
-
'
|
|
6703
|
-
'
|
|
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'),
|
|
6704
7171
|
].join('\n'),
|
|
6705
7172
|
});
|
|
6706
7173
|
break;
|
|
@@ -6712,7 +7179,7 @@ export class SshTui {
|
|
|
6712
7179
|
case 'model':
|
|
6713
7180
|
void this.runModelCommand().catch((error) => {
|
|
6714
7181
|
if (error instanceof UserQuestionError) {
|
|
6715
|
-
this.pushRow({ kind: 'system', text: '
|
|
7182
|
+
this.pushRow({ kind: 'system', text: t('help.modelCancel') });
|
|
6716
7183
|
}
|
|
6717
7184
|
else {
|
|
6718
7185
|
this.pushRow({ kind: 'error', text: `/model failed: ${errorChain(error)}` });
|
|
@@ -6723,7 +7190,7 @@ export class SshTui {
|
|
|
6723
7190
|
case 'provider':
|
|
6724
7191
|
void this.runProviderCommand().catch((error) => {
|
|
6725
7192
|
if (error instanceof UserQuestionError) {
|
|
6726
|
-
this.pushRow({ kind: 'system', text: '
|
|
7193
|
+
this.pushRow({ kind: 'system', text: t('help.providerCancel') });
|
|
6727
7194
|
}
|
|
6728
7195
|
else {
|
|
6729
7196
|
this.pushRow({ kind: 'error', text: `/provider failed: ${errorChain(error)}` });
|
|
@@ -6734,7 +7201,7 @@ export class SshTui {
|
|
|
6734
7201
|
case 'submodel':
|
|
6735
7202
|
void this.runSubmodelCommand(arg).catch((error) => {
|
|
6736
7203
|
if (error instanceof UserQuestionError) {
|
|
6737
|
-
this.pushRow({ kind: 'system', text: '
|
|
7204
|
+
this.pushRow({ kind: 'system', text: t('help.submodelCancel') });
|
|
6738
7205
|
}
|
|
6739
7206
|
else {
|
|
6740
7207
|
this.pushRow({ kind: 'error', text: `/submodel failed: ${errorChain(error)}` });
|
|
@@ -6745,7 +7212,7 @@ export class SshTui {
|
|
|
6745
7212
|
case 'subeffort':
|
|
6746
7213
|
void this.runSubeffortCommand().catch((error) => {
|
|
6747
7214
|
if (error instanceof UserQuestionError) {
|
|
6748
|
-
this.pushRow({ kind: 'system', text: '
|
|
7215
|
+
this.pushRow({ kind: 'system', text: t('help.subeffortCancel') });
|
|
6749
7216
|
}
|
|
6750
7217
|
else {
|
|
6751
7218
|
this.pushRow({ kind: 'error', text: `/subeffort failed: ${errorChain(error)}` });
|
|
@@ -6756,7 +7223,7 @@ export class SshTui {
|
|
|
6756
7223
|
case 'mode':
|
|
6757
7224
|
void this.runModeCommand().catch((error) => {
|
|
6758
7225
|
if (error instanceof UserQuestionError) {
|
|
6759
|
-
this.pushRow({ kind: 'system', text: '
|
|
7226
|
+
this.pushRow({ kind: 'system', text: t('help.modeCancel') });
|
|
6760
7227
|
}
|
|
6761
7228
|
else {
|
|
6762
7229
|
this.pushRow({ kind: 'error', text: `/mode failed: ${errorChain(error)}` });
|
|
@@ -6764,6 +7231,18 @@ export class SshTui {
|
|
|
6764
7231
|
this.markDirty();
|
|
6765
7232
|
});
|
|
6766
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;
|
|
6767
7246
|
case 'find':
|
|
6768
7247
|
this.runFindCommand(arg);
|
|
6769
7248
|
break;
|
|
@@ -6778,28 +7257,37 @@ export class SshTui {
|
|
|
6778
7257
|
this.searchQuery = '';
|
|
6779
7258
|
this.planNudgePending = false;
|
|
6780
7259
|
this.pendingReveal = undefined;
|
|
6781
|
-
this.pushRow({ kind: 'system', text: '
|
|
7260
|
+
this.pushRow({ kind: 'system', text: t('clear.transcript') });
|
|
6782
7261
|
break;
|
|
6783
7262
|
case 'status':
|
|
6784
7263
|
{
|
|
6785
7264
|
const plan = this.findLivePlanRow();
|
|
6786
7265
|
const waiting = this.rows.filter(row => row.kind === 'question' && row.status === 'waiting').length;
|
|
6787
7266
|
const provider = this.currentProviderId();
|
|
6788
|
-
const route = describeProviderRoute(provider);
|
|
6789
7267
|
const model = this.selectionRef?.current?.model ?? this.agent.options.model ?? 'default';
|
|
6790
7268
|
const effort = this.selectionRef?.current?.reasoningEffort;
|
|
6791
|
-
const
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
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
|
+
});
|
|
6803
7291
|
this.pushRow({ kind: 'system', text: lines.join('\n') });
|
|
6804
7292
|
}
|
|
6805
7293
|
break;
|
|
@@ -6848,7 +7336,7 @@ export class SshTui {
|
|
|
6848
7336
|
const activity = card?.lastActivity ? ` · ${card.lastActivity}` : '';
|
|
6849
7337
|
return `▶ ${label} ${sub.id}(${sub.provider})运行 ${Math.floor((Date.now() - sub.startedAt) / 1000)}s [${runId.slice(0, 8)}]${activity}`;
|
|
6850
7338
|
});
|
|
6851
|
-
this.pushRow({ kind: 'system', text:
|
|
7339
|
+
this.pushRow({ kind: 'system', text: t('sub.listHint', { lines: lines.join('\n') }) });
|
|
6852
7340
|
}
|
|
6853
7341
|
break;
|
|
6854
7342
|
}
|