dsh-code 0.8.0 → 0.9.1

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
@@ -26518,6 +26544,15 @@ const DEEPSEEK_WAVE_BANDS = {
26518
26544
  .35,
26519
26545
  .55,
26520
26546
  1
26547
+ ]],
26548
+ unknown: [[
26549
+ .1,
26550
+ .7,
26551
+ 1
26552
+ ], [
26553
+ .35,
26554
+ .55,
26555
+ 1
26521
26556
  ]]
26522
26557
  },
26523
26558
  aurora: {
@@ -26546,6 +26581,23 @@ const DEEPSEEK_WAVE_BANDS = {
26546
26581
  .35,
26547
26582
  2
26548
26583
  ]
26584
+ ],
26585
+ unknown: [
26586
+ [
26587
+ .35,
26588
+ .15,
26589
+ 0
26590
+ ],
26591
+ [
26592
+ -.5,
26593
+ .6,
26594
+ 1
26595
+ ],
26596
+ [
26597
+ .75,
26598
+ .35,
26599
+ 2
26600
+ ]
26549
26601
  ]
26550
26602
  },
26551
26603
  pulse: {
@@ -26562,15 +26614,25 @@ const DEEPSEEK_WAVE_BANDS = {
26562
26614
  .45,
26563
26615
  .55,
26564
26616
  1.1
26617
+ ]],
26618
+ unknown: [[
26619
+ .1,
26620
+ .55,
26621
+ .8
26622
+ ], [
26623
+ .45,
26624
+ .55,
26625
+ 1.1
26565
26626
  ]]
26566
26627
  }
26567
26628
  };
26568
26629
  /** Original Codex duration used as the animation's sampling timeline. */
26569
26630
  function deepseekWaveBaseDuration(tier, style) {
26631
+ const pro = tier === "deepseek" || tier === "unknown";
26570
26632
  switch (style) {
26571
- case "aurora": return tier === "flash" ? 1300 : 1600;
26572
- case "pulse": return tier === "flash" ? 900 : 1250;
26573
- case "wave": return tier === "flash" ? 1e3 : 1300;
26633
+ case "aurora": return pro ? 1600 : 1300;
26634
+ case "pulse": return pro ? 1250 : 900;
26635
+ case "wave": return pro ? 1300 : 1e3;
26574
26636
  }
26575
26637
  }
