dsh-ssh-tui 0.5.1 → 0.5.3
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 +13 -4
- package/README.md +9 -6
- package/lib/approval-reviewer.js +56 -12
- package/lib/approval-reviewer.js.map +1 -1
- package/lib/auto-approval.js +75 -8
- package/lib/auto-approval.js.map +1 -1
- package/lib/i18n/en.js +36 -7
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +1 -0
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +36 -7
- package/lib/i18n/zh.js.map +1 -1
- package/lib/tui.js +512 -61
- package/lib/tui.js.map +1 -1
- package/lib/types/approval-reviewer.d.ts +12 -4
- package/lib/types/auto-approval.d.ts +25 -2
- package/lib/types/i18n/index.d.ts +2 -0
- package/lib/types/tui.d.ts +77 -3
- package/package.json +4 -4
package/lib/tui.js
CHANGED
|
@@ -21,7 +21,7 @@ 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 { sessionEvents, settingsNamespace } from './dsh-compat.js';
|
|
24
|
-
import { classifyApproval,
|
|
24
|
+
import { classifyApproval, commandForApprovalRequest, isApprovalStatusArg, parseAutoApprovalMode } from './auto-approval.js';
|
|
25
25
|
import { buildReviewUserMessage, parseReviewOutput, REVIEW_SYSTEM_PROMPT } from './approval-reviewer.js';
|
|
26
26
|
import { loadProviderCatalog, mergeProviderEntries } from './provider-catalog.js';
|
|
27
27
|
import { formatFooterCwd, formatSessionTime, listResumableSessions } from './session-list.js';
|
|
@@ -124,6 +124,117 @@ export function formatTokens(n) {
|
|
|
124
124
|
return `${scaled(n / 1_000)}K`;
|
|
125
125
|
return `${scaled(n / 1_000_000)}M`;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Prompt occupancy of the next request, from DSH `contextPressure`.
|
|
129
|
+
* Provider-agnostic: uses the routed model's advertised window, not a
|
|
130
|
+
* hardcoded xAI size. Compaction-basic still owns in-turn pressure at 80%.
|
|
131
|
+
*/
|
|
132
|
+
export const CONTEXT_PRESSURE_WARN_RATIO = 0.8;
|
|
133
|
+
export const CONTEXT_PRESSURE_DANGER_RATIO = 0.95;
|
|
134
|
+
/** Idle auto-compact starts here so recovery finishes before the 80% in-turn trigger. */
|
|
135
|
+
export const CONTEXT_IDLE_COMPACT_RATIO = 0.72;
|
|
136
|
+
/** Prompt-side occupancy of one usage sample: uncached input plus cache traffic. */
|
|
137
|
+
export function promptPressureTokens(usage) {
|
|
138
|
+
return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
139
|
+
}
|
|
140
|
+
/** Prefer the next-request projection; fall back to last-request pressure. */
|
|
141
|
+
export function contextPressureUsedTokens(pressure) {
|
|
142
|
+
if (pressure === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
if (typeof pressure.projectedTokens === 'number' && Number.isFinite(pressure.projectedTokens)) {
|
|
145
|
+
return Math.max(0, pressure.projectedTokens);
|
|
146
|
+
}
|
|
147
|
+
if (typeof pressure.pressureTokens === 'number' && Number.isFinite(pressure.pressureTokens)) {
|
|
148
|
+
return Math.max(0, pressure.pressureTokens);
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
export function parseContextPressure(value) {
|
|
153
|
+
if (value === null || typeof value !== 'object')
|
|
154
|
+
return undefined;
|
|
155
|
+
const raw = value;
|
|
156
|
+
const window = typeof raw.contextWindow === 'number' && Number.isFinite(raw.contextWindow)
|
|
157
|
+
? raw.contextWindow
|
|
158
|
+
: undefined;
|
|
159
|
+
if (window === undefined || window <= 0)
|
|
160
|
+
return undefined;
|
|
161
|
+
const used = contextPressureUsedTokens({
|
|
162
|
+
...(typeof raw.projectedTokens === 'number' ? { projectedTokens: raw.projectedTokens } : {}),
|
|
163
|
+
...(typeof raw.pressureTokens === 'number' ? { pressureTokens: raw.pressureTokens } : {}),
|
|
164
|
+
});
|
|
165
|
+
if (used === undefined)
|
|
166
|
+
return undefined;
|
|
167
|
+
return { usedTokens: used, contextWindow: window };
|
|
168
|
+
}
|
|
169
|
+
export function contextPressureView(sample) {
|
|
170
|
+
const percent = (sample.usedTokens / sample.contextWindow) * 100;
|
|
171
|
+
return {
|
|
172
|
+
usedTokens: sample.usedTokens,
|
|
173
|
+
contextWindow: sample.contextWindow,
|
|
174
|
+
percent,
|
|
175
|
+
level: percent >= CONTEXT_PRESSURE_DANGER_RATIO * 100
|
|
176
|
+
? 'danger'
|
|
177
|
+
: percent >= CONTEXT_PRESSURE_WARN_RATIO * 100
|
|
178
|
+
? 'warn'
|
|
179
|
+
: 'ok',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 8-segment Braille ring. Empty `⣀`; full `⣿`. Width is always 1 cell.
|
|
184
|
+
* Index is `ceil(percent / 12.5)` clamped to 0..8.
|
|
185
|
+
*/
|
|
186
|
+
export const CONTEXT_RING_EMPTY = '⣀';
|
|
187
|
+
export const CONTEXT_RING_SEGMENTS = ['⣀', '⠉', '⠋', '⠛', '⠞', '⠟', '⠿', '⡿', '⣿'];
|
|
188
|
+
export function formatContextPressureRing(percent) {
|
|
189
|
+
if (!Number.isFinite(percent) || percent <= 0)
|
|
190
|
+
return CONTEXT_RING_EMPTY;
|
|
191
|
+
const filled = Math.min(8, Math.max(0, Math.ceil(percent / 12.5)));
|
|
192
|
+
return CONTEXT_RING_SEGMENTS[filled] ?? '⣿';
|
|
193
|
+
}
|
|
194
|
+
export function contextPressureRingColor(level) {
|
|
195
|
+
if (level === 'danger')
|
|
196
|
+
return '31';
|
|
197
|
+
if (level === 'warn')
|
|
198
|
+
return '33';
|
|
199
|
+
return '32';
|
|
200
|
+
}
|
|
201
|
+
export function formatContextPressureChip(view, color = false) {
|
|
202
|
+
const ring = formatContextPressureRing(view.percent);
|
|
203
|
+
const painted = color
|
|
204
|
+
? `\x1b[${contextPressureRingColor(view.level)}m${ring}\x1b[0m`
|
|
205
|
+
: ring;
|
|
206
|
+
return t('footer.contextRing', {
|
|
207
|
+
ring: painted,
|
|
208
|
+
used: formatTokens(view.usedTokens),
|
|
209
|
+
window: formatTokens(view.contextWindow),
|
|
210
|
+
percent: Math.round(view.percent),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
export function formatContextPressureStatusLine(view) {
|
|
214
|
+
if (view === undefined)
|
|
215
|
+
return t('status.contextNone');
|
|
216
|
+
return t('status.contextLine', {
|
|
217
|
+
used: formatTokens(view.usedTokens),
|
|
218
|
+
window: formatTokens(view.contextWindow),
|
|
219
|
+
percent: view.percent.toFixed(1),
|
|
220
|
+
level: t(`status.contextLevel.${view.level}`),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export function contextPressureAlertText(view) {
|
|
224
|
+
const vars = {
|
|
225
|
+
used: formatTokens(view.usedTokens),
|
|
226
|
+
window: formatTokens(view.contextWindow),
|
|
227
|
+
percent: view.percent.toFixed(0),
|
|
228
|
+
};
|
|
229
|
+
return view.level === 'danger'
|
|
230
|
+
? t('context.alertDanger', vars)
|
|
231
|
+
: t('context.alertWarn', vars);
|
|
232
|
+
}
|
|
233
|
+
export function shouldIdleAutoCompact(view) {
|
|
234
|
+
if (view === undefined)
|
|
235
|
+
return false;
|
|
236
|
+
return view.usedTokens / view.contextWindow >= CONTEXT_IDLE_COMPACT_RATIO;
|
|
237
|
+
}
|
|
127
238
|
/** Compact duration, matching the web stats line (45.2s / 2m42s). */
|
|
128
239
|
export function formatDuration(ms) {
|
|
129
240
|
const seconds = ms / 1_000;
|
|
@@ -368,6 +479,8 @@ export function footerIdentityParts(input) {
|
|
|
368
479
|
if (input.quotaPercent !== undefined) {
|
|
369
480
|
parts.push(formatFooterQuota(input.quotaPercent, input.quotaCode));
|
|
370
481
|
}
|
|
482
|
+
if (input.contextChip !== undefined && input.contextChip !== '')
|
|
483
|
+
parts.push(input.contextChip);
|
|
371
484
|
if (input.search !== undefined)
|
|
372
485
|
parts.push(t('footer.search', { index: input.search.index + 1, total: input.search.total }));
|
|
373
486
|
if (input.foldedInput)
|
|
@@ -611,6 +724,7 @@ export function formatStatusReport(input) {
|
|
|
611
724
|
fit.line,
|
|
612
725
|
`plan: ${input.plan}`,
|
|
613
726
|
formatQuotaStatusLine(input.quota),
|
|
727
|
+
formatContextPressureStatusLine(input.context),
|
|
614
728
|
`paint: ${input.paint}`,
|
|
615
729
|
`disconnect: ${input.disconnect ?? 'pause'}`,
|
|
616
730
|
input.waitingQuestions > 0 ? `questions: waiting ${input.waitingQuestions}` : 'questions: none',
|
|
@@ -653,6 +767,7 @@ const LOCAL_COMMANDS = [
|
|
|
653
767
|
{ name: 'view', key: 'cmd.view' },
|
|
654
768
|
{ name: 'usage', key: 'cmd.usage' },
|
|
655
769
|
{ name: 'balance', key: 'cmd.usage', aliasOf: 'usage' },
|
|
770
|
+
{ name: 'quota', key: 'cmd.usage', aliasOf: 'usage' },
|
|
656
771
|
{ name: 'subagents', key: 'cmd.subagents' },
|
|
657
772
|
{ name: 'resume', key: 'cmd.resume' },
|
|
658
773
|
{ name: 'setup', key: 'cmd.setup' },
|
|
@@ -1995,7 +2110,7 @@ function friendlyArgsSummary(name, args) {
|
|
|
1995
2110
|
return sliceCodePoints(args, 120);
|
|
1996
2111
|
const preferred = [
|
|
1997
2112
|
'path', 'file_path', 'file', 'query', 'pattern', 'url', 'command',
|
|
1998
|
-
'description', 'content', 'file_text', 'old_string', 'new_string',
|
|
2113
|
+
'name', 'skill', 'description', 'content', 'file_text', 'old_string', 'new_string',
|
|
1999
2114
|
'old_str', 'new_str', 'insert_line', 'line', 'offset', 'limit',
|
|
2000
2115
|
];
|
|
2001
2116
|
const parts = [];
|
|
@@ -2114,16 +2229,35 @@ const READ_TOOL_NAMES = new Set(['read']);
|
|
|
2114
2229
|
const TOOL_FLIP_MS = 280;
|
|
2115
2230
|
export function toolTargetPath(name, args, fallback = '') {
|
|
2116
2231
|
const parsed = parseJsonArgs(args);
|
|
2117
|
-
if (parsed === null)
|
|
2118
|
-
return fallback;
|
|
2119
2232
|
if (READ_TOOL_NAMES.has(name)) {
|
|
2233
|
+
if (parsed === null)
|
|
2234
|
+
return fallback;
|
|
2120
2235
|
return firstString(parsed, ['path', 'file_path', 'url']) || fallback;
|
|
2121
2236
|
}
|
|
2122
2237
|
if (DIFF_TOOL_NAMES.has(name)) {
|
|
2238
|
+
if (parsed === null)
|
|
2239
|
+
return fallback;
|
|
2123
2240
|
return firstString(parsed, ['file_path', 'path']) || fallback;
|
|
2124
2241
|
}
|
|
2125
2242
|
return fallback;
|
|
2126
2243
|
}
|
|
2244
|
+
/** Path shown on a compact single-file edit summary. */
|
|
2245
|
+
export function compactEditPath(item) {
|
|
2246
|
+
const fromArgs = toolTargetPath(item.name, item.args);
|
|
2247
|
+
if (fromArgs !== '')
|
|
2248
|
+
return fromArgs;
|
|
2249
|
+
const fromDiff = item.diff?.map(hunk => hunk.path ?? '').find(path => path !== '');
|
|
2250
|
+
if (fromDiff !== undefined && fromDiff !== '')
|
|
2251
|
+
return fromDiff;
|
|
2252
|
+
const summary = item.summary?.trim() ?? '';
|
|
2253
|
+
return summary;
|
|
2254
|
+
}
|
|
2255
|
+
function sameToolPath(left, right) {
|
|
2256
|
+
if (left === '' || right === '')
|
|
2257
|
+
return false;
|
|
2258
|
+
const normalize = (value) => value.replaceAll('\\', '/').replace(/\/+$/u, '');
|
|
2259
|
+
return normalize(left) === normalize(right);
|
|
2260
|
+
}
|
|
2127
2261
|
export function countOutputLines(text) {
|
|
2128
2262
|
if (text === '')
|
|
2129
2263
|
return 0;
|
|
@@ -2148,8 +2282,8 @@ export function canMergeToolCall(previous, next) {
|
|
|
2148
2282
|
if (kind === undefined || mergeableToolKind(previous.name) !== kind)
|
|
2149
2283
|
return false;
|
|
2150
2284
|
const previousPath = toolTargetPath(previous.name, previous.args, previous.summary);
|
|
2151
|
-
const nextPath = toolTargetPath(next.name, next.args);
|
|
2152
|
-
return previousPath
|
|
2285
|
+
const nextPath = toolTargetPath(next.name, next.args, previousPath);
|
|
2286
|
+
return sameToolPath(previousPath, nextPath);
|
|
2153
2287
|
}
|
|
2154
2288
|
export function compactToolGroups(tools) {
|
|
2155
2289
|
const edits = [];
|
|
@@ -2196,13 +2330,15 @@ const SUBAGENT_TOOL_NAMES = new Set(['subagent', 'subagent_fork', 'task']);
|
|
|
2196
2330
|
const HIDDEN_TOOL_NAMES = new Set(['get_goal']);
|
|
2197
2331
|
const TOOL_TITLE_KEYS = [
|
|
2198
2332
|
'edit', 'write', 'str_replace_editor', 'fetch', 'list_files', 'list', 'ls',
|
|
2199
|
-
'find', 'search', 'delete', 'rm', 'rename', 'mv', 'mkdir', 'skills',
|
|
2333
|
+
'find', 'search', 'delete', 'rm', 'rename', 'mv', 'mkdir', 'skills', 'skill',
|
|
2200
2334
|
'create_goal', 'update_goal', 'complete_goal', 'clear_goal', 'pause_goal',
|
|
2201
2335
|
'resume_goal', 'todo_write', 'todo', 'compact', 'glob', 'grep', 'read',
|
|
2202
2336
|
'web_search', 'web_fetch',
|
|
2203
2337
|
];
|
|
2204
2338
|
function toolTitle(name) {
|
|
2205
|
-
|
|
2339
|
+
if (name === '' || name.startsWith('call-'))
|
|
2340
|
+
return t('card.tool');
|
|
2341
|
+
return t(`toolTitle.${name}`, undefined, name === 'tool' ? t('card.tool') : name);
|
|
2206
2342
|
}
|
|
2207
2343
|
const MAX_SUBAGENT_LOGS = 80;
|
|
2208
2344
|
const TODO_STATUS_MARK = {
|
|
@@ -2237,10 +2373,7 @@ export function applyTurnEndToPlan(plan) {
|
|
|
2237
2373
|
export function planCloseNudgeText(plan) {
|
|
2238
2374
|
const leftover = plan.todos.filter(item => item.status !== 'completed');
|
|
2239
2375
|
const lines = leftover.map(item => `- [${item.status}] ${item.content}`);
|
|
2240
|
-
return [
|
|
2241
|
-
'本轮结束时计划条还有未完成待办。请立刻再调用一次 todo_write,把已经做完的标成 completed,还没做的留 pending。不要开新任务。',
|
|
2242
|
-
...lines,
|
|
2243
|
-
].join('\n');
|
|
2376
|
+
return [t('plan.nudge'), ...lines].join('\n');
|
|
2244
2377
|
}
|
|
2245
2378
|
/** Category for jump / search. Assistant replies are not collapsible cards. */
|
|
2246
2379
|
export function cardCategoryOf(row) {
|
|
@@ -2386,6 +2519,32 @@ export function isPromptInjectionMessage(sourceKind, text, plugin) {
|
|
|
2386
2519
|
|| SYSTEM_PRESET_HINT.test(text)
|
|
2387
2520
|
|| promptInjectionSources(text, plugin).some(id => id !== SYSTEM_PRESET_LABEL());
|
|
2388
2521
|
}
|
|
2522
|
+
/** Official `/compact` idle-only failures, mapped to a local sentence. */
|
|
2523
|
+
export function formatCompactCommandError(text) {
|
|
2524
|
+
const raw = text.trim();
|
|
2525
|
+
if (raw === '')
|
|
2526
|
+
return t('command.failed');
|
|
2527
|
+
if (raw.includes('agent is not idle')
|
|
2528
|
+
|| raw.includes('active compaction')
|
|
2529
|
+
|| raw.includes('requires an idle agent')) {
|
|
2530
|
+
return t('compact.busy');
|
|
2531
|
+
}
|
|
2532
|
+
if (raw.includes('No compactable history'))
|
|
2533
|
+
return t('compact.nothing');
|
|
2534
|
+
if (raw.startsWith('Usage: /compact'))
|
|
2535
|
+
return t('compact.usage');
|
|
2536
|
+
if (raw === 'Compaction cancelled.')
|
|
2537
|
+
return t('compact.cancelled');
|
|
2538
|
+
if (raw.includes('could not produce a useful summary'))
|
|
2539
|
+
return t('compact.noSummary');
|
|
2540
|
+
if (raw.includes('history selected for compaction changed'))
|
|
2541
|
+
return t('compact.changed');
|
|
2542
|
+
if (raw.includes('did not finish cleanly'))
|
|
2543
|
+
return t('compact.commit');
|
|
2544
|
+
if (raw.includes('could not be saved'))
|
|
2545
|
+
return t('compact.persistence');
|
|
2546
|
+
return raw;
|
|
2547
|
+
}
|
|
2389
2548
|
export function compactionHeaderText(row) {
|
|
2390
2549
|
const recovered = row.prunedTokens > 0
|
|
2391
2550
|
? t('compact.recoverTokens', { tokens: formatTokens(row.prunedTokens) })
|
|
@@ -2637,6 +2796,13 @@ export function presentToolCall(name, args) {
|
|
|
2637
2796
|
if (name === 'get_goal') {
|
|
2638
2797
|
return { title: toolTitle('get_goal'), summary: friendlyArgsSummary(name, args) };
|
|
2639
2798
|
}
|
|
2799
|
+
if (name === 'skill' || name === 'skills') {
|
|
2800
|
+
const skill = typeof parsed?.name === 'string' ? parsed.name.trim()
|
|
2801
|
+
: typeof parsed?.skill === 'string' ? parsed.skill.trim()
|
|
2802
|
+
: typeof parsed?.id === 'string' ? parsed.id.trim()
|
|
2803
|
+
: '';
|
|
2804
|
+
return { title: toolTitle('skill'), summary: skill || friendlyArgsSummary(name, args) };
|
|
2805
|
+
}
|
|
2640
2806
|
if (name === 'read') {
|
|
2641
2807
|
const path = typeof parsed?.path === 'string' ? parsed.path
|
|
2642
2808
|
: typeof parsed?.file_path === 'string' ? parsed.file_path
|
|
@@ -3157,6 +3323,8 @@ export class SshTui {
|
|
|
3157
3323
|
activeSubagents = new Map();
|
|
3158
3324
|
subagentSessions = new Set();
|
|
3159
3325
|
openToolCalls = new Map();
|
|
3326
|
+
/** Survives result settlement so a card-less result can still be labelled. */
|
|
3327
|
+
toolCallNames = new Map();
|
|
3160
3328
|
stats = {
|
|
3161
3329
|
turns: 0,
|
|
3162
3330
|
steps: 0,
|
|
@@ -3204,6 +3372,10 @@ export class SshTui {
|
|
|
3204
3372
|
quotaAlerted = new Set();
|
|
3205
3373
|
quotaStepsSinceRefresh = 0;
|
|
3206
3374
|
quotaRefreshInFlight = false;
|
|
3375
|
+
contextPressure;
|
|
3376
|
+
contextAlertLevel;
|
|
3377
|
+
idleCompactInFlight = false;
|
|
3378
|
+
lastIdleCompactAt = 0;
|
|
3207
3379
|
searchHits = [];
|
|
3208
3380
|
searchIndex = -1;
|
|
3209
3381
|
searchQuery = '';
|
|
@@ -3469,10 +3641,15 @@ export class SshTui {
|
|
|
3469
3641
|
addDel.del += stat.del;
|
|
3470
3642
|
}
|
|
3471
3643
|
}
|
|
3644
|
+
const singleEditPath = kind === 'edits' && groups.edits.length === 1
|
|
3645
|
+
? compactEditPath(groups.edits[0])
|
|
3646
|
+
: '';
|
|
3472
3647
|
const title = kind === 'edits'
|
|
3473
3648
|
? (groups.edits.length > 1
|
|
3474
3649
|
? t('compact.editsFiles', { files: groups.edits.length })
|
|
3475
|
-
:
|
|
3650
|
+
: singleEditPath === ''
|
|
3651
|
+
? t('compact.edits')
|
|
3652
|
+
: t('compact.editsFile', { path: singleEditPath }))
|
|
3476
3653
|
: (groups.failedCalls > 0
|
|
3477
3654
|
? t('compact.toolsFailed', { count: groups.calls.length, failed: groups.failedCalls })
|
|
3478
3655
|
: t('compact.tools', { count: groups.calls.length }));
|
|
@@ -3586,6 +3763,7 @@ export class SshTui {
|
|
|
3586
3763
|
this.streamingReasoning = undefined;
|
|
3587
3764
|
this.thinkingStartedAt = undefined;
|
|
3588
3765
|
this.status = this.agent.status === 'running' ? 'running' : 'idle';
|
|
3766
|
+
this.refreshContextPressure({ compact: false });
|
|
3589
3767
|
this.dirty = true;
|
|
3590
3768
|
}
|
|
3591
3769
|
/** Show the first-launch provider/API-key onboarding when nothing is configured. */
|
|
@@ -4053,11 +4231,24 @@ export class SshTui {
|
|
|
4053
4231
|
return parts.join(' · ');
|
|
4054
4232
|
}
|
|
4055
4233
|
mergeIntoToolCard(previous, next) {
|
|
4234
|
+
const ids = previous.mergedCallIds ?? [previous.callId];
|
|
4235
|
+
if (!ids.includes(previous.callId))
|
|
4236
|
+
ids.push(previous.callId);
|
|
4237
|
+
if (!ids.includes(next.callId))
|
|
4238
|
+
ids.push(next.callId);
|
|
4239
|
+
previous.mergedCallIds = ids;
|
|
4056
4240
|
previous.callId = next.callId;
|
|
4057
4241
|
previous.name = next.name;
|
|
4058
4242
|
previous.args = next.args;
|
|
4059
|
-
|
|
4060
|
-
|
|
4243
|
+
if (next.title !== '')
|
|
4244
|
+
previous.title = next.title;
|
|
4245
|
+
if (next.summary !== '')
|
|
4246
|
+
previous.summary = next.summary;
|
|
4247
|
+
if (next.diff !== undefined && next.diff.length > 0) {
|
|
4248
|
+
previous.diff = (previous.repeats ?? 1) > 1 && previous.diff !== undefined && previous.diff.length > 0
|
|
4249
|
+
? [...previous.diff, ...next.diff]
|
|
4250
|
+
: next.diff;
|
|
4251
|
+
}
|
|
4061
4252
|
previous.status = 'running';
|
|
4062
4253
|
previous.output = '';
|
|
4063
4254
|
previous.exitCode = undefined;
|
|
@@ -4066,6 +4257,14 @@ export class SshTui {
|
|
|
4066
4257
|
if (!this.replaying)
|
|
4067
4258
|
previous.flipUntil = Date.now() + TOOL_FLIP_MS;
|
|
4068
4259
|
}
|
|
4260
|
+
findToolRowByCallId(callId) {
|
|
4261
|
+
return this.rows.findLast((candidate) => candidate.kind === 'tool'
|
|
4262
|
+
&& (candidate.callId === callId || candidate.mergedCallIds?.includes(callId) === true));
|
|
4263
|
+
}
|
|
4264
|
+
findMergeableToolRow(next) {
|
|
4265
|
+
const previous = this.rows.findLast((candidate) => candidate.kind === 'tool');
|
|
4266
|
+
return canMergeToolCall(previous, next) ? previous : undefined;
|
|
4267
|
+
}
|
|
4069
4268
|
/** Append one transcript row, bounding memory on long sessions. */
|
|
4070
4269
|
pushRow(row) {
|
|
4071
4270
|
this.rows.push(row);
|
|
@@ -4224,15 +4423,18 @@ export class SshTui {
|
|
|
4224
4423
|
shouldDockPlan() {
|
|
4225
4424
|
return this.findLivePlanRow() !== undefined;
|
|
4226
4425
|
}
|
|
4227
|
-
/**
|
|
4228
|
-
|
|
4426
|
+
/** Send the leftover-todo nudge only from true idle, so /compact is not blocked. */
|
|
4427
|
+
flushPlanCloseNudge() {
|
|
4229
4428
|
if (this.replaying || this.agentGone || this.planNudgePending)
|
|
4230
4429
|
return;
|
|
4231
4430
|
if (this.agent.status === 'running')
|
|
4232
4431
|
return;
|
|
4432
|
+
const plan = this.findLivePlanRow();
|
|
4433
|
+
if (plan === undefined || plan.turnLeftOpen !== true)
|
|
4434
|
+
return;
|
|
4233
4435
|
this.planNudgePending = true;
|
|
4234
4436
|
const text = planCloseNudgeText(plan);
|
|
4235
|
-
this.pushRow({ kind: 'system', text: '
|
|
4437
|
+
this.pushRow({ kind: 'system', text: t('plan.nudgeQueued') });
|
|
4236
4438
|
const message = createUserMessage({
|
|
4237
4439
|
content: [{ type: 'text', text }],
|
|
4238
4440
|
source: { kind: 'user' },
|
|
@@ -4242,7 +4444,7 @@ export class SshTui {
|
|
|
4242
4444
|
}
|
|
4243
4445
|
catch (error) {
|
|
4244
4446
|
this.planNudgePending = false;
|
|
4245
|
-
this.pushRow({ kind: 'error', text:
|
|
4447
|
+
this.pushRow({ kind: 'error', text: t('plan.nudgeFailed', { error: errorChain(error) }) });
|
|
4246
4448
|
}
|
|
4247
4449
|
}
|
|
4248
4450
|
/** Compact web-style plan strip pinned above the input, not in the transcript. */
|
|
@@ -4661,7 +4863,7 @@ export class SshTui {
|
|
|
4661
4863
|
const header = buildToolHeader({
|
|
4662
4864
|
focused,
|
|
4663
4865
|
expanded: row.expanded,
|
|
4664
|
-
title: toolTitle(row.name)
|
|
4866
|
+
title: toolTitle(row.name),
|
|
4665
4867
|
summary: this.toolCardSummary(row),
|
|
4666
4868
|
status: row.status,
|
|
4667
4869
|
command: row.command,
|
|
@@ -5207,6 +5409,9 @@ export class SshTui {
|
|
|
5207
5409
|
...(quotaWindow === undefined || this.quotaSnapshot === undefined || this.quotaSnapshot.provider !== provider
|
|
5208
5410
|
? {}
|
|
5209
5411
|
: { quotaCode: this.quotaSnapshot.plan, quotaPercent: quotaWindow.remainingPercent }),
|
|
5412
|
+
...(this.contextPressure === undefined
|
|
5413
|
+
? {}
|
|
5414
|
+
: { contextChip: formatContextPressureChip(this.contextPressure, false) }),
|
|
5210
5415
|
...(balanceText === undefined ? {} : { balanceText }),
|
|
5211
5416
|
...(this.searchHits.length > 0 && this.searchIndex >= 0
|
|
5212
5417
|
? { search: { index: this.searchIndex, total: this.searchHits.length } }
|
|
@@ -5225,7 +5430,9 @@ export class SshTui {
|
|
|
5225
5430
|
: activity.text;
|
|
5226
5431
|
const identity = footerIdentityParts(footer);
|
|
5227
5432
|
const statusText = fitFooterStatusLine(activityText, identity, Math.max(1, width));
|
|
5228
|
-
const statusLine = this.
|
|
5433
|
+
const statusLine = this.contextPressure === undefined || !this.color
|
|
5434
|
+
? this.styleLine('system', statusText)
|
|
5435
|
+
: this.styleLine('system', statusText).replace(formatContextPressureRing(this.contextPressure.percent), `\x1b[${contextPressureRingColor(this.contextPressure.level)}m${formatContextPressureRing(this.contextPressure.percent)}\x1b[0m\x1b[90m`);
|
|
5229
5436
|
const paintRows = [
|
|
5230
5437
|
...headerLines,
|
|
5231
5438
|
...visible,
|
|
@@ -5512,6 +5719,7 @@ export class SshTui {
|
|
|
5512
5719
|
return;
|
|
5513
5720
|
}
|
|
5514
5721
|
this.lastActivity = Date.now();
|
|
5722
|
+
this.refreshContextPressure();
|
|
5515
5723
|
switch (event.type) {
|
|
5516
5724
|
case 'user/message': {
|
|
5517
5725
|
const text = event.data.content
|
|
@@ -5617,13 +5825,14 @@ export class SshTui {
|
|
|
5617
5825
|
}
|
|
5618
5826
|
case 'tool/call': {
|
|
5619
5827
|
this.openToolCalls.set(String(event.data.callId), event.data.name);
|
|
5828
|
+
this.toolCallNames.set(String(event.data.callId), event.data.name);
|
|
5620
5829
|
this.pendingToolTimes.set(String(event.data.callId), event.time);
|
|
5621
5830
|
if (!HIDDEN_TOOL_NAMES.has(event.data.name)) {
|
|
5622
5831
|
const present = presentToolCall(event.data.name, event.data.arguments);
|
|
5623
|
-
const previous = this.
|
|
5624
|
-
if (
|
|
5832
|
+
const previous = this.findMergeableToolRow({ name: event.data.name, args: event.data.arguments });
|
|
5833
|
+
if (previous !== undefined) {
|
|
5625
5834
|
this.mergeIntoToolCard(previous, {
|
|
5626
|
-
callId: event.data.callId,
|
|
5835
|
+
callId: String(event.data.callId),
|
|
5627
5836
|
name: event.data.name,
|
|
5628
5837
|
args: event.data.arguments,
|
|
5629
5838
|
title: present.title,
|
|
@@ -5634,7 +5843,7 @@ export class SshTui {
|
|
|
5634
5843
|
else {
|
|
5635
5844
|
const row = {
|
|
5636
5845
|
kind: 'tool',
|
|
5637
|
-
callId: event.data.callId,
|
|
5846
|
+
callId: String(event.data.callId),
|
|
5638
5847
|
name: event.data.name,
|
|
5639
5848
|
args: event.data.arguments,
|
|
5640
5849
|
status: 'running',
|
|
@@ -5665,7 +5874,8 @@ export class SshTui {
|
|
|
5665
5874
|
this.stats.toolMs += Math.max(0, event.time - dispatchedAt);
|
|
5666
5875
|
this.pendingToolTimes.delete(String(event.data.message.source.callId));
|
|
5667
5876
|
}
|
|
5668
|
-
const
|
|
5877
|
+
const callId = String(event.data.message.source.callId);
|
|
5878
|
+
const row = this.findToolRowByCallId(callId);
|
|
5669
5879
|
const output = collectText(event.data.message.content);
|
|
5670
5880
|
if (row !== undefined) {
|
|
5671
5881
|
const metaDiffs = diffMetaDiffs(event.data.meta);
|
|
@@ -5696,11 +5906,21 @@ export class SshTui {
|
|
|
5696
5906
|
}
|
|
5697
5907
|
}
|
|
5698
5908
|
else {
|
|
5699
|
-
const
|
|
5909
|
+
const sourceName = event.data.message.source.name;
|
|
5910
|
+
const recordedName = this.toolCallNames.get(callId)
|
|
5911
|
+
?? (typeof sourceName === 'string' ? sourceName : '');
|
|
5912
|
+
if (recordedName !== '' && HIDDEN_TOOL_NAMES.has(recordedName))
|
|
5913
|
+
break;
|
|
5914
|
+
// Never title a card with the call id (`call-<uuid>`). Prefer the
|
|
5915
|
+
// recorded tool name; fall back to a generic tool card.
|
|
5916
|
+
const toolName = recordedName === '' || recordedName.startsWith('call-')
|
|
5917
|
+
? 'tool'
|
|
5918
|
+
: recordedName;
|
|
5919
|
+
const present = presentToolCall(toolName, '');
|
|
5700
5920
|
this.pushRow({
|
|
5701
5921
|
kind: 'tool',
|
|
5702
|
-
callId
|
|
5703
|
-
name:
|
|
5922
|
+
callId,
|
|
5923
|
+
name: toolName,
|
|
5704
5924
|
args: '',
|
|
5705
5925
|
status: event.data.error === undefined ? 'ok' : 'error',
|
|
5706
5926
|
output,
|
|
@@ -5762,6 +5982,7 @@ export class SshTui {
|
|
|
5762
5982
|
case 'turn/end': {
|
|
5763
5983
|
const reason = event.data.reason;
|
|
5764
5984
|
this.openToolCalls.clear();
|
|
5985
|
+
this.toolCallNames.clear();
|
|
5765
5986
|
this.pendingToolTimes.clear();
|
|
5766
5987
|
this.stalledWarningShown = false;
|
|
5767
5988
|
this.pendingMessages.clear();
|
|
@@ -5793,7 +6014,9 @@ export class SshTui {
|
|
|
5793
6014
|
kind: 'system',
|
|
5794
6015
|
text: planDockNote(livePlan),
|
|
5795
6016
|
});
|
|
5796
|
-
|
|
6017
|
+
// Driver is still `running` while `turn/end` is appended. Wait for
|
|
6018
|
+
// idle so this follow-up does not make `/compact` report busy.
|
|
6019
|
+
queueMicrotask(() => this.flushPlanCloseNudge());
|
|
5797
6020
|
}
|
|
5798
6021
|
}
|
|
5799
6022
|
this.markDirty();
|
|
@@ -5824,6 +6047,10 @@ export class SshTui {
|
|
|
5824
6047
|
if (status !== 'running')
|
|
5825
6048
|
this.endWait();
|
|
5826
6049
|
this.status = status === 'running' ? 'running' : 'idle';
|
|
6050
|
+
if (status !== 'running') {
|
|
6051
|
+
this.flushPlanCloseNudge();
|
|
6052
|
+
this.maybeIdleAutoCompact();
|
|
6053
|
+
}
|
|
5827
6054
|
this.markDirty();
|
|
5828
6055
|
};
|
|
5829
6056
|
handleError = ({ agent, error }) => {
|
|
@@ -5938,7 +6165,7 @@ export class SshTui {
|
|
|
5938
6165
|
if (named !== undefined)
|
|
5939
6166
|
return named;
|
|
5940
6167
|
}
|
|
5941
|
-
return
|
|
6168
|
+
return undefined;
|
|
5942
6169
|
}
|
|
5943
6170
|
handleCompactionEvent(type, event) {
|
|
5944
6171
|
const payload = event.data;
|
|
@@ -5961,11 +6188,13 @@ export class SshTui {
|
|
|
5961
6188
|
const row = this.findCompactionRow(compactionId);
|
|
5962
6189
|
if (type === 'compaction/prune') {
|
|
5963
6190
|
const tokens = typeof data.shadowedTokenCount === 'number' ? data.shadowedTokenCount : 0;
|
|
6191
|
+
// Automatic tool-result prunes have no compactionId. Never attach them
|
|
6192
|
+
// to a leftover /compact card — that made the next /compact look failed.
|
|
5964
6193
|
if (row !== undefined) {
|
|
5965
6194
|
row.pruneCount += 1;
|
|
5966
6195
|
row.prunedTokens += Math.max(0, tokens);
|
|
6196
|
+
this.markDirty();
|
|
5967
6197
|
}
|
|
5968
|
-
this.markDirty();
|
|
5969
6198
|
return;
|
|
5970
6199
|
}
|
|
5971
6200
|
if (type === 'compaction/summary') {
|
|
@@ -5997,10 +6226,145 @@ export class SshTui {
|
|
|
5997
6226
|
text: error === undefined ? '上下文压缩已完成。' : `上下文压缩失败:${error}`,
|
|
5998
6227
|
});
|
|
5999
6228
|
}
|
|
6000
|
-
if (this.status.startsWith('压缩'))
|
|
6229
|
+
if (this.status.startsWith('压缩') || this.status.startsWith('compact')) {
|
|
6001
6230
|
this.status = this.agent.status === 'running' ? 'running' : 'idle';
|
|
6231
|
+
}
|
|
6232
|
+
this.idleCompactInFlight = false;
|
|
6233
|
+
this.markDirty();
|
|
6234
|
+
this.refreshContextPressure();
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
readContextPressure() {
|
|
6238
|
+
const projections = this.ctx.get('sessionProjections');
|
|
6239
|
+
const fromProjection = parseContextPressure(projections?.snapshot?.(this.agent.session)?.values?.contextPressure);
|
|
6240
|
+
if (fromProjection !== undefined)
|
|
6241
|
+
return contextPressureView(fromProjection);
|
|
6242
|
+
const requestContext = this.agent.session.requestContext?.();
|
|
6243
|
+
const window = typeof requestContext?.contextWindow === 'number' && requestContext.contextWindow > 0
|
|
6244
|
+
? requestContext.contextWindow
|
|
6245
|
+
: undefined;
|
|
6246
|
+
if (window === undefined)
|
|
6247
|
+
return undefined;
|
|
6248
|
+
const used = promptPressureTokens(this.stats.usage);
|
|
6249
|
+
if (used <= 0)
|
|
6250
|
+
return undefined;
|
|
6251
|
+
return contextPressureView({ usedTokens: used, contextWindow: window });
|
|
6252
|
+
}
|
|
6253
|
+
refreshContextPressure(options = {}) {
|
|
6254
|
+
const next = this.readContextPressure();
|
|
6255
|
+
const previous = this.contextPressure;
|
|
6256
|
+
this.contextPressure = next;
|
|
6257
|
+
if (this.replaying) {
|
|
6258
|
+
this.contextAlertLevel = next?.level === 'ok' ? undefined : next?.level;
|
|
6259
|
+
return;
|
|
6260
|
+
}
|
|
6261
|
+
if (next !== undefined && next.level !== 'ok' && next.level !== this.contextAlertLevel) {
|
|
6262
|
+
this.contextAlertLevel = next.level;
|
|
6263
|
+
this.pushRow({ kind: 'system', text: contextPressureAlertText(next) });
|
|
6264
|
+
}
|
|
6265
|
+
else if (next === undefined || next.level === 'ok') {
|
|
6266
|
+
this.contextAlertLevel = undefined;
|
|
6267
|
+
}
|
|
6268
|
+
if (previous?.usedTokens !== next?.usedTokens
|
|
6269
|
+
|| previous?.contextWindow !== next?.contextWindow
|
|
6270
|
+
|| previous?.level !== next?.level) {
|
|
6002
6271
|
this.markDirty();
|
|
6003
6272
|
}
|
|
6273
|
+
if (options.compact !== false && this.agent.status !== 'running')
|
|
6274
|
+
this.maybeIdleAutoCompact();
|
|
6275
|
+
}
|
|
6276
|
+
canRunCompactCommand() {
|
|
6277
|
+
if (this.replaying || this.agentGone || this.exiting)
|
|
6278
|
+
return false;
|
|
6279
|
+
if (this.agent.status === 'running')
|
|
6280
|
+
return false;
|
|
6281
|
+
if (this.idleCompactInFlight)
|
|
6282
|
+
return false;
|
|
6283
|
+
if (this.rows.some(row => row.kind === 'compaction' && row.status === 'running'))
|
|
6284
|
+
return false;
|
|
6285
|
+
return this.ctx.get('commands') !== undefined;
|
|
6286
|
+
}
|
|
6287
|
+
maybeIdleAutoCompact() {
|
|
6288
|
+
if (!this.canRunCompactCommand())
|
|
6289
|
+
return;
|
|
6290
|
+
if (!shouldIdleAutoCompact(this.contextPressure))
|
|
6291
|
+
return;
|
|
6292
|
+
if (Date.now() - this.lastIdleCompactAt < 8_000)
|
|
6293
|
+
return;
|
|
6294
|
+
this.dispatchCompactCommand('idle');
|
|
6295
|
+
}
|
|
6296
|
+
dispatchCompactCommand(reason) {
|
|
6297
|
+
const commands = this.ctx.get('commands');
|
|
6298
|
+
if (commands?.execute === undefined) {
|
|
6299
|
+
if (reason === 'user')
|
|
6300
|
+
this.pushRow({ kind: 'error', text: `Unknown command: /compact (try /help)` });
|
|
6301
|
+
return;
|
|
6302
|
+
}
|
|
6303
|
+
if (this.agent.status === 'running'
|
|
6304
|
+
|| this.rows.some(row => row.kind === 'compaction' && row.status === 'running')) {
|
|
6305
|
+
if (reason === 'user')
|
|
6306
|
+
this.pushRow({ kind: 'error', text: t('compact.busy') });
|
|
6307
|
+
this.markDirty();
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
this.idleCompactInFlight = true;
|
|
6311
|
+
this.lastIdleCompactAt = Date.now();
|
|
6312
|
+
if (reason === 'idle') {
|
|
6313
|
+
const view = this.contextPressure;
|
|
6314
|
+
this.pushRow({
|
|
6315
|
+
kind: 'system',
|
|
6316
|
+
text: view === undefined
|
|
6317
|
+
? t('context.autoCompact')
|
|
6318
|
+
: t('context.autoCompactAt', {
|
|
6319
|
+
used: formatTokens(view.usedTokens),
|
|
6320
|
+
window: formatTokens(view.contextWindow),
|
|
6321
|
+
percent: view.percent.toFixed(0),
|
|
6322
|
+
}),
|
|
6323
|
+
});
|
|
6324
|
+
}
|
|
6325
|
+
this.commandAbort?.abort();
|
|
6326
|
+
const controller = new AbortController();
|
|
6327
|
+
this.commandAbort = controller;
|
|
6328
|
+
void commands.execute(this.agent, '/compact', [], controller.signal).then((execution) => {
|
|
6329
|
+
if (execution === undefined) {
|
|
6330
|
+
this.idleCompactInFlight = false;
|
|
6331
|
+
if (reason === 'user')
|
|
6332
|
+
this.pushRow({ kind: 'error', text: `Unknown command: /compact (try /help)` });
|
|
6333
|
+
return;
|
|
6334
|
+
}
|
|
6335
|
+
const compactionRunning = () => this.rows.some(row => row.kind === 'compaction' && row.status === 'running');
|
|
6336
|
+
const releaseIfSettled = () => {
|
|
6337
|
+
queueMicrotask(() => {
|
|
6338
|
+
if (!compactionRunning())
|
|
6339
|
+
this.idleCompactInFlight = false;
|
|
6340
|
+
});
|
|
6341
|
+
};
|
|
6342
|
+
if (this.seenCommandDoneIds.has(String(execution.commandId))) {
|
|
6343
|
+
releaseIfSettled();
|
|
6344
|
+
return;
|
|
6345
|
+
}
|
|
6346
|
+
if (execution.result?.kind === 'error') {
|
|
6347
|
+
this.idleCompactInFlight = false;
|
|
6348
|
+
this.pushRow({
|
|
6349
|
+
kind: 'error',
|
|
6350
|
+
text: formatCompactCommandError(this.formatCommandText(String(execution.result.text ?? ''))),
|
|
6351
|
+
});
|
|
6352
|
+
}
|
|
6353
|
+
else if (typeof execution.result?.text === 'string' && execution.result.text !== '') {
|
|
6354
|
+
this.pushRow({ kind: 'system', text: this.formatCommandText(execution.result.text) });
|
|
6355
|
+
releaseIfSettled();
|
|
6356
|
+
}
|
|
6357
|
+
else {
|
|
6358
|
+
releaseIfSettled();
|
|
6359
|
+
}
|
|
6360
|
+
}).catch((error) => {
|
|
6361
|
+
this.idleCompactInFlight = false;
|
|
6362
|
+
this.pushRow({ kind: 'error', text: `/compact failed: ${errorChain(error)}` });
|
|
6363
|
+
}).finally(() => {
|
|
6364
|
+
if (this.commandAbort === controller)
|
|
6365
|
+
this.commandAbort = undefined;
|
|
6366
|
+
this.markDirty();
|
|
6367
|
+
});
|
|
6004
6368
|
}
|
|
6005
6369
|
handleCommandRun(data) {
|
|
6006
6370
|
const name = String(data?.name ?? '').trim();
|
|
@@ -6064,10 +6428,14 @@ export class SshTui {
|
|
|
6064
6428
|
const kind = typeof payload.kind === 'string' ? payload.kind : '';
|
|
6065
6429
|
const text = typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
6066
6430
|
if (kind === 'error') {
|
|
6067
|
-
const errText = this.formatCommandText(text);
|
|
6431
|
+
const errText = formatCompactCommandError(this.formatCommandText(text));
|
|
6068
6432
|
this.pushRow({ kind: 'error', text: errText === '' ? t('command.failed') : errText });
|
|
6069
|
-
if (this.status.startsWith('压缩'))
|
|
6433
|
+
if (this.status.startsWith('压缩') || this.status.startsWith('compact')) {
|
|
6070
6434
|
this.status = this.agent.status === 'running' ? 'running' : 'idle';
|
|
6435
|
+
}
|
|
6436
|
+
if (!this.rows.some(row => row.kind === 'compaction' && row.status === 'running')) {
|
|
6437
|
+
this.idleCompactInFlight = false;
|
|
6438
|
+
}
|
|
6071
6439
|
this.markDirty();
|
|
6072
6440
|
return;
|
|
6073
6441
|
}
|
|
@@ -6366,19 +6734,38 @@ export class SshTui {
|
|
|
6366
6734
|
source: { kind: 'plugin', plugin: 'dsh-ssh-tui' },
|
|
6367
6735
|
})],
|
|
6368
6736
|
system: REVIEW_SYSTEM_PROMPT,
|
|
6369
|
-
maxTokens:
|
|
6370
|
-
|
|
6737
|
+
maxTokens: 400,
|
|
6738
|
+
reasoningEffort: ReasoningEffortId('off'),
|
|
6371
6739
|
signal,
|
|
6372
6740
|
};
|
|
6373
6741
|
this.aiReviewCount += 1;
|
|
6374
6742
|
let text = '';
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6743
|
+
try {
|
|
6744
|
+
for await (const chunk of llm.stream(options)) {
|
|
6745
|
+
// Classifier reads the final assistant reply only. Reasoning/thinking
|
|
6746
|
+
// is ignored even when it happens to contain JSON.
|
|
6747
|
+
if (chunk.type === 'text-delta')
|
|
6748
|
+
text += chunk.text;
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
catch (error) {
|
|
6752
|
+
this.pushRow({
|
|
6753
|
+
kind: 'system',
|
|
6754
|
+
text: t('approval.reviewFailed', { error: errorChain(error) }),
|
|
6755
|
+
});
|
|
6756
|
+
this.markDirty();
|
|
6757
|
+
return undefined;
|
|
6378
6758
|
}
|
|
6379
6759
|
const verdict = parseReviewOutput(text);
|
|
6380
|
-
if (verdict === undefined)
|
|
6760
|
+
if (verdict === undefined) {
|
|
6761
|
+
const preview = text.trim() === '' ? t('approval.reviewNoReply') : text.trim();
|
|
6762
|
+
this.pushRow({
|
|
6763
|
+
kind: 'system',
|
|
6764
|
+
text: t('approval.reviewUnparsed', { output: preview.slice(0, 160) }),
|
|
6765
|
+
});
|
|
6766
|
+
this.markDirty();
|
|
6381
6767
|
return undefined;
|
|
6768
|
+
}
|
|
6382
6769
|
this.pushRow({
|
|
6383
6770
|
kind: 'system',
|
|
6384
6771
|
text: t('approval.reviewRow', {
|
|
@@ -6391,6 +6778,28 @@ export class SshTui {
|
|
|
6391
6778
|
this.markDirty();
|
|
6392
6779
|
return verdict.approved ? 'allow' : 'deny';
|
|
6393
6780
|
}
|
|
6781
|
+
recordAutoApproval(decision, risk, toolName, command, reason) {
|
|
6782
|
+
if (decision === 'allow')
|
|
6783
|
+
this.autoAllowedCount += 1;
|
|
6784
|
+
else
|
|
6785
|
+
this.autoDeniedCount += 1;
|
|
6786
|
+
const subject = (command ?? '').trim() !== ''
|
|
6787
|
+
? command.replace(/\s+/gu, ' ').trim()
|
|
6788
|
+
: toolName;
|
|
6789
|
+
const clipped = Array.from(subject).length > 160
|
|
6790
|
+
? `${Array.from(subject).slice(0, 160).join('')}…`
|
|
6791
|
+
: subject;
|
|
6792
|
+
this.pushRow({
|
|
6793
|
+
kind: 'system',
|
|
6794
|
+
text: t('approval.decisionRow', {
|
|
6795
|
+
verdict: decision === 'allow' ? t('approval.reviewApproved') : t('approval.reviewRejected'),
|
|
6796
|
+
command: clipped,
|
|
6797
|
+
risk,
|
|
6798
|
+
reason,
|
|
6799
|
+
}),
|
|
6800
|
+
});
|
|
6801
|
+
this.markDirty();
|
|
6802
|
+
}
|
|
6394
6803
|
handleApproval = async (request, _next) => {
|
|
6395
6804
|
// Auto mode classifies BEFORE waiting for a display, Codex-style: allow
|
|
6396
6805
|
// shapes approve, danger shapes REJECT (the model reads the rejection and
|
|
@@ -6400,15 +6809,19 @@ export class SshTui {
|
|
|
6400
6809
|
if (this.autoApprovalMode === 'auto') {
|
|
6401
6810
|
const row = request.callId === undefined
|
|
6402
6811
|
? undefined
|
|
6403
|
-
: this.
|
|
6404
|
-
const command =
|
|
6812
|
+
: this.findToolRowByCallId(String(request.callId));
|
|
6813
|
+
const command = commandForApprovalRequest({
|
|
6814
|
+
toolName: request.toolName,
|
|
6815
|
+
...(request.reason === undefined ? {} : { reason: request.reason }),
|
|
6816
|
+
...(row === undefined ? {} : { row: { name: row.name, args: row.args, ...(row.command === undefined ? {} : { command: row.command }) } }),
|
|
6817
|
+
});
|
|
6405
6818
|
const decision = classifyApproval(request.toolName, command);
|
|
6406
6819
|
if (decision === 'allow') {
|
|
6407
|
-
this.
|
|
6820
|
+
this.recordAutoApproval('allow', 'low', request.toolName, command, t('approval.ruleAllow'));
|
|
6408
6821
|
return 'allowed-once';
|
|
6409
6822
|
}
|
|
6410
6823
|
if (decision === 'deny') {
|
|
6411
|
-
this.
|
|
6824
|
+
this.recordAutoApproval('deny', 'high', request.toolName, command, t('approval.ruleDeny'));
|
|
6412
6825
|
return 'rejected';
|
|
6413
6826
|
}
|
|
6414
6827
|
// Unknown shape: the rule table cannot judge it — hand it to the
|
|
@@ -6420,10 +6833,14 @@ export class SshTui {
|
|
|
6420
6833
|
this.autoAllowedCount += 1;
|
|
6421
6834
|
return 'allowed-once';
|
|
6422
6835
|
}
|
|
6423
|
-
if (reviewed === 'deny'
|
|
6836
|
+
if (reviewed === 'deny') {
|
|
6424
6837
|
this.autoDeniedCount += 1;
|
|
6425
6838
|
return 'rejected';
|
|
6426
6839
|
}
|
|
6840
|
+
if (!this.hasLiveDisplay()) {
|
|
6841
|
+
this.recordAutoApproval('deny', 'medium', request.toolName, command, t('approval.ruleDetached'));
|
|
6842
|
+
return 'rejected';
|
|
6843
|
+
}
|
|
6427
6844
|
}
|
|
6428
6845
|
if (!this.hasLiveDisplay()) {
|
|
6429
6846
|
try {
|
|
@@ -7120,6 +7537,20 @@ export class SshTui {
|
|
|
7120
7537
|
const provider = this.effectiveSubagentProvider();
|
|
7121
7538
|
const current = this.subagentSelection.current;
|
|
7122
7539
|
const direct = arg.trim();
|
|
7540
|
+
if (direct.toLowerCase() === 'reset' || direct === '跟随' || direct === '默认') {
|
|
7541
|
+
const parentProvider = this.currentProviderId();
|
|
7542
|
+
const nextModel = defaultSubagentModelForProvider(parentProvider, [], this.selectionRef?.current?.model ?? this.agent.options.model);
|
|
7543
|
+
const persisted = await this.saveSubagentSelection({
|
|
7544
|
+
model: nextModel,
|
|
7545
|
+
reasoningEffort: undefined,
|
|
7546
|
+
});
|
|
7547
|
+
this.pushRow({
|
|
7548
|
+
kind: 'system',
|
|
7549
|
+
text: `子代理已恢复跟随父会话 ${parentProvider},模型改为 ${nextModel}${persisted ? '' : '(仅当前会话)'}。`,
|
|
7550
|
+
});
|
|
7551
|
+
this.markDirty();
|
|
7552
|
+
return;
|
|
7553
|
+
}
|
|
7123
7554
|
let selectedId = direct;
|
|
7124
7555
|
if (selectedId === '') {
|
|
7125
7556
|
const { options, source } = await this.subagentModelOptions(provider);
|
|
@@ -7410,7 +7841,7 @@ export class SshTui {
|
|
|
7410
7841
|
this.markDirty();
|
|
7411
7842
|
}
|
|
7412
7843
|
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
7413
|
-
async runModeCommand() {
|
|
7844
|
+
async runModeCommand(arg = '') {
|
|
7414
7845
|
const agentPresets = this.ctx.get('agentPresets');
|
|
7415
7846
|
if (agentPresets === undefined) {
|
|
7416
7847
|
this.pushRow({ kind: 'error', text: 'agentPresets 服务不可用。' });
|
|
@@ -7423,15 +7854,30 @@ export class SshTui {
|
|
|
7423
7854
|
this.markDirty();
|
|
7424
7855
|
return;
|
|
7425
7856
|
}
|
|
7426
|
-
const
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7857
|
+
const direct = arg.trim().toLowerCase();
|
|
7858
|
+
let selected = direct === ''
|
|
7859
|
+
? undefined
|
|
7860
|
+
: presets.find(preset => preset.id.toLowerCase() === direct
|
|
7861
|
+
|| (preset.name ?? '').toLowerCase() === direct);
|
|
7862
|
+
if (selected === undefined && direct !== '') {
|
|
7863
|
+
this.pushRow({
|
|
7864
|
+
kind: 'error',
|
|
7865
|
+
text: t('mode.unknown', { id: arg.trim(), available: presets.map(preset => preset.id).join(', ') }),
|
|
7866
|
+
});
|
|
7867
|
+
this.markDirty();
|
|
7868
|
+
return;
|
|
7869
|
+
}
|
|
7870
|
+
if (selected === undefined) {
|
|
7871
|
+
const answer = await this.askQuestion({
|
|
7872
|
+
id: 'mode-pick',
|
|
7873
|
+
question: '选择模式',
|
|
7874
|
+
options: presets.map(preset => ({
|
|
7875
|
+
label: preset.name ?? preset.id,
|
|
7876
|
+
description: `${preset.id === this.presetId ? '当前 · ' : ''}${preset.description ?? ''}`.trim(),
|
|
7877
|
+
})),
|
|
7878
|
+
});
|
|
7879
|
+
selected = presets.find(preset => (preset.name ?? preset.id) === answer.selected[0]);
|
|
7880
|
+
}
|
|
7435
7881
|
if (selected === undefined)
|
|
7436
7882
|
return;
|
|
7437
7883
|
const selectedName = selected.name ?? selected.id;
|
|
@@ -8948,7 +9394,7 @@ export class SshTui {
|
|
|
8948
9394
|
});
|
|
8949
9395
|
break;
|
|
8950
9396
|
case 'mode':
|
|
8951
|
-
void this.runModeCommand().catch((error) => {
|
|
9397
|
+
void this.runModeCommand(arg).catch((error) => {
|
|
8952
9398
|
if (error instanceof UserQuestionError) {
|
|
8953
9399
|
this.pushRow({ kind: 'system', text: t('help.modeCancel') });
|
|
8954
9400
|
}
|
|
@@ -9034,6 +9480,7 @@ export class SshTui {
|
|
|
9034
9480
|
disconnect: this.disconnectPolicy,
|
|
9035
9481
|
waitingQuestions: waiting,
|
|
9036
9482
|
...(quota === undefined ? {} : { quota }),
|
|
9483
|
+
...(this.contextPressure === undefined ? {} : { context: this.contextPressure }),
|
|
9037
9484
|
parentModel: model,
|
|
9038
9485
|
...(sub.provider === undefined ? {} : { subProvider: sub.provider }),
|
|
9039
9486
|
subModel: sub.model,
|
|
@@ -9103,8 +9550,8 @@ export class SshTui {
|
|
|
9103
9550
|
});
|
|
9104
9551
|
break;
|
|
9105
9552
|
case 'approval': {
|
|
9106
|
-
const requested = arg === '' ? 'toggle' : arg;
|
|
9107
|
-
if (requested
|
|
9553
|
+
const requested = arg.trim() === '' ? 'toggle' : arg.trim();
|
|
9554
|
+
if (isApprovalStatusArg(requested)) {
|
|
9108
9555
|
this.pushRow({
|
|
9109
9556
|
kind: 'system',
|
|
9110
9557
|
text: this.autoApprovalMode === 'auto'
|
|
@@ -9118,7 +9565,7 @@ export class SshTui {
|
|
|
9118
9565
|
? this.autoApprovalMode === 'auto' ? 'off' : 'auto'
|
|
9119
9566
|
: parseAutoApprovalMode(requested);
|
|
9120
9567
|
if (next === undefined) {
|
|
9121
|
-
this.pushRow({ kind: 'error', text: t('approval.unknown', { arg }) });
|
|
9568
|
+
this.pushRow({ kind: 'error', text: t('approval.unknown', { arg: requested }) });
|
|
9122
9569
|
this.markDirty();
|
|
9123
9570
|
break;
|
|
9124
9571
|
}
|
|
@@ -9168,6 +9615,10 @@ export class SshTui {
|
|
|
9168
9615
|
this.pushRow({ kind: 'error', text: `Unknown command: /${command} (try /help)` });
|
|
9169
9616
|
break;
|
|
9170
9617
|
}
|
|
9618
|
+
if (command === 'compact') {
|
|
9619
|
+
this.dispatchCompactCommand('user');
|
|
9620
|
+
break;
|
|
9621
|
+
}
|
|
9171
9622
|
this.commandAbort?.abort();
|
|
9172
9623
|
const controller = new AbortController();
|
|
9173
9624
|
this.commandAbort = controller;
|
|
@@ -9182,7 +9633,7 @@ export class SshTui {
|
|
|
9182
9633
|
if (this.seenCommandDoneIds.has(String(execution.commandId)))
|
|
9183
9634
|
return;
|
|
9184
9635
|
if (execution.result.kind === 'error') {
|
|
9185
|
-
this.pushRow({ kind: 'error', text: this.formatCommandText(execution.result.text) });
|
|
9636
|
+
this.pushRow({ kind: 'error', text: formatCompactCommandError(this.formatCommandText(execution.result.text)) });
|
|
9186
9637
|
}
|
|
9187
9638
|
else if (execution.result.text !== undefined && execution.result.text !== '') {
|
|
9188
9639
|
this.pushRow({ kind: 'system', text: this.formatCommandText(execution.result.text) });
|