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/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(`search: ${props.query === "" ? "type to filter" : props.query}`), viewport.contentColumns)), ...visible.map((row, index) => {
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: "all",
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 (key.escape || input === "q") return close();
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") return setDensity((value) => value === "comfortable" ? "dense" : "comfortable");
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
- footer: "type search · tab/←→ filters · ↑↓/pg navigate · e details · t transcript · enter resume"
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, with the effective effort and the model default marked.
28011
- * A model WITHOUT an adapter-declared default leads with a "Default"
28012
- * (provider-default) row the web effort pane's first entry so the user
28013
- * can clear a picked level back to provider behavior instead of being forced
28014
- * to choose an advertised one. Enter applies one level; Esc returns to the
28015
- * model list without applying.
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 [cursor, setCursor] = (0, import_react.useState)(0);
28019
- const efforts = row.reasoning?.efforts ?? [];
28020
- const rows = row.reasoning !== void 0 && row.reasoning.defaultEffort === void 0 ? [{
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
- }, ...efforts] : efforts;
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 (rows.length === 0) return;
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: rows.map((effort) => ({
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 y/n approval bar rendered while an approval ask is pending. */
28768
- function ApprovalBar({ snapshot, locked }) {
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 [scroll, setScroll] = (0, import_react.useState)(0);
29239
+ const [cursor, setCursor] = (0, import_react.useState)(0);
28772
29240
  const pending = snapshot.pending;
28773
- const active = !locked && snapshot.pending !== void 0 && !snapshot.answered;
28774
- const content = (0, import_react.useMemo)(() => pending === void 0 ? [] : [...styledLines([lineSegment(pending.headline, "warn")], viewport.contentColumns), ...pending.command === "" ? [] : textLines(` ${pending.command}`, viewport.contentColumns, "dim")], [pending, viewport.contentColumns]);
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
- setScroll(0);
29244
+ setCursor(0);
28778
29245
  }, [pending]);
28779
- (0, import_react.useEffect)(() => {
28780
- if (visibleScroll !== scroll) setScroll(visibleScroll);
28781
- }, [visibleScroll, scroll]);
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
- setScroll((current) => moveScroll(current, -1, content.length, viewport.bodyRows));
29259
+ setCursor((current) => (current + APPROVAL_OPTIONS.length - 1) % APPROVAL_OPTIONS.length);
28786
29260
  return;
28787
29261
  }
28788
29262
  if (key.downArrow) {
28789
- setScroll((current) => moveScroll(current, 1, content.length, viewport.bodyRows));
29263
+ setCursor((current) => (current + 1) % APPROVAL_OPTIONS.length);
28790
29264
  return;
28791
29265
  }
28792
- if (key.pageUp) {
28793
- setScroll((current) => moveScroll(current, -Math.max(1, viewport.bodyRows - 1), content.length, viewport.bodyRows));
29266
+ if (key.return) {
29267
+ decide(APPROVAL_OPTIONS[cursor]);
28794
29268
  return;
28795
29269
  }
28796
- if (key.pageDown) {
28797
- setScroll((current) => moveScroll(current, Math.max(1, viewport.bodyRows - 1), content.length, viewport.bodyRows));
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
- snapshot.pending.answer("allowed-once");
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 === "n" || input === "N") snapshot.pending.answer("rejected");
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 (snapshot.pending === void 0) return void 0;
29291
+ if (pending === void 0) return void 0;
28808
29292
  if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
28809
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("approval · y allow · n reject", viewport.contentColumns));
28810
- const { answered } = snapshot;
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(`⏸ waiting for approval · lines ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(StyledRows, { lines: content.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
28822
- dimColor: true,
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
- }, dim(truncateColumns(answered ? "submitted…" : "↑↓/pgup/pgdn scroll · y allow once · n reject", viewport.contentColumns))));
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 (cursor !== 0) setCursor(0);
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
- if (cursor >= rows.length) setCursor(rows.length - 1);
29081
- }, [rows.length, cursor]);
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
- if (mentionActive && mentionToken !== void 0) {
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
- notify("current model does not expose reasoning efforts", "warning");
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: () => setResumeOpen(true),
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 set = (next) => {
31158
- snapshot = next;
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
- set({
31176
- pending: void 0,
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 pending = {
31183
- headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
31184
- toolName: request.toolName,
31185
- command: preview(request),
31186
- answer: (outcome) => {
31187
- if (resolved) return;
31188
- resolved = true;
31189
- detachAbort();
31190
- set({
31191
- pending,
31192
- answered: true
31193
- });
31194
- settle(outcome);
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
- set({
31198
- pending,
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") set({
31205
- pending: void 0,
31206
- answered: false
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 defaults = defaultModel.currentSelection();
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, defaults);
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 !== void 0 && subject.id === session.id) store.apply(event);
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 projected = projectSessionRows(await sessionQuery.listSessions(signal), options);
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),