dsh-code 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +12 -3
- package/README.md +12 -3
- package/lib/index.mjs +1079 -204
- package/lib/types/app.d.ts +13 -0
- package/lib/types/approval.d.ts +3 -1
- package/lib/types/kernel-panels.d.ts +58 -8
- package/lib/types/models.d.ts +15 -1
- package/lib/types/render/projection.d.ts +2 -0
- package/lib/types/render/tool-preview.d.ts +10 -0
- package/lib/types/session-directory.d.ts +46 -2
- package/lib/types/subagents.d.ts +60 -0
- package/package.json +1 -1
- package/src/app.ts +359 -75
- package/src/approval.ts +161 -135
- package/src/index.ts +175 -8
- package/src/kernel-panels.ts +310 -30
- package/src/models.ts +26 -0
- package/src/render/lines.ts +236 -233
- package/src/render/projection.ts +5 -1
- package/src/render/tool-preview.ts +77 -50
- package/src/session-directory.ts +128 -6
- package/src/subagents.ts +165 -0
package/lib/index.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-run
|
|
|
2
2
|
import { a as getPalette, c as parseThemeName, i as error, l as setTheme, n as brand, o as getTheme, r as dim, s as inkColor, u as chalk } from "./theme-BEi4i_aN.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
6
6
|
import os, { homedir } from "node:os";
|
|
7
|
-
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { basename, dirname, join, resolve } from "node:path";
|
|
9
9
|
import z from "@deepseek-ai/schemastery";
|
|
10
10
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
@@ -24633,6 +24633,30 @@ function toolArgumentsPreview(args, toolName) {
|
|
|
24633
24633
|
} catch {}
|
|
24634
24634
|
return boundedRawPreview(args);
|
|
24635
24635
|
}
|
|
24636
|
+
/** Visible budget for one delegation prompt row on the tool card. */
|
|
24637
|
+
const MAX_PROMPT_CHARS = 160;
|
|
24638
|
+
/**
|
|
24639
|
+
* Bounded prompt preview for delegation-style tools (`subagent`): the
|
|
24640
|
+
* `prompt` argument rendered as the card's second row, so the transcript
|
|
24641
|
+
* shows what the child agent was asked — not just its description label —
|
|
24642
|
+
* while it runs (Codex's SpawnAgent card preview). Anything else returns ''.
|
|
24643
|
+
* @param toolName - the tool the arguments belong to.
|
|
24644
|
+
* @param args - raw JSON arguments string as the model produced it.
|
|
24645
|
+
* @returns the one-line prompt preview, or '' when none applies.
|
|
24646
|
+
*/
|
|
24647
|
+
function toolPromptPreview(toolName, args) {
|
|
24648
|
+
if (toolName !== "subagent" || args === "" || args.length > MAX_PARSE_CHARS) return "";
|
|
24649
|
+
try {
|
|
24650
|
+
const parsed = JSON.parse(args);
|
|
24651
|
+
if (parsed === null || typeof parsed !== "object") return "";
|
|
24652
|
+
const prompt = parsed["prompt"];
|
|
24653
|
+
if (typeof prompt !== "string" || prompt === "") return "";
|
|
24654
|
+
const flat = prompt.replace(/\s+/gu, " ").trim();
|
|
24655
|
+
return flat.length > MAX_PROMPT_CHARS ? `${flat.slice(0, 159)}…` : flat;
|
|
24656
|
+
} catch {
|
|
24657
|
+
return "";
|
|
24658
|
+
}
|
|
24659
|
+
}
|
|
24636
24660
|
//#endregion
|
|
24637
24661
|
//#region src/render/tool-detail.ts
|
|
24638
24662
|
/**
|
|
@@ -25091,6 +25115,7 @@ function projectEvent(view, event) {
|
|
|
25091
25115
|
name: data.name,
|
|
25092
25116
|
arguments: data.arguments,
|
|
25093
25117
|
preview: toolArgumentsPreview(data.arguments, data.name),
|
|
25118
|
+
prompt: toolPromptPreview(data.name, data.arguments),
|
|
25094
25119
|
state: "running",
|
|
25095
25120
|
summary: "",
|
|
25096
25121
|
detail: void 0
|
|
@@ -25632,6 +25657,7 @@ function replayProjectEvent(acc, event) {
|
|
|
25632
25657
|
name: data.name,
|
|
25633
25658
|
arguments: data.arguments,
|
|
25634
25659
|
preview: toolArgumentsPreview(data.arguments, data.name),
|
|
25660
|
+
prompt: toolPromptPreview(data.name, data.arguments),
|
|
25635
25661
|
state: "running",
|
|
25636
25662
|
summary: "",
|
|
25637
25663
|
detail: void 0
|
|
@@ -26823,6 +26849,174 @@ function isOfficialDeepSeekLabel(label) {
|
|
|
26823
26849
|
return provider.toLowerCase().includes("deepseek") || model.toLowerCase().includes("deepseek");
|
|
26824
26850
|
}
|
|
26825
26851
|
//#endregion
|
|
26852
|
+
//#region src/session-directory.ts
|
|
26853
|
+
/** Lightweight session-directory projection for the /resume picker. */
|
|
26854
|
+
/** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
|
|
26855
|
+
const CASE_INSENSITIVE_FS = process.platform === "win32" || process.platform === "darwin";
|
|
26856
|
+
/** True when the header describes a subagent conversation (durable lineage). */
|
|
26857
|
+
function isSubagentSession(header) {
|
|
26858
|
+
return header.origin === "subagent" || header.parentSession !== void 0;
|
|
26859
|
+
}
|
|
26860
|
+
function comparablePath(value) {
|
|
26861
|
+
const resolved = resolve(value);
|
|
26862
|
+
const fold = (path) => CASE_INSENSITIVE_FS ? path.toLowerCase() : path;
|
|
26863
|
+
try {
|
|
26864
|
+
return fold(realpathSync(resolved));
|
|
26865
|
+
} catch {
|
|
26866
|
+
return fold(resolved);
|
|
26867
|
+
}
|
|
26868
|
+
}
|
|
26869
|
+
/** Platform-consistent path equality for session cwd comparisons. */
|
|
26870
|
+
function samePath(left, right) {
|
|
26871
|
+
if (left === void 0) return false;
|
|
26872
|
+
return comparablePath(left) === comparablePath(right);
|
|
26873
|
+
}
|
|
26874
|
+
/**
|
|
26875
|
+
* Unique header match by exact id or unique id prefix (root and subagent
|
|
26876
|
+
* headers alike); the caller applies any lineage gate.
|
|
26877
|
+
* @param headers - the persisted headers.
|
|
26878
|
+
* @param wanted - the id or id prefix.
|
|
26879
|
+
* @returns the uniquely matched header.
|
|
26880
|
+
* @throws when nothing matches or the prefix is ambiguous.
|
|
26881
|
+
*/
|
|
26882
|
+
function matchSessionId(headers, wanted) {
|
|
26883
|
+
const exact = headers.filter((header) => header.id === wanted);
|
|
26884
|
+
const matches = exact.length > 0 ? exact : headers.filter((header) => header.id.startsWith(wanted));
|
|
26885
|
+
if (matches.length === 0) throw new Error(`no persisted session matches "${wanted}"`);
|
|
26886
|
+
if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`);
|
|
26887
|
+
return matches[0];
|
|
26888
|
+
}
|
|
26889
|
+
/** The newest persisted ROOT session pinned to this cwd, or undefined. */
|
|
26890
|
+
function newestRootForCwd(headers, cwd) {
|
|
26891
|
+
return headers.filter((header) => !isSubagentSession(header) && samePath(header.cwd, cwd)).sort((left, right) => right.createdAt - left.createdAt)[0];
|
|
26892
|
+
}
|
|
26893
|
+
/**
|
|
26894
|
+
* Filter/sort header-only records. No session log is loaded here. Sorting is
|
|
26895
|
+
* by LAST ACTIVITY (`updated` — artifact mtime when the caller resolved one,
|
|
26896
|
+
* else createdAt), matching the codex resume picker's default UpdatedAt
|
|
26897
|
+
* ordering: a session you kept talking in outranks one created later but idle.
|
|
26898
|
+
* @param records - the header-only records.
|
|
26899
|
+
* @param options - filter/sort options.
|
|
26900
|
+
* @param updated - per-session last-activity timestamps, when resolved.
|
|
26901
|
+
*/
|
|
26902
|
+
function projectSessionRows(records, options, updated) {
|
|
26903
|
+
const needle = options.query.trim().toLowerCase();
|
|
26904
|
+
return records.filter((record) => options.sessions === "all" || !isSubagentSession(record.header)).filter((record) => options.cwd === "all" || samePath(record.header.cwd, options.currentCwd)).map((record) => {
|
|
26905
|
+
const cwd = record.header.cwd ?? "";
|
|
26906
|
+
const subagent = isSubagentSession(record.header);
|
|
26907
|
+
const activity = updated?.get(record.header.id);
|
|
26908
|
+
return {
|
|
26909
|
+
id: record.header.id,
|
|
26910
|
+
createdAt: record.header.createdAt,
|
|
26911
|
+
updatedAt: activity === void 0 || !Number.isFinite(activity) || activity < record.header.createdAt ? record.header.createdAt : activity,
|
|
26912
|
+
cwd,
|
|
26913
|
+
workspace: cwd === "" ? "(no workspace)" : basename(cwd),
|
|
26914
|
+
parent: record.header.parentSession,
|
|
26915
|
+
subagent,
|
|
26916
|
+
resumable: !subagent,
|
|
26917
|
+
live: record.live,
|
|
26918
|
+
persisted: record.persisted,
|
|
26919
|
+
preset: record.header.agentPreset ?? "standard"
|
|
26920
|
+
};
|
|
26921
|
+
}).filter((row) => needle === "" || `${row.id} ${row.cwd} ${row.workspace} ${row.preset}`.toLowerCase().includes(needle)).sort((left, right) => options.sort === "newest" ? right.updatedAt - left.updatedAt || right.createdAt - left.createdAt : left.updatedAt - right.updatedAt || left.createdAt - right.createdAt);
|
|
26922
|
+
}
|
|
26923
|
+
/** Merge page-local title observations without disturbing directory order. */
|
|
26924
|
+
function mergeSessionTitles(rows, observations) {
|
|
26925
|
+
const titles = /* @__PURE__ */ new Map();
|
|
26926
|
+
for (const observation of observations) {
|
|
26927
|
+
if (observation.status !== "fulfilled") continue;
|
|
26928
|
+
const title = observation.value?.title?.title ?? observation.value?.title?.text;
|
|
26929
|
+
if (title !== void 0 && title.trim() !== "") titles.set(observation.sessionId, title);
|
|
26930
|
+
}
|
|
26931
|
+
return rows.map((row) => titles.has(row.id) ? {
|
|
26932
|
+
...row,
|
|
26933
|
+
title: titles.get(row.id)
|
|
26934
|
+
} : row);
|
|
26935
|
+
}
|
|
26936
|
+
/**
|
|
26937
|
+
* Encode a session id the way the JSONL backend does for its on-disk layout
|
|
26938
|
+
* (`encodeSegment`: safe units literal, everything else `~XXXX`). Used ONLY to
|
|
26939
|
+
* validate that a `locate()` path really is this session's directory before
|
|
26940
|
+
* any deletion touches the filesystem — a local copy of the pure upstream
|
|
26941
|
+
* contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
|
|
26942
|
+
*/
|
|
26943
|
+
function encodeSessionSegment(raw) {
|
|
26944
|
+
if (raw.length === 0) throw new Error("cannot encode an empty path segment");
|
|
26945
|
+
if (raw === ".") return "~002E";
|
|
26946
|
+
if (raw === "..") return "~002E~002E";
|
|
26947
|
+
let out = "";
|
|
26948
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
26949
|
+
const code = raw.charCodeAt(i);
|
|
26950
|
+
const ch = String.fromCharCode(code);
|
|
26951
|
+
if (ch !== "~" && /^[A-Za-z0-9._-]$/u.test(ch)) out += ch;
|
|
26952
|
+
else out += `~${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
26953
|
+
}
|
|
26954
|
+
return out;
|
|
26955
|
+
}
|
|
26956
|
+
/** The session-log artifact names the JSONL backend may create. */
|
|
26957
|
+
const SESSION_ARTIFACT_NAMES = ["session.jsonl", "session.jsonl.zstd"];
|
|
26958
|
+
/**
|
|
26959
|
+
* Guard one `locate()` artifact path before deletion (codex's scoped-path
|
|
26960
|
+
* check, adapted to the JSONL layout): the file must be a `session.jsonl`
|
|
26961
|
+
* artifact sitting in the directory named exactly `encodeSegment(id)`.
|
|
26962
|
+
* @param artifact - the path the persistence backend located.
|
|
26963
|
+
* @param id - the session id the artifact claims to belong to.
|
|
26964
|
+
* @returns the owning session directory, or undefined when the layout is unexpected.
|
|
26965
|
+
*/
|
|
26966
|
+
function sessionArtifactDirectory(artifact, id) {
|
|
26967
|
+
if (basename(artifact) !== "session.jsonl" && basename(artifact) !== "session.jsonl.zstd") return void 0;
|
|
26968
|
+
const dir = dirname(artifact);
|
|
26969
|
+
if (basename(dir) !== encodeSessionSegment(id)) return void 0;
|
|
26970
|
+
return dir;
|
|
26971
|
+
}
|
|
26972
|
+
/**
|
|
26973
|
+
* Collect one session's deletion subtree: the id plus every record whose
|
|
26974
|
+
* parent chain leads to it (codex deletes subagent threads with their root).
|
|
26975
|
+
* @param records - the full directory listing.
|
|
26976
|
+
* @param id - the root session id to delete.
|
|
26977
|
+
* @returns the ids to delete, root first.
|
|
26978
|
+
*/
|
|
26979
|
+
function collectDeletionSubtree(records, id) {
|
|
26980
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
26981
|
+
for (const record of records) parentOf.set(record.header.id, record.header.parentSession);
|
|
26982
|
+
const doomed = /* @__PURE__ */ new Set([id]);
|
|
26983
|
+
for (let pass = 0; pass < 2; pass += 1) for (const candidate of parentOf.keys()) {
|
|
26984
|
+
if (doomed.has(candidate)) continue;
|
|
26985
|
+
let ancestor = parentOf.get(candidate);
|
|
26986
|
+
let depth = 0;
|
|
26987
|
+
while (ancestor !== void 0 && depth < 64) {
|
|
26988
|
+
if (doomed.has(ancestor)) {
|
|
26989
|
+
doomed.add(candidate);
|
|
26990
|
+
break;
|
|
26991
|
+
}
|
|
26992
|
+
ancestor = parentOf.get(ancestor);
|
|
26993
|
+
depth += 1;
|
|
26994
|
+
}
|
|
26995
|
+
}
|
|
26996
|
+
return [...doomed];
|
|
26997
|
+
}
|
|
26998
|
+
/**
|
|
26999
|
+
* Codex-style relative time for session rows ("now", "5m ago", "3h ago",
|
|
27000
|
+
* "2d ago"; older than a week falls back to the local date).
|
|
27001
|
+
* @param timestamp - epoch milliseconds of the last activity.
|
|
27002
|
+
* @param now - the pinned reference clock (one value per list render).
|
|
27003
|
+
*/
|
|
27004
|
+
function formatRelativeTime(timestamp, now) {
|
|
27005
|
+
const seconds = Math.round((now - timestamp) / 1e3);
|
|
27006
|
+
if (seconds < 0) return "now";
|
|
27007
|
+
if (seconds < 60) return "now";
|
|
27008
|
+
const minutes = Math.round(seconds / 60);
|
|
27009
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
27010
|
+
const hours = Math.round(minutes / 60);
|
|
27011
|
+
if (hours < 24) return `${hours}h ago`;
|
|
27012
|
+
const days = Math.round(hours / 24);
|
|
27013
|
+
if (days < 7) return `${days}d ago`;
|
|
27014
|
+
const date = new Date(timestamp);
|
|
27015
|
+
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
27016
|
+
const day = `${date.getDate()}`.padStart(2, "0");
|
|
27017
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
27018
|
+
}
|
|
27019
|
+
//#endregion
|
|
26826
27020
|
//#region src/render/status.ts
|
|
26827
27021
|
/**
|
|
26828
27022
|
* Status-bar composition for the TUI footer. Codex/Claude-Code-style split
|
|
@@ -27506,6 +27700,7 @@ function transcriptEntryLines(entry, columns) {
|
|
|
27506
27700
|
lineSegment(entry.name, "brand"),
|
|
27507
27701
|
lineSegment(entry.preview === "" ? "" : ` ${entry.preview}`, "dim")
|
|
27508
27702
|
], width),
|
|
27703
|
+
...entry.prompt === "" ? [] : textLines(` └ ${entry.prompt}`, width, "dim"),
|
|
27509
27704
|
...entry.summary === "" ? [] : textLines(` ⎿ ${entry.summary}`, width, entry.state === "error" ? "error" : "dim"),
|
|
27510
27705
|
...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
|
|
27511
27706
|
];
|
|
@@ -27530,6 +27725,17 @@ function transcriptEntryLines(entry, columns) {
|
|
|
27530
27725
|
//#endregion
|
|
27531
27726
|
//#region src/kernel-panels.ts
|
|
27532
27727
|
/** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
|
|
27728
|
+
/** True for the Ctrl+F search-focus toggle. */
|
|
27729
|
+
function isSearchToggle(input, key) {
|
|
27730
|
+
return key.ctrl === true && input === "f";
|
|
27731
|
+
}
|
|
27732
|
+
/** The search-state line: gated panels show only an ACTIVE filter (the ctrl+f
|
|
27733
|
+
* toggle lives in the footer), direct-typing panels keep the plain prompt. */
|
|
27734
|
+
function searchLine(searching, query) {
|
|
27735
|
+
if (searching === true) return `search: ${query === "" ? "type to filter · esc stops" : query}`;
|
|
27736
|
+
if (searching === false) return query === "" ? "" : `search: ${query}`;
|
|
27737
|
+
return `search: ${query === "" ? "type to filter" : query}`;
|
|
27738
|
+
}
|
|
27533
27739
|
function ListFrame(props) {
|
|
27534
27740
|
const stdout = useStdout().stdout;
|
|
27535
27741
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
@@ -27560,7 +27766,7 @@ function ListFrame(props) {
|
|
|
27560
27766
|
}, truncateColumns(singleLineText(props.title), viewport.contentColumns)), (0, import_react.createElement)(Text, {
|
|
27561
27767
|
dimColor: true,
|
|
27562
27768
|
wrap: "truncate-end"
|
|
27563
|
-
}, truncateColumns(singleLineText(
|
|
27769
|
+
}, truncateColumns(singleLineText(searchLine(props.searching, props.query)), viewport.contentColumns)), ...visible.map((row, index) => {
|
|
27564
27770
|
const absolute = offset + index;
|
|
27565
27771
|
const selected = !props.loading && props.error === void 0 && props.rows.length > 0 && absolute === props.cursor;
|
|
27566
27772
|
return (0, import_react.createElement)(Text, {
|
|
@@ -27701,10 +27907,10 @@ function PluginPanel({ load, close, initialQuery = "" }) {
|
|
|
27701
27907
|
footer: "↑↓ inspect · enter details · r refresh · esc close"
|
|
27702
27908
|
});
|
|
27703
27909
|
}
|
|
27704
|
-
function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
27910
|
+
function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }) {
|
|
27705
27911
|
const [options, setOptions] = (0, import_react.useState)({
|
|
27706
27912
|
sessions: "roots",
|
|
27707
|
-
cwd: "
|
|
27913
|
+
cwd: "current",
|
|
27708
27914
|
sort: "newest",
|
|
27709
27915
|
currentCwd,
|
|
27710
27916
|
query: ""
|
|
@@ -27717,6 +27923,10 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
|
27717
27923
|
const [error, setError] = (0, import_react.useState)();
|
|
27718
27924
|
const [expanded, setExpanded] = (0, import_react.useState)();
|
|
27719
27925
|
const [transcript, setTranscript] = (0, import_react.useState)();
|
|
27926
|
+
/** Ctrl+F-gated search: typing filters only while searching (codex). */
|
|
27927
|
+
const [searching, setSearching] = (0, import_react.useState)(false);
|
|
27928
|
+
/** Reference clock pinned per row render, so relative times never drift mid-list. */
|
|
27929
|
+
const now = (0, import_react.useMemo)(() => Date.now(), [rows, options]);
|
|
27720
27930
|
const transcriptLoad = (0, import_react.useRef)();
|
|
27721
27931
|
(0, import_react.useEffect)(() => () => transcriptLoad.current?.abort(), []);
|
|
27722
27932
|
(0, import_react.useEffect)(() => {
|
|
@@ -27735,7 +27945,7 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
|
27735
27945
|
}
|
|
27736
27946
|
});
|
|
27737
27947
|
return () => controller.abort();
|
|
27738
|
-
}, [options]);
|
|
27948
|
+
}, [options, reloadToken]);
|
|
27739
27949
|
(0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
|
|
27740
27950
|
const cycle = () => {
|
|
27741
27951
|
if (focus === 3) {
|
|
@@ -27758,7 +27968,34 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
|
27758
27968
|
});
|
|
27759
27969
|
};
|
|
27760
27970
|
useInput((input, key) => {
|
|
27761
|
-
if (
|
|
27971
|
+
if (deleteConfirmId !== void 0) return;
|
|
27972
|
+
if (key.escape) {
|
|
27973
|
+
if (searching) {
|
|
27974
|
+
setSearching(false);
|
|
27975
|
+
return;
|
|
27976
|
+
}
|
|
27977
|
+
return close();
|
|
27978
|
+
}
|
|
27979
|
+
if (isSearchToggle(input, key)) {
|
|
27980
|
+
setSearching((current) => !current);
|
|
27981
|
+
return;
|
|
27982
|
+
}
|
|
27983
|
+
if (searching) {
|
|
27984
|
+
if (key.return) {
|
|
27985
|
+
setSearching(false);
|
|
27986
|
+
return;
|
|
27987
|
+
}
|
|
27988
|
+
const next = editQuery(options.query, input, key);
|
|
27989
|
+
if (next !== void 0) {
|
|
27990
|
+
setOptions((value) => ({
|
|
27991
|
+
...value,
|
|
27992
|
+
query: next
|
|
27993
|
+
}));
|
|
27994
|
+
setCursor(0);
|
|
27995
|
+
}
|
|
27996
|
+
return;
|
|
27997
|
+
}
|
|
27998
|
+
if (input === "q") return close();
|
|
27762
27999
|
if (key.tab) return setFocus((value) => (value + (key.shift ? 3 : 1)) % 4);
|
|
27763
28000
|
if (key.leftArrow) return cycle();
|
|
27764
28001
|
if (key.rightArrow) return cycle();
|
|
@@ -27768,7 +28005,7 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
|
27768
28005
|
if (key.pageDown) return setCursor((value) => Math.min(rows.length - 1, value + 8));
|
|
27769
28006
|
if (input === "g") return setCursor(0);
|
|
27770
28007
|
if (input === "G") return setCursor(Math.max(0, rows.length - 1));
|
|
27771
|
-
if (input === "d"
|
|
28008
|
+
if (input === "d" && rows[cursor] !== void 0 && requestDelete !== void 0) return requestDelete(rows[cursor]);
|
|
27772
28009
|
if (input === "e" && rows[cursor] !== void 0) return setExpanded((value) => value === rows[cursor].id ? void 0 : rows[cursor].id);
|
|
27773
28010
|
if (input === "t" && rows[cursor] !== void 0) {
|
|
27774
28011
|
const row = rows[cursor];
|
|
@@ -27790,14 +28027,6 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
|
27790
28027
|
return;
|
|
27791
28028
|
}
|
|
27792
28029
|
if (key.return && rows[cursor]?.resumable === true) return select(rows[cursor]);
|
|
27793
|
-
const next = editQuery(options.query, input, key);
|
|
27794
|
-
if (next !== void 0) {
|
|
27795
|
-
setOptions((value) => ({
|
|
27796
|
-
...value,
|
|
27797
|
-
query: next
|
|
27798
|
-
}));
|
|
27799
|
-
setCursor(0);
|
|
27800
|
-
}
|
|
27801
28030
|
}, { isActive: transcript === void 0 });
|
|
27802
28031
|
if (transcript !== void 0) return (0, import_react.createElement)(DocumentPanel, {
|
|
27803
28032
|
title: `transcript · ${transcript.id}`,
|
|
@@ -27808,19 +28037,21 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
|
|
|
27808
28037
|
setTranscript(void 0);
|
|
27809
28038
|
}
|
|
27810
28039
|
});
|
|
28040
|
+
const pendingRow = deleteConfirmId === void 0 ? void 0 : rows.find((row) => row.id === deleteConfirmId);
|
|
27811
28041
|
const toolbar = `[${focus === 0 ? ">" : ""}${options.sessions}] [${focus === 1 ? ">" : ""}${options.cwd} cwd] [${focus === 2 ? ">" : ""}${options.sort}] [${focus === 3 ? ">" : ""}${density}]`;
|
|
27812
28042
|
return (0, import_react.createElement)(ListFrame, {
|
|
27813
|
-
title: `/resume · ${toolbar}`,
|
|
28043
|
+
title: deleteConfirmId === void 0 ? `/resume${deleteMode ? " — delete mode" : ""}${searching ? " — searching" : ""} · ${toolbar}` : `permanently delete ${pendingRow === void 0 ? deleteConfirmId.slice(-12) : pendingRow.title ?? pendingRow.id}? this cannot be undone · subagent threads go too`,
|
|
27814
28044
|
rows: rows.map((row) => ({
|
|
27815
28045
|
key: row.id,
|
|
27816
28046
|
disabled: !row.resumable,
|
|
27817
|
-
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}`}` : ""}`
|
|
28047
|
+
text: `${row.subagent ? "↳" : "○"} ${row.title ?? row.id.slice(-12)}${density === "comfortable" ? ` · ${formatRelativeTime(row.updatedAt ?? row.createdAt, now)} · ${row.workspace} · ${row.preset}` : ""}${row.live ? " · live" : ""}${expanded === row.id ? ` · ${row.id} · ${row.cwd}${row.parent === void 0 ? "" : ` · parent ${row.parent}`}` : ""}`
|
|
27818
28048
|
})),
|
|
27819
28049
|
cursor,
|
|
27820
28050
|
loading,
|
|
27821
28051
|
error,
|
|
27822
28052
|
query: options.query,
|
|
27823
|
-
|
|
28053
|
+
searching,
|
|
28054
|
+
footer: "tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · d delete · enter resume"
|
|
27824
28055
|
});
|
|
27825
28056
|
}
|
|
27826
28057
|
function DocumentPanel({ title, text, error, close }) {
|
|
@@ -28007,21 +28238,31 @@ function StatuslinePanel({ enabled, change, close }) {
|
|
|
28007
28238
|
/**
|
|
28008
28239
|
* The `/model` reasoning-effort stage (the Codex model → reasoning popup
|
|
28009
28240
|
* contract): one bounded list over the selected model's adapter-advertised
|
|
28010
|
-
* effort levels
|
|
28011
|
-
*
|
|
28012
|
-
*
|
|
28013
|
-
*
|
|
28014
|
-
*
|
|
28015
|
-
* model
|
|
28241
|
+
* effort levels — in the adapter's own display order, ids verbatim (the
|
|
28242
|
+
* kernel treats them as opaque and rejects anything else) — with the
|
|
28243
|
+
* effective effort and the model default marked. A model WITHOUT an
|
|
28244
|
+
* adapter-declared default leads with a "Default" (provider-default) row —
|
|
28245
|
+
* the web effort pane's first entry — so the user can clear a picked level
|
|
28246
|
+
* back to provider behavior. A model advertising no levels opens the same
|
|
28247
|
+
* stage with an explicit empty state (the web pane's "no levels" copy)
|
|
28248
|
+
* instead of a bare failure notice. Enter applies one level; Esc returns to
|
|
28249
|
+
* the model list without applying.
|
|
28016
28250
|
*/
|
|
28017
28251
|
function EffortPanel({ row, current, select, back }) {
|
|
28018
|
-
const
|
|
28019
|
-
const
|
|
28020
|
-
const
|
|
28252
|
+
const advertised = row.reasoning?.efforts ?? [];
|
|
28253
|
+
const empty = row.reasoning === void 0 || advertised.length === 0;
|
|
28254
|
+
const hasDefaultRow = row.reasoning !== void 0 && row.reasoning.defaultEffort === void 0;
|
|
28255
|
+
const rows = empty ? [{
|
|
28256
|
+
id: "",
|
|
28257
|
+
name: ""
|
|
28258
|
+
}] : hasDefaultRow ? [{
|
|
28021
28259
|
id: "",
|
|
28022
28260
|
name: "Default"
|
|
28023
|
-
}, ...
|
|
28261
|
+
}, ...advertised] : advertised;
|
|
28024
28262
|
const effective = current === void 0 || current === "" ? "" : current;
|
|
28263
|
+
const wanted = effective === "" ? row.reasoning?.defaultEffort ?? "" : effective;
|
|
28264
|
+
const initialCursor = Math.max(0, rows.findIndex((effort) => effort.id === wanted));
|
|
28265
|
+
const [cursor, setCursor] = (0, import_react.useState)(initialCursor);
|
|
28025
28266
|
(0, import_react.useEffect)(() => {
|
|
28026
28267
|
if (rows.length === 0) {
|
|
28027
28268
|
if (cursor !== 0) setCursor(0);
|
|
@@ -28031,7 +28272,15 @@ function EffortPanel({ row, current, select, back }) {
|
|
|
28031
28272
|
}, [rows.length, cursor]);
|
|
28032
28273
|
useInput((input, key) => {
|
|
28033
28274
|
if (key.escape || input === "q") return back();
|
|
28034
|
-
if (
|
|
28275
|
+
if (empty) return;
|
|
28276
|
+
if (input === "g") {
|
|
28277
|
+
setCursor(0);
|
|
28278
|
+
return;
|
|
28279
|
+
}
|
|
28280
|
+
if (input === "G") {
|
|
28281
|
+
setCursor(rows.length - 1);
|
|
28282
|
+
return;
|
|
28283
|
+
}
|
|
28035
28284
|
if (key.upArrow) {
|
|
28036
28285
|
setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
|
|
28037
28286
|
return;
|
|
@@ -28044,14 +28293,190 @@ function EffortPanel({ row, current, select, back }) {
|
|
|
28044
28293
|
});
|
|
28045
28294
|
return (0, import_react.createElement)(ListFrame, {
|
|
28046
28295
|
title: `/model — effort for ${row.providerName} · ${row.modelName}`,
|
|
28047
|
-
rows:
|
|
28296
|
+
rows: empty ? [{
|
|
28297
|
+
key: "empty",
|
|
28298
|
+
disabled: true,
|
|
28299
|
+
text: "this model advertises no reasoning effort levels — the provider default applies"
|
|
28300
|
+
}] : rows.map((effort) => ({
|
|
28048
28301
|
key: effort.id,
|
|
28049
28302
|
text: `${effort.id === effective ? "●" : "○"} ${effort.name}${effort.id === row.reasoning?.defaultEffort ? " · default" : ""}${effort.description === void 0 ? "" : ` · ${effort.description}`}`
|
|
28050
28303
|
})),
|
|
28051
28304
|
cursor,
|
|
28052
28305
|
loading: false,
|
|
28053
28306
|
query: "",
|
|
28054
|
-
footer: "↑↓ choose · enter apply · esc/q back"
|
|
28307
|
+
footer: empty ? "esc back" : "↑↓ choose · enter apply · esc/q back"
|
|
28308
|
+
});
|
|
28309
|
+
}
|
|
28310
|
+
/**
|
|
28311
|
+
* The /agents panel (the Codex agent-picker contract, read-only): this
|
|
28312
|
+
* conversation's subagent conversations — live rows from the activity feed
|
|
28313
|
+
* first, persisted children the feed has not seen this process after — with
|
|
28314
|
+
* Enter/t opening the child's full transcript in the shared read-only
|
|
28315
|
+
* document view (the same projection the exporter uses).
|
|
28316
|
+
*/
|
|
28317
|
+
function AgentsPanel({ live, load, readTranscript, close }) {
|
|
28318
|
+
const [dirRows, setDirRows] = (0, import_react.useState)(void 0);
|
|
28319
|
+
const [error, setError] = (0, import_react.useState)();
|
|
28320
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
28321
|
+
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
28322
|
+
const [transcript, setTranscript] = (0, import_react.useState)();
|
|
28323
|
+
const transcriptLoad = (0, import_react.useRef)();
|
|
28324
|
+
(0, import_react.useEffect)(() => () => transcriptLoad.current?.abort(), []);
|
|
28325
|
+
const refresh = () => {
|
|
28326
|
+
setLoading(true);
|
|
28327
|
+
setError(void 0);
|
|
28328
|
+
Promise.resolve().then(load).then((value) => {
|
|
28329
|
+
setDirRows(value);
|
|
28330
|
+
setLoading(false);
|
|
28331
|
+
}, (reason) => {
|
|
28332
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
28333
|
+
setLoading(false);
|
|
28334
|
+
});
|
|
28335
|
+
};
|
|
28336
|
+
(0, import_react.useEffect)(refresh, []);
|
|
28337
|
+
const rows = (0, import_react.useMemo)(() => {
|
|
28338
|
+
const seen = new Set(live.map((row) => row.id));
|
|
28339
|
+
const feedRows = live.map((row) => ({
|
|
28340
|
+
id: row.id,
|
|
28341
|
+
label: row.label,
|
|
28342
|
+
activity: row.activity,
|
|
28343
|
+
running: row.state === "running",
|
|
28344
|
+
done: row.state === "done",
|
|
28345
|
+
live: true
|
|
28346
|
+
}));
|
|
28347
|
+
const persisted = (dirRows ?? []).filter((row) => !seen.has(row.id)).map((row) => ({
|
|
28348
|
+
id: row.id,
|
|
28349
|
+
label: row.title ?? row.id.slice(-12),
|
|
28350
|
+
activity: row.workspace,
|
|
28351
|
+
running: false,
|
|
28352
|
+
done: !row.live,
|
|
28353
|
+
live: row.live
|
|
28354
|
+
}));
|
|
28355
|
+
return [...feedRows, ...persisted];
|
|
28356
|
+
}, [live, dirRows]);
|
|
28357
|
+
(0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
|
|
28358
|
+
const openTranscript = () => {
|
|
28359
|
+
const row = rows[cursor];
|
|
28360
|
+
if (row === void 0) return;
|
|
28361
|
+
transcriptLoad.current?.abort();
|
|
28362
|
+
setTranscript({ id: row.id });
|
|
28363
|
+
const controller = new AbortController();
|
|
28364
|
+
transcriptLoad.current = controller;
|
|
28365
|
+
Promise.resolve().then(() => readTranscript(row.id, controller.signal)).then((text) => {
|
|
28366
|
+
if (!controller.signal.aborted) setTranscript({
|
|
28367
|
+
id: row.id,
|
|
28368
|
+
text
|
|
28369
|
+
});
|
|
28370
|
+
}, (reason) => {
|
|
28371
|
+
if (!controller.signal.aborted) setTranscript({
|
|
28372
|
+
id: row.id,
|
|
28373
|
+
error: reason instanceof Error ? reason.message : String(reason)
|
|
28374
|
+
});
|
|
28375
|
+
});
|
|
28376
|
+
};
|
|
28377
|
+
useInput((input, key) => {
|
|
28378
|
+
if (key.escape || input === "q") return close();
|
|
28379
|
+
if (input === "r") return refresh();
|
|
28380
|
+
if (key.upArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length);
|
|
28381
|
+
if (key.downArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + 1) % rows.length);
|
|
28382
|
+
if ((key.return || input === "t") && rows[cursor] !== void 0) return openTranscript();
|
|
28383
|
+
}, { isActive: transcript === void 0 });
|
|
28384
|
+
if (transcript !== void 0) return (0, import_react.createElement)(DocumentPanel, {
|
|
28385
|
+
title: `subagent · ${transcript.id.slice(-12)}`,
|
|
28386
|
+
text: transcript.text,
|
|
28387
|
+
error: transcript.error,
|
|
28388
|
+
close: () => {
|
|
28389
|
+
transcriptLoad.current?.abort();
|
|
28390
|
+
setTranscript(void 0);
|
|
28391
|
+
}
|
|
28392
|
+
});
|
|
28393
|
+
return (0, import_react.createElement)(ListFrame, {
|
|
28394
|
+
title: `/agents · ${live.length} live · ${rows.length} total`,
|
|
28395
|
+
rows: rows.map((row) => ({
|
|
28396
|
+
key: row.id,
|
|
28397
|
+
text: `${row.running ? "●" : row.done ? "✓" : row.live ? "⏸" : "○"} ${row.label} · ${row.activity}${row.live ? " · live" : ""}`
|
|
28398
|
+
})),
|
|
28399
|
+
cursor,
|
|
28400
|
+
loading,
|
|
28401
|
+
...error === void 0 ? {} : { error },
|
|
28402
|
+
query: "",
|
|
28403
|
+
footer: "↑↓ choose · enter/t transcript · r refresh · esc close"
|
|
28404
|
+
});
|
|
28405
|
+
}
|
|
28406
|
+
/**
|
|
28407
|
+
* The /subagent model panel: which model configuration delegated subagents
|
|
28408
|
+
* run on. The kernel seeds child agents from the parent's CREATE-TIME
|
|
28409
|
+
* AgentOptions, so a mid-session /model switch would otherwise leave them on
|
|
28410
|
+
* the launch-time route; the TUI mirrors the selection onto subagent-origin
|
|
28411
|
+
* requests (or an explicit override picked here) via an agent/request
|
|
28412
|
+
* listener. The leading "inherit" row restores follow-the-current-model
|
|
28413
|
+
* behavior; picking a model with several advertised efforts opens the same
|
|
28414
|
+
* effort stage /model uses. Effort overrides are not offered separately —
|
|
28415
|
+
* the kernel's AgentOptions has no effort channel for children, so the level
|
|
28416
|
+
* rides the selected model exactly as /model applies it.
|
|
28417
|
+
*/
|
|
28418
|
+
function SubagentPanel({ current, load, pick, inherit, close }) {
|
|
28419
|
+
const [directory, setDirectory] = (0, import_react.useState)(void 0);
|
|
28420
|
+
const [error, setError] = (0, import_react.useState)();
|
|
28421
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
28422
|
+
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
28423
|
+
const [effortFor, setEffortFor] = (0, import_react.useState)(void 0);
|
|
28424
|
+
const refresh = () => {
|
|
28425
|
+
setLoading(true);
|
|
28426
|
+
setError(void 0);
|
|
28427
|
+
Promise.resolve().then(load).then((value) => {
|
|
28428
|
+
setDirectory(value);
|
|
28429
|
+
setLoading(false);
|
|
28430
|
+
}, (reason) => {
|
|
28431
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
28432
|
+
setLoading(false);
|
|
28433
|
+
});
|
|
28434
|
+
};
|
|
28435
|
+
(0, import_react.useEffect)(refresh, []);
|
|
28436
|
+
const rows = (0, import_react.useMemo)(() => directory?.rows ?? [], [directory]);
|
|
28437
|
+
(0, import_react.useEffect)(() => {
|
|
28438
|
+
if (current === "" || rows.length === 0) return;
|
|
28439
|
+
const index = rows.findIndex((row) => current.startsWith(`${row.provider}/${row.model}`));
|
|
28440
|
+
if (index >= 0) setCursor(index + 1);
|
|
28441
|
+
}, [rows, current]);
|
|
28442
|
+
(0, import_react.useEffect)(() => setCursor((value) => Math.min(value, rows.length)), [rows.length]);
|
|
28443
|
+
useInput((input, key) => {
|
|
28444
|
+
if (effortFor !== void 0) return;
|
|
28445
|
+
if (key.escape || input === "q") return close();
|
|
28446
|
+
if (input === "r" && !loading) return refresh();
|
|
28447
|
+
if (key.upArrow) return setCursor((value) => (value + rows.length) % (rows.length + 1));
|
|
28448
|
+
if (key.downArrow) return setCursor((value) => (value + 1) % (rows.length + 1));
|
|
28449
|
+
if (key.return) {
|
|
28450
|
+
if (cursor === 0) return inherit();
|
|
28451
|
+
const row = rows[cursor - 1];
|
|
28452
|
+
if (row === void 0) return;
|
|
28453
|
+
if (row.reasoning !== void 0 && row.reasoning.efforts.length > 1) {
|
|
28454
|
+
setEffortFor(row);
|
|
28455
|
+
return;
|
|
28456
|
+
}
|
|
28457
|
+
pick(row, row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : void 0);
|
|
28458
|
+
}
|
|
28459
|
+
});
|
|
28460
|
+
if (effortFor !== void 0) return (0, import_react.createElement)(EffortPanel, {
|
|
28461
|
+
row: effortFor,
|
|
28462
|
+
current: current === "" ? void 0 : current.split("@")[1],
|
|
28463
|
+
select: (effortId) => pick(effortFor, effortId),
|
|
28464
|
+
back: () => setEffortFor(void 0)
|
|
28465
|
+
});
|
|
28466
|
+
return (0, import_react.createElement)(ListFrame, {
|
|
28467
|
+
title: `/subagent — model for delegated agents${current === "" ? "" : ` · override ${current}`}`,
|
|
28468
|
+
rows: [{
|
|
28469
|
+
key: "__inherit__",
|
|
28470
|
+
text: `${current === "" ? "●" : "○"} inherit — follow the current model (/model switches apply)`
|
|
28471
|
+
}, ...rows.map((row) => ({
|
|
28472
|
+
key: `${row.provider}/${row.model}`,
|
|
28473
|
+
text: `${current.startsWith(`${row.provider}/${row.model}`) ? "●" : "○"} ${row.providerName} · ${row.modelName}`
|
|
28474
|
+
}))],
|
|
28475
|
+
cursor,
|
|
28476
|
+
loading,
|
|
28477
|
+
...error === void 0 ? {} : { error },
|
|
28478
|
+
query: "",
|
|
28479
|
+
footer: "↑↓ choose · enter apply · r refresh · esc close"
|
|
28055
28480
|
});
|
|
28056
28481
|
}
|
|
28057
28482
|
/** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
|
|
@@ -28588,6 +29013,23 @@ function Header({ resumed }) {
|
|
|
28588
29013
|
function todoMark(status) {
|
|
28589
29014
|
return status === "completed" ? "✓" : status === "in_progress" ? "●" : "○";
|
|
28590
29015
|
}
|
|
29016
|
+
/**
|
|
29017
|
+
* One-row live subagent summary (the Codex agent status feed, compressed to
|
|
29018
|
+
* the transcript's budget): running count, total, and the most recently
|
|
29019
|
+
* active child's current activity. One line, never more — the full view is
|
|
29020
|
+
* the /agents panel.
|
|
29021
|
+
*/
|
|
29022
|
+
function AgentsLine({ rows }) {
|
|
29023
|
+
if (rows.length === 0) return void 0;
|
|
29024
|
+
const running = rows.filter((row) => row.state !== "done").length;
|
|
29025
|
+
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
29026
|
+
const mark = newest.state === "done" ? "✓" : newest.state === "idle" ? "⏸" : "●";
|
|
29027
|
+
return (0, import_react.createElement)(Box, { paddingX: 1 }, (0, import_react.createElement)(Text, {
|
|
29028
|
+
color: inkColor(getPalette().brand),
|
|
29029
|
+
bold: true,
|
|
29030
|
+
wrap: "truncate-end"
|
|
29031
|
+
}, `agents ${running} live`, (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, ` · ${rows.length} total · /agents`), (0, import_react.createElement)(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`)));
|
|
29032
|
+
}
|
|
28591
29033
|
/** One-row todo summary: task count cannot grow the live Ink tree. */
|
|
28592
29034
|
function TodoPanel({ todos }) {
|
|
28593
29035
|
if (todos.length === 0) return void 0;
|
|
@@ -28764,50 +29206,96 @@ function NoticeLine({ text, tone, columns }) {
|
|
|
28764
29206
|
wrap: "truncate-end"
|
|
28765
29207
|
}, truncateColumns(`${mark} ${singleLineText(text)}`, Math.max(1, columns - 2))));
|
|
28766
29208
|
}
|
|
28767
|
-
/** The
|
|
28768
|
-
|
|
29209
|
+
/** The fixed decision list; answers stay in the binary answerer vocabulary. */
|
|
29210
|
+
const APPROVAL_OPTIONS = [
|
|
29211
|
+
{
|
|
29212
|
+
key: "allow",
|
|
29213
|
+
label: "Yes, proceed",
|
|
29214
|
+
hotkey: "y"
|
|
29215
|
+
},
|
|
29216
|
+
{
|
|
29217
|
+
key: "reject-note",
|
|
29218
|
+
label: "No, and tell it what to do differently",
|
|
29219
|
+
hotkey: "n"
|
|
29220
|
+
},
|
|
29221
|
+
{
|
|
29222
|
+
key: "reject",
|
|
29223
|
+
label: "No, continue without running it",
|
|
29224
|
+
hotkey: "d"
|
|
29225
|
+
}
|
|
29226
|
+
];
|
|
29227
|
+
/**
|
|
29228
|
+
* The approval dialog (Codex ApprovalOverlay contract): a bold question
|
|
29229
|
+
* header, the bounded command body with an explicit overflow marker, a
|
|
29230
|
+
* numbered option list with a `›` cursor, single-key shortcuts, and digits
|
|
29231
|
+
* for direct selection. Askers queue FIFO — the count rides the header.
|
|
29232
|
+
* The upstream answerer vocabulary stays binary (`allowed-once` /
|
|
29233
|
+
* `rejected`): "tell it what to do differently" rejects and hands the
|
|
29234
|
+
* composer back with a hint notice, exactly Codex's decline-then-type flow.
|
|
29235
|
+
*/
|
|
29236
|
+
function ApprovalBar({ snapshot, locked, notify }) {
|
|
28769
29237
|
const stdout = useStdout().stdout;
|
|
28770
29238
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
28771
|
-
const [
|
|
29239
|
+
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
28772
29240
|
const pending = snapshot.pending;
|
|
28773
|
-
const active = !locked &&
|
|
28774
|
-
const
|
|
28775
|
-
const visibleScroll = clampScroll(scroll, content.length, viewport.bodyRows);
|
|
29241
|
+
const active = !locked && pending !== void 0 && !snapshot.answered;
|
|
29242
|
+
const body = (0, import_react.useMemo)(() => pending === void 0 || pending.command === "" ? [] : textLines(pending.command, viewport.contentColumns, "dim"), [pending, viewport.contentColumns]);
|
|
28776
29243
|
(0, import_react.useEffect)(() => {
|
|
28777
|
-
|
|
29244
|
+
setCursor(0);
|
|
28778
29245
|
}, [pending]);
|
|
28779
|
-
|
|
28780
|
-
|
|
28781
|
-
|
|
29246
|
+
const decide = (option) => {
|
|
29247
|
+
const ask = snapshot.pending;
|
|
29248
|
+
if (ask === void 0 || snapshot.answered) return;
|
|
29249
|
+
if (option.key === "allow") {
|
|
29250
|
+
ask.answer("allowed-once");
|
|
29251
|
+
return;
|
|
29252
|
+
}
|
|
29253
|
+
ask.answer("rejected");
|
|
29254
|
+
if (option.key === "reject-note") notify("rejected — type below what it should do differently (it steers the next step)", "warning");
|
|
29255
|
+
};
|
|
28782
29256
|
useInput((input, key) => {
|
|
28783
|
-
if (snapshot.pending === void 0) return;
|
|
29257
|
+
if (snapshot.pending === void 0 || snapshot.answered) return;
|
|
28784
29258
|
if (key.upArrow) {
|
|
28785
|
-
|
|
29259
|
+
setCursor((current) => (current + APPROVAL_OPTIONS.length - 1) % APPROVAL_OPTIONS.length);
|
|
28786
29260
|
return;
|
|
28787
29261
|
}
|
|
28788
29262
|
if (key.downArrow) {
|
|
28789
|
-
|
|
29263
|
+
setCursor((current) => (current + 1) % APPROVAL_OPTIONS.length);
|
|
28790
29264
|
return;
|
|
28791
29265
|
}
|
|
28792
|
-
if (key.
|
|
28793
|
-
|
|
29266
|
+
if (key.return) {
|
|
29267
|
+
decide(APPROVAL_OPTIONS[cursor]);
|
|
28794
29268
|
return;
|
|
28795
29269
|
}
|
|
28796
|
-
if (key.
|
|
28797
|
-
|
|
29270
|
+
if (key.escape) {
|
|
29271
|
+
decide(APPROVAL_OPTIONS[2]);
|
|
28798
29272
|
return;
|
|
28799
29273
|
}
|
|
28800
|
-
if (snapshot.answered) return;
|
|
28801
29274
|
if (input === "y" || input === "Y") {
|
|
28802
|
-
|
|
29275
|
+
decide(APPROVAL_OPTIONS[0]);
|
|
29276
|
+
return;
|
|
29277
|
+
}
|
|
29278
|
+
if (input === "n" || input === "N") {
|
|
29279
|
+
decide(APPROVAL_OPTIONS[1]);
|
|
29280
|
+
return;
|
|
29281
|
+
}
|
|
29282
|
+
if (input === "d" || input === "D") {
|
|
29283
|
+
decide(APPROVAL_OPTIONS[2]);
|
|
28803
29284
|
return;
|
|
28804
29285
|
}
|
|
28805
|
-
if (input
|
|
29286
|
+
if (/^[1-9]$/u.test(input)) {
|
|
29287
|
+
const index = Number(input) - 1;
|
|
29288
|
+
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index]);
|
|
29289
|
+
}
|
|
28806
29290
|
}, { isActive: active });
|
|
28807
|
-
if (
|
|
29291
|
+
if (pending === void 0) return void 0;
|
|
28808
29292
|
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
28809
|
-
|
|
28810
|
-
|
|
29293
|
+
const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : "";
|
|
29294
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`approval${queuedSuffix} · enter/y allow · esc/n reject`, viewport.contentColumns));
|
|
29295
|
+
const reservedRows = 3 + APPROVAL_OPTIONS.length;
|
|
29296
|
+
const bodyBudget = Math.max(1, viewport.bodyRows - reservedRows);
|
|
29297
|
+
const visibleBody = body.slice(0, bodyBudget);
|
|
29298
|
+
const overflow = body.length - visibleBody.length;
|
|
28811
29299
|
return (0, import_react.createElement)(Box, {
|
|
28812
29300
|
flexDirection: "column",
|
|
28813
29301
|
width: viewport.outerColumns,
|
|
@@ -28818,10 +29306,25 @@ function ApprovalBar({ snapshot, locked }) {
|
|
|
28818
29306
|
color: inkColor(getPalette().warn),
|
|
28819
29307
|
bold: true,
|
|
28820
29308
|
wrap: "truncate-end"
|
|
28821
|
-
}, truncateColumns(
|
|
28822
|
-
|
|
29309
|
+
}, truncateColumns(`${pending.headline}${queuedSuffix}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 && body.length > 0 }), ...visibleBody.map((line, index) => (0, import_react.createElement)(StyledRows, {
|
|
29310
|
+
key: `body-${index}`,
|
|
29311
|
+
lines: [line]
|
|
29312
|
+
})), ...overflow > 0 ? [(0, import_react.createElement)(Text, {
|
|
29313
|
+
key: "overflow",
|
|
29314
|
+
color: inkColor(getPalette().dim),
|
|
29315
|
+
wrap: "truncate-end"
|
|
29316
|
+
}, truncateColumns(`… +${overflow} more lines · ctrl+o shows the full call in the transcript`, viewport.contentColumns))] : [], ...body.length > 0 ? [(0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 })] : [], ...APPROVAL_OPTIONS.map((option, index) => {
|
|
29317
|
+
const selected = !snapshot.answered && index === cursor;
|
|
29318
|
+
return (0, import_react.createElement)(Text, {
|
|
29319
|
+
key: option.key,
|
|
29320
|
+
color: selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().text),
|
|
29321
|
+
bold: selected || void 0,
|
|
29322
|
+
wrap: "truncate-end"
|
|
29323
|
+
}, truncateColumns(`${selected ? "›" : " "} ${index + 1}. ${option.label} (${option.hotkey})`, viewport.contentColumns));
|
|
29324
|
+
}), (0, import_react.createElement)(Text, {
|
|
29325
|
+
color: inkColor(getPalette().dim),
|
|
28823
29326
|
wrap: "truncate-end"
|
|
28824
|
-
},
|
|
29327
|
+
}, truncateColumns(snapshot.answered ? "submitted…" : "↑↓ choose · enter confirm · y/n/d quick · esc reject", viewport.contentColumns)));
|
|
28825
29328
|
}
|
|
28826
29329
|
/**
|
|
28827
29330
|
* The ask_user_question bar: walks one request question by question,
|
|
@@ -29067,18 +29570,31 @@ function QuestionBar({ store, snapshot, locked }) {
|
|
|
29067
29570
|
}, dim(truncateColumns(footer, viewport.contentColumns))));
|
|
29068
29571
|
}
|
|
29069
29572
|
/** The /model panel: a scrolling list over the advisory model directory. */
|
|
29070
|
-
function ModelPanel({ directory, error, onSelect, onProviders, onRetry, onClose }) {
|
|
29573
|
+
function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry, onClose }) {
|
|
29071
29574
|
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
29072
29575
|
const stdout = useStdout().stdout;
|
|
29073
29576
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
29074
29577
|
const rows = directory?.rows ?? [];
|
|
29578
|
+
const positioned = (0, import_react.useRef)(false);
|
|
29075
29579
|
(0, import_react.useEffect)(() => {
|
|
29076
|
-
if (rows.length === 0) {
|
|
29077
|
-
if (
|
|
29580
|
+
if (positioned.current || rows.length === 0 || current === void 0) {
|
|
29581
|
+
if (rows.length === 0) {
|
|
29582
|
+
if (cursor !== 0) setCursor(0);
|
|
29583
|
+
return;
|
|
29584
|
+
}
|
|
29585
|
+
if (cursor >= rows.length) setCursor(rows.length - 1);
|
|
29078
29586
|
return;
|
|
29079
29587
|
}
|
|
29080
|
-
|
|
29081
|
-
|
|
29588
|
+
const index = rows.findIndex((row) => `${row.provider}/${row.model}` === current);
|
|
29589
|
+
if (index >= 0) {
|
|
29590
|
+
positioned.current = true;
|
|
29591
|
+
setCursor(index);
|
|
29592
|
+
} else if (cursor >= rows.length) setCursor(Math.max(0, rows.length - 1));
|
|
29593
|
+
}, [
|
|
29594
|
+
rows,
|
|
29595
|
+
cursor,
|
|
29596
|
+
current
|
|
29597
|
+
]);
|
|
29082
29598
|
useInput((input, key) => {
|
|
29083
29599
|
if (key.escape || input === "q") {
|
|
29084
29600
|
onClose();
|
|
@@ -29518,6 +30034,9 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
|
|
|
29518
30034
|
(0, import_react.createElement)(Box, { key: "local-statusline" }, row("/statusline", "customize the status line items")),
|
|
29519
30035
|
(0, import_react.createElement)(Box, { key: "local-theme" }, row("/theme", "switch the color theme")),
|
|
29520
30036
|
(0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
|
|
30037
|
+
(0, import_react.createElement)(Box, { key: "local-agents" }, row("/agents", "inspect subagent sessions of this conversation")),
|
|
30038
|
+
(0, import_react.createElement)(Box, { key: "local-subagent" }, row("/subagent", "choose the model delegated subagents run on")),
|
|
30039
|
+
(0, import_react.createElement)(Box, { key: "local-delete" }, row("/delete", "delete a session and its subagent threads")),
|
|
29521
30040
|
(0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
|
|
29522
30041
|
(0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
|
|
29523
30042
|
(0, import_react.createElement)(Box, { key: "local-title" }, row("/title", "rename this session (/title <text>)")),
|
|
@@ -29819,6 +30338,21 @@ function completionCandidates(value, descriptors, skills) {
|
|
|
29819
30338
|
description: "search and recall past prompts",
|
|
29820
30339
|
origin: "command"
|
|
29821
30340
|
},
|
|
30341
|
+
{
|
|
30342
|
+
label: "/agents",
|
|
30343
|
+
description: "inspect subagent sessions of this conversation",
|
|
30344
|
+
origin: "command"
|
|
30345
|
+
},
|
|
30346
|
+
{
|
|
30347
|
+
label: "/subagent",
|
|
30348
|
+
description: "choose the model delegated subagents run on",
|
|
30349
|
+
origin: "command"
|
|
30350
|
+
},
|
|
30351
|
+
{
|
|
30352
|
+
label: "/delete",
|
|
30353
|
+
description: "delete a session and its subagent threads",
|
|
30354
|
+
origin: "command"
|
|
30355
|
+
},
|
|
29822
30356
|
{
|
|
29823
30357
|
label: "/clear",
|
|
29824
30358
|
description: "clear the screen",
|
|
@@ -29920,7 +30454,7 @@ function CompletionMenu({ active, mention, index, rows }) {
|
|
|
29920
30454
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
29921
30455
|
* box passes every key through untouched.
|
|
29922
30456
|
*/
|
|
29923
|
-
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }) {
|
|
30457
|
+
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, openAgents, openSubagent, openDelete, deleteConfirm, confirmDelete, cancelDelete, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }) {
|
|
29924
30458
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
29925
30459
|
const [value, setValue] = (0, import_react.useState)("");
|
|
29926
30460
|
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
@@ -30015,8 +30549,39 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
30015
30549
|
description: row.description,
|
|
30016
30550
|
origin: "path"
|
|
30017
30551
|
})) : candidates;
|
|
30552
|
+
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
30553
|
+
const acceptMenuCandidate = () => {
|
|
30554
|
+
if (mentionActive && mentionToken !== void 0) {
|
|
30555
|
+
const row = mentionRows[completionIndex % mentionRows.length];
|
|
30556
|
+
if (row !== void 0) {
|
|
30557
|
+
const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
|
|
30558
|
+
setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
|
|
30559
|
+
setCursor(mentionToken.start + insertion.length);
|
|
30560
|
+
}
|
|
30561
|
+
} else if (pathActive) {
|
|
30562
|
+
const row = pathRows[completionIndex % Math.max(1, pathRows.length)];
|
|
30563
|
+
if (row !== void 0) {
|
|
30564
|
+
const insertion = row.kind === "directory" ? `${row.label}/` : row.label;
|
|
30565
|
+
setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor));
|
|
30566
|
+
setCursor(pathTokenStart + insertion.length);
|
|
30567
|
+
}
|
|
30568
|
+
} else {
|
|
30569
|
+
const candidate = candidates[completionIndex % candidates.length];
|
|
30570
|
+
if (candidate !== void 0) {
|
|
30571
|
+
setValue(`${candidate.label} `);
|
|
30572
|
+
setCursor(candidate.label.length + 1);
|
|
30573
|
+
}
|
|
30574
|
+
}
|
|
30575
|
+
setCompletionIndex(0);
|
|
30576
|
+
setDismissedMenuValue(void 0);
|
|
30577
|
+
};
|
|
30018
30578
|
useInput((input, key) => {
|
|
30019
30579
|
if (!active) return;
|
|
30580
|
+
if (deleteConfirm !== void 0) {
|
|
30581
|
+
if (input === "y" || input === "Y") confirmDelete();
|
|
30582
|
+
else cancelDelete();
|
|
30583
|
+
return;
|
|
30584
|
+
}
|
|
30020
30585
|
if (key.tab && key.shift) {
|
|
30021
30586
|
try {
|
|
30022
30587
|
const next = cyclePermission();
|
|
@@ -30072,6 +30637,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
30072
30637
|
setDismissedMenuValue(void 0);
|
|
30073
30638
|
return;
|
|
30074
30639
|
}
|
|
30640
|
+
if (menuActive) {
|
|
30641
|
+
if (!(!mentionActive && !pathActive && candidates.some((candidate) => candidate.label === value))) {
|
|
30642
|
+
acceptMenuCandidate();
|
|
30643
|
+
return;
|
|
30644
|
+
}
|
|
30645
|
+
}
|
|
30075
30646
|
const text = value.trim();
|
|
30076
30647
|
setValue("");
|
|
30077
30648
|
setCursor(0);
|
|
@@ -30162,6 +30733,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
30162
30733
|
openHistory();
|
|
30163
30734
|
return;
|
|
30164
30735
|
}
|
|
30736
|
+
if (text === "/agents") {
|
|
30737
|
+
openAgents();
|
|
30738
|
+
return;
|
|
30739
|
+
}
|
|
30740
|
+
if (text === "/subagent") {
|
|
30741
|
+
openSubagent();
|
|
30742
|
+
return;
|
|
30743
|
+
}
|
|
30744
|
+
if (text === "/delete" || text.startsWith("/delete ")) {
|
|
30745
|
+
openDelete(text.slice(7).trim());
|
|
30746
|
+
return;
|
|
30747
|
+
}
|
|
30165
30748
|
if (busy && !text.startsWith("/")) {
|
|
30166
30749
|
steer(text);
|
|
30167
30750
|
return;
|
|
@@ -30200,29 +30783,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
30200
30783
|
return;
|
|
30201
30784
|
}
|
|
30202
30785
|
if (key.tab && menuActive) {
|
|
30203
|
-
|
|
30204
|
-
const row = mentionRows[completionIndex % mentionRows.length];
|
|
30205
|
-
if (row !== void 0) {
|
|
30206
|
-
const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
|
|
30207
|
-
setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
|
|
30208
|
-
setCursor(mentionToken.start + insertion.length);
|
|
30209
|
-
}
|
|
30210
|
-
} else if (pathActive) {
|
|
30211
|
-
const row = pathRows[completionIndex % Math.max(1, pathRows.length)];
|
|
30212
|
-
if (row !== void 0) {
|
|
30213
|
-
const insertion = row.kind === "directory" ? `${row.label}/` : row.label;
|
|
30214
|
-
setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor));
|
|
30215
|
-
setCursor(pathTokenStart + insertion.length);
|
|
30216
|
-
}
|
|
30217
|
-
} else {
|
|
30218
|
-
const candidate = candidates[completionIndex % candidates.length];
|
|
30219
|
-
if (candidate !== void 0) {
|
|
30220
|
-
setValue(`${candidate.label} `);
|
|
30221
|
-
setCursor(candidate.label.length + 1);
|
|
30222
|
-
}
|
|
30223
|
-
}
|
|
30224
|
-
setCompletionIndex(0);
|
|
30225
|
-
setDismissedMenuValue(void 0);
|
|
30786
|
+
acceptMenuCandidate();
|
|
30226
30787
|
return;
|
|
30227
30788
|
}
|
|
30228
30789
|
if (key.backspace || key.delete) {
|
|
@@ -30311,6 +30872,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
30311
30872
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
|
|
30312
30873
|
const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
|
|
30313
30874
|
if (frozen) {
|
|
30875
|
+
if (deleteConfirm !== void 0) return (0, import_react.createElement)(Box, {
|
|
30876
|
+
width: Math.max(1, columns - 1),
|
|
30877
|
+
borderStyle: "round",
|
|
30878
|
+
borderColor: inkColor(getPalette().warn),
|
|
30879
|
+
paddingX: 1
|
|
30880
|
+
}, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, (0, import_react.createElement)(Text, {
|
|
30881
|
+
color: inkColor(getPalette().warn),
|
|
30882
|
+
bold: true
|
|
30883
|
+
}, "❯ "), (0, import_react.createElement)(Text, {
|
|
30884
|
+
color: inkColor(getPalette().warn),
|
|
30885
|
+
bold: true
|
|
30886
|
+
}, "y delete · any other key cancels")));
|
|
30314
30887
|
const frozen = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
|
|
30315
30888
|
return (0, import_react.createElement)(Box, {
|
|
30316
30889
|
width: Math.max(1, columns - 1),
|
|
@@ -30679,6 +31252,36 @@ function App(props) {
|
|
|
30679
31252
|
const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
|
|
30680
31253
|
const [themeOpen, setThemeOpen] = (0, import_react.useState)(false);
|
|
30681
31254
|
const [historyOpen, setHistoryOpen] = (0, import_react.useState)(false);
|
|
31255
|
+
const [agentsOpen, setAgentsOpen] = (0, import_react.useState)(false);
|
|
31256
|
+
const [subagentOpen, setSubagentOpen] = (0, import_react.useState)(false);
|
|
31257
|
+
/** /delete state: delete-mode hint plus an optional pre-armed row id. */
|
|
31258
|
+
const [resumeDelete, setResumeDelete] = (0, import_react.useState)({ mode: false });
|
|
31259
|
+
/** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
|
|
31260
|
+
* composer takes the keys, the resume panel yields until it settles. */
|
|
31261
|
+
const [deleteConfirmId, setDeleteConfirmId] = (0, import_react.useState)(void 0);
|
|
31262
|
+
/** Bumped after a deletion so the /resume listing reloads immediately. */
|
|
31263
|
+
const [deleteReloadToken, setDeleteReloadToken] = (0, import_react.useState)(0);
|
|
31264
|
+
const requestDelete = (0, import_react.useCallback)((row) => {
|
|
31265
|
+
setDeleteConfirmId(row.id);
|
|
31266
|
+
}, []);
|
|
31267
|
+
const cancelDelete = (0, import_react.useCallback)(() => {
|
|
31268
|
+
setDeleteConfirmId(void 0);
|
|
31269
|
+
}, []);
|
|
31270
|
+
const confirmDelete = (0, import_react.useCallback)(() => {
|
|
31271
|
+
const id = deleteConfirmId;
|
|
31272
|
+
if (id === void 0) return;
|
|
31273
|
+
setDeleteConfirmId(void 0);
|
|
31274
|
+
props.deleteSession(id).then((outcome) => {
|
|
31275
|
+
notify(outcome);
|
|
31276
|
+
setDeleteReloadToken((token) => token + 1);
|
|
31277
|
+
}, (reason) => {
|
|
31278
|
+
notify(`delete failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
31279
|
+
});
|
|
31280
|
+
}, [
|
|
31281
|
+
deleteConfirmId,
|
|
31282
|
+
props.deleteSession,
|
|
31283
|
+
notify
|
|
31284
|
+
]);
|
|
30682
31285
|
/** The /history panel's accepted entry: text plus its recall-space index. */
|
|
30683
31286
|
const [historyFill, setHistoryFill] = (0, import_react.useState)(void 0);
|
|
30684
31287
|
/** Submissions recorded in this process (Codex local history; persistent file stays in the runner). */
|
|
@@ -30712,9 +31315,10 @@ function App(props) {
|
|
|
30712
31315
|
const [refreshEpoch, setRefreshEpoch] = (0, import_react.useState)(0);
|
|
30713
31316
|
const approvalSnapshot = (0, import_react.useSyncExternalStore)(props.approval.subscribe, props.approval.getSnapshot);
|
|
30714
31317
|
const questionSnapshot = (0, import_react.useSyncExternalStore)(props.questions.subscribe, props.questions.getSnapshot);
|
|
31318
|
+
const agentRows = (0, import_react.useSyncExternalStore)(props.subagents.subscribe, props.subagents.getSnapshot);
|
|
30715
31319
|
const approvalPending = approvalSnapshot.pending !== void 0;
|
|
30716
31320
|
const questionPending = questionSnapshot.pending !== void 0;
|
|
30717
|
-
const inputActive = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
31321
|
+
const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
30718
31322
|
(0, import_react.useEffect)(() => {
|
|
30719
31323
|
if (!approvalPending && !questionPending) return;
|
|
30720
31324
|
setModelOpen(false);
|
|
@@ -30729,6 +31333,9 @@ function App(props) {
|
|
|
30729
31333
|
setStatuslineOpen(false);
|
|
30730
31334
|
setThemeOpen(false);
|
|
30731
31335
|
setHistoryOpen(false);
|
|
31336
|
+
setAgentsOpen(false);
|
|
31337
|
+
setSubagentOpen(false);
|
|
31338
|
+
setDeleteConfirmId(void 0);
|
|
30732
31339
|
setVerboseOpen(false);
|
|
30733
31340
|
}, [approvalPending, questionPending]);
|
|
30734
31341
|
const settledRowsCache = (0, import_react.useRef)(void 0);
|
|
@@ -30788,8 +31395,8 @@ function App(props) {
|
|
|
30788
31395
|
const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
|
|
30789
31396
|
const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
|
|
30790
31397
|
const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
|
|
30791
|
-
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
30792
|
-
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
|
|
31398
|
+
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending;
|
|
31399
|
+
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
|
|
30793
31400
|
const closeInspector = (0, import_react.useCallback)(() => {
|
|
30794
31401
|
setVerboseOpen(false);
|
|
30795
31402
|
}, []);
|
|
@@ -30898,6 +31505,7 @@ function App(props) {
|
|
|
30898
31505
|
onBack: () => setProviderOpen(false)
|
|
30899
31506
|
});
|
|
30900
31507
|
else if (effortFor !== void 0) modelSurface = (0, import_react.createElement)(EffortPanel, {
|
|
31508
|
+
key: `${effortFor.provider}/${effortFor.model}`,
|
|
30901
31509
|
row: effortFor,
|
|
30902
31510
|
current: effortLabel,
|
|
30903
31511
|
select: (effortId) => applyModel(effortFor, effortId),
|
|
@@ -30906,6 +31514,7 @@ function App(props) {
|
|
|
30906
31514
|
else modelSurface = (0, import_react.createElement)(ModelPanel, {
|
|
30907
31515
|
directory,
|
|
30908
31516
|
error: modelError,
|
|
31517
|
+
current: modelLabel,
|
|
30909
31518
|
onSelect: (row) => {
|
|
30910
31519
|
if (row.reasoning !== void 0 && row.reasoning.efforts.length > 1) {
|
|
30911
31520
|
setEffortFor(row);
|
|
@@ -30936,13 +31545,14 @@ function App(props) {
|
|
|
30936
31545
|
dim: false,
|
|
30937
31546
|
maxRows: answerRows,
|
|
30938
31547
|
prefix: " "
|
|
30939
|
-
}, busy ? (0, import_react.createElement)(Caret) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, (0, import_react.createElement)(QuestionBar, {
|
|
31548
|
+
}, busy ? (0, import_react.createElement)(Caret) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, { rows: agentRows }) : void 0, (0, import_react.createElement)(QuestionBar, {
|
|
30940
31549
|
store: props.questions,
|
|
30941
31550
|
snapshot: questionSnapshot,
|
|
30942
31551
|
locked: false
|
|
30943
31552
|
}), (0, import_react.createElement)(ApprovalBar, {
|
|
30944
31553
|
snapshot: approvalSnapshot,
|
|
30945
|
-
locked: questionPending
|
|
31554
|
+
locked: questionPending,
|
|
31555
|
+
notify
|
|
30946
31556
|
}), modelSurface, helpOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HelpPanel, {
|
|
30947
31557
|
descriptors,
|
|
30948
31558
|
skills,
|
|
@@ -30981,6 +31591,10 @@ function App(props) {
|
|
|
30981
31591
|
currentCwd: props.workspaceRoot,
|
|
30982
31592
|
load: props.loadSessions,
|
|
30983
31593
|
readTranscript: props.loadSessionTranscript,
|
|
31594
|
+
requestDelete,
|
|
31595
|
+
deleteConfirmId,
|
|
31596
|
+
reloadToken: deleteReloadToken,
|
|
31597
|
+
deleteMode: resumeDelete.mode,
|
|
30984
31598
|
select: (row) => {
|
|
30985
31599
|
props.switchSession(row);
|
|
30986
31600
|
setResumeOpen(false);
|
|
@@ -31016,6 +31630,29 @@ function App(props) {
|
|
|
31016
31630
|
setHistoryOpen(false);
|
|
31017
31631
|
},
|
|
31018
31632
|
close: () => setHistoryOpen(false)
|
|
31633
|
+
}) : void 0, agentsOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(AgentsPanel, {
|
|
31634
|
+
live: agentRows,
|
|
31635
|
+
load: props.loadSubagents,
|
|
31636
|
+
readTranscript: props.loadSessionTranscript,
|
|
31637
|
+
close: () => setAgentsOpen(false)
|
|
31638
|
+
}) : void 0, subagentOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(SubagentPanel, {
|
|
31639
|
+
current: props.subagentModel,
|
|
31640
|
+
load: props.loadModels,
|
|
31641
|
+
pick: (row, effortId) => {
|
|
31642
|
+
try {
|
|
31643
|
+
const label = props.setSubagentModel(row, effortId);
|
|
31644
|
+
notify(`subagents → ${label}`);
|
|
31645
|
+
setSubagentOpen(false);
|
|
31646
|
+
} catch (reason) {
|
|
31647
|
+
notify(`subagent model change failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
31648
|
+
}
|
|
31649
|
+
},
|
|
31650
|
+
inherit: () => {
|
|
31651
|
+
props.clearSubagentModel();
|
|
31652
|
+
notify("subagents → inherit current model");
|
|
31653
|
+
setSubagentOpen(false);
|
|
31654
|
+
},
|
|
31655
|
+
close: () => setSubagentOpen(false)
|
|
31019
31656
|
}) : void 0, notice === void 0 ? void 0 : (0, import_react.createElement)(NoticeLine, {
|
|
31020
31657
|
text: notice.text,
|
|
31021
31658
|
tone: notice.tone,
|
|
@@ -31057,7 +31694,8 @@ function App(props) {
|
|
|
31057
31694
|
return;
|
|
31058
31695
|
}
|
|
31059
31696
|
if (row.reasoning === void 0 || row.reasoning.efforts.length === 0) {
|
|
31060
|
-
|
|
31697
|
+
setEffortFor(row);
|
|
31698
|
+
setModelOpen(true);
|
|
31061
31699
|
return;
|
|
31062
31700
|
}
|
|
31063
31701
|
setEffortFor(row);
|
|
@@ -31071,7 +31709,10 @@ function App(props) {
|
|
|
31071
31709
|
},
|
|
31072
31710
|
openMode: () => setModeOpen(true),
|
|
31073
31711
|
openPermission: () => setPermissionOpen(true),
|
|
31074
|
-
openResume: () =>
|
|
31712
|
+
openResume: () => {
|
|
31713
|
+
setResumeDelete({ mode: false });
|
|
31714
|
+
setResumeOpen(true);
|
|
31715
|
+
},
|
|
31075
31716
|
openPlugin: (query = "") => {
|
|
31076
31717
|
setPluginQuery(query);
|
|
31077
31718
|
setPluginOpen(true);
|
|
@@ -31079,6 +31720,20 @@ function App(props) {
|
|
|
31079
31720
|
openStatusline: () => setStatuslineOpen(true),
|
|
31080
31721
|
openTheme: () => setThemeOpen(true),
|
|
31081
31722
|
openHistory: () => setHistoryOpen(true),
|
|
31723
|
+
openAgents: () => setAgentsOpen(true),
|
|
31724
|
+
openSubagent: () => setSubagentOpen(true),
|
|
31725
|
+
openDelete: (id) => {
|
|
31726
|
+
const armed = id === void 0 || id === "" ? void 0 : id;
|
|
31727
|
+
setResumeDelete({
|
|
31728
|
+
mode: true,
|
|
31729
|
+
...armed === void 0 ? {} : { id: armed }
|
|
31730
|
+
});
|
|
31731
|
+
setDeleteConfirmId(armed);
|
|
31732
|
+
setResumeOpen(true);
|
|
31733
|
+
},
|
|
31734
|
+
deleteConfirm: deleteConfirmId,
|
|
31735
|
+
confirmDelete,
|
|
31736
|
+
cancelDelete,
|
|
31082
31737
|
createSession: props.createSession,
|
|
31083
31738
|
cancelSessionSwitch: props.cancelSessionSwitch,
|
|
31084
31739
|
notify,
|
|
@@ -31149,15 +31804,26 @@ function App(props) {
|
|
|
31149
31804
|
* @returns the store the renderer subscribes to.
|
|
31150
31805
|
*/
|
|
31151
31806
|
function mountApprovalAnswerer(ctx, owns, preview) {
|
|
31807
|
+
const queue = [];
|
|
31152
31808
|
let snapshot = {
|
|
31153
31809
|
pending: void 0,
|
|
31154
|
-
answered: false
|
|
31810
|
+
answered: false,
|
|
31811
|
+
queued: 0
|
|
31155
31812
|
};
|
|
31156
31813
|
const listeners = /* @__PURE__ */ new Set();
|
|
31157
|
-
const
|
|
31158
|
-
|
|
31814
|
+
const publish = () => {
|
|
31815
|
+
const head = queue[0];
|
|
31816
|
+
snapshot = {
|
|
31817
|
+
pending: head === void 0 ? void 0 : head.pending,
|
|
31818
|
+
answered: head !== void 0 && head.answered,
|
|
31819
|
+
queued: Math.max(0, queue.length - 1)
|
|
31820
|
+
};
|
|
31159
31821
|
for (const listener of listeners) listener();
|
|
31160
31822
|
};
|
|
31823
|
+
const removeSlot = (slot) => {
|
|
31824
|
+
const at = queue.indexOf(slot);
|
|
31825
|
+
if (at !== -1) queue.splice(at, 1);
|
|
31826
|
+
};
|
|
31161
31827
|
ctx.on("approval/request", (request, next) => {
|
|
31162
31828
|
if (!owns(request.agent)) return next();
|
|
31163
31829
|
if (request.signal?.aborted === true) return Promise.resolve("cancelled");
|
|
@@ -31172,39 +31838,36 @@ function mountApprovalAnswerer(ctx, owns, preview) {
|
|
|
31172
31838
|
if (resolved) return;
|
|
31173
31839
|
resolved = true;
|
|
31174
31840
|
detachAbort();
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
answered: false
|
|
31178
|
-
});
|
|
31841
|
+
removeSlot(slot);
|
|
31842
|
+
publish();
|
|
31179
31843
|
settle("cancelled");
|
|
31180
31844
|
};
|
|
31181
31845
|
if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
|
|
31182
|
-
const
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
answered
|
|
31193
|
-
|
|
31194
|
-
|
|
31846
|
+
const slot = {
|
|
31847
|
+
answered: false,
|
|
31848
|
+
pending: {
|
|
31849
|
+
headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
|
|
31850
|
+
toolName: request.toolName,
|
|
31851
|
+
command: preview(request),
|
|
31852
|
+
answer: (outcome) => {
|
|
31853
|
+
if (resolved) return;
|
|
31854
|
+
resolved = true;
|
|
31855
|
+
detachAbort();
|
|
31856
|
+
slot.answered = true;
|
|
31857
|
+
publish();
|
|
31858
|
+
settle(outcome);
|
|
31859
|
+
}
|
|
31195
31860
|
}
|
|
31196
31861
|
};
|
|
31197
|
-
|
|
31198
|
-
|
|
31199
|
-
answered: false
|
|
31200
|
-
});
|
|
31862
|
+
queue.push(slot);
|
|
31863
|
+
publish();
|
|
31201
31864
|
return new Promise((resolve) => {
|
|
31202
31865
|
settle = resolve;
|
|
31203
31866
|
}).then((outcome) => {
|
|
31204
|
-
if (outcome !== "cancelled")
|
|
31205
|
-
|
|
31206
|
-
|
|
31207
|
-
}
|
|
31867
|
+
if (outcome !== "cancelled") {
|
|
31868
|
+
removeSlot(slot);
|
|
31869
|
+
publish();
|
|
31870
|
+
}
|
|
31208
31871
|
return outcome;
|
|
31209
31872
|
});
|
|
31210
31873
|
});
|
|
@@ -31352,6 +32015,32 @@ function buildModelSelection(row, effortId) {
|
|
|
31352
32015
|
...selected === void 0 ? {} : { reasoningEffort: ReasoningEffortId(selected) }
|
|
31353
32016
|
};
|
|
31354
32017
|
}
|
|
32018
|
+
/** Display label for one applied selection: `provider/model` or `provider/model@effort`. */
|
|
32019
|
+
function modelSelectionLabel(selection) {
|
|
32020
|
+
return selection.reasoningEffort === void 0 ? `${selection.provider}/${selection.model}` : `${selection.provider}/${selection.model}@${selection.reasoningEffort}`;
|
|
32021
|
+
}
|
|
32022
|
+
/**
|
|
32023
|
+
* Apply one model selection onto a resolved request config — the exact
|
|
32024
|
+
* semantics of the kernel's `installModelSelection` request listener,
|
|
32025
|
+
* extracted so the TUI can mirror it for subagent-origin requests: children
|
|
32026
|
+
* spawned by the subagent tool inherit the parent's CREATE-TIME AgentOptions,
|
|
32027
|
+
* which a mid-session /model switch never touches, so delegated work would
|
|
32028
|
+
* otherwise keep running on the launch-time route. An absent effort strips
|
|
32029
|
+
* any inherited effort (restoring the selected model's provider default),
|
|
32030
|
+
* matching the kernel listener field-for-field.
|
|
32031
|
+
* @param resolved - the config the inner chain produced.
|
|
32032
|
+
* @param selection - the selection to enforce.
|
|
32033
|
+
* @returns the overridden config.
|
|
32034
|
+
*/
|
|
32035
|
+
function applyModelSelectionToConfig(resolved, selection) {
|
|
32036
|
+
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved;
|
|
32037
|
+
return {
|
|
32038
|
+
...withoutInheritedEffort,
|
|
32039
|
+
provider: selection.provider,
|
|
32040
|
+
model: selection.model,
|
|
32041
|
+
...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
|
|
32042
|
+
};
|
|
32043
|
+
}
|
|
31355
32044
|
/**
|
|
31356
32045
|
* Load the selectable model directory from the live `ctx.llm` registry.
|
|
31357
32046
|
* Providers are listed synchronously; each provider's models are discovered
|
|
@@ -32000,6 +32689,153 @@ function createTranscriptStore(replay) {
|
|
|
32000
32689
|
}
|
|
32001
32690
|
};
|
|
32002
32691
|
}
|
|
32692
|
+
/** Bounded last-activity text (plain characters, display-sliced later). */
|
|
32693
|
+
const MAX_ACTIVITY_CHARS = 80;
|
|
32694
|
+
/** Single-line bounded preview of an assembled message's text content. */
|
|
32695
|
+
function messagePreview(content) {
|
|
32696
|
+
if (!Array.isArray(content)) return "replied";
|
|
32697
|
+
const texts = [];
|
|
32698
|
+
for (const block of content) {
|
|
32699
|
+
if (texts.join(" ").length >= MAX_ACTIVITY_CHARS) break;
|
|
32700
|
+
if (typeof block === "object" && block !== null) {
|
|
32701
|
+
const { type, text } = block;
|
|
32702
|
+
if (type === "text" && typeof text === "string" && text !== "") texts.push(text);
|
|
32703
|
+
}
|
|
32704
|
+
}
|
|
32705
|
+
const joined = texts.join(" ").replace(/\s+/gu, " ").trim();
|
|
32706
|
+
return joined === "" ? "replied" : bound(joined);
|
|
32707
|
+
}
|
|
32708
|
+
/** Bound one activity string to the display budget. */
|
|
32709
|
+
function bound(text) {
|
|
32710
|
+
const flat = text.replace(/\s+/gu, " ").trim();
|
|
32711
|
+
return flat.length > MAX_ACTIVITY_CHARS ? `${flat.slice(0, 79)}…` : flat;
|
|
32712
|
+
}
|
|
32713
|
+
/**
|
|
32714
|
+
* Fold one child-session event into its feed row (pure).
|
|
32715
|
+
* Unknown event kinds leave the row untouched.
|
|
32716
|
+
* @param previous - the row's current state, when any.
|
|
32717
|
+
* @param sessionId - the child session id.
|
|
32718
|
+
* @param event - the child session event.
|
|
32719
|
+
* @returns the next row state.
|
|
32720
|
+
*/
|
|
32721
|
+
function foldSubagentRow(previous, sessionId, event) {
|
|
32722
|
+
const base = previous ?? {
|
|
32723
|
+
id: sessionId,
|
|
32724
|
+
label: `agent ${sessionId.slice(-6)}`,
|
|
32725
|
+
state: "running",
|
|
32726
|
+
activity: "starting…",
|
|
32727
|
+
updatedAt: event.time
|
|
32728
|
+
};
|
|
32729
|
+
const data = event.data;
|
|
32730
|
+
switch (event.type) {
|
|
32731
|
+
case "session/title": {
|
|
32732
|
+
const title = data["title"];
|
|
32733
|
+
const text = typeof title === "string" && title.trim() !== "" ? title : void 0;
|
|
32734
|
+
return text === void 0 || text === base.label ? base : {
|
|
32735
|
+
...base,
|
|
32736
|
+
label: bound(text),
|
|
32737
|
+
updatedAt: event.time
|
|
32738
|
+
};
|
|
32739
|
+
}
|
|
32740
|
+
case "request/header": return {
|
|
32741
|
+
...base,
|
|
32742
|
+
state: "running",
|
|
32743
|
+
activity: "working…",
|
|
32744
|
+
updatedAt: event.time
|
|
32745
|
+
};
|
|
32746
|
+
case "user/message": return {
|
|
32747
|
+
...base,
|
|
32748
|
+
state: "running",
|
|
32749
|
+
activity: "prompted",
|
|
32750
|
+
updatedAt: event.time
|
|
32751
|
+
};
|
|
32752
|
+
case "assistant/chunk": return {
|
|
32753
|
+
...base,
|
|
32754
|
+
state: "running",
|
|
32755
|
+
activity: "thinking…",
|
|
32756
|
+
updatedAt: event.time
|
|
32757
|
+
};
|
|
32758
|
+
case "assistant/message": return {
|
|
32759
|
+
...base,
|
|
32760
|
+
state: "idle",
|
|
32761
|
+
activity: messagePreview(data["message"] === void 0 ? void 0 : data["message"].content),
|
|
32762
|
+
updatedAt: event.time
|
|
32763
|
+
};
|
|
32764
|
+
case "tool/call": {
|
|
32765
|
+
const name = typeof data["name"] === "string" ? data["name"] : "tool";
|
|
32766
|
+
return {
|
|
32767
|
+
...base,
|
|
32768
|
+
state: "running",
|
|
32769
|
+
activity: `tool ${name}`,
|
|
32770
|
+
updatedAt: event.time
|
|
32771
|
+
};
|
|
32772
|
+
}
|
|
32773
|
+
case "tool/result": return {
|
|
32774
|
+
...base,
|
|
32775
|
+
state: "running",
|
|
32776
|
+
activity: "tool done",
|
|
32777
|
+
updatedAt: event.time
|
|
32778
|
+
};
|
|
32779
|
+
case "turn/start": return {
|
|
32780
|
+
...base,
|
|
32781
|
+
state: "running",
|
|
32782
|
+
activity: base.activity === "starting…" ? "working…" : base.activity,
|
|
32783
|
+
updatedAt: event.time
|
|
32784
|
+
};
|
|
32785
|
+
case "turn/end": return {
|
|
32786
|
+
...base,
|
|
32787
|
+
state: "done",
|
|
32788
|
+
activity: "finished",
|
|
32789
|
+
updatedAt: event.time
|
|
32790
|
+
};
|
|
32791
|
+
default: return base;
|
|
32792
|
+
}
|
|
32793
|
+
}
|
|
32794
|
+
/**
|
|
32795
|
+
* Create one subagent feed. `apply` folds a child event (the caller gates
|
|
32796
|
+
* which sessions are children); `reset` clears on a session switch. Row
|
|
32797
|
+
* order is first-seen; the snapshot array is frozen and only replaced when
|
|
32798
|
+
* a row actually changed.
|
|
32799
|
+
* @returns the mutable feed handle plus its `SubagentFeedView`.
|
|
32800
|
+
*/
|
|
32801
|
+
function createSubagentFeed() {
|
|
32802
|
+
let rows = Object.freeze([]);
|
|
32803
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
32804
|
+
let scheduled = false;
|
|
32805
|
+
const notify = () => {
|
|
32806
|
+
if (scheduled) return;
|
|
32807
|
+
scheduled = true;
|
|
32808
|
+
queueMicrotask(() => {
|
|
32809
|
+
scheduled = false;
|
|
32810
|
+
for (const listener of listeners) listener();
|
|
32811
|
+
});
|
|
32812
|
+
};
|
|
32813
|
+
return {
|
|
32814
|
+
apply(sessionId, event) {
|
|
32815
|
+
const index = rows.findIndex((row) => row.id === sessionId);
|
|
32816
|
+
const previous = index === -1 ? void 0 : rows[index];
|
|
32817
|
+
const next = foldSubagentRow(previous, sessionId, event);
|
|
32818
|
+
if (next === previous) return;
|
|
32819
|
+
if (index === -1 && rows.length >= 8) return;
|
|
32820
|
+
rows = Object.freeze(index === -1 ? [...rows, next] : rows.map((row, at) => at === index ? next : row));
|
|
32821
|
+
notify();
|
|
32822
|
+
},
|
|
32823
|
+
reset() {
|
|
32824
|
+
if (rows.length === 0) return;
|
|
32825
|
+
rows = Object.freeze([]);
|
|
32826
|
+
notify();
|
|
32827
|
+
},
|
|
32828
|
+
subscribe(listener) {
|
|
32829
|
+
listeners.add(listener);
|
|
32830
|
+
return () => {
|
|
32831
|
+
listeners.delete(listener);
|
|
32832
|
+
};
|
|
32833
|
+
},
|
|
32834
|
+
getSnapshot() {
|
|
32835
|
+
return rows;
|
|
32836
|
+
}
|
|
32837
|
+
};
|
|
32838
|
+
}
|
|
32003
32839
|
//#endregion
|
|
32004
32840
|
//#region src/skills.ts
|
|
32005
32841
|
function toRows(skills) {
|
|
@@ -32287,76 +33123,6 @@ function listPluginRows(ctx) {
|
|
|
32287
33123
|
return rows;
|
|
32288
33124
|
}
|
|
32289
33125
|
//#endregion
|
|
32290
|
-
//#region src/session-directory.ts
|
|
32291
|
-
/** Lightweight session-directory projection for the /resume picker. */
|
|
32292
|
-
/** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
|
|
32293
|
-
const CASE_INSENSITIVE_FS = process.platform === "win32" || process.platform === "darwin";
|
|
32294
|
-
/** True when the header describes a subagent conversation (durable lineage). */
|
|
32295
|
-
function isSubagentSession(header) {
|
|
32296
|
-
return header.origin === "subagent" || header.parentSession !== void 0;
|
|
32297
|
-
}
|
|
32298
|
-
function comparablePath(value) {
|
|
32299
|
-
const resolved = resolve(value);
|
|
32300
|
-
return CASE_INSENSITIVE_FS ? resolved.toLowerCase() : resolved;
|
|
32301
|
-
}
|
|
32302
|
-
/** Platform-consistent path equality for session cwd comparisons. */
|
|
32303
|
-
function samePath(left, right) {
|
|
32304
|
-
if (left === void 0) return false;
|
|
32305
|
-
return comparablePath(left) === comparablePath(right);
|
|
32306
|
-
}
|
|
32307
|
-
/**
|
|
32308
|
-
* Unique header match by exact id or unique id prefix (root and subagent
|
|
32309
|
-
* headers alike); the caller applies any lineage gate.
|
|
32310
|
-
* @param headers - the persisted headers.
|
|
32311
|
-
* @param wanted - the id or id prefix.
|
|
32312
|
-
* @returns the uniquely matched header.
|
|
32313
|
-
* @throws when nothing matches or the prefix is ambiguous.
|
|
32314
|
-
*/
|
|
32315
|
-
function matchSessionId(headers, wanted) {
|
|
32316
|
-
const exact = headers.filter((header) => header.id === wanted);
|
|
32317
|
-
const matches = exact.length > 0 ? exact : headers.filter((header) => header.id.startsWith(wanted));
|
|
32318
|
-
if (matches.length === 0) throw new Error(`no persisted session matches "${wanted}"`);
|
|
32319
|
-
if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`);
|
|
32320
|
-
return matches[0];
|
|
32321
|
-
}
|
|
32322
|
-
/** The newest persisted ROOT session pinned to this cwd, or undefined. */
|
|
32323
|
-
function newestRootForCwd(headers, cwd) {
|
|
32324
|
-
return headers.filter((header) => !isSubagentSession(header) && samePath(header.cwd, cwd)).sort((left, right) => right.createdAt - left.createdAt)[0];
|
|
32325
|
-
}
|
|
32326
|
-
/** Filter/sort header-only records. No session log is loaded here. */
|
|
32327
|
-
function projectSessionRows(records, options) {
|
|
32328
|
-
const needle = options.query.trim().toLowerCase();
|
|
32329
|
-
return records.filter((record) => options.sessions === "all" || !isSubagentSession(record.header)).filter((record) => options.cwd === "all" || samePath(record.header.cwd, options.currentCwd)).map((record) => {
|
|
32330
|
-
const cwd = record.header.cwd ?? "";
|
|
32331
|
-
const subagent = isSubagentSession(record.header);
|
|
32332
|
-
return {
|
|
32333
|
-
id: record.header.id,
|
|
32334
|
-
createdAt: record.header.createdAt,
|
|
32335
|
-
cwd,
|
|
32336
|
-
workspace: cwd === "" ? "(no workspace)" : basename(cwd),
|
|
32337
|
-
parent: record.header.parentSession,
|
|
32338
|
-
subagent,
|
|
32339
|
-
resumable: !subagent,
|
|
32340
|
-
live: record.live,
|
|
32341
|
-
persisted: record.persisted,
|
|
32342
|
-
preset: record.header.agentPreset ?? "standard"
|
|
32343
|
-
};
|
|
32344
|
-
}).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);
|
|
32345
|
-
}
|
|
32346
|
-
/** Merge page-local title observations without disturbing directory order. */
|
|
32347
|
-
function mergeSessionTitles(rows, observations) {
|
|
32348
|
-
const titles = /* @__PURE__ */ new Map();
|
|
32349
|
-
for (const observation of observations) {
|
|
32350
|
-
if (observation.status !== "fulfilled") continue;
|
|
32351
|
-
const title = observation.value?.title?.title ?? observation.value?.title?.text;
|
|
32352
|
-
if (title !== void 0 && title.trim() !== "") titles.set(observation.sessionId, title);
|
|
32353
|
-
}
|
|
32354
|
-
return rows.map((row) => titles.has(row.id) ? {
|
|
32355
|
-
...row,
|
|
32356
|
-
title: titles.get(row.id)
|
|
32357
|
-
} : row);
|
|
32358
|
-
}
|
|
32359
|
-
//#endregion
|
|
32360
33126
|
//#region src/index.ts
|
|
32361
33127
|
/**
|
|
32362
33128
|
* @deepseek-ai/dsh-code — the interactive terminal driver. The bundle patch
|
|
@@ -32512,7 +33278,7 @@ async function run(ctx, startup, io) {
|
|
|
32512
33278
|
const sessionQuery = ctx.get("sessionQuery");
|
|
32513
33279
|
if (agents === void 0 || defaultModel === void 0 || sessions === void 0) return;
|
|
32514
33280
|
const cwd = process.cwd();
|
|
32515
|
-
const
|
|
33281
|
+
const currentDefaults = () => defaultModel.currentSelection();
|
|
32516
33282
|
const presets = agentPresetsFrom(ctx);
|
|
32517
33283
|
if (presets === void 0) throw new Error("agent preset service is unavailable; check the dsh-code bundle patch");
|
|
32518
33284
|
const permissionPresets = permissionPresetsFrom(ctx);
|
|
@@ -32528,7 +33294,7 @@ async function run(ctx, startup, io) {
|
|
|
32528
33294
|
mode = (await presets.mount(agentCtx, sessionPreset)).id;
|
|
32529
33295
|
installModelSelection(agentCtx, {
|
|
32530
33296
|
get current() {
|
|
32531
|
-
return resolveEffectiveSelection(selectionState.picked, agentCtx.agent?.session.requestHeader()?.config,
|
|
33297
|
+
return resolveEffectiveSelection(selectionState.picked, agentCtx.agent?.session.requestHeader()?.config, currentDefaults());
|
|
32532
33298
|
},
|
|
32533
33299
|
set current(value) {
|
|
32534
33300
|
selectionState.picked = value;
|
|
@@ -32536,12 +33302,16 @@ async function run(ctx, startup, io) {
|
|
|
32536
33302
|
assembled: void 0
|
|
32537
33303
|
});
|
|
32538
33304
|
};
|
|
33305
|
+
const seedOptions = pendingSelection === void 0 ? {
|
|
33306
|
+
provider: currentDefaults().provider,
|
|
33307
|
+
model: currentDefaults().model
|
|
33308
|
+
} : {
|
|
33309
|
+
provider: pendingSelection.provider,
|
|
33310
|
+
model: pendingSelection.model
|
|
33311
|
+
};
|
|
32539
33312
|
const handle = next.resume ? await agents.resume({
|
|
32540
33313
|
resumeSessionId: SessionId(next.sessionId),
|
|
32541
|
-
agentOptions:
|
|
32542
|
-
provider: defaults.provider,
|
|
32543
|
-
model: defaults.model
|
|
32544
|
-
},
|
|
33314
|
+
agentOptions: seedOptions,
|
|
32545
33315
|
signal: quitAbort.signal,
|
|
32546
33316
|
setup
|
|
32547
33317
|
}) : await agents.create({
|
|
@@ -32550,10 +33320,7 @@ async function run(ctx, startup, io) {
|
|
|
32550
33320
|
cwd: nextCwd,
|
|
32551
33321
|
agentPreset: mode
|
|
32552
33322
|
},
|
|
32553
|
-
agentOptions:
|
|
32554
|
-
provider: defaults.provider,
|
|
32555
|
-
model: defaults.model
|
|
32556
|
-
},
|
|
33323
|
+
agentOptions: seedOptions,
|
|
32557
33324
|
signal: quitAbort.signal,
|
|
32558
33325
|
setup
|
|
32559
33326
|
});
|
|
@@ -32575,6 +33342,7 @@ async function run(ctx, startup, io) {
|
|
|
32575
33342
|
let agent;
|
|
32576
33343
|
let session;
|
|
32577
33344
|
let store = createTranscriptStore();
|
|
33345
|
+
const subagents = createSubagentFeed();
|
|
32578
33346
|
let mentions = createMentions(ctx, void 0, cwd);
|
|
32579
33347
|
/** Explicit model pick made before any session exists (a bare launch). */
|
|
32580
33348
|
let pendingSelection;
|
|
@@ -32625,13 +33393,26 @@ async function run(ctx, startup, io) {
|
|
|
32625
33393
|
mentions = prepared.mentions;
|
|
32626
33394
|
}
|
|
32627
33395
|
const off = ctx.on("session/event", (subject, event) => {
|
|
32628
|
-
if (session
|
|
33396
|
+
if (session === void 0) return;
|
|
33397
|
+
if (subject.id === session.id) {
|
|
33398
|
+
store.apply(event);
|
|
33399
|
+
return;
|
|
33400
|
+
}
|
|
33401
|
+
if (subject.header.parentSession === session.id) subagents.apply(subject.id, event);
|
|
32629
33402
|
});
|
|
32630
33403
|
const commands = watchCommands(ctx);
|
|
32631
33404
|
if (agent !== void 0) commands.setAgent(agent);
|
|
32632
33405
|
const skills = watchSkills(ctx);
|
|
32633
33406
|
if (agent !== void 0) skills.setAgent(agent);
|
|
32634
33407
|
const approval = mountApprovalAnswerer(ctx, (candidate) => agent !== void 0 && candidate.id === agent.id, (request) => approvalCommandPreview(store.getView().entries, request.callId, request.toolName));
|
|
33408
|
+
let subagentOverride;
|
|
33409
|
+
ctx.on("agent/request", (payload, next) => {
|
|
33410
|
+
const subject = payload.agent;
|
|
33411
|
+
const header = subject.session.header;
|
|
33412
|
+
if (header.parentSession === void 0 && header.origin !== "subagent") return next();
|
|
33413
|
+
const picked = subagentOverride ?? resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, subject.session.requestHeader()?.config, currentDefaults());
|
|
33414
|
+
return next().then((resolved) => applyModelSelectionToConfig(resolved, picked));
|
|
33415
|
+
});
|
|
32635
33416
|
const questions = mountQuestionProvider(ctx);
|
|
32636
33417
|
const bridge = { notify: () => {} };
|
|
32637
33418
|
const statuslinePath = join(homedir(), ".dsh", "dsh-code", "statusline.json");
|
|
@@ -32838,6 +33619,7 @@ async function run(ctx, startup, io) {
|
|
|
32838
33619
|
session = next.session;
|
|
32839
33620
|
store = next.store;
|
|
32840
33621
|
mentions = next.mentions;
|
|
33622
|
+
subagents.reset();
|
|
32841
33623
|
pendingMode = void 0;
|
|
32842
33624
|
pendingPermission = void 0;
|
|
32843
33625
|
commands.setAgent(agent);
|
|
@@ -32950,8 +33732,33 @@ async function run(ctx, startup, io) {
|
|
|
32950
33732
|
const selection = buildModelSelection(row, effortId);
|
|
32951
33733
|
if (active === void 0) pendingSelection = selection;
|
|
32952
33734
|
else active.selection.picked = selection;
|
|
33735
|
+
defaultModel.saveSelection(selection).catch((error) => {
|
|
33736
|
+
bridge.notify(`model switch applies to this session but was not saved as the default: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
33737
|
+
});
|
|
33738
|
+
const llm = ctx.get("llm");
|
|
33739
|
+
const resolveCallConfig = llm?.resolveCallConfig;
|
|
33740
|
+
if (llm !== void 0 && typeof resolveCallConfig === "function") Promise.resolve(resolveCallConfig.call(llm, {
|
|
33741
|
+
provider: selection.provider,
|
|
33742
|
+
model: selection.model,
|
|
33743
|
+
...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
|
|
33744
|
+
})).catch((error) => {
|
|
33745
|
+
bridge.notify(`model selection rejected: ${error instanceof Error ? error.message : String(error)} — reopen /model to pick again`, "error");
|
|
33746
|
+
});
|
|
32953
33747
|
return `${row.provider}/${row.model}`;
|
|
32954
33748
|
};
|
|
33749
|
+
/** The /subagent override label, '' when delegated agents follow the current model. */
|
|
33750
|
+
const subagentModelLabel = () => subagentOverride === void 0 ? "" : modelSelectionLabel(subagentOverride);
|
|
33751
|
+
/** Apply one /subagent model pick; returns the override label. */
|
|
33752
|
+
const setSubagentModel = (row, effortId) => {
|
|
33753
|
+
subagentOverride = buildModelSelection(row, effortId);
|
|
33754
|
+
renderCurrent();
|
|
33755
|
+
return modelSelectionLabel(subagentOverride);
|
|
33756
|
+
};
|
|
33757
|
+
/** Drop the /subagent override: delegated agents follow the current model again. */
|
|
33758
|
+
const clearSubagentModel = () => {
|
|
33759
|
+
subagentOverride = void 0;
|
|
33760
|
+
renderCurrent();
|
|
33761
|
+
};
|
|
32955
33762
|
/**
|
|
32956
33763
|
* Export the folded transcript to a markdown file (/export). The default
|
|
32957
33764
|
* target sits beside the session's cwd so the file lands in the user's
|
|
@@ -32994,11 +33801,61 @@ async function run(ctx, startup, io) {
|
|
|
32994
33801
|
};
|
|
32995
33802
|
const loadSessions = async (options, signal) => {
|
|
32996
33803
|
if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
|
|
32997
|
-
const
|
|
33804
|
+
const records = await sessionQuery.listSessions(signal);
|
|
33805
|
+
const updated = /* @__PURE__ */ new Map();
|
|
33806
|
+
for (const record of records) {
|
|
33807
|
+
const location = persistence?.locate(record.header);
|
|
33808
|
+
if (location === void 0) continue;
|
|
33809
|
+
try {
|
|
33810
|
+
updated.set(record.header.id, (await stat(location.path)).mtimeMs);
|
|
33811
|
+
} catch {}
|
|
33812
|
+
}
|
|
33813
|
+
const projected = projectSessionRows(records, options, updated);
|
|
32998
33814
|
const page = projected.slice(0, 32);
|
|
32999
33815
|
if (page.length === 0) return projected;
|
|
33000
33816
|
return mergeSessionTitles(projected, await sessionQuery.readTitleSnapshots(page.map((row) => row.id), signal));
|
|
33001
33817
|
};
|
|
33818
|
+
/**
|
|
33819
|
+
* Delete one session subtree (/delete, codex semantics: subagent threads go
|
|
33820
|
+
* with their root). The kernel persistence seam has NO deletion API by
|
|
33821
|
+
* design — logs accumulate "until removed externally" — so this is the
|
|
33822
|
+
* controlled external removal: guards (live/current refusal, subtree
|
|
33823
|
+
* collection, and the JSONL layout check `encodeSegment(id)/session.jsonl`)
|
|
33824
|
+
* run before any filesystem touch, and only the backend-located artifacts
|
|
33825
|
+
* are removed. Backends without a locatable artifact (SQLite) are refused.
|
|
33826
|
+
* @param id - the root session id to delete.
|
|
33827
|
+
* @returns the outcome line for the panel/notice.
|
|
33828
|
+
*/
|
|
33829
|
+
const deleteSession = async (id) => {
|
|
33830
|
+
if (sessionQuery === void 0) return "session query is unavailable in this profile";
|
|
33831
|
+
if (session !== void 0 && session.id === id) return "cannot delete the session you are using — switch or /new first";
|
|
33832
|
+
const records = await sessionQuery.listSessions();
|
|
33833
|
+
const target = records.find((record) => record.header.id === id);
|
|
33834
|
+
if (target === void 0) return `no persisted session matches "${id}"`;
|
|
33835
|
+
if (target.live) return "cannot delete a live session — it is open in this or another process";
|
|
33836
|
+
const doomed = collectDeletionSubtree(records, id);
|
|
33837
|
+
const byId = new Map(records.map((record) => [record.header.id, record]));
|
|
33838
|
+
let removed = 0;
|
|
33839
|
+
for (const candidate of doomed) {
|
|
33840
|
+
const record = byId.get(candidate);
|
|
33841
|
+
if (record === void 0 || record.live) continue;
|
|
33842
|
+
const location = persistence?.locate(record.header);
|
|
33843
|
+
if (location === void 0) return `session backend exposes no deletable artifact for ${candidate.slice(-12)} (deletion is unsupported on this backend)`;
|
|
33844
|
+
const dir = sessionArtifactDirectory(location.path, candidate);
|
|
33845
|
+
if (dir === void 0) return `refusing to delete: unexpected artifact layout at ${location.path}`;
|
|
33846
|
+
try {
|
|
33847
|
+
for (const name of SESSION_ARTIFACT_NAMES) await rm(join(dir, name), { force: true });
|
|
33848
|
+
await rm(dir, {
|
|
33849
|
+
force: true,
|
|
33850
|
+
recursive: false
|
|
33851
|
+
}).catch(() => {});
|
|
33852
|
+
removed += 1;
|
|
33853
|
+
} catch (error) {
|
|
33854
|
+
return `delete failed for ${candidate.slice(-12)}: ${error instanceof Error ? error.message : String(error)}`;
|
|
33855
|
+
}
|
|
33856
|
+
}
|
|
33857
|
+
return `deleted ${removed} session${removed === 1 ? "" : "s"}`;
|
|
33858
|
+
};
|
|
33002
33859
|
const loadSessionTranscript = async (id, signal) => {
|
|
33003
33860
|
if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
|
|
33004
33861
|
const snapshot = await sessionQuery.readSession(id, signal);
|
|
@@ -33041,6 +33898,7 @@ async function run(ctx, startup, io) {
|
|
|
33041
33898
|
session = next.session;
|
|
33042
33899
|
store = next.store;
|
|
33043
33900
|
mentions = next.mentions;
|
|
33901
|
+
subagents.reset();
|
|
33044
33902
|
pendingMode = void 0;
|
|
33045
33903
|
pendingPermission = void 0;
|
|
33046
33904
|
commands.setAgent(agent);
|
|
@@ -33156,6 +34014,7 @@ async function run(ctx, startup, io) {
|
|
|
33156
34014
|
const appElement = () => {
|
|
33157
34015
|
const sessionCwd = session?.header.cwd ?? cwd;
|
|
33158
34016
|
const currentView = store.getView();
|
|
34017
|
+
const defaults = currentDefaults();
|
|
33159
34018
|
const model = currentView.model !== "" ? currentView.model : pendingSelection !== void 0 ? `${pendingSelection.provider}/${pendingSelection.model}` : `${defaults.provider}/${defaults.model}`;
|
|
33160
34019
|
const effort = resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, session?.requestHeader()?.config, defaults).reasoningEffort;
|
|
33161
34020
|
const permission = permissionPresets === void 0 ? currentView.permission : effectivePermission(permissionPresets, session, pendingPermission);
|
|
@@ -33164,6 +34023,7 @@ async function run(ctx, startup, io) {
|
|
|
33164
34023
|
store,
|
|
33165
34024
|
approval,
|
|
33166
34025
|
questions,
|
|
34026
|
+
subagents,
|
|
33167
34027
|
commands,
|
|
33168
34028
|
skills,
|
|
33169
34029
|
model,
|
|
@@ -33189,6 +34049,10 @@ async function run(ctx, startup, io) {
|
|
|
33189
34049
|
cyclePermission: cyclePermission$1,
|
|
33190
34050
|
setPermission: setPermissionAction,
|
|
33191
34051
|
selectModel,
|
|
34052
|
+
subagentModel: subagentModelLabel(),
|
|
34053
|
+
setSubagentModel,
|
|
34054
|
+
clearSubagentModel,
|
|
34055
|
+
deleteSession,
|
|
33192
34056
|
exportTranscript,
|
|
33193
34057
|
renameTitle,
|
|
33194
34058
|
loadPresets: () => presets.list(),
|
|
@@ -33197,6 +34061,17 @@ async function run(ctx, startup, io) {
|
|
|
33197
34061
|
createSession,
|
|
33198
34062
|
loadSessions,
|
|
33199
34063
|
loadSessionTranscript,
|
|
34064
|
+
loadSubagents: () => {
|
|
34065
|
+
const current = session;
|
|
34066
|
+
if (current === void 0 || sessionQuery === void 0) return Promise.resolve([]);
|
|
34067
|
+
return loadSessions({
|
|
34068
|
+
sessions: "all",
|
|
34069
|
+
cwd: "all",
|
|
34070
|
+
sort: "newest",
|
|
34071
|
+
currentCwd: current.header.cwd ?? cwd,
|
|
34072
|
+
query: ""
|
|
34073
|
+
}).then((rows) => rows.filter((row) => row.parent === current.id));
|
|
34074
|
+
},
|
|
33200
34075
|
switchSession,
|
|
33201
34076
|
cancelSessionSwitch,
|
|
33202
34077
|
loadPlugins: () => listPluginRows(ctx),
|