26576
26638
  /**
@@ -26822,6 +26884,206 @@ function isOfficialDeepSeekLabel(label) {
26822
26884
  const model = slash < 0 ? "" : label.slice(slash + 1);
26823
26885
  return provider.toLowerCase().includes("deepseek") || model.toLowerCase().includes("deepseek");
26824
26886
  }
26887
+ /**
26888
+ * Known reasoning-effort ranks in ascending order. Effort ids are opaque
26889
+ * adapter-owned strings, so the rank table covers the conventional names
26890
+ * (off → low → medium → high → xhigh → max/ultra); an unrecognized id
26891
+ * ranks as unknown (0), which never triggers the high-effort wave.
26892
+ */
26893
+ const EFFORT_RANK = {
26894
+ off: 0,
26895
+ none: 0,
26896
+ low: 1,
26897
+ medium: 2,
26898
+ med: 2,
26899
+ high: 3,
26900
+ xhigh: 4,
26901
+ "x-high": 4,
26902
+ "very-high": 4,
26903
+ max: 5,
26904
+ maximum: 5,
26905
+ ultra: 5
26906
+ };
26907
+ /**
26908
+ * True when an effective reasoning effort is STRICTLY above `high` — the
26909
+ * trigger gate for the "Into the Unknown" wave on non-DeepSeek routes.
26910
+ * Absent efforts and unrecognized ids never qualify.
26911
+ * @param effort - the effective reasoning-effort id ('' or undefined when none).
26912
+ * @returns whether the effort ranks above high.
26913
+ */
26914
+ function effortAboveHigh(effort) {
26915
+ if (effort === void 0 || effort === "") return false;
26916
+ const rank = EFFORT_RANK[effort.trim().toLowerCase()];
26917
+ return rank !== void 0 && rank > 3;
26918
+ }
26919
+ //#endregion
26920
+ //#region src/session-directory.ts
26921
+ /** Lightweight session-directory projection for the /resume picker. */
26922
+ /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
26923
+ const CASE_INSENSITIVE_FS = process.platform === "win32" || process.platform === "darwin";
26924
+ /** True when the header describes a subagent conversation (durable lineage). */
26925
+ function isSubagentSession(header) {
26926
+ return header.origin === "subagent" || header.parentSession !== void 0;
26927
+ }
26928
+ function comparablePath(value) {
26929
+ const resolved = resolve(value);
26930
+ const fold = (path) => CASE_INSENSITIVE_FS ? path.toLowerCase() : path;
26931
+ try {
26932
+ return fold(realpathSync(resolved));
26933
+ } catch {
26934
+ return fold(resolved);
26935
+ }
26936
+ }
26937
+ /** Platform-consistent path equality for session cwd comparisons. */
26938
+ function samePath(left, right) {
26939
+ if (left === void 0) return false;
26940
+ return comparablePath(left) === comparablePath(right);
26941
+ }
26942
+ /**
26943
+ * Unique header match by exact id or unique id prefix (root and subagent
26944
+ * headers alike); the caller applies any lineage gate.
26945
+ * @param headers - the persisted headers.
26946
+ * @param wanted - the id or id prefix.
26947
+ * @returns the uniquely matched header.
26948
+ * @throws when nothing matches or the prefix is ambiguous.
26949
+ */
26950
+ function matchSessionId(headers, wanted) {
26951
+ const exact = headers.filter((header) => header.id === wanted);
26952
+ const matches = exact.length > 0 ? exact : headers.filter((header) => header.id.startsWith(wanted));
26953
+ if (matches.length === 0) throw new Error(`no persisted session matches "${wanted}"`);
26954
+ if (matches.length > 1) throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`);
26955
+ return matches[0];
26956
+ }
26957
+ /** The newest persisted ROOT session pinned to this cwd, or undefined. */
26958
+ function newestRootForCwd(headers, cwd) {
26959
+ return headers.filter((header) => !isSubagentSession(header) && samePath(header.cwd, cwd)).sort((left, right) => right.createdAt - left.createdAt)[0];
26960
+ }
26961
+ /**
26962
+ * Filter/sort header-only records. No session log is loaded here. Sorting is
26963
+ * by LAST ACTIVITY (`updated` — artifact mtime when the caller resolved one,
26964
+ * else createdAt), matching the codex resume picker's default UpdatedAt
26965
+ * ordering: a session you kept talking in outranks one created later but idle.
26966
+ * @param records - the header-only records.
26967
+ * @param options - filter/sort options.
26968
+ * @param updated - per-session last-activity timestamps, when resolved.
26969
+ */
26970
+ function projectSessionRows(records, options, updated) {
26971
+ const needle = options.query.trim().toLowerCase();
26972
+ return records.filter((record) => options.sessions === "all" || !isSubagentSession(record.header)).filter((record) => options.cwd === "all" || samePath(record.header.cwd, options.currentCwd)).map((record) => {
26973
+ const cwd = record.header.cwd ?? "";
26974
+ const subagent = isSubagentSession(record.header);
26975
+ const activity = updated?.get(record.header.id);
26976
+ return {
26977
+ id: record.header.id,
26978
+ createdAt: record.header.createdAt,
26979
+ updatedAt: activity === void 0 || !Number.isFinite(activity) || activity < record.header.createdAt ? record.header.createdAt : activity,
26980
+ cwd,
26981
+ workspace: cwd === "" ? "(no workspace)" : basename(cwd),
26982
+ parent: record.header.parentSession,
26983
+ subagent,
26984
+ resumable: !subagent,
26985
+ live: record.live,
26986
+ persisted: record.persisted,
26987
+ preset: record.header.agentPreset ?? "standard"
26988
+ };
26989
+ }).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);
26990
+ }
26991
+ /** Merge page-local title observations without disturbing directory order. */
26992
+ function mergeSessionTitles(rows, observations) {
26993
+ const titles = /* @__PURE__ */ new Map();
26994
+ for (const observation of observations) {
26995
+ if (observation.status !== "fulfilled") continue;
26996
+ const title = observation.value?.title?.title ?? observation.value?.title?.text;
26997
+ if (title !== void 0 && title.trim() !== "") titles.set(observation.sessionId, title);
26998
+ }
26999
+ return rows.map((row) => titles.has(row.id) ? {
27000
+ ...row,
27001
+ title: titles.get(row.id)
27002
+ } : row);
27003
+ }
27004
+ /**
27005
+ * Encode a session id the way the JSONL backend does for its on-disk layout
27006
+ * (`encodeSegment`: safe units literal, everything else `~XXXX`). Used ONLY to
27007
+ * validate that a `locate()` path really is this session's directory before
27008
+ * any deletion touches the filesystem — a local copy of the pure upstream
27009
+ * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
27010
+ */
27011
+ function encodeSessionSegment(raw) {
27012
+ if (raw.length === 0) throw new Error("cannot encode an empty path segment");
27013
+ if (raw === ".") return "~002E";
27014
+ if (raw === "..") return "~002E~002E";
27015
+ let out = "";
27016
+ for (let i = 0; i < raw.length; i += 1) {
27017
+ const code = raw.charCodeAt(i);
27018
+ const ch = String.fromCharCode(code);
27019
+ if (ch !== "~" && /^[A-Za-z0-9._-]$/u.test(ch)) out += ch;
27020
+ else out += `~${code.toString(16).toUpperCase().padStart(4, "0")}`;
27021
+ }
27022
+ return out;
27023
+ }
27024
+ /** The session-log artifact names the JSONL backend may create. */
27025
+ const SESSION_ARTIFACT_NAMES = ["session.jsonl", "session.jsonl.zstd"];
27026
+ /**
27027
+ * Guard one `locate()` artifact path before deletion (codex's scoped-path
27028
+ * check, adapted to the JSONL layout): the file must be a `session.jsonl`
27029
+ * artifact sitting in the directory named exactly `encodeSegment(id)`.
27030
+ * @param artifact - the path the persistence backend located.
27031
+ * @param id - the session id the artifact claims to belong to.
27032
+ * @returns the owning session directory, or undefined when the layout is unexpected.
27033
+ */
27034
+ function sessionArtifactDirectory(artifact, id) {
27035
+ if (basename(artifact) !== "session.jsonl" && basename(artifact) !== "session.jsonl.zstd") return void 0;
27036
+ const dir = dirname(artifact);
27037
+ if (basename(dir) !== encodeSessionSegment(id)) return void 0;
27038
+ return dir;
27039
+ }
27040
+ /**
27041
+ * Collect one session's deletion subtree: the id plus every record whose
27042
+ * parent chain leads to it (codex deletes subagent threads with their root).
27043
+ * @param records - the full directory listing.
27044
+ * @param id - the root session id to delete.
27045
+ * @returns the ids to delete, root first.
27046
+ */
27047
+ function collectDeletionSubtree(records, id) {
27048
+ const parentOf = /* @__PURE__ */ new Map();
27049
+ for (const record of records) parentOf.set(record.header.id, record.header.parentSession);
27050
+ const doomed = /* @__PURE__ */ new Set([id]);
27051
+ for (let pass = 0; pass < 2; pass += 1) for (const candidate of parentOf.keys()) {
27052
+ if (doomed.has(candidate)) continue;
27053
+ let ancestor = parentOf.get(candidate);
27054
+ let depth = 0;
27055
+ while (ancestor !== void 0 && depth < 64) {
27056
+ if (doomed.has(ancestor)) {
27057
+ doomed.add(candidate);
27058
+ break;
27059
+ }
27060
+ ancestor = parentOf.get(ancestor);
27061
+ depth += 1;
27062
+ }
27063
+ }
27064
+ return [...doomed];
27065
+ }
27066
+ /**
27067
+ * Codex-style relative time for session rows ("now", "5m ago", "3h ago",
27068
+ * "2d ago"; older than a week falls back to the local date).
27069
+ * @param timestamp - epoch milliseconds of the last activity.
27070
+ * @param now - the pinned reference clock (one value per list render).
27071
+ */
27072
+ function formatRelativeTime(timestamp, now) {
27073
+ const seconds = Math.round((now - timestamp) / 1e3);
27074
+ if (seconds < 0) return "now";
27075
+ if (seconds < 60) return "now";
27076
+ const minutes = Math.round(seconds / 60);
27077
+ if (minutes < 60) return `${minutes}m ago`;
27078
+ const hours = Math.round(minutes / 60);
27079
+ if (hours < 24) return `${hours}h ago`;
27080
+ const days = Math.round(hours / 24);
27081
+ if (days < 7) return `${days}d ago`;
27082
+ const date = new Date(timestamp);
27083
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
27084
+ const day = `${date.getDate()}`.padStart(2, "0");
27085
+ return `${date.getFullYear()}-${month}-${day}`;
27086
+ }
26825
27087
  //#endregion
26826
27088
  //#region src/render/status.ts
26827
27089
  /**
@@ -27506,6 +27768,7 @@ function transcriptEntryLines(entry, columns) {
27506
27768
  lineSegment(entry.name, "brand"),
27507
27769
  lineSegment(entry.preview === "" ? "" : ` ${entry.preview}`, "dim")
27508
27770
  ], width),
27771
+ ...entry.prompt === "" ? [] : textLines(` └ ${entry.prompt}`, width, "dim"),
27509
27772
  ...entry.summary === "" ? [] : textLines(` ⎿ ${entry.summary}`, width, entry.state === "error" ? "error" : "dim"),
27510
27773
  ...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
27511
27774
  ];
@@ -27530,6 +27793,17 @@ function transcriptEntryLines(entry, columns) {
27530
27793
  //#endregion
27531
27794
  //#region src/kernel-panels.ts
27532
27795
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
27796
+ /** True for the Ctrl+F search-focus toggle. */
27797
+ function isSearchToggle(input, key) {
27798
+ return key.ctrl === true && input === "f";
27799
+ }
27800
+ /** The search-state line: gated panels show only an ACTIVE filter (the ctrl+f
27801
+ * toggle lives in the footer), direct-typing panels keep the plain prompt. */
27802
+ function searchLine(searching, query) {
27803
+ if (searching === true) return `search: ${query === "" ? "type to filter · esc stops" : query}`;
27804
+ if (searching === false) return query === "" ? "" : `search: ${query}`;
27805
+ return `search: ${query === "" ? "type to filter" : query}`;
27806
+ }
27533
27807
  function ListFrame(props) {
27534
27808
  const stdout = useStdout().stdout;
27535
27809
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
@@ -27560,7 +27834,7 @@ function ListFrame(props) {
27560
27834
  }, truncateColumns(singleLineText(props.title), viewport.contentColumns)), (0, import_react.createElement)(Text, {
27561
27835
  dimColor: true,
27562
27836
  wrap: "truncate-end"
27563
- }, truncateColumns(singleLineText(`search: ${props.query === "" ? "type to filter" : props.query}`), viewport.contentColumns)), ...visible.map((row, index) => {
27837
+ }, truncateColumns(singleLineText(searchLine(props.searching, props.query)), viewport.contentColumns)), ...visible.map((row, index) => {
27564
27838
  const absolute = offset + index;
27565
27839
  const selected = !props.loading && props.error === void 0 && props.rows.length > 0 && absolute === props.cursor;
27566
27840
  return (0, import_react.createElement)(Text, {
@@ -27701,10 +27975,10 @@ function PluginPanel({ load, close, initialQuery = "" }) {
27701
27975
  footer: "↑↓ inspect · enter details · r refresh · esc close"
27702
27976
  });
27703
27977
  }
27704
- function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
27978
+ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }) {
27705
27979
  const [options, setOptions] = (0, import_react.useState)({
27706
27980
  sessions: "roots",
27707
- cwd: "all",
27981
+ cwd: "current",
27708
27982
  sort: "newest",
27709
27983
  currentCwd,
27710
27984
  query: ""
@@ -27717,6 +27991,10 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
27717
27991
  const [error, setError] = (0, import_react.useState)();
27718
27992
  const [expanded, setExpanded] = (0, import_react.useState)();
27719
27993
  const [transcript, setTranscript] = (0, import_react.useState)();
27994
+ /** Ctrl+F-gated search: typing filters only while searching (codex). */
27995
+ const [searching, setSearching] = (0, import_react.useState)(false);
27996
+ /** Reference clock pinned per row render, so relative times never drift mid-list. */
27997
+ const now = (0, import_react.useMemo)(() => Date.now(), [rows, options]);
27720
27998
  const transcriptLoad = (0, import_react.useRef)();
27721
27999
  (0, import_react.useEffect)(() => () => transcriptLoad.current?.abort(), []);
27722
28000
  (0, import_react.useEffect)(() => {
@@ -27735,7 +28013,7 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
27735
28013
  }
27736
28014
  });
27737
28015
  return () => controller.abort();
27738
- }, [options]);
28016
+ }, [options, reloadToken]);
27739
28017
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
27740
28018
  const cycle = () => {
27741
28019
  if (focus === 3) {
@@ -27758,7 +28036,34 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
27758
28036
  });
27759
28037
  };
27760
28038
  useInput((input, key) => {
27761
- if (key.escape || input === "q") return close();
28039
+ if (deleteConfirmId !== void 0) return;
28040
+ if (key.escape) {
28041
+ if (searching) {
28042
+ setSearching(false);
28043
+ return;
28044
+ }
28045
+ return close();
28046
+ }
28047
+ if (isSearchToggle(input, key)) {
28048
+ setSearching((current) => !current);
28049
+ return;
28050
+ }
28051
+ if (searching) {
28052
+ if (key.return) {
28053
+ setSearching(false);
28054
+ return;
28055
+ }
28056
+ const next = editQuery(options.query, input, key);
28057
+ if (next !== void 0) {
28058
+ setOptions((value) => ({
28059
+ ...value,
28060
+ query: next
28061
+ }));
28062
+ setCursor(0);
28063
+ }
28064
+ return;
28065
+ }
28066
+ if (input === "q") return close();
27762
28067
  if (key.tab) return setFocus((value) => (value + (key.shift ? 3 : 1)) % 4);
27763
28068
  if (key.leftArrow) return cycle();
27764
28069
  if (key.rightArrow) return cycle();
@@ -27768,7 +28073,7 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
27768
28073
  if (key.pageDown) return setCursor((value) => Math.min(rows.length - 1, value + 8));
27769
28074
  if (input === "g") return setCursor(0);
27770
28075
  if (input === "G") return setCursor(Math.max(0, rows.length - 1));
27771
- if (input === "d") return setDensity((value) => value === "comfortable" ? "dense" : "comfortable");
28076
+ if (input === "d" && rows[cursor] !== void 0 && requestDelete !== void 0) return requestDelete(rows[cursor]);
27772
28077
  if (input === "e" && rows[cursor] !== void 0) return setExpanded((value) => value === rows[cursor].id ? void 0 : rows[cursor].id);
27773
28078
  if (input === "t" && rows[cursor] !== void 0) {
27774
28079
  const row = rows[cursor];
@@ -27790,14 +28095,6 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
27790
28095
  return;
27791
28096
  }
27792
28097
  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
28098
  }, { isActive: transcript === void 0 });
27802
28099
  if (transcript !== void 0) return (0, import_react.createElement)(DocumentPanel, {
27803
28100
  title: `transcript · ${transcript.id}`,
@@ -27808,19 +28105,21 @@ function ResumePanel({ currentCwd, load, readTranscript, select, close }) {
27808
28105
  setTranscript(void 0);
27809
28106
  }
27810
28107
  });
28108
+ const pendingRow = deleteConfirmId === void 0 ? void 0 : rows.find((row) => row.id === deleteConfirmId);
27811
28109
  const toolbar = `[${focus === 0 ? ">" : ""}${options.sessions}] [${focus === 1 ? ">" : ""}${options.cwd} cwd] [${focus === 2 ? ">" : ""}${options.sort}] [${focus === 3 ? ">" : ""}${density}]`;
27812
28110
  return (0, import_react.createElement)(ListFrame, {
27813
- title: `/resume · ${toolbar}`,
28111
+ 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
28112
  rows: rows.map((row) => ({
27815
28113
  key: row.id,
27816
28114
  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}`}` : ""}`
28115
+ 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
28116
  })),
27819
28117
  cursor,
27820
28118
  loading,
27821
28119
  error,
27822
28120
  query: options.query,
27823
- footer: "type search · tab/←→ filters · ↑↓/pg navigate · e details · t transcript · enter resume"
28121
+ searching,
28122
+ footer: "tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · d delete · enter resume"
27824
28123
  });
27825
28124
  }
