dsh-code 0.4.0 → 0.5.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.md +201 -61
- package/README.zh.md +204 -65
- package/bin/deepseek.mjs +70 -0
- package/cordis.patch.yml +62 -6
- package/lib/index.mjs +1108 -250
- package/lib/startup.mjs +34 -17
- package/lib/types/app.d.ts +19 -1
- package/lib/types/commands.d.ts +2 -0
- package/lib/types/index.d.ts +5 -5
- package/lib/types/internals.d.ts +2 -0
- package/lib/types/kernel-panels.d.ts +23 -0
- package/lib/types/plugin-inventory.d.ts +11 -0
- package/lib/types/presets.d.ts +32 -0
- package/lib/types/render/inspector.d.ts +4 -0
- package/lib/types/render/status.d.ts +2 -0
- package/lib/types/render/text.d.ts +9 -0
- package/lib/types/session-directory.d.ts +54 -0
- package/lib/types/session-switch.d.ts +17 -0
- package/lib/types/skills.d.ts +2 -0
- package/lib/types/startup.d.ts +11 -1
- package/package.json +6 -1
- package/src/app.ts +311 -75
- package/src/commands.ts +15 -1
- package/src/index.ts +332 -133
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +254 -0
- package/src/plugin-inventory.ts +47 -0
- package/src/presets.ts +64 -0
- package/src/render/inspector.ts +13 -4
- package/src/render/markdown.ts +15 -1
- package/src/render/status.ts +3 -0
- package/src/render/text.ts +34 -6
- package/src/session-directory.ts +102 -0
- package/src/session-switch.ts +58 -0
- package/src/skills.ts +20 -7
- package/src/startup.ts +38 -20
- package/src/pictures/1.png +0 -0
package/lib/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { readdir, writeFile } from "node:fs/promises";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
4
|
+
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import { createElement, memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
6
|
import z from "@deepseek-ai/schemastery";
|
|
7
7
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
@@ -879,14 +879,24 @@ const ORDERED = /^\s*(\d+)[.)]\s+(.*)$/u;
|
|
|
879
879
|
/** Render markdown text into styled lines of at most `width` columns. */
|
|
880
880
|
function renderMarkdown(text, width) {
|
|
881
881
|
const lines = [];
|
|
882
|
+
let separatorPending = false;
|
|
882
883
|
const push = (segments) => {
|
|
883
884
|
for (const wrapped of wrapSegments(segments, Math.max(10, width))) lines.push({ segments: merge(wrapped) });
|
|
884
885
|
};
|
|
886
|
+
const startBlock = () => {
|
|
887
|
+
if (separatorPending && lines.length > 0 && lines.at(-1)?.segments.length !== 0) lines.push({ segments: [] });
|
|
888
|
+
separatorPending = false;
|
|
889
|
+
};
|
|
885
890
|
const source = text.replaceAll("\r", "").split("\n");
|
|
886
891
|
let index = 0;
|
|
887
892
|
while (index < source.length) {
|
|
888
893
|
const line = source[index] ?? "";
|
|
889
894
|
index += 1;
|
|
895
|
+
if (line.trim() === "") {
|
|
896
|
+
separatorPending = lines.length > 0;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
startBlock();
|
|
890
900
|
const fence = FENCE.exec(line);
|
|
891
901
|
if (fence !== null) {
|
|
892
902
|
const language = fence[1] ?? "";
|
|
@@ -898,7 +908,6 @@ function renderMarkdown(text, width) {
|
|
|
898
908
|
index += 1;
|
|
899
909
|
continue;
|
|
900
910
|
}
|
|
901
|
-
if (line.trim() === "") continue;
|
|
902
911
|
if (RULE.test(line.trim())) {
|
|
903
912
|
push([seg(` ${"─".repeat(Math.max(1, Math.floor(width / 4)))}`, "dim")]);
|
|
904
913
|
continue;
|
|
@@ -971,6 +980,64 @@ function caretVisible(tick) {
|
|
|
971
980
|
return tick % 2 === 0;
|
|
972
981
|
}
|
|
973
982
|
//#endregion
|
|
983
|
+
//#region src/render/inspector.ts
|
|
984
|
+
/** Composer/status chrome plus one optional, fixed-height local notice row. */
|
|
985
|
+
const INSPECTOR_CHROME_ROWS = 5;
|
|
986
|
+
/** One transcript-to-composer gutter, collapsed on short terminals. */
|
|
987
|
+
function layoutGutterRows(rows) {
|
|
988
|
+
return Math.max(1, Math.floor(rows)) >= 14 ? 1 : 0;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Keep the inspector plus its persistent status/composer chrome below
|
|
992
|
+
* `stdout.rows`: at equality Ink clears the terminal and rewrites all
|
|
993
|
+
* accumulated `<Static>` output on every frame.
|
|
994
|
+
*/
|
|
995
|
+
function panelViewport(columns, rows) {
|
|
996
|
+
const safeColumns = Math.max(1, Math.floor(columns));
|
|
997
|
+
const safeRows = Math.max(1, Math.floor(rows));
|
|
998
|
+
const maxHeight = Math.max(0, Math.min(safeRows - 2 - INSPECTOR_CHROME_ROWS - layoutGutterRows(safeRows), Math.floor(safeRows / 2)));
|
|
999
|
+
const compact = maxHeight < 5 || safeColumns < 8;
|
|
1000
|
+
const gapRows = !compact && maxHeight >= 7 ? 2 : 0;
|
|
1001
|
+
return {
|
|
1002
|
+
maxHeight,
|
|
1003
|
+
bodyRows: compact ? 0 : maxHeight - 4 - gapRows,
|
|
1004
|
+
gapRows,
|
|
1005
|
+
contentColumns: compact ? Math.max(1, safeColumns - 1) : Math.max(1, safeColumns - 4),
|
|
1006
|
+
compact
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/** Backward-compatible name for the Ctrl+O-specific caller and tests. */
|
|
1010
|
+
function inspectorViewport(columns, rows) {
|
|
1011
|
+
return panelViewport(columns, rows);
|
|
1012
|
+
}
|
|
1013
|
+
/** Clamp a first-visible row to the range representable by one viewport. */
|
|
1014
|
+
function clampScroll(offset, totalRows, visibleRows) {
|
|
1015
|
+
const last = Math.max(0, Math.max(0, Math.floor(totalRows)) - Math.max(0, Math.floor(visibleRows)));
|
|
1016
|
+
return Math.max(0, Math.min(Math.floor(offset), last));
|
|
1017
|
+
}
|
|
1018
|
+
/** Move a viewport by a signed row delta without escaping its content. */
|
|
1019
|
+
function moveScroll(offset, delta, totalRows, visibleRows) {
|
|
1020
|
+
return clampScroll(offset + delta, totalRows, visibleRows);
|
|
1021
|
+
}
|
|
1022
|
+
/** Keep one focused row visible while preserving the current window when possible. */
|
|
1023
|
+
function revealRow(offset, row, totalRows, visibleRows) {
|
|
1024
|
+
const size = Math.max(1, Math.floor(visibleRows));
|
|
1025
|
+
const target = Math.max(0, Math.min(Math.floor(row), Math.max(0, totalRows - 1)));
|
|
1026
|
+
if (target < offset) return clampScroll(target, totalRows, size);
|
|
1027
|
+
if (target >= offset + size) return clampScroll(target - size + 1, totalRows, size);
|
|
1028
|
+
return clampScroll(offset, totalRows, size);
|
|
1029
|
+
}
|
|
1030
|
+
/** Center a selected list row where possible, clamped at both ends. */
|
|
1031
|
+
function selectionWindow(cursor, totalRows, visibleRows) {
|
|
1032
|
+
return clampScroll(cursor - Math.floor(Math.max(1, visibleRows) / 2), totalRows, visibleRows);
|
|
1033
|
+
}
|
|
1034
|
+
/** Follow appended history only while the inspector cursor was at the tail. */
|
|
1035
|
+
function followInspectorCursor(cursor, previousLength, nextLength) {
|
|
1036
|
+
const nextLast = Math.max(0, nextLength - 1);
|
|
1037
|
+
if (cursor >= Math.max(0, previousLength - 1)) return nextLast;
|
|
1038
|
+
return Math.min(cursor, nextLast);
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
974
1041
|
//#region src/render/status.ts
|
|
975
1042
|
/**
|
|
976
1043
|
* Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
|
|
@@ -1029,6 +1096,7 @@ function buildStatusGroups(facts, stats) {
|
|
|
1029
1096
|
facts.plan ? "⧉ plan" : void 0
|
|
1030
1097
|
].filter((part) => part !== void 0 && part !== "");
|
|
1031
1098
|
if (identity.length > 0) groups.push(identity.join(" · "));
|
|
1099
|
+
if (facts.mode !== void 0 && facts.mode !== "") groups.push(`mode ${facts.mode}`);
|
|
1032
1100
|
if (stats.turns > 0 || stats.steps > 0) {
|
|
1033
1101
|
groups.push(`T${stats.turns} · S${stats.steps}`);
|
|
1034
1102
|
const durations = [];
|
|
@@ -1076,12 +1144,37 @@ const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu;
|
|
|
1076
1144
|
function displayText(text) {
|
|
1077
1145
|
return text.replace(CONTROL_ESCAPE, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
1078
1146
|
}
|
|
1147
|
+
/** Collapse external text to one terminal-safe logical row. */
|
|
1148
|
+
function singleLineText(text) {
|
|
1149
|
+
return displayText(text).replace(/\r?\n/gu, " ↵ ").replace(/\t/gu, " ");
|
|
1150
|
+
}
|
|
1079
1151
|
/** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
|
|
1080
1152
|
function cellWidth(text) {
|
|
1081
1153
|
let columns = 0;
|
|
1082
1154
|
for (const char of text) columns += (char.codePointAt(0) ?? 0) > 11903 ? 2 : 1;
|
|
1083
1155
|
return columns;
|
|
1084
1156
|
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Truncate one display-safe row without ever exceeding its physical-column
|
|
1159
|
+
* budget. The ellipsis is included inside the budget, matching Codex's popup
|
|
1160
|
+
* truncation contract; the previous app-local helper appended it after the
|
|
1161
|
+
* row was already full and could force an extra terminal wrap.
|
|
1162
|
+
*/
|
|
1163
|
+
function truncateColumns(text, columns) {
|
|
1164
|
+
const limit = Math.max(0, Math.floor(columns));
|
|
1165
|
+
if (limit === 0) return "";
|
|
1166
|
+
if (cellWidth(text) <= limit) return text;
|
|
1167
|
+
const contentLimit = limit - 1;
|
|
1168
|
+
let used = 0;
|
|
1169
|
+
let result = "";
|
|
1170
|
+
for (const char of text) {
|
|
1171
|
+
const width = cellWidth(char);
|
|
1172
|
+
if (used + width > contentLimit) break;
|
|
1173
|
+
result += char;
|
|
1174
|
+
used += width;
|
|
1175
|
+
}
|
|
1176
|
+
return `${result}…`;
|
|
1177
|
+
}
|
|
1085
1178
|
/** Read one Unicode character immediately before `end`. */
|
|
1086
1179
|
function previousCharacter(text, end) {
|
|
1087
1180
|
const last = text.charCodeAt(end - 1);
|
|
@@ -1145,58 +1238,6 @@ function displayTail(text, columns, rows) {
|
|
|
1145
1238
|
};
|
|
1146
1239
|
}
|
|
1147
1240
|
//#endregion
|
|
1148
|
-
//#region src/render/inspector.ts
|
|
1149
|
-
/** The three-row read-only composer frame plus its one-row status footer. */
|
|
1150
|
-
const INSPECTOR_CHROME_ROWS = 4;
|
|
1151
|
-
/**
|
|
1152
|
-
* Keep the inspector plus its persistent status/composer chrome below
|
|
1153
|
-
* `stdout.rows`: at equality Ink clears the terminal and rewrites all
|
|
1154
|
-
* accumulated `<Static>` output on every frame.
|
|
1155
|
-
*/
|
|
1156
|
-
function panelViewport(columns, rows) {
|
|
1157
|
-
const safeColumns = Math.max(1, Math.floor(columns));
|
|
1158
|
-
const safeRows = Math.max(1, Math.floor(rows));
|
|
1159
|
-
const maxHeight = Math.max(0, Math.min(safeRows - 2 - INSPECTOR_CHROME_ROWS, Math.floor(safeRows / 2)));
|
|
1160
|
-
const compact = maxHeight < 5 || safeColumns < 8;
|
|
1161
|
-
return {
|
|
1162
|
-
maxHeight,
|
|
1163
|
-
bodyRows: compact ? 0 : maxHeight - 4,
|
|
1164
|
-
contentColumns: compact ? Math.max(1, safeColumns - 1) : Math.max(1, safeColumns - 4),
|
|
1165
|
-
compact
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
/** Backward-compatible name for the Ctrl+O-specific caller and tests. */
|
|
1169
|
-
function inspectorViewport(columns, rows) {
|
|
1170
|
-
return panelViewport(columns, rows);
|
|
1171
|
-
}
|
|
1172
|
-
/** Clamp a first-visible row to the range representable by one viewport. */
|
|
1173
|
-
function clampScroll(offset, totalRows, visibleRows) {
|
|
1174
|
-
const last = Math.max(0, Math.max(0, Math.floor(totalRows)) - Math.max(0, Math.floor(visibleRows)));
|
|
1175
|
-
return Math.max(0, Math.min(Math.floor(offset), last));
|
|
1176
|
-
}
|
|
1177
|
-
/** Move a viewport by a signed row delta without escaping its content. */
|
|
1178
|
-
function moveScroll(offset, delta, totalRows, visibleRows) {
|
|
1179
|
-
return clampScroll(offset + delta, totalRows, visibleRows);
|
|
1180
|
-
}
|
|
1181
|
-
/** Keep one focused row visible while preserving the current window when possible. */
|
|
1182
|
-
function revealRow(offset, row, totalRows, visibleRows) {
|
|
1183
|
-
const size = Math.max(1, Math.floor(visibleRows));
|
|
1184
|
-
const target = Math.max(0, Math.min(Math.floor(row), Math.max(0, totalRows - 1)));
|
|
1185
|
-
if (target < offset) return clampScroll(target, totalRows, size);
|
|
1186
|
-
if (target >= offset + size) return clampScroll(target - size + 1, totalRows, size);
|
|
1187
|
-
return clampScroll(offset, totalRows, size);
|
|
1188
|
-
}
|
|
1189
|
-
/** Center a selected list row where possible, clamped at both ends. */
|
|
1190
|
-
function selectionWindow(cursor, totalRows, visibleRows) {
|
|
1191
|
-
return clampScroll(cursor - Math.floor(Math.max(1, visibleRows) / 2), totalRows, visibleRows);
|
|
1192
|
-
}
|
|
1193
|
-
/** Follow appended history only while the inspector cursor was at the tail. */
|
|
1194
|
-
function followInspectorCursor(cursor, previousLength, nextLength) {
|
|
1195
|
-
const nextLast = Math.max(0, nextLength - 1);
|
|
1196
|
-
if (cursor >= Math.max(0, previousLength - 1)) return nextLast;
|
|
1197
|
-
return Math.min(cursor, nextLast);
|
|
1198
|
-
}
|
|
1199
|
-
//#endregion
|
|
1200
1241
|
//#region src/render/lines.ts
|
|
1201
1242
|
/** Construct one segment without leaking mutable objects into cached rows. */
|
|
1202
1243
|
function lineSegment(text, style = "plain") {
|
|
@@ -1315,6 +1356,291 @@ function transcriptEntryLines(entry, columns) {
|
|
|
1315
1356
|
}
|
|
1316
1357
|
}
|
|
1317
1358
|
//#endregion
|
|
1359
|
+
//#region src/kernel-panels.ts
|
|
1360
|
+
/** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
|
|
1361
|
+
function color(rgb) {
|
|
1362
|
+
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
|
1363
|
+
}
|
|
1364
|
+
function ListFrame(props) {
|
|
1365
|
+
const stdout = useStdout().stdout;
|
|
1366
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
1367
|
+
if (viewport.maxHeight === 0) return createElement(Box, { display: "none" });
|
|
1368
|
+
if (viewport.compact) return createElement(Text, { wrap: "truncate-end" }, truncateColumns(`${props.title} · esc close`, viewport.contentColumns));
|
|
1369
|
+
const stateRows = props.loading ? [{
|
|
1370
|
+
key: "loading",
|
|
1371
|
+
text: " loading…"
|
|
1372
|
+
}] : props.error !== void 0 ? [{
|
|
1373
|
+
key: "error",
|
|
1374
|
+
text: ` ${singleLineText(props.error)}`
|
|
1375
|
+
}] : props.rows.length === 0 ? [{
|
|
1376
|
+
key: "empty",
|
|
1377
|
+
text: " no matching entries"
|
|
1378
|
+
}] : props.rows;
|
|
1379
|
+
const bodyRows = Math.max(1, viewport.bodyRows - 1);
|
|
1380
|
+
const offset = revealRow(0, props.cursor, stateRows.length, bodyRows);
|
|
1381
|
+
const visible = stateRows.slice(offset, offset + bodyRows);
|
|
1382
|
+
return createElement(Box, {
|
|
1383
|
+
borderStyle: "round",
|
|
1384
|
+
borderColor: color(TUI_RGB.dim),
|
|
1385
|
+
flexDirection: "column",
|
|
1386
|
+
paddingX: 1
|
|
1387
|
+
}, createElement(Text, {
|
|
1388
|
+
color: color(TUI_RGB.brandBright),
|
|
1389
|
+
wrap: "truncate-end"
|
|
1390
|
+
}, truncateColumns(props.title, viewport.contentColumns)), createElement(Text, {
|
|
1391
|
+
dimColor: true,
|
|
1392
|
+
wrap: "truncate-end"
|
|
1393
|
+
}, truncateColumns(`search: ${props.query === "" ? "type to filter" : props.query}`, viewport.contentColumns)), ...visible.map((row, index) => {
|
|
1394
|
+
const absolute = offset + index;
|
|
1395
|
+
const selected = !props.loading && props.error === void 0 && props.rows.length > 0 && absolute === props.cursor;
|
|
1396
|
+
return createElement(Text, {
|
|
1397
|
+
key: row.key,
|
|
1398
|
+
color: selected ? color(TUI_RGB.brandBright) : row.disabled ? color(TUI_RGB.dim) : void 0,
|
|
1399
|
+
dimColor: row.disabled,
|
|
1400
|
+
wrap: "truncate-end"
|
|
1401
|
+
}, truncateColumns(`${selected ? "› " : " "}${row.text}`, viewport.contentColumns));
|
|
1402
|
+
}), createElement(Text, {
|
|
1403
|
+
dimColor: true,
|
|
1404
|
+
wrap: "truncate-end"
|
|
1405
|
+
}, truncateColumns(props.footer, viewport.contentColumns)));
|
|
1406
|
+
}
|
|
1407
|
+
function editQuery(query, input, key) {
|
|
1408
|
+
if (key.backspace || key.delete) return query.slice(0, -1);
|
|
1409
|
+
if (input.length === 1 && input >= " " && input !== "") return query + input;
|
|
1410
|
+
}
|
|
1411
|
+
function ModePanel({ current, load, select, close }) {
|
|
1412
|
+
const [rows, setRows] = useState([]);
|
|
1413
|
+
const [query, setQuery] = useState("");
|
|
1414
|
+
const [cursor, setCursor] = useState(0);
|
|
1415
|
+
const [loading, setLoading] = useState(true);
|
|
1416
|
+
const [error, setError] = useState();
|
|
1417
|
+
const refresh = () => {
|
|
1418
|
+
setLoading(true);
|
|
1419
|
+
setError(void 0);
|
|
1420
|
+
Promise.resolve().then(load).then((value) => {
|
|
1421
|
+
setRows(value);
|
|
1422
|
+
setLoading(false);
|
|
1423
|
+
}, (reason) => {
|
|
1424
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
1425
|
+
setLoading(false);
|
|
1426
|
+
});
|
|
1427
|
+
};
|
|
1428
|
+
useEffect(refresh, []);
|
|
1429
|
+
const visible = useMemo(() => rows.filter((row) => `${row.id} ${row.name ?? ""} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
|
|
1430
|
+
useEffect(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
|
|
1431
|
+
useInput((input, key) => {
|
|
1432
|
+
if (key.escape || input === "q") return close();
|
|
1433
|
+
if (input === "r" && query === "") return refresh();
|
|
1434
|
+
if (key.upArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length);
|
|
1435
|
+
if (key.downArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + 1) % visible.length);
|
|
1436
|
+
if (key.return && visible[cursor]?.broken === void 0) return select(visible[cursor].id);
|
|
1437
|
+
const next = editQuery(query, input, key);
|
|
1438
|
+
if (next !== void 0) {
|
|
1439
|
+
setQuery(next);
|
|
1440
|
+
setCursor(0);
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
return createElement(ListFrame, {
|
|
1444
|
+
title: `/mode · current ${current}`,
|
|
1445
|
+
rows: visible.map((row) => ({
|
|
1446
|
+
key: row.id,
|
|
1447
|
+
disabled: row.broken !== void 0,
|
|
1448
|
+
text: `${row.id === current ? "●" : "○"} ${row.name ?? row.id} · ${row.description ?? row.trust}${row.broken === void 0 ? "" : ` · broken: ${row.broken}`}`
|
|
1449
|
+
})),
|
|
1450
|
+
cursor,
|
|
1451
|
+
loading,
|
|
1452
|
+
error,
|
|
1453
|
+
query,
|
|
1454
|
+
footer: "↑↓ choose · enter switch · r refresh · esc close"
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
function PluginPanel({ load, close, initialQuery = "" }) {
|
|
1458
|
+
const [epoch, setEpoch] = useState(0);
|
|
1459
|
+
const [query, setQuery] = useState(initialQuery);
|
|
1460
|
+
const [cursor, setCursor] = useState(0);
|
|
1461
|
+
const [expanded, setExpanded] = useState(false);
|
|
1462
|
+
const rows = useMemo(() => load().filter((row) => `${row.entryId} ${row.moduleName} ${row.phase ?? ""}`.toLowerCase().includes(query.toLowerCase())), [epoch, query]);
|
|
1463
|
+
useEffect(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
|
|
1464
|
+
useInput((input, key) => {
|
|
1465
|
+
if (key.escape || input === "q") return close();
|
|
1466
|
+
if (input === "r" && query === "") return setEpoch((value) => value + 1);
|
|
1467
|
+
if (key.upArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length);
|
|
1468
|
+
if (key.downArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + 1) % rows.length);
|
|
1469
|
+
if (key.return) return setExpanded((value) => !value);
|
|
1470
|
+
const next = editQuery(query, input, key);
|
|
1471
|
+
if (next !== void 0) {
|
|
1472
|
+
setQuery(next);
|
|
1473
|
+
setCursor(0);
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
return createElement(ListFrame, {
|
|
1477
|
+
title: "/plugin · loader inspector",
|
|
1478
|
+
rows: rows.map((row, index) => ({
|
|
1479
|
+
key: row.entryId,
|
|
1480
|
+
disabled: !row.enabled,
|
|
1481
|
+
text: `${row.enabled ? "●" : "○"} ${row.entryId} · ${row.phase ?? "not mounted"}${expanded && index === cursor ? ` · ${row.moduleName}` : ""}`
|
|
1482
|
+
})),
|
|
1483
|
+
cursor,
|
|
1484
|
+
loading: false,
|
|
1485
|
+
query,
|
|
1486
|
+
footer: "↑↓ inspect · enter details · r refresh · esc close"
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
1490
|
+
const [options, setOptions] = useState({
|
|
1491
|
+
sessions: "roots",
|
|
1492
|
+
cwd: "all",
|
|
1493
|
+
sort: "newest",
|
|
1494
|
+
currentCwd,
|
|
1495
|
+
query: ""
|
|
1496
|
+
});
|
|
1497
|
+
const [focus, setFocus] = useState(0);
|
|
1498
|
+
const [density, setDensity] = useState("comfortable");
|
|
1499
|
+
const [rows, setRows] = useState([]);
|
|
1500
|
+
const [cursor, setCursor] = useState(0);
|
|
1501
|
+
const [loading, setLoading] = useState(true);
|
|
1502
|
+
const [error, setError] = useState();
|
|
1503
|
+
const [expanded, setExpanded] = useState();
|
|
1504
|
+
const [transcript, setTranscript] = useState();
|
|
1505
|
+
const transcriptLoad = useRef();
|
|
1506
|
+
useEffect(() => () => transcriptLoad.current?.abort(), []);
|
|
1507
|
+
useEffect(() => {
|
|
1508
|
+
const controller = new AbortController();
|
|
1509
|
+
setLoading(true);
|
|
1510
|
+
setError(void 0);
|
|
1511
|
+
Promise.resolve().then(() => load(options, controller.signal)).then((value) => {
|
|
1512
|
+
if (!controller.signal.aborted) {
|
|
1513
|
+
setRows(value);
|
|
1514
|
+
setLoading(false);
|
|
1515
|
+
}
|
|
1516
|
+
}, (reason) => {
|
|
1517
|
+
if (!controller.signal.aborted) {
|
|
1518
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
1519
|
+
setLoading(false);
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
return () => controller.abort();
|
|
1523
|
+
}, [options]);
|
|
1524
|
+
useEffect(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
|
|
1525
|
+
const cycle = () => {
|
|
1526
|
+
if (focus === 3) {
|
|
1527
|
+
setDensity((current) => current === "comfortable" ? "dense" : "comfortable");
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
setOptions((value) => {
|
|
1531
|
+
if (focus === 0) return {
|
|
1532
|
+
...value,
|
|
1533
|
+
sessions: value.sessions === "roots" ? "all" : "roots"
|
|
1534
|
+
};
|
|
1535
|
+
if (focus === 1) return {
|
|
1536
|
+
...value,
|
|
1537
|
+
cwd: value.cwd === "all" ? "current" : "all"
|
|
1538
|
+
};
|
|
1539
|
+
return {
|
|
1540
|
+
...value,
|
|
1541
|
+
sort: value.sort === "newest" ? "oldest" : "newest"
|
|
1542
|
+
};
|
|
1543
|
+
});
|
|
1544
|
+
};
|
|
1545
|
+
useInput((input, key) => {
|
|
1546
|
+
if (key.escape || input === "q") return close();
|
|
1547
|
+
if (key.tab) return setFocus((value) => (value + (key.shift ? 3 : 1)) % 4);
|
|
1548
|
+
if (key.leftArrow) return cycle();
|
|
1549
|
+
if (key.rightArrow) return cycle();
|
|
1550
|
+
if (key.upArrow) return setCursor((value) => rows.length === 0 ? 0 : Math.max(0, value - 1));
|
|
1551
|
+
if (key.downArrow) return setCursor((value) => rows.length === 0 ? 0 : Math.min(rows.length - 1, value + 1));
|
|
1552
|
+
if (key.pageUp) return setCursor((value) => Math.max(0, value - 8));
|
|
1553
|
+
if (key.pageDown) return setCursor((value) => Math.min(rows.length - 1, value + 8));
|
|
1554
|
+
if (input === "g") return setCursor(0);
|
|
1555
|
+
if (input === "G") return setCursor(Math.max(0, rows.length - 1));
|
|
1556
|
+
if (input === "d") return setDensity((value) => value === "comfortable" ? "dense" : "comfortable");
|
|
1557
|
+
if (input === "e" && rows[cursor] !== void 0) return setExpanded((value) => value === rows[cursor].id ? void 0 : rows[cursor].id);
|
|
1558
|
+
if (input === "t" && rows[cursor] !== void 0) {
|
|
1559
|
+
const row = rows[cursor];
|
|
1560
|
+
transcriptLoad.current?.abort();
|
|
1561
|
+
setTranscript({ id: row.id });
|
|
1562
|
+
const controller = new AbortController();
|
|
1563
|
+
transcriptLoad.current = controller;
|
|
1564
|
+
Promise.resolve().then(() => readTranscript(row.id, controller.signal)).then((text) => {
|
|
1565
|
+
if (!controller.signal.aborted) setTranscript({
|
|
1566
|
+
id: row.id,
|
|
1567
|
+
text
|
|
1568
|
+
});
|
|
1569
|
+
}, (reason) => {
|
|
1570
|
+
if (!controller.signal.aborted) setTranscript({
|
|
1571
|
+
id: row.id,
|
|
1572
|
+
error: reason instanceof Error ? reason.message : String(reason)
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (key.return && rows[cursor]?.resumable === true) return select(rows[cursor]);
|
|
1578
|
+
const next = editQuery(options.query, input, key);
|
|
1579
|
+
if (next !== void 0) {
|
|
1580
|
+
setOptions((value) => ({
|
|
1581
|
+
...value,
|
|
1582
|
+
query: next
|
|
1583
|
+
}));
|
|
1584
|
+
setCursor(0);
|
|
1585
|
+
}
|
|
1586
|
+
}, { isActive: transcript === void 0 });
|
|
1587
|
+
if (transcript !== void 0) return createElement(DocumentPanel, {
|
|
1588
|
+
title: `transcript · ${transcript.id}`,
|
|
1589
|
+
text: transcript.text,
|
|
1590
|
+
error: transcript.error,
|
|
1591
|
+
close: () => {
|
|
1592
|
+
transcriptLoad.current?.abort();
|
|
1593
|
+
setTranscript(void 0);
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
const toolbar = `[${focus === 0 ? ">" : ""}${options.sessions}] [${focus === 1 ? ">" : ""}${options.cwd} cwd] [${focus === 2 ? ">" : ""}${options.sort}] [${focus === 3 ? ">" : ""}${density}]`;
|
|
1597
|
+
return createElement(ListFrame, {
|
|
1598
|
+
title: `/resume · ${toolbar}`,
|
|
1599
|
+
rows: rows.map((row) => ({
|
|
1600
|
+
key: row.id,
|
|
1601
|
+
disabled: !row.resumable,
|
|
1602
|
+
text: `${row.subagent ? "↳" : "○"} ${row.title ?? row.id.slice(-12)}${density === "comfortable" ? ` · ${row.workspace} · ${row.preset}` : ""}${row.live ? " · live" : ""}${expanded === row.id ? ` · ${row.id} · ${row.cwd}${row.parent === void 0 ? "" : ` · parent ${row.parent}`}` : ""}`
|
|
1603
|
+
})),
|
|
1604
|
+
cursor,
|
|
1605
|
+
loading,
|
|
1606
|
+
error,
|
|
1607
|
+
query: options.query,
|
|
1608
|
+
footer: "type search · tab/←→ filters · ↑↓/pg navigate · e details · t transcript · enter resume"
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
function DocumentPanel({ title, text, error, close }) {
|
|
1612
|
+
const stdout = useStdout().stdout;
|
|
1613
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
1614
|
+
const [scroll, setScroll] = useState(0);
|
|
1615
|
+
const lines = useMemo(() => text === void 0 ? [] : textLines(text, viewport.contentColumns).map((line) => line.segments.map((segment) => segment.text).join("")), [text, viewport.contentColumns]);
|
|
1616
|
+
useInput((input, key) => {
|
|
1617
|
+
if (key.escape || input === "q" || input === "t") return close();
|
|
1618
|
+
if (key.upArrow) return setScroll((value) => Math.max(0, value - 1));
|
|
1619
|
+
if (key.downArrow) return setScroll((value) => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + 1));
|
|
1620
|
+
if (key.pageUp) return setScroll((value) => Math.max(0, value - Math.max(1, viewport.bodyRows - 1)));
|
|
1621
|
+
if (key.pageDown) return setScroll((value) => Math.min(Math.max(0, lines.length - viewport.bodyRows), value + Math.max(1, viewport.bodyRows - 1)));
|
|
1622
|
+
if (input === "g") return setScroll(0);
|
|
1623
|
+
if (input === "G") return setScroll(Math.max(0, lines.length - viewport.bodyRows));
|
|
1624
|
+
});
|
|
1625
|
+
if (viewport.compact) return createElement(Text, { wrap: "truncate-end" }, truncateColumns("transcript · esc close", viewport.contentColumns));
|
|
1626
|
+
const body = error !== void 0 ? [`error: ${singleLineText(error)}`] : text === void 0 ? ["loading transcript…"] : lines.slice(scroll, scroll + viewport.bodyRows);
|
|
1627
|
+
return createElement(Box, {
|
|
1628
|
+
borderStyle: "round",
|
|
1629
|
+
borderColor: color(TUI_RGB.dim),
|
|
1630
|
+
flexDirection: "column",
|
|
1631
|
+
paddingX: 1
|
|
1632
|
+
}, createElement(Text, {
|
|
1633
|
+
color: color(TUI_RGB.brandBright),
|
|
1634
|
+
wrap: "truncate-end"
|
|
1635
|
+
}, truncateColumns(title, viewport.contentColumns)), ...body.map((line, index) => createElement(Text, {
|
|
1636
|
+
key: `${scroll}-${index}`,
|
|
1637
|
+
wrap: "truncate-end"
|
|
1638
|
+
}, truncateColumns(line, viewport.contentColumns))), createElement(Text, {
|
|
1639
|
+
dimColor: true,
|
|
1640
|
+
wrap: "truncate-end"
|
|
1641
|
+
}, truncateColumns(`lines ${lines.length === 0 ? 0 : scroll + 1}-${Math.min(lines.length, scroll + viewport.bodyRows)}/${lines.length} · ↑↓/pg/g/G · t/esc close`, viewport.contentColumns)));
|
|
1642
|
+
}
|
|
1643
|
+
//#endregion
|
|
1318
1644
|
//#region src/app.ts
|
|
1319
1645
|
/**
|
|
1320
1646
|
* The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live
|
|
@@ -1339,21 +1665,10 @@ const RESIZE_REFLOW_CLEAR = "\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H";
|
|
|
1339
1665
|
function inkColor(triple) {
|
|
1340
1666
|
return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`;
|
|
1341
1667
|
}
|
|
1342
|
-
/** Truncate text to a visible-column budget, appending … when cut. */
|
|
1343
|
-
function truncateColumns(text, max) {
|
|
1344
|
-
let columns = 0;
|
|
1345
|
-
let out = "";
|
|
1346
|
-
for (const char of text) {
|
|
1347
|
-
const width = (char.codePointAt(0) ?? 0) > 11903 ? 2 : 1;
|
|
1348
|
-
if (columns + width > max) return `${out}…`;
|
|
1349
|
-
out += char;
|
|
1350
|
-
columns += width;
|
|
1351
|
-
}
|
|
1352
|
-
return out;
|
|
1353
|
-
}
|
|
1354
1668
|
/** Pad text with spaces to a visible-column target (menu name column). */
|
|
1355
1669
|
function padColumns(text, width) {
|
|
1356
|
-
|
|
1670
|
+
const clipped = truncateColumns(singleLineText(text), width);
|
|
1671
|
+
return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
|
|
1357
1672
|
}
|
|
1358
1673
|
/** Interval-driven frame counter for one self-contained animated leaf. */
|
|
1359
1674
|
function useFrames(intervalMs) {
|
|
@@ -1521,11 +1836,15 @@ function StyledRows({ lines }) {
|
|
|
1521
1836
|
...lineStyleProps(segment.style)
|
|
1522
1837
|
}, segment.text)))));
|
|
1523
1838
|
}
|
|
1839
|
+
/** Codex-style panel rhythm that still participates in the row budget. */
|
|
1840
|
+
function PanelGap({ visible }) {
|
|
1841
|
+
return visible ? createElement(Text, null, " ") : void 0;
|
|
1842
|
+
}
|
|
1524
1843
|
/** One settled markdown document rendered as styled lines at the terminal width. */
|
|
1525
1844
|
function MarkdownBody({ text }) {
|
|
1526
1845
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
1527
1846
|
const lines = useMemo(() => renderMarkdown(displayText(text), Math.max(20, columns - 2)), [text, columns]);
|
|
1528
|
-
return createElement(Box, { flexDirection: "column" }, ...lines.map((line, index) => createElement(Text, { key: index },
|
|
1847
|
+
return createElement(Box, { flexDirection: "column" }, ...lines.map((line, index) => createElement(Text, { key: index }, line.segments.length === 0 ? " " : line.segments.map((segment, at) => createElement(Text, {
|
|
1529
1848
|
key: at,
|
|
1530
1849
|
...segmentProps(segment.style)
|
|
1531
1850
|
}, segment.text)))));
|
|
@@ -1691,9 +2010,21 @@ function StatusLine({ facts, stats, busy }) {
|
|
|
1691
2010
|
wrap: "truncate-end"
|
|
1692
2011
|
}, busy ? "● " : "○ ", groups.join(" | ")));
|
|
1693
2012
|
}
|
|
2013
|
+
/**
|
|
2014
|
+
* One fixed-height local feedback row. Errors remain visible while a slash
|
|
2015
|
+
* subpage is open, but arbitrary exception text can never add physical rows
|
|
2016
|
+
* above the composer.
|
|
2017
|
+
*/
|
|
2018
|
+
function NoticeLine({ text, tone, columns }) {
|
|
2019
|
+
const color = tone === "error" ? TUI_RGB.error : tone === "warning" ? TUI_RGB.warn : TUI_RGB.brandBright;
|
|
2020
|
+
const mark = tone === "error" ? "⨯" : tone === "warning" ? "!" : "•";
|
|
2021
|
+
return createElement(Box, { paddingLeft: 2 }, createElement(Text, {
|
|
2022
|
+
color: inkColor(color),
|
|
2023
|
+
wrap: "truncate-end"
|
|
2024
|
+
}, truncateColumns(`${mark} ${singleLineText(text)}`, Math.max(1, columns - 2))));
|
|
2025
|
+
}
|
|
1694
2026
|
/** The y/n approval bar rendered while an approval ask is pending. */
|
|
1695
|
-
function ApprovalBar({
|
|
1696
|
-
const snapshot = useSyncExternalStore(approval.subscribe, approval.getSnapshot);
|
|
2027
|
+
function ApprovalBar({ snapshot, locked }) {
|
|
1697
2028
|
const stdout = useStdout().stdout;
|
|
1698
2029
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
1699
2030
|
const [scroll, setScroll] = useState(0);
|
|
@@ -1745,7 +2076,7 @@ function ApprovalBar({ approval, locked }) {
|
|
|
1745
2076
|
color: inkColor(TUI_RGB.warn),
|
|
1746
2077
|
bold: true,
|
|
1747
2078
|
wrap: "truncate-end"
|
|
1748
|
-
}, truncateColumns(`⏸ waiting for approval · lines ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)), createElement(StyledRows, { lines: content.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), createElement(Text, {
|
|
2079
|
+
}, truncateColumns(`⏸ waiting for approval · lines ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(StyledRows, { lines: content.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Text, {
|
|
1749
2080
|
dimColor: true,
|
|
1750
2081
|
wrap: "truncate-end"
|
|
1751
2082
|
}, dim(truncateColumns(answered ? "submitted…" : "↑↓/pgup/pgdn scroll · y allow once · n reject", viewport.contentColumns))));
|
|
@@ -1758,8 +2089,7 @@ function ApprovalBar({ approval, locked }) {
|
|
|
1758
2089
|
* service with a `plan-review` intent — the approve option gets a ✓ mark,
|
|
1759
2090
|
* the answer encoding stays identical.
|
|
1760
2091
|
*/
|
|
1761
|
-
function QuestionBar({ store, locked }) {
|
|
1762
|
-
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
2092
|
+
function QuestionBar({ store, snapshot, locked }) {
|
|
1763
2093
|
const stdout = useStdout().stdout;
|
|
1764
2094
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
1765
2095
|
const pending = snapshot.pending;
|
|
@@ -1960,13 +2290,13 @@ function QuestionBar({ store, locked }) {
|
|
|
1960
2290
|
color: inkColor(isPlan ? TUI_RGB.brand : TUI_RGB.brandDeep),
|
|
1961
2291
|
bold: true,
|
|
1962
2292
|
wrap: "truncate-end"
|
|
1963
|
-
}, truncateColumns(`${isPlan ? "📋 plan review" : "❓ question"} ${index + 1}/${pending.request.questions.length} · lines ${rendered.lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(rendered.lines.length, visibleScroll + viewport.bodyRows)}/${rendered.lines.length}`, viewport.contentColumns)), createElement(StyledRows, { lines: rendered.lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), createElement(Text, {
|
|
2293
|
+
}, truncateColumns(`${isPlan ? "📋 plan review" : "❓ question"} ${index + 1}/${pending.request.questions.length} · lines ${rendered.lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(rendered.lines.length, visibleScroll + viewport.bodyRows)}/${rendered.lines.length}`, viewport.contentColumns)), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(StyledRows, { lines: rendered.lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Text, {
|
|
1964
2294
|
dimColor: true,
|
|
1965
2295
|
wrap: "truncate-end"
|
|
1966
2296
|
}, dim(truncateColumns(footer, viewport.contentColumns))));
|
|
1967
2297
|
}
|
|
1968
2298
|
/** The /model panel: a scrolling list over the advisory model directory. */
|
|
1969
|
-
function ModelPanel({ directory, error, onSelect, onClose }) {
|
|
2299
|
+
function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
|
|
1970
2300
|
const [cursor, setCursor] = useState(0);
|
|
1971
2301
|
const stdout = useStdout().stdout;
|
|
1972
2302
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
@@ -1983,6 +2313,10 @@ function ModelPanel({ directory, error, onSelect, onClose }) {
|
|
|
1983
2313
|
onClose();
|
|
1984
2314
|
return;
|
|
1985
2315
|
}
|
|
2316
|
+
if (input === "r") {
|
|
2317
|
+
onRetry();
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
1986
2320
|
if (rows.length === 0) return;
|
|
1987
2321
|
if (key.upArrow) {
|
|
1988
2322
|
setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
|
|
@@ -2011,9 +2345,27 @@ function ModelPanel({ directory, error, onSelect, onClose }) {
|
|
|
2011
2345
|
if (key.return && rows[cursor] !== void 0) onSelect(rows[cursor]);
|
|
2012
2346
|
});
|
|
2013
2347
|
if (viewport.maxHeight === 0) return createElement(Box, { display: "none" });
|
|
2014
|
-
if (viewport.compact) return createElement(Text, { wrap: "truncate-end" }, truncateColumns("/model · esc/q close", viewport.contentColumns));
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2348
|
+
if (viewport.compact) return createElement(Text, { wrap: "truncate-end" }, truncateColumns("/model · r retry · esc/q close", viewport.contentColumns));
|
|
2349
|
+
const stateRows = directory === void 0 && error === void 0 ? [createElement(Text, {
|
|
2350
|
+
key: "loading",
|
|
2351
|
+
dimColor: true,
|
|
2352
|
+
wrap: "truncate-end"
|
|
2353
|
+
}, " loading models…")] : error !== void 0 ? [createElement(Text, {
|
|
2354
|
+
key: "error",
|
|
2355
|
+
color: inkColor(TUI_RGB.error),
|
|
2356
|
+
wrap: "truncate-end"
|
|
2357
|
+
}, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))] : [...directory?.failures.length === 0 ? [] : [createElement(Text, {
|
|
2358
|
+
key: "failures",
|
|
2359
|
+
color: inkColor(TUI_RGB.warn),
|
|
2360
|
+
wrap: "truncate-end"
|
|
2361
|
+
}, truncateColumns(` unavailable providers: ${directory?.failures.join(", ")}`, viewport.contentColumns))], ...rows.length === 0 ? [createElement(Text, {
|
|
2362
|
+
key: "empty",
|
|
2363
|
+
dimColor: true,
|
|
2364
|
+
wrap: "truncate-end"
|
|
2365
|
+
}, " no models available")] : []];
|
|
2366
|
+
const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length);
|
|
2367
|
+
const first = selectionWindow(cursor, rows.length, rowBudget);
|
|
2368
|
+
const visible = rowBudget === 0 ? [] : rows.slice(first, first + rowBudget);
|
|
2017
2369
|
return createElement(Box, {
|
|
2018
2370
|
flexDirection: "column",
|
|
2019
2371
|
paddingX: 1,
|
|
@@ -2023,13 +2375,7 @@ function ModelPanel({ directory, error, onSelect, onClose }) {
|
|
|
2023
2375
|
color: inkColor(TUI_RGB.brand),
|
|
2024
2376
|
bold: true,
|
|
2025
2377
|
wrap: "truncate-end"
|
|
2026
|
-
}, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)),
|
|
2027
|
-
dimColor: true,
|
|
2028
|
-
wrap: "truncate-end"
|
|
2029
|
-
}, " loading models…") : void 0, error !== void 0 ? createElement(Text, {
|
|
2030
|
-
color: inkColor(TUI_RGB.error),
|
|
2031
|
-
wrap: "truncate-end"
|
|
2032
|
-
}, truncateColumns(` ${displayText(error)}`, viewport.contentColumns)) : void 0, ...visible.map((row) => {
|
|
2378
|
+
}, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), createElement(PanelGap, { visible: viewport.gapRows > 0 }), ...stateRows, ...visible.map((row) => {
|
|
2033
2379
|
const index = rows.indexOf(row);
|
|
2034
2380
|
const label = displayText(`${row.providerName} · ${row.modelName}`);
|
|
2035
2381
|
return createElement(Text, {
|
|
@@ -2037,22 +2383,22 @@ function ModelPanel({ directory, error, onSelect, onClose }) {
|
|
|
2037
2383
|
color: index === cursor ? inkColor(TUI_RGB.brandBright) : inkColor(TUI_RGB.dim),
|
|
2038
2384
|
wrap: "truncate-end"
|
|
2039
2385
|
}, truncateColumns(`${index === cursor ? "❯ " : " "}${label}`, viewport.contentColumns));
|
|
2040
|
-
}), createElement(Text, {
|
|
2386
|
+
}), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Text, {
|
|
2041
2387
|
dimColor: true,
|
|
2042
2388
|
wrap: "truncate-end"
|
|
2043
|
-
}, dim(truncateColumns("↑↓ move · pgup/pgdn page · g/G ends · enter select · esc/q close", viewport.contentColumns))));
|
|
2389
|
+
}, dim(truncateColumns("↑↓ move · pgup/pgdn page · g/G ends · enter select · r retry · esc/q close", viewport.contentColumns))));
|
|
2044
2390
|
}
|
|
2045
2391
|
/**
|
|
2046
2392
|
* The /help overlay: one scrolling card with the keyboard map, the TUI-local
|
|
2047
2393
|
* commands, the live registry commands, and the user-invocable skills — the
|
|
2048
2394
|
* real command surface, replacing the one-line notice.
|
|
2049
2395
|
*/
|
|
2050
|
-
function HelpPanel({ descriptors, skills, onClose }) {
|
|
2396
|
+
function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
|
|
2051
2397
|
const stdout = useStdout().stdout;
|
|
2052
2398
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
2053
2399
|
const [scroll, setScroll] = useState(0);
|
|
2054
|
-
const nameWidth = 18;
|
|
2055
|
-
const descBudget = Math.max(
|
|
2400
|
+
const nameWidth = Math.min(18, Math.max(1, viewport.contentColumns - 2));
|
|
2401
|
+
const descBudget = Math.max(0, viewport.contentColumns - nameWidth - 2);
|
|
2056
2402
|
const row = (label, description) => createElement(Text, {
|
|
2057
2403
|
dimColor: true,
|
|
2058
2404
|
wrap: "truncate-end"
|
|
@@ -2088,13 +2434,23 @@ function HelpPanel({ descriptors, skills, onClose }) {
|
|
|
2088
2434
|
dimColor: true,
|
|
2089
2435
|
wrap: "truncate-end"
|
|
2090
2436
|
}, " ctrl+k cut to end of line · ctrl+u clear line · ctrl+a / ctrl+e line ends"),
|
|
2437
|
+
createElement(Text, { key: "commands-gap" }, " "),
|
|
2091
2438
|
createElement(Text, {
|
|
2092
2439
|
key: "commands-title",
|
|
2093
2440
|
bold: true,
|
|
2094
2441
|
wrap: "truncate-end"
|
|
2095
2442
|
}, " commands"),
|
|
2443
|
+
...commandError === void 0 ? [] : [createElement(Text, {
|
|
2444
|
+
key: "commands-error",
|
|
2445
|
+
color: inkColor(TUI_RGB.error),
|
|
2446
|
+
wrap: "truncate-end"
|
|
2447
|
+
}, truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns))],
|
|
2096
2448
|
createElement(Box, { key: "local-help" }, row("/help", "show this overlay")),
|
|
2097
2449
|
createElement(Box, { key: "local-model" }, row("/model", "switch the model")),
|
|
2450
|
+
createElement(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
|
|
2451
|
+
createElement(Box, { key: "local-new" }, row("/new", "create and switch to a fresh session (/new [preset])")),
|
|
2452
|
+
createElement(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
|
|
2453
|
+
createElement(Box, { key: "local-plugin" }, row("/plugin", "inspect the live plugin composition")),
|
|
2098
2454
|
createElement(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
|
|
2099
2455
|
createElement(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
|
|
2100
2456
|
createElement(Box, { key: "local-title" }, row("/title", "rename this session (/title <text>)")),
|
|
@@ -2104,11 +2460,16 @@ function HelpPanel({ descriptors, skills, onClose }) {
|
|
|
2104
2460
|
dimColor: true,
|
|
2105
2461
|
wrap: "truncate-end"
|
|
2106
2462
|
}, ` ${padColumns(`/${descriptor.name}`, nameWidth)}${dim(truncateColumns(displayText(descriptor.description), descBudget))}`)),
|
|
2107
|
-
...skills.length === 0 ? [] : [createElement(Text, {
|
|
2463
|
+
...skills.length === 0 && skillError === void 0 ? [] : [createElement(Text, { key: "skills-gap" }, " "), createElement(Text, {
|
|
2108
2464
|
key: "skills-title",
|
|
2109
2465
|
bold: true,
|
|
2110
2466
|
wrap: "truncate-end"
|
|
2111
2467
|
}, " skills")],
|
|
2468
|
+
...skillError === void 0 ? [] : [createElement(Text, {
|
|
2469
|
+
key: "skills-error",
|
|
2470
|
+
color: inkColor(TUI_RGB.error),
|
|
2471
|
+
wrap: "truncate-end"
|
|
2472
|
+
}, truncateColumns(` skill catalog unavailable: ${singleLineText(skillError)}`, viewport.contentColumns))],
|
|
2112
2473
|
...skills.map((skill) => createElement(Text, {
|
|
2113
2474
|
key: `skill-${skill.name}`,
|
|
2114
2475
|
dimColor: true,
|
|
@@ -2145,7 +2506,7 @@ function HelpPanel({ descriptors, skills, onClose }) {
|
|
|
2145
2506
|
color: inkColor(TUI_RGB.brand),
|
|
2146
2507
|
bold: true,
|
|
2147
2508
|
wrap: "truncate-end"
|
|
2148
|
-
}, truncateColumns(`/help — keys and commands · rows ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)), ...content.slice(visibleScroll, visibleScroll + viewport.bodyRows), createElement(Text, {
|
|
2509
|
+
}, truncateColumns(`/help — keys and commands · rows ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)), createElement(PanelGap, { visible: viewport.gapRows > 0 }), ...content.slice(visibleScroll, visibleScroll + viewport.bodyRows), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Text, {
|
|
2149
2510
|
dimColor: true,
|
|
2150
2511
|
wrap: "truncate-end"
|
|
2151
2512
|
}, dim(truncateColumns("↑↓ scroll · pgup/pgdn page · g/G ends · esc/q close", viewport.contentColumns))));
|
|
@@ -2269,7 +2630,7 @@ function VerbosePanel({ entries, onClose }) {
|
|
|
2269
2630
|
color: inkColor(TUI_RGB.brand),
|
|
2270
2631
|
bold: true,
|
|
2271
2632
|
wrap: "truncate-end"
|
|
2272
|
-
}, truncateColumns(title, viewport.contentColumns)), createElement(Box, { flexDirection: "column" }, entry === void 0 ? createElement(Text, { dimColor: true }, " no durable entries yet") : createElement(StyledRows, { lines: visible })), createElement(Text, {
|
|
2633
|
+
}, truncateColumns(title, viewport.contentColumns)), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Box, { flexDirection: "column" }, entry === void 0 ? createElement(Text, { dimColor: true }, " no durable entries yet") : createElement(StyledRows, { lines: visible })), createElement(PanelGap, { visible: viewport.gapRows > 0 }), createElement(Text, {
|
|
2273
2634
|
dimColor: true,
|
|
2274
2635
|
wrap: "truncate-end"
|
|
2275
2636
|
}, dim(truncateColumns("←→ entry · ↑↓ scroll · pgup/pgdn page · g/G ends · ctrl+o/esc/q close", viewport.contentColumns))));
|
|
@@ -2307,6 +2668,26 @@ function completionCandidates(value, descriptors, skills) {
|
|
|
2307
2668
|
description: "switch the model",
|
|
2308
2669
|
origin: "command"
|
|
2309
2670
|
},
|
|
2671
|
+
{
|
|
2672
|
+
label: "/mode",
|
|
2673
|
+
description: "select the agent preset",
|
|
2674
|
+
origin: "command"
|
|
2675
|
+
},
|
|
2676
|
+
{
|
|
2677
|
+
label: "/new",
|
|
2678
|
+
description: "start a fresh session",
|
|
2679
|
+
origin: "command"
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
label: "/resume",
|
|
2683
|
+
description: "browse or switch sessions",
|
|
2684
|
+
origin: "command"
|
|
2685
|
+
},
|
|
2686
|
+
{
|
|
2687
|
+
label: "/plugin",
|
|
2688
|
+
description: "inspect the plugin composition",
|
|
2689
|
+
origin: "command"
|
|
2690
|
+
},
|
|
2310
2691
|
{
|
|
2311
2692
|
label: "/clear",
|
|
2312
2693
|
description: "clear the screen",
|
|
@@ -2362,16 +2743,19 @@ function CompletionMenu({ active, mention, index, rows }) {
|
|
|
2362
2743
|
const columns = stdout?.columns ?? 80;
|
|
2363
2744
|
const terminalRows = stdout?.rows ?? 30;
|
|
2364
2745
|
if (!active) return void 0;
|
|
2365
|
-
const
|
|
2366
|
-
const
|
|
2746
|
+
const contentColumns = Math.max(1, columns - 4);
|
|
2747
|
+
const nameWidth = Math.min(18, Math.max(1, contentColumns - 2), Math.max(0, ...rows.map((row) => visibleColumns(row.label))) + 2);
|
|
2748
|
+
const descBudget = Math.max(0, contentColumns - nameWidth - 2);
|
|
2367
2749
|
const showFooter = terminalRows >= 12;
|
|
2368
|
-
const
|
|
2750
|
+
const verticalPadding = terminalRows >= 14 ? 1 : 0;
|
|
2751
|
+
const limit = Math.max(1, Math.min(6, terminalRows - (showFooter ? 11 : 10) - verticalPadding * 2));
|
|
2369
2752
|
const selected = rows.length === 0 ? 0 : index % rows.length;
|
|
2370
2753
|
const first = selectionWindow(selected, rows.length, limit);
|
|
2371
2754
|
const visible = rows.slice(first, first + limit);
|
|
2372
2755
|
return createElement(Box, {
|
|
2373
2756
|
flexDirection: "column",
|
|
2374
|
-
marginLeft: 2
|
|
2757
|
+
marginLeft: 2,
|
|
2758
|
+
paddingY: verticalPadding
|
|
2375
2759
|
}, ...rows.length === 0 ? [createElement(Text, {
|
|
2376
2760
|
key: "loading",
|
|
2377
2761
|
dimColor: true
|
|
@@ -2393,7 +2777,7 @@ function CompletionMenu({ active, mention, index, rows }) {
|
|
|
2393
2777
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
2394
2778
|
* box passes every key through untouched.
|
|
2395
2779
|
*/
|
|
2396
|
-
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openHelp, notify, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle }) {
|
|
2780
|
+
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openHelp, openMode, openResume, openPlugin, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle }) {
|
|
2397
2781
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
2398
2782
|
const [value, setValue] = useState("");
|
|
2399
2783
|
const [cursor, setCursor] = useState(0);
|
|
@@ -2401,6 +2785,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2401
2785
|
const historyIndex = useRef(null);
|
|
2402
2786
|
const draft = useRef("");
|
|
2403
2787
|
const [completionIndex, setCompletionIndex] = useState(0);
|
|
2788
|
+
const [dismissedMenuValue, setDismissedMenuValue] = useState(void 0);
|
|
2404
2789
|
const candidates = completionCandidates(value, descriptors, skills);
|
|
2405
2790
|
const slashActive = candidates.length > 0 && value.startsWith("/") && !value.includes(" ") && !value.includes("\n");
|
|
2406
2791
|
const beforeCursor = value.slice(0, cursor);
|
|
@@ -2449,7 +2834,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2449
2834
|
mentionActive,
|
|
2450
2835
|
mentionToken?.query
|
|
2451
2836
|
]);
|
|
2452
|
-
const menuActive = (slashActive || mentionActive || pathActive) &&
|
|
2837
|
+
const menuActive = (slashActive || mentionActive || pathActive) && dismissedMenuValue !== value;
|
|
2453
2838
|
const menuRows = mentionActive ? mentionRows.map((row) => ({
|
|
2454
2839
|
label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
|
|
2455
2840
|
description: row.description,
|
|
@@ -2462,8 +2847,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2462
2847
|
useInput((input, key) => {
|
|
2463
2848
|
if (!active) return;
|
|
2464
2849
|
if (key.tab && key.shift) {
|
|
2465
|
-
|
|
2466
|
-
|
|
2850
|
+
try {
|
|
2851
|
+
const next = cyclePermission();
|
|
2852
|
+
if (next !== "") notify(`permission → ${next}`);
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2855
|
+
}
|
|
2467
2856
|
return;
|
|
2468
2857
|
}
|
|
2469
2858
|
if (key.ctrl && input === "r") {
|
|
@@ -2480,15 +2869,24 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2480
2869
|
setValue("");
|
|
2481
2870
|
setCursor(0);
|
|
2482
2871
|
setCompletionIndex(0);
|
|
2872
|
+
setDismissedMenuValue(void 0);
|
|
2483
2873
|
} else quit();
|
|
2484
2874
|
return;
|
|
2485
2875
|
}
|
|
2486
2876
|
if (key.ctrl && input === "d") {
|
|
2487
|
-
if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)");
|
|
2877
|
+
if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)", "warning");
|
|
2488
2878
|
else quit();
|
|
2489
2879
|
return;
|
|
2490
2880
|
}
|
|
2491
2881
|
if (key.escape) {
|
|
2882
|
+
if (menuActive) {
|
|
2883
|
+
setDismissedMenuValue(value);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (hasNotice) {
|
|
2887
|
+
dismissNotice();
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2492
2890
|
if (busy) interrupt();
|
|
2493
2891
|
return;
|
|
2494
2892
|
}
|
|
@@ -2496,13 +2894,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2496
2894
|
if (key.meta || key.ctrl && input === "j") {
|
|
2497
2895
|
setValue(value.slice(0, cursor) + "\n" + value.slice(cursor));
|
|
2498
2896
|
setCursor(cursor + 1);
|
|
2897
|
+
setDismissedMenuValue(void 0);
|
|
2499
2898
|
return;
|
|
2500
2899
|
}
|
|
2501
2900
|
const text = value.trim();
|
|
2502
2901
|
setValue("");
|
|
2503
2902
|
setCursor(0);
|
|
2504
2903
|
setCompletionIndex(0);
|
|
2904
|
+
setDismissedMenuValue(void 0);
|
|
2505
2905
|
if (text === "") return;
|
|
2906
|
+
dismissNotice();
|
|
2506
2907
|
history.current = [...history.current, text];
|
|
2507
2908
|
historyIndex.current = null;
|
|
2508
2909
|
if (text === "/quit") {
|
|
@@ -2516,6 +2917,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2516
2917
|
if (text === "/clear") {
|
|
2517
2918
|
refresh();
|
|
2518
2919
|
clearView();
|
|
2920
|
+
dismissNotice();
|
|
2519
2921
|
return;
|
|
2520
2922
|
}
|
|
2521
2923
|
if (text === "/export" || text.startsWith("/export ")) {
|
|
@@ -2523,13 +2925,36 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2523
2925
|
return;
|
|
2524
2926
|
}
|
|
2525
2927
|
if (text === "/title" || text.startsWith("/title ")) {
|
|
2526
|
-
|
|
2928
|
+
const outcome = renameTitle(text.slice(7));
|
|
2929
|
+
notify(outcome, outcome.startsWith("rename failed:") ? "error" : outcome.startsWith("usage:") || outcome.includes("unavailable") ? "warning" : "info");
|
|
2527
2930
|
return;
|
|
2528
2931
|
}
|
|
2529
2932
|
if (text === "/model" || text.startsWith("/model ")) {
|
|
2530
2933
|
openModel();
|
|
2531
2934
|
return;
|
|
2532
2935
|
}
|
|
2936
|
+
if (text === "/mode" || text.startsWith("/mode ")) {
|
|
2937
|
+
if (text.slice(5).trim() === "") openMode();
|
|
2938
|
+
else dispatch(text);
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (text === "/resume cancel") {
|
|
2942
|
+
notify(cancelSessionSwitch() ? "pending session switch cancelled" : "no pending session switch", "info");
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
if (text === "/resume" || text.startsWith("/resume ")) {
|
|
2946
|
+
if (text.slice(7).trim() === "") openResume();
|
|
2947
|
+
else dispatch(text);
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
if (text === "/new" || text.startsWith("/new ")) {
|
|
2951
|
+
createSession(text.slice(4).trim() || void 0);
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
if (text === "/plugin" || text.startsWith("/plugin ")) {
|
|
2955
|
+
openPlugin(text.slice(7).trim());
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2533
2958
|
if (busy && !text.startsWith("/")) {
|
|
2534
2959
|
steer(text);
|
|
2535
2960
|
return;
|
|
@@ -2553,6 +2978,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2553
2978
|
historyIndex.current = next;
|
|
2554
2979
|
setValue(entries[next] ?? "");
|
|
2555
2980
|
setCursor((entries[next] ?? "").length);
|
|
2981
|
+
setDismissedMenuValue(void 0);
|
|
2556
2982
|
return;
|
|
2557
2983
|
}
|
|
2558
2984
|
if (key.downArrow) {
|
|
@@ -2563,11 +2989,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2563
2989
|
historyIndex.current = null;
|
|
2564
2990
|
setValue(draft.current);
|
|
2565
2991
|
setCursor(draft.current.length);
|
|
2992
|
+
setDismissedMenuValue(void 0);
|
|
2566
2993
|
return;
|
|
2567
2994
|
}
|
|
2568
2995
|
historyIndex.current = next;
|
|
2569
2996
|
setValue(entries[next] ?? "");
|
|
2570
2997
|
setCursor((entries[next] ?? "").length);
|
|
2998
|
+
setDismissedMenuValue(void 0);
|
|
2571
2999
|
return;
|
|
2572
3000
|
}
|
|
2573
3001
|
if (key.tab && menuActive) {
|
|
@@ -2593,6 +3021,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2593
3021
|
}
|
|
2594
3022
|
}
|
|
2595
3023
|
setCompletionIndex(0);
|
|
3024
|
+
setDismissedMenuValue(void 0);
|
|
2596
3025
|
return;
|
|
2597
3026
|
}
|
|
2598
3027
|
if (key.backspace || key.delete) {
|
|
@@ -2600,6 +3029,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2600
3029
|
setValue(value.slice(0, cursor - 1) + value.slice(cursor));
|
|
2601
3030
|
setCursor(cursor - 1);
|
|
2602
3031
|
setCompletionIndex(0);
|
|
3032
|
+
setDismissedMenuValue(void 0);
|
|
2603
3033
|
}
|
|
2604
3034
|
return;
|
|
2605
3035
|
}
|
|
@@ -2614,10 +3044,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2614
3044
|
if (key.ctrl && input === "u") {
|
|
2615
3045
|
setValue("");
|
|
2616
3046
|
setCursor(0);
|
|
3047
|
+
setDismissedMenuValue(void 0);
|
|
2617
3048
|
return;
|
|
2618
3049
|
}
|
|
2619
3050
|
if (key.ctrl && input === "k") {
|
|
2620
3051
|
setValue(value.slice(0, cursor));
|
|
3052
|
+
setDismissedMenuValue(void 0);
|
|
2621
3053
|
return;
|
|
2622
3054
|
}
|
|
2623
3055
|
if (key.ctrl && input === "l") {
|
|
@@ -2636,6 +3068,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2636
3068
|
setValue(value.slice(0, cursor) + input + value.slice(cursor));
|
|
2637
3069
|
setCursor(cursor + input.length);
|
|
2638
3070
|
setCompletionIndex(0);
|
|
3071
|
+
setDismissedMenuValue(void 0);
|
|
2639
3072
|
}
|
|
2640
3073
|
});
|
|
2641
3074
|
if (frozen) {
|
|
@@ -2667,18 +3100,23 @@ function App(props) {
|
|
|
2667
3100
|
const [modelOpen, setModelOpen] = useState(false);
|
|
2668
3101
|
const [directory, setDirectory] = useState(void 0);
|
|
2669
3102
|
const [modelError, setModelError] = useState(void 0);
|
|
2670
|
-
const [
|
|
2671
|
-
const
|
|
2672
|
-
|
|
2673
|
-
|
|
3103
|
+
const [modelLoadEpoch, setModelLoadEpoch] = useState(0);
|
|
3104
|
+
const [notice, setNotice] = useState(void 0);
|
|
3105
|
+
const notify = useCallback((text, tone = "info") => {
|
|
3106
|
+
setNotice({
|
|
3107
|
+
text,
|
|
3108
|
+
tone
|
|
3109
|
+
});
|
|
3110
|
+
}, []);
|
|
2674
3111
|
useEffect(() => {
|
|
2675
3112
|
props.onBridgeReady({ notify });
|
|
2676
3113
|
}, []);
|
|
2677
3114
|
useEffect(() => {
|
|
2678
|
-
if (!modelOpen
|
|
3115
|
+
if (!modelOpen) return;
|
|
2679
3116
|
let cancelled = false;
|
|
3117
|
+
setDirectory(void 0);
|
|
2680
3118
|
setModelError(void 0);
|
|
2681
|
-
props.loadModels().then((loaded) => {
|
|
3119
|
+
Promise.resolve().then(() => props.loadModels()).then((loaded) => {
|
|
2682
3120
|
if (!cancelled) setDirectory(loaded);
|
|
2683
3121
|
}, (error) => {
|
|
2684
3122
|
if (!cancelled) setModelError(error instanceof Error ? error.message : String(error));
|
|
@@ -2686,21 +3124,32 @@ function App(props) {
|
|
|
2686
3124
|
return () => {
|
|
2687
3125
|
cancelled = true;
|
|
2688
3126
|
};
|
|
2689
|
-
}, [
|
|
3127
|
+
}, [
|
|
3128
|
+
modelOpen,
|
|
3129
|
+
modelLoadEpoch,
|
|
3130
|
+
props.loadModels
|
|
3131
|
+
]);
|
|
2690
3132
|
const busy = view.busy;
|
|
2691
3133
|
const [showReasoning, setShowReasoning] = useState(false);
|
|
2692
3134
|
const [verboseOpen, setVerboseOpen] = useState(false);
|
|
2693
3135
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
3136
|
+
const [modeOpen, setModeOpen] = useState(false);
|
|
3137
|
+
const [resumeOpen, setResumeOpen] = useState(false);
|
|
3138
|
+
const [pluginOpen, setPluginOpen] = useState(false);
|
|
3139
|
+
const [pluginQuery, setPluginQuery] = useState("");
|
|
2694
3140
|
const [refreshEpoch, setRefreshEpoch] = useState(0);
|
|
2695
3141
|
const approvalSnapshot = useSyncExternalStore(props.approval.subscribe, props.approval.getSnapshot);
|
|
2696
3142
|
const questionSnapshot = useSyncExternalStore(props.questions.subscribe, props.questions.getSnapshot);
|
|
2697
3143
|
const approvalPending = approvalSnapshot.pending !== void 0;
|
|
2698
3144
|
const questionPending = questionSnapshot.pending !== void 0;
|
|
2699
|
-
const inputActive = !modelOpen && !helpOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
3145
|
+
const inputActive = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
2700
3146
|
useEffect(() => {
|
|
2701
3147
|
if (!approvalPending && !questionPending) return;
|
|
2702
3148
|
setModelOpen(false);
|
|
2703
3149
|
setHelpOpen(false);
|
|
3150
|
+
setModeOpen(false);
|
|
3151
|
+
setResumeOpen(false);
|
|
3152
|
+
setPluginOpen(false);
|
|
2704
3153
|
setVerboseOpen(false);
|
|
2705
3154
|
}, [approvalPending, questionPending]);
|
|
2706
3155
|
const settled = useMemo(() => settledEntryCount(view.entries), [view.entries]);
|
|
@@ -2715,14 +3164,19 @@ function App(props) {
|
|
|
2715
3164
|
showReasoning,
|
|
2716
3165
|
verbose: false
|
|
2717
3166
|
});
|
|
2718
|
-
|
|
2719
|
-
|
|
3167
|
+
const roomyPrompt = entry.kind === "user" && !entry.notice;
|
|
3168
|
+
if (roomyPrompt) rows.push(createElement(Box, {
|
|
3169
|
+
key: `prompt-before-${index}`,
|
|
2720
3170
|
paddingX: 1
|
|
2721
3171
|
}, createElement(Text, null, " ")));
|
|
2722
3172
|
rows.push(createElement(Box, {
|
|
2723
3173
|
key: index,
|
|
2724
3174
|
paddingX: 1
|
|
2725
3175
|
}, row));
|
|
3176
|
+
if (roomyPrompt) rows.push(createElement(Box, {
|
|
3177
|
+
key: `prompt-after-${index}`,
|
|
3178
|
+
paddingX: 1
|
|
3179
|
+
}, createElement(Text, null, " ")));
|
|
2726
3180
|
});
|
|
2727
3181
|
return rows;
|
|
2728
3182
|
}, [
|
|
@@ -2762,7 +3216,8 @@ function App(props) {
|
|
|
2762
3216
|
}, [appStdout]);
|
|
2763
3217
|
const terminalRows = terminalSize.rows;
|
|
2764
3218
|
const terminalColumns = terminalSize.columns;
|
|
2765
|
-
const
|
|
3219
|
+
const composerGutterRows = layoutGutterRows(terminalRows);
|
|
3220
|
+
const dynamicRows = Math.max(1, terminalRows - 12 - composerGutterRows);
|
|
2766
3221
|
const streamingActive = view.streaming !== "" || view.streamingReasoning !== "";
|
|
2767
3222
|
const deepDivingVisible = busy && !streamingActive;
|
|
2768
3223
|
const allLiveLines = useMemo(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2))), [
|
|
@@ -2775,8 +3230,8 @@ function App(props) {
|
|
|
2775
3230
|
const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
|
|
2776
3231
|
const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
|
|
2777
3232
|
const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
|
|
2778
|
-
const transcriptVisible = !modelOpen && !helpOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
2779
|
-
const modalVisible = modelOpen || helpOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
|
|
3233
|
+
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
3234
|
+
const modalVisible = modelOpen || helpOpen || modeOpen || resumeOpen || pluginOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
|
|
2780
3235
|
const closeInspector = useCallback(() => {
|
|
2781
3236
|
setVerboseOpen(false);
|
|
2782
3237
|
}, []);
|
|
@@ -2801,17 +3256,25 @@ function App(props) {
|
|
|
2801
3256
|
maxRows: answerRows
|
|
2802
3257
|
}, busy ? createElement(Caret) : void 0) : void 0, deepDivingVisible ? createElement(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : void 0, createElement(QuestionBar, {
|
|
2803
3258
|
store: props.questions,
|
|
3259
|
+
snapshot: questionSnapshot,
|
|
2804
3260
|
locked: false
|
|
2805
3261
|
}), createElement(ApprovalBar, {
|
|
2806
|
-
|
|
3262
|
+
snapshot: approvalSnapshot,
|
|
2807
3263
|
locked: questionPending
|
|
2808
3264
|
}), modelOpen && !approvalPending && !questionPending ? createElement(ModelPanel, {
|
|
2809
3265
|
directory,
|
|
2810
3266
|
error: modelError,
|
|
2811
3267
|
onSelect: (row) => {
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3268
|
+
try {
|
|
3269
|
+
setModelLabel(props.selectModel(row));
|
|
3270
|
+
notify(`model → next step uses ${row.provider}/${row.model}`);
|
|
3271
|
+
setModelOpen(false);
|
|
3272
|
+
} catch (error) {
|
|
3273
|
+
notify(`model switch failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3274
|
+
}
|
|
3275
|
+
},
|
|
3276
|
+
onRetry: () => {
|
|
3277
|
+
setModelLoadEpoch((epoch) => epoch + 1);
|
|
2815
3278
|
},
|
|
2816
3279
|
onClose: () => {
|
|
2817
3280
|
setModelOpen(false);
|
|
@@ -2819,17 +3282,45 @@ function App(props) {
|
|
|
2819
3282
|
}) : void 0, helpOpen && !approvalPending && !questionPending ? createElement(HelpPanel, {
|
|
2820
3283
|
descriptors,
|
|
2821
3284
|
skills,
|
|
3285
|
+
commandError: props.commands.error,
|
|
3286
|
+
skillError: props.skills.error,
|
|
2822
3287
|
onClose: () => {
|
|
2823
3288
|
setHelpOpen(false);
|
|
2824
3289
|
}
|
|
2825
3290
|
}) : void 0, verboseOpen && !approvalPending && !questionPending ? createElement(MemoVerbosePanel, {
|
|
2826
3291
|
entries: view.entries,
|
|
2827
3292
|
onClose: closeInspector
|
|
2828
|
-
}) : void 0,
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
3293
|
+
}) : void 0, modeOpen && !approvalPending && !questionPending ? createElement(ModePanel, {
|
|
3294
|
+
current: props.mode,
|
|
3295
|
+
load: props.loadPresets,
|
|
3296
|
+
select: (id) => {
|
|
3297
|
+
props.switchMode(id).then((label) => {
|
|
3298
|
+
notify(`mode → ${label}`);
|
|
3299
|
+
setModeOpen(false);
|
|
3300
|
+
}, (reason) => notify(`mode switch failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error"));
|
|
3301
|
+
},
|
|
3302
|
+
close: () => setModeOpen(false)
|
|
3303
|
+
}) : void 0, resumeOpen && !approvalPending && !questionPending ? createElement(ResumePanel, {
|
|
3304
|
+
currentCwd: props.workspaceRoot,
|
|
3305
|
+
load: props.loadSessions,
|
|
3306
|
+
readTranscript: props.loadSessionTranscript,
|
|
3307
|
+
select: (row) => {
|
|
3308
|
+
props.switchSession(row);
|
|
3309
|
+
setResumeOpen(false);
|
|
3310
|
+
},
|
|
3311
|
+
close: () => setResumeOpen(false)
|
|
3312
|
+
}) : void 0, pluginOpen && !approvalPending && !questionPending ? createElement(PluginPanel, {
|
|
3313
|
+
load: props.loadPlugins,
|
|
3314
|
+
initialQuery: pluginQuery,
|
|
3315
|
+
close: () => setPluginOpen(false)
|
|
3316
|
+
}) : void 0, notice === void 0 ? void 0 : createElement(NoticeLine, {
|
|
3317
|
+
text: notice.text,
|
|
3318
|
+
tone: notice.tone,
|
|
3319
|
+
columns: terminalColumns
|
|
3320
|
+
}), createElement(Box, {
|
|
3321
|
+
flexDirection: "column",
|
|
3322
|
+
marginTop: composerGutterRows
|
|
3323
|
+
}, createElement(Input, {
|
|
2833
3324
|
active: inputActive,
|
|
2834
3325
|
frozen: modalVisible,
|
|
2835
3326
|
busy,
|
|
@@ -2840,12 +3331,26 @@ function App(props) {
|
|
|
2840
3331
|
interrupt: props.interrupt,
|
|
2841
3332
|
quit: props.quit,
|
|
2842
3333
|
openModel: () => {
|
|
3334
|
+
setDirectory(void 0);
|
|
3335
|
+
setModelError(void 0);
|
|
2843
3336
|
setModelOpen(true);
|
|
2844
3337
|
},
|
|
2845
3338
|
openHelp: () => {
|
|
2846
3339
|
setHelpOpen(true);
|
|
2847
3340
|
},
|
|
3341
|
+
openMode: () => setModeOpen(true),
|
|
3342
|
+
openResume: () => setResumeOpen(true),
|
|
3343
|
+
openPlugin: (query = "") => {
|
|
3344
|
+
setPluginQuery(query);
|
|
3345
|
+
setPluginOpen(true);
|
|
3346
|
+
},
|
|
3347
|
+
createSession: props.createSession,
|
|
3348
|
+
cancelSessionSwitch: props.cancelSessionSwitch,
|
|
2848
3349
|
notify,
|
|
3350
|
+
hasNotice: notice !== void 0,
|
|
3351
|
+
dismissNotice: () => {
|
|
3352
|
+
setNotice(void 0);
|
|
3353
|
+
},
|
|
2849
3354
|
openVerbose: () => {
|
|
2850
3355
|
setVerboseOpen(true);
|
|
2851
3356
|
},
|
|
@@ -2863,6 +3368,7 @@ function App(props) {
|
|
|
2863
3368
|
}), createElement(StatusLine, {
|
|
2864
3369
|
facts: {
|
|
2865
3370
|
model: modelLabel,
|
|
3371
|
+
mode: props.mode,
|
|
2866
3372
|
cwd: props.cwd,
|
|
2867
3373
|
branch: props.branch,
|
|
2868
3374
|
sessionId: props.sessionId,
|
|
@@ -2975,10 +3481,17 @@ function watchCommands(ctx) {
|
|
|
2975
3481
|
const commands = ctx.get("commands");
|
|
2976
3482
|
let agent;
|
|
2977
3483
|
let descriptors = [];
|
|
3484
|
+
let error;
|
|
2978
3485
|
const listeners = /* @__PURE__ */ new Set();
|
|
2979
3486
|
const refresh = () => {
|
|
2980
3487
|
if (commands === void 0 || agent === void 0) return;
|
|
2981
|
-
|
|
3488
|
+
try {
|
|
3489
|
+
descriptors = commands.list(agent);
|
|
3490
|
+
error = void 0;
|
|
3491
|
+
} catch (cause) {
|
|
3492
|
+
descriptors = [...descriptors];
|
|
3493
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
3494
|
+
}
|
|
2982
3495
|
for (const listener of listeners) listener();
|
|
2983
3496
|
};
|
|
2984
3497
|
if (commands !== void 0) ctx.on("commands/change", () => refresh());
|
|
@@ -2986,6 +3499,9 @@ function watchCommands(ctx) {
|
|
|
2986
3499
|
get descriptors() {
|
|
2987
3500
|
return descriptors;
|
|
2988
3501
|
},
|
|
3502
|
+
get error() {
|
|
3503
|
+
return error;
|
|
3504
|
+
},
|
|
2989
3505
|
subscribe(listener) {
|
|
2990
3506
|
listeners.add(listener);
|
|
2991
3507
|
return () => {
|
|
@@ -3019,9 +3535,14 @@ function isSlashLine(line) {
|
|
|
3019
3535
|
const internals = {
|
|
3020
3536
|
mount: (element) => {
|
|
3021
3537
|
const instance = render(element);
|
|
3022
|
-
return {
|
|
3023
|
-
|
|
3024
|
-
|
|
3538
|
+
return {
|
|
3539
|
+
rerender(element) {
|
|
3540
|
+
instance.rerender(element);
|
|
3541
|
+
},
|
|
3542
|
+
unmount() {
|
|
3543
|
+
instance.unmount();
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3025
3546
|
},
|
|
3026
3547
|
stderr: process.stderr
|
|
3027
3548
|
};
|
|
@@ -3345,24 +3866,36 @@ function watchSkills(ctx) {
|
|
|
3345
3866
|
const skills = ctx.get("skills");
|
|
3346
3867
|
let agent;
|
|
3347
3868
|
let rows = [];
|
|
3869
|
+
let error;
|
|
3348
3870
|
const listeners = /* @__PURE__ */ new Set();
|
|
3349
3871
|
const reload = () => {
|
|
3350
|
-
|
|
3351
|
-
skills
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3872
|
+
const currentAgent = agent;
|
|
3873
|
+
if (skills === void 0 || currentAgent === void 0) return;
|
|
3874
|
+
Promise.resolve().then(() => skills.list({
|
|
3875
|
+
cwd: currentAgent.session.header.cwd,
|
|
3876
|
+
scope: currentAgent
|
|
3877
|
+
})).then((summaries) => {
|
|
3355
3878
|
const next = toRows(summaries);
|
|
3356
|
-
|
|
3879
|
+
const unchanged = next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name);
|
|
3357
3880
|
rows = next;
|
|
3881
|
+
const recovered = error !== void 0;
|
|
3882
|
+
error = void 0;
|
|
3883
|
+
if (unchanged && !recovered) return;
|
|
3358
3884
|
for (const listener of listeners) listener();
|
|
3359
|
-
}
|
|
3885
|
+
}).catch((cause) => {
|
|
3886
|
+
rows = [...rows];
|
|
3887
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
3888
|
+
for (const listener of listeners) listener();
|
|
3889
|
+
});
|
|
3360
3890
|
};
|
|
3361
3891
|
if (skills !== void 0) ctx.on("skills/change", reload);
|
|
3362
3892
|
return {
|
|
3363
3893
|
get rows() {
|
|
3364
3894
|
return rows;
|
|
3365
3895
|
},
|
|
3896
|
+
get error() {
|
|
3897
|
+
return error;
|
|
3898
|
+
},
|
|
3366
3899
|
subscribe(listener) {
|
|
3367
3900
|
listeners.add(listener);
|
|
3368
3901
|
return () => {
|
|
@@ -3443,15 +3976,152 @@ function buildExportMarkdown(view, sessionId) {
|
|
|
3443
3976
|
return out.join("\n");
|
|
3444
3977
|
}
|
|
3445
3978
|
//#endregion
|
|
3979
|
+
//#region src/session-switch.ts
|
|
3980
|
+
var SessionSwitchQueue = class {
|
|
3981
|
+
execute;
|
|
3982
|
+
failed;
|
|
3983
|
+
pending;
|
|
3984
|
+
pumping = false;
|
|
3985
|
+
constructor(execute, failed) {
|
|
3986
|
+
this.execute = execute;
|
|
3987
|
+
this.failed = failed;
|
|
3988
|
+
}
|
|
3989
|
+
/** Queue a request; a later request replaces any request still waiting. */
|
|
3990
|
+
request(activity, value) {
|
|
3991
|
+
this.pending = {
|
|
3992
|
+
activity,
|
|
3993
|
+
value
|
|
3994
|
+
};
|
|
3995
|
+
const outcome = activity.status === "running" || this.pumping ? "queued" : "started";
|
|
3996
|
+
if (!this.pumping) this.pump();
|
|
3997
|
+
return outcome;
|
|
3998
|
+
}
|
|
3999
|
+
/** Cancel only work that has not begun activation. */
|
|
4000
|
+
cancel() {
|
|
4001
|
+
if (this.pending === void 0) return false;
|
|
4002
|
+
this.pending = void 0;
|
|
4003
|
+
return true;
|
|
4004
|
+
}
|
|
4005
|
+
async pump() {
|
|
4006
|
+
this.pumping = true;
|
|
4007
|
+
try {
|
|
4008
|
+
while (this.pending !== void 0) {
|
|
4009
|
+
const observed = this.pending;
|
|
4010
|
+
await observed.activity.whenIdle();
|
|
4011
|
+
if (this.pending !== observed) continue;
|
|
4012
|
+
this.pending = void 0;
|
|
4013
|
+
try {
|
|
4014
|
+
await this.execute(observed.value);
|
|
4015
|
+
} catch (error) {
|
|
4016
|
+
this.failed(error);
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
} finally {
|
|
4020
|
+
this.pumping = false;
|
|
4021
|
+
if (this.pending !== void 0) this.pump();
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
};
|
|
4025
|
+
//#endregion
|
|
4026
|
+
//#region src/presets.ts
|
|
4027
|
+
/** Read an optional Cordis service without requiring its package at build time. */
|
|
4028
|
+
function agentPresetsFrom(ctx) {
|
|
4029
|
+
return ctx.get("agentPresets");
|
|
4030
|
+
}
|
|
4031
|
+
/** A preset may change only before the first durable turn begins. */
|
|
4032
|
+
function isBlankSession(events) {
|
|
4033
|
+
return !events.some((event) => event.type === "turn/start");
|
|
4034
|
+
}
|
|
4035
|
+
/** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
|
|
4036
|
+
function resolvePreset(session) {
|
|
4037
|
+
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
|
4038
|
+
const event = session.events[index];
|
|
4039
|
+
if (event.type === "agent-preset/selected" && event.data?.agentPreset !== void 0) return event.data.agentPreset;
|
|
4040
|
+
}
|
|
4041
|
+
return session.header.agentPreset ?? "standard";
|
|
4042
|
+
}
|
|
4043
|
+
/** Recompose atomically from the caller's perspective, logging only success. */
|
|
4044
|
+
async function switchPreset(service, agent, presetId) {
|
|
4045
|
+
if (!isBlankSession(agent.session.events)) throw new Error("mode is locked after the first turn; use /new <mode>");
|
|
4046
|
+
const preset = await service.recompose(agent.ctx, presetId);
|
|
4047
|
+
agent.session.append("agent-preset/selected", { agentPreset: preset.id });
|
|
4048
|
+
return preset;
|
|
4049
|
+
}
|
|
4050
|
+
//#endregion
|
|
4051
|
+
//#region src/plugin-inventory.ts
|
|
4052
|
+
const PHASES = {
|
|
4053
|
+
0: "pending",
|
|
4054
|
+
1: "loading",
|
|
4055
|
+
2: "active",
|
|
4056
|
+
3: "failed",
|
|
4057
|
+
4: null,
|
|
4058
|
+
5: "unloading"
|
|
4059
|
+
};
|
|
4060
|
+
/** Snapshot the live Loader; group-only rows are composition containers, not plugins. */
|
|
4061
|
+
function listPluginRows(ctx) {
|
|
4062
|
+
const loader = ctx.get("loader");
|
|
4063
|
+
if (loader === void 0) return [];
|
|
4064
|
+
const rows = [];
|
|
4065
|
+
for (const entry of loader.entries()) {
|
|
4066
|
+
if (entry.options.group === true) continue;
|
|
4067
|
+
rows.push({
|
|
4068
|
+
entryId: entry.id,
|
|
4069
|
+
moduleName: entry.options.name,
|
|
4070
|
+
enabled: !entry.disabled,
|
|
4071
|
+
phase: entry.fiber === void 0 ? null : PHASES[entry.fiber.state] ?? null
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
return rows;
|
|
4075
|
+
}
|
|
4076
|
+
//#endregion
|
|
4077
|
+
//#region src/session-directory.ts
|
|
4078
|
+
/** Lightweight session-directory projection for the /resume picker. */
|
|
4079
|
+
function samePath(left, right) {
|
|
4080
|
+
if (left === void 0) return false;
|
|
4081
|
+
return resolve(left).toLowerCase() === resolve(right).toLowerCase();
|
|
4082
|
+
}
|
|
4083
|
+
/** Filter/sort header-only records. No session log is loaded here. */
|
|
4084
|
+
function projectSessionRows(records, options) {
|
|
4085
|
+
const needle = options.query.trim().toLowerCase();
|
|
4086
|
+
return records.filter((record) => options.sessions === "all" || record.header.parentSession === void 0 && record.header.origin !== "subagent").filter((record) => options.cwd === "all" || samePath(record.header.cwd, options.currentCwd)).map((record) => {
|
|
4087
|
+
const cwd = record.header.cwd ?? "";
|
|
4088
|
+
const subagent = record.header.origin === "subagent" || record.header.parentSession !== void 0;
|
|
4089
|
+
return {
|
|
4090
|
+
id: record.header.id,
|
|
4091
|
+
createdAt: record.header.createdAt,
|
|
4092
|
+
cwd,
|
|
4093
|
+
workspace: cwd === "" ? "(no workspace)" : basename(cwd),
|
|
4094
|
+
parent: record.header.parentSession,
|
|
4095
|
+
subagent,
|
|
4096
|
+
resumable: !subagent,
|
|
4097
|
+
live: record.live,
|
|
4098
|
+
persisted: record.persisted,
|
|
4099
|
+
preset: record.header.agentPreset ?? "standard"
|
|
4100
|
+
};
|
|
4101
|
+
}).filter((row) => needle === "" || `${row.id} ${row.cwd} ${row.workspace} ${row.preset}`.toLowerCase().includes(needle)).sort((left, right) => options.sort === "newest" ? right.createdAt - left.createdAt : left.createdAt - right.createdAt);
|
|
4102
|
+
}
|
|
4103
|
+
/** Merge page-local title observations without disturbing directory order. */
|
|
4104
|
+
function mergeSessionTitles(rows, observations) {
|
|
4105
|
+
const titles = /* @__PURE__ */ new Map();
|
|
4106
|
+
for (const observation of observations) {
|
|
4107
|
+
if (observation.status !== "fulfilled") continue;
|
|
4108
|
+
const title = observation.value?.title?.title ?? observation.value?.title?.text;
|
|
4109
|
+
if (title !== void 0 && title.trim() !== "") titles.set(observation.sessionId, title);
|
|
4110
|
+
}
|
|
4111
|
+
return rows.map((row) => titles.has(row.id) ? {
|
|
4112
|
+
...row,
|
|
4113
|
+
title: titles.get(row.id)
|
|
4114
|
+
} : row);
|
|
4115
|
+
}
|
|
4116
|
+
//#endregion
|
|
3446
4117
|
//#region src/index.ts
|
|
3447
4118
|
/**
|
|
3448
4119
|
* @deepseek-ai/dsh-code — the interactive terminal driver. The bundle patch
|
|
3449
4120
|
* rides over dsh-base without Host, HTTP, or browser plugins; this runner
|
|
3450
|
-
* creates
|
|
3451
|
-
*
|
|
3452
|
-
* durable session, answers approval asks with a y/n bar,
|
|
3453
|
-
*
|
|
3454
|
-
* process exit.
|
|
4121
|
+
* creates or resumes preset-composed Agents through the core registry, keeps
|
|
4122
|
+
* one Ink owner while the active session changes, folds submitted prompts
|
|
4123
|
+
* into the selected durable session, answers approval asks with a y/n bar,
|
|
4124
|
+
* dispatches slash commands, and on quit flushes and requests process exit.
|
|
3455
4125
|
*
|
|
3456
4126
|
* @module @deepseek-ai/dsh-code
|
|
3457
4127
|
*/
|
|
@@ -3465,7 +4135,8 @@ const inject = [
|
|
|
3465
4135
|
];
|
|
3466
4136
|
const Config = z.object({ startup: z.object({
|
|
3467
4137
|
kind: z.string().required(),
|
|
3468
|
-
sessionId: z.string()
|
|
4138
|
+
sessionId: z.string(),
|
|
4139
|
+
mode: z.string()
|
|
3469
4140
|
}) });
|
|
3470
4141
|
/** Report an unexpected direct-driver failure and request a failing exit. */
|
|
3471
4142
|
function fail(io, error) {
|
|
@@ -3495,11 +4166,13 @@ function gitBranch(cwd) {
|
|
|
3495
4166
|
async function resolveTarget(startup, persistence, cwd) {
|
|
3496
4167
|
if (startup.kind === "fresh") return {
|
|
3497
4168
|
sessionId: `session-${randomUUID()}`,
|
|
3498
|
-
resume: false
|
|
4169
|
+
resume: false,
|
|
4170
|
+
mode: startup.mode
|
|
3499
4171
|
};
|
|
3500
4172
|
if (startup.kind === "named") return {
|
|
3501
4173
|
sessionId: startup.sessionId,
|
|
3502
|
-
resume: false
|
|
4174
|
+
resume: false,
|
|
4175
|
+
mode: startup.mode
|
|
3503
4176
|
};
|
|
3504
4177
|
if (persistence === void 0) throw new Error("cannot resolve the requested session: session persistence is not configured");
|
|
3505
4178
|
const headers = await persistence.list();
|
|
@@ -3550,63 +4223,76 @@ async function run(ctx, startup, io) {
|
|
|
3550
4223
|
const defaultModel = ctx.get("agentDefaultModel");
|
|
3551
4224
|
const sessions = ctx.get("sessions");
|
|
3552
4225
|
const persistence = ctx.get("sessionPersistence");
|
|
4226
|
+
const sessionQuery = ctx.get("sessionQuery");
|
|
3553
4227
|
if (agents === void 0 || defaultModel === void 0 || sessions === void 0) return;
|
|
3554
4228
|
const cwd = process.cwd();
|
|
3555
4229
|
const target = await resolveTarget(startup, persistence, cwd);
|
|
3556
4230
|
const defaults = defaultModel.currentSelection();
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
4231
|
+
const presets = agentPresetsFrom(ctx);
|
|
4232
|
+
if (presets === void 0) throw new Error("agent preset service is unavailable; check the dsh-code bundle patch");
|
|
4233
|
+
/** Prepare a complete next session before disturbing the currently visible one. */
|
|
4234
|
+
const prepare = async (next) => {
|
|
4235
|
+
const nextCwd = next.cwd ?? cwd;
|
|
4236
|
+
const selectionState = {};
|
|
4237
|
+
let mode = next.mode;
|
|
4238
|
+
if (!next.resume) mode = (await presets.resolve(mode)).id;
|
|
4239
|
+
const setup = async (agentCtx) => {
|
|
4240
|
+
const sessionPreset = next.resume ? resolvePreset(agentCtx.agent.session) : mode;
|
|
4241
|
+
mode = (await presets.mount(agentCtx, sessionPreset)).id;
|
|
4242
|
+
installModelSelection(agentCtx, {
|
|
4243
|
+
get current() {
|
|
4244
|
+
if (selectionState.picked !== void 0) return selectionState.picked;
|
|
4245
|
+
const logged = agentCtx.agent?.session.requestHeader()?.config;
|
|
4246
|
+
if (logged !== void 0) return {
|
|
4247
|
+
provider: logged.provider,
|
|
4248
|
+
model: logged.model,
|
|
4249
|
+
...logged.reasoningEffort === void 0 ? {} : { reasoningEffort: logged.reasoningEffort }
|
|
4250
|
+
};
|
|
4251
|
+
return defaults;
|
|
4252
|
+
},
|
|
4253
|
+
set current(value) {
|
|
4254
|
+
selectionState.picked = value;
|
|
4255
|
+
},
|
|
4256
|
+
assembled: void 0
|
|
4257
|
+
});
|
|
4258
|
+
};
|
|
4259
|
+
const handle = next.resume ? await agents.resume({
|
|
4260
|
+
resumeSessionId: SessionId(next.sessionId),
|
|
3563
4261
|
agentOptions: {
|
|
3564
4262
|
provider: defaults.provider,
|
|
3565
4263
|
model: defaults.model
|
|
3566
4264
|
},
|
|
3567
|
-
setup
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
model: logged.model,
|
|
3575
|
-
...logged.reasoningEffort === void 0 ? {} : { reasoningEffort: logged.reasoningEffort }
|
|
3576
|
-
};
|
|
3577
|
-
return defaults;
|
|
3578
|
-
},
|
|
3579
|
-
set current(next) {
|
|
3580
|
-
picked = next;
|
|
3581
|
-
},
|
|
3582
|
-
assembled: void 0
|
|
3583
|
-
});
|
|
3584
|
-
}
|
|
3585
|
-
})).agent;
|
|
3586
|
-
session = agent.session;
|
|
3587
|
-
} else {
|
|
3588
|
-
agent = (await agents.create({
|
|
3589
|
-
sessionId: SessionId(target.sessionId),
|
|
3590
|
-
meta: { cwd },
|
|
4265
|
+
setup
|
|
4266
|
+
}) : await agents.create({
|
|
4267
|
+
sessionId: SessionId(next.sessionId),
|
|
4268
|
+
meta: {
|
|
4269
|
+
cwd: nextCwd,
|
|
4270
|
+
agentPreset: mode
|
|
4271
|
+
},
|
|
3591
4272
|
agentOptions: {
|
|
3592
4273
|
provider: defaults.provider,
|
|
3593
4274
|
model: defaults.model
|
|
3594
4275
|
},
|
|
3595
|
-
setup
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
4276
|
+
setup
|
|
4277
|
+
});
|
|
4278
|
+
const session = handle.agent.session;
|
|
4279
|
+
const sessionCwd = session.header.cwd ?? nextCwd;
|
|
4280
|
+
return {
|
|
4281
|
+
handle,
|
|
4282
|
+
agent: handle.agent,
|
|
4283
|
+
session,
|
|
4284
|
+
store: createTranscriptStore(session.events),
|
|
4285
|
+
mentions: createMentions(ctx, handle.agent, sessionCwd),
|
|
4286
|
+
mode: mode ?? "standard",
|
|
4287
|
+
selection: selectionState,
|
|
4288
|
+
resumed: next.resume
|
|
4289
|
+
};
|
|
4290
|
+
};
|
|
4291
|
+
let active = await prepare(target);
|
|
4292
|
+
let agent = active.agent;
|
|
4293
|
+
let session = active.session;
|
|
4294
|
+
let store = active.store;
|
|
4295
|
+
let mentions = active.mentions;
|
|
3610
4296
|
const off = ctx.on("session/event", (subject, event) => {
|
|
3611
4297
|
if (subject.id === session.id) store.apply(event);
|
|
3612
4298
|
});
|
|
@@ -3616,39 +4302,53 @@ async function run(ctx, startup, io) {
|
|
|
3616
4302
|
skills.setAgent(agent);
|
|
3617
4303
|
const approval = mountApprovalAnswerer(ctx, (candidate) => candidate.id === agent.id, (request) => approvalCommandPreview(store.getView().entries, request.callId, request.toolName));
|
|
3618
4304
|
const questions = mountQuestionProvider(ctx);
|
|
3619
|
-
const mentions = createMentions(ctx, agent, session.header.cwd ?? cwd);
|
|
3620
4305
|
const bridge = { notify: () => {} };
|
|
3621
4306
|
const mountRef = {};
|
|
3622
4307
|
let quitting = false;
|
|
3623
4308
|
const quit = () => {
|
|
3624
4309
|
if (quitting) return;
|
|
3625
4310
|
quitting = true;
|
|
4311
|
+
switchQueue.cancel();
|
|
3626
4312
|
off();
|
|
3627
4313
|
mountRef.current?.unmount();
|
|
3628
4314
|
sessions.flush(session).catch((flushError) => {
|
|
3629
4315
|
internals.stderr.write(`dsh: session flush failed: ${flushError instanceof Error ? flushError.message : String(flushError)}\n`);
|
|
4316
|
+
}).then(() => active.handle.dispose()).catch((disposeError) => {
|
|
4317
|
+
internals.stderr.write(`dsh: agent disposal failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)}\n`);
|
|
3630
4318
|
}).then(() => {
|
|
3631
4319
|
io.exit(0);
|
|
3632
4320
|
});
|
|
3633
4321
|
};
|
|
3634
4322
|
/** Run one slash line through the command registry (closed namespace). */
|
|
3635
4323
|
const runSlash = (line) => {
|
|
4324
|
+
if (line.startsWith("/mode ")) {
|
|
4325
|
+
switchModeAction(line.slice(6).trim());
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
if (line.startsWith("/resume ")) {
|
|
4329
|
+
requestResume(line.slice(8).trim());
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
3636
4332
|
const registry = ctx.get("commands");
|
|
3637
4333
|
if (registry === void 0) {
|
|
3638
|
-
bridge.notify("no command registry is mounted in this composition");
|
|
4334
|
+
bridge.notify("no command registry is mounted in this composition", "error");
|
|
3639
4335
|
return;
|
|
3640
4336
|
}
|
|
3641
4337
|
const controller = new AbortController();
|
|
3642
|
-
registry.execute(agent, line, controller.signal).then((execution) => {
|
|
3643
|
-
if (execution === void 0)
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
4338
|
+
Promise.resolve().then(() => registry.execute(agent, line, controller.signal)).then((execution) => {
|
|
4339
|
+
if (execution === void 0) try {
|
|
4340
|
+
agent.followup(createUserMessage({
|
|
4341
|
+
content: [{
|
|
4342
|
+
type: "text",
|
|
4343
|
+
text: line
|
|
4344
|
+
}],
|
|
4345
|
+
source: { kind: "user" }
|
|
4346
|
+
}));
|
|
4347
|
+
} catch (error) {
|
|
4348
|
+
bridge.notify(`command fallback failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
4349
|
+
}
|
|
3650
4350
|
}, (error) => {
|
|
3651
|
-
bridge.notify(`command failed: ${error instanceof Error ? error.message : String(error)}
|
|
4351
|
+
bridge.notify(`command failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3652
4352
|
});
|
|
3653
4353
|
};
|
|
3654
4354
|
/** Deliver one readable line to the agent, expanding session mentions first. */
|
|
@@ -3663,22 +4363,26 @@ async function run(ctx, startup, io) {
|
|
|
3663
4363
|
try {
|
|
3664
4364
|
parsed = mentions.parse(line);
|
|
3665
4365
|
} catch (error) {
|
|
3666
|
-
bridge.notify(`invalid session reference: ${error instanceof Error ? error.message : String(error)}
|
|
4366
|
+
bridge.notify(`invalid session reference: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3667
4367
|
return;
|
|
3668
4368
|
}
|
|
3669
4369
|
const deliver = (readable, context) => {
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
4370
|
+
try {
|
|
4371
|
+
if (context !== void 0) agent.inject(context);
|
|
4372
|
+
const message = createUserMessage({
|
|
4373
|
+
content: [{
|
|
4374
|
+
type: "text",
|
|
4375
|
+
text: readable
|
|
4376
|
+
}],
|
|
4377
|
+
source: { kind: "user" }
|
|
4378
|
+
});
|
|
4379
|
+
if (mode === "steer") {
|
|
4380
|
+
agent.steer(message);
|
|
4381
|
+
bridge.notify("steering queued — the next step sees it");
|
|
4382
|
+
} else agent.followup(message);
|
|
4383
|
+
} catch (error) {
|
|
4384
|
+
bridge.notify(`${mode === "steer" ? "steering" : "message"} failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
4385
|
+
}
|
|
3682
4386
|
};
|
|
3683
4387
|
if (parsed.references.length === 0) {
|
|
3684
4388
|
deliver(parsed.text);
|
|
@@ -3689,7 +4393,7 @@ async function run(ctx, startup, io) {
|
|
|
3689
4393
|
deliver(prepared.text, prepared.additionalContext);
|
|
3690
4394
|
}, (error) => {
|
|
3691
4395
|
if (controller.signal.aborted) return;
|
|
3692
|
-
bridge.notify(`session reference failed: ${error instanceof Error ? error.message : String(error)}
|
|
4396
|
+
bridge.notify(`session reference failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3693
4397
|
});
|
|
3694
4398
|
};
|
|
3695
4399
|
/** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
|
|
@@ -3707,9 +4411,14 @@ async function run(ctx, startup, io) {
|
|
|
3707
4411
|
/** Interrupt the running turn (Esc); true when a turn was actually cancelled. */
|
|
3708
4412
|
const interrupt = () => {
|
|
3709
4413
|
if (agent.status !== "running") return false;
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
4414
|
+
try {
|
|
4415
|
+
agent.cancel({ kind: "user" });
|
|
4416
|
+
bridge.notify("turn cancelled — Ctrl+C or /quit to exit");
|
|
4417
|
+
return true;
|
|
4418
|
+
} catch (error) {
|
|
4419
|
+
bridge.notify(`cancel failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
4420
|
+
return false;
|
|
4421
|
+
}
|
|
3713
4422
|
};
|
|
3714
4423
|
/**
|
|
3715
4424
|
* Cycle to the next permission preset (Shift+Tab, the Claude-Code
|
|
@@ -3719,18 +4428,23 @@ async function run(ctx, startup, io) {
|
|
|
3719
4428
|
const cyclePermission = () => {
|
|
3720
4429
|
const service = ctx.get("permissionPresets");
|
|
3721
4430
|
if (service === void 0 || service.names.length === 0) {
|
|
3722
|
-
bridge.notify("permission presets are not mounted in this composition");
|
|
4431
|
+
bridge.notify("permission presets are not mounted in this composition", "warning");
|
|
3723
4432
|
return "";
|
|
3724
4433
|
}
|
|
3725
4434
|
const at = service.names.indexOf(service.current(session.events));
|
|
3726
4435
|
const next = service.names[(at + 1) % service.names.length] ?? "";
|
|
3727
4436
|
if (next === "") return "";
|
|
3728
|
-
|
|
3729
|
-
|
|
4437
|
+
try {
|
|
4438
|
+
service.set(session, next);
|
|
4439
|
+
return next;
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
bridge.notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
4442
|
+
return "";
|
|
4443
|
+
}
|
|
3730
4444
|
};
|
|
3731
4445
|
/** Apply one /model selection: takes effect from the next assembled step. */
|
|
3732
4446
|
const selectModel = (row) => {
|
|
3733
|
-
picked = {
|
|
4447
|
+
active.selection.picked = {
|
|
3734
4448
|
provider: row.provider,
|
|
3735
4449
|
model: row.model
|
|
3736
4450
|
};
|
|
@@ -3743,14 +4457,15 @@ async function run(ctx, startup, io) {
|
|
|
3743
4457
|
*/
|
|
3744
4458
|
const exportTranscript = async (argument) => {
|
|
3745
4459
|
const wanted = argument.trim();
|
|
4460
|
+
const sessionCwd = session.header.cwd ?? cwd;
|
|
3746
4461
|
const defaultName = `dsh-session-${session.id.slice(-8)}.md`;
|
|
3747
|
-
const target = wanted === "" ? join(
|
|
4462
|
+
const target = wanted === "" ? join(sessionCwd, defaultName) : /^[a-zA-Z]:[\\/]/u.test(wanted) || wanted.startsWith("/") ? wanted : join(sessionCwd, wanted);
|
|
3748
4463
|
const markdown = buildExportMarkdown(store.getView(), session.id);
|
|
3749
4464
|
try {
|
|
3750
4465
|
await writeFile(target, `${markdown}\n`, "utf8");
|
|
3751
4466
|
bridge.notify(`exported to ${target}`);
|
|
3752
4467
|
} catch (error) {
|
|
3753
|
-
bridge.notify(`export failed: ${error instanceof Error ? error.message : String(error)}
|
|
4468
|
+
bridge.notify(`export failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3754
4469
|
}
|
|
3755
4470
|
};
|
|
3756
4471
|
/**
|
|
@@ -3770,32 +4485,171 @@ async function run(ctx, startup, io) {
|
|
|
3770
4485
|
return `rename failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
3771
4486
|
}
|
|
3772
4487
|
};
|
|
3773
|
-
const
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
4488
|
+
const loadSessions = async (options, signal) => {
|
|
4489
|
+
if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
|
|
4490
|
+
const projected = projectSessionRows(await sessionQuery.listSessions(signal), options);
|
|
4491
|
+
const page = projected.slice(0, 32);
|
|
4492
|
+
if (page.length === 0) return projected;
|
|
4493
|
+
return mergeSessionTitles(projected, await sessionQuery.readTitleSnapshots(page.map((row) => row.id), signal));
|
|
4494
|
+
};
|
|
4495
|
+
const loadSessionTranscript = async (id, signal) => {
|
|
4496
|
+
if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
|
|
4497
|
+
const snapshot = await sessionQuery.readSession(id, signal);
|
|
4498
|
+
return buildExportMarkdown(createTranscriptStore(snapshot.events).getView(), snapshot.session.id);
|
|
4499
|
+
};
|
|
4500
|
+
const switchModeAction = async (id) => {
|
|
4501
|
+
if (id === "") throw new Error("usage: /mode <preset>");
|
|
4502
|
+
const preset = await switchPreset(presets, agent, id);
|
|
4503
|
+
active.mode = preset.id;
|
|
4504
|
+
commands.setAgent(agent);
|
|
4505
|
+
skills.setAgent(agent);
|
|
4506
|
+
renderCurrent();
|
|
4507
|
+
return preset.id;
|
|
4508
|
+
};
|
|
4509
|
+
const activate = async (nextTarget) => {
|
|
4510
|
+
const previous = active;
|
|
4511
|
+
const next = await prepare(nextTarget);
|
|
4512
|
+
active = next;
|
|
4513
|
+
agent = next.agent;
|
|
4514
|
+
session = next.session;
|
|
4515
|
+
store = next.store;
|
|
4516
|
+
mentions = next.mentions;
|
|
4517
|
+
commands.setAgent(agent);
|
|
4518
|
+
skills.setAgent(agent);
|
|
4519
|
+
try {
|
|
4520
|
+
process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
|
|
4521
|
+
renderCurrent();
|
|
4522
|
+
} catch (error) {
|
|
4523
|
+
active = previous;
|
|
4524
|
+
agent = previous.agent;
|
|
4525
|
+
session = previous.session;
|
|
4526
|
+
store = previous.store;
|
|
4527
|
+
mentions = previous.mentions;
|
|
4528
|
+
commands.setAgent(agent);
|
|
4529
|
+
skills.setAgent(agent);
|
|
4530
|
+
await next.handle.dispose();
|
|
4531
|
+
renderCurrent();
|
|
4532
|
+
throw error;
|
|
3797
4533
|
}
|
|
3798
|
-
|
|
4534
|
+
let cleanupWarning;
|
|
4535
|
+
try {
|
|
4536
|
+
await sessions.flush(previous.session);
|
|
4537
|
+
} catch (error) {
|
|
4538
|
+
cleanupWarning = `previous session flush failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
4539
|
+
}
|
|
4540
|
+
try {
|
|
4541
|
+
await previous.handle.dispose();
|
|
4542
|
+
} catch (error) {
|
|
4543
|
+
cleanupWarning = `${cleanupWarning === void 0 ? "" : `${cleanupWarning}; `}previous agent release failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
4544
|
+
}
|
|
4545
|
+
bridge.notify(cleanupWarning === void 0 ? `${next.resumed ? "resumed" : "created"} ${next.session.id.slice(-12)} · mode ${next.mode}` : `switched to ${next.session.id.slice(-12)}, but ${cleanupWarning}`, cleanupWarning === void 0 ? "info" : "warning");
|
|
4546
|
+
};
|
|
4547
|
+
const switchQueue = new SessionSwitchQueue(async (request) => {
|
|
4548
|
+
if (!quitting) await activate(request.target);
|
|
4549
|
+
}, (error) => bridge.notify(`session switch failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
|
|
4550
|
+
const requestSwitch = (request) => {
|
|
4551
|
+
if (request.target.sessionId === session.id) {
|
|
4552
|
+
bridge.notify("that session is already active", "warning");
|
|
4553
|
+
return;
|
|
4554
|
+
}
|
|
4555
|
+
if (switchQueue.request(agent, request) === "queued") bridge.notify(`will switch to ${request.label} when the current turn finishes · /resume cancel to abort`);
|
|
4556
|
+
};
|
|
4557
|
+
const resolveResumeId = async (wanted) => {
|
|
4558
|
+
if (wanted === "") throw new Error("usage: /resume <id|prefix>");
|
|
4559
|
+
if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
|
|
4560
|
+
const records = await sessionQuery.listSessions();
|
|
4561
|
+
const exact = records.filter((record) => record.header.id === wanted);
|
|
4562
|
+
const matches = exact.length > 0 ? exact : records.filter((record) => record.header.id.startsWith(wanted));
|
|
4563
|
+
if (matches.length === 0) throw new Error(`no session matches "${wanted}"`);
|
|
4564
|
+
if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches)`);
|
|
4565
|
+
if (matches[0].header.parentSession !== void 0 || matches[0].header.origin === "subagent") throw new Error("subagent conversations are read-only in /resume; resume a root session");
|
|
4566
|
+
if (agents.get(SessionId(matches[0].header.id)) !== void 0 && matches[0].header.id !== session.id) throw new Error("that session is already live in another owner");
|
|
4567
|
+
return matches[0].header.id;
|
|
4568
|
+
};
|
|
4569
|
+
const requestResume = (wanted) => {
|
|
4570
|
+
resolveResumeId(wanted).then((id) => {
|
|
4571
|
+
requestSwitch({
|
|
4572
|
+
target: {
|
|
4573
|
+
sessionId: id,
|
|
4574
|
+
resume: true
|
|
4575
|
+
},
|
|
4576
|
+
label: id.slice(-12)
|
|
4577
|
+
});
|
|
4578
|
+
}, (error) => bridge.notify(`resume failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
|
|
4579
|
+
};
|
|
4580
|
+
const createSession = (mode) => {
|
|
4581
|
+
const nextCwd = session.header.cwd ?? cwd;
|
|
4582
|
+
const id = `session-${randomUUID()}`;
|
|
4583
|
+
requestSwitch({
|
|
4584
|
+
target: {
|
|
4585
|
+
sessionId: id,
|
|
4586
|
+
resume: false,
|
|
4587
|
+
mode,
|
|
4588
|
+
cwd: nextCwd
|
|
4589
|
+
},
|
|
4590
|
+
label: id.slice(-12)
|
|
4591
|
+
});
|
|
4592
|
+
};
|
|
4593
|
+
const switchSession = (row) => {
|
|
4594
|
+
if (!row.resumable) {
|
|
4595
|
+
bridge.notify("subagent conversations are read-only", "warning");
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
requestSwitch({
|
|
4599
|
+
target: {
|
|
4600
|
+
sessionId: row.id,
|
|
4601
|
+
resume: true
|
|
4602
|
+
},
|
|
4603
|
+
label: row.title ?? row.id.slice(-12)
|
|
4604
|
+
});
|
|
4605
|
+
};
|
|
4606
|
+
const cancelSessionSwitch = () => {
|
|
4607
|
+
return switchQueue.cancel();
|
|
4608
|
+
};
|
|
4609
|
+
const appElement = () => {
|
|
4610
|
+
const sessionCwd = session.header.cwd ?? cwd;
|
|
4611
|
+
const model = store.getView().model !== "" ? store.getView().model : `${defaults.provider}/${defaults.model}`;
|
|
4612
|
+
return createElement(App, {
|
|
4613
|
+
key: session.id,
|
|
4614
|
+
store,
|
|
4615
|
+
approval,
|
|
4616
|
+
questions,
|
|
4617
|
+
commands,
|
|
4618
|
+
skills,
|
|
4619
|
+
model,
|
|
4620
|
+
cwd: basename(sessionCwd),
|
|
4621
|
+
workspaceRoot: sessionCwd,
|
|
4622
|
+
branch: gitBranch(sessionCwd),
|
|
4623
|
+
sessionId: session.id.slice(-8),
|
|
4624
|
+
resumed: active.resumed,
|
|
4625
|
+
mode: active.mode,
|
|
4626
|
+
dispatch,
|
|
4627
|
+
steer,
|
|
4628
|
+
interrupt,
|
|
4629
|
+
quit,
|
|
4630
|
+
loadModels: () => loadModelDirectory(ctx),
|
|
4631
|
+
loadMentions: mentions.candidates,
|
|
4632
|
+
cyclePermission,
|
|
4633
|
+
selectModel,
|
|
4634
|
+
exportTranscript,
|
|
4635
|
+
renameTitle,
|
|
4636
|
+
loadPresets: () => presets.list(),
|
|
4637
|
+
switchMode: switchModeAction,
|
|
4638
|
+
createSession,
|
|
4639
|
+
loadSessions,
|
|
4640
|
+
loadSessionTranscript,
|
|
4641
|
+
switchSession,
|
|
4642
|
+
cancelSessionSwitch,
|
|
4643
|
+
loadPlugins: () => listPluginRows(ctx),
|
|
4644
|
+
onBridgeReady: (instance) => {
|
|
4645
|
+
bridge.notify = instance.notify;
|
|
4646
|
+
}
|
|
4647
|
+
});
|
|
4648
|
+
};
|
|
4649
|
+
const renderCurrent = () => {
|
|
4650
|
+
mountRef.current?.rerender(appElement());
|
|
4651
|
+
};
|
|
4652
|
+
mountRef.current = io.mount(appElement());
|
|
3799
4653
|
}
|
|
3800
4654
|
/**
|
|
3801
4655
|
* Mount the interactive terminal driver.
|
|
@@ -3808,8 +4662,12 @@ function apply(ctx, config) {
|
|
|
3808
4662
|
sessionId: config.startup.sessionId
|
|
3809
4663
|
} : config.startup.kind === "latest" ? { kind: "latest" } : config.startup.kind === "named" && config.startup.sessionId !== void 0 ? {
|
|
3810
4664
|
kind: "named",
|
|
3811
|
-
sessionId: config.startup.sessionId
|
|
3812
|
-
|
|
4665
|
+
sessionId: config.startup.sessionId,
|
|
4666
|
+
...config.startup.mode === void 0 ? {} : { mode: config.startup.mode }
|
|
4667
|
+
} : {
|
|
4668
|
+
kind: "fresh",
|
|
4669
|
+
...config.startup.mode === void 0 ? {} : { mode: config.startup.mode }
|
|
4670
|
+
};
|
|
3813
4671
|
const exit = ctx.get("appExit");
|
|
3814
4672
|
if (exit === void 0) throw new Error("tui-runner: the launcher must provide ctx.appExit before the tree mounts");
|
|
3815
4673
|
const io = {
|