dsh-ssh-tui 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +31 -8
- package/README.md +30 -7
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/picker.js +51 -7
- package/lib/picker.js.map +1 -1
- package/lib/tui.js +708 -104
- package/lib/tui.js.map +1 -1
- package/lib/types/picker.d.ts +6 -3
- package/lib/types/tui.d.ts +83 -7
- package/package.json +30 -28
package/lib/tui.js
CHANGED
|
@@ -206,7 +206,7 @@ const LOCAL_COMMANDS = [
|
|
|
206
206
|
{ name: 'status', description: 'show session, provider and model status' },
|
|
207
207
|
{ name: 'usage', description: 'show OpenCode Zen billing / Go quota usage' },
|
|
208
208
|
{ name: 'quota', description: 'alias of /usage for OpenCode Go quota' },
|
|
209
|
-
{ name: 'subagents', description: 'list active subagents' },
|
|
209
|
+
{ name: 'subagents', description: 'list active subagents; kill <id> to stop one' },
|
|
210
210
|
{ name: 'resume', description: 'resume a past session (empty = session picker)' },
|
|
211
211
|
{ name: 'setup', description: 're-open provider / API key setup' },
|
|
212
212
|
{ name: 'dialog-test', description: 'verify the question dialog' },
|
|
@@ -637,6 +637,15 @@ function cursorVisualPosition(text, cursor, width) {
|
|
|
637
637
|
}
|
|
638
638
|
return { row, col };
|
|
639
639
|
}
|
|
640
|
+
/** Build per-model reasoningEfforts from a provider-level reasoning default. */
|
|
641
|
+
function reasoningEffortsForDefault(reasoning) {
|
|
642
|
+
if (typeof reasoning !== 'string')
|
|
643
|
+
return undefined;
|
|
644
|
+
const level = reasoning.trim();
|
|
645
|
+
if (level === '' || level === 'off')
|
|
646
|
+
return undefined;
|
|
647
|
+
return { off: null, [level]: level };
|
|
648
|
+
}
|
|
640
649
|
const OPENCODE_GO_USAGE_URL = 'https://opencode.ai/zen/go/v1/usage';
|
|
641
650
|
const OPENCODE_ZEN_BASE_URL = 'https://opencode.ai/zen/v1';
|
|
642
651
|
/**
|
|
@@ -754,7 +763,7 @@ function openCodeApiErrorMessage(payload) {
|
|
|
754
763
|
return '';
|
|
755
764
|
}
|
|
756
765
|
/** Whether `text` could still grow into a recognized escape sequence. */
|
|
757
|
-
function isEscapePrefix(text) {
|
|
766
|
+
export function isEscapePrefix(text) {
|
|
758
767
|
if (text === '\x1b')
|
|
759
768
|
return true;
|
|
760
769
|
if (!text.startsWith('\x1b'))
|
|
@@ -767,7 +776,7 @@ function isEscapePrefix(text) {
|
|
|
767
776
|
return true;
|
|
768
777
|
if (/^\x1b\[[HF]$/u.test(text))
|
|
769
778
|
return true;
|
|
770
|
-
if (/^\x1b\[\d
|
|
779
|
+
if (/^\x1b\[\d+~?$/u.test(text))
|
|
771
780
|
return true;
|
|
772
781
|
if (/^\x1b\[<(?:\d*;?)*[Mm]?$/u.test(text))
|
|
773
782
|
return true;
|
|
@@ -804,6 +813,12 @@ function lastCodePoints(text, max) {
|
|
|
804
813
|
return '';
|
|
805
814
|
return Array.from(text).slice(-max).join('');
|
|
806
815
|
}
|
|
816
|
+
/** Format a model list compactly: show the first few entries and an ellipsis. */
|
|
817
|
+
function formatModelList(models, max = 5) {
|
|
818
|
+
const shown = models.slice(0, max);
|
|
819
|
+
const text = shown.join(', ');
|
|
820
|
+
return models.length > max ? `${text}…(共 ${models.length} 个)` : text;
|
|
821
|
+
}
|
|
807
822
|
/** Prefer the fields a human scans for; fall back to the first scalar pairs. */
|
|
808
823
|
function friendlyArgsSummary(name, args) {
|
|
809
824
|
const parsed = parseJsonArgs(args);
|
|
@@ -838,6 +853,89 @@ function friendlyArgsSummary(name, args) {
|
|
|
838
853
|
}
|
|
839
854
|
const SHELL_TOOL_NAMES = new Set(['bash', 'pwsh']);
|
|
840
855
|
const DIFF_TOOL_NAMES = new Set(['edit', 'write', 'str_replace_editor']);
|
|
856
|
+
const SUBAGENT_TOOL_NAMES = new Set(['subagent', 'subagent_fork', 'task']);
|
|
857
|
+
const MAX_SUBAGENT_LOGS = 80;
|
|
858
|
+
const TODO_STATUS_MARK = {
|
|
859
|
+
pending: '○',
|
|
860
|
+
in_progress: '◉',
|
|
861
|
+
completed: '✓',
|
|
862
|
+
};
|
|
863
|
+
/** Parse a todo_write payload into displayable plan items. */
|
|
864
|
+
export function parsePlanTodos(value) {
|
|
865
|
+
const root = typeof value === 'string' ? parseJsonArgs(value) : value;
|
|
866
|
+
const todos = root !== null && typeof root === 'object' && !Array.isArray(root)
|
|
867
|
+
? root.todos
|
|
868
|
+
: Array.isArray(root) ? root : undefined;
|
|
869
|
+
if (!Array.isArray(todos))
|
|
870
|
+
return [];
|
|
871
|
+
const out = [];
|
|
872
|
+
for (const item of todos) {
|
|
873
|
+
if (typeof item !== 'object' || item === null)
|
|
874
|
+
continue;
|
|
875
|
+
const content = typeof item.content === 'string'
|
|
876
|
+
? item.content.trim()
|
|
877
|
+
: '';
|
|
878
|
+
if (content === '')
|
|
879
|
+
continue;
|
|
880
|
+
const status = item.status;
|
|
881
|
+
out.push({
|
|
882
|
+
content,
|
|
883
|
+
status: status === 'in_progress' || status === 'completed' ? status : 'pending',
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
/** Compact todo-list summary: done/total plus the first in-progress task. */
|
|
889
|
+
export function todoSummary(value) {
|
|
890
|
+
const todos = parsePlanTodos(value);
|
|
891
|
+
if (todos.length === 0)
|
|
892
|
+
return '计划列表';
|
|
893
|
+
const done = todos.filter(item => item.status === 'completed').length;
|
|
894
|
+
const active = todos.find(item => item.status === 'in_progress');
|
|
895
|
+
const extra = todos.filter(item => item.status === 'in_progress').length;
|
|
896
|
+
const head = `${done}/${todos.length} 完成`;
|
|
897
|
+
if (active === undefined)
|
|
898
|
+
return head;
|
|
899
|
+
return extra > 1 ? `${head} · ${active.content} +${extra - 1}` : `${head} · ${active.content}`;
|
|
900
|
+
}
|
|
901
|
+
/** Compact ask_user_question summary from tool arguments. */
|
|
902
|
+
export function askSummary(value) {
|
|
903
|
+
const root = typeof value === 'string' ? parseJsonArgs(value) : value;
|
|
904
|
+
const questions = root !== null && typeof root === 'object' && !Array.isArray(root)
|
|
905
|
+
? root.questions
|
|
906
|
+
: undefined;
|
|
907
|
+
if (!Array.isArray(questions) || questions.length === 0)
|
|
908
|
+
return '等待回答';
|
|
909
|
+
const first = questions[0];
|
|
910
|
+
const text = typeof first === 'object' && first !== null && typeof first.question === 'string'
|
|
911
|
+
? first.question
|
|
912
|
+
: '等待回答';
|
|
913
|
+
return questions.length > 1 ? `${text}(${questions.length} 题)` : text;
|
|
914
|
+
}
|
|
915
|
+
/** One-line subagent card header used while collapsed. */
|
|
916
|
+
export function subagentHeaderText(row, now = Date.now()) {
|
|
917
|
+
const elapsed = Math.max(0, Math.floor(((row.endedAt ?? now) - row.startedAt) / 1000));
|
|
918
|
+
const elapsedLabel = elapsed >= 60 ? `${Math.floor(elapsed / 60)}m${elapsed % 60}s` : `${elapsed}s`;
|
|
919
|
+
const state = row.status === 'running'
|
|
920
|
+
? '运行中'
|
|
921
|
+
: row.status === 'ok'
|
|
922
|
+
? '完成'
|
|
923
|
+
: row.status === 'aborted'
|
|
924
|
+
? '已中断'
|
|
925
|
+
: '失败';
|
|
926
|
+
const activity = row.lastActivity === '' ? '' : ` · ${row.lastActivity}`;
|
|
927
|
+
const id = row.sessionId.slice(0, 8);
|
|
928
|
+
return `${row.label} [${id}] ${state} · ${elapsedLabel}${activity}`;
|
|
929
|
+
}
|
|
930
|
+
function appendSubagentLog(row, entry) {
|
|
931
|
+
row.logs.push(entry);
|
|
932
|
+
if (row.logs.length > MAX_SUBAGENT_LOGS)
|
|
933
|
+
row.logs.splice(0, row.logs.length - MAX_SUBAGENT_LOGS);
|
|
934
|
+
row.lastActivity = entry.text;
|
|
935
|
+
}
|
|
936
|
+
function planReviewOf(question) {
|
|
937
|
+
return question.intent?.kind === 'plan-review' && question.detail !== undefined && question.detail !== '';
|
|
938
|
+
}
|
|
841
939
|
/** Derive the intended file change from a mutation tool's arguments. */
|
|
842
940
|
function diffHunksFromArgs(name, argsRaw) {
|
|
843
941
|
const args = parseJsonArgs(argsRaw);
|
|
@@ -899,6 +997,22 @@ export function presentToolCall(name, args) {
|
|
|
899
997
|
...diff === null || diff === undefined ? {} : { diff },
|
|
900
998
|
};
|
|
901
999
|
}
|
|
1000
|
+
if (SUBAGENT_TOOL_NAMES.has(name)) {
|
|
1001
|
+
const description = typeof parsed?.description === 'string' ? parsed.description.trim() : '';
|
|
1002
|
+
return {
|
|
1003
|
+
title: name === 'subagent_fork' ? '子代理 fork' : '子代理',
|
|
1004
|
+
summary: description === '' ? friendlyArgsSummary(name, args) : description,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
if (name === 'todo_write' || name === 'todo') {
|
|
1008
|
+
return { title: '计划', summary: todoSummary(parsed) };
|
|
1009
|
+
}
|
|
1010
|
+
if (name === 'ask_user_question') {
|
|
1011
|
+
return { title: '提问用户', summary: askSummary(parsed) };
|
|
1012
|
+
}
|
|
1013
|
+
if (name === 'exit_plan_mode') {
|
|
1014
|
+
return { title: '退出计划模式', summary: '等待确认计划' };
|
|
1015
|
+
}
|
|
902
1016
|
return { title: name, summary: friendlyArgsSummary(name, args) };
|
|
903
1017
|
}
|
|
904
1018
|
/** Validate a tool/result meta payload's structured diff, mirroring the web card. */
|
|
@@ -1234,7 +1348,7 @@ export class SshTui {
|
|
|
1234
1348
|
this.useAlternateScreen = process.env.DSH_TUI_NO_ALT_SCREEN !== '1' && process.env.DSH_TUI_NO_ALT_SCREEN !== 'true';
|
|
1235
1349
|
this.pushRow({ kind: 'brand-logo' });
|
|
1236
1350
|
this.pushRow({ kind: 'system', text: 'DeepSeek Harness — SSH TUI' });
|
|
1237
|
-
this.pushRow({ kind: 'system', text: '
|
|
1351
|
+
this.pushRow({ kind: 'system', text: '输入 /help 查看命令 · /setup 配置提供商 · ↑/↓ 选择卡片 · Enter 展开/折叠 · Ctrl+R 全部展开/收起 · Ctrl+T 折叠输入 · Esc 取消' });
|
|
1238
1352
|
}
|
|
1239
1353
|
/** Enter raw mode, switch to the alternate screen, and start listening. */
|
|
1240
1354
|
start() {
|
|
@@ -1258,9 +1372,12 @@ export class SshTui {
|
|
|
1258
1372
|
const now = Date.now();
|
|
1259
1373
|
if (this.agent.status === 'running')
|
|
1260
1374
|
this.updateTerminalTitle();
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1375
|
+
const animating = (this.streaming !== undefined && this.streaming.reasoning !== '')
|
|
1376
|
+
|| this.activeSubagents.size > 0
|
|
1377
|
+
|| this.rows.some(row => (row.kind === 'question' && row.status === 'waiting')
|
|
1378
|
+
|| (row.kind === 'plan' && (row.active || row.pending || row.todos.some(item => item.status === 'in_progress')))
|
|
1379
|
+
|| (row.kind === 'goal' && (row.phase === 'active' || row.phase === 'blocked')));
|
|
1380
|
+
if (animating && now - this.lastPaintAt >= 200) {
|
|
1264
1381
|
this.dirty = true;
|
|
1265
1382
|
}
|
|
1266
1383
|
// While a turn is waiting on the provider with no new events, repaint at
|
|
@@ -1324,7 +1441,7 @@ export class SshTui {
|
|
|
1324
1441
|
const content = await readFile(credentialFile, 'utf8');
|
|
1325
1442
|
if (this.disposed)
|
|
1326
1443
|
return;
|
|
1327
|
-
stored = new RegExp(`^${envRef}\\s*:\\s*\\S`, 'm').test(content);
|
|
1444
|
+
stored = new RegExp(`^${escapeRegex(envRef)}\\s*:\\s*\\S`, 'm').test(content);
|
|
1328
1445
|
}
|
|
1329
1446
|
}
|
|
1330
1447
|
catch {
|
|
@@ -1484,13 +1601,60 @@ export class SshTui {
|
|
|
1484
1601
|
}
|
|
1485
1602
|
/** The transcript rows that support per-row expand/collapse. */
|
|
1486
1603
|
collapsibleRows() {
|
|
1487
|
-
const rows = this.rows.filter((row) => row.kind === 'reasoning'
|
|
1604
|
+
const rows = this.rows.filter((row) => row.kind === 'reasoning'
|
|
1605
|
+
|| row.kind === 'tool'
|
|
1606
|
+
|| row.kind === 'subagent'
|
|
1607
|
+
|| row.kind === 'plan'
|
|
1608
|
+
|| row.kind === 'question'
|
|
1609
|
+
|| row.kind === 'goal');
|
|
1488
1610
|
if (this.streaming !== undefined && this.streaming.reasoning !== '') {
|
|
1489
1611
|
this.streamingReasoning ??= { kind: 'streaming-reasoning', expanded: false };
|
|
1490
1612
|
rows.push(this.streamingReasoning);
|
|
1491
1613
|
}
|
|
1492
1614
|
return rows;
|
|
1493
1615
|
}
|
|
1616
|
+
spinnerFrame(periodMs = 120) {
|
|
1617
|
+
return SPINNER[Math.floor(Date.now() / periodMs) % SPINNER.length] ?? '⠋';
|
|
1618
|
+
}
|
|
1619
|
+
findSubagentRow(sessionId) {
|
|
1620
|
+
return this.rows.findLast((row) => row.kind === 'subagent' && row.sessionId === sessionId);
|
|
1621
|
+
}
|
|
1622
|
+
findLivePlanRow() {
|
|
1623
|
+
return this.rows.findLast((row) => row.kind === 'plan');
|
|
1624
|
+
}
|
|
1625
|
+
upsertPlanRow(patch) {
|
|
1626
|
+
const existing = this.findLivePlanRow();
|
|
1627
|
+
if (existing !== undefined) {
|
|
1628
|
+
Object.assign(existing, patch);
|
|
1629
|
+
return existing;
|
|
1630
|
+
}
|
|
1631
|
+
const row = {
|
|
1632
|
+
kind: 'plan',
|
|
1633
|
+
active: patch.active ?? false,
|
|
1634
|
+
pending: patch.pending ?? false,
|
|
1635
|
+
todos: patch.todos ?? [],
|
|
1636
|
+
expanded: false,
|
|
1637
|
+
};
|
|
1638
|
+
this.pushRow(row);
|
|
1639
|
+
return row;
|
|
1640
|
+
}
|
|
1641
|
+
paintCollapsibleHeader(addDisplay, row, kind, header, width, colorize) {
|
|
1642
|
+
const focused = this.focusedRow === row;
|
|
1643
|
+
const marker = row.expanded ? '▾' : '▸';
|
|
1644
|
+
const prefix = focused ? '▶ ' : ' ';
|
|
1645
|
+
const plain = `${prefix}${marker} ${header}`;
|
|
1646
|
+
const paint = colorize ?? ((line) => this.styleLine(kind, line));
|
|
1647
|
+
if (!row.expanded) {
|
|
1648
|
+
const collapsed = truncateToWidth(plain, Math.max(1, width - 2));
|
|
1649
|
+
const styled = paint(collapsed);
|
|
1650
|
+
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
for (const wrapped of wrap(plain, width)) {
|
|
1654
|
+
const styled = paint(wrapped);
|
|
1655
|
+
addDisplay(focused && this.color ? `\x1b[7m${styled}\x1b[27m` : styled, row);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1494
1658
|
/** Move the expand/collapse focus among reasoning and tool rows. */
|
|
1495
1659
|
moveCollapsibleFocus(delta) {
|
|
1496
1660
|
const rows = this.collapsibleRows();
|
|
@@ -1598,6 +1762,7 @@ export class SshTui {
|
|
|
1598
1762
|
return this.styleLine('tool', line);
|
|
1599
1763
|
return `${this.styleLine('tool', line.slice(0, dotIndex))}\x1b[${dotColor}m●${this.styleLine('tool', line.slice(dotIndex + 1))}`;
|
|
1600
1764
|
};
|
|
1765
|
+
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
1601
1766
|
const state = running ? 'running…' : ok ? 'ok' : 'error';
|
|
1602
1767
|
const summary = row.summary === '' ? '' : ` ${row.summary}`;
|
|
1603
1768
|
const exit = !running && row.command !== undefined
|
|
@@ -1609,7 +1774,7 @@ export class SshTui {
|
|
|
1609
1774
|
: '';
|
|
1610
1775
|
const focused = this.focusedRow === row;
|
|
1611
1776
|
const marker = row.expanded ? '▾' : '▸';
|
|
1612
|
-
const plainHeader = `${marker} ● ${row.title}${summary} [${state}]${exit}`;
|
|
1777
|
+
const plainHeader = `${marker} ● ${row.title}${summary} [${state}]${exit}${spinner}`;
|
|
1613
1778
|
if (!row.expanded) {
|
|
1614
1779
|
const collapsed = truncateToWidth(`${focused ? '▶ ' : ' '}${plainHeader}`, Math.max(1, width - 2));
|
|
1615
1780
|
const styled = styleToolHeader(collapsed);
|
|
@@ -1626,6 +1791,116 @@ export class SshTui {
|
|
|
1626
1791
|
}
|
|
1627
1792
|
continue;
|
|
1628
1793
|
}
|
|
1794
|
+
if (row.kind === 'subagent') {
|
|
1795
|
+
const running = row.status === 'running';
|
|
1796
|
+
const ok = row.status === 'ok';
|
|
1797
|
+
const dotColor = !this.color ? undefined : running ? '33' : ok ? '32' : '31';
|
|
1798
|
+
const styleHeader = (line) => {
|
|
1799
|
+
const dotIndex = line.indexOf('●');
|
|
1800
|
+
if (dotColor === undefined || dotIndex === -1)
|
|
1801
|
+
return this.styleLine('tool', line);
|
|
1802
|
+
return `${this.styleLine('tool', line.slice(0, dotIndex))}\x1b[${dotColor}m●${this.styleLine('tool', line.slice(dotIndex + 1))}`;
|
|
1803
|
+
};
|
|
1804
|
+
const spinner = running ? ` ${this.spinnerFrame()}` : '';
|
|
1805
|
+
const header = `● ${subagentHeaderText(row)}${spinner}${row.expanded ? '' : ' · Enter 展开'}`;
|
|
1806
|
+
this.paintCollapsibleHeader(addDisplay, row, 'tool', header, width, styleHeader);
|
|
1807
|
+
if (row.expanded) {
|
|
1808
|
+
addDisplay(this.styleLine('tool-result', ` 会话 ${row.sessionId} · ${row.provider}${row.local ? '' : ' · 外部进程'}`));
|
|
1809
|
+
if (row.stopReason !== undefined) {
|
|
1810
|
+
addDisplay(this.styleLine('tool-result', ` 结束原因:${row.stopReason}`));
|
|
1811
|
+
}
|
|
1812
|
+
if (row.logs.length === 0) {
|
|
1813
|
+
addDisplay(this.styleLine('tool-result', running ? ' 等待子代理输出…' : ' 没有可见输出'));
|
|
1814
|
+
}
|
|
1815
|
+
else {
|
|
1816
|
+
for (const entry of row.logs) {
|
|
1817
|
+
const kind = entry.kind === 'assistant'
|
|
1818
|
+
? 'assistant'
|
|
1819
|
+
: entry.kind === 'result' && row.status === 'error'
|
|
1820
|
+
? 'error'
|
|
1821
|
+
: 'tool-result';
|
|
1822
|
+
for (const wrapped of wrap(entry.text, Math.max(1, width - 2))) {
|
|
1823
|
+
addDisplay(this.styleLine(kind, ` ${wrapped}`));
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (row.kind === 'plan') {
|
|
1831
|
+
const running = row.todos.some(item => item.status === 'in_progress');
|
|
1832
|
+
const spinner = (row.active || row.pending || running) ? ` ${this.spinnerFrame()}` : '';
|
|
1833
|
+
const mode = row.pending
|
|
1834
|
+
? '切换中'
|
|
1835
|
+
: row.active
|
|
1836
|
+
? '计划模式'
|
|
1837
|
+
: '计划';
|
|
1838
|
+
const header = `● ${mode}${spinner} · ${todoSummary(row.todos)}${row.expanded ? '' : ' · Enter 展开'}`;
|
|
1839
|
+
this.paintCollapsibleHeader(addDisplay, row, 'system', header, width);
|
|
1840
|
+
if (row.expanded) {
|
|
1841
|
+
addDisplay(this.styleLine('system', row.active
|
|
1842
|
+
? ' 当前处于计划模式:只规划、不改代码,确认后再执行。'
|
|
1843
|
+
: ' 计划模式已关闭。可用 /plan 重新进入。'));
|
|
1844
|
+
if (row.pending)
|
|
1845
|
+
addDisplay(this.styleLine('system', ' 模式切换将在下一步生效。'));
|
|
1846
|
+
if (row.todos.length === 0) {
|
|
1847
|
+
addDisplay(this.styleLine('tool-result', ' 还没有任务列表'));
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
for (const item of row.todos) {
|
|
1851
|
+
const mark = TODO_STATUS_MARK[item.status];
|
|
1852
|
+
for (const wrapped of wrap(`${mark} ${item.content}`, Math.max(1, width - 2))) {
|
|
1853
|
+
addDisplay(this.styleLine(item.status === 'completed' ? 'system' : 'tool', ` ${wrapped}`));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (row.kind === 'question') {
|
|
1861
|
+
const waiting = row.status === 'waiting';
|
|
1862
|
+
const spinner = waiting ? ` ${this.spinnerFrame()}` : '';
|
|
1863
|
+
const state = waiting ? '等待回答' : row.status === 'answered' ? '已回答' : '已取消';
|
|
1864
|
+
const title = row.intent === 'plan-review' ? '计划待审' : '提问用户';
|
|
1865
|
+
const header = `● ${title}${spinner} · ${state} · ${row.summary}${row.expanded ? '' : ' · Enter 展开'}`;
|
|
1866
|
+
this.paintCollapsibleHeader(addDisplay, row, waiting ? 'tool' : 'system', header, width);
|
|
1867
|
+
if (row.expanded) {
|
|
1868
|
+
if (row.header !== undefined)
|
|
1869
|
+
addDisplay(this.styleLine('system', ` ${row.header}`));
|
|
1870
|
+
for (const wrapped of wrap(row.title, Math.max(1, width - 2))) {
|
|
1871
|
+
addDisplay(this.styleLine('assistant', ` ${wrapped}`));
|
|
1872
|
+
}
|
|
1873
|
+
if (row.detail !== undefined && row.detail !== '') {
|
|
1874
|
+
for (const wrapped of wrap(row.detail, Math.max(1, width - 2))) {
|
|
1875
|
+
addDisplay(this.styleLine('tool-result', ` ${wrapped}`));
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
addDisplay(this.styleLine('system', waiting
|
|
1879
|
+
? ' 用下方对话框选择,数字/字母选中,Enter 提交,Esc 取消。'
|
|
1880
|
+
: ` ${row.summary}`));
|
|
1881
|
+
}
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
if (row.kind === 'goal') {
|
|
1885
|
+
const live = row.phase === 'active' || row.phase === 'blocked';
|
|
1886
|
+
const spinner = live ? ` ${this.spinnerFrame()}` : '';
|
|
1887
|
+
const phase = row.phase === 'active' ? '进行中'
|
|
1888
|
+
: row.phase === 'paused' ? '已暂停'
|
|
1889
|
+
: row.phase === 'blocked' ? '受阻'
|
|
1890
|
+
: row.phase === 'complete' ? '已完成'
|
|
1891
|
+
: '已清除';
|
|
1892
|
+
const header = `● 目标${spinner} · ${phase} · ${row.objective}${row.expanded ? '' : ' · Enter 展开'}`;
|
|
1893
|
+
this.paintCollapsibleHeader(addDisplay, row, live ? 'tool' : 'system', header, width);
|
|
1894
|
+
if (row.expanded) {
|
|
1895
|
+
addDisplay(this.styleLine('system', ' 用 /goal 查看、暂停、恢复或清除当前目标。'));
|
|
1896
|
+
if (row.blockedReason !== undefined) {
|
|
1897
|
+
for (const wrapped of wrap(row.blockedReason, Math.max(1, width - 2))) {
|
|
1898
|
+
addDisplay(this.styleLine('error', ` ${wrapped}`));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1629
1904
|
pushRow(row.kind, row.text);
|
|
1630
1905
|
}
|
|
1631
1906
|
if (this.streaming !== undefined) {
|
|
@@ -1705,7 +1980,9 @@ export class SshTui {
|
|
|
1705
1980
|
case 'models':
|
|
1706
1981
|
addDialog(`提供商:${providerLabel}`);
|
|
1707
1982
|
addDialog('模型 ID(多个用逗号或空格分隔):');
|
|
1708
|
-
addDialog(
|
|
1983
|
+
addDialog(ob.models.length > 0
|
|
1984
|
+
? ` 已获取(${ob.models.length}):${formatModelList(ob.models, 6)}`
|
|
1985
|
+
: ` 默认:${template.defaultModels.join(', ')}`);
|
|
1709
1986
|
if (template.api !== undefined)
|
|
1710
1987
|
addDialog(' Ctrl+F = 从端点获取模型列表');
|
|
1711
1988
|
addDialog(' Enter 确认,Esc 取消');
|
|
@@ -1716,7 +1993,7 @@ export class SshTui {
|
|
|
1716
1993
|
addDialog(` Provider ID: ${ob.providerId}`);
|
|
1717
1994
|
addDialog(` Base URL: ${ob.baseUrl === '' ? (template.defaultBaseUrl || '(默认)') : ob.baseUrl}`);
|
|
1718
1995
|
addDialog(` API 协议: ${template.api ?? 'deepseek-official'}`);
|
|
1719
|
-
addDialog(` 模型: ${ob.models
|
|
1996
|
+
addDialog(` 模型: ${formatModelList(ob.models, 8)}`);
|
|
1720
1997
|
addDialog(` API Key: ${sliceCodePoints(ob.key, 6)}…${lastCodePoints(ob.key, 4)}(长度 ${ob.key.length})`);
|
|
1721
1998
|
addDialog(' y = 保存, n = 重填, Esc = 取消');
|
|
1722
1999
|
break;
|
|
@@ -1725,21 +2002,35 @@ export class SshTui {
|
|
|
1725
2002
|
}
|
|
1726
2003
|
else {
|
|
1727
2004
|
const d = this.dialog;
|
|
1728
|
-
|
|
1729
|
-
if (
|
|
1730
|
-
addDialog(
|
|
2005
|
+
const review = planReviewOf(d.question);
|
|
2006
|
+
if (review) {
|
|
2007
|
+
addDialog(`计划待审 ${d.index + 1}/${d.total}${d.question.header === undefined ? '' : ` · ${d.question.header}`}`);
|
|
2008
|
+
addDialog(d.question.question);
|
|
2009
|
+
if (d.question.detail !== undefined && d.question.detail !== '') {
|
|
2010
|
+
addDialog(truncate(d.question.detail, 12));
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
else {
|
|
2014
|
+
addDialog(`提问用户 ${d.index + 1}/${d.total}: ${d.question.question}`);
|
|
2015
|
+
if (d.question.header !== undefined && d.question.header !== '')
|
|
2016
|
+
addDialog(d.question.header);
|
|
2017
|
+
if (d.question.detail !== undefined && d.question.detail !== '') {
|
|
2018
|
+
addDialog(truncate(d.question.detail, 6));
|
|
2019
|
+
}
|
|
1731
2020
|
}
|
|
1732
2021
|
const options = d.question.options ?? [];
|
|
2022
|
+
const approve = d.question.intent?.approve;
|
|
1733
2023
|
for (const [index, option] of options.entries()) {
|
|
1734
2024
|
const marker = d.selected.has(index) ? '●' : '○';
|
|
1735
2025
|
const key = QUESTION_OPTION_KEYS[index] ?? '?';
|
|
2026
|
+
const recommended = option.label === approve ? '(推荐)' : '';
|
|
1736
2027
|
const extra = option.description === undefined ? '' : ` — ${option.description}`;
|
|
1737
|
-
addDialog(` ${key} ${marker} ${option.label}${extra}`);
|
|
2028
|
+
addDialog(` ${key} ${marker} ${option.label}${recommended}${extra}`);
|
|
1738
2029
|
}
|
|
1739
2030
|
if (options.length === 0) {
|
|
1740
|
-
addDialog('
|
|
2031
|
+
addDialog(' (自由输入:在下方输入后按 Enter)');
|
|
1741
2032
|
}
|
|
1742
|
-
addDialog(` ${d.question.multiSelect === true ? '
|
|
2033
|
+
addDialog(` ${d.question.multiSelect === true ? '数字/字母切换,Enter 提交' : '数字/字母选择,Enter 提交'},Esc 取消`);
|
|
1743
2034
|
}
|
|
1744
2035
|
}
|
|
1745
2036
|
const fitLine = (text) => truncateToWidth(text, Math.max(1, width));
|
|
@@ -1848,8 +2139,24 @@ export class SshTui {
|
|
|
1848
2139
|
if (this.pendingMessages.size > 0)
|
|
1849
2140
|
statusText += ` · 排队 ${this.pendingMessages.size}`;
|
|
1850
2141
|
const idleMs = Date.now() - this.lastActivity;
|
|
1851
|
-
|
|
1852
|
-
|
|
2142
|
+
const livePlan = this.findLivePlanRow();
|
|
2143
|
+
const waitingQuestions = this.rows.some(row => row.kind === 'question' && row.status === 'waiting');
|
|
2144
|
+
if (waitingQuestions || this.dialog?.kind === 'questions') {
|
|
2145
|
+
statusText += this.dialog?.kind === 'questions' && planReviewOf(this.dialog.question)
|
|
2146
|
+
? ' · 计划待审'
|
|
2147
|
+
: ' · 等待用户回答';
|
|
2148
|
+
}
|
|
2149
|
+
else if (livePlan?.active === true || livePlan?.pending === true) {
|
|
2150
|
+
statusText += livePlan.pending ? ' · 计划模式切换中' : ' · 计划模式';
|
|
2151
|
+
}
|
|
2152
|
+
const liveGoal = this.rows.findLast((row) => row.kind === 'goal');
|
|
2153
|
+
if (liveGoal !== undefined && (liveGoal.phase === 'active' || liveGoal.phase === 'paused' || liveGoal.phase === 'blocked')) {
|
|
2154
|
+
const phase = liveGoal.phase === 'active' ? '目标进行中' : liveGoal.phase === 'paused' ? '目标已暂停' : '目标受阻';
|
|
2155
|
+
statusText += ` · ${phase}`;
|
|
2156
|
+
}
|
|
2157
|
+
if (this.activeSubagents.size > 0) {
|
|
2158
|
+
const spinner = this.spinnerFrame(160);
|
|
2159
|
+
statusText += ` · ${spinner} 子代理 ${this.activeSubagents.size}`;
|
|
1853
2160
|
}
|
|
1854
2161
|
else if (this.agent.status === 'running' && this.openToolCalls.size > 0) {
|
|
1855
2162
|
statusText += ` · 工具执行中 ${this.openToolCalls.size}`;
|
|
@@ -1885,6 +2192,9 @@ export class SshTui {
|
|
|
1885
2192
|
this.pendingMessages.size,
|
|
1886
2193
|
this.commandSuggestions.length,
|
|
1887
2194
|
this.suggestionIndex,
|
|
2195
|
+
this.activeSubagents.size,
|
|
2196
|
+
this.dialog?.kind ?? '',
|
|
2197
|
+
this.findLivePlanRow()?.active === true ? 'plan' : '',
|
|
1888
2198
|
].join('\x1f');
|
|
1889
2199
|
const chromeChanged = chromeKey !== this.lastChromeKey;
|
|
1890
2200
|
// Incremental repaint: rewrite only rows whose content changed, so slow
|
|
@@ -1914,7 +2224,7 @@ export class SshTui {
|
|
|
1914
2224
|
const prefix = input.slice(1).toLowerCase();
|
|
1915
2225
|
const dsh = (this.ctx.get('commands')?.list(this.agent) ?? []).map(command => ({
|
|
1916
2226
|
name: command.name,
|
|
1917
|
-
description: command.description,
|
|
2227
|
+
description: command.input?.images === true ? `${command.description}(可附图)` : command.description,
|
|
1918
2228
|
local: false,
|
|
1919
2229
|
}));
|
|
1920
2230
|
const all = [
|
|
@@ -2004,10 +2314,25 @@ export class SshTui {
|
|
|
2004
2314
|
this.lastTitleUpdateAt = now;
|
|
2005
2315
|
const spinner = SPINNER[Math.floor(now / 800) % SPINNER.length];
|
|
2006
2316
|
let detail = '运行中';
|
|
2007
|
-
if (this.
|
|
2008
|
-
detail =
|
|
2009
|
-
|
|
2317
|
+
if (this.dialog?.kind === 'questions') {
|
|
2318
|
+
detail = planReviewOf(this.dialog.question) ? '计划待审' : '等待用户回答';
|
|
2319
|
+
}
|
|
2320
|
+
else if (this.activeSubagents.size > 0) {
|
|
2010
2321
|
detail = `运行中 · 子代理 ${this.activeSubagents.size}`;
|
|
2322
|
+
}
|
|
2323
|
+
else if (this.openToolCalls.size > 0) {
|
|
2324
|
+
detail = `运行中 · 工具 ${this.openToolCalls.size}`;
|
|
2325
|
+
}
|
|
2326
|
+
else if (this.findLivePlanRow()?.active === true) {
|
|
2327
|
+
detail = '计划模式';
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
const liveGoal = this.rows.findLast((row) => row.kind === 'goal');
|
|
2331
|
+
if (liveGoal?.phase === 'active')
|
|
2332
|
+
detail = '目标进行中';
|
|
2333
|
+
else if (liveGoal?.phase === 'blocked')
|
|
2334
|
+
detail = '目标受阻';
|
|
2335
|
+
}
|
|
2011
2336
|
this.write(`\x1b]0;dsh ${spinner} ${detail}\x07`);
|
|
2012
2337
|
return;
|
|
2013
2338
|
}
|
|
@@ -2158,14 +2483,20 @@ export class SshTui {
|
|
|
2158
2483
|
this.recordUsage(event.data.turn, event.data.step, event.data.usage);
|
|
2159
2484
|
}
|
|
2160
2485
|
const reasoningExpanded = this.streamingReasoning?.expanded ?? false;
|
|
2486
|
+
const interrupted = event.data.interrupted === true;
|
|
2161
2487
|
this.streaming = undefined;
|
|
2162
2488
|
this.streamingReasoning = undefined;
|
|
2163
2489
|
this.thinkingStartedAt = undefined;
|
|
2490
|
+
const interruptedMark = interrupted ? ' ⚠ 已中断' : '';
|
|
2164
2491
|
if (reasoning !== '') {
|
|
2165
|
-
this.pushRow({ kind: 'reasoning', text: reasoning
|
|
2492
|
+
this.pushRow({ kind: 'reasoning', text: `${reasoning}${interruptedMark}`, expanded: reasoningExpanded });
|
|
2493
|
+
}
|
|
2494
|
+
if (text !== '') {
|
|
2495
|
+
this.pushRow({ kind: 'assistant', text: `${text}${interruptedMark}` });
|
|
2496
|
+
}
|
|
2497
|
+
else if (interrupted && reasoning === '') {
|
|
2498
|
+
this.pushRow({ kind: 'system', text: '本轮输出已中断,没有可见内容。' });
|
|
2166
2499
|
}
|
|
2167
|
-
if (text !== '')
|
|
2168
|
-
this.pushRow({ kind: 'assistant', text });
|
|
2169
2500
|
this.markDirty();
|
|
2170
2501
|
break;
|
|
2171
2502
|
}
|
|
@@ -2185,7 +2516,7 @@ export class SshTui {
|
|
|
2185
2516
|
...present.command === undefined ? {} : { command: present.command },
|
|
2186
2517
|
...present.cwd === undefined ? {} : { cwd: present.cwd },
|
|
2187
2518
|
...present.diff === undefined ? {} : { diff: present.diff },
|
|
2188
|
-
expanded: DIFF_TOOL_NAMES.has(event.data.name),
|
|
2519
|
+
expanded: DIFF_TOOL_NAMES.has(event.data.name) && !SUBAGENT_TOOL_NAMES.has(event.data.name),
|
|
2189
2520
|
};
|
|
2190
2521
|
this.pushRow(row);
|
|
2191
2522
|
this.streaming = undefined;
|
|
@@ -2298,7 +2629,13 @@ export class SshTui {
|
|
|
2298
2629
|
this.markDirty();
|
|
2299
2630
|
break;
|
|
2300
2631
|
}
|
|
2632
|
+
case 'todo/write': {
|
|
2633
|
+
this.upsertPlanRow({ todos: parsePlanTodos(event.data.todos) });
|
|
2634
|
+
this.markDirty();
|
|
2635
|
+
break;
|
|
2636
|
+
}
|
|
2301
2637
|
default:
|
|
2638
|
+
this.handleExtensionEvent(event);
|
|
2302
2639
|
break;
|
|
2303
2640
|
}
|
|
2304
2641
|
};
|
|
@@ -2308,6 +2645,7 @@ export class SshTui {
|
|
|
2308
2645
|
this.lastActivity = Date.now();
|
|
2309
2646
|
if (status === 'running') {
|
|
2310
2647
|
this.completionSignaled = false;
|
|
2648
|
+
this.completedAt = 0;
|
|
2311
2649
|
}
|
|
2312
2650
|
else if (!this.completionSignaled && this.status === 'running') {
|
|
2313
2651
|
this.completionSignaled = true;
|
|
@@ -2345,56 +2683,187 @@ export class SshTui {
|
|
|
2345
2683
|
this.status = 'disposed';
|
|
2346
2684
|
this.markDirty();
|
|
2347
2685
|
};
|
|
2348
|
-
/**
|
|
2686
|
+
/** Plan-mode / command / team events that plugins merge into SessionEventMap. */
|
|
2687
|
+
handleExtensionEvent(event) {
|
|
2688
|
+
const type = String(event.type);
|
|
2689
|
+
const data = event.data;
|
|
2690
|
+
if (type === 'plan/mode') {
|
|
2691
|
+
const active = data?.active === true;
|
|
2692
|
+
this.upsertPlanRow({ active, pending: false });
|
|
2693
|
+
this.pushRow({
|
|
2694
|
+
kind: 'system',
|
|
2695
|
+
text: active
|
|
2696
|
+
? '已进入计划模式:先规划、等确认后再改代码。可用 /plan off 退出。'
|
|
2697
|
+
: '已退出计划模式,可以继续执行改动。',
|
|
2698
|
+
});
|
|
2699
|
+
this.markDirty();
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (type === 'command/run' && data?.name === 'plan') {
|
|
2703
|
+
const args = String(data.args ?? '').trim();
|
|
2704
|
+
const wantsActive = args !== 'off';
|
|
2705
|
+
const current = this.findLivePlanRow();
|
|
2706
|
+
this.upsertPlanRow({
|
|
2707
|
+
pending: current !== undefined && current.active !== wantsActive,
|
|
2708
|
+
active: current?.active ?? false,
|
|
2709
|
+
});
|
|
2710
|
+
this.pushRow({
|
|
2711
|
+
kind: 'system',
|
|
2712
|
+
text: wantsActive ? '已请求进入计划模式。' : '已请求退出计划模式。',
|
|
2713
|
+
});
|
|
2714
|
+
this.markDirty();
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
if (type === 'goal/change') {
|
|
2718
|
+
this.handleGoalChange(data);
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
if (type.startsWith('team/')) {
|
|
2722
|
+
this.pushRow({ kind: 'system', text: `[团队] ${type}` });
|
|
2723
|
+
this.markDirty();
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
handleGoalChange(data) {
|
|
2727
|
+
const payload = data !== null && typeof data === 'object' ? data : {};
|
|
2728
|
+
const existing = this.rows.findLast((row) => row.kind === 'goal');
|
|
2729
|
+
if (payload.operation === 'clear') {
|
|
2730
|
+
if (existing !== undefined) {
|
|
2731
|
+
existing.phase = 'cleared';
|
|
2732
|
+
existing.blockedReason = undefined;
|
|
2733
|
+
}
|
|
2734
|
+
else {
|
|
2735
|
+
this.pushRow({ kind: 'goal', objective: '(已清除)', phase: 'cleared', expanded: false });
|
|
2736
|
+
}
|
|
2737
|
+
this.pushRow({ kind: 'system', text: '当前目标已清除。' });
|
|
2738
|
+
this.markDirty();
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const goal = payload.goal !== null && typeof payload.goal === 'object' ? payload.goal : {};
|
|
2742
|
+
const objective = typeof goal.objective === 'string' && goal.objective.trim() !== '' ? goal.objective.trim() : '(未命名目标)';
|
|
2743
|
+
const phase = goal.phase === 'paused' || goal.phase === 'blocked' || goal.phase === 'complete' ? goal.phase : 'active';
|
|
2744
|
+
const blocked = goal.blockedReason !== null && typeof goal.blockedReason === 'object'
|
|
2745
|
+
? goal.blockedReason.message
|
|
2746
|
+
: undefined;
|
|
2747
|
+
const blockedReason = typeof blocked === 'string' ? blocked : undefined;
|
|
2748
|
+
if (existing !== undefined) {
|
|
2749
|
+
existing.objective = objective;
|
|
2750
|
+
existing.phase = phase;
|
|
2751
|
+
existing.blockedReason = blockedReason;
|
|
2752
|
+
}
|
|
2753
|
+
else {
|
|
2754
|
+
this.pushRow({
|
|
2755
|
+
kind: 'goal',
|
|
2756
|
+
objective,
|
|
2757
|
+
phase,
|
|
2758
|
+
...(blockedReason === undefined ? {} : { blockedReason }),
|
|
2759
|
+
expanded: false,
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
const notice = phase === 'active' ? '已设置目标'
|
|
2763
|
+
: phase === 'paused' ? '目标已暂停'
|
|
2764
|
+
: phase === 'blocked' ? '目标受阻'
|
|
2765
|
+
: '目标已完成';
|
|
2766
|
+
this.pushRow({ kind: 'system', text: `${notice}:${objective}` });
|
|
2767
|
+
this.markDirty();
|
|
2768
|
+
}
|
|
2769
|
+
handleSubagentExtensionEvent(row, event) {
|
|
2770
|
+
const type = String(event.type);
|
|
2771
|
+
const data = event.data;
|
|
2772
|
+
if (type === 'plan/mode') {
|
|
2773
|
+
appendSubagentLog(row, {
|
|
2774
|
+
kind: 'system',
|
|
2775
|
+
text: data?.active === true ? '进入计划模式' : '退出计划模式',
|
|
2776
|
+
});
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
if (type.startsWith('team/')) {
|
|
2780
|
+
appendSubagentLog(row, { kind: 'team', text: `[团队] ${type}` });
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
/** Fold a live subagent's own session events into that child's card. */
|
|
2349
2784
|
handleSubagentSessionEvent = (sessionId, event) => {
|
|
2350
|
-
const
|
|
2785
|
+
const row = this.findSubagentRow(String(sessionId));
|
|
2786
|
+
if (row === undefined)
|
|
2787
|
+
return;
|
|
2351
2788
|
switch (event.type) {
|
|
2352
2789
|
case 'user/message': {
|
|
2353
2790
|
const text = collectText(event.data.content);
|
|
2354
2791
|
if (text !== '')
|
|
2355
|
-
|
|
2792
|
+
appendSubagentLog(row, { kind: 'user', text: `❯ ${truncate(text, 4)}` });
|
|
2356
2793
|
break;
|
|
2357
2794
|
}
|
|
2358
|
-
case 'assistant/chunk':
|
|
2359
|
-
// Child chunks are coalesced into assistant/message to avoid flooding.
|
|
2795
|
+
case 'assistant/chunk':
|
|
2360
2796
|
break;
|
|
2361
|
-
}
|
|
2362
2797
|
case 'assistant/message': {
|
|
2363
2798
|
const text = collectText(event.data.message.content);
|
|
2364
2799
|
if (text !== '')
|
|
2365
|
-
|
|
2800
|
+
appendSubagentLog(row, { kind: 'assistant', text: truncate(text, 8) });
|
|
2366
2801
|
break;
|
|
2367
2802
|
}
|
|
2368
|
-
case 'tool/call':
|
|
2369
|
-
|
|
2803
|
+
case 'tool/call': {
|
|
2804
|
+
const present = presentToolCall(event.data.name, event.data.arguments);
|
|
2805
|
+
appendSubagentLog(row, { kind: 'tool', text: `▶ ${present.title} ${present.summary}` });
|
|
2370
2806
|
break;
|
|
2807
|
+
}
|
|
2371
2808
|
case 'tool/result': {
|
|
2372
|
-
const output = truncate(collectText(event.data.message.content),
|
|
2373
|
-
const ok = event.data.error === undefined &&
|
|
2374
|
-
|
|
2809
|
+
const output = truncate(collectText(event.data.message.content), 3);
|
|
2810
|
+
const ok = event.data.error === undefined && event.data.message.content[0]?.isError !== true;
|
|
2811
|
+
appendSubagentLog(row, {
|
|
2812
|
+
kind: 'result',
|
|
2813
|
+
text: `${ok ? '✓' : '✗'} ${event.data.message.source.callId}${output === '' ? '' : ` · ${output}`}`,
|
|
2814
|
+
});
|
|
2375
2815
|
break;
|
|
2376
2816
|
}
|
|
2377
2817
|
case 'turn/end':
|
|
2378
|
-
|
|
2818
|
+
appendSubagentLog(row, { kind: 'turn', text: `轮次结束(${event.data.reason.kind})` });
|
|
2379
2819
|
break;
|
|
2380
2820
|
case 'approval/asked':
|
|
2381
|
-
|
|
2821
|
+
appendSubagentLog(row, { kind: 'approval', text: `等待审批:${event.data.toolName}` });
|
|
2382
2822
|
break;
|
|
2383
2823
|
default:
|
|
2824
|
+
this.handleSubagentExtensionEvent(row, event);
|
|
2384
2825
|
break;
|
|
2385
2826
|
}
|
|
2386
2827
|
this.lastActivity = Date.now();
|
|
2387
2828
|
this.markDirty();
|
|
2388
2829
|
};
|
|
2389
2830
|
handleSubagentStart = (info) => {
|
|
2831
|
+
const sessionId = String(info.id);
|
|
2390
2832
|
this.activeSubagents.set(String(info.runId), {
|
|
2391
|
-
id:
|
|
2833
|
+
id: sessionId,
|
|
2392
2834
|
provider: info.provider,
|
|
2393
2835
|
startedAt: Date.now(),
|
|
2394
2836
|
});
|
|
2395
|
-
this.subagentSessions.add(
|
|
2837
|
+
this.subagentSessions.add(sessionId);
|
|
2396
2838
|
this.lastActivity = Date.now();
|
|
2397
|
-
|
|
2839
|
+
const existing = this.findSubagentRow(sessionId);
|
|
2840
|
+
if (existing !== undefined) {
|
|
2841
|
+
existing.runId = String(info.runId);
|
|
2842
|
+
existing.provider = info.provider;
|
|
2843
|
+
existing.local = info.local;
|
|
2844
|
+
existing.status = 'running';
|
|
2845
|
+
existing.startedAt = Date.now();
|
|
2846
|
+
existing.endedAt = undefined;
|
|
2847
|
+
existing.stopReason = undefined;
|
|
2848
|
+
existing.lastActivity = '已启动';
|
|
2849
|
+
existing.expanded = false;
|
|
2850
|
+
appendSubagentLog(existing, { kind: 'system', text: `已启动(${info.provider}${info.local ? '' : ',外部进程'})` });
|
|
2851
|
+
}
|
|
2852
|
+
else {
|
|
2853
|
+
this.pushRow({
|
|
2854
|
+
kind: 'subagent',
|
|
2855
|
+
sessionId,
|
|
2856
|
+
runId: String(info.runId),
|
|
2857
|
+
provider: info.provider,
|
|
2858
|
+
local: info.local,
|
|
2859
|
+
label: `子代理 ${info.provider}`,
|
|
2860
|
+
status: 'running',
|
|
2861
|
+
startedAt: Date.now(),
|
|
2862
|
+
lastActivity: '已启动',
|
|
2863
|
+
logs: [{ kind: 'system', text: `已启动(${info.provider}${info.local ? '' : ',外部进程'})` }],
|
|
2864
|
+
expanded: false,
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2398
2867
|
this.markDirty();
|
|
2399
2868
|
};
|
|
2400
2869
|
handleSubagentEnd = (info) => {
|
|
@@ -2404,10 +2873,34 @@ export class SshTui {
|
|
|
2404
2873
|
const output = info.lastAssistantMessage === undefined
|
|
2405
2874
|
? ''
|
|
2406
2875
|
: truncate(collectText(info.lastAssistantMessage), 6);
|
|
2407
|
-
this.
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2876
|
+
const row = this.findSubagentRow(String(info.id)) ?? this.rows.findLast((candidate) => candidate.kind === 'subagent' && candidate.runId === String(info.runId));
|
|
2877
|
+
const failed = info.stopReason !== 'completed';
|
|
2878
|
+
if (row !== undefined) {
|
|
2879
|
+
row.status = info.stopReason === 'aborted' ? 'aborted' : failed ? 'error' : 'ok';
|
|
2880
|
+
row.endedAt = Date.now();
|
|
2881
|
+
row.stopReason = info.stopReason;
|
|
2882
|
+
appendSubagentLog(row, {
|
|
2883
|
+
kind: failed ? 'result' : 'assistant',
|
|
2884
|
+
text: `结束(${info.stopReason})${output === '' ? '' : ` · ${output}`}`,
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
this.pushRow({
|
|
2889
|
+
kind: 'subagent',
|
|
2890
|
+
sessionId: String(info.id),
|
|
2891
|
+
runId: String(info.runId),
|
|
2892
|
+
provider: info.provider,
|
|
2893
|
+
local: info.local,
|
|
2894
|
+
label: `子代理 ${info.provider}`,
|
|
2895
|
+
status: info.stopReason === 'aborted' ? 'aborted' : failed ? 'error' : 'ok',
|
|
2896
|
+
startedAt: Date.now(),
|
|
2897
|
+
endedAt: Date.now(),
|
|
2898
|
+
stopReason: info.stopReason,
|
|
2899
|
+
lastActivity: `结束(${info.stopReason})`,
|
|
2900
|
+
logs: [{ kind: 'system', text: `结束(${info.stopReason})${output === '' ? '' : ` · ${output}`}` }],
|
|
2901
|
+
expanded: false,
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2411
2904
|
this.markDirty();
|
|
2412
2905
|
};
|
|
2413
2906
|
// ── approval and questions ──────────────────────────────────────────────
|
|
@@ -2438,38 +2931,77 @@ export class SshTui {
|
|
|
2438
2931
|
const agentLabel = request.agent === undefined || request.agent.id === this.agent.id
|
|
2439
2932
|
? undefined
|
|
2440
2933
|
: `子代理 ${request.agent.id}`;
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2934
|
+
const cards = [];
|
|
2935
|
+
for (const question of request.questions) {
|
|
2936
|
+
const card = {
|
|
2937
|
+
kind: 'question',
|
|
2938
|
+
questionId: question.id,
|
|
2939
|
+
title: question.question,
|
|
2940
|
+
...(question.header === undefined ? {} : { header: question.header }),
|
|
2941
|
+
...(question.detail === undefined ? {} : { detail: question.detail }),
|
|
2942
|
+
intent: planReviewOf(question) ? 'plan-review' : 'ask',
|
|
2943
|
+
status: 'waiting',
|
|
2944
|
+
summary: question.question,
|
|
2945
|
+
expanded: false,
|
|
2946
|
+
};
|
|
2947
|
+
cards.push(card);
|
|
2948
|
+
this.pushRow(card);
|
|
2949
|
+
}
|
|
2950
|
+
this.markDirty();
|
|
2951
|
+
const settleCards = (status, summary) => {
|
|
2952
|
+
for (const card of cards) {
|
|
2953
|
+
if (card.status === 'waiting') {
|
|
2954
|
+
card.status = status;
|
|
2955
|
+
card.summary = summary;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
try {
|
|
2960
|
+
for (const [index, question] of request.questions.entries()) {
|
|
2961
|
+
const answer = await new Promise((resolve, reject) => {
|
|
2962
|
+
const fail = (error) => {
|
|
2963
|
+
request.signal?.removeEventListener('abort', onAbort);
|
|
2964
|
+
reject(error);
|
|
2965
|
+
};
|
|
2966
|
+
const onAbort = () => {
|
|
2967
|
+
request.signal?.removeEventListener('abort', onAbort);
|
|
2968
|
+
if (dialog !== undefined) {
|
|
2969
|
+
dialog.reject(new UserQuestionError('ask_user_question was interrupted before the user answered', 'ASK_ABORTED'));
|
|
2970
|
+
}
|
|
2971
|
+
else {
|
|
2972
|
+
reject(new UserQuestionError('ask_user_question was interrupted before the user answered', 'ASK_ABORTED'));
|
|
2973
|
+
}
|
|
2974
|
+
};
|
|
2975
|
+
let dialog;
|
|
2976
|
+
request.signal?.addEventListener('abort', onAbort, { once: true });
|
|
2977
|
+
if (request.signal?.aborted === true) {
|
|
2978
|
+
onAbort();
|
|
2979
|
+
return;
|
|
2454
2980
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2981
|
+
const labeled = agentLabel === undefined
|
|
2982
|
+
? question
|
|
2983
|
+
: { ...question, question: `[${agentLabel}] ${question.question}` };
|
|
2984
|
+
dialog = this.openQuestion(labeled, index, request.questions.length, (selection) => {
|
|
2985
|
+
request.signal?.removeEventListener('abort', onAbort);
|
|
2986
|
+
resolve(selection);
|
|
2987
|
+
}, fail);
|
|
2988
|
+
});
|
|
2989
|
+
answers.push({ id: question.id, selected: answer.selected, custom: answer.custom });
|
|
2990
|
+
const card = cards[index];
|
|
2991
|
+
if (card !== undefined) {
|
|
2992
|
+
card.status = 'answered';
|
|
2993
|
+
card.summary = answer.custom !== undefined && answer.custom !== ''
|
|
2994
|
+
? answer.custom
|
|
2995
|
+
: answer.selected.join(', ') || '已回答';
|
|
2461
2996
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
});
|
|
2470
|
-
answers.push({ id: question.id, selected: answer.selected, custom: answer.custom });
|
|
2997
|
+
}
|
|
2998
|
+
settleCards('answered', '已回答');
|
|
2999
|
+
return { answers };
|
|
3000
|
+
}
|
|
3001
|
+
catch (error) {
|
|
3002
|
+
settleCards('cancelled', error instanceof UserQuestionError ? error.message : '已取消');
|
|
3003
|
+
throw error;
|
|
2471
3004
|
}
|
|
2472
|
-
return { answers };
|
|
2473
3005
|
};
|
|
2474
3006
|
/** Queue one dialog behind an already-open one instead of overwriting it. */
|
|
2475
3007
|
openDialog(dialog) {
|
|
@@ -2623,9 +3155,13 @@ export class SshTui {
|
|
|
2623
3155
|
}
|
|
2624
3156
|
if (ids.has(modelId))
|
|
2625
3157
|
return true;
|
|
3158
|
+
const modelEntry = { id: modelId };
|
|
3159
|
+
const reasoningEfforts = reasoningEffortsForDefault(profile.reasoning);
|
|
3160
|
+
if (reasoningEfforts !== undefined)
|
|
3161
|
+
modelEntry.reasoningEfforts = reasoningEfforts;
|
|
2626
3162
|
try {
|
|
2627
3163
|
await settings.mutate(settingsNamespace('llm-pi-ai'), [
|
|
2628
|
-
{ op: 'set', path: ['providers', provider, 'models'], value: [...models,
|
|
3164
|
+
{ op: 'set', path: ['providers', provider, 'models'], value: [...models, modelEntry] },
|
|
2629
3165
|
]);
|
|
2630
3166
|
this.pushRow({ kind: 'system', text: `模型 ${modelId} 已加入提供商 ${provider} 的配置。` });
|
|
2631
3167
|
this.markDirty();
|
|
@@ -2758,17 +3294,21 @@ export class SshTui {
|
|
|
2758
3294
|
effortOptions = [];
|
|
2759
3295
|
}
|
|
2760
3296
|
if (effortOptions.length === 0) {
|
|
2761
|
-
|
|
3297
|
+
// No selectable reasoning effort for this model: do not invent
|
|
3298
|
+
// `off/high/max`, which the adapter may reject for the exact model.
|
|
3299
|
+
}
|
|
3300
|
+
let effort;
|
|
3301
|
+
if (effortOptions.length > 0) {
|
|
3302
|
+
const effortAnswer = await this.askQuestion({
|
|
3303
|
+
id: 'effort-pick',
|
|
3304
|
+
question: `选择思考强度(${selected.id})`,
|
|
3305
|
+
options: effortOptions.map(option => ({
|
|
3306
|
+
label: option.label,
|
|
3307
|
+
description: option.id === String(current?.reasoningEffort) ? '当前' : undefined,
|
|
3308
|
+
})),
|
|
3309
|
+
});
|
|
3310
|
+
effort = effortOptions.find(option => option.label === effortAnswer.selected[0])?.id;
|
|
2762
3311
|
}
|
|
2763
|
-
const effortAnswer = await this.askQuestion({
|
|
2764
|
-
id: 'effort-pick',
|
|
2765
|
-
question: `选择思考强度(${selected.id})`,
|
|
2766
|
-
options: effortOptions.map(option => ({
|
|
2767
|
-
label: option.label,
|
|
2768
|
-
description: option.id === String(current?.reasoningEffort) ? '当前' : undefined,
|
|
2769
|
-
})),
|
|
2770
|
-
});
|
|
2771
|
-
const effort = effortOptions.find(option => option.label === effortAnswer.selected[0])?.id;
|
|
2772
3312
|
const next = {
|
|
2773
3313
|
provider,
|
|
2774
3314
|
model: selected.id,
|
|
@@ -2778,7 +3318,10 @@ export class SshTui {
|
|
|
2778
3318
|
this.selectionRef.current = next;
|
|
2779
3319
|
this.onSelectionChanged?.(next);
|
|
2780
3320
|
await this.ctx.get('agentDefaultModel')?.saveSelection(next);
|
|
2781
|
-
this.pushRow({
|
|
3321
|
+
this.pushRow({
|
|
3322
|
+
kind: 'system',
|
|
3323
|
+
text: `模型已切换:${selected.id}(思考强度 ${effort ?? '默认'}${effortOptions.length === 0 ? ',该模型未声明可选强度' : ''});下一步请求生效。`,
|
|
3324
|
+
});
|
|
2782
3325
|
this.markDirty();
|
|
2783
3326
|
}
|
|
2784
3327
|
/** Provider route the next subagent request should use. */
|
|
@@ -2885,8 +3428,13 @@ export class SshTui {
|
|
|
2885
3428
|
catch {
|
|
2886
3429
|
effortOptions = [];
|
|
2887
3430
|
}
|
|
2888
|
-
if (effortOptions.length === 0) {
|
|
2889
|
-
|
|
3431
|
+
if (effortOptions.length === 0 && current.reasoningEffort === undefined) {
|
|
3432
|
+
this.pushRow({
|
|
3433
|
+
kind: 'system',
|
|
3434
|
+
text: `模型 ${provider}/${current.model} 未声明可选 reasoning effort,已保持提供商默认;请勿手动设置 high/max。`,
|
|
3435
|
+
});
|
|
3436
|
+
this.markDirty();
|
|
3437
|
+
return;
|
|
2890
3438
|
}
|
|
2891
3439
|
const choices = [
|
|
2892
3440
|
{ id: undefined, label: SUBAGENT_DEFAULT_EFFORT_LABEL },
|
|
@@ -3657,7 +4205,7 @@ export class SshTui {
|
|
|
3657
4205
|
state.models = ids;
|
|
3658
4206
|
this.input = '';
|
|
3659
4207
|
this.cursor = 0;
|
|
3660
|
-
this.pushRow({ kind: 'system', text: `已从端点获取 ${ids.length}
|
|
4208
|
+
this.pushRow({ kind: 'system', text: `已从端点获取 ${ids.length} 个模型:${formatModelList(ids, 6)}(Enter 确认,也可继续修改)。` });
|
|
3661
4209
|
}
|
|
3662
4210
|
}
|
|
3663
4211
|
catch (error) {
|
|
@@ -3709,12 +4257,18 @@ export class SshTui {
|
|
|
3709
4257
|
const defaultEffort = model !== undefined && llm !== undefined
|
|
3710
4258
|
? await defaultReasoningEffort(llm, state.providerId, model)
|
|
3711
4259
|
: undefined;
|
|
4260
|
+
const reasoningEfforts = defaultEffort === undefined
|
|
4261
|
+
? undefined
|
|
4262
|
+
: { off: null, [defaultEffort]: defaultEffort };
|
|
3712
4263
|
const profile = {
|
|
3713
4264
|
displayName: template.label,
|
|
3714
4265
|
apiKeyEnv: envRef,
|
|
3715
4266
|
api: template.api,
|
|
3716
4267
|
baseURL: state.baseUrl === '' ? template.defaultBaseUrl : state.baseUrl,
|
|
3717
|
-
models: state.models.map(id => ({
|
|
4268
|
+
models: state.models.map(id => ({
|
|
4269
|
+
id,
|
|
4270
|
+
...(reasoningEfforts === undefined ? {} : { reasoningEfforts }),
|
|
4271
|
+
})),
|
|
3718
4272
|
...(defaultEffort === undefined ? {} : { reasoning: defaultEffort }),
|
|
3719
4273
|
};
|
|
3720
4274
|
if (settings === undefined) {
|
|
@@ -3786,7 +4340,6 @@ export class SshTui {
|
|
|
3786
4340
|
const home = dshHomeDir();
|
|
3787
4341
|
const file = join(home, IS_WINDOWS ? 'env.cmd' : 'env.sh');
|
|
3788
4342
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
3789
|
-
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
3790
4343
|
if (IS_WINDOWS) {
|
|
3791
4344
|
let previous = '';
|
|
3792
4345
|
try {
|
|
@@ -3844,6 +4397,8 @@ export class SshTui {
|
|
|
3844
4397
|
}
|
|
3845
4398
|
/** Idempotently source $DSH_HOME/env.sh from the user's POSIX shell rc files. */
|
|
3846
4399
|
async ensurePosixEnvHook() {
|
|
4400
|
+
if (process.env.DSH_TUI_NO_RC_HOOK === '1' || process.env.DSH_TUI_NO_RC_HOOK === 'true')
|
|
4401
|
+
return;
|
|
3847
4402
|
const envFile = join(dshHomeDir(), 'env.sh');
|
|
3848
4403
|
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
3849
4404
|
const sourceLine = `[ -f ${quote(envFile)} ] && . ${quote(envFile)}`;
|
|
@@ -3988,14 +4543,16 @@ export class SshTui {
|
|
|
3988
4543
|
.filter(item => item.name !== 'help' && item.name !== 'exit')
|
|
3989
4544
|
.map(item => `/${item.name.padEnd(12)} ${item.description}`);
|
|
3990
4545
|
const dsh = (this.ctx.get('commands')?.list(this.agent) ?? [])
|
|
3991
|
-
.map(item => `/${item.name.padEnd(12)} ${item.description} (dsh)`);
|
|
4546
|
+
.map(item => `/${item.name.padEnd(12)} ${item.description}${item.input?.images === true ? '(可附图)' : ''} (dsh)`);
|
|
3992
4547
|
this.pushRow({
|
|
3993
4548
|
kind: 'system',
|
|
3994
4549
|
text: [
|
|
3995
4550
|
...local,
|
|
3996
4551
|
...dsh,
|
|
3997
4552
|
'',
|
|
3998
|
-
'Enter
|
|
4553
|
+
'运行中按 Enter 可插入指示;Esc / Ctrl+C 取消当前轮次。',
|
|
4554
|
+
'↑/↓ 或 Ctrl+N/P 选择思考、工具、子代理、计划或提问卡片;Enter 展开/折叠;Ctrl+R 全部展开或收起。',
|
|
4555
|
+
'计划模式、提问用户和当前目标会显示独立卡片;多个子代理默认各自折叠,互不混排。',
|
|
3999
4556
|
].join('\n'),
|
|
4000
4557
|
});
|
|
4001
4558
|
break;
|
|
@@ -4054,12 +4611,24 @@ export class SshTui {
|
|
|
4054
4611
|
this.streamingReasoning = undefined;
|
|
4055
4612
|
this.thinkingStartedAt = undefined;
|
|
4056
4613
|
this.focusedRow = null;
|
|
4614
|
+
this.pushRow({ kind: 'system', text: '转录已清空。子代理、计划与提问卡片会在新事件到达时重新出现。' });
|
|
4057
4615
|
break;
|
|
4058
4616
|
case 'status':
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4617
|
+
{
|
|
4618
|
+
const plan = this.findLivePlanRow();
|
|
4619
|
+
const waiting = this.rows.filter(row => row.kind === 'question' && row.status === 'waiting').length;
|
|
4620
|
+
const lines = [
|
|
4621
|
+
`session: ${this.agent.id}`,
|
|
4622
|
+
`model: ${this.agent.options.model ?? 'default'}`,
|
|
4623
|
+
`provider: ${this.agent.options.provider ?? 'default'}`,
|
|
4624
|
+
`status: ${this.agent.status}`,
|
|
4625
|
+
`preset: ${this.presetName}`,
|
|
4626
|
+
`subagents: ${this.activeSubagents.size}`,
|
|
4627
|
+
`plan: ${plan === undefined ? 'off' : plan.pending ? 'pending' : plan.active ? 'on' : 'off'}`,
|
|
4628
|
+
waiting > 0 ? `questions: waiting ${waiting}` : 'questions: none',
|
|
4629
|
+
];
|
|
4630
|
+
this.pushRow({ kind: 'system', text: lines.join('\n') });
|
|
4631
|
+
}
|
|
4063
4632
|
break;
|
|
4064
4633
|
case 'usage':
|
|
4065
4634
|
case 'quota':
|
|
@@ -4069,12 +4638,43 @@ export class SshTui {
|
|
|
4069
4638
|
});
|
|
4070
4639
|
break;
|
|
4071
4640
|
case 'subagents': {
|
|
4641
|
+
const trimmed = arg.trim();
|
|
4642
|
+
if (trimmed !== '' && trimmed !== 'list') {
|
|
4643
|
+
const [action, ...ids] = trimmed.split(/\s+/u);
|
|
4644
|
+
if (action === 'kill' || action === 'stop') {
|
|
4645
|
+
if (ids.length === 0) {
|
|
4646
|
+
this.pushRow({ kind: 'error', text: '/subagents kill <session-id> — 缺少子代理会话 ID' });
|
|
4647
|
+
break;
|
|
4648
|
+
}
|
|
4649
|
+
const subagents = this.ctx.get('subagents');
|
|
4650
|
+
if (subagents === undefined) {
|
|
4651
|
+
this.pushRow({ kind: 'error', text: 'subagents service is unavailable' });
|
|
4652
|
+
break;
|
|
4653
|
+
}
|
|
4654
|
+
const targets = ids.map(id => SessionId(id));
|
|
4655
|
+
void subagents.drainContinuableChildren(this.agent, targets).then(() => {
|
|
4656
|
+
this.pushRow({ kind: 'system', text: `已请求释放子代理:${ids.join(', ')}` });
|
|
4657
|
+
this.markDirty();
|
|
4658
|
+
}).catch((error) => {
|
|
4659
|
+
this.pushRow({ kind: 'error', text: `/subagents kill failed: ${errorChain(error)}` });
|
|
4660
|
+
this.markDirty();
|
|
4661
|
+
});
|
|
4662
|
+
break;
|
|
4663
|
+
}
|
|
4664
|
+
this.pushRow({ kind: 'error', text: `/subagents 未知操作 "${action}"(支持 list / kill <id>)` });
|
|
4665
|
+
break;
|
|
4666
|
+
}
|
|
4072
4667
|
if (this.activeSubagents.size === 0) {
|
|
4073
4668
|
this.pushRow({ kind: 'system', text: '当前没有活动的子代理。' });
|
|
4074
4669
|
}
|
|
4075
4670
|
else {
|
|
4076
|
-
const lines = [...this.activeSubagents.entries()].map(([runId, sub]) =>
|
|
4077
|
-
|
|
4671
|
+
const lines = [...this.activeSubagents.entries()].map(([runId, sub]) => {
|
|
4672
|
+
const card = this.findSubagentRow(sub.id);
|
|
4673
|
+
const label = card?.label ?? sub.id;
|
|
4674
|
+
const activity = card?.lastActivity ? ` · ${card.lastActivity}` : '';
|
|
4675
|
+
return `▶ ${label} ${sub.id}(${sub.provider})运行 ${Math.floor((Date.now() - sub.startedAt) / 1000)}s [${runId.slice(0, 8)}]${activity}`;
|
|
4676
|
+
});
|
|
4677
|
+
this.pushRow({ kind: 'system', text: `${lines.join('\n')}\n↑/↓ 选择对应卡片,Enter 展开/折叠,Ctrl+R 全部展开或收起。` });
|
|
4078
4678
|
}
|
|
4079
4679
|
break;
|
|
4080
4680
|
}
|
|
@@ -4124,7 +4724,7 @@ export class SshTui {
|
|
|
4124
4724
|
this.commandAbort?.abort();
|
|
4125
4725
|
const controller = new AbortController();
|
|
4126
4726
|
this.commandAbort = controller;
|
|
4127
|
-
void commands.execute(this.agent, text, controller.signal).then((execution) => {
|
|
4727
|
+
void commands.execute(this.agent, text, [], controller.signal).then((execution) => {
|
|
4128
4728
|
if (execution === undefined) {
|
|
4129
4729
|
this.pushRow({ kind: 'error', text: `Unknown command: /${command} (try /help)` });
|
|
4130
4730
|
return;
|
|
@@ -4247,6 +4847,10 @@ export class SshTui {
|
|
|
4247
4847
|
function optionsLength(dialog) {
|
|
4248
4848
|
return dialog.kind === 'questions' ? dialog.question.options?.length ?? 0 : 0;
|
|
4249
4849
|
}
|
|
4850
|
+
/** Escape a string for safe interpolation into a RegExp source. */
|
|
4851
|
+
function escapeRegex(value) {
|
|
4852
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
4853
|
+
}
|
|
4250
4854
|
function envRefForId(providerId) {
|
|
4251
4855
|
return `${providerId.replaceAll('-', '_').toUpperCase()}_API_KEY`;
|
|
4252
4856
|
}
|