27826
28125
  function DocumentPanel({ title, text, error, close }) {
@@ -28007,21 +28306,31 @@ function StatuslinePanel({ enabled, change, close }) {
28007
28306
  /**
28008
28307
  * The `/model` reasoning-effort stage (the Codex model → reasoning popup
28009
28308
  * 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.
28309
+ * effort levels in the adapter's own display order, ids verbatim (the
28310
+ * kernel treats them as opaque and rejects anything else) — with the
28311
+ * effective effort and the model default marked. A model WITHOUT an
28312
+ * adapter-declared default leads with a "Default" (provider-default) row
28313
+ * the web effort pane's first entry so the user can clear a picked level
28314
+ * back to provider behavior. A model advertising no levels opens the same
28315
+ * stage with an explicit empty state (the web pane's "no levels" copy)
28316
+ * instead of a bare failure notice. Enter applies one level; Esc returns to
28317
+ * the model list without applying.
28016
28318
  */
28017
28319
  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 ? [{
28320
+ const advertised = row.reasoning?.efforts ?? [];
28321
+ const empty = row.reasoning === void 0 || advertised.length === 0;
28322
+ const hasDefaultRow = row.reasoning !== void 0 && row.reasoning.defaultEffort === void 0;
28323
+ const rows = empty ? [{
28324
+ id: "",
28325
+ name: ""
28326
+ }] : hasDefaultRow ? [{
28021
28327
  id: "",
28022
28328
  name: "Default"
28023
- }, ...efforts] : efforts;
28329
+ }, ...advertised] : advertised;
28024
28330
  const effective = current === void 0 || current === "" ? "" : current;
28331
+ const wanted = effective === "" ? row.reasoning?.defaultEffort ?? "" : effective;
28332
+ const initialCursor = Math.max(0, rows.findIndex((effort) => effort.id === wanted));
28333
+ const [cursor, setCursor] = (0, import_react.useState)(initialCursor);
28025
28334
  (0, import_react.useEffect)(() => {
28026
28335
  if (rows.length === 0) {
28027
28336
  if (cursor !== 0) setCursor(0);
@@ -28031,7 +28340,15 @@ function EffortPanel({ row, current, select, back }) {
28031
28340
  }, [rows.length, cursor]);
28032
28341
  useInput((input, key) => {
28033
28342
  if (key.escape || input === "q") return back();
28034
- if (rows.length === 0) return;
28343
+ if (empty) return;
28344
+ if (input === "g") {
28345
+ setCursor(0);
28346
+ return;
28347
+ }
28348
+ if (input === "G") {
28349
+ setCursor(rows.length - 1);
28350
+ return;
28351
+ }
28035
28352
  if (key.upArrow) {
28036
28353
  setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
28037
28354
  return;
@@ -28044,14 +28361,190 @@ function EffortPanel({ row, current, select, back }) {
28044
28361
  });
28045
28362
  return (0, import_react.createElement)(ListFrame, {
28046
28363
  title: `/model — effort for ${row.providerName} · ${row.modelName}`,
28047
- rows: rows.map((effort) => ({
28364
+ rows: empty ? [{
28365
+ key: "empty",
28366
+ disabled: true,
28367
+ text: "this model advertises no reasoning effort levels — the provider default applies"
28368
+ }] : rows.map((effort) => ({
28048
28369
  key: effort.id,
28049
28370
  text: `${effort.id === effective ? "●" : "○"} ${effort.name}${effort.id === row.reasoning?.defaultEffort ? " · default" : ""}${effort.description === void 0 ? "" : ` · ${effort.description}`}`
28050
28371
  })),
28051
28372
  cursor,
28052
28373
  loading: false,
28053
28374
  query: "",
28054
- footer: "↑↓ choose · enter apply · esc/q back"
28375
+ footer: empty ? "esc back" : "↑↓ choose · enter apply · esc/q back"
28376
+ });
28377
+ }
28378
+ /**
28379
+ * The /agents panel (the Codex agent-picker contract, read-only): this
28380
+ * conversation's subagent conversations — live rows from the activity feed
28381
+ * first, persisted children the feed has not seen this process after — with
28382
+ * Enter/t opening the child's full transcript in the shared read-only
28383
+ * document view (the same projection the exporter uses).
28384
+ */
28385
+ function AgentsPanel({ live, load, readTranscript, close }) {
28386
+ const [dirRows, setDirRows] = (0, import_react.useState)(void 0);
28387
+ const [error, setError] = (0, import_react.useState)();
28388
+ const [loading, setLoading] = (0, import_react.useState)(true);
28389
+ const [cursor, setCursor] = (0, import_react.useState)(0);
28390
+ const [transcript, setTranscript] = (0, import_react.useState)();
28391
+ const transcriptLoad = (0, import_react.useRef)();
28392
+ (0, import_react.useEffect)(() => () => transcriptLoad.current?.abort(), []);
28393
+ const refresh = () => {
28394
+ setLoading(true);
28395
+ setError(void 0);
28396
+ Promise.resolve().then(load).then((value) => {
28397
+ setDirRows(value);
28398
+ setLoading(false);
28399
+ }, (reason) => {
28400
+ setError(reason instanceof Error ? reason.message : String(reason));
28401
+ setLoading(false);
28402
+ });
28403
+ };
28404
+ (0, import_react.useEffect)(refresh, []);
28405
+ const rows = (0, import_react.useMemo)(() => {
28406
+ const seen = new Set(live.map((row) => row.id));
28407
+ const feedRows = live.map((row) => ({
28408
+ id: row.id,
28409
+ label: row.label,
28410
+ activity: row.activity,
28411
+ running: row.state === "running",
28412
+ done: row.state === "done",
28413
+ live: true
28414
+ }));
28415
+ const persisted = (dirRows ?? []).filter((row) => !seen.has(row.id)).map((row) => ({
28416
+ id: row.id,
28417
+ label: row.title ?? row.id.slice(-12),
28418
+ activity: row.workspace,
28419
+ running: false,
28420
+ done: !row.live,
28421
+ live: row.live
28422
+ }));
28423
+ return [...feedRows, ...persisted];
28424
+ }, [live, dirRows]);
28425
+ (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
28426
+ const openTranscript = () => {
28427
+ const row = rows[cursor];
28428
+ if (row === void 0) return;
28429
+ transcriptLoad.current?.abort();
28430
+ setTranscript({ id: row.id });
28431
+ const controller = new AbortController();
28432
+ transcriptLoad.current = controller;
28433
+ Promise.resolve().then(() => readTranscript(row.id, controller.signal)).then((text) => {
28434
+ if (!controller.signal.aborted) setTranscript({
28435
+ id: row.id,
28436
+ text
28437
+ });
28438
+ }, (reason) => {
28439
+ if (!controller.signal.aborted) setTranscript({
28440
+ id: row.id,
28441
+ error: reason instanceof Error ? reason.message : String(reason)
28442
+ });
28443
+ });
28444
+ };
28445
+ useInput((input, key) => {
28446
+ if (key.escape || input === "q") return close();
28447
+ if (input === "r") return refresh();
28448
+ if (key.upArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length);
28449
+ if (key.downArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + 1) % rows.length);
28450
+ if ((key.return || input === "t") && rows[cursor] !== void 0) return openTranscript();
28451
+ }, { isActive: transcript === void 0 });
28452
+ if (transcript !== void 0) return (0, import_react.createElement)(DocumentPanel, {
28453
+ title: `subagent · ${transcript.id.slice(-12)}`,
28454
+ text: transcript.text,
28455
+ error: transcript.error,
28456
+ close: () => {
28457
+ transcriptLoad.current?.abort();
28458
+ setTranscript(void 0);
28459
+ }
28460
+ });
28461
+ return (0, import_react.createElement)(ListFrame, {
28462
+ title: `/agents · ${live.length} live · ${rows.length} total`,
28463
+ rows: rows.map((row) => ({
28464
+ key: row.id,
28465
+ text: `${row.running ? "●" : row.done ? "✓" : row.live ? "⏸" : "○"} ${row.label} · ${row.activity}${row.live ? " · live" : ""}`
28466
+ })),
28467
+ cursor,
28468
+ loading,
28469
+ ...error === void 0 ? {} : { error },
28470
+ query: "",
28471
+ footer: "↑↓ choose · enter/t transcript · r refresh · esc close"
28472
+ });
28473
+ }
28474
+ /**
28475
+ * The /subagent model panel: which model configuration delegated subagents
28476
+ * run on. The kernel seeds child agents from the parent's CREATE-TIME
28477
+ * AgentOptions, so a mid-session /model switch would otherwise leave them on
28478
+ * the launch-time route; the TUI mirrors the selection onto subagent-origin
28479
+ * requests (or an explicit override picked here) via an agent/request
28480
+ * listener. The leading "inherit" row restores follow-the-current-model
28481
+ * behavior; picking a model with several advertised efforts opens the same
28482
+ * effort stage /model uses. Effort overrides are not offered separately —
28483
+ * the kernel's AgentOptions has no effort channel for children, so the level
28484
+ * rides the selected model exactly as /model applies it.
28485
+ */
28486
+ function SubagentPanel({ current, load, pick, inherit, close }) {
28487
+ const [directory, setDirectory] = (0, import_react.useState)(void 0);
28488
+ const [error, setError] = (0, import_react.useState)();
28489
+ const [loading, setLoading] = (0, import_react.useState)(true);
28490
+ const [cursor, setCursor] = (0, import_react.useState)(0);
28491
+ const [effortFor, setEffortFor] = (0, import_react.useState)(void 0);
28492
+ const refresh = () => {
28493
+ setLoading(true);
28494
+ setError(void 0);
28495
+ Promise.resolve().then(load).then((value) => {
28496
+ setDirectory(value);
28497
+ setLoading(false);
28498
+ }, (reason) => {
28499
+ setError(reason instanceof Error ? reason.message : String(reason));
28500
+ setLoading(false);
28501
+ });
28502
+ };
28503
+ (0, import_react.useEffect)(refresh, []);
28504
+ const rows = (0, import_react.useMemo)(() => directory?.rows ?? [], [directory]);
28505
+ (0, import_react.useEffect)(() => {
28506
+ if (current === "" || rows.length === 0) return;
28507
+ const index = rows.findIndex((row) => current.startsWith(`${row.provider}/${row.model}`));
28508
+ if (index >= 0) setCursor(index + 1);
28509
+ }, [rows, current]);
28510
+ (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, rows.length)), [rows.length]);
28511
+ useInput((input, key) => {
28512
+ if (effortFor !== void 0) return;
28513
+ if (key.escape || input === "q") return close();
28514
+ if (input === "r" && !loading) return refresh();
28515
+ if (key.upArrow) return setCursor((value) => (value + rows.length) % (rows.length + 1));
28516
+ if (key.downArrow) return setCursor((value) => (value + 1) % (rows.length + 1));
28517
+ if (key.return) {
28518
+ if (cursor === 0) return inherit();
28519
+ const row = rows[cursor - 1];
28520
+ if (row === void 0) return;
28521
+ if (row.reasoning !== void 0 && row.reasoning.efforts.length > 1) {
28522
+ setEffortFor(row);
28523
+ return;
28524
+ }
28525
+ pick(row, row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : void 0);
28526
+ }
28527
+ });
28528
+ if (effortFor !== void 0) return (0, import_react.createElement)(EffortPanel, {
28529
+ row: effortFor,
28530
+ current: current === "" ? void 0 : current.split("@")[1],
28531
+ select: (effortId) => pick(effortFor, effortId),
28532
+ back: () => setEffortFor(void 0)
28533
+ });
28534
+ return (0, import_react.createElement)(ListFrame, {
28535
+ title: `/subagent — model for delegated agents${current === "" ? "" : ` · override ${current}`}`,
28536
+ rows: [{
28537
+ key: "__inherit__",
28538
+ text: `${current === "" ? "●" : "○"} inherit — follow the current model (/model switches apply)`
28539
+ }, ...rows.map((row) => ({
28540
+ key: `${row.provider}/${row.model}`,
28541
+ text: `${current.startsWith(`${row.provider}/${row.model}`) ? "●" : "○"} ${row.providerName} · ${row.modelName}`
28542
+ }))],
28543
+ cursor,
28544
+ loading,
28545
+ ...error === void 0 ? {} : { error },
28546
+ query: "",
28547
+ footer: "↑↓ choose · enter apply · r refresh · esc close"
28055
28548
  });
28056
28549
  }
28057
28550
  /** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
@@ -28588,6 +29081,23 @@ function Header({ resumed }) {
28588
29081
  function todoMark(status) {
28589
29082
  return status === "completed" ? "✓" : status === "in_progress" ? "●" : "○";
28590
29083
  }
29084
+ /**
29085
+ * One-row live subagent summary (the Codex agent status feed, compressed to
29086
+ * the transcript's budget): running count, total, and the most recently
29087
+ * active child's current activity. One line, never more — the full view is
29088
+ * the /agents panel.
29089
+ */
29090
+ function AgentsLine({ rows }) {
29091
+ if (rows.length === 0) return void 0;
29092
+ const running = rows.filter((row) => row.state !== "done").length;
29093
+ const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0];
29094
+ const mark = newest.state === "done" ? "✓" : newest.state === "idle" ? "⏸" : "●";
29095
+ return (0, import_react.createElement)(Box, { paddingX: 1 }, (0, import_react.createElement)(Text, {
29096
+ color: inkColor(getPalette().brand),
29097
+ bold: true,
29098
+ wrap: "truncate-end"
29099
+ }, `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}`)));
29100
+ }
28591
29101
  /** One-row todo summary: task count cannot grow the live Ink tree. */
28592
29102
  function TodoPanel({ todos }) {
28593
29103
  if (todos.length === 0) return void 0;
@@ -28692,9 +29202,10 @@ function statusToneProps(tone) {
28692
29202
  */
28693
29203
  /** Theme anchors for the one-shot composer wave, read from the active palette
28694
29204
  * so the wave stays coordinated in both themes. The flash tier runs the
28695
- * brand blues; the deepseek tier swaps in the code sky-blue for a brighter,
28696
- * richer mix. Codex's Wave bands carry no hue index (only hues[0] tints the
28697
- * row), so the accent the prompt keeps is always hues[0]. */
29205
+ * brand blues; the deepseek AND unknown tiers swap in the code sky-blue for
29206
+ * a brighter, richer mix (the unknown tier reuses the pro palette). Codex's
29207
+ * Wave bands carry no hue index (only hues[0] tints the row), so the accent
29208
+ * the prompt keeps is always hues[0]. */
28698
29209
  function deepseekWaveHues(tier) {
28699
29210
  const palette = getPalette();
28700
29211
  return tier === "flash" ? [
@@ -28764,50 +29275,96 @@ function NoticeLine({ text, tone, columns }) {
28764
29275
  wrap: "truncate-end"
28765
29276
  }, truncateColumns(`${mark} ${singleLineText(text)}`, Math.max(1, columns - 2))));
28766
29277
  }
28767
- /** The y/n approval bar rendered while an approval ask is pending. */
28768
- function ApprovalBar({ snapshot, locked }) {
29278
+ /** The fixed decision list; answers stay in the binary answerer vocabulary. */
29279
+ const APPROVAL_OPTIONS = [
29280
+ {
29281
+ key: "allow",
29282
+ label: "Yes, proceed",
29283
+ hotkey: "y"
29284
+ },
29285
+ {
29286
+ key: "reject-note",
29287
+ label: "No, and tell it what to do differently",
29288
+ hotkey: "n"
29289
+ },
29290
+ {
29291
+ key: "reject",
29292
+ label: "No, continue without running it",
29293
+ hotkey: "d"
29294
+ }
29295
+ ];
29296
+ /**
29297
+ * The approval dialog (Codex ApprovalOverlay contract): a bold question
29298
+ * header, the bounded command body with an explicit overflow marker, a
29299
+ * numbered option list with a `›` cursor, single-key shortcuts, and digits
29300
+ * for direct selection. Askers queue FIFO — the count rides the header.
29301
+ * The upstream answerer vocabulary stays binary (`allowed-once` /
29302
+ * `rejected`): "tell it what to do differently" rejects and hands the
29303
+ * composer back with a hint notice, exactly Codex's decline-then-type flow.
29304
+ */
29305
+ function ApprovalBar({ snapshot, locked, notify }) {
28769
29306
  const stdout = useStdout().stdout;
28770
29307
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
28771
- const [scroll, setScroll] = (0, import_react.useState)(0);
29308
+ const [cursor, setCursor] = (0, import_react.useState)(0);
28772
29309
  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);
29310
+ const active = !locked && pending !== void 0 && !snapshot.answered;
29311
+ const body = (0, import_react.useMemo)(() => pending === void 0 || pending.command === "" ? [] : textLines(pending.command, viewport.contentColumns, "dim"), [pending, viewport.contentColumns]);
28776
29312
  (0, import_react.useEffect)(() => {
28777
- setScroll(0);
29313
+ setCursor(0);
28778
29314
  }, [pending]);
28779
- (0, import_react.useEffect)(() => {
28780
- if (visibleScroll !== scroll) setScroll(visibleScroll);
28781
- }, [visibleScroll, scroll]);
29315
+ const decide = (option) => {
29316
+ const ask = snapshot.pending;
29317
+ if (ask === void 0 || snapshot.answered) return;
29318
+ if (option.key === "allow") {
29319
+ ask.answer("allowed-once");
29320
+ return;
29321
+ }
29322
+ ask.answer("rejected");
29323
+ if (option.key === "reject-note") notify("rejected — type below what it should do differently (it steers the next step)", "warning");
29324
+ };
28782
29325
  useInput((input, key) => {
28783
- if (snapshot.pending === void 0) return;
29326
+ if (snapshot.pending === void 0 || snapshot.answered) return;
28784
29327
  if (key.upArrow) {
28785
- setScroll((current) => moveScroll(current, -1, content.length, viewport.bodyRows));
29328
+ setCursor((current) => (current + APPROVAL_OPTIONS.length - 1) % APPROVAL_OPTIONS.length);
28786
29329
  return;
28787
29330
  }
28788
29331
  if (key.downArrow) {
28789
- setScroll((current) => moveScroll(current, 1, content.length, viewport.bodyRows));
29332
+ setCursor((current) => (current + 1) % APPROVAL_OPTIONS.length);
28790
29333
  return;
28791
29334
  }
28792
- if (key.pageUp) {
28793
- setScroll((current) => moveScroll(current, -Math.max(1, viewport.bodyRows - 1), content.length, viewport.bodyRows));
29335
+ if (key.return) {
29336
+ decide(APPROVAL_OPTIONS[cursor]);
28794
29337
  return;
28795
29338
  }
28796
- if (key.pageDown) {
28797
- setScroll((current) => moveScroll(current, Math.max(1, viewport.bodyRows - 1), content.length, viewport.bodyRows));
29339
+ if (key.escape) {
29340
+ decide(APPROVAL_OPTIONS[2]);
28798
29341
  return;
28799
29342
  }
28800
- if (snapshot.answered) return;
28801
29343
  if (input === "y" || input === "Y") {
28802
- snapshot.pending.answer("allowed-once");
29344
+ decide(APPROVAL_OPTIONS[0]);
29345
+ return;
29346
+ }
29347
+ if (input === "n" || input === "N") {
29348
+ decide(APPROVAL_OPTIONS[1]);
29349
+ return;
29350
+ }
29351
+ if (input === "d" || input === "D") {
29352
+ decide(APPROVAL_OPTIONS[2]);
28803
29353
  return;
28804
29354
  }
28805
- if (input === "n" || input === "N") snapshot.pending.answer("rejected");
29355
+ if (/^[1-9]$/u.test(input)) {
29356
+ const index = Number(input) - 1;
29357
+ if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index]);
29358
+ }
28806
29359
  }, { isActive: active });
28807
- if (snapshot.pending === void 0) return void 0;
29360
+ if (pending === void 0) return void 0;
28808
29361
  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;
29362
+ const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : "";
29363
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`approval${queuedSuffix} · enter/y allow · esc/n reject`, viewport.contentColumns));
29364
+ const reservedRows = 3 + APPROVAL_OPTIONS.length;
29365
+ const bodyBudget = Math.max(1, viewport.bodyRows - reservedRows);
29366
+ const visibleBody = body.slice(0, bodyBudget);
29367
+ const overflow = body.length - visibleBody.length;
28811
29368
  return (0, import_react.createElement)(Box, {
28812
29369
  flexDirection: "column",
28813
29370
  width: viewport.outerColumns,
@@ -28818,10 +29375,25 @@ function ApprovalBar({ snapshot, locked }) {
28818
29375
  color: inkColor(getPalette().warn),
28819
29376
  bold: true,
28820
29377
  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,
29378
+ }, 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, {
29379
+ key: `body-${index}`,
29380
+ lines: [line]
29381
+ })), ...overflow > 0 ? [(0, import_react.createElement)(Text, {
29382
+ key: "overflow",
29383
+ color: inkColor(getPalette().dim),
29384
+ wrap: "truncate-end"
29385
+ }, 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) => {
29386
+ const selected = !snapshot.answered && index === cursor;
29387
+ return (0, import_react.createElement)(Text, {
29388
+ key: option.key,
29389
+ color: selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().text),
29390
+ bold: selected || void 0,
29391
+ wrap: "truncate-end"
29392
+ }, truncateColumns(`${selected ? "›" : " "} ${index + 1}. ${option.label} (${option.hotkey})`, viewport.contentColumns));
29393
+ }), (0, import_react.createElement)(Text, {
29394
+ color: inkColor(getPalette().dim),
28823
29395
  wrap: "truncate-end"
28824
- }, dim(truncateColumns(answered ? "submitted…" : "↑↓/pgup/pgdn scroll · y allow once · n reject", viewport.contentColumns))));
29396
+ }, truncateColumns(snapshot.answered ? "submitted…" : "↑↓ choose · enter confirm · y/n/d quick · esc reject", viewport.contentColumns)));
28825
29397
  }
