dsh-ssh-tui 0.3.8 → 0.3.10
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 +38 -23
- package/README.md +17 -10
- package/lib/i18n/en.js +433 -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 +433 -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 +1004 -365
- 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 +153 -7
- 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));
|
|
@@ -355,9 +384,10 @@ const PLUGIN_VERSION = (() => {
|
|
|
355
384
|
}
|
|
356
385
|
})();
|
|
357
386
|
const STALL_WARNING_MS = 60000;
|
|
387
|
+
const CTRL_C_EXIT_WINDOW_MS = 2000;
|
|
358
388
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
359
389
|
const QUESTION_OPTION_KEYS = '123456789abcdefghijklmnopqrstuvwxyz';
|
|
360
|
-
const SUBAGENT_DEFAULT_EFFORT_LABEL = '
|
|
390
|
+
const SUBAGENT_DEFAULT_EFFORT_LABEL = () => t('footer.effortDefault');
|
|
361
391
|
const RESERVED_BOTTOM_LINES = 3; // input line + stats line + status line
|
|
362
392
|
const MAX_TRANSCRIPT_ROWS = 5000;
|
|
363
393
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -487,20 +517,46 @@ const DEEPSEEK_LOGO_VARIANTS = [
|
|
|
487
517
|
],
|
|
488
518
|
},
|
|
489
519
|
];
|
|
520
|
+
/** Lines printed by `/status` — SSH first-boot diagnostics, no extra command. */
|
|
521
|
+
export function formatStatusReport(input) {
|
|
522
|
+
const route = describeProviderRoute(input.provider);
|
|
523
|
+
const effort = input.effort === undefined ? '' : ` (${input.effort})`;
|
|
524
|
+
const fit = describeSubagentFit({
|
|
525
|
+
parentProvider: input.provider,
|
|
526
|
+
parentModel: input.parentModel,
|
|
527
|
+
subProvider: input.subProvider,
|
|
528
|
+
subModel: input.subModel,
|
|
529
|
+
});
|
|
530
|
+
return [
|
|
531
|
+
`session: ${input.sessionId}`,
|
|
532
|
+
`plugin: dsh-ssh-tui ${input.pluginVersion}`,
|
|
533
|
+
`cwd: ${input.cwd ?? ''}`,
|
|
534
|
+
`route: ${input.provider}/${input.model}${effort}`,
|
|
535
|
+
`provider: ${route.kind}`,
|
|
536
|
+
`status: ${input.agentStatus}`,
|
|
537
|
+
`preset: ${input.preset}`,
|
|
538
|
+
`subagents: ${input.activeSubagents}`,
|
|
539
|
+
fit.line,
|
|
540
|
+
`plan: ${input.plan}`,
|
|
541
|
+
formatQuotaStatusLine(input.quota),
|
|
542
|
+
`paint: ${input.paint}`,
|
|
543
|
+
input.waitingQuestions > 0 ? `questions: waiting ${input.waitingQuestions}` : 'questions: none',
|
|
544
|
+
];
|
|
545
|
+
}
|
|
490
546
|
/** Human-facing kind for a live LLM route. */
|
|
491
547
|
export function describeProviderRoute(provider) {
|
|
492
548
|
const id = provider.trim();
|
|
493
549
|
if (id === 'deepseek-official' || id === 'deepseek') {
|
|
494
|
-
return { kind: '
|
|
550
|
+
return { kind: t('route.deepseek'), short: t('route.deepseek') };
|
|
495
551
|
}
|
|
496
552
|
if (id === 'xai' || id === 'grok' || id.startsWith('xai-')) {
|
|
497
|
-
return { kind: '
|
|
553
|
+
return { kind: t('route.supergrokKind'), short: t('route.supergrokShort') };
|
|
498
554
|
}
|
|
499
555
|
if (id === 'opencode-go')
|
|
500
|
-
return { kind: '
|
|
556
|
+
return { kind: t('route.go'), short: t('route.go') };
|
|
501
557
|
if (id === 'opencode')
|
|
502
|
-
return { kind: '
|
|
503
|
-
return { kind: '
|
|
558
|
+
return { kind: t('route.zen'), short: t('route.zen') };
|
|
559
|
+
return { kind: t('route.registered'), short: id };
|
|
504
560
|
}
|
|
505
561
|
/** Routes that authenticate without a harness API-key credential. */
|
|
506
562
|
export function providerUsesLocalOAuth(provider) {
|
|
@@ -517,15 +573,22 @@ const LOCAL_COMMANDS = [
|
|
|
517
573
|
{ name: 'quit', description: 'exit the TUI' },
|
|
518
574
|
{ name: 'exit', description: 'exit the TUI' },
|
|
519
575
|
{ name: 'clear', description: 'clear the transcript view' },
|
|
520
|
-
{ name: 'status', description: 'show session,
|
|
576
|
+
{ name: 'status', description: 'show session, route, quota window, subagent fit, paint, and plugin version' },
|
|
521
577
|
{ name: 'usage', description: 'show remaining quota or account balance for the current provider' },
|
|
522
578
|
{ name: 'balance', description: 'alias of /usage: DeepSeek / OpenAI-compatible balance, or subscription quota' },
|
|
523
579
|
{ name: 'subagents', description: 'list active subagents; kill <id> to stop one' },
|
|
524
580
|
{ name: 'resume', description: 'resume a past session (empty = session picker)' },
|
|
525
581
|
{ name: 'setup', description: 'add or update an API-key provider without wiping other saved routes' },
|
|
526
582
|
{ name: 'find', description: 'search thinking / plan / subagent / reply cards' },
|
|
583
|
+
{ name: 'language', description: 'switch UI language (zh / en); empty opens a picker' },
|
|
584
|
+
{ name: 'lang', description: 'alias of /language' },
|
|
527
585
|
{ name: 'dialog-test', description: 'verify the question dialog' },
|
|
528
586
|
];
|
|
587
|
+
function localizedCommands() {
|
|
588
|
+
return LOCAL_COMMANDS.map(command => (command.name === 'language' || command.name === 'lang'
|
|
589
|
+
? { name: command.name, description: t('lang.cmd') }
|
|
590
|
+
: command));
|
|
591
|
+
}
|
|
529
592
|
/**
|
|
530
593
|
* Terminal cell width for one string.
|
|
531
594
|
*
|
|
@@ -538,6 +601,67 @@ const LOCAL_COMMANDS = [
|
|
|
538
601
|
* Overflow into the input box is handled by clipping/padding painted rows to
|
|
539
602
|
* the measured column count, not by inflating glyph width.
|
|
540
603
|
*/
|
|
604
|
+
/**
|
|
605
|
+
* Codex-style compact elapsed: `0s`, `1m 05s`, `1h 01m 01s`.
|
|
606
|
+
* Used by the workspace wait card while the model has not streamed yet.
|
|
607
|
+
*/
|
|
608
|
+
export function fmtElapsedCompact(elapsedSecs) {
|
|
609
|
+
const secs = Math.max(0, Math.floor(elapsedSecs));
|
|
610
|
+
if (secs < 60)
|
|
611
|
+
return `${secs}s`;
|
|
612
|
+
if (secs < 3600) {
|
|
613
|
+
const minutes = Math.floor(secs / 60);
|
|
614
|
+
const seconds = secs % 60;
|
|
615
|
+
return `${minutes}m ${String(seconds).padStart(2, '0')}s`;
|
|
616
|
+
}
|
|
617
|
+
const hours = Math.floor(secs / 3600);
|
|
618
|
+
const minutes = Math.floor((secs % 3600) / 60);
|
|
619
|
+
const seconds = secs % 60;
|
|
620
|
+
return `${hours}h ${String(minutes).padStart(2, '0')}m ${String(seconds).padStart(2, '0')}s`;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Sweep highlight across `text` (Codex `shimmer.rs`). Truecolor blends a
|
|
624
|
+
* highlight band; otherwise DIM / default / BOLD. Process-start based so
|
|
625
|
+
* every paint of the same frame stays in phase.
|
|
626
|
+
*/
|
|
627
|
+
export function shimmerText(text, nowMs, color) {
|
|
628
|
+
const chars = Array.from(text);
|
|
629
|
+
if (chars.length === 0)
|
|
630
|
+
return '';
|
|
631
|
+
if (!color)
|
|
632
|
+
return text;
|
|
633
|
+
const padding = 10;
|
|
634
|
+
const period = chars.length + padding * 2;
|
|
635
|
+
const sweepMs = 2000;
|
|
636
|
+
const pos = Math.floor(((nowMs % sweepMs) / sweepMs) * period);
|
|
637
|
+
const bandHalf = 5;
|
|
638
|
+
let out = '';
|
|
639
|
+
for (let index = 0; index < chars.length; index += 1) {
|
|
640
|
+
const dist = Math.abs(index + padding - pos);
|
|
641
|
+
const t = dist <= bandHalf
|
|
642
|
+
? 0.5 * (1 + Math.cos(Math.PI * (dist / bandHalf)))
|
|
643
|
+
: 0;
|
|
644
|
+
const style = t < 0.2 ? '2' : t < 0.6 ? '0' : '1';
|
|
645
|
+
out += `\x1b[${style}m${chars[index]}\x1b[0m`;
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
/** One-line wait copy: current tool, else the user's last prompt. */
|
|
650
|
+
export function waitCardCopy(input) {
|
|
651
|
+
const toolTitle = input.toolTitle?.trim() ?? '';
|
|
652
|
+
const toolSummary = input.toolSummary?.trim() ?? '';
|
|
653
|
+
if (toolTitle !== '') {
|
|
654
|
+
return {
|
|
655
|
+
header: t('wait.working'),
|
|
656
|
+
detail: toolSummary === '' ? toolTitle : `${toolTitle} ${toolSummary}`,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
const prompt = (input.prompt ?? '').replace(/\s+/gu, ' ').trim();
|
|
660
|
+
if (prompt !== '') {
|
|
661
|
+
return { header: t('wait.working'), detail: prompt };
|
|
662
|
+
}
|
|
663
|
+
return { header: t('wait.working') };
|
|
664
|
+
}
|
|
541
665
|
export function displayWidth(text) {
|
|
542
666
|
let width = 0;
|
|
543
667
|
for (const char of text) {
|
|
@@ -744,6 +868,7 @@ function paintSegmentedLine(line, start, end, segments) {
|
|
|
744
868
|
if (segments.length === 0)
|
|
745
869
|
return line;
|
|
746
870
|
let out = '';
|
|
871
|
+
let cursor = start;
|
|
747
872
|
for (const seg of segments) {
|
|
748
873
|
if (seg.end <= start)
|
|
749
874
|
continue;
|
|
@@ -753,8 +878,14 @@ function paintSegmentedLine(line, start, end, segments) {
|
|
|
753
878
|
const to = Math.min(seg.end, end);
|
|
754
879
|
if (to <= from)
|
|
755
880
|
continue;
|
|
881
|
+
// Gaps (the tool title) stay default foreground — do not drop them.
|
|
882
|
+
if (from > cursor)
|
|
883
|
+
out += line.slice(cursor - start, from - start);
|
|
756
884
|
out += `\x1b[${seg.sgr}m${line.slice(from - start, to - start)}\x1b[0m`;
|
|
885
|
+
cursor = to;
|
|
757
886
|
}
|
|
887
|
+
if (cursor < end)
|
|
888
|
+
out += line.slice(cursor - start, end - start);
|
|
758
889
|
return out === '' ? line : out;
|
|
759
890
|
}
|
|
760
891
|
/** Wrap `text` and color each output line by overlapping `segments`. */
|
|
@@ -1098,16 +1229,29 @@ function backwardSliceByWidth(text, end, maxWidth) {
|
|
|
1098
1229
|
};
|
|
1099
1230
|
}
|
|
1100
1231
|
/**
|
|
1101
|
-
* Fold a long
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1232
|
+
* Fold a long input into one terminal row around the cursor.
|
|
1233
|
+
*
|
|
1234
|
+
* Newlines from a paste are display-only: they do not occupy cells, so a
|
|
1235
|
+
* naive `displayWidth(input)` under-counts a multi-line paste and parks the
|
|
1236
|
+
* caret in the middle of later text. Fold the *current line* (between the
|
|
1237
|
+
* surrounding newlines) and keep `\n` out of the visible slice.
|
|
1104
1238
|
*/
|
|
1105
1239
|
export function foldInputView(input, cursor, maxWidth) {
|
|
1106
1240
|
const width = Math.max(1, maxWidth);
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1241
|
+
const safeCursor = Math.max(0, Math.min(cursor, input.length));
|
|
1242
|
+
const lineStart = input.lastIndexOf('\n', Math.max(0, safeCursor - 1)) + 1;
|
|
1243
|
+
const lineEndRaw = input.indexOf('\n', safeCursor);
|
|
1244
|
+
const lineEnd = lineEndRaw === -1 ? input.length : lineEndRaw;
|
|
1245
|
+
const line = input.slice(lineStart, lineEnd);
|
|
1246
|
+
const lineCursor = safeCursor - lineStart;
|
|
1247
|
+
const totalWidth = displayWidth(line);
|
|
1248
|
+
const cursorOffset = displayWidth(line.slice(0, lineCursor));
|
|
1249
|
+
const hasMoreLines = lineStart > 0 || lineEnd < input.length;
|
|
1250
|
+
if (totalWidth <= width && !hasMoreLines) {
|
|
1251
|
+
return { text: line, cursorOffset, folded: false };
|
|
1252
|
+
}
|
|
1109
1253
|
if (totalWidth <= width) {
|
|
1110
|
-
return { text:
|
|
1254
|
+
return { text: line, cursorOffset, folded: true };
|
|
1111
1255
|
}
|
|
1112
1256
|
const before = cursorOffset;
|
|
1113
1257
|
const after = totalWidth - cursorOffset;
|
|
@@ -1120,9 +1264,9 @@ export function foldInputView(input, cursor, maxWidth) {
|
|
|
1120
1264
|
// If the tail is shorter than its budget, spend the spare columns on the
|
|
1121
1265
|
// side before the cursor so the cursor stays visible near its true offset.
|
|
1122
1266
|
beforeBudget = Math.min(before, beforeBudget + (available - beforeBudget - afterBudget));
|
|
1123
|
-
const beforeSlice = backwardSliceByWidth(
|
|
1124
|
-
const afterSlice = forwardSliceByWidth(
|
|
1125
|
-
const beforeText =
|
|
1267
|
+
const beforeSlice = backwardSliceByWidth(line, lineCursor, beforeBudget);
|
|
1268
|
+
const afterSlice = forwardSliceByWidth(line.slice(lineCursor), afterBudget);
|
|
1269
|
+
const beforeText = line.slice(beforeSlice.start, lineCursor);
|
|
1126
1270
|
return {
|
|
1127
1271
|
text: `${leftFolded ? '…' : ''}${beforeText}${afterSlice.text}${rightFolded ? '…' : ''}`,
|
|
1128
1272
|
cursorOffset: (leftFolded ? 1 : 0) + displayWidth(beforeText),
|
|
@@ -1285,7 +1429,12 @@ export function crossedQuotaThresholds(previousRemaining, remaining) {
|
|
|
1285
1429
|
}
|
|
1286
1430
|
export function quotaAlertText(snapshot, window) {
|
|
1287
1431
|
const reset = window.resetsAt === undefined ? '' : `(${formatQuotaReset(window.resetsAt)})`;
|
|
1288
|
-
return
|
|
1432
|
+
return t('quota.alert', {
|
|
1433
|
+
plan: snapshot.plan,
|
|
1434
|
+
period: quotaPeriodLabel(window.period),
|
|
1435
|
+
percent: window.remainingPercent.toFixed(0),
|
|
1436
|
+
reset,
|
|
1437
|
+
});
|
|
1289
1438
|
}
|
|
1290
1439
|
/**
|
|
1291
1440
|
* How often to re-fetch quota, based on the tightest window.
|
|
@@ -1311,12 +1460,12 @@ export function quotaRefreshEverySteps(window) {
|
|
|
1311
1460
|
export const quotaRefreshEveryTurns = quotaRefreshEverySteps;
|
|
1312
1461
|
function quotaPeriodLabel(period) {
|
|
1313
1462
|
if (period === 'hourly')
|
|
1314
|
-
return '
|
|
1463
|
+
return t('quota.periodHourly');
|
|
1315
1464
|
if (period === 'weekly')
|
|
1316
|
-
return '
|
|
1465
|
+
return t('quota.periodWeekly');
|
|
1317
1466
|
if (period === 'monthly')
|
|
1318
|
-
return '
|
|
1319
|
-
return '
|
|
1467
|
+
return t('quota.periodMonthly');
|
|
1468
|
+
return t('quota.periodUnknown');
|
|
1320
1469
|
}
|
|
1321
1470
|
function formatQuotaReset(iso) {
|
|
1322
1471
|
const reset = new Date(iso);
|
|
@@ -1399,6 +1548,20 @@ export function formatQuotaSnapshot(snapshot) {
|
|
|
1399
1548
|
}
|
|
1400
1549
|
return lines.join('\n');
|
|
1401
1550
|
}
|
|
1551
|
+
/** Compact `/status` quota line: tightest window first, then the rest. */
|
|
1552
|
+
export function formatQuotaStatusLine(snapshot) {
|
|
1553
|
+
if (snapshot === undefined || snapshot.windows.length === 0)
|
|
1554
|
+
return 'quota: none';
|
|
1555
|
+
const tightest = tightestQuotaWindow(snapshot);
|
|
1556
|
+
const ordered = tightest === undefined
|
|
1557
|
+
? snapshot.windows
|
|
1558
|
+
: [tightest, ...snapshot.windows.filter(window => window !== tightest)];
|
|
1559
|
+
const parts = ordered.map(window => {
|
|
1560
|
+
const remaining = Math.max(0, Math.min(100, window.remainingPercent));
|
|
1561
|
+
return `${window.label} ${remaining.toFixed(0)}%`;
|
|
1562
|
+
});
|
|
1563
|
+
return `quota: ${snapshot.plan} ${parts.join(' · ')}`;
|
|
1564
|
+
}
|
|
1402
1565
|
/** Tightest remaining window — used for threshold alerts. */
|
|
1403
1566
|
export function tightestQuotaWindow(snapshot) {
|
|
1404
1567
|
return snapshot.windows.reduce((best, window) => {
|
|
@@ -1674,24 +1837,22 @@ function friendlyArgsSummary(name, args) {
|
|
|
1674
1837
|
const SHELL_TOOL_NAMES = new Set(['bash', 'pwsh']);
|
|
1675
1838
|
const DIFF_TOOL_NAMES = new Set(['edit', 'write', 'str_replace_editor']);
|
|
1676
1839
|
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
|
-
};
|
|
1840
|
+
/**
|
|
1841
|
+
* Tool calls that already have a dedicated transcript card (goal/change,
|
|
1842
|
+
* plan dock, question dialog). Showing them again as raw `get_goal` cards
|
|
1843
|
+
* just duplicates chrome.
|
|
1844
|
+
*/
|
|
1845
|
+
const HIDDEN_TOOL_NAMES = new Set(['get_goal']);
|
|
1846
|
+
const TOOL_TITLE_KEYS = [
|
|
1847
|
+
'edit', 'write', 'str_replace_editor', 'fetch', 'list_files', 'list', 'ls',
|
|
1848
|
+
'find', 'search', 'delete', 'rm', 'rename', 'mv', 'mkdir', 'skills',
|
|
1849
|
+
'create_goal', 'update_goal', 'complete_goal', 'clear_goal', 'pause_goal',
|
|
1850
|
+
'resume_goal', 'todo_write', 'todo', 'compact', 'glob', 'grep', 'read',
|
|
1851
|
+
'web_search', 'web_fetch',
|
|
1852
|
+
];
|
|
1853
|
+
function toolTitle(name) {
|
|
1854
|
+
return t(`toolTitle.${name}`, undefined, name);
|
|
1855
|
+
}
|
|
1695
1856
|
const MAX_SUBAGENT_LOGS = 80;
|
|
1696
1857
|
const TODO_STATUS_MARK = {
|
|
1697
1858
|
pending: '○',
|
|
@@ -1746,19 +1907,15 @@ export function cardCategoryOf(row) {
|
|
|
1746
1907
|
return 'question';
|
|
1747
1908
|
if (row.kind === 'goal')
|
|
1748
1909
|
return 'goal';
|
|
1910
|
+
if (row.kind === 'prompt')
|
|
1911
|
+
return 'prompt';
|
|
1749
1912
|
if (row.kind === 'compaction')
|
|
1750
1913
|
return 'tool';
|
|
1751
1914
|
return undefined;
|
|
1752
1915
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
subagent: '子代理',
|
|
1757
|
-
reply: '回复',
|
|
1758
|
-
tool: '工具',
|
|
1759
|
-
question: '提问',
|
|
1760
|
-
goal: '目标',
|
|
1761
|
-
};
|
|
1916
|
+
function cardCategoryLabel(category) {
|
|
1917
|
+
return t(`card.${category}`);
|
|
1918
|
+
}
|
|
1762
1919
|
const SEARCHABLE_CATEGORIES = ['thinking', 'plan', 'subagent', 'reply'];
|
|
1763
1920
|
function parseCardCategoryToken(token) {
|
|
1764
1921
|
const id = token.trim().toLowerCase();
|
|
@@ -1776,6 +1933,8 @@ function parseCardCategoryToken(token) {
|
|
|
1776
1933
|
return 'question';
|
|
1777
1934
|
if (id === 'goal' || id === '目标')
|
|
1778
1935
|
return 'goal';
|
|
1936
|
+
if (id === 'prompt' || id === '提示词' || id === '注入')
|
|
1937
|
+
return 'prompt';
|
|
1779
1938
|
return undefined;
|
|
1780
1939
|
}
|
|
1781
1940
|
/** Split `/find thinking padAnsi` into an optional category and a query. */
|
|
@@ -1812,21 +1971,81 @@ function rowSearchHaystack(row) {
|
|
|
1812
1971
|
return `${row.objective} ${row.blockedReason ?? ''}`;
|
|
1813
1972
|
case 'compaction':
|
|
1814
1973
|
return `${row.summary ?? ''} ${row.error ?? ''}`;
|
|
1974
|
+
case 'prompt':
|
|
1975
|
+
return `${row.sources.join(' ')} ${row.text}`;
|
|
1815
1976
|
default:
|
|
1816
1977
|
return '';
|
|
1817
1978
|
}
|
|
1818
1979
|
}
|
|
1980
|
+
const PROMPT_SOURCE_PATTERNS = [
|
|
1981
|
+
{ id: 'AGENTS.MD', pattern: /\bAGENTS\.md\b/iu },
|
|
1982
|
+
{ id: 'CLAUDE.MD', pattern: /\bCLAUDE\.md\b/iu },
|
|
1983
|
+
{ id: 'GEMINI.MD', pattern: /\bGEMINI\.md\b/iu },
|
|
1984
|
+
{ id: 'CURSOR.MD', pattern: /\b(?:\.?cursor(?:\/rules)?|CURSOR\.md)\b/iu },
|
|
1985
|
+
{ id: 'COPILOT.MD', pattern: /\b(?:COPILOT\.md|\.github\/copilot-instructions)\b/iu },
|
|
1986
|
+
{ id: 'WINDSURF.MD', pattern: /\bWINDSURF\.md\b/iu },
|
|
1987
|
+
];
|
|
1988
|
+
const SYSTEM_PRESET_HINT = /you are an ai agent powered by deepseek harness|powered by DeepSeek Harness|harness identity|deployment persona|system prompt/iu;
|
|
1989
|
+
const SYSTEM_PRESET_LABEL = () => t('prompt.systemPreset');
|
|
1990
|
+
const CONTEXT_LABEL = () => t('prompt.context');
|
|
1991
|
+
/** Classify one injected prompt blob into display sources. */
|
|
1992
|
+
export function promptInjectionSources(text, plugin) {
|
|
1993
|
+
const found = [];
|
|
1994
|
+
const seen = new Set();
|
|
1995
|
+
const add = (id) => {
|
|
1996
|
+
if (seen.has(id))
|
|
1997
|
+
return;
|
|
1998
|
+
seen.add(id);
|
|
1999
|
+
found.push(id);
|
|
2000
|
+
};
|
|
2001
|
+
for (const { id, pattern } of PROMPT_SOURCE_PATTERNS) {
|
|
2002
|
+
if (pattern.test(text))
|
|
2003
|
+
add(id);
|
|
2004
|
+
}
|
|
2005
|
+
const fromTags = text.matchAll(/Additional instructions from:\s*([^\n<]+)/giu);
|
|
2006
|
+
for (const match of fromTags) {
|
|
2007
|
+
const raw = (match[1] ?? '').trim();
|
|
2008
|
+
const file = raw.split(/[\\/]/u).filter(Boolean).at(-1);
|
|
2009
|
+
if (file !== undefined && /\.md$/iu.test(file))
|
|
2010
|
+
add(file.toUpperCase());
|
|
2011
|
+
}
|
|
2012
|
+
const looksSystem = SYSTEM_PRESET_HINT.test(text)
|
|
2013
|
+
|| plugin === 'system-prompt'
|
|
2014
|
+
|| plugin === 'dsh-system-prompt';
|
|
2015
|
+
if (looksSystem)
|
|
2016
|
+
add(SYSTEM_PRESET_LABEL());
|
|
2017
|
+
if (found.length === 0)
|
|
2018
|
+
add(CONTEXT_LABEL());
|
|
2019
|
+
const systemIndex = found.indexOf(SYSTEM_PRESET_LABEL());
|
|
2020
|
+
if (systemIndex > 0) {
|
|
2021
|
+
found.splice(systemIndex, 1);
|
|
2022
|
+
found.unshift(SYSTEM_PRESET_LABEL());
|
|
2023
|
+
}
|
|
2024
|
+
return found;
|
|
2025
|
+
}
|
|
2026
|
+
export function promptInjectionTitle(sources) {
|
|
2027
|
+
return sources.length === 0 ? t('prompt.inject') : t('prompt.injectWith', { sources: sources.join(' ') });
|
|
2028
|
+
}
|
|
2029
|
+
export function isPromptInjectionMessage(sourceKind, text, plugin) {
|
|
2030
|
+
if (sourceKind === 'user')
|
|
2031
|
+
return false;
|
|
2032
|
+
if (sourceKind === 'plugin')
|
|
2033
|
+
return true;
|
|
2034
|
+
return /<system-reminder\b/iu.test(text)
|
|
2035
|
+
|| SYSTEM_PRESET_HINT.test(text)
|
|
2036
|
+
|| promptInjectionSources(text, plugin).some(id => id !== SYSTEM_PRESET_LABEL());
|
|
2037
|
+
}
|
|
1819
2038
|
export function compactionHeaderText(row) {
|
|
1820
2039
|
const recovered = row.prunedTokens > 0
|
|
1821
|
-
?
|
|
2040
|
+
? t('compact.recoverTokens', { tokens: formatTokens(row.prunedTokens) })
|
|
1822
2041
|
: row.pruneCount > 0
|
|
1823
|
-
?
|
|
1824
|
-
: '
|
|
2042
|
+
? t('compact.pruneChunks', { count: row.pruneCount })
|
|
2043
|
+
: t('compact.prepare');
|
|
1825
2044
|
if (row.status === 'running')
|
|
1826
|
-
return
|
|
2045
|
+
return t('compact.running', { detail: recovered });
|
|
1827
2046
|
if (row.status === 'error')
|
|
1828
|
-
return
|
|
1829
|
-
return
|
|
2047
|
+
return t('compact.failed', { error: row.error ?? t('quota.unknown') });
|
|
2048
|
+
return t('compact.done', { detail: recovered });
|
|
1830
2049
|
}
|
|
1831
2050
|
/** Transcript rows matching a `/find` query, newest last. */
|
|
1832
2051
|
export function matchTranscriptRows(rows, raw) {
|
|
@@ -1849,20 +2068,20 @@ export function planDockNote(plan) {
|
|
|
1849
2068
|
const allDone = plan.todos.length > 0 && plan.todos.every(item => item.status === 'completed');
|
|
1850
2069
|
const leftover = plan.todos.filter(item => item.status !== 'completed').length;
|
|
1851
2070
|
if (plan.turnLeftOpen === true && leftover > 0) {
|
|
1852
|
-
return
|
|
2071
|
+
return t('plan.leftOpen', { count: leftover });
|
|
1853
2072
|
}
|
|
1854
2073
|
if (plan.pending)
|
|
1855
|
-
return '
|
|
2074
|
+
return t('plan.pendingNext');
|
|
1856
2075
|
if (plan.active)
|
|
1857
|
-
return '
|
|
2076
|
+
return t('plan.planningOnly');
|
|
1858
2077
|
if (running)
|
|
1859
|
-
return '
|
|
2078
|
+
return t('plan.executing');
|
|
1860
2079
|
if (allDone)
|
|
1861
|
-
return '
|
|
2080
|
+
return t('plan.allDone');
|
|
1862
2081
|
if (plan.todos.length > 0 || (plan.planMarkdown !== undefined && plan.planMarkdown !== '')) {
|
|
1863
|
-
return '
|
|
2082
|
+
return t('plan.stillOpen');
|
|
1864
2083
|
}
|
|
1865
|
-
return '
|
|
2084
|
+
return t('plan.closed');
|
|
1866
2085
|
}
|
|
1867
2086
|
/** Compact per-status counts matching the web plan strip. */
|
|
1868
2087
|
export function todoProgressLabel(todos) {
|
|
@@ -1871,11 +2090,11 @@ export function todoProgressLabel(todos) {
|
|
|
1871
2090
|
const pending = todos.length - done - active;
|
|
1872
2091
|
const parts = [];
|
|
1873
2092
|
if (done > 0)
|
|
1874
|
-
parts.push(
|
|
2093
|
+
parts.push(t('plan.todoDone', { count: done }));
|
|
1875
2094
|
if (active > 0)
|
|
1876
|
-
parts.push(
|
|
2095
|
+
parts.push(t('plan.todoActive', { count: active }));
|
|
1877
2096
|
if (pending > 0)
|
|
1878
|
-
parts.push(
|
|
2097
|
+
parts.push(t('plan.todoPending', { count: pending }));
|
|
1879
2098
|
return parts.join(' · ');
|
|
1880
2099
|
}
|
|
1881
2100
|
function todoItemKind(status) {
|
|
@@ -2030,7 +2249,7 @@ export function presentToolCall(name, args) {
|
|
|
2030
2249
|
const diff = diffHunksFromArgs(name, args);
|
|
2031
2250
|
const path = diff?.[0]?.path;
|
|
2032
2251
|
return {
|
|
2033
|
-
title:
|
|
2252
|
+
title: toolTitle(name),
|
|
2034
2253
|
summary: path ?? friendlyArgsSummary(name, args),
|
|
2035
2254
|
...diff === null || diff === undefined ? {} : { diff },
|
|
2036
2255
|
};
|
|
@@ -2038,47 +2257,62 @@ export function presentToolCall(name, args) {
|
|
|
2038
2257
|
if (SUBAGENT_TOOL_NAMES.has(name)) {
|
|
2039
2258
|
const description = typeof parsed?.description === 'string' ? parsed.description.trim() : '';
|
|
2040
2259
|
return {
|
|
2041
|
-
title: name === 'subagent_fork' ? '
|
|
2260
|
+
title: toolTitle(name === 'subagent_fork' ? 'subagent_fork' : 'subagent'),
|
|
2042
2261
|
summary: description === '' ? friendlyArgsSummary(name, args) : description,
|
|
2043
2262
|
};
|
|
2044
2263
|
}
|
|
2045
2264
|
if (name === 'todo_write' || name === 'todo') {
|
|
2046
|
-
return { title: '
|
|
2265
|
+
return { title: toolTitle('todo_write'), summary: todoSummary(parsed) };
|
|
2047
2266
|
}
|
|
2048
2267
|
if (name === 'ask_user_question') {
|
|
2049
|
-
return { title: '
|
|
2268
|
+
return { title: toolTitle('ask_user_question'), summary: askSummary(parsed) };
|
|
2050
2269
|
}
|
|
2051
2270
|
if (name === 'exit_plan_mode') {
|
|
2052
2271
|
const plan = typeof parsed?.plan === 'string' ? parsed.plan : '';
|
|
2053
|
-
return { title: '
|
|
2272
|
+
return { title: toolTitle('exit_plan_mode'), summary: planTitleFromMarkdown(plan) ?? t('plan.waitConfirm') };
|
|
2273
|
+
}
|
|
2274
|
+
if (name === 'update_goal' || name === 'create_goal') {
|
|
2275
|
+
const action = typeof parsed?.action === 'string' ? parsed.action.trim() : '';
|
|
2276
|
+
const objective = typeof parsed?.objective === 'string' ? parsed.objective.trim() : '';
|
|
2277
|
+
const titleKey = name === 'create_goal' || action === 'create' || action === 'set'
|
|
2278
|
+
? 'create_goal'
|
|
2279
|
+
: action === 'pause' ? 'pause_goal'
|
|
2280
|
+
: action === 'resume' ? 'resume_goal'
|
|
2281
|
+
: action === 'clear' ? 'clear_goal'
|
|
2282
|
+
: action === 'complete' ? 'complete_goal'
|
|
2283
|
+
: 'update_goal';
|
|
2284
|
+
return { title: toolTitle(titleKey), summary: objective || action || friendlyArgsSummary(name, args) };
|
|
2285
|
+
}
|
|
2286
|
+
if (name === 'get_goal') {
|
|
2287
|
+
return { title: toolTitle('get_goal'), summary: friendlyArgsSummary(name, args) };
|
|
2054
2288
|
}
|
|
2055
2289
|
if (name === 'read') {
|
|
2056
2290
|
const path = typeof parsed?.path === 'string' ? parsed.path
|
|
2057
2291
|
: typeof parsed?.file_path === 'string' ? parsed.file_path
|
|
2058
2292
|
: typeof parsed?.url === 'string' ? parsed.url
|
|
2059
2293
|
: '';
|
|
2060
|
-
return { title: '
|
|
2294
|
+
return { title: toolTitle('read'), summary: path || friendlyArgsSummary(name, args) };
|
|
2061
2295
|
}
|
|
2062
2296
|
if (name === 'grep') {
|
|
2063
2297
|
const pattern = typeof parsed?.pattern === 'string' ? parsed.pattern : '';
|
|
2064
2298
|
const path = typeof parsed?.path === 'string' ? parsed.path : '';
|
|
2065
|
-
return { title: '
|
|
2299
|
+
return { title: toolTitle('grep'), summary: [pattern, path].filter(Boolean).join(' ') || friendlyArgsSummary(name, args) };
|
|
2066
2300
|
}
|
|
2067
2301
|
if (name === 'glob') {
|
|
2068
2302
|
const pattern = typeof parsed?.pattern === 'string' ? parsed.pattern
|
|
2069
2303
|
: typeof parsed?.glob_pattern === 'string' ? parsed.glob_pattern
|
|
2070
2304
|
: '';
|
|
2071
|
-
return { title: '
|
|
2305
|
+
return { title: toolTitle('glob'), summary: pattern || friendlyArgsSummary(name, args) };
|
|
2072
2306
|
}
|
|
2073
2307
|
if (name === 'web_search') {
|
|
2074
2308
|
const query = typeof parsed?.query === 'string' ? parsed.query : typeof parsed?.q === 'string' ? parsed.q : '';
|
|
2075
|
-
return { title: '
|
|
2309
|
+
return { title: toolTitle('web_search'), summary: query || friendlyArgsSummary(name, args) };
|
|
2076
2310
|
}
|
|
2077
2311
|
if (name === 'web_fetch') {
|
|
2078
2312
|
const url = typeof parsed?.url === 'string' ? parsed.url : '';
|
|
2079
|
-
return { title: '
|
|
2313
|
+
return { title: toolTitle('web_fetch'), summary: url || friendlyArgsSummary(name, args) };
|
|
2080
2314
|
}
|
|
2081
|
-
return { title:
|
|
2315
|
+
return { title: toolTitle(name), summary: friendlyArgsSummary(name, args) };
|
|
2082
2316
|
}
|
|
2083
2317
|
/** Validate a tool/result meta payload's structured diff, mirroring the web card. */
|
|
2084
2318
|
export function diffMetaDiffs(meta) {
|
|
@@ -2118,6 +2352,76 @@ function capDisplayLines(lines, maxLines) {
|
|
|
2118
2352
|
return [marker];
|
|
2119
2353
|
return [...lines.slice(0, budget - 2), marker, ...lines.slice(-1)];
|
|
2120
2354
|
}
|
|
2355
|
+
/** Running / ok / error → ANSI color for the status dot and status word only. */
|
|
2356
|
+
export function toolStateColor(status) {
|
|
2357
|
+
if (status === 'ok')
|
|
2358
|
+
return '32';
|
|
2359
|
+
if (status === 'error')
|
|
2360
|
+
return '31';
|
|
2361
|
+
return '33';
|
|
2362
|
+
}
|
|
2363
|
+
export function toolStateLabel(status) {
|
|
2364
|
+
if (status === 'ok')
|
|
2365
|
+
return 'ok';
|
|
2366
|
+
if (status === 'error')
|
|
2367
|
+
return 'error';
|
|
2368
|
+
return 'running…';
|
|
2369
|
+
}
|
|
2370
|
+
/** Header + SGR spans: default title, dim operand, colored ● and [ok]/[error]. */
|
|
2371
|
+
export function buildToolHeader(input) {
|
|
2372
|
+
const running = input.status === undefined || input.status === 'running';
|
|
2373
|
+
const state = toolStateLabel(input.status);
|
|
2374
|
+
const exit = !running && input.command !== undefined
|
|
2375
|
+
? input.signal !== undefined
|
|
2376
|
+
? ` [信号 ${input.signal}]`
|
|
2377
|
+
: (input.exitCode ?? 0) !== 0
|
|
2378
|
+
? ` [退出码 ${input.exitCode}]`
|
|
2379
|
+
: ''
|
|
2380
|
+
: '';
|
|
2381
|
+
const spinner = input.spinner ?? '';
|
|
2382
|
+
const prefix = input.focused ? '▶ ' : ' ';
|
|
2383
|
+
const marker = input.expanded ? '▾' : '▸';
|
|
2384
|
+
const lead = `${prefix}${marker} ● ${input.title}`;
|
|
2385
|
+
const summaryText = input.summary === '' ? '' : ` ${input.summary}`;
|
|
2386
|
+
const stateToken = `[${state}]`;
|
|
2387
|
+
const tail = ` ${stateToken}${exit}${spinner}`;
|
|
2388
|
+
const plain = `${lead}${summaryText}${tail}`;
|
|
2389
|
+
const stateCode = toolStateColor(input.status);
|
|
2390
|
+
const dotIndex = lead.indexOf('●');
|
|
2391
|
+
const stateIndex = lead.length + summaryText.length + 2;
|
|
2392
|
+
const segments = [];
|
|
2393
|
+
if (dotIndex >= 0)
|
|
2394
|
+
segments.push({ start: dotIndex, end: dotIndex + '●'.length, sgr: stateCode });
|
|
2395
|
+
if (summaryText.length > 0) {
|
|
2396
|
+
segments.push({ start: lead.length, end: lead.length + summaryText.length, sgr: '90' });
|
|
2397
|
+
}
|
|
2398
|
+
segments.push({ start: stateIndex, end: stateIndex + stateToken.length + exit.length, sgr: stateCode });
|
|
2399
|
+
if (spinner !== '') {
|
|
2400
|
+
segments.push({
|
|
2401
|
+
start: stateIndex + stateToken.length + exit.length,
|
|
2402
|
+
end: plain.length,
|
|
2403
|
+
sgr: '90',
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
return { plain, segments: segments.filter(segment => segment.end > segment.start) };
|
|
2407
|
+
}
|
|
2408
|
+
/** How many terminal rows a tool body occupies after wrapping. */
|
|
2409
|
+
export function wrappedToolBodyLineCount(lines, width) {
|
|
2410
|
+
const inner = Math.max(1, width - 2);
|
|
2411
|
+
let count = 0;
|
|
2412
|
+
for (const line of lines) {
|
|
2413
|
+
count += Math.max(1, wrap(line.text, inner).length);
|
|
2414
|
+
}
|
|
2415
|
+
return count;
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* True when the full tool body plus a one-line header fits in the workspace
|
|
2419
|
+
* (the rows between the title bar and the input chrome). Oversized bodies
|
|
2420
|
+
* open a dedicated inspect overlay instead of dumping into the transcript.
|
|
2421
|
+
*/
|
|
2422
|
+
export function toolBodyFitsWorkspace(bodyLines, workspaceRows) {
|
|
2423
|
+
return bodyLines + 1 <= Math.max(1, workspaceRows);
|
|
2424
|
+
}
|
|
2121
2425
|
/** Flatten hunks into git-style `-`/`+` lines plus the web-compatible footer. */
|
|
2122
2426
|
export function renderToolDiff(diffs, maxLines) {
|
|
2123
2427
|
const rows = [];
|
|
@@ -2239,17 +2543,19 @@ function parseJsonBody(text) {
|
|
|
2239
2543
|
* converted into readable indented content instead of raw JSON text.
|
|
2240
2544
|
*/
|
|
2241
2545
|
export function toolBodyLines(row, maxLines) {
|
|
2546
|
+
const unlimited = !Number.isFinite(maxLines) || maxLines >= Number.MAX_SAFE_INTEGER;
|
|
2242
2547
|
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);
|
|
2548
|
+
// File-edit diffs are never truncated in the card: omitting hunks would
|
|
2549
|
+
// hide the exact code change the model applied. `maxLines` only governs
|
|
2550
|
+
// shell and generic JSON output bodies (and the inspect overlay).
|
|
2551
|
+
return renderToolDiff(row.diff, unlimited ? Number.MAX_SAFE_INTEGER : maxLines);
|
|
2247
2552
|
}
|
|
2248
2553
|
if (row.command !== undefined) {
|
|
2249
2554
|
const out = [];
|
|
2250
2555
|
if (row.output !== '') {
|
|
2251
|
-
|
|
2252
|
-
|
|
2556
|
+
const text = unlimited ? row.output : truncate(row.output, maxLines);
|
|
2557
|
+
for (const line of text.split('\n')) {
|
|
2558
|
+
out.push({ kind: 'tool-result', text: line });
|
|
2253
2559
|
}
|
|
2254
2560
|
}
|
|
2255
2561
|
else if (row.status !== 'running' && row.status !== undefined) {
|
|
@@ -2257,9 +2563,10 @@ export function toolBodyLines(row, maxLines) {
|
|
|
2257
2563
|
}
|
|
2258
2564
|
return out;
|
|
2259
2565
|
}
|
|
2260
|
-
const specialized = specializedToolBody(row);
|
|
2261
|
-
if (specialized !== null)
|
|
2262
|
-
return capDisplayLines(specialized, maxLines);
|
|
2566
|
+
const specialized = specializedToolBody(row, unlimited ? Number.MAX_SAFE_INTEGER : maxLines);
|
|
2567
|
+
if (specialized !== null) {
|
|
2568
|
+
return unlimited ? specialized : capDisplayLines(specialized, maxLines);
|
|
2569
|
+
}
|
|
2263
2570
|
const out = [];
|
|
2264
2571
|
const args = parseJsonArgs(row.args);
|
|
2265
2572
|
if (args !== null && Object.keys(args).length > 0) {
|
|
@@ -2277,12 +2584,13 @@ export function toolBodyLines(row, maxLines) {
|
|
|
2277
2584
|
}
|
|
2278
2585
|
}
|
|
2279
2586
|
else {
|
|
2280
|
-
|
|
2587
|
+
const text = unlimited ? row.output : truncate(row.output, maxLines);
|
|
2588
|
+
for (const line of text.split('\n')) {
|
|
2281
2589
|
out.push({ kind: 'tool-result', text: line });
|
|
2282
2590
|
}
|
|
2283
2591
|
}
|
|
2284
2592
|
}
|
|
2285
|
-
return capDisplayLines(out, maxLines);
|
|
2593
|
+
return unlimited ? out : capDisplayLines(out, maxLines);
|
|
2286
2594
|
}
|
|
2287
2595
|
function firstString(record, keys) {
|
|
2288
2596
|
for (const key of keys) {
|
|
@@ -2292,9 +2600,11 @@ function firstString(record, keys) {
|
|
|
2292
2600
|
}
|
|
2293
2601
|
return '';
|
|
2294
2602
|
}
|
|
2295
|
-
function specializedToolBody(row) {
|
|
2603
|
+
function specializedToolBody(row, maxLines = Number.MAX_SAFE_INTEGER) {
|
|
2296
2604
|
const name = row.name ?? '';
|
|
2297
2605
|
const args = parseJsonArgs(row.args);
|
|
2606
|
+
const unlimited = !Number.isFinite(maxLines) || maxLines >= Number.MAX_SAFE_INTEGER;
|
|
2607
|
+
const take = (text, fallback) => unlimited ? text : truncate(text, Math.min(maxLines, fallback));
|
|
2298
2608
|
if (name === 'todo_write' || name === 'todo') {
|
|
2299
2609
|
const todos = parsePlanTodos(args ?? row.args);
|
|
2300
2610
|
const out = [{ kind: 'diff-path', text: todoProgressLabel(todos) || '待办列表' }];
|
|
@@ -2332,7 +2642,7 @@ function specializedToolBody(row) {
|
|
|
2332
2642
|
out.push({ kind: 'tool-result', text: `offset ${offset ?? 1}${limit === undefined ? '' : ` · limit ${limit}`}` });
|
|
2333
2643
|
}
|
|
2334
2644
|
if (row.output !== '') {
|
|
2335
|
-
for (const line of
|
|
2645
|
+
for (const line of take(row.output, 40).split('\n')) {
|
|
2336
2646
|
out.push({ kind: 'tool-result', text: line });
|
|
2337
2647
|
}
|
|
2338
2648
|
}
|
|
@@ -2346,7 +2656,7 @@ function specializedToolBody(row) {
|
|
|
2346
2656
|
const path = firstString(args, ['path', 'glob']);
|
|
2347
2657
|
const out = [{ kind: 'diff-path', text: [pattern, path].filter(Boolean).join(' ') || name }];
|
|
2348
2658
|
if (row.output !== '') {
|
|
2349
|
-
for (const line of
|
|
2659
|
+
for (const line of take(row.output, 30).split('\n')) {
|
|
2350
2660
|
out.push({ kind: 'tool-result', text: line });
|
|
2351
2661
|
}
|
|
2352
2662
|
}
|
|
@@ -2356,12 +2666,27 @@ function specializedToolBody(row) {
|
|
|
2356
2666
|
const query = firstString(args, ['query', 'q', 'url']);
|
|
2357
2667
|
const out = [{ kind: 'diff-path', text: query || name }];
|
|
2358
2668
|
if (row.output !== '') {
|
|
2359
|
-
for (const line of
|
|
2669
|
+
for (const line of take(row.output, 24).split('\n')) {
|
|
2360
2670
|
out.push({ kind: 'assistant', text: line });
|
|
2361
2671
|
}
|
|
2362
2672
|
}
|
|
2363
2673
|
return out;
|
|
2364
2674
|
}
|
|
2675
|
+
if (name === 'update_goal' || name === 'create_goal' || name === 'get_goal') {
|
|
2676
|
+
const objective = args === null ? '' : firstString(args, ['objective', 'goal']);
|
|
2677
|
+
const action = args === null ? '' : firstString(args, ['action']);
|
|
2678
|
+
const out = [];
|
|
2679
|
+
if (action !== '')
|
|
2680
|
+
out.push({ kind: 'diff-path', text: action });
|
|
2681
|
+
if (objective !== '')
|
|
2682
|
+
out.push({ kind: 'assistant', text: objective });
|
|
2683
|
+
if (row.output !== '') {
|
|
2684
|
+
for (const line of take(row.output, 12).split('\n')) {
|
|
2685
|
+
out.push({ kind: 'tool-result', text: line });
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
return out.length > 0 ? out : null;
|
|
2689
|
+
}
|
|
2365
2690
|
return null;
|
|
2366
2691
|
}
|
|
2367
2692
|
/** Recover the shell tools' exit marker, mirroring @deepseek-ai/dsh-shell/render. */
|
|
@@ -2421,6 +2746,7 @@ export class SshTui {
|
|
|
2421
2746
|
focusedRow = null;
|
|
2422
2747
|
pendingMessages = new Map();
|
|
2423
2748
|
lastActivity = Date.now();
|
|
2749
|
+
lastIdleCtrlCAt = 0;
|
|
2424
2750
|
stalledWarningShown = false;
|
|
2425
2751
|
lastPaintAt = 0;
|
|
2426
2752
|
commandAbort;
|
|
@@ -2448,6 +2774,8 @@ export class SshTui {
|
|
|
2448
2774
|
escapeBuffer = '';
|
|
2449
2775
|
escapeTimer;
|
|
2450
2776
|
thinkingStartedAt;
|
|
2777
|
+
waitStartedAt;
|
|
2778
|
+
waitPrompt;
|
|
2451
2779
|
completionSignaled = false;
|
|
2452
2780
|
replaying = false;
|
|
2453
2781
|
completedAt = 0;
|
|
@@ -2458,6 +2786,10 @@ export class SshTui {
|
|
|
2458
2786
|
lastPaintHeight = 0;
|
|
2459
2787
|
lastChromeStart = 0;
|
|
2460
2788
|
lastTranscriptStart = -1;
|
|
2789
|
+
/** 1-based screen row of the footer `目录:` chip, when painted. */
|
|
2790
|
+
cwdChipRow;
|
|
2791
|
+
/** Set when a card expand/collapse moves chrome; next paint full-redraws. */
|
|
2792
|
+
forceFullPaint = false;
|
|
2461
2793
|
paintIntervalMs;
|
|
2462
2794
|
paintLink = 'local';
|
|
2463
2795
|
paintProbed = false;
|
|
@@ -2499,7 +2831,10 @@ export class SshTui {
|
|
|
2499
2831
|
});
|
|
2500
2832
|
this.pushRow({ kind: 'brand-logo' });
|
|
2501
2833
|
this.pushRow({ kind: 'system', text: 'DeepSeek Harness — SSH TUI' });
|
|
2502
|
-
this.pushRow({ kind: 'system', text: '
|
|
2834
|
+
this.pushRow({ kind: 'system', text: t('boot.help') });
|
|
2835
|
+
if (config.cwdNotice !== undefined && config.cwdNotice !== '') {
|
|
2836
|
+
this.pushRow({ kind: /进入|Entered/u.test(config.cwdNotice) ? 'system' : 'error', text: config.cwdNotice });
|
|
2837
|
+
}
|
|
2503
2838
|
}
|
|
2504
2839
|
/** Enter raw mode, switch to the alternate screen, and start listening. */
|
|
2505
2840
|
start() {
|
|
@@ -2561,6 +2896,7 @@ export class SshTui {
|
|
|
2561
2896
|
this.updateTerminalTitle();
|
|
2562
2897
|
const animating = (this.streaming !== undefined && this.streaming.reasoning !== '')
|
|
2563
2898
|
|| this.activeSubagents.size > 0
|
|
2899
|
+
|| this.waitCardVisible()
|
|
2564
2900
|
|| this.rows.some(row => (row.kind === 'question' && row.status === 'waiting')
|
|
2565
2901
|
|| (row.kind === 'plan' && (row.active || row.pending || row.todos.some(item => item.status === 'in_progress')))
|
|
2566
2902
|
|| (row.kind === 'goal' && (row.phase === 'active' || row.phase === 'blocked'))
|
|
@@ -2830,7 +3166,8 @@ export class SshTui {
|
|
|
2830
3166
|
|| row.kind === 'plan'
|
|
2831
3167
|
|| row.kind === 'question'
|
|
2832
3168
|
|| row.kind === 'goal'
|
|
2833
|
-
|| row.kind === 'compaction'
|
|
3169
|
+
|| row.kind === 'compaction'
|
|
3170
|
+
|| row.kind === 'prompt');
|
|
2834
3171
|
if (this.streaming !== undefined && this.streaming.reasoning !== '') {
|
|
2835
3172
|
this.streamingReasoning ??= { kind: 'streaming-reasoning', expanded: false };
|
|
2836
3173
|
rows.push(this.streamingReasoning);
|
|
@@ -2840,6 +3177,43 @@ export class SshTui {
|
|
|
2840
3177
|
spinnerFrame(periodMs = 120) {
|
|
2841
3178
|
return SPINNER[Math.floor(Date.now() / periodMs) % SPINNER.length] ?? '⠋';
|
|
2842
3179
|
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Codex wait card: shown while the turn is running and there is no live
|
|
3182
|
+
* thinking/reply stream. A live tool keeps the card so the detail line can
|
|
3183
|
+
* name what is happening (Codex `update_details`).
|
|
3184
|
+
*/
|
|
3185
|
+
waitCardVisible() {
|
|
3186
|
+
if (this.agent.status !== 'running')
|
|
3187
|
+
return false;
|
|
3188
|
+
if (this.streaming !== undefined && (this.streaming.reasoning !== '' || this.streaming.text !== '')) {
|
|
3189
|
+
return false;
|
|
3190
|
+
}
|
|
3191
|
+
if (this.rows.some(row => row.kind === 'compaction' && row.status === 'running'))
|
|
3192
|
+
return false;
|
|
3193
|
+
if (this.dialog?.kind === 'questions' || this.dialog?.kind === 'confirm')
|
|
3194
|
+
return false;
|
|
3195
|
+
return true;
|
|
3196
|
+
}
|
|
3197
|
+
beginWait(prompt) {
|
|
3198
|
+
this.waitStartedAt = Date.now();
|
|
3199
|
+
const trimmed = prompt?.replace(/\s+/gu, ' ').trim();
|
|
3200
|
+
this.waitPrompt = trimmed === undefined || trimmed === '' ? this.waitPrompt : trimmed;
|
|
3201
|
+
}
|
|
3202
|
+
endWait() {
|
|
3203
|
+
this.waitStartedAt = undefined;
|
|
3204
|
+
this.waitPrompt = undefined;
|
|
3205
|
+
}
|
|
3206
|
+
waitCardSource() {
|
|
3207
|
+
const liveTool = this.rows.findLast((row) => row.kind === 'tool' && (row.status === undefined || row.status === 'running'));
|
|
3208
|
+
if (liveTool !== undefined) {
|
|
3209
|
+
return { toolTitle: liveTool.title, toolSummary: liveTool.summary, prompt: this.waitPrompt };
|
|
3210
|
+
}
|
|
3211
|
+
const liveSub = this.rows.findLast((row) => row.kind === 'subagent' && row.status === 'running');
|
|
3212
|
+
if (liveSub !== undefined) {
|
|
3213
|
+
return { toolTitle: liveSub.label, toolSummary: liveSub.lastActivity, prompt: this.waitPrompt };
|
|
3214
|
+
}
|
|
3215
|
+
return { prompt: this.waitPrompt };
|
|
3216
|
+
}
|
|
2843
3217
|
findSubagentRow(sessionId) {
|
|
2844
3218
|
return this.rows.findLast((row) => row.kind === 'subagent' && row.sessionId === sessionId);
|
|
2845
3219
|
}
|
|
@@ -2943,7 +3317,7 @@ export class SshTui {
|
|
|
2943
3317
|
const summary = title ?? (counts === '' ? '还没有任务' : counts);
|
|
2944
3318
|
const marker = plan.expanded ? '▾' : '▸';
|
|
2945
3319
|
const focused = this.focusedRow === plan ? '▶ ' : ' ';
|
|
2946
|
-
const header = `${focused}${marker} ${mode}${spinner} · ${summary}${plan.expanded || yieldBottom ? '' : '
|
|
3320
|
+
const header = `${focused}${marker} ${mode}${spinner} · ${summary}${plan.expanded || yieldBottom ? '' : t('card.expand')}`;
|
|
2947
3321
|
const lines = [this.styleLine('plan-dock', padToWidth(header, width))];
|
|
2948
3322
|
if (yieldBottom || !plan.expanded)
|
|
2949
3323
|
return lines;
|
|
@@ -2975,6 +3349,104 @@ export class SshTui {
|
|
|
2975
3349
|
}
|
|
2976
3350
|
return lines;
|
|
2977
3351
|
}
|
|
3352
|
+
paintToolBodyLine(addDisplay, row, line, width) {
|
|
3353
|
+
const inner = Math.max(1, width - 2);
|
|
3354
|
+
const fillRow = line.kind === 'diff-add' || line.kind === 'diff-del';
|
|
3355
|
+
for (const wrapped of wrap(line.text, inner)) {
|
|
3356
|
+
const body = fillRow ? padToWidth(` ${wrapped}`, width) : ` ${wrapped}`;
|
|
3357
|
+
const kind = line.kind;
|
|
3358
|
+
const style = kind === 'diff-add' || kind === 'diff-del' || kind === 'diff-path'
|
|
3359
|
+
? this.styleLine(kind, body)
|
|
3360
|
+
: kind === 'todo-done' || kind === 'todo-active' || kind === 'todo-pending'
|
|
3361
|
+
? this.styleLine(kind, body)
|
|
3362
|
+
: kind === 'error'
|
|
3363
|
+
? this.styleLine('error', body)
|
|
3364
|
+
: kind === 'assistant'
|
|
3365
|
+
? this.styleLine('assistant', body)
|
|
3366
|
+
: this.styleLine('tool-result', body);
|
|
3367
|
+
addDisplay(style, row);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
workspaceRowsFor(_width, height) {
|
|
3371
|
+
const header = 2;
|
|
3372
|
+
const chrome = RESERVED_BOTTOM_LINES + 1;
|
|
3373
|
+
return Math.max(1, height - header - chrome);
|
|
3374
|
+
}
|
|
3375
|
+
paintInspectOverlay(width, height) {
|
|
3376
|
+
const dialog = this.dialog;
|
|
3377
|
+
if (dialog === undefined || dialog.kind !== 'inspect')
|
|
3378
|
+
return;
|
|
3379
|
+
const header = this.styleLine('system', truncateToWidth(`工具全文 · ${dialog.title}`, width));
|
|
3380
|
+
const hint = this.styleLine('system', truncateToWidth('PgUp/PgDn/滚轮滚动 · Esc 返回会话', width));
|
|
3381
|
+
const divider = this.styleLine('system', repeatToWidth('─', width));
|
|
3382
|
+
const bodyBudget = Math.max(1, height - 4);
|
|
3383
|
+
const rendered = [];
|
|
3384
|
+
for (const line of dialog.lines) {
|
|
3385
|
+
const inner = Math.max(1, width - 2);
|
|
3386
|
+
const fillRow = line.kind === 'diff-add' || line.kind === 'diff-del';
|
|
3387
|
+
for (const wrapped of wrap(line.text, inner)) {
|
|
3388
|
+
const body = fillRow ? padToWidth(` ${wrapped}`, width) : ` ${wrapped}`;
|
|
3389
|
+
const kind = line.kind;
|
|
3390
|
+
rendered.push(kind === 'diff-add' || kind === 'diff-del' || kind === 'diff-path'
|
|
3391
|
+
? this.styleLine(kind, body)
|
|
3392
|
+
: kind === 'todo-done' || kind === 'todo-active' || kind === 'todo-pending'
|
|
3393
|
+
? this.styleLine(kind, body)
|
|
3394
|
+
: kind === 'error'
|
|
3395
|
+
? this.styleLine('error', body)
|
|
3396
|
+
: kind === 'assistant'
|
|
3397
|
+
? this.styleLine('assistant', body)
|
|
3398
|
+
: this.styleLine('tool-result', body));
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
const maxOffset = Math.max(0, rendered.length - bodyBudget);
|
|
3402
|
+
if (dialog.offset > maxOffset)
|
|
3403
|
+
dialog.offset = maxOffset;
|
|
3404
|
+
if (dialog.offset < 0)
|
|
3405
|
+
dialog.offset = 0;
|
|
3406
|
+
const slice = rendered.slice(dialog.offset, dialog.offset + bodyBudget);
|
|
3407
|
+
while (slice.length < bodyBudget)
|
|
3408
|
+
slice.push('');
|
|
3409
|
+
const pos = rendered.length === 0
|
|
3410
|
+
? '0/0'
|
|
3411
|
+
: `${dialog.offset + 1}–${Math.min(rendered.length, dialog.offset + bodyBudget)}/${rendered.length}`;
|
|
3412
|
+
const footer = this.styleLine('system', truncateToWidth(`全文 ${pos} · Esc 返回`, width));
|
|
3413
|
+
const paintRows = [header, divider, ...slice, hint, footer];
|
|
3414
|
+
this.write(composePaintOutput({
|
|
3415
|
+
width,
|
|
3416
|
+
height,
|
|
3417
|
+
paintRows,
|
|
3418
|
+
previousRows: this.lastPaintRows,
|
|
3419
|
+
sizeChanged: true,
|
|
3420
|
+
chromeChanged: true,
|
|
3421
|
+
chromeStart: 0,
|
|
3422
|
+
previousChromeStart: 0,
|
|
3423
|
+
cursorRow: height,
|
|
3424
|
+
cursorColumn: 1,
|
|
3425
|
+
}));
|
|
3426
|
+
this.lastPaintRows = paintRows.length > height ? paintRows.slice(0, height) : paintRows;
|
|
3427
|
+
this.lastChromeKey = `inspect:${dialog.offset}:${width}x${height}`;
|
|
3428
|
+
this.lastPaintWidth = width;
|
|
3429
|
+
this.lastPaintHeight = height;
|
|
3430
|
+
this.lastChromeStart = 0;
|
|
3431
|
+
this.lastTranscriptStart = -1;
|
|
3432
|
+
}
|
|
3433
|
+
openToolInspect(row) {
|
|
3434
|
+
const lines = toolBodyLines(row, Number.MAX_SAFE_INTEGER);
|
|
3435
|
+
this.openDialog({
|
|
3436
|
+
kind: 'inspect',
|
|
3437
|
+
title: `${row.title}${row.summary === '' ? '' : ` ${row.summary}`}`,
|
|
3438
|
+
lines,
|
|
3439
|
+
offset: 0,
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
closeInspect() {
|
|
3443
|
+
if (this.dialog?.kind !== 'inspect')
|
|
3444
|
+
return;
|
|
3445
|
+
this.dialog = undefined;
|
|
3446
|
+
this.forceFullPaint = true;
|
|
3447
|
+
this.markDirty();
|
|
3448
|
+
this.showNextDialog();
|
|
3449
|
+
}
|
|
2978
3450
|
paintCollapsibleHeader(addDisplay, row, kind, header, width, colorize) {
|
|
2979
3451
|
const focused = this.focusedRow === row;
|
|
2980
3452
|
const marker = row.expanded ? '▾' : '▸';
|
|
@@ -3021,8 +3493,23 @@ export class SshTui {
|
|
|
3021
3493
|
const target = focused ?? rows[rows.length - 1];
|
|
3022
3494
|
if (target === undefined)
|
|
3023
3495
|
return;
|
|
3496
|
+
this.toggleCard(target);
|
|
3497
|
+
}
|
|
3498
|
+
toggleCard(target) {
|
|
3499
|
+
if (target.kind === 'tool' && !target.expanded) {
|
|
3500
|
+
const width = Math.max(10, process.stdout.columns || 80);
|
|
3501
|
+
const height = Math.max(6, process.stdout.rows || 24);
|
|
3502
|
+
const body = toolBodyLines(target, Number.MAX_SAFE_INTEGER);
|
|
3503
|
+
const bodyRows = wrappedToolBodyLineCount(body, width);
|
|
3504
|
+
if (!toolBodyFitsWorkspace(bodyRows, this.workspaceRowsFor(width, height))) {
|
|
3505
|
+
this.focusedRow = target;
|
|
3506
|
+
this.openToolInspect(target);
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3024
3510
|
target.expanded = !target.expanded;
|
|
3025
3511
|
this.focusedRow = target;
|
|
3512
|
+
this.forceFullPaint = true;
|
|
3026
3513
|
this.markDirty();
|
|
3027
3514
|
}
|
|
3028
3515
|
/** Expand all collapsible blocks, or collapse them again when all are open. */
|
|
@@ -3031,9 +3518,26 @@ export class SshTui {
|
|
|
3031
3518
|
if (rows.length === 0)
|
|
3032
3519
|
return;
|
|
3033
3520
|
const allExpanded = rows.every(row => row.expanded);
|
|
3034
|
-
|
|
3035
|
-
row
|
|
3036
|
-
|
|
3521
|
+
if (allExpanded) {
|
|
3522
|
+
for (const row of rows)
|
|
3523
|
+
row.expanded = false;
|
|
3524
|
+
this.focusedRow = null;
|
|
3525
|
+
}
|
|
3526
|
+
else {
|
|
3527
|
+
const width = Math.max(10, process.stdout.columns || 80);
|
|
3528
|
+
const height = Math.max(6, process.stdout.rows || 24);
|
|
3529
|
+
const workspace = this.workspaceRowsFor(width, height);
|
|
3530
|
+
for (const row of rows) {
|
|
3531
|
+
if (row.kind === 'tool') {
|
|
3532
|
+
const bodyRows = wrappedToolBodyLineCount(toolBodyLines(row, Number.MAX_SAFE_INTEGER), width);
|
|
3533
|
+
if (!toolBodyFitsWorkspace(bodyRows, workspace))
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
row.expanded = true;
|
|
3537
|
+
}
|
|
3538
|
+
this.focusedRow = rows[rows.length - 1] ?? null;
|
|
3539
|
+
}
|
|
3540
|
+
this.forceFullPaint = true;
|
|
3037
3541
|
this.markDirty();
|
|
3038
3542
|
}
|
|
3039
3543
|
highlightSearchLine(line) {
|
|
@@ -3044,9 +3548,21 @@ export class SshTui {
|
|
|
3044
3548
|
revealRow(row) {
|
|
3045
3549
|
if (row === undefined)
|
|
3046
3550
|
return;
|
|
3551
|
+
if (row.kind === 'tool') {
|
|
3552
|
+
const width = Math.max(10, process.stdout.columns || 80);
|
|
3553
|
+
const height = Math.max(6, process.stdout.rows || 24);
|
|
3554
|
+
const body = toolBodyLines(row, Number.MAX_SAFE_INTEGER);
|
|
3555
|
+
const bodyRows = wrappedToolBodyLineCount(body, width);
|
|
3556
|
+
if (!toolBodyFitsWorkspace(bodyRows, this.workspaceRowsFor(width, height))) {
|
|
3557
|
+
this.focusedRow = row;
|
|
3558
|
+
this.openToolInspect(row);
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3047
3562
|
if (row.kind !== 'assistant' && 'expanded' in row) {
|
|
3048
3563
|
row.expanded = true;
|
|
3049
3564
|
this.focusedRow = row;
|
|
3565
|
+
this.forceFullPaint = true;
|
|
3050
3566
|
}
|
|
3051
3567
|
else {
|
|
3052
3568
|
this.focusedRow = null;
|
|
@@ -3063,18 +3579,18 @@ export class SshTui {
|
|
|
3063
3579
|
const live = this.findLivePlanRow();
|
|
3064
3580
|
if (live !== undefined) {
|
|
3065
3581
|
this.focusCard(live);
|
|
3066
|
-
this.pushRow({ kind: 'system', text:
|
|
3582
|
+
this.pushRow({ kind: 'system', text: t('jump.planDock', { category: cardCategoryLabel(category) }) });
|
|
3067
3583
|
this.revealRow(live);
|
|
3068
3584
|
return;
|
|
3069
3585
|
}
|
|
3070
3586
|
}
|
|
3071
3587
|
const target = this.rows.findLast(row => cardCategoryOf(row) === category);
|
|
3072
3588
|
if (target === undefined) {
|
|
3073
|
-
this.pushRow({ kind: 'system', text:
|
|
3589
|
+
this.pushRow({ kind: 'system', text: t('jump.missing', { category: cardCategoryLabel(category) }) });
|
|
3074
3590
|
this.markDirty();
|
|
3075
3591
|
return;
|
|
3076
3592
|
}
|
|
3077
|
-
this.pushRow({ kind: 'system', text:
|
|
3593
|
+
this.pushRow({ kind: 'system', text: t('jump.latest', { category: cardCategoryLabel(category) }) });
|
|
3078
3594
|
this.revealRow(target);
|
|
3079
3595
|
}
|
|
3080
3596
|
applySearchHits(query, hits) {
|
|
@@ -3088,7 +3604,7 @@ export class SshTui {
|
|
|
3088
3604
|
}
|
|
3089
3605
|
this.searchIndex = hits.length - 1;
|
|
3090
3606
|
const hit = hits[this.searchIndex];
|
|
3091
|
-
const where = hit === undefined ? '' :
|
|
3607
|
+
const where = hit === undefined ? '' : cardCategoryLabel(cardCategoryOf(hit) ?? 'reply');
|
|
3092
3608
|
this.pushRow({
|
|
3093
3609
|
kind: 'system',
|
|
3094
3610
|
text: `找到 ${hits.length} 条${query === '' ? '' : `「${query}」`} · 第 ${hits.length}/${hits.length} 条(${where})。Ctrl+G / Alt+N 下一条,Alt+P 上一条。`,
|
|
@@ -3097,7 +3613,7 @@ export class SshTui {
|
|
|
3097
3613
|
}
|
|
3098
3614
|
runFindCommand(arg) {
|
|
3099
3615
|
const parsed = parseFindQuery(arg);
|
|
3100
|
-
const label = parsed.category === undefined ? '' : `${
|
|
3616
|
+
const label = parsed.category === undefined ? '' : `${cardCategoryLabel(parsed.category)} `;
|
|
3101
3617
|
const hits = matchTranscriptRows(this.rows, arg);
|
|
3102
3618
|
this.applySearchHits(`${label}${parsed.query}`.trim(), hits);
|
|
3103
3619
|
}
|
|
@@ -3110,7 +3626,7 @@ export class SshTui {
|
|
|
3110
3626
|
const count = this.searchHits.length;
|
|
3111
3627
|
this.searchIndex = (this.searchIndex + delta + count) % count;
|
|
3112
3628
|
const hit = this.searchHits[this.searchIndex];
|
|
3113
|
-
const where = hit === undefined ? '' :
|
|
3629
|
+
const where = hit === undefined ? '' : cardCategoryLabel(cardCategoryOf(hit) ?? 'reply');
|
|
3114
3630
|
this.pushRow({
|
|
3115
3631
|
kind: 'system',
|
|
3116
3632
|
text: `搜索「${this.searchQuery}」· 第 ${this.searchIndex + 1}/${count} 条(${where})。`,
|
|
@@ -3122,6 +3638,10 @@ export class SshTui {
|
|
|
3122
3638
|
return;
|
|
3123
3639
|
const width = Math.max(10, process.stdout.columns || 80);
|
|
3124
3640
|
const height = Math.max(6, process.stdout.rows || 24);
|
|
3641
|
+
if (this.dialog?.kind === 'inspect') {
|
|
3642
|
+
this.paintInspectOverlay(width, height);
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3125
3645
|
const display = [];
|
|
3126
3646
|
const displayRefs = [];
|
|
3127
3647
|
const searchHit = this.searchHits[this.searchIndex];
|
|
@@ -3159,7 +3679,7 @@ export class SshTui {
|
|
|
3159
3679
|
const focused = this.focusedRow === row;
|
|
3160
3680
|
const marker = row.expanded ? '▾' : '▸';
|
|
3161
3681
|
const lines = row.text.split('\n').length;
|
|
3162
|
-
const header = `${marker} 已思考 · ${lines} 行${row.expanded ? '' : '
|
|
3682
|
+
const header = `${marker} 已思考 · ${lines} 行${row.expanded ? '' : t('card.expand')}`;
|
|
3163
3683
|
const line = `${focused ? '▶ ' : ' '}${header}`;
|
|
3164
3684
|
const styled = this.styleLine('reasoning', line);
|
|
3165
3685
|
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
@@ -3172,91 +3692,62 @@ export class SshTui {
|
|
|
3172
3692
|
}
|
|
3173
3693
|
if (row.kind === 'tool') {
|
|
3174
3694
|
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
3695
|
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
|
-
];
|
|
3696
|
+
const header = buildToolHeader({
|
|
3697
|
+
focused,
|
|
3698
|
+
expanded: row.expanded,
|
|
3699
|
+
title: toolTitle(row.name) || row.title,
|
|
3700
|
+
summary: row.summary,
|
|
3701
|
+
status: row.status,
|
|
3702
|
+
command: row.command,
|
|
3703
|
+
signal: row.signal,
|
|
3704
|
+
exitCode: row.exitCode,
|
|
3705
|
+
spinner: running ? ` ${this.spinnerFrame()}` : '',
|
|
3706
|
+
});
|
|
3707
|
+
const headerSegments = this.color ? header.segments : [];
|
|
3203
3708
|
if (!row.expanded) {
|
|
3204
|
-
const collapsed = truncateToWidth(
|
|
3205
|
-
const styled =
|
|
3709
|
+
const collapsed = truncateToWidth(header.plain, Math.max(1, width - 2));
|
|
3710
|
+
const styled = headerSegments.length === 0
|
|
3206
3711
|
? collapsed
|
|
3207
3712
|
: paintSegmentedLine(collapsed, 0, collapsed.length, headerSegments);
|
|
3208
3713
|
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
3209
3714
|
continue;
|
|
3210
3715
|
}
|
|
3211
3716
|
const expandedHeaderLines = headerSegments.length === 0
|
|
3212
|
-
? wrap(
|
|
3213
|
-
: wrapSegmented(
|
|
3717
|
+
? wrap(header.plain, width)
|
|
3718
|
+
: wrapSegmented(header.plain, Math.max(1, width), headerSegments);
|
|
3214
3719
|
for (const wrapped of expandedHeaderLines) {
|
|
3215
3720
|
addDisplay(wrapped, row);
|
|
3216
3721
|
}
|
|
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
|
-
}
|
|
3722
|
+
for (const line of toolBodyLines(row, Number.MAX_SAFE_INTEGER)) {
|
|
3723
|
+
this.paintToolBodyLine(addDisplay, row, line, width);
|
|
3234
3724
|
}
|
|
3235
3725
|
continue;
|
|
3236
3726
|
}
|
|
3237
3727
|
if (row.kind === 'subagent') {
|
|
3238
3728
|
const running = row.status === 'running';
|
|
3239
3729
|
const ok = row.status === 'ok';
|
|
3240
|
-
const
|
|
3730
|
+
const aborted = row.status === 'aborted';
|
|
3731
|
+
const dotColor = !this.color ? undefined : running ? '33' : ok ? '32' : aborted ? '33' : '31';
|
|
3241
3732
|
const styleHeader = (line) => {
|
|
3242
3733
|
const safe = sanitizeTerminalText(line);
|
|
3243
3734
|
if (!this.color)
|
|
3244
3735
|
return safe;
|
|
3245
3736
|
const dotIndex = safe.indexOf('●');
|
|
3246
3737
|
if (dotColor === undefined || dotIndex === -1)
|
|
3247
|
-
return
|
|
3248
|
-
return
|
|
3738
|
+
return safe;
|
|
3739
|
+
return `${safe.slice(0, dotIndex)}\x1b[${dotColor}m●\x1b[0m${safe.slice(dotIndex + 1)}`;
|
|
3249
3740
|
};
|
|
3250
3741
|
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3251
|
-
const header = `● ${subagentHeaderText(row)}${spinner}${row.expanded ? '' : '
|
|
3252
|
-
this.paintCollapsibleHeader(addDisplay, row, '
|
|
3742
|
+
const header = `● ${subagentHeaderText(row)}${spinner}${row.expanded ? '' : t('card.expand')}`;
|
|
3743
|
+
this.paintCollapsibleHeader(addDisplay, row, 'system', header, width, styleHeader);
|
|
3253
3744
|
if (row.expanded) {
|
|
3254
|
-
addDisplay(this.styleLine('
|
|
3745
|
+
addDisplay(this.styleLine('system', ` 会话 ${row.sessionId} · ${row.provider}${row.local ? '' : ' · 外部进程'}`), row);
|
|
3255
3746
|
if (row.stopReason !== undefined) {
|
|
3256
|
-
addDisplay(this.styleLine('
|
|
3747
|
+
addDisplay(this.styleLine('system', ` 结束原因:${row.stopReason}`), row);
|
|
3257
3748
|
}
|
|
3258
3749
|
if (row.logs.length === 0) {
|
|
3259
|
-
addDisplay(this.styleLine('
|
|
3750
|
+
addDisplay(this.styleLine('system', running ? ' 等待子代理输出…' : ' 没有可见输出'), row);
|
|
3260
3751
|
}
|
|
3261
3752
|
else {
|
|
3262
3753
|
for (const entry of row.logs) {
|
|
@@ -3264,7 +3755,7 @@ export class SshTui {
|
|
|
3264
3755
|
? 'assistant'
|
|
3265
3756
|
: entry.kind === 'result' && row.status === 'error'
|
|
3266
3757
|
? 'error'
|
|
3267
|
-
: '
|
|
3758
|
+
: 'system';
|
|
3268
3759
|
for (const wrapped of wrap(entry.text, Math.max(1, width - 2))) {
|
|
3269
3760
|
addDisplay(this.styleLine(kind, ` ${wrapped}`), row);
|
|
3270
3761
|
}
|
|
@@ -3279,7 +3770,7 @@ export class SshTui {
|
|
|
3279
3770
|
const counts = todoProgressLabel(row.todos);
|
|
3280
3771
|
const title = planTitleFromMarkdown(row.planMarkdown ?? '');
|
|
3281
3772
|
const summary = title ?? (counts === '' ? '已归档' : counts);
|
|
3282
|
-
const header = `计划 · ${summary}${row.expanded ? '' : '
|
|
3773
|
+
const header = `计划 · ${summary}${row.expanded ? '' : t('card.expand')}`;
|
|
3283
3774
|
this.paintCollapsibleHeader(addDisplay, row, 'plan-dock', header, width);
|
|
3284
3775
|
if (row.expanded) {
|
|
3285
3776
|
addDisplay(this.styleLine('plan-dock', ` ${planDockNote({ ...row, active: false, pending: false })}`), row);
|
|
@@ -3297,12 +3788,22 @@ export class SshTui {
|
|
|
3297
3788
|
}
|
|
3298
3789
|
continue;
|
|
3299
3790
|
}
|
|
3791
|
+
if (row.kind === 'prompt') {
|
|
3792
|
+
const header = `● ${promptInjectionTitle(row.sources)}${row.expanded ? '' : t('card.expand')}`;
|
|
3793
|
+
this.paintCollapsibleHeader(addDisplay, row, 'system', header, width);
|
|
3794
|
+
if (row.expanded) {
|
|
3795
|
+
for (const wrapped of wrap(row.text, Math.max(1, width - 2))) {
|
|
3796
|
+
addDisplay(this.styleLine('system', ` ${wrapped}`), row);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3300
3801
|
if (row.kind === 'question') {
|
|
3301
3802
|
const waiting = row.status === 'waiting';
|
|
3302
3803
|
const spinner = waiting ? ` ${this.spinnerFrame()}` : '';
|
|
3303
3804
|
const state = waiting ? '等待回答' : row.status === 'answered' ? '已回答' : '已取消';
|
|
3304
3805
|
const title = row.intent === 'plan-review' ? '计划待审' : '提问用户';
|
|
3305
|
-
const header = `● ${title}${spinner} · ${state} · ${row.summary}${row.expanded ? '' : '
|
|
3806
|
+
const header = `● ${title}${spinner} · ${state} · ${row.summary}${row.expanded ? '' : t('card.expand')}`;
|
|
3306
3807
|
this.paintCollapsibleHeader(addDisplay, row, waiting ? 'tool' : 'system', header, width);
|
|
3307
3808
|
if (row.expanded) {
|
|
3308
3809
|
if (row.header !== undefined)
|
|
@@ -3336,7 +3837,7 @@ export class SshTui {
|
|
|
3336
3837
|
: row.phase === 'blocked' ? '受阻'
|
|
3337
3838
|
: row.phase === 'complete' ? '已完成'
|
|
3338
3839
|
: '已清除';
|
|
3339
|
-
const header = `● 目标${spinner} · ${phase} · ${row.objective}${row.expanded ? '' : '
|
|
3840
|
+
const header = `● 目标${spinner} · ${phase} · ${row.objective}${row.expanded ? '' : t('card.expand')}`;
|
|
3340
3841
|
this.paintCollapsibleHeader(addDisplay, row, live ? 'tool' : 'system', header, width);
|
|
3341
3842
|
if (row.expanded) {
|
|
3342
3843
|
addDisplay(this.styleLine('system', ' 用 /goal 查看、暂停、恢复或清除当前目标。'), row);
|
|
@@ -3352,7 +3853,7 @@ export class SshTui {
|
|
|
3352
3853
|
const running = row.status === 'running';
|
|
3353
3854
|
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
3354
3855
|
const elapsed = Math.max(0, Math.floor(((row.endedAt ?? Date.now()) - row.startedAt) / 1000));
|
|
3355
|
-
const header = `● ${compactionHeaderText(row)}${spinner} · ${elapsed}s${row.expanded ? '' : '
|
|
3856
|
+
const header = `● ${compactionHeaderText(row)}${spinner} · ${elapsed}s${row.expanded ? '' : t('card.expand')}`;
|
|
3356
3857
|
this.paintCollapsibleHeader(addDisplay, row, running ? 'tool' : row.status === 'error' ? 'error' : 'system', header, width);
|
|
3357
3858
|
if (row.expanded) {
|
|
3358
3859
|
addDisplay(this.styleLine('system', running
|
|
@@ -3402,6 +3903,20 @@ export class SshTui {
|
|
|
3402
3903
|
}
|
|
3403
3904
|
}
|
|
3404
3905
|
}
|
|
3906
|
+
else if (this.waitCardVisible()) {
|
|
3907
|
+
const copy = waitCardCopy(this.waitCardSource());
|
|
3908
|
+
const started = this.waitStartedAt ?? Date.now();
|
|
3909
|
+
const elapsed = fmtElapsedCompact((Date.now() - started) / 1000);
|
|
3910
|
+
const hint = t('wait.interrupt', { elapsed });
|
|
3911
|
+
const spinner = this.spinnerFrame();
|
|
3912
|
+
const header = this.color
|
|
3913
|
+
? `${spinner} ${shimmerText(copy.header, Date.now(), true)} ${this.styleLine('system', hint)}`
|
|
3914
|
+
: `${spinner} ${copy.header} ${hint}`;
|
|
3915
|
+
addDisplay(header);
|
|
3916
|
+
if (copy.detail !== undefined && copy.detail !== '') {
|
|
3917
|
+
addDisplay(this.styleLine('system', ` └ ${copy.detail}`));
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3405
3920
|
const dialogLines = [];
|
|
3406
3921
|
const addDialog = (text) => {
|
|
3407
3922
|
for (const wrapped of wrap(text, Math.max(1, width))) {
|
|
@@ -3416,53 +3931,57 @@ export class SshTui {
|
|
|
3416
3931
|
else if (this.dialog.kind === 'onboarding') {
|
|
3417
3932
|
const ob = this.onboarding;
|
|
3418
3933
|
if (ob !== undefined) {
|
|
3419
|
-
const template =
|
|
3934
|
+
const template = providerTemplates()[ob.providerType];
|
|
3420
3935
|
const providerLabel = `${template.label}${template.defaultBaseUrl === '' ? '' : `(${template.defaultBaseUrl})`}`;
|
|
3421
3936
|
switch (ob.step) {
|
|
3422
3937
|
case 'provider':
|
|
3423
|
-
addDialog('
|
|
3424
|
-
addDialog('
|
|
3425
|
-
addDialog('
|
|
3426
|
-
addDialog('
|
|
3427
|
-
addDialog('
|
|
3428
|
-
addDialog('
|
|
3429
|
-
addDialog('
|
|
3938
|
+
addDialog(t('onboard.title'));
|
|
3939
|
+
addDialog(t('onboard.opt1'));
|
|
3940
|
+
addDialog(t('onboard.opt2'));
|
|
3941
|
+
addDialog(t('onboard.opt3'));
|
|
3942
|
+
addDialog(t('onboard.opt4'));
|
|
3943
|
+
addDialog(t('onboard.opt5'));
|
|
3944
|
+
addDialog(t('onboard.pickHint'));
|
|
3430
3945
|
break;
|
|
3431
3946
|
case 'id':
|
|
3432
|
-
addDialog(
|
|
3433
|
-
addDialog('
|
|
3434
|
-
addDialog(
|
|
3435
|
-
addDialog('
|
|
3947
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3948
|
+
addDialog(t('onboard.idPrompt'));
|
|
3949
|
+
addDialog(t('onboard.default', { value: template.defaultId }));
|
|
3950
|
+
addDialog(t('onboard.enterEsc'));
|
|
3436
3951
|
break;
|
|
3437
3952
|
case 'key':
|
|
3438
|
-
addDialog(
|
|
3439
|
-
addDialog('
|
|
3440
|
-
addDialog('
|
|
3953
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3954
|
+
addDialog(t('onboard.keyPrompt'));
|
|
3955
|
+
addDialog(t('onboard.enterEsc'));
|
|
3441
3956
|
break;
|
|
3442
3957
|
case 'base-url':
|
|
3443
|
-
addDialog(
|
|
3444
|
-
addDialog(
|
|
3445
|
-
addDialog('
|
|
3958
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3959
|
+
addDialog(t('onboard.basePrompt', { fallback: template.defaultBaseUrl || t('onboard.baseFallback') }));
|
|
3960
|
+
addDialog(t('onboard.enterEsc'));
|
|
3446
3961
|
break;
|
|
3447
3962
|
case 'models':
|
|
3448
|
-
addDialog(
|
|
3449
|
-
addDialog('
|
|
3963
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
3964
|
+
addDialog(t('onboard.modelsPrompt'));
|
|
3450
3965
|
addDialog(ob.models.length > 0
|
|
3451
|
-
?
|
|
3452
|
-
:
|
|
3966
|
+
? t('onboard.modelsFetched', { count: ob.models.length, list: formatModelList(ob.models, 6) })
|
|
3967
|
+
: t('onboard.default', { value: template.defaultModels.join(', ') }));
|
|
3453
3968
|
if (template.api !== undefined)
|
|
3454
|
-
addDialog('
|
|
3455
|
-
addDialog('
|
|
3969
|
+
addDialog(t('onboard.ctrlF'));
|
|
3970
|
+
addDialog(t('onboard.enterEsc'));
|
|
3456
3971
|
break;
|
|
3457
3972
|
case 'confirm':
|
|
3458
|
-
addDialog('
|
|
3459
|
-
addDialog(
|
|
3973
|
+
addDialog(t('onboard.confirmTitle'));
|
|
3974
|
+
addDialog(t('onboard.confirmProvider', { label: providerLabel }));
|
|
3460
3975
|
addDialog(` Provider ID: ${ob.providerId}`);
|
|
3461
|
-
addDialog(
|
|
3462
|
-
addDialog(
|
|
3463
|
-
addDialog(
|
|
3464
|
-
addDialog(
|
|
3465
|
-
|
|
3976
|
+
addDialog(t('onboard.confirmBase', { url: ob.baseUrl === '' ? (template.defaultBaseUrl || t('onboard.defaultParen')) : ob.baseUrl }));
|
|
3977
|
+
addDialog(t('onboard.confirmApi', { api: template.api ?? 'deepseek-official' }));
|
|
3978
|
+
addDialog(t('onboard.confirmModels', { list: formatModelList(ob.models, 8) }));
|
|
3979
|
+
addDialog(t('onboard.confirmKey', {
|
|
3980
|
+
head: sliceCodePoints(ob.key, 6),
|
|
3981
|
+
tail: lastCodePoints(ob.key, 4),
|
|
3982
|
+
length: ob.key.length,
|
|
3983
|
+
}));
|
|
3984
|
+
addDialog(t('onboard.confirmHint'));
|
|
3466
3985
|
break;
|
|
3467
3986
|
}
|
|
3468
3987
|
}
|
|
@@ -3526,35 +4045,24 @@ export class SshTui {
|
|
|
3526
4045
|
const prompt = this.color ? `\x1b[36m${promptPlain.trimEnd()}\x1b[0m ` : promptPlain;
|
|
3527
4046
|
const promptWidth = displayWidth(promptPlain);
|
|
3528
4047
|
const masked = this.dialog?.kind === 'onboarding' && this.onboarding?.step === 'key';
|
|
4048
|
+
const inputTextWidth = Math.max(1, width - promptWidth);
|
|
3529
4049
|
const inputView = masked
|
|
3530
4050
|
? { text: '•'.repeat(this.input.length), cursorOffset: displayWidth('•'.repeat(this.cursor)), folded: false }
|
|
3531
4051
|
: this.inputFolded
|
|
3532
|
-
? foldInputView(this.input, this.cursor,
|
|
4052
|
+
? foldInputView(this.input, this.cursor, inputTextWidth)
|
|
3533
4053
|
: { text: this.input, cursorOffset: displayWidth(this.input.slice(0, this.cursor)), folded: false };
|
|
3534
|
-
const inputTextWidth = Math.max(1, width - promptWidth);
|
|
3535
4054
|
const inputTextLines = wrap(inputView.text, inputTextWidth);
|
|
3536
4055
|
const inputDisplayLines = inputTextLines.map((line, index) => index === 0 ? `${prompt}${line}` : line);
|
|
3537
|
-
//
|
|
3538
|
-
// the
|
|
3539
|
-
//
|
|
4056
|
+
// Folded/masked views are one logical row around the caret. Place the
|
|
4057
|
+
// cursor with the prompt width of that row — never wrap the offset
|
|
4058
|
+
// across the full terminal grid, which parked the caret on a later
|
|
4059
|
+
// chrome line after a long paste. Un-folded multi-line input still
|
|
4060
|
+
// maps through wrap() so newlines stay on the right visual row.
|
|
3540
4061
|
let cursorRowOffset;
|
|
3541
4062
|
let column;
|
|
3542
4063
|
if (inputView.folded || masked) {
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
if (!inputView.folded
|
|
3546
|
-
&& cursorPlainOffset > 0
|
|
3547
|
-
&& cursorPlainOffset % grid === 0
|
|
3548
|
-
&& Math.floor(cursorPlainOffset / grid) >= inputDisplayLines.length) {
|
|
3549
|
-
inputDisplayLines.push('');
|
|
3550
|
-
}
|
|
3551
|
-
cursorRowOffset = Math.min(Math.floor(cursorPlainOffset / grid), Math.max(0, inputDisplayLines.length - 1));
|
|
3552
|
-
column = cursorPlainOffset % grid + 1;
|
|
3553
|
-
if (cursorPlainOffset > 0
|
|
3554
|
-
&& cursorPlainOffset % grid === 0
|
|
3555
|
-
&& Math.floor(cursorPlainOffset / grid) >= inputDisplayLines.length) {
|
|
3556
|
-
column = grid;
|
|
3557
|
-
}
|
|
4064
|
+
cursorRowOffset = 0;
|
|
4065
|
+
column = Math.min(width, promptWidth + inputView.cursorOffset + 1);
|
|
3558
4066
|
}
|
|
3559
4067
|
else {
|
|
3560
4068
|
const pos = cursorVisualPosition(inputView.text, this.cursor, inputTextWidth);
|
|
@@ -3674,6 +4182,7 @@ export class SshTui {
|
|
|
3674
4182
|
foldedInput: inputView.folded,
|
|
3675
4183
|
multiLineInput: inputRows > 1,
|
|
3676
4184
|
queued: this.pendingMessages.size,
|
|
4185
|
+
cwdLabel: formatFooterCwd(this.workspaceCwd()),
|
|
3677
4186
|
};
|
|
3678
4187
|
const activity = footerActivity(footer);
|
|
3679
4188
|
const activityText = activity.kind === 'compacting'
|
|
@@ -3681,7 +4190,8 @@ export class SshTui {
|
|
|
3681
4190
|
: activity.kind === 'subagents'
|
|
3682
4191
|
? `${this.spinnerFrame(160)} ${activity.text}`
|
|
3683
4192
|
: activity.text;
|
|
3684
|
-
const
|
|
4193
|
+
const identity = footerIdentityParts(footer);
|
|
4194
|
+
const statusText = fitFooterStatusLine(activityText, identity, Math.max(1, width));
|
|
3685
4195
|
const statusLine = this.styleLine('system', statusText);
|
|
3686
4196
|
const paintRows = [
|
|
3687
4197
|
...headerLines,
|
|
@@ -3720,7 +4230,11 @@ export class SshTui {
|
|
|
3720
4230
|
].join('\x1f');
|
|
3721
4231
|
const chromeChanged = chromeKey !== this.lastChromeKey || chromeStart !== this.lastChromeStart;
|
|
3722
4232
|
const transcriptScrolled = start !== this.lastTranscriptStart;
|
|
3723
|
-
const sizeChanged =
|
|
4233
|
+
const sizeChanged = this.forceFullPaint
|
|
4234
|
+
|| width !== this.lastPaintWidth
|
|
4235
|
+
|| height !== this.lastPaintHeight
|
|
4236
|
+
|| transcriptScrolled;
|
|
4237
|
+
this.forceFullPaint = false;
|
|
3724
4238
|
// One stdout write per frame: dirty rows only, so jump-host SSH sees a
|
|
3725
4239
|
// single packet instead of one write per line. Clip/pad so leftover
|
|
3726
4240
|
// wide glyphs cannot wrap into the input box.
|
|
@@ -3744,7 +4258,19 @@ export class SshTui {
|
|
|
3744
4258
|
this.lastPaintHeight = height;
|
|
3745
4259
|
this.lastChromeStart = chromeStart;
|
|
3746
4260
|
this.lastTranscriptStart = start;
|
|
4261
|
+
const cwdChip = formatFooterCwd(this.workspaceCwd());
|
|
4262
|
+
this.cwdChipRow = cwdChip !== '' && statusText.includes(cwdChip)
|
|
4263
|
+
? Math.min(height, paintRows.length)
|
|
4264
|
+
: undefined;
|
|
3747
4265
|
};
|
|
4266
|
+
workspaceCwd() {
|
|
4267
|
+
return this.agent.session.header?.cwd ?? process.cwd();
|
|
4268
|
+
}
|
|
4269
|
+
announceWorkspaceCwd() {
|
|
4270
|
+
const cwd = this.workspaceCwd();
|
|
4271
|
+
this.pushRow({ kind: 'system', text: t('cwd.full', { cwd }) });
|
|
4272
|
+
this.markDirty();
|
|
4273
|
+
}
|
|
3748
4274
|
buildSuggestions() {
|
|
3749
4275
|
const input = this.input;
|
|
3750
4276
|
if (!input.startsWith('/'))
|
|
@@ -3756,7 +4282,7 @@ export class SshTui {
|
|
|
3756
4282
|
local: false,
|
|
3757
4283
|
}));
|
|
3758
4284
|
const all = [
|
|
3759
|
-
...
|
|
4285
|
+
...localizedCommands().map(command => ({ name: command.name, description: command.description, local: true })),
|
|
3760
4286
|
...dsh,
|
|
3761
4287
|
];
|
|
3762
4288
|
const filtered = prefix === ''
|
|
@@ -3883,13 +4409,6 @@ export class SshTui {
|
|
|
3883
4409
|
this.dirty = false;
|
|
3884
4410
|
this.paint();
|
|
3885
4411
|
};
|
|
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
4412
|
styleLine(kind, text) {
|
|
3894
4413
|
const safe = sanitizeTerminalText(text);
|
|
3895
4414
|
if (!this.color)
|
|
@@ -3898,7 +4417,7 @@ export class SshTui {
|
|
|
3898
4417
|
kind === 'assistant' ? '1;37' :
|
|
3899
4418
|
kind === 'reasoning' ? '2;3' :
|
|
3900
4419
|
kind === 'brand' ? '1;38;2;77;107;253' :
|
|
3901
|
-
kind === 'tool' || kind === 'tool-result' ? '
|
|
4420
|
+
kind === 'tool' || kind === 'tool-result' ? '37' :
|
|
3902
4421
|
// Codex-like: muted add/del that blend into the terminal background.
|
|
3903
4422
|
kind === 'diff-add' ? '38;2;122;168;116;48;2;18;42;24' :
|
|
3904
4423
|
kind === 'diff-del' ? '38;2;196;122;122;48;2;48;20;20' :
|
|
@@ -3949,11 +4468,17 @@ export class SshTui {
|
|
|
3949
4468
|
.map(block => block.text)
|
|
3950
4469
|
.join('');
|
|
3951
4470
|
if (text !== '') {
|
|
3952
|
-
const
|
|
4471
|
+
const source = event.data.source;
|
|
4472
|
+
const sourceKind = source.kind ?? '';
|
|
3953
4473
|
if (sourceKind === 'user') {
|
|
3954
4474
|
this.pushRow({ kind: 'user', text: `❯ ${text}` });
|
|
4475
|
+
if (!this.replaying)
|
|
4476
|
+
this.beginWait(text);
|
|
4477
|
+
}
|
|
4478
|
+
else if (isPromptInjectionMessage(sourceKind, text, source.plugin)) {
|
|
4479
|
+
this.pushPromptInjection(text, source.plugin);
|
|
3955
4480
|
}
|
|
3956
|
-
else if (sourceKind === 'plugin' &&
|
|
4481
|
+
else if (sourceKind === 'plugin' && source.form === 'snapshot') {
|
|
3957
4482
|
this.pushRow({ kind: 'system', text: text });
|
|
3958
4483
|
}
|
|
3959
4484
|
else {
|
|
@@ -4042,22 +4567,24 @@ export class SshTui {
|
|
|
4042
4567
|
case 'tool/call': {
|
|
4043
4568
|
this.openToolCalls.set(String(event.data.callId), event.data.name);
|
|
4044
4569
|
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
|
-
|
|
4570
|
+
if (!HIDDEN_TOOL_NAMES.has(event.data.name)) {
|
|
4571
|
+
const present = presentToolCall(event.data.name, event.data.arguments);
|
|
4572
|
+
const row = {
|
|
4573
|
+
kind: 'tool',
|
|
4574
|
+
callId: event.data.callId,
|
|
4575
|
+
name: event.data.name,
|
|
4576
|
+
args: event.data.arguments,
|
|
4577
|
+
status: 'running',
|
|
4578
|
+
output: '',
|
|
4579
|
+
title: present.title,
|
|
4580
|
+
summary: present.summary,
|
|
4581
|
+
...present.command === undefined ? {} : { command: present.command },
|
|
4582
|
+
...present.cwd === undefined ? {} : { cwd: present.cwd },
|
|
4583
|
+
...present.diff === undefined ? {} : { diff: present.diff },
|
|
4584
|
+
expanded: false,
|
|
4585
|
+
};
|
|
4586
|
+
this.pushRow(row);
|
|
4587
|
+
}
|
|
4061
4588
|
if (event.data.name === 'exit_plan_mode') {
|
|
4062
4589
|
const markdown = planMarkdownFromArgs(event.data.arguments);
|
|
4063
4590
|
if (markdown !== undefined)
|
|
@@ -4080,8 +4607,6 @@ export class SshTui {
|
|
|
4080
4607
|
const metaDiffs = diffMetaDiffs(event.data.meta);
|
|
4081
4608
|
if (metaDiffs !== null) {
|
|
4082
4609
|
row.diff = metaDiffs;
|
|
4083
|
-
if (DIFF_TOOL_NAMES.has(row.name))
|
|
4084
|
-
row.expanded = true;
|
|
4085
4610
|
}
|
|
4086
4611
|
const isShell = SHELL_TOOL_NAMES.has(row.name);
|
|
4087
4612
|
if (isShell) {
|
|
@@ -4167,6 +4692,7 @@ export class SshTui {
|
|
|
4167
4692
|
this.streaming = undefined;
|
|
4168
4693
|
this.streamingReasoning = undefined;
|
|
4169
4694
|
this.thinkingStartedAt = undefined;
|
|
4695
|
+
this.endWait();
|
|
4170
4696
|
if (reason.kind === 'completed' && !this.replaying && !this.completionSignaled) {
|
|
4171
4697
|
this.completionSignaled = true;
|
|
4172
4698
|
this.completedAt = Date.now();
|
|
@@ -4207,13 +4733,18 @@ export class SshTui {
|
|
|
4207
4733
|
if (status === 'running') {
|
|
4208
4734
|
this.completionSignaled = false;
|
|
4209
4735
|
this.completedAt = 0;
|
|
4736
|
+
if (this.waitStartedAt === undefined)
|
|
4737
|
+
this.beginWait();
|
|
4210
4738
|
}
|
|
4211
4739
|
else if (!this.completionSignaled && this.status === 'running') {
|
|
4212
4740
|
this.completionSignaled = true;
|
|
4213
4741
|
this.completedAt = Date.now();
|
|
4214
4742
|
this.updateTerminalTitle();
|
|
4215
4743
|
this.playCompletionSignal();
|
|
4744
|
+
this.endWait();
|
|
4216
4745
|
}
|
|
4746
|
+
if (status !== 'running')
|
|
4747
|
+
this.endWait();
|
|
4217
4748
|
this.status = status === 'running' ? 'running' : 'idle';
|
|
4218
4749
|
this.markDirty();
|
|
4219
4750
|
};
|
|
@@ -4481,6 +5012,16 @@ export class SshTui {
|
|
|
4481
5012
|
this.pushRow({ kind: 'system', text: `${notice}:${objective}` });
|
|
4482
5013
|
this.markDirty();
|
|
4483
5014
|
}
|
|
5015
|
+
pushPromptInjection(text, plugin) {
|
|
5016
|
+
const sources = promptInjectionSources(text, plugin);
|
|
5017
|
+
this.pushRow({
|
|
5018
|
+
kind: 'prompt',
|
|
5019
|
+
sources,
|
|
5020
|
+
text,
|
|
5021
|
+
...(plugin === undefined ? {} : { plugin }),
|
|
5022
|
+
expanded: false,
|
|
5023
|
+
});
|
|
5024
|
+
}
|
|
4484
5025
|
handleSubagentExtensionEvent(row, event) {
|
|
4485
5026
|
const type = String(event.type);
|
|
4486
5027
|
const data = event.data;
|
|
@@ -4516,6 +5057,8 @@ export class SshTui {
|
|
|
4516
5057
|
break;
|
|
4517
5058
|
}
|
|
4518
5059
|
case 'tool/call': {
|
|
5060
|
+
if (HIDDEN_TOOL_NAMES.has(event.data.name))
|
|
5061
|
+
break;
|
|
4519
5062
|
const present = presentToolCall(event.data.name, event.data.arguments);
|
|
4520
5063
|
appendSubagentLog(row, { kind: 'tool', text: `▶ ${present.title} ${present.summary}` });
|
|
4521
5064
|
break;
|
|
@@ -4812,7 +5355,7 @@ export class SshTui {
|
|
|
4812
5355
|
/** Default listing endpoint for a built-in OpenCode route with no stored base URL. */
|
|
4813
5356
|
openCodeListingBaseURL(provider) {
|
|
4814
5357
|
if (provider === 'opencode-go')
|
|
4815
|
-
return
|
|
5358
|
+
return providerTemplates()['opencode-go'].defaultBaseUrl;
|
|
4816
5359
|
if (provider === 'opencode')
|
|
4817
5360
|
return OPENCODE_ZEN_BASE_URL;
|
|
4818
5361
|
return undefined;
|
|
@@ -5365,7 +5908,7 @@ export class SshTui {
|
|
|
5365
5908
|
return;
|
|
5366
5909
|
}
|
|
5367
5910
|
const choices = [
|
|
5368
|
-
{ id: undefined, label: SUBAGENT_DEFAULT_EFFORT_LABEL },
|
|
5911
|
+
{ id: undefined, label: SUBAGENT_DEFAULT_EFFORT_LABEL() },
|
|
5369
5912
|
...effortOptions.map(option => ({ id: option.id, label: option.label })),
|
|
5370
5913
|
];
|
|
5371
5914
|
const answer = await this.askQuestion({
|
|
@@ -5398,6 +5941,41 @@ export class SshTui {
|
|
|
5398
5941
|
});
|
|
5399
5942
|
this.markDirty();
|
|
5400
5943
|
}
|
|
5944
|
+
/** /language or /lang: persist zh/en and repaint chrome immediately. */
|
|
5945
|
+
async runLanguageCommand(arg) {
|
|
5946
|
+
const direct = localeFromTag(arg);
|
|
5947
|
+
let next = direct;
|
|
5948
|
+
if (next === undefined && arg.trim() !== '') {
|
|
5949
|
+
this.pushRow({ kind: 'error', text: t('lang.unknown', { id: arg.trim() }) });
|
|
5950
|
+
this.markDirty();
|
|
5951
|
+
return;
|
|
5952
|
+
}
|
|
5953
|
+
if (next === undefined) {
|
|
5954
|
+
const current = getLocale();
|
|
5955
|
+
const answer = await this.askQuestion({
|
|
5956
|
+
id: 'language-pick',
|
|
5957
|
+
question: t('lang.pick'),
|
|
5958
|
+
options: [
|
|
5959
|
+
{ label: t('lang.zh'), description: current === 'zh' ? t('lang.current') : t('lang.zhDesc') },
|
|
5960
|
+
{ label: t('lang.en'), description: current === 'en' ? t('lang.current') : t('lang.enDesc') },
|
|
5961
|
+
],
|
|
5962
|
+
}, 0, 1, current === 'en' ? 1 : 0);
|
|
5963
|
+
const picked = answer.selected[0];
|
|
5964
|
+
next = picked === t('lang.en') ? 'en' : 'zh';
|
|
5965
|
+
}
|
|
5966
|
+
setLocale(next);
|
|
5967
|
+
const settings = this.ctx.get('settings');
|
|
5968
|
+
if (settings === undefined) {
|
|
5969
|
+
this.pushRow({ kind: 'error', text: t('lang.settingsMissing') });
|
|
5970
|
+
}
|
|
5971
|
+
else {
|
|
5972
|
+
await settings.replace(UI_LOCALE_NAMESPACE, { language: next });
|
|
5973
|
+
applySavedLocale({ language: next });
|
|
5974
|
+
}
|
|
5975
|
+
this.forceFullPaint = true;
|
|
5976
|
+
this.pushRow({ kind: 'system', text: t('lang.switched', { name: localeDisplayName(next) }) });
|
|
5977
|
+
this.markDirty();
|
|
5978
|
+
}
|
|
5401
5979
|
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
5402
5980
|
async runModeCommand() {
|
|
5403
5981
|
const agentPresets = this.ctx.get('agentPresets');
|
|
@@ -5762,7 +6340,10 @@ export class SshTui {
|
|
|
5762
6340
|
if (match !== null) {
|
|
5763
6341
|
switch (match[1]) {
|
|
5764
6342
|
case 'A':
|
|
5765
|
-
if (this.
|
|
6343
|
+
if (this.dialog?.kind === 'inspect') {
|
|
6344
|
+
this.scrollInspectOrTranscript(-1);
|
|
6345
|
+
}
|
|
6346
|
+
else if (this.suggestionsVisible()) {
|
|
5766
6347
|
this.suggestionIndex = Math.max(0, this.suggestionIndex - 1);
|
|
5767
6348
|
this.markDirty();
|
|
5768
6349
|
}
|
|
@@ -5774,7 +6355,10 @@ export class SshTui {
|
|
|
5774
6355
|
}
|
|
5775
6356
|
return;
|
|
5776
6357
|
case 'B':
|
|
5777
|
-
if (this.
|
|
6358
|
+
if (this.dialog?.kind === 'inspect') {
|
|
6359
|
+
this.scrollInspectOrTranscript(1);
|
|
6360
|
+
}
|
|
6361
|
+
else if (this.suggestionsVisible()) {
|
|
5778
6362
|
this.suggestionIndex = Math.min(this.commandSuggestions.length - 1, this.suggestionIndex + 1);
|
|
5779
6363
|
this.markDirty();
|
|
5780
6364
|
}
|
|
@@ -5799,13 +6383,11 @@ export class SshTui {
|
|
|
5799
6383
|
const y = Number(sgrMouse[3]);
|
|
5800
6384
|
if (sgrMouse[4] === 'M') {
|
|
5801
6385
|
if (button === 64) {
|
|
5802
|
-
this.
|
|
5803
|
-
this.markDirty();
|
|
6386
|
+
this.scrollInspectOrTranscript(3);
|
|
5804
6387
|
return;
|
|
5805
6388
|
}
|
|
5806
6389
|
if (button === 65) {
|
|
5807
|
-
this.
|
|
5808
|
-
this.markDirty();
|
|
6390
|
+
this.scrollInspectOrTranscript(-3);
|
|
5809
6391
|
return;
|
|
5810
6392
|
}
|
|
5811
6393
|
if (button === 0) {
|
|
@@ -5816,13 +6398,11 @@ export class SshTui {
|
|
|
5816
6398
|
return;
|
|
5817
6399
|
}
|
|
5818
6400
|
if (combined === '\x1b[5~') {
|
|
5819
|
-
this.
|
|
5820
|
-
this.markDirty();
|
|
6401
|
+
this.scrollInspectOrTranscript(Math.max(3, Math.floor((process.stdout.rows || 24) / 2)));
|
|
5821
6402
|
return;
|
|
5822
6403
|
}
|
|
5823
6404
|
if (combined === '\x1b[6~') {
|
|
5824
|
-
this.
|
|
5825
|
-
this.markDirty();
|
|
6405
|
+
this.scrollInspectOrTranscript(-Math.max(3, Math.floor((process.stdout.rows || 24) / 2)));
|
|
5826
6406
|
return;
|
|
5827
6407
|
}
|
|
5828
6408
|
if (parseCursorPositionReply(combined) !== undefined)
|
|
@@ -5955,6 +6535,10 @@ export class SshTui {
|
|
|
5955
6535
|
return;
|
|
5956
6536
|
this.input = `${this.input.slice(0, this.cursor)}${normalized}${this.input.slice(this.cursor)}`;
|
|
5957
6537
|
this.cursor += normalized.length;
|
|
6538
|
+
const cols = Math.max(10, process.stdout.columns || 80);
|
|
6539
|
+
const lineWidth = Math.max(1, cols - 2);
|
|
6540
|
+
if (normalized.includes('\n') || displayWidth(this.input) > lineWidth)
|
|
6541
|
+
this.inputFolded = true;
|
|
5958
6542
|
this.markDirty();
|
|
5959
6543
|
}
|
|
5960
6544
|
handlePlainText(text) {
|
|
@@ -6089,6 +6673,12 @@ export class SshTui {
|
|
|
6089
6673
|
const dialog = this.dialog;
|
|
6090
6674
|
if (dialog === undefined)
|
|
6091
6675
|
return;
|
|
6676
|
+
if (dialog.kind === 'inspect') {
|
|
6677
|
+
if (text === '\x1b' || text === '\x03' || text === 'q' || text === 'Q' || text === '\r' || text === '\n') {
|
|
6678
|
+
this.closeInspect();
|
|
6679
|
+
}
|
|
6680
|
+
return;
|
|
6681
|
+
}
|
|
6092
6682
|
if (dialog.kind === 'onboarding') {
|
|
6093
6683
|
this.handleOnboardingChar(text);
|
|
6094
6684
|
return;
|
|
@@ -6184,7 +6774,7 @@ export class SshTui {
|
|
|
6184
6774
|
if (text === '\r' || text === '\n') {
|
|
6185
6775
|
const value = this.input.trim();
|
|
6186
6776
|
if (state.step === 'id') {
|
|
6187
|
-
const template =
|
|
6777
|
+
const template = providerTemplates()[state.providerType];
|
|
6188
6778
|
const id = value === '' ? template.defaultId : value;
|
|
6189
6779
|
if (!/^[a-z0-9][a-z0-9-]*$/u.test(id)) {
|
|
6190
6780
|
this.pushRow({ kind: 'error', text: 'Provider ID 只能包含小写字母、数字和连字符,且不能以连字符开头。' });
|
|
@@ -6202,7 +6792,7 @@ export class SshTui {
|
|
|
6202
6792
|
state.key = value;
|
|
6203
6793
|
}
|
|
6204
6794
|
else if (state.step === 'models') {
|
|
6205
|
-
const template =
|
|
6795
|
+
const template = providerTemplates()[state.providerType];
|
|
6206
6796
|
const parsed = value === ''
|
|
6207
6797
|
? template.defaultModels
|
|
6208
6798
|
: value.split(/[\s,,]+/u).filter(Boolean);
|
|
@@ -6281,7 +6871,7 @@ export class SshTui {
|
|
|
6281
6871
|
const state = this.onboarding;
|
|
6282
6872
|
if (state === undefined || state.step !== 'models')
|
|
6283
6873
|
return;
|
|
6284
|
-
const template =
|
|
6874
|
+
const template = providerTemplates()[state.providerType];
|
|
6285
6875
|
const providerType = state.providerType;
|
|
6286
6876
|
const baseUrl = state.baseUrl;
|
|
6287
6877
|
const key = state.key;
|
|
@@ -6339,7 +6929,7 @@ export class SshTui {
|
|
|
6339
6929
|
try {
|
|
6340
6930
|
const credentials = this.ctx.get('credentials');
|
|
6341
6931
|
const settings = this.ctx.get('settings');
|
|
6342
|
-
const template =
|
|
6932
|
+
const template = providerTemplates()[state.providerType];
|
|
6343
6933
|
if (state.providerType === 'official') {
|
|
6344
6934
|
const envRef = 'DEEPSEEK_API_KEY';
|
|
6345
6935
|
await this.saveCredential(credentials, envRef, state.key);
|
|
@@ -6577,6 +7167,10 @@ export class SshTui {
|
|
|
6577
7167
|
}
|
|
6578
7168
|
handleEscape() {
|
|
6579
7169
|
if (this.dialog !== undefined) {
|
|
7170
|
+
if (this.dialog.kind === 'inspect') {
|
|
7171
|
+
this.closeInspect();
|
|
7172
|
+
return;
|
|
7173
|
+
}
|
|
6580
7174
|
if (this.dialog.kind === 'confirm')
|
|
6581
7175
|
this.closeConfirm('cancel');
|
|
6582
7176
|
else if (this.dialog.kind === 'onboarding')
|
|
@@ -6612,26 +7206,47 @@ export class SshTui {
|
|
|
6612
7206
|
handleMouseClick(y) {
|
|
6613
7207
|
if (this.dialog !== undefined)
|
|
6614
7208
|
return;
|
|
7209
|
+
if (this.cwdChipRow !== undefined && y === this.cwdChipRow) {
|
|
7210
|
+
this.announceWorkspaceCwd();
|
|
7211
|
+
return;
|
|
7212
|
+
}
|
|
6615
7213
|
const row = this.clickableRows.get(y);
|
|
6616
7214
|
if (row === undefined)
|
|
6617
7215
|
return;
|
|
6618
|
-
this.
|
|
6619
|
-
|
|
7216
|
+
this.toggleCard(row);
|
|
7217
|
+
}
|
|
7218
|
+
scrollInspectOrTranscript(delta) {
|
|
7219
|
+
if (this.dialog?.kind === 'inspect') {
|
|
7220
|
+
this.dialog.offset = Math.max(0, this.dialog.offset + delta);
|
|
7221
|
+
this.markDirty();
|
|
7222
|
+
return;
|
|
7223
|
+
}
|
|
7224
|
+
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
6620
7225
|
this.markDirty();
|
|
6621
7226
|
}
|
|
6622
7227
|
handleCtrlC() {
|
|
6623
7228
|
if (this.dialog !== undefined) {
|
|
6624
7229
|
this.handleEscape();
|
|
7230
|
+
this.lastIdleCtrlCAt = 0;
|
|
6625
7231
|
return;
|
|
6626
7232
|
}
|
|
6627
7233
|
if (this.agent.status === 'running') {
|
|
6628
|
-
this.
|
|
7234
|
+
this.lastIdleCtrlCAt = 0;
|
|
7235
|
+
this.pushRow({ kind: 'system', text: t('cancel.ctrlC') });
|
|
6629
7236
|
this.agent.cancel({ kind: 'user' });
|
|
6630
7237
|
this.status = 'cancelling…';
|
|
6631
7238
|
this.markDirty();
|
|
6632
7239
|
return;
|
|
6633
7240
|
}
|
|
6634
|
-
|
|
7241
|
+
const now = Date.now();
|
|
7242
|
+
if (now - this.lastIdleCtrlCAt <= CTRL_C_EXIT_WINDOW_MS) {
|
|
7243
|
+
this.lastIdleCtrlCAt = 0;
|
|
7244
|
+
void this.requestExit(130);
|
|
7245
|
+
return;
|
|
7246
|
+
}
|
|
7247
|
+
this.lastIdleCtrlCAt = now;
|
|
7248
|
+
this.pushRow({ kind: 'system', text: t('exit.ctrlCAgain') });
|
|
7249
|
+
this.markDirty();
|
|
6635
7250
|
}
|
|
6636
7251
|
submit() {
|
|
6637
7252
|
if (this.dialog !== undefined) {
|
|
@@ -6670,10 +7285,11 @@ export class SshTui {
|
|
|
6670
7285
|
});
|
|
6671
7286
|
if (this.agent.status === 'running') {
|
|
6672
7287
|
this.pendingMessages.set(message.id, text);
|
|
6673
|
-
this.pushRow({ kind: 'system', text:
|
|
7288
|
+
this.pushRow({ kind: 'system', text: t('steer.queued', { text }) });
|
|
6674
7289
|
this.agent.steer(message);
|
|
6675
7290
|
}
|
|
6676
7291
|
else {
|
|
7292
|
+
this.beginWait(text);
|
|
6677
7293
|
this.agent.followup(message);
|
|
6678
7294
|
}
|
|
6679
7295
|
this.markDirty();
|
|
@@ -6683,7 +7299,7 @@ export class SshTui {
|
|
|
6683
7299
|
const arg = rest.join(' ');
|
|
6684
7300
|
switch (command) {
|
|
6685
7301
|
case 'help': {
|
|
6686
|
-
const local =
|
|
7302
|
+
const local = localizedCommands()
|
|
6687
7303
|
.filter(item => item.name !== 'help' && item.name !== 'exit')
|
|
6688
7304
|
.map(item => `/${item.name.padEnd(12)} ${item.description}`);
|
|
6689
7305
|
const dsh = (this.ctx.get('commands')?.list(this.agent) ?? [])
|
|
@@ -6694,13 +7310,13 @@ export class SshTui {
|
|
|
6694
7310
|
...local,
|
|
6695
7311
|
...dsh,
|
|
6696
7312
|
'',
|
|
6697
|
-
'
|
|
6698
|
-
'
|
|
6699
|
-
'
|
|
6700
|
-
'
|
|
6701
|
-
'
|
|
6702
|
-
'
|
|
6703
|
-
'
|
|
7313
|
+
t('help.intro1'),
|
|
7314
|
+
t('help.intro2'),
|
|
7315
|
+
t('help.intro3'),
|
|
7316
|
+
t('help.intro4'),
|
|
7317
|
+
t('help.intro5'),
|
|
7318
|
+
t('help.intro6'),
|
|
7319
|
+
t('help.intro7'),
|
|
6704
7320
|
].join('\n'),
|
|
6705
7321
|
});
|
|
6706
7322
|
break;
|
|
@@ -6712,7 +7328,7 @@ export class SshTui {
|
|
|
6712
7328
|
case 'model':
|
|
6713
7329
|
void this.runModelCommand().catch((error) => {
|
|
6714
7330
|
if (error instanceof UserQuestionError) {
|
|
6715
|
-
this.pushRow({ kind: 'system', text: '
|
|
7331
|
+
this.pushRow({ kind: 'system', text: t('help.modelCancel') });
|
|
6716
7332
|
}
|
|
6717
7333
|
else {
|
|
6718
7334
|
this.pushRow({ kind: 'error', text: `/model failed: ${errorChain(error)}` });
|
|
@@ -6723,7 +7339,7 @@ export class SshTui {
|
|
|
6723
7339
|
case 'provider':
|
|
6724
7340
|
void this.runProviderCommand().catch((error) => {
|
|
6725
7341
|
if (error instanceof UserQuestionError) {
|
|
6726
|
-
this.pushRow({ kind: 'system', text: '
|
|
7342
|
+
this.pushRow({ kind: 'system', text: t('help.providerCancel') });
|
|
6727
7343
|
}
|
|
6728
7344
|
else {
|
|
6729
7345
|
this.pushRow({ kind: 'error', text: `/provider failed: ${errorChain(error)}` });
|
|
@@ -6734,7 +7350,7 @@ export class SshTui {
|
|
|
6734
7350
|
case 'submodel':
|
|
6735
7351
|
void this.runSubmodelCommand(arg).catch((error) => {
|
|
6736
7352
|
if (error instanceof UserQuestionError) {
|
|
6737
|
-
this.pushRow({ kind: 'system', text: '
|
|
7353
|
+
this.pushRow({ kind: 'system', text: t('help.submodelCancel') });
|
|
6738
7354
|
}
|
|
6739
7355
|
else {
|
|
6740
7356
|
this.pushRow({ kind: 'error', text: `/submodel failed: ${errorChain(error)}` });
|
|
@@ -6745,7 +7361,7 @@ export class SshTui {
|
|
|
6745
7361
|
case 'subeffort':
|
|
6746
7362
|
void this.runSubeffortCommand().catch((error) => {
|
|
6747
7363
|
if (error instanceof UserQuestionError) {
|
|
6748
|
-
this.pushRow({ kind: 'system', text: '
|
|
7364
|
+
this.pushRow({ kind: 'system', text: t('help.subeffortCancel') });
|
|
6749
7365
|
}
|
|
6750
7366
|
else {
|
|
6751
7367
|
this.pushRow({ kind: 'error', text: `/subeffort failed: ${errorChain(error)}` });
|
|
@@ -6756,7 +7372,7 @@ export class SshTui {
|
|
|
6756
7372
|
case 'mode':
|
|
6757
7373
|
void this.runModeCommand().catch((error) => {
|
|
6758
7374
|
if (error instanceof UserQuestionError) {
|
|
6759
|
-
this.pushRow({ kind: 'system', text: '
|
|
7375
|
+
this.pushRow({ kind: 'system', text: t('help.modeCancel') });
|
|
6760
7376
|
}
|
|
6761
7377
|
else {
|
|
6762
7378
|
this.pushRow({ kind: 'error', text: `/mode failed: ${errorChain(error)}` });
|
|
@@ -6764,6 +7380,18 @@ export class SshTui {
|
|
|
6764
7380
|
this.markDirty();
|
|
6765
7381
|
});
|
|
6766
7382
|
break;
|
|
7383
|
+
case 'language':
|
|
7384
|
+
case 'lang':
|
|
7385
|
+
void this.runLanguageCommand(arg).catch((error) => {
|
|
7386
|
+
if (error instanceof UserQuestionError) {
|
|
7387
|
+
this.pushRow({ kind: 'system', text: t('help.modeCancel') });
|
|
7388
|
+
}
|
|
7389
|
+
else {
|
|
7390
|
+
this.pushRow({ kind: 'error', text: `/language failed: ${errorChain(error)}` });
|
|
7391
|
+
}
|
|
7392
|
+
this.markDirty();
|
|
7393
|
+
});
|
|
7394
|
+
break;
|
|
6767
7395
|
case 'find':
|
|
6768
7396
|
this.runFindCommand(arg);
|
|
6769
7397
|
break;
|
|
@@ -6772,34 +7400,45 @@ export class SshTui {
|
|
|
6772
7400
|
this.streaming = undefined;
|
|
6773
7401
|
this.streamingReasoning = undefined;
|
|
6774
7402
|
this.thinkingStartedAt = undefined;
|
|
7403
|
+
this.waitStartedAt = undefined;
|
|
7404
|
+
this.waitPrompt = undefined;
|
|
6775
7405
|
this.focusedRow = null;
|
|
6776
7406
|
this.searchHits = [];
|
|
6777
7407
|
this.searchIndex = -1;
|
|
6778
7408
|
this.searchQuery = '';
|
|
6779
7409
|
this.planNudgePending = false;
|
|
6780
7410
|
this.pendingReveal = undefined;
|
|
6781
|
-
this.pushRow({ kind: 'system', text: '
|
|
7411
|
+
this.pushRow({ kind: 'system', text: t('clear.transcript') });
|
|
6782
7412
|
break;
|
|
6783
7413
|
case 'status':
|
|
6784
7414
|
{
|
|
6785
7415
|
const plan = this.findLivePlanRow();
|
|
6786
7416
|
const waiting = this.rows.filter(row => row.kind === 'question' && row.status === 'waiting').length;
|
|
6787
7417
|
const provider = this.currentProviderId();
|
|
6788
|
-
const route = describeProviderRoute(provider);
|
|
6789
7418
|
const model = this.selectionRef?.current?.model ?? this.agent.options.model ?? 'default';
|
|
6790
7419
|
const effort = this.selectionRef?.current?.reasoningEffort;
|
|
6791
|
-
const
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
7420
|
+
const sub = this.subagentSelection.current;
|
|
7421
|
+
const quota = this.quotaSnapshot !== undefined && this.quotaSnapshot.provider === provider
|
|
7422
|
+
? this.quotaSnapshot
|
|
7423
|
+
: undefined;
|
|
7424
|
+
const lines = formatStatusReport({
|
|
7425
|
+
sessionId: this.agent.id,
|
|
7426
|
+
pluginVersion: PLUGIN_VERSION,
|
|
7427
|
+
provider,
|
|
7428
|
+
model,
|
|
7429
|
+
...(effort === undefined ? {} : { effort }),
|
|
7430
|
+
agentStatus: this.agent.status,
|
|
7431
|
+
preset: this.presetName,
|
|
7432
|
+
activeSubagents: this.activeSubagents.size,
|
|
7433
|
+
plan: plan === undefined ? 'off' : plan.pending ? 'pending' : plan.active ? 'on' : 'off',
|
|
7434
|
+
paint: formatLinkQualityChip(this.paintLink, this.paintIntervalMs, this.paintRttMs, this.paintProbed),
|
|
7435
|
+
waitingQuestions: waiting,
|
|
7436
|
+
...(quota === undefined ? {} : { quota }),
|
|
7437
|
+
parentModel: model,
|
|
7438
|
+
...(sub.provider === undefined ? {} : { subProvider: sub.provider }),
|
|
7439
|
+
subModel: sub.model,
|
|
7440
|
+
cwd: this.workspaceCwd(),
|
|
7441
|
+
});
|
|
6803
7442
|
this.pushRow({ kind: 'system', text: lines.join('\n') });
|
|
6804
7443
|
}
|
|
6805
7444
|
break;
|
|
@@ -6848,7 +7487,7 @@ export class SshTui {
|
|
|
6848
7487
|
const activity = card?.lastActivity ? ` · ${card.lastActivity}` : '';
|
|
6849
7488
|
return `▶ ${label} ${sub.id}(${sub.provider})运行 ${Math.floor((Date.now() - sub.startedAt) / 1000)}s [${runId.slice(0, 8)}]${activity}`;
|
|
6850
7489
|
});
|
|
6851
|
-
this.pushRow({ kind: 'system', text:
|
|
7490
|
+
this.pushRow({ kind: 'system', text: t('sub.listHint', { lines: lines.join('\n') }) });
|
|
6852
7491
|
}
|
|
6853
7492
|
break;
|
|
6854
7493
|
}
|