28826
29398
  /**
28827
29399
  * The ask_user_question bar: walks one request question by question,
@@ -29067,18 +29639,31 @@ function QuestionBar({ store, snapshot, locked }) {
29067
29639
  }, dim(truncateColumns(footer, viewport.contentColumns))));
29068
29640
  }
29069
29641
  /** The /model panel: a scrolling list over the advisory model directory. */
29070
- function ModelPanel({ directory, error, onSelect, onProviders, onRetry, onClose }) {
29642
+ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry, onClose }) {
29071
29643
  const [cursor, setCursor] = (0, import_react.useState)(0);
29072
29644
  const stdout = useStdout().stdout;
29073
29645
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29074
29646
  const rows = directory?.rows ?? [];
29647
+ const positioned = (0, import_react.useRef)(false);
29075
29648
  (0, import_react.useEffect)(() => {
29076
- if (rows.length === 0) {
29077
- if (cursor !== 0) setCursor(0);
29649
+ if (positioned.current || rows.length === 0 || current === void 0) {
29650
+ if (rows.length === 0) {
29651
+ if (cursor !== 0) setCursor(0);
29652
+ return;
29653
+ }
29654
+ if (cursor >= rows.length) setCursor(rows.length - 1);
29078
29655
  return;
29079
29656
  }
29080
- if (cursor >= rows.length) setCursor(rows.length - 1);
29081
- }, [rows.length, cursor]);
29657
+ const index = rows.findIndex((row) => `${row.provider}/${row.model}` === current);
29658
+ if (index >= 0) {
29659
+ positioned.current = true;
29660
+ setCursor(index);
29661
+ } else if (cursor >= rows.length) setCursor(Math.max(0, rows.length - 1));
29662
+ }, [
29663
+ rows,
29664
+ cursor,
29665
+ current
29666
+ ]);
29082
29667
  useInput((input, key) => {
29083
29668
  if (key.escape || input === "q") {
29084
29669
  onClose();
@@ -29518,6 +30103,9 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
29518
30103
  (0, import_react.createElement)(Box, { key: "local-statusline" }, row("/statusline", "customize the status line items")),
29519
30104
  (0, import_react.createElement)(Box, { key: "local-theme" }, row("/theme", "switch the color theme")),
29520
30105
  (0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
30106
+ (0, import_react.createElement)(Box, { key: "local-agents" }, row("/agents", "inspect subagent sessions of this conversation")),
30107
+ (0, import_react.createElement)(Box, { key: "local-subagent" }, row("/subagent", "choose the model delegated subagents run on")),
30108
+ (0, import_react.createElement)(Box, { key: "local-delete" }, row("/delete", "delete a session and its subagent threads")),
29521
30109
  (0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
29522
30110
  (0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
29523
30111
  (0, import_react.createElement)(Box, { key: "local-title" }, row("/title", "rename this session (/title <text>)")),
@@ -29819,6 +30407,21 @@ function completionCandidates(value, descriptors, skills) {
29819
30407
  description: "search and recall past prompts",
29820
30408
  origin: "command"
29821
30409
  },
30410
+ {
30411
+ label: "/agents",
30412
+ description: "inspect subagent sessions of this conversation",
30413
+ origin: "command"
30414
+ },
30415
+ {
30416
+ label: "/subagent",
30417
+ description: "choose the model delegated subagents run on",
30418
+ origin: "command"
30419
+ },
30420
+ {
30421
+ label: "/delete",
30422
+ description: "delete a session and its subagent threads",
30423
+ origin: "command"
30424
+ },
29822
30425
  {
29823
30426
  label: "/clear",
29824
30427
  description: "clear the screen",
@@ -29920,7 +30523,7 @@ function CompletionMenu({ active, mention, index, rows }) {
29920
30523
  * While a modal (approval / question / model panel) owns the keys, the
29921
30524
  * box passes every key through untouched.
29922
30525
  */
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 }) {
30526
+ 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
30527
  const columns = useStdout().stdout?.columns ?? 80;
29925
30528
  const [value, setValue] = (0, import_react.useState)("");
29926
30529
  const [cursor, setCursor] = (0, import_react.useState)(0);
@@ -30015,8 +30618,39 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30015
30618
  description: row.description,
30016
30619
  origin: "path"
30017
30620
  })) : candidates;
30621
+ /** Accept the highlighted completion-menu candidate into the draft. */
30622
+ const acceptMenuCandidate = () => {
30623
+ if (mentionActive && mentionToken !== void 0) {
30624
+ const row = mentionRows[completionIndex % mentionRows.length];
30625
+ if (row !== void 0) {
30626
+ const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
30627
+ setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
30628
+ setCursor(mentionToken.start + insertion.length);
30629
+ }
30630
+ } else if (pathActive) {
30631
+ const row = pathRows[completionIndex % Math.max(1, pathRows.length)];
30632
+ if (row !== void 0) {
30633
+ const insertion = row.kind === "directory" ? `${row.label}/` : row.label;
30634
+ setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor));
30635
+ setCursor(pathTokenStart + insertion.length);
30636
+ }
30637
+ } else {
30638
+ const candidate = candidates[completionIndex % candidates.length];
30639
+ if (candidate !== void 0) {
30640
+ setValue(`${candidate.label} `);
30641
+ setCursor(candidate.label.length + 1);
30642
+ }
30643
+ }
30644
+ setCompletionIndex(0);
30645
+ setDismissedMenuValue(void 0);
30646
+ };
30018
30647
  useInput((input, key) => {
30019
30648
  if (!active) return;
30649
+ if (deleteConfirm !== void 0) {
30650
+ if (input === "y" || input === "Y") confirmDelete();
30651
+ else cancelDelete();
30652
+ return;
30653
+ }
30020
30654
  if (key.tab && key.shift) {
30021
30655
  try {
30022
30656
  const next = cyclePermission();
@@ -30072,6 +30706,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30072
30706
  setDismissedMenuValue(void 0);
30073
30707
  return;
30074
30708
  }
30709
+ if (menuActive) {
30710
+ if (!(!mentionActive && !pathActive && candidates.some((candidate) => candidate.label === value))) {
30711
+ acceptMenuCandidate();
30712
+ return;
30713
+ }
30714
+ }
30075
30715
  const text = value.trim();
30076
30716
  setValue("");
30077
30717
  setCursor(0);
@@ -30162,6 +30802,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30162
30802
  openHistory();
30163
30803
  return;
30164
30804
  }
30805
+ if (text === "/agents") {
30806
+ openAgents();
30807
+ return;
30808
+ }
30809
+ if (text === "/subagent") {
30810
+ openSubagent();
30811
+ return;
30812
+ }
30813
+ if (text === "/delete" || text.startsWith("/delete ")) {
30814
+ openDelete(text.slice(7).trim());
30815
+ return;
30816
+ }
30165
30817
  if (busy && !text.startsWith("/")) {
30166
30818
  steer(text);
30167
30819
  return;
@@ -30200,29 +30852,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30200
30852
  return;
30201
30853
  }
30202
30854
  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);
30855
+ acceptMenuCandidate();
30226
30856
  return;
30227
30857
  }
30228
30858
  if (key.backspace || key.delete) {
@@ -30311,6 +30941,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30311
30941
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
30312
30942
  const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
30313
30943
  if (frozen) {
30944
+ if (deleteConfirm !== void 0) return (0, import_react.createElement)(Box, {
30945
+ width: Math.max(1, columns - 1),
30946
+ borderStyle: "round",
30947
+ borderColor: inkColor(getPalette().warn),
30948
+ paddingX: 1
30949
+ }, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, (0, import_react.createElement)(Text, {
30950
+ color: inkColor(getPalette().warn),
30951
+ bold: true
30952
+ }, "❯ "), (0, import_react.createElement)(Text, {
30953
+ color: inkColor(getPalette().warn),
30954
+ bold: true
30955
+ }, "y delete · any other key cancels")));
30314
30956
  const frozen = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
30315
30957
  return (0, import_react.createElement)(Box, {
30316
30958
  width: Math.max(1, columns - 1),
@@ -30384,17 +31026,17 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30384
31026
  backgroundColor: waveBg(cells.length)
30385
31027
  });
30386
31028
  if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
30387
- const word = "deepseek";
30388
- const start = Math.max(2, Math.floor((contentWidth - 8) / 2));
31029
+ const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
31030
+ const start = Math.max(2, Math.floor((contentWidth - word.length) / 2));
30389
31031
  let clear = true;
30390
- for (let at = 0; at < 8; at += 1) {
31032
+ for (let at = 0; at < word.length; at += 1) {
30391
31033
  const cell = cells[start + at];
30392
31034
  if (cell === void 0 || cell.char !== " " && cell.dim !== true) {
30393
31035
  clear = false;
30394
31036
  break;
30395
31037
  }
30396
31038
  }
30397
- if (clear) for (let at = 0; at < 8; at += 1) {
31039
+ if (clear) for (let at = 0; at < word.length; at += 1) {
30398
31040
  const cell = cells[start + at];
30399
31041
  cell.char = word[at];
30400
31042
  cell.color = inkColor(deepseekWaveWordHue(at, hues));
@@ -30402,7 +31044,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30402
31044
  cell.dim = false;
30403
31045
  }
30404
31046
  }
30405
- if (waveTier === "deepseek" && style === "wave") {
31047
+ if ((waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
30406
31048
  const spark = deepseekWaveSpark(waveTick);
30407
31049
  if (spark !== null) {
30408
31050
  const last = cells[cells.length - 1];
@@ -30573,16 +31215,18 @@ function App(props) {
30573
31215
  const [effortFor, setEffortFor] = (0, import_react.useState)(void 0);
30574
31216
  /** Effective reasoning effort, shown in the /model picker and switch notice. */
30575
31217
  const [effortLabel, setEffortLabel] = (0, import_react.useState)(props.effort);
30576
- /** DeepSeek easter egg: switching INTO an official DeepSeek route plays
30577
- * one of Codex's three ignition styles (Wave / Aurora / Pulse, picked at
30578
- * random without repeating) across the composer's padded band (33ms tick,
30579
- * per-style durations), then the band returns to static while the prompt
30580
- * marker keeps the tier accent. The trigger follows the applied model
30581
- * label (what the status bar actually shows), never the initial paint,
30582
- * and the tier is derived from the label and cached at the switch. The
30583
- * 33ms tick itself lives inside Input, so the sweep re-renders only the
30584
- * composer row, not the whole tree, at 30fps; App owns the rarely-changing
30585
- * tier/style and Input starts the sweep whenever that pair changes. */
31218
+ /** DeepSeek easter egg: switching INTO an official DeepSeek route — or
31219
+ * onto a NON-DeepSeek model running a reasoning effort strictly above
31220
+ * high plays one of Codex's three ignition styles (Wave / Aurora /
31221
+ * Pulse, picked at random without repeating) across the composer's
31222
+ * padded band (33ms tick, per-style durations), then the band returns
31223
+ * to static while the prompt marker keeps the tier accent. The trigger
31224
+ * follows the applied model label (what the status bar actually shows),
31225
+ * never the initial paint, and the tier is derived from the label and
31226
+ * cached at the switch. The 33ms tick itself lives inside Input, so the
31227
+ * sweep re-renders only the composer row, not the whole tree, at 30fps;
31228
+ * App owns the rarely-changing tier/style and Input starts the sweep
31229
+ * whenever that pair changes. */
30586
31230
  const [waveTier, setWaveTier] = (0, import_react.useState)(null);
30587
31231
  const [waveStyle, setWaveStyle] = (0, import_react.useState)(null);
30588
31232
  const previousModel = (0, import_react.useRef)(void 0);
@@ -30594,13 +31238,15 @@ function App(props) {
30594
31238
  const effortChanged = previousEffort.current !== effortLabel;
30595
31239
  previousEffort.current = effortLabel;
30596
31240
  const modelChanged = previous !== void 0 && previous !== modelLabel;
30597
- if (!isOfficialDeepSeekLabel(modelLabel)) {
31241
+ const official = isOfficialDeepSeekLabel(modelLabel);
31242
+ const unknownTrigger = !official && effortAboveHigh(effortLabel);
31243
+ if (!official && !unknownTrigger) {
30598
31244
  setWaveTier(null);
30599
31245
  setWaveStyle(null);
30600
31246
  return;
30601
31247
  }
30602
31248
  if (modelChanged || effortChanged) {
30603
- setWaveTier(deepseekWaveTier(modelLabel));
31249
+ setWaveTier(official ? deepseekWaveTier(modelLabel) : "unknown");
30604
31250
  const nextStyle = deepseekWaveStyleRandom(previousStyle.current);
30605
31251
  previousStyle.current = nextStyle;
30606
31252
  setWaveStyle(nextStyle);
@@ -30679,6 +31325,36 @@ function App(props) {
30679
31325
  const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
30680
31326
  const [themeOpen, setThemeOpen] = (0, import_react.useState)(false);
30681
31327
  const [historyOpen, setHistoryOpen] = (0, import_react.useState)(false);
31328
+ const [agentsOpen, setAgentsOpen] = (0, import_react.useState)(false);
31329
+ const [subagentOpen, setSubagentOpen] = (0, import_react.useState)(false);
31330
+ /** /delete state: delete-mode hint plus an optional pre-armed row id. */
31331
+ const [resumeDelete, setResumeDelete] = (0, import_react.useState)({ mode: false });
31332
+ /** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
31333
+ * composer takes the keys, the resume panel yields until it settles. */
31334
+ const [deleteConfirmId, setDeleteConfirmId] = (0, import_react.useState)(void 0);
31335
+ /** Bumped after a deletion so the /resume listing reloads immediately. */
31336
+ const [deleteReloadToken, setDeleteReloadToken] = (0, import_react.useState)(0);
31337
+ const requestDelete = (0, import_react.useCallback)((row) => {
31338
+ setDeleteConfirmId(row.id);
31339
+ }, []);
31340
+ const cancelDelete = (0, import_react.useCallback)(() => {
31341
+ setDeleteConfirmId(void 0);
31342
+ }, []);
31343
+ const confirmDelete = (0, import_react.useCallback)(() => {
31344
+ const id = deleteConfirmId;
31345
+ if (id === void 0) return;
31346
+ setDeleteConfirmId(void 0);
31347
+ props.deleteSession(id).then((outcome) => {
31348
+ notify(outcome);
31349
+ setDeleteReloadToken((token) => token + 1);
31350
+ }, (reason) => {
31351
+ notify(`delete failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
31352
+ });
31353
+ }, [
31354
+ deleteConfirmId,
31355
+ props.deleteSession,
31356
+ notify
31357
+ ]);
30682
31358
  /** The /history panel's accepted entry: text plus its recall-space index. */
30683
31359
  const [historyFill, setHistoryFill] = (0, import_react.useState)(void 0);
30684
31360
  /** Submissions recorded in this process (Codex local history; persistent file stays in the runner). */
@@ -30712,9 +31388,10 @@ function App(props) {
30712
31388
  const [refreshEpoch, setRefreshEpoch] = (0, import_react.useState)(0);
30713
31389
  const approvalSnapshot = (0, import_react.useSyncExternalStore)(props.approval.subscribe, props.approval.getSnapshot);
30714
31390
  const questionSnapshot = (0, import_react.useSyncExternalStore)(props.questions.subscribe, props.questions.getSnapshot);
31391
+ const agentRows = (0, import_react.useSyncExternalStore)(props.subagents.subscribe, props.subagents.getSnapshot);
30715
31392
  const approvalPending = approvalSnapshot.pending !== void 0;
30716
31393
  const questionPending = questionSnapshot.pending !== void 0;
30717
- const inputActive = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
31394
+ const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending;
30718
31395
  (0, import_react.useEffect)(() => {
30719
31396
  if (!approvalPending && !questionPending) return;
30720
31397
  setModelOpen(false);
@@ -30729,6 +31406,9 @@ function App(props) {
30729
31406
  setStatuslineOpen(false);
30730
31407
  setThemeOpen(false);
30731
31408
  setHistoryOpen(false);
31409
+ setAgentsOpen(false);
31410
+ setSubagentOpen(false);
31411
+ setDeleteConfirmId(void 0);
30732
31412
  setVerboseOpen(false);
30733
31413
  }, [approvalPending, questionPending]);
30734
31414
  const settledRowsCache = (0, import_react.useRef)(void 0);
@@ -30788,8 +31468,8 @@ function App(props) {
30788
31468
  const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
30789
31469
  const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
30790
31470
  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;
31471
+ const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending;
31472
+ const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
30793
31473
  const closeInspector = (0, import_react.useCallback)(() => {
30794
31474
  setVerboseOpen(false);
30795
31475
  }, []);
@@ -30898,6 +31578,7 @@ function App(props) {
30898
31578
  onBack: () => setProviderOpen(false)
30899
31579
  });
30900
31580
  else if (effortFor !== void 0) modelSurface = (0, import_react.createElement)(EffortPanel, {
31581
+ key: `${effortFor.provider}/${effortFor.model}`,
30901
31582
  row: effortFor,
30902
31583
  current: effortLabel,
30903
31584
  select: (effortId) => applyModel(effortFor, effortId),
@@ -30906,6 +31587,7 @@ function App(props) {
30906
31587
  else modelSurface = (0, import_react.createElement)(ModelPanel, {
30907
31588
  directory,
30908
31589
  error: modelError,
31590
+ current: modelLabel,
30909
31591
  onSelect: (row) => {
30910
31592
  if (row.reasoning !== void 0 && row.reasoning.efforts.length > 1) {
30911
31593
  setEffortFor(row);
@@ -30936,13 +31618,14 @@ function App(props) {
30936
31618
  dim: false,
30937
31619
  maxRows: answerRows,
30938
31620
  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, {
31621
+ }, 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
31622
  store: props.questions,
30941
31623
  snapshot: questionSnapshot,
30942
31624
  locked: false
30943
31625
  }), (0, import_react.createElement)(ApprovalBar, {
30944
31626
  snapshot: approvalSnapshot,
30945
- locked: questionPending
31627
+ locked: questionPending,
31628
+ notify
30946
31629
  }), modelSurface, helpOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HelpPanel, {
30947
31630
  descriptors,
30948
31631
  skills,
@@ -30981,6 +31664,10 @@ function App(props) {
30981
31664
  currentCwd: props.workspaceRoot,
30982
31665
  load: props.loadSessions,
30983
31666
  readTranscript: props.loadSessionTranscript,
31667
+ requestDelete,
31668
+ deleteConfirmId,
31669
+ reloadToken: deleteReloadToken,
31670
+ deleteMode: resumeDelete.mode,
30984
31671
  select: (row) => {
30985
31672
  props.switchSession(row);
30986
31673
  setResumeOpen(false);
@@ -31016,6 +31703,29 @@ function App(props) {
31016
31703
  setHistoryOpen(false);
31017
31704
  },
31018
31705
  close: () => setHistoryOpen(false)
31706
+ }) : void 0, agentsOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(AgentsPanel, {
31707
+ live: agentRows,
31708
+ load: props.loadSubagents,
31709
+ readTranscript: props.loadSessionTranscript,
31710
+ close: () => setAgentsOpen(false)
31711
+ }) : void 0, subagentOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(SubagentPanel, {
31712
+ current: props.subagentModel,
31713
+ load: props.loadModels,
31714
+ pick: (row, effortId) => {
31715
+ try {
31716
+ const label = props.setSubagentModel(row, effortId);
31717
+ notify(`subagents → ${label}`);
31718
+ setSubagentOpen(false);
31719
+ } catch (reason) {
31720
+ notify(`subagent model change failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
31721
+ }
31722
+ },
31723
+ inherit: () => {
31724
+ props.clearSubagentModel();
31725
+ notify("subagents → inherit current model");
31726
+ setSubagentOpen(false);
31727
+ },
31728
+ close: () => setSubagentOpen(false)
31019
31729
  }) : void 0, notice === void 0 ? void 0 : (0, import_react.createElement)(NoticeLine, {
31020
31730
  text: notice.text,
31021
31731
  tone: notice.tone,
@@ -31057,7 +31767,8 @@ function App(props) {
31057
31767
  return;
31058
31768
  }
31059
31769
  if (row.reasoning === void 0 || row.reasoning.efforts.length === 0) {
31060
- notify("current model does not expose reasoning efforts", "warning");
31770
+ setEffortFor(row);
31771
+ setModelOpen(true);
31061
31772
  return;
31062
31773
  }
31063
31774
  setEffortFor(row);
@@ -31071,7 +31782,10 @@ function App(props) {
31071
31782
  },
31072
31783
  openMode: () => setModeOpen(true),
31073
31784
  openPermission: () => setPermissionOpen(true),
31074
- openResume: () => setResumeOpen(true),
31785
+ openResume: () => {
31786
+ setResumeDelete({ mode: false });
31787
+ setResumeOpen(true);
31788
+ },
31075
31789
  openPlugin: (query = "") => {
31076
31790
  setPluginQuery(query);
31077
31791
  setPluginOpen(true);
@@ -31079,6 +31793,20 @@ function App(props) {
31079
31793
  openStatusline: () => setStatuslineOpen(true),
31080
31794
  openTheme: () => setThemeOpen(true),
31081
31795
  openHistory: () => setHistoryOpen(true),
31796
+ openAgents: () => setAgentsOpen(true),
31797
+ openSubagent: () => setSubagentOpen(true),
31798
+ openDelete: (id) => {
31799
+ const armed = id === void 0 || id === "" ? void 0 : id;
31800
+ setResumeDelete({
31801
+ mode: true,
31802
+ ...armed === void 0 ? {} : { id: armed }
31803
+ });
31804
+ setDeleteConfirmId(armed);
31805
+ setResumeOpen(true);
31806
+ },
31807
+ deleteConfirm: deleteConfirmId,
31808
+ confirmDelete,
31809
+ cancelDelete,
31082
31810
  createSession: props.createSession,
31083
31811
  cancelSessionSwitch: props.cancelSessionSwitch,
31084
31812
  notify,
@@ -31149,15 +31877,26 @@ function App(props) {
31149
31877
  * @returns the store the renderer subscribes to.
31150
31878
  */
31151
31879
  function mountApprovalAnswerer(ctx, owns, preview) {
31880
+ const queue = [];
31152
31881
  let snapshot = {
31153
31882
  pending: void 0,
31154
- answered: false
31883
+ answered: false,
31884
+ queued: 0
31155
31885
  };
31156
31886
  const listeners = /* @__PURE__ */ new Set();
31157
- const set = (next) => {
31158
- snapshot = next;
31887
+ const publish = () => {
31888
+ const head = queue[0];
31889
+ snapshot = {
31890
+ pending: head === void 0 ? void 0 : head.pending,
31891
+ answered: head !== void 0 && head.answered,
31892
+ queued: Math.max(0, queue.length - 1)
31893
+ };
31159
31894
  for (const listener of listeners) listener();
31160
31895
  };
31896
+ const removeSlot = (slot) => {
31897
+ const at = queue.indexOf(slot);
31898
+ if (at !== -1) queue.splice(at, 1);
31899
+ };
31161
31900
  ctx.on("approval/request", (request, next) => {
31162
31901
  if (!owns(request.agent)) return next();
31163
31902
  if (request.signal?.aborted === true) return Promise.resolve("cancelled");
@@ -31172,39 +31911,36 @@ function mountApprovalAnswerer(ctx, owns, preview) {
31172
31911
  if (resolved) return;
31173
31912
  resolved = true;
31174
31913
  detachAbort();
31175
- set({
31176
- pending: void 0,
31177
- answered: false
31178
- });
31914
+ removeSlot(slot);
31915
+ publish();
31179
31916
  settle("cancelled");
31180
31917
  };
31181
31918
  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);
31919
+ const slot = {
31920
+ answered: false,
31921
+ pending: {
31922
+ headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
31923
+ toolName: request.toolName,
31924
+ command: preview(request),
31925
+ answer: (outcome) => {
31926
+ if (resolved) return;
31927
+ resolved = true;
31928
+ detachAbort();
31929
+ slot.answered = true;
31930
+ publish();
31931
+ settle(outcome);
31932
+ }
31195
31933
  }
31196
31934
  };
31197
- set({
31198
- pending,
31199
- answered: false
31200
- });
31935
+ queue.push(slot);
31936
+ publish();
31201
31937
  return new Promise((resolve) => {
31202
31938
  settle = resolve;
31203
31939
  }).then((outcome) => {
31204
- if (outcome !== "cancelled") set({
31205
- pending: void 0,
31206
- answered: false
31207
- });
31940
+ if (outcome !== "cancelled") {
31941
+ removeSlot(slot);
31942
+ publish();
31943
+ }
31208
31944
  return outcome;
31209
31945
  });
31210
31946
  });
@@ -31352,6 +32088,32 @@ function buildModelSelection(row, effortId) {
31352
32088
  ...selected === void 0 ? {} : { reasoningEffort: ReasoningEffortId(selected) }
31353
32089
  };
31354
32090
  }
32091
+ /** Display label for one applied selection: `provider/model` or `provider/model@effort`. */
32092
+ function modelSelectionLabel(selection) {
32093
+ return selection.reasoningEffort === void 0 ? `${selection.provider}/${selection.model}` : `${selection.provider}/${selection.model}@${selection.reasoningEffort}`;
32094
+ }
32095
+ /**
32096
+ * Apply one model selection onto a resolved request config — the exact
32097
+ * semantics of the kernel's `installModelSelection` request listener,
32098
+ * extracted so the TUI can mirror it for subagent-origin requests: children
32099
+ * spawned by the subagent tool inherit the parent's CREATE-TIME AgentOptions,
32100
+ * which a mid-session /model switch never touches, so delegated work would
32101
+ * otherwise keep running on the launch-time route. An absent effort strips
32102
+ * any inherited effort (restoring the selected model's provider default),
32103
+ * matching the kernel listener field-for-field.
32104
+ * @param resolved - the config the inner chain produced.
32105
+ * @param selection - the selection to enforce.
32106
+ * @returns the overridden config.
32107
+ */
32108
+ function applyModelSelectionToConfig(resolved, selection) {
32109
+ const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved;
32110
+ return {
32111
+ ...withoutInheritedEffort,
32112
+ provider: selection.provider,
32113
+ model: selection.model,
32114
+ ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
32115
+ };
32116
+ }
31355
32117
  /**
31356
32118
  * Load the selectable model directory from the live `ctx.llm` registry.
31357
32119
  * Providers are listed synchronously; each provider's models are discovered
@@ -32000,6 +32762,153 @@ function createTranscriptStore(replay) {
32000
32762
  }
32001
32763
  };
32002
32764
  }
32765
+ /** Bounded last-activity text (plain characters, display-sliced later). */
32766
+ const MAX_ACTIVITY_CHARS = 80;
32767
+ /** Single-line bounded preview of an assembled message's text content. */
32768
+ function messagePreview(content) {
32769
+ if (!Array.isArray(content)) return "replied";
32770
+ const texts = [];
32771
+ for (const block of content) {
32772
+ if (texts.join(" ").length >= MAX_ACTIVITY_CHARS) break;
32773
+ if (typeof block === "object" && block !== null) {
32774
+ const { type, text } = block;
32775
+ if (type === "text" && typeof text === "string" && text !== "") texts.push(text);
32776
+ }
32777
+ }
32778
+ const joined = texts.join(" ").replace(/\s+/gu, " ").trim();
32779
+ return joined === "" ? "replied" : bound(joined);
32780
+ }
32781
+ /** Bound one activity string to the display budget. */
32782
+ function bound(text) {
32783
+ const flat = text.replace(/\s+/gu, " ").trim();
32784
+ return flat.length > MAX_ACTIVITY_CHARS ? `${flat.slice(0, 79)}…` : flat;
32785
+ }
32786
+ /**
32787
+ * Fold one child-session event into its feed row (pure).
32788
+ * Unknown event kinds leave the row untouched.
32789
+ * @param previous - the row's current state, when any.
32790
+ * @param sessionId - the child session id.
32791
+ * @param event - the child session event.
32792
+ * @returns the next row state.
32793
+ */
32794
+ function foldSubagentRow(previous, sessionId, event) {
32795
+ const base = previous ?? {
32796
+ id: sessionId,
32797
+ label: `agent ${sessionId.slice(-6)}`,
32798
+ state: "running",
32799
+ activity: "starting…",
32800
+ updatedAt: event.time
32801
+ };
32802
+ const data = event.data;
32803
+ switch (event.type) {
32804
+ case "session/title": {
32805
+ const title = data["title"];
32806
+ const text = typeof title === "string" && title.trim() !== "" ? title : void 0;
32807
+ return text === void 0 || text === base.label ? base : {
32808
+ ...base,
32809
+ label: bound(text),
32810
+ updatedAt: event.time
32811
+ };
32812
+ }
32813
+ case "request/header": return {
32814
+ ...base,
32815
+ state: "running",
32816
+ activity: "working…",
32817
+ updatedAt: event.time
32818
+ };
32819
+ case "user/message": return {
32820
+ ...base,
32821
+ state: "running",
32822
+ activity: "prompted",
32823
+ updatedAt: event.time
32824
+ };
32825
+ case "assistant/chunk": return {
32826
+ ...base,
32827
+ state: "running",
32828
+ activity: "thinking…",
32829
+ updatedAt: event.time
32830
+ };
32831
+ case "assistant/message": return {
32832
+ ...base,
32833
+ state: "idle",
32834
+ activity: messagePreview(data["message"] === void 0 ? void 0 : data["message"].content),
32835
+ updatedAt: event.time
32836
+ };
32837
+ case "tool/call": {
32838
+ const name = typeof data["name"] === "string" ? data["name"] : "tool";
32839
+ return {
32840
+ ...base,
32841
+ state: "running",
32842
+ activity: `tool ${name}`,
32843
+ updatedAt: event.time
32844
+ };
32845
+ }
32846
+ case "tool/result": return {
32847
+ ...base,
32848
+ state: "running",
32849
+ activity: "tool done",
32850
+ updatedAt: event.time
32851
+ };
32852
+ case "turn/start": return {
32853
+ ...base,
32854
+ state: "running",
32855
+ activity: base.activity === "starting…" ? "working…" : base.activity,
32856
+ updatedAt: event.time
32857
+ };
32858
+ case "turn/end": return {
32859
+ ...base,
32860
+ state: "done",
32861
+ activity: "finished",
32862
+ updatedAt: event.time
32863
+ };
32864
+ default: return base;
32865
+ }
32866
+ }
32867
+ /**
32868
+ * Create one subagent feed. `apply` folds a child event (the caller gates
32869
+ * which sessions are children); `reset` clears on a session switch. Row
32870
+ * order is first-seen; the snapshot array is frozen and only replaced when
32871
+ * a row actually changed.
32872
+ * @returns the mutable feed handle plus its `SubagentFeedView`.
32873
+ */
32874
+ function createSubagentFeed() {
32875
+ let rows = Object.freeze([]);
32876
+ const listeners = /* @__PURE__ */ new Set();
32877
+ let scheduled = false;
32878
+ const notify = () => {
32879
+ if (scheduled) return;
32880
+ scheduled = true;
32881
+ queueMicrotask(() => {
32882
+ scheduled = false;
32883
+ for (const listener of listeners) listener();
32884
+ });
32885
+ };
32886
+ return {
32887
+ apply(sessionId, event) {
32888
+ const index = rows.findIndex((row) => row.id === sessionId);
32889
+ const previous = index === -1 ? void 0 : rows[index];
32890
+ const next = foldSubagentRow(previous, sessionId, event);
32891
+ if (next === previous) return;
32892
+ if (index === -1 && rows.length >= 8) return;
32893
+ rows = Object.freeze(index === -1 ? [...rows, next] : rows.map((row, at) => at === index ? next : row));
32894
+ notify();
32895
+ },
32896
+ reset() {
32897
+ if (rows.length === 0) return;
32898
+ rows = Object.freeze([]);
32899
+ notify();
32900
+ },
32901
+ subscribe(listener) {
32902
+ listeners.add(listener);
32903
+ return () => {
32904
+ listeners.delete(listener);
32905
+ };
32906
+ },
32907
+ getSnapshot() {
32908
+ return rows;
32909
+ }
32910
+ };
32911
+ }
32003
32912
  //#endregion
32004
32913
  //#region src/skills.ts
32005
32914
  function toRows(skills) {
@@ -32287,76 +33196,6 @@ function listPluginRows(ctx) {
32287
33196
  return rows;
32288
33197
  }
32289
33198
  //#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
33199
  //#region src/index.ts
32361
33200
  /**
32362
33201
  * @deepseek-ai/dsh-code — the interactive terminal driver. The bundle patch
@@ -32512,7 +33351,7 @@ async function run(ctx, startup, io) {
32512
33351
  const sessionQuery = ctx.get("sessionQuery");
32513
33352
  if (agents === void 0 || defaultModel === void 0 || sessions === void 0) return;
32514
33353
  const cwd = process.cwd();
32515
- const defaults = defaultModel.currentSelection();
33354
+ const currentDefaults = () => defaultModel.currentSelection();
32516
33355
  const presets = agentPresetsFrom(ctx);
32517
33356
  if (presets === void 0) throw new Error("agent preset service is unavailable; check the dsh-code bundle patch");
32518
33357
  const permissionPresets = permissionPresetsFrom(ctx);
@@ -32528,7 +33367,7 @@ async function run(ctx, startup, io) {
32528
33367
  mode = (await presets.mount(agentCtx, sessionPreset)).id;
32529
33368
  installModelSelection(agentCtx, {
32530
33369
  get current() {
32531
- return resolveEffectiveSelection(selectionState.picked, agentCtx.agent?.session.requestHeader()?.config, defaults);
33370
+ return resolveEffectiveSelection(selectionState.picked, agentCtx.agent?.session.requestHeader()?.config, currentDefaults());
32532
33371
  },
32533
33372
  set current(value) {
32534
33373
  selectionState.picked = value;
@@ -32536,12 +33375,16 @@ async function run(ctx, startup, io) {
32536
33375
  assembled: void 0
32537
33376
  });
32538
33377
  };
33378
+ const seedOptions = pendingSelection === void 0 ? {
33379
+ provider: currentDefaults().provider,
33380
+ model: currentDefaults().model
33381
+ } : {
33382
+ provider: pendingSelection.provider,
33383
+ model: pendingSelection.model
33384
+ };
32539
33385
  const handle = next.resume ? await agents.resume({
32540
33386
  resumeSessionId: SessionId(next.sessionId),
32541
- agentOptions: {
32542
- provider: defaults.provider,
32543
- model: defaults.model
32544
- },
33387
+ agentOptions: seedOptions,
32545
33388
  signal: quitAbort.signal,
32546
33389
  setup
32547
33390
  }) : await agents.create({
@@ -32550,10 +33393,7 @@ async function run(ctx, startup, io) {
32550
33393
  cwd: nextCwd,
32551
33394
  agentPreset: mode
32552
33395
  },
32553
- agentOptions: {
32554
- provider: defaults.provider,
32555
- model: defaults.model
32556
- },
33396
+ agentOptions: seedOptions,
32557
33397
  signal: quitAbort.signal,
32558
33398
  setup
32559
33399
  });
@@ -32575,6 +33415,7 @@ async function run(ctx, startup, io) {
32575
33415
  let agent;
32576
33416
  let session;
32577
33417
  let store = createTranscriptStore();
33418
+ const subagents = createSubagentFeed();
32578
33419
  let mentions = createMentions(ctx, void 0, cwd);
32579
33420
  /** Explicit model pick made before any session exists (a bare launch). */
32580
33421
  let pendingSelection;
@@ -32625,13 +33466,26 @@ async function run(ctx, startup, io) {
32625
33466
  mentions = prepared.mentions;
32626
33467
  }
32627
33468
  const off = ctx.on("session/event", (subject, event) => {
32628
- if (session !== void 0 && subject.id === session.id) store.apply(event);
33469
+ if (session === void 0) return;
33470
+ if (subject.id === session.id) {
33471
+ store.apply(event);
33472
+ return;
33473
+ }
33474
+ if (subject.header.parentSession === session.id) subagents.apply(subject.id, event);
32629
33475
  });
32630
33476
  const commands = watchCommands(ctx);
32631
33477
  if (agent !== void 0) commands.setAgent(agent);
32632
33478
  const skills = watchSkills(ctx);
32633
33479
  if (agent !== void 0) skills.setAgent(agent);
32634
33480
  const approval = mountApprovalAnswerer(ctx, (candidate) => agent !== void 0 && candidate.id === agent.id, (request) => approvalCommandPreview(store.getView().entries, request.callId, request.toolName));
33481
+ let subagentOverride;
33482
+ ctx.on("agent/request", (payload, next) => {
33483
+ const subject = payload.agent;
33484
+ const header = subject.session.header;
33485
+ if (header.parentSession === void 0 && header.origin !== "subagent") return next();
33486
+ const picked = subagentOverride ?? resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, subject.session.requestHeader()?.config, currentDefaults());
33487
+ return next().then((resolved) => applyModelSelectionToConfig(resolved, picked));
33488
+ });
32635
33489
  const questions = mountQuestionProvider(ctx);
32636
33490
  const bridge = { notify: () => {} };
32637
33491
  const statuslinePath = join(homedir(), ".dsh", "dsh-code", "statusline.json");
@@ -32838,6 +33692,7 @@ async function run(ctx, startup, io) {
32838
33692
  session = next.session;
32839
33693
  store = next.store;
32840
33694
  mentions = next.mentions;
33695
+ subagents.reset();
32841
33696
  pendingMode = void 0;
32842
33697
  pendingPermission = void 0;
32843
33698
  commands.setAgent(agent);
@@ -32950,8 +33805,33 @@ async function run(ctx, startup, io) {
32950
33805
  const selection = buildModelSelection(row, effortId);
32951
33806
  if (active === void 0) pendingSelection = selection;
32952
33807
  else active.selection.picked = selection;
33808
+ defaultModel.saveSelection(selection).catch((error) => {
33809
+ bridge.notify(`model switch applies to this session but was not saved as the default: ${error instanceof Error ? error.message : String(error)}`, "warning");
33810
+ });
33811
+ const llm = ctx.get("llm");
33812
+ const resolveCallConfig = llm?.resolveCallConfig;
33813
+ if (llm !== void 0 && typeof resolveCallConfig === "function") Promise.resolve(resolveCallConfig.call(llm, {
33814
+ provider: selection.provider,
33815
+ model: selection.model,
33816
+ ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
33817
+ })).catch((error) => {
33818
+ bridge.notify(`model selection rejected: ${error instanceof Error ? error.message : String(error)} — reopen /model to pick again`, "error");
33819
+ });
32953
33820
  return `${row.provider}/${row.model}`;
32954
33821
  };
33822
+ /** The /subagent override label, '' when delegated agents follow the current model. */
33823
+ const subagentModelLabel = () => subagentOverride === void 0 ? "" : modelSelectionLabel(subagentOverride);
33824
+ /** Apply one /subagent model pick; returns the override label. */
33825
+ const setSubagentModel = (row, effortId) => {
33826
+ subagentOverride = buildModelSelection(row, effortId);
33827
+ renderCurrent();
33828
+ return modelSelectionLabel(subagentOverride);
33829
+ };
33830
+ /** Drop the /subagent override: delegated agents follow the current model again. */
33831
+ const clearSubagentModel = () => {
33832
+ subagentOverride = void 0;
33833
+ renderCurrent();
33834
+ };
32955
33835
  /**
32956
33836
  * Export the folded transcript to a markdown file (/export). The default
32957
33837
  * target sits beside the session's cwd so the file lands in the user's
@@ -32994,11 +33874,61 @@ async function run(ctx, startup, io) {
32994
33874
  };
32995
33875
  const loadSessions = async (options, signal) => {
32996
33876
  if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
32997
- const projected = projectSessionRows(await sessionQuery.listSessions(signal), options);
33877
+ const records = await sessionQuery.listSessions(signal);
33878
+ const updated = /* @__PURE__ */ new Map();
33879
+ for (const record of records) {
33880
+ const location = persistence?.locate(record.header);
33881
+ if (location === void 0) continue;
33882
+ try {
33883
+ updated.set(record.header.id, (await stat(location.path)).mtimeMs);
33884
+ } catch {}
33885
+ }
33886
+ const projected = projectSessionRows(records, options, updated);
32998
33887
  const page = projected.slice(0, 32);
32999
33888
  if (page.length === 0) return projected;
33000
33889
  return mergeSessionTitles(projected, await sessionQuery.readTitleSnapshots(page.map((row) => row.id), signal));
33001
33890
  };
33891
+ /**
33892
+ * Delete one session subtree (/delete, codex semantics: subagent threads go
33893
+ * with their root). The kernel persistence seam has NO deletion API by
33894
+ * design — logs accumulate "until removed externally" — so this is the
33895
+ * controlled external removal: guards (live/current refusal, subtree
33896
+ * collection, and the JSONL layout check `encodeSegment(id)/session.jsonl`)
33897
+ * run before any filesystem touch, and only the backend-located artifacts
33898
+ * are removed. Backends without a locatable artifact (SQLite) are refused.
33899
+ * @param id - the root session id to delete.
33900
+ * @returns the outcome line for the panel/notice.
33901
+ */
33902
+ const deleteSession = async (id) => {
33903
+ if (sessionQuery === void 0) return "session query is unavailable in this profile";
33904
+ if (session !== void 0 && session.id === id) return "cannot delete the session you are using — switch or /new first";
33905
+ const records = await sessionQuery.listSessions();
33906
+ const target = records.find((record) => record.header.id === id);
33907
+ if (target === void 0) return `no persisted session matches "${id}"`;
33908
+ if (target.live) return "cannot delete a live session — it is open in this or another process";
33909
+ const doomed = collectDeletionSubtree(records, id);
33910
+ const byId = new Map(records.map((record) => [record.header.id, record]));
33911
+ let removed = 0;
33912
+ for (const candidate of doomed) {
33913
+ const record = byId.get(candidate);
33914
+ if (record === void 0 || record.live) continue;
33915
+ const location = persistence?.locate(record.header);
33916
+ if (location === void 0) return `session backend exposes no deletable artifact for ${candidate.slice(-12)} (deletion is unsupported on this backend)`;
33917
+ const dir = sessionArtifactDirectory(location.path, candidate);
33918
+ if (dir === void 0) return `refusing to delete: unexpected artifact layout at ${location.path}`;
33919
+ try {
33920
+ for (const name of SESSION_ARTIFACT_NAMES) await rm(join(dir, name), { force: true });
33921
+ await rm(dir, {
33922
+ force: true,
33923
+ recursive: false
33924
+ }).catch(() => {});
33925
+ removed += 1;
33926
+ } catch (error) {
33927
+ return `delete failed for ${candidate.slice(-12)}: ${error instanceof Error ? error.message : String(error)}`;
33928
+ }
33929
+ }
33930
+ return `deleted ${removed} session${removed === 1 ? "" : "s"}`;
33931
+ };
33002
33932
  const loadSessionTranscript = async (id, signal) => {
33003
33933
  if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
33004
33934
  const snapshot = await sessionQuery.readSession(id, signal);
@@ -33041,6 +33971,7 @@ async function run(ctx, startup, io) {
33041
33971
  session = next.session;
33042
33972
  store = next.store;
33043
33973
  mentions = next.mentions;
33974
+ subagents.reset();
33044
33975
  pendingMode = void 0;
33045
33976
  pendingPermission = void 0;
33046
33977
  commands.setAgent(agent);
@@ -33156,6 +34087,7 @@ async function run(ctx, startup, io) {
33156
34087
  const appElement = () => {
33157
34088
  const sessionCwd = session?.header.cwd ?? cwd;
33158
34089
  const currentView = store.getView();
34090
+ const defaults = currentDefaults();
33159
34091
  const model = currentView.model !== "" ? currentView.model : pendingSelection !== void 0 ? `${pendingSelection.provider}/${pendingSelection.model}` : `${defaults.provider}/${defaults.model}`;
33160
34092
  const effort = resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, session?.requestHeader()?.config, defaults).reasoningEffort;
33161
34093
  const permission = permissionPresets === void 0 ? currentView.permission : effectivePermission(permissionPresets, session, pendingPermission);
@@ -33164,6 +34096,7 @@ async function run(ctx, startup, io) {
33164
34096
  store,
33165
34097
  approval,
33166
34098
  questions,
34099
+ subagents,
33167
34100
  commands,
33168
34101
  skills,
33169
34102
  model,
@@ -33189,6 +34122,10 @@ async function run(ctx, startup, io) {
33189
34122
  cyclePermission: cyclePermission$1,
33190
34123
  setPermission: setPermissionAction,
33191
34124
  selectModel,
34125
+ subagentModel: subagentModelLabel(),
34126
+ setSubagentModel,
34127
+ clearSubagentModel,
34128
+ deleteSession,
33192
34129
  exportTranscript,
33193
34130
  renameTitle,
33194
34131
  loadPresets: () => presets.list(),
@@ -33197,6 +34134,17 @@ async function run(ctx, startup, io) {
33197
34134
  createSession,
33198
34135
  loadSessions,
33199
34136
  loadSessionTranscript,
34137
+ loadSubagents: () => {
34138
+ const current = session;
34139
+ if (current === void 0 || sessionQuery === void 0) return Promise.resolve([]);
34140
+ return loadSessions({
34141
+ sessions: "all",
34142
+ cwd: "all",
34143
+ sort: "newest",
34144
+ currentCwd: current.header.cwd ?? cwd,
34145
+ query: ""
34146
+ }).then((rows) => rows.filter((row) => row.parent === current.id));
34147
+ },
33200
34148
  switchSession,
33201
34149
  cancelSessionSwitch,
33202
34150
  loadPlugins: () => listPluginRows(ctx),