dsh-code 1.0.5 → 1.0.7

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.
Files changed (47) hide show
  1. package/README.en.md +338 -286
  2. package/README.md +68 -16
  3. package/bin/deepseek.mjs +204 -4
  4. package/cordis.patch.yml +105 -7
  5. package/lib/index.mjs +2148 -439
  6. package/lib/session-query.mjs +149 -0
  7. package/lib/types/app.d.ts +34 -8
  8. package/lib/types/attachments.d.ts +36 -4
  9. package/lib/types/index.d.ts +38 -2
  10. package/lib/types/kernel-panels.d.ts +23 -0
  11. package/lib/types/provider-settings.d.ts +6 -11
  12. package/lib/types/render/animations.d.ts +74 -7
  13. package/lib/types/render/editor.d.ts +4 -3
  14. package/lib/types/render/export.d.ts +0 -6
  15. package/lib/types/render/fuzzy.d.ts +21 -0
  16. package/lib/types/render/ime-cursor.d.ts +60 -0
  17. package/lib/types/render/projection.d.ts +80 -4
  18. package/lib/types/render/status.d.ts +1 -1
  19. package/lib/types/session-directory.d.ts +48 -13
  20. package/lib/types/session-query.d.ts +92 -0
  21. package/lib/types/store.d.ts +3 -0
  22. package/lib/types/terminal-title.d.ts +58 -0
  23. package/lib/types/update-panel.d.ts +49 -0
  24. package/lib/types/update.d.ts +66 -0
  25. package/package.json +307 -162
  26. package/src/app.ts +730 -266
  27. package/src/attachments.ts +110 -11
  28. package/src/commands.ts +35 -5
  29. package/src/index.ts +1986 -1779
  30. package/src/internals.ts +66 -40
  31. package/src/kernel-panels.ts +89 -3
  32. package/src/provider-settings.ts +12 -12
  33. package/src/render/animations.ts +606 -403
  34. package/src/render/editor.ts +5 -4
  35. package/src/render/export.ts +13 -3
  36. package/src/render/fuzzy.ts +83 -0
  37. package/src/render/ime-cursor.ts +147 -0
  38. package/src/render/projection.ts +1974 -1621
  39. package/src/render/status.ts +18 -4
  40. package/src/session-directory.ts +94 -16
  41. package/src/session-query.ts +235 -0
  42. package/src/skills.ts +23 -9
  43. package/src/store.ts +39 -1
  44. package/src/subagents.ts +26 -3
  45. package/src/terminal-title.ts +173 -0
  46. package/src/update-panel.ts +246 -0
  47. package/src/update.ts +110 -0
package/lib/index.mjs CHANGED
@@ -2,14 +2,14 @@ import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-run
2
2
  import { a as inkColor, c as chalk, i as getTheme, n as dim, o as parseThemeName, r as getPalette, s as setTheme } from "./theme-DCT8Y2xf.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
- import { readFileSync, realpathSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
6
6
  import os, { homedir } from "node:os";
7
- import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
7
+ import { appendFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
11
- import { MessageId, ReasoningEffortId, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
12
- import { SessionId } from "@deepseek-ai/dsh-session";
11
+ import { MessageId, ReasoningEffortId, assistantStreamFirstTokenTime, boundContextSummary, createUserMessage, isTokenDelta, normalizeApiKey } from "@deepseek-ai/dsh-llm";
12
+ import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
13
13
  import { PassThrough, Stream } from "node:stream";
14
14
  import process$1, { cwd, env } from "node:process";
15
15
  import { EventEmitter } from "node:events";
@@ -24732,6 +24732,225 @@ function ThemePanel({ current, select, close }) {
24732
24732
  wrap: "truncate-end"
24733
24733
  }, truncateColumns(`↑↓ choose · enter apply · esc/q close${hiddenThemes > 0 ? ` · +${hiddenThemes} more` : ""}`, viewport.contentColumns)));
24734
24734
  }
24735
+ /** Keep the newest UPDATE_OUTPUT_CAP lines of streamed update output. */
24736
+ function clipUpdateLines(lines) {
24737
+ return lines.length <= 800 ? lines : lines.slice(lines.length - 800);
24738
+ }
24739
+ /** Rendered facts of one probe: current/target versions, actions, refusals. */
24740
+ function updatePlanView(status) {
24741
+ const rows = [];
24742
+ const latest = status.code.latest ?? "unknown";
24743
+ rows.push(status.code.latest !== null && status.code.latest === status.code.running ? {
24744
+ key: "code",
24745
+ text: `dsh-code ${status.code.running} (latest)`
24746
+ } : {
24747
+ key: "code",
24748
+ text: `dsh-code ${status.code.running} → ${latest}`
24749
+ });
24750
+ if (status.host.targetLine === null) rows.push({
24751
+ key: "host",
24752
+ text: "harness pinned line unreadable — update would install @deepseek-ai/dsh@latest",
24753
+ tone: "warn"
24754
+ });
24755
+ else if (status.host.installed === null) rows.push({
24756
+ key: "host",
24757
+ text: `harness not installed → ${status.host.targetLine}`
24758
+ });
24759
+ else if (status.host.installed === status.host.targetLine) rows.push({
24760
+ key: "host",
24761
+ text: `harness ${status.host.installed} (on pinned line)`
24762
+ });
24763
+ else rows.push({
24764
+ key: "host",
24765
+ text: `harness ${status.host.installed} → ${status.host.targetLine}`
24766
+ });
24767
+ const mounted = status.profile.mounted === null ? status.profile.spec ?? "not mounted" : `dsh-code ${status.profile.mounted}`;
24768
+ rows.push({
24769
+ key: "profile",
24770
+ text: `profile ${mounted}${status.profile.localCheckout ? " · local checkout" : ""}`,
24771
+ tone: status.profile.localCheckout ? "dim" : void 0
24772
+ });
24773
+ for (const plugin of status.plan.pluginSpecs) rows.push({
24774
+ key: `plugin:${plugin}`,
24775
+ text: `plugin ${plugin}`,
24776
+ tone: "dim"
24777
+ });
24778
+ const registryBlocker = status.blockers.registry ?? null;
24779
+ if (registryBlocker !== null) rows.push({
24780
+ key: "blocker:registry",
24781
+ text: registryBlocker,
24782
+ tone: "error"
24783
+ });
24784
+ if (status.blockers.downgrade) rows.push({
24785
+ key: "blocker:downgrade",
24786
+ text: `refusing to downgrade the host: dsh-code@${latest} needs ${status.host.targetLine ?? "the pinned line"}, but ${status.host.installed ?? "the installed host"} is newer — wait for the next dsh-code release`,
24787
+ tone: "error"
24788
+ });
24789
+ const checkoutBlocker = status.blockers.localCheckout ?? null;
24790
+ if (checkoutBlocker !== null) for (const [index, line] of checkoutBlocker.entries()) rows.push({
24791
+ key: `blocker:checkout:${index}`,
24792
+ text: line,
24793
+ tone: "error"
24794
+ });
24795
+ if (status.upToDate) rows.push({
24796
+ key: "uptodate",
24797
+ text: "everything is already on the pinned line — nothing to update",
24798
+ tone: "ok"
24799
+ });
24800
+ return {
24801
+ rows,
24802
+ runnable: status.upToDate !== true && registryBlocker === null && status.blockers.downgrade !== true && checkoutBlocker === null
24803
+ };
24804
+ }
24805
+ /** Footer hint line per phase; the plan phase names the confirm key only when runnable. */
24806
+ function updateFooter(phase, runnable, upToDate) {
24807
+ if (phase === "probe") return "checking… · esc close";
24808
+ if (phase === "error") return "r recheck · esc close";
24809
+ if (phase === "apply") return "updating… · ↑↓ scroll · esc waits";
24810
+ if (phase === "done") return "r recheck · esc close";
24811
+ if (upToDate) return "up to date · r recheck · esc close";
24812
+ return runnable ? "enter update · r recheck · esc close" : "blocked · r recheck · esc close";
24813
+ }
24814
+ /**
24815
+ * The /update surface: probe on open (and on r), confirm with enter/y,
24816
+ * stream the aligned apply, and land on a bounded result view. Escape is
24817
+ * locked while the apply child runs — killing npm mid-install is exactly
24818
+ * the half-updated state this command exists to prevent.
24819
+ */
24820
+ function UpdatePanel({ probe, apply, close, notify }) {
24821
+ const [phase, setPhase] = (0, import_react.useState)("probe");
24822
+ const [status, setStatus] = (0, import_react.useState)();
24823
+ const [probeError, setProbeError] = (0, import_react.useState)();
24824
+ const [lines, setLines] = (0, import_react.useState)([]);
24825
+ const [exit, setExit] = (0, import_react.useState)();
24826
+ const [applyError, setApplyError] = (0, import_react.useState)();
24827
+ const [anchor, setAnchor] = (0, import_react.useState)("tail");
24828
+ const [epoch, setEpoch] = (0, import_react.useState)(0);
24829
+ (0, import_react.useEffect)(() => {
24830
+ let disposed = false;
24831
+ setPhase("probe");
24832
+ setStatus(void 0);
24833
+ setProbeError(void 0);
24834
+ setAnchor("tail");
24835
+ probe().then((value) => {
24836
+ if (disposed) return;
24837
+ setStatus(value);
24838
+ setPhase("plan");
24839
+ }, (reason) => {
24840
+ if (disposed) return;
24841
+ setProbeError(reason instanceof Error ? reason.message : String(reason));
24842
+ setPhase("error");
24843
+ });
24844
+ return () => {
24845
+ disposed = true;
24846
+ };
24847
+ }, [epoch, probe]);
24848
+ const stdout = useStdout().stdout;
24849
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
24850
+ const start = () => {
24851
+ if (phase !== "plan" || status === void 0) return;
24852
+ if (!updatePlanView(status).runnable) return;
24853
+ setPhase("apply");
24854
+ setLines([]);
24855
+ setExit(void 0);
24856
+ setApplyError(void 0);
24857
+ setAnchor("tail");
24858
+ apply((line) => {
24859
+ setLines((previous) => clipUpdateLines([...previous, singleLineText(line)]));
24860
+ }).then((code) => {
24861
+ setExit(code);
24862
+ setPhase("done");
24863
+ notify(code === 0 ? "update installed — restart dsh to activate" : `update failed (exit ${code})`, code === 0 ? "info" : "error");
24864
+ }, (reason) => {
24865
+ const message = reason instanceof Error ? reason.message : String(reason);
24866
+ setApplyError(message);
24867
+ setPhase("done");
24868
+ notify(`update failed: ${message}`, "error");
24869
+ });
24870
+ };
24871
+ const planView = status === void 0 ? void 0 : updatePlanView(status);
24872
+ const rows = phase === "probe" ? [{
24873
+ key: "probe",
24874
+ text: "checking npm for the aligned update…",
24875
+ tone: "dim"
24876
+ }] : phase === "error" ? [{
24877
+ key: "error",
24878
+ text: singleLineText(probeError ?? "probe failed"),
24879
+ tone: "error"
24880
+ }] : phase === "plan" && planView !== void 0 ? planView.rows : phase === "apply" ? lines.map((line, index) => ({
24881
+ key: `out:${index}`,
24882
+ text: line,
24883
+ tone: "dim"
24884
+ })) : [
24885
+ ...exit === 0 ? [{
24886
+ key: "ok",
24887
+ text: "update installed — restart dsh to load the new version (/quit or ctrl+c)",
24888
+ tone: "ok"
24889
+ }] : [],
24890
+ ...exit !== void 0 && exit !== 0 ? [{
24891
+ key: "fail",
24892
+ text: `update failed (exit ${exit})`,
24893
+ tone: "error"
24894
+ }] : [],
24895
+ ...applyError !== void 0 ? [{
24896
+ key: "fail:start",
24897
+ text: singleLineText(applyError),
24898
+ tone: "error"
24899
+ }] : [],
24900
+ ...lines.map((line, index) => ({
24901
+ key: `out:${index}`,
24902
+ text: line,
24903
+ tone: "dim"
24904
+ }))
24905
+ ];
24906
+ const budget = Math.max(1, viewport.bodyRows);
24907
+ const tailOffset = clampScroll(Math.max(0, rows.length - budget), rows.length, budget);
24908
+ const offset = anchor === "tail" ? tailOffset : clampScroll(anchor, rows.length, budget);
24909
+ useInput((input, key) => {
24910
+ if (phase === "apply") {
24911
+ if (key.upArrow) setAnchor(offset <= 0 ? 0 : offset - 1);
24912
+ if (key.downArrow && offset >= tailOffset) setAnchor("tail");
24913
+ else if (key.downArrow) setAnchor(offset + 1);
24914
+ return;
24915
+ }
24916
+ if (key.escape || input === "q") return close();
24917
+ if (input === "r") {
24918
+ setEpoch((value) => value + 1);
24919
+ return;
24920
+ }
24921
+ if (key.return || input === "y") {
24922
+ start();
24923
+ return;
24924
+ }
24925
+ if (key.upArrow) setAnchor(offset <= 0 ? 0 : offset - 1);
24926
+ if (key.downArrow && offset >= tailOffset) setAnchor("tail");
24927
+ else if (key.downArrow) setAnchor(offset + 1);
24928
+ });
24929
+ if (viewport.maxHeight === 0 || viewport.compact) {
24930
+ const summary = phase === "probe" ? "checking…" : phase === "error" ? "probe failed" : phase === "apply" ? singleLineText(lines[lines.length - 1] ?? "updating…") : phase === "done" ? exit === 0 ? "installed — restart to activate" : "failed" : status?.upToDate === true ? "up to date" : planView?.runnable === true ? "enter updates" : "blocked";
24931
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(singleLineText(`/update · ${summary}`), viewport.contentColumns));
24932
+ }
24933
+ const visible = rows.slice(offset, offset + budget);
24934
+ const toneColor = (tone) => tone === "ok" ? inkColor(getPalette().success) : tone === "error" ? inkColor(getPalette().error) : tone === "warn" ? inkColor(getPalette().warn) : tone === "dim" ? inkColor(getPalette().dim) : void 0;
24935
+ const title = status === void 0 ? "/update · aligned upgrade" : `/update · dsh-code ${status.code.running}${status.code.latest !== null && status.code.latest !== status.code.running ? ` → ${status.code.latest}` : ""}`;
24936
+ return (0, import_react.createElement)(Box, {
24937
+ width: viewport.outerColumns,
24938
+ borderStyle: "round",
24939
+ borderColor: inkColor(getPalette().dim),
24940
+ flexDirection: "column",
24941
+ paddingX: 1
24942
+ }, (0, import_react.createElement)(Text, {
24943
+ color: inkColor(getPalette().brandBright),
24944
+ wrap: "truncate-end"
24945
+ }, truncateColumns(singleLineText(title), viewport.contentColumns)), ...visible.map((row) => (0, import_react.createElement)(Text, {
24946
+ key: row.key,
24947
+ color: toneColor(row.tone),
24948
+ wrap: "truncate-end"
24949
+ }, truncateColumns(` ${singleLineText(row.text)}`, viewport.contentColumns))), (0, import_react.createElement)(Text, {
24950
+ dimColor: true,
24951
+ wrap: "truncate-end"
24952
+ }, truncateColumns(singleLineText(updateFooter(phase, planView?.runnable ?? false, status?.upToDate === true)), viewport.contentColumns)));
24953
+ }
24735
24954
  //#endregion
24736
24955
  //#region src/whale-glyph.ts
24737
24956
  /** Half-block whale glyph rows; render with the brand color. */
@@ -24797,6 +25016,166 @@ function dshKernelVersion() {
24797
25016
  return cachedDshKernelVersion;
24798
25017
  }
24799
25018
  //#endregion
25019
+ //#region src/terminal-title.ts
25020
+ /**
25021
+ * Terminal tab/window title management for the TUI.
25022
+ *
25023
+ * Terminals label their tab from the window title, which an application sets
25024
+ * with an OSC 0 sequence; without one the tab shows the process name ("node").
25025
+ * The title text is untrusted display content (session names arrive through
25026
+ * events and user input), so it is sanitized before emission: control
25027
+ * characters and bidi/invisible formatting codepoints are stripped, whitespace
25028
+ * runs collapse to single spaces, and the result is bounded. Clearing writes
25029
+ * an empty OSC payload and the terminal falls back to its own default; the
25030
+ * previously set title is not portable to read back and is never restored.
25031
+ *
25032
+ * @module @deepseek-ai/dsh-code/terminal-title
25033
+ */
25034
+ /** The host process title at module load, restored on unmount. On Windows
25035
+ * this reaches the tab through SetConsoleTitleW (ConPTY reflects it to VS
25036
+ * Code and friends without any user setting); on POSIX it becomes the ps
25037
+ * name. Node replaces the argv memory, so the original must be saved before
25038
+ * the first assignment. */
25039
+ const initialProcessTitle = process.title;
25040
+ /** Tab label before a session carries a name. */
25041
+ const DEFAULT_TERMINAL_TITLE = "deepseek";
25042
+ /** Control characters, DEL/C1 range, and bidi or invisible formatting
25043
+ * codepoints that could terminate the OSC sequence or visually reorder the
25044
+ * title relative to its underlying text. */
25045
+ const DISALLOWED_TITLE_CHARS = /[\u0000-\u001F\u007F-\u009F\u00AD\u034F\u061C\u180E\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/;
25046
+ /** Normalize untrusted title text into one bounded display line: disallowed
25047
+ * codepoints dropped, whitespace runs collapsed to single spaces, leading and
25048
+ * trailing whitespace removed, length bounded. */
25049
+ function sanitizeTerminalTitle(text) {
25050
+ const chars = [];
25051
+ let pendingSpace = false;
25052
+ for (const ch of text) {
25053
+ if (DISALLOWED_TITLE_CHARS.test(ch)) continue;
25054
+ if (ch.trim() === "") {
25055
+ if (chars.length > 0) pendingSpace = true;
25056
+ continue;
25057
+ }
25058
+ if (pendingSpace) {
25059
+ if (chars.length + 1 >= 240) break;
25060
+ chars.push(" ");
25061
+ pendingSpace = false;
25062
+ }
25063
+ if (chars.length >= 240) break;
25064
+ chars.push(ch);
25065
+ }
25066
+ return chars.join("");
25067
+ }
25068
+ /** Build one OSC 0 title sequence. An empty sanitized title yields an empty
25069
+ * string: emitting nothing is distinct from clearing, which is a separate
25070
+ * policy decision made by the caller. */
25071
+ function terminalTitleSequence(text) {
25072
+ const title = sanitizeTerminalTitle(text);
25073
+ return title === "" ? "" : `\x1b]0;${title}\x07`;
25074
+ }
25075
+ /** Clear the managed title with an empty OSC payload; the terminal falls back
25076
+ * to its own default label. */
25077
+ function clearTerminalTitleSequence() {
25078
+ return "\x1B]0;\x07";
25079
+ }
25080
+ /** VS Code renders an application-set tab title only when
25081
+ * "terminal.integrated.tabs.title" maps to the sequence variable; the editor
25082
+ * default shows the process name instead ("node" for a Node CLI). Inside a VS
25083
+ * Code integrated terminal, align the user settings once: if the key is
25084
+ * absent, insert it and keep a one-shot backup of the original file. A value
25085
+ * the user already set is never overwritten, an unparseable file is never
25086
+ * touched, and every failure degrades to a no-op - the OSC and process-title
25087
+ * channels keep working everywhere else. */
25088
+ function ensureVsCodeTabTitleSetting(options = {}) {
25089
+ const env = options.env ?? process.env;
25090
+ if (env["TERM_PROGRAM"] !== "vscode") return {
25091
+ wrote: false,
25092
+ reason: "not-vscode"
25093
+ };
25094
+ if ((options.isTTY ?? process.stdout.isTTY) !== true) return {
25095
+ wrote: false,
25096
+ reason: "not-vscode"
25097
+ };
25098
+ let file = options.settingsFile;
25099
+ if (file === void 0) {
25100
+ const base = process.platform === "win32" ? env["APPDATA"] : env["HOME"];
25101
+ if (base === void 0 || base === "") return {
25102
+ wrote: false,
25103
+ reason: "error"
25104
+ };
25105
+ file = process.platform === "win32" ? join(base, "Code", "User", "settings.json") : join(base, ".config", "Code", "User", "settings.json");
25106
+ }
25107
+ try {
25108
+ if (!existsSync(file)) {
25109
+ mkdirSync(dirname(file), { recursive: true });
25110
+ writeFileSync(file, "{\n \"terminal.integrated.tabs.title\": \"${sequence}\"\n}\n", "utf8");
25111
+ return { wrote: true };
25112
+ }
25113
+ const text = readFileSync(file, "utf8");
25114
+ if (text.includes("\"terminal.integrated.tabs.title\"")) return {
25115
+ wrote: false,
25116
+ reason: "key-present"
25117
+ };
25118
+ try {
25119
+ JSON.parse(text);
25120
+ } catch {
25121
+ return {
25122
+ wrote: false,
25123
+ reason: "unparseable"
25124
+ };
25125
+ }
25126
+ const close = text.lastIndexOf("}");
25127
+ if (close < 0) return {
25128
+ wrote: false,
25129
+ reason: "unparseable"
25130
+ };
25131
+ const before = text.slice(0, close);
25132
+ const trimmedBefore = before.replace(/[ \t\r\n]+$/, "");
25133
+ const tail = before.slice(trimmedBefore.length);
25134
+ const inserted = (trimmedBefore.trim() !== "" && !trimmedBefore.trimEnd().endsWith("{") ? "," : "") + "\n \"terminal.integrated.tabs.title\": \"${sequence}\"\n";
25135
+ writeFileSync(file + ".dsh-backup", text, "utf8");
25136
+ writeFileSync(file, trimmedBefore + inserted + tail + text.slice(close), "utf8");
25137
+ return { wrote: true };
25138
+ } catch {
25139
+ return {
25140
+ wrote: false,
25141
+ reason: "error"
25142
+ };
25143
+ }
25144
+ }
25145
+ /**
25146
+ * Keep the terminal tab label on `title` (sanitized; empty titles leave the
25147
+ * current label alone). Two delivery channels run in parallel: the OSC 0
25148
+ * sequence to stdout, and the host process title. Writes are deduplicated by
25149
+ * title, and on unmount the managed title is cleared and the process title
25150
+ * restored so the host shell regains its default label.
25151
+ */
25152
+ function useTerminalTitle(title, options = {}) {
25153
+ const { clearOnUnmount = true } = options;
25154
+ const { stdout } = useStdout();
25155
+ const writtenRef = (0, import_react.useRef)(void 0);
25156
+ const processTitleRef = (0, import_react.useRef)(false);
25157
+ (0, import_react.useLayoutEffect)(() => {
25158
+ if (stdout === void 0) return void 0;
25159
+ return () => {
25160
+ if (processTitleRef.current) {
25161
+ processTitleRef.current = false;
25162
+ process.title = initialProcessTitle;
25163
+ }
25164
+ if (clearOnUnmount && writtenRef.current !== void 0) stdout.write(clearTerminalTitleSequence());
25165
+ };
25166
+ }, [stdout, clearOnUnmount]);
25167
+ (0, import_react.useLayoutEffect)(() => {
25168
+ if (stdout === void 0) return;
25169
+ if (title === writtenRef.current) return;
25170
+ const sequence = terminalTitleSequence(title);
25171
+ if (sequence === "") return;
25172
+ stdout.write(sequence);
25173
+ process.title = sanitizeTerminalTitle(title);
25174
+ processTitleRef.current = true;
25175
+ writtenRef.current = title;
25176
+ }, [stdout, title]);
25177
+ }
25178
+ //#endregion
24800
25179
  //#region src/render/tool-preview.ts
24801
25180
  /**
24802
25181
  * Bounded preview line for a tool invocation's raw JSON arguments: the first
@@ -25083,6 +25462,75 @@ function appendStreamingTail(current, delta) {
25083
25462
  const next = current + delta;
25084
25463
  return next.length <= MAX_STREAMING_CHARS ? next : next.slice(-65536);
25085
25464
  }
25465
+ /** Fold one `schedule/change` into the active-reminder list (create/delete/dispatch). */
25466
+ function applyScheduleChange(rows, data) {
25467
+ if (data.operation === "create" && data.schedule !== void 0) {
25468
+ const schedule = data.schedule;
25469
+ const row = {
25470
+ id: schedule.id,
25471
+ kind: schedule.kind,
25472
+ prompt: schedule.prompt,
25473
+ targetAt: Date.parse(schedule.scheduledAt),
25474
+ ...schedule.kind === "every" ? { everySeconds: schedule.everySeconds ?? 0 } : {}
25475
+ };
25476
+ return [...rows.filter((existing) => existing.id !== schedule.id), row];
25477
+ }
25478
+ if (data.operation === "delete" && data.id !== void 0) return rows.filter((existing) => existing.id !== data.id);
25479
+ if (data.operation === "dispatch" && data.id !== void 0) {
25480
+ if (data.acceptedAt === void 0) return rows.filter((existing) => existing.id !== data.id);
25481
+ const accepted = Date.parse(data.acceptedAt);
25482
+ return rows.map((existing) => existing.id === data.id ? {
25483
+ ...existing,
25484
+ targetAt: nextEveryTarget(existing.targetAt, accepted, existing.everySeconds ?? 0)
25485
+ } : existing);
25486
+ }
25487
+ return rows;
25488
+ }
25489
+ /** First anchor-aligned target after `acceptedAt`, stepping from the previous aligned target. */
25490
+ function nextEveryTarget(previousTarget, acceptedAt, everySeconds) {
25491
+ const interval = Math.max(1, everySeconds) * 1e3;
25492
+ if (acceptedAt <= previousTarget) return previousTarget + interval;
25493
+ return previousTarget + Math.ceil((acceptedAt - previousTarget + 1) / interval) * interval;
25494
+ }
25495
+ /** Plugin snapshot sources folded into token stats but never rendered as rows. */
25496
+ const HIDDEN_SNAPSHOT_PLUGINS = /* @__PURE__ */ new Set(["time-context", "tmux-context"]);
25497
+ /** Plugin prompt sources rendered as full user rows (they ARE the conversation). */
25498
+ const REMINDER_PLUGINS = /* @__PURE__ */ new Set(["schedule"]);
25499
+ /** Assemble the effective system prompt from surface nodes: head text plus every later non-empty node. */
25500
+ function assembleSystemPrompt(nodes) {
25501
+ if (nodes.size === 0) return "";
25502
+ return [...nodes.entries()].sort((left, right) => left[0] - right[0]).map(([, text]) => text).filter((text) => text !== "").join("\n\n");
25503
+ }
25504
+ /**
25505
+ * Apply one surface event's replace to the live system nodes. Any surface
25506
+ * event may shadow system nodes — the kernel's compaction summary lands as a
25507
+ * `user/message` replace whose range can cover later system nodes (only node
25508
+ * 0 is compaction-protected upstream) — so every surface fold retires covered
25509
+ * nodes, not just `system/message` itself.
25510
+ * @param nodes - the live system-node map (mutated when the event replaces).
25511
+ * @param surfaceOp - the surface operation the event carries, when it is a
25512
+ * surface event (log-only events have none and change nothing).
25513
+ * @returns the reassembled prompt when nodes were retired, `changed: false`
25514
+ * when the event shadows nothing.
25515
+ */
25516
+ function retireShadowedSystemNodes(nodes, surfaceOp) {
25517
+ if (surfaceOp === void 0 || surfaceOp === "append") return {
25518
+ prompt: "",
25519
+ changed: false
25520
+ };
25521
+ let changed = false;
25522
+ for (const seq of nodes.keys()) if (seq >= surfaceOp.startSeq && seq <= surfaceOp.endSeq) {
25523
+ nodes.delete(seq);
25524
+ changed = true;
25525
+ }
25526
+ return changed ? {
25527
+ prompt: assembleSystemPrompt(nodes),
25528
+ changed
25529
+ } : {
25530
+ prompt: "",
25531
+ changed: false
25532
+ };
25533
+ }
25086
25534
  /** Join the text blocks of a content list; non-text blocks contribute nothing. */
25087
25535
  function textOf(content) {
25088
25536
  return content.filter((block) => block.type === "text").map((block) => block.text).join("");
@@ -25091,6 +25539,10 @@ function textOf(content) {
25091
25539
  function imagesOf(content) {
25092
25540
  return content.filter((block) => block.type === "image").map((block) => block.attachment);
25093
25541
  }
25542
+ /** Durable file references in their model-visible order. */
25543
+ function filesOf(content) {
25544
+ return content.filter((block) => block.type === "file").map((block) => block.attachment);
25545
+ }
25094
25546
  /** Human-readable bounded image labels for transcript, inspector, and export surfaces. */
25095
25547
  function imageLabels(images) {
25096
25548
  if (images === void 0 || images.length === 0) return "";
@@ -25101,9 +25553,19 @@ function imageLabels(images) {
25101
25553
  return `[image: ${name} · ${original === void 0 ? `${image.width}×${image.height}` : `${image.width}×${image.height} · original ${original.width}×${original.height}`} · ${image.bytes} B]`;
25102
25554
  }).join("\n");
25103
25555
  }
25104
- /** Prompt text with its durable image labels, without exposing local paths or bytes. */
25556
+ /** Human-readable bounded file labels for the same surfaces (0.1.5 file blocks). */
25557
+ function fileLabels(files) {
25558
+ if (files === void 0 || files.length === 0) return "";
25559
+ return files.map((file, index) => {
25560
+ const rawName = file.name?.trim() || `file ${index + 1}`;
25561
+ return `[file: ${rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`} · ${file.bytes} B]`;
25562
+ }).join("\n");
25563
+ }
25564
+ /** Prompt text with its durable image and file labels, without exposing local paths or bytes. */
25105
25565
  function promptDisplayText(entry) {
25106
- const labels = imageLabels(entry.images);
25566
+ const imageText = imageLabels(entry.images);
25567
+ const fileText = fileLabels(entry.files);
25568
+ const labels = imageText === "" ? fileText : fileText === "" ? imageText : `${imageText}\n${fileText}`;
25107
25569
  return entry.text === "" ? labels : labels === "" ? entry.text : `${entry.text}\n${labels}`;
25108
25570
  }
25109
25571
  /** Join the reasoning blocks of a content list; non-reasoning blocks contribute nothing. */
@@ -25150,8 +25612,10 @@ function createReplayAccumulator() {
25150
25612
  plan: false,
25151
25613
  permission: "",
25152
25614
  title: "",
25615
+ systemPrompt: "",
25153
25616
  sandbox: "",
25154
25617
  goal: void 0,
25618
+ schedules: [],
25155
25619
  stats: {
25156
25620
  turns: 0,
25157
25621
  steps: 0,
@@ -25185,6 +25649,7 @@ function createReplayAccumulator() {
25185
25649
  turnFiles: /* @__PURE__ */ new Map(),
25186
25650
  turnSteps: /* @__PURE__ */ new Map(),
25187
25651
  turnTools: /* @__PURE__ */ new Map(),
25652
+ systemNodes: /* @__PURE__ */ new Map(),
25188
25653
  ops: 0
25189
25654
  };
25190
25655
  }
@@ -25267,6 +25732,17 @@ function retireReplayEntry(acc, index) {
25267
25732
  * like the copy-on-write reducer returning its input view unchanged.
25268
25733
  */
25269
25734
  function replayProjectEvent(acc, event) {
25735
+ const shadow = retireShadowedSystemNodes(acc.systemNodes, event.surfaceOp);
25736
+ if (shadow.changed) {
25737
+ acc.systemPrompt = shadow.prompt;
25738
+ acc.stats = {
25739
+ ...acc.stats,
25740
+ contextSegments: {
25741
+ ...acc.stats.contextSegments,
25742
+ system: estimateTokens(shadow.prompt)
25743
+ }
25744
+ };
25745
+ }
25270
25746
  switch (event.type) {
25271
25747
  case "user/message": {
25272
25748
  const message = event.data;
@@ -25285,12 +25761,14 @@ function replayProjectEvent(acc, event) {
25285
25761
  }
25286
25762
  const text = textOf(message.content);
25287
25763
  const images = imagesOf(message.content);
25288
- if (message.source.kind === "user") {
25764
+ const files = filesOf(message.content);
25765
+ if (message.source.kind === "user" || message.source.kind === "plugin" && REMINDER_PLUGINS.has(message.source.plugin)) {
25289
25766
  appendReplayEntry(acc, {
25290
25767
  kind: "user",
25291
25768
  text,
25292
25769
  notice: false,
25293
- ...images.length === 0 ? {} : { images }
25770
+ ...images.length === 0 ? {} : { images },
25771
+ ...files.length === 0 ? {} : { files }
25294
25772
  });
25295
25773
  acc.stats = {
25296
25774
  ...acc.stats,
@@ -25301,7 +25779,17 @@ function replayProjectEvent(acc, event) {
25301
25779
  };
25302
25780
  return true;
25303
25781
  }
25304
- const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
25782
+ if (message.source.kind === "plugin" && HIDDEN_SNAPSHOT_PLUGINS.has(message.source.plugin)) {
25783
+ acc.stats = {
25784
+ ...acc.stats,
25785
+ contextSegments: {
25786
+ ...acc.stats.contextSegments,
25787
+ system: acc.stats.contextSegments.system + estimateTokens(text)
25788
+ }
25789
+ };
25790
+ return true;
25791
+ }
25792
+ const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind === "plugin" ? message.source.plugin : message.source.kind;
25305
25793
  const summary = boundContextSummary(notice);
25306
25794
  appendReplayEntry(acc, {
25307
25795
  kind: "user",
@@ -25335,45 +25823,68 @@ function replayProjectEvent(acc, event) {
25335
25823
  ids.splice(start, 0, ...inserted.map((message) => message.id));
25336
25824
  for (const message of inserted) {
25337
25825
  const images = imagesOf(message.content);
25826
+ const files = filesOf(message.content);
25338
25827
  appendReplayEntry(acc, {
25339
25828
  kind: "pending",
25340
25829
  messageId: message.id,
25341
25830
  target,
25342
25831
  text: pendingText(message.content),
25343
- ...images.length === 0 ? {} : { images }
25832
+ ...images.length === 0 ? {} : { images },
25833
+ ...files.length === 0 ? {} : { files }
25344
25834
  });
25345
25835
  indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1);
25346
25836
  acc.ops += 1;
25347
25837
  }
25348
25838
  return true;
25349
25839
  }
25350
- case "assistant/chunk": {
25351
- const chunk = event.data.chunk;
25840
+ case "system/message": {
25841
+ const text = textOf(event.data.message.content);
25842
+ retireShadowedSystemNodes(acc.systemNodes, event.surfaceOp);
25843
+ acc.systemNodes.set(event.seq, text);
25844
+ acc.systemPrompt = assembleSystemPrompt(acc.systemNodes);
25845
+ acc.stats = {
25846
+ ...acc.stats,
25847
+ contextSegments: {
25848
+ ...acc.stats.contextSegments,
25849
+ system: estimateTokens(acc.systemPrompt)
25850
+ }
25851
+ };
25852
+ return true;
25853
+ }
25854
+ case "assistant/attempt": {
25352
25855
  const key = `${event.data.turn}:${event.data.step}`;
25353
- if ((chunk.type === "text-delta" || chunk.type === "reasoning-delta" ? chunk.text : "") !== "" && !acc.firstChunkAt.has(key)) {
25354
- acc.firstChunkAt.set(key, event.time);
25355
- const started = acc.stepStart.get(key);
25356
- if (started !== void 0) acc.stats = {
25357
- ...acc.stats,
25358
- ttftMs: acc.stats.ttftMs + Math.max(0, event.time - started),
25359
- ttftSteps: acc.stats.ttftSteps + 1
25360
- };
25361
- }
25362
- if (chunk.type === "text-delta") {
25363
- acc.streaming = appendStreamingTail(acc.streaming, chunk.text);
25364
- return true;
25365
- }
25366
- if (chunk.type === "reasoning-delta") {
25367
- acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
25368
- return true;
25856
+ let changed = false;
25857
+ if (!acc.firstChunkAt.has(key)) {
25858
+ const first = assistantStreamFirstTokenTime(event.data.stream ?? []);
25859
+ if (first !== void 0) {
25860
+ acc.firstChunkAt.set(key, first);
25861
+ const started = acc.stepStart.get(key);
25862
+ if (started !== void 0) acc.stats = {
25863
+ ...acc.stats,
25864
+ ttftMs: acc.stats.ttftMs + Math.max(0, first - started),
25865
+ ttftSteps: acc.stats.ttftSteps + 1
25866
+ };
25867
+ changed = true;
25868
+ }
25369
25869
  }
25370
- return false;
25870
+ const streamed = acc.streaming !== "" || acc.streamingReasoning !== "";
25871
+ acc.streaming = "";
25872
+ acc.streamingReasoning = "";
25873
+ return changed || streamed;
25371
25874
  }
25372
25875
  case "assistant/message": {
25373
25876
  const key = `${event.data.turn}:${event.data.step}`;
25374
25877
  const started = acc.stepStart.get(key);
25375
25878
  acc.stepStart.delete(key);
25376
- const firstChunk = acc.firstChunkAt.get(key);
25879
+ let firstChunk = acc.firstChunkAt.get(key);
25880
+ if (firstChunk === void 0) {
25881
+ firstChunk = assistantStreamFirstTokenTime(event.data.stream ?? []);
25882
+ if (firstChunk !== void 0 && started !== void 0) acc.stats = {
25883
+ ...acc.stats,
25884
+ ttftMs: acc.stats.ttftMs + Math.max(0, firstChunk - started),
25885
+ ttftSteps: acc.stats.ttftSteps + 1
25886
+ };
25887
+ }
25377
25888
  acc.firstChunkAt.delete(key);
25378
25889
  if (acc.turnSteps.get(event.data.turn) === key) acc.turnSteps.delete(event.data.turn);
25379
25890
  const usage = event.data.usage;
@@ -25596,6 +26107,10 @@ function replayProjectEvent(acc, event) {
25596
26107
  });
25597
26108
  return true;
25598
26109
  }
26110
+ case "schedule/change":
26111
+ acc.schedules = applyScheduleChange(acc.schedules, event.data);
26112
+ acc.ops += 1;
26113
+ return true;
25599
26114
  case "session/title":
25600
26115
  acc.title = event.data.title;
25601
26116
  return true;
@@ -25632,11 +26147,7 @@ function replayProjectEvent(acc, event) {
25632
26147
  acc.model = `${config.provider}/${config.model}`;
25633
26148
  acc.stats = {
25634
26149
  ...acc.stats,
25635
- reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort),
25636
- contextSegments: {
25637
- ...acc.stats.contextSegments,
25638
- system: estimateTokens(event.data.header.system ?? "")
25639
- }
26150
+ reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort)
25640
26151
  };
25641
26152
  return true;
25642
26153
  }
@@ -25701,8 +26212,10 @@ function materializeReplayView(acc, copy) {
25701
26212
  plan: acc.plan,
25702
26213
  permission: acc.permission,
25703
26214
  title: acc.title,
26215
+ systemPrompt: acc.systemPrompt,
25704
26216
  sandbox: acc.sandbox,
25705
26217
  goal: acc.goal,
26218
+ schedules: acc.schedules,
25706
26219
  pending: {
25707
26220
  "next-turn": [...acc.pendingTurn],
25708
26221
  "next-step": [...acc.pendingStep]
@@ -25716,11 +26229,58 @@ function materializeReplayView(acc, copy) {
25716
26229
  lastPruneTokens: acc.lastPruneTokens,
25717
26230
  turnFiles: new Map([...acc.turnFiles].map(([turn, files]) => [turn, new Set(files)])),
25718
26231
  turnSteps: new Map(acc.turnSteps),
25719
- turnTools: new Map([...acc.turnTools].map(([turn, tools]) => [turn, new Set(tools)]))
26232
+ turnTools: new Map([...acc.turnTools].map(([turn, tools]) => [turn, new Set(tools)])),
26233
+ systemNodes: new Map(acc.systemNodes)
25720
26234
  }
25721
26235
  };
25722
26236
  }
25723
26237
  /**
26238
+ * Fold one process-local assistant-stream chunk frame (session-log v2+ keeps
26239
+ * durable logs settlement-only; live typing rides the `agent/assistant-stream`
26240
+ * agent event). Same first-token anchoring the durable `assistant/chunk` event
26241
+ * used to carry: the first non-empty delta anchors the TTFT and empty
26242
+ * keep-alive deltas do not count. The caller maps the frame's attempt to the
26243
+ * `turn:step` key (the start frame owns turn/step; chunk frames do not).
26244
+ * @param acc - the live replay accumulator.
26245
+ * @param key - the `turn:step` key the attempt's start frame declared.
26246
+ * @param time - the frame's safe-integer timestamp.
26247
+ * @param chunk - the model chunk the frame carries.
26248
+ * @returns whether the accumulator changed (the store stays silent otherwise).
26249
+ */
26250
+ function applyAssistantStreamChunk(acc, key, time, chunk) {
26251
+ if (isTokenDelta(chunk) && !acc.firstChunkAt.has(key)) {
26252
+ acc.firstChunkAt.set(key, time);
26253
+ const started = acc.stepStart.get(key);
26254
+ if (started !== void 0) acc.stats = {
26255
+ ...acc.stats,
26256
+ ttftMs: acc.stats.ttftMs + Math.max(0, time - started),
26257
+ ttftSteps: acc.stats.ttftSteps + 1
26258
+ };
26259
+ }
26260
+ if (chunk.type === "text-delta") {
26261
+ acc.streaming = appendStreamingTail(acc.streaming, chunk.text);
26262
+ return true;
26263
+ }
26264
+ if (chunk.type === "reasoning-delta") {
26265
+ acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
26266
+ return true;
26267
+ }
26268
+ return false;
26269
+ }
26270
+ /**
26271
+ * Drop the live streaming tails without a settlement (an `agent/assistant-stream`
26272
+ * end frame with an `abandoned` outcome, or a session switch). The next start
26273
+ * frame rebuilds from scratch.
26274
+ * @param acc - the live replay accumulator.
26275
+ * @returns whether any tail text was discarded.
26276
+ */
26277
+ function clearAssistantStream(acc) {
26278
+ const streamed = acc.streaming !== "" || acc.streamingReasoning !== "";
26279
+ acc.streaming = "";
26280
+ acc.streamingReasoning = "";
26281
+ return streamed;
26282
+ }
26283
+ /**
25724
26284
  * The append-only flush boundary for a transcript view: the count of entries
25725
26285
  * no later event can remove. Entries at or beyond this index are mutable and
25726
26286
  * must stay in the live tree.
@@ -25748,6 +26308,132 @@ function settledEntryCount(entries) {
25748
26308
  return entries.length;
25749
26309
  }
25750
26310
  //#endregion
26311
+ //#region src/render/ime-cursor.ts
26312
+ /**
26313
+ * IME cursor anchoring for the composer.
26314
+ *
26315
+ * Ink keeps the real terminal cursor hidden and parks it just below the
26316
+ * dynamic tree (after the status row). IME composition text and candidate
26317
+ * windows anchor to that real cursor cell - VS Code's integrated terminal
26318
+ * positions its hidden IME textarea there, and Windows consoles behave the
26319
+ * same - so CJK input appeared at the bottom of the screen instead of at the
26320
+ * caret. The anchor moves the real cursor onto the caret cell without
26321
+ * touching Ink's relative erase ledger:
26322
+ *
26323
+ * - the displacement is owned: before any foreign write or re-anchor, the
26324
+ * wrapper cancels it (cursor down, column 1), so every writer - Ink's
26325
+ * log-update rewrites, the resize replay, protocol pushes - keeps seeing
26326
+ * the cursor exactly where it was left;
26327
+ * - log-update frame chunks (which start with the erase-line sequence) get
26328
+ * the anchor re-appended inside the same write, so a repaint can never
26329
+ * leave the cursor behind - in particular the caret blink keeps the anchor
26330
+ * stable while an IME composition is open, because the terminal parses the
26331
+ * rewrite and the re-anchor as one atomic update.
26332
+ *
26333
+ * @module @deepseek-ai/dsh-code/render/ime-cursor
26334
+ */
26335
+ /** Cancel `rows` of owned upward displacement and return to column 1. */
26336
+ function imeCursorRestore(rows) {
26337
+ return rows > 0 ? `\x1b[${rows}B\r` : "";
26338
+ }
26339
+ /** Move onto the caret cell: `rows` up from Ink's parked row, 0-based column. */
26340
+ function imeCursorMove(rows, column) {
26341
+ return rows > 0 ? `\x1b[${rows}A\x1b[${column + 1}G` : "";
26342
+ }
26343
+ /**
26344
+ * Rows between the caret cell and Ink's parked cursor row: the band's bottom
26345
+ * blank row, the editor window rows below the caret, the status footer, and
26346
+ * Ink's own below-frame row. Rows above the composer (gutter, live content)
26347
+ * never enter this distance.
26348
+ */
26349
+ function imeCursorRowsUp(input) {
26350
+ return Math.max(1, input.editorWindowRows - input.caretRowInWindow + input.rowsBelowComposer);
26351
+ }
26352
+ /** log-update chunks start with the erase-line sequence; a chunk without it
26353
+ * leaves no frame behind, so the next commit re-applies the anchor. */
26354
+ const FRAME_CHUNK_MARKER = "\x1B[2K";
26355
+ const ANCHOR_STATE = Symbol.for("dsh-code.ime-cursor-anchor");
26356
+ /**
26357
+ * Take over `stream.write` so the anchor displacement stays invisible to every
26358
+ * other writer. Idempotent per stream: a second install returns the live
26359
+ * handle. Returns `undefined` on non-TTY streams where anchoring is meaningless.
26360
+ */
26361
+ function installImeCursorAnchor(stream) {
26362
+ if (stream?.isTTY !== true) return void 0;
26363
+ const target = stream;
26364
+ const installed = target[ANCHOR_STATE];
26365
+ if (installed !== void 0) return installed;
26366
+ const originalWrite = target.write.bind(target);
26367
+ let rows = 0;
26368
+ let column = -1;
26369
+ let detached = false;
26370
+ const anchor = {
26371
+ anchor(nextRows, nextColumn) {
26372
+ if (detached) return;
26373
+ if (nextRows <= 0) {
26374
+ if (rows !== 0) originalWrite(imeCursorRestore(rows));
26375
+ rows = 0;
26376
+ column = -1;
26377
+ return;
26378
+ }
26379
+ if (rows === nextRows && column === nextColumn) return;
26380
+ originalWrite(imeCursorRestore(rows) + imeCursorMove(nextRows, nextColumn));
26381
+ rows = nextRows;
26382
+ column = nextColumn;
26383
+ },
26384
+ release() {
26385
+ if (detached) return;
26386
+ detached = true;
26387
+ if (rows !== 0) originalWrite(imeCursorRestore(rows));
26388
+ rows = 0;
26389
+ column = -1;
26390
+ target.write = originalWrite;
26391
+ delete target[ANCHOR_STATE];
26392
+ }
26393
+ };
26394
+ target.write = ((chunk, ...rest) => {
26395
+ const ownedRows = rows;
26396
+ const ownedColumn = column;
26397
+ if (ownedRows === 0 || typeof chunk !== "string") {
26398
+ if (ownedRows !== 0) {
26399
+ originalWrite(imeCursorRestore(ownedRows));
26400
+ rows = 0;
26401
+ column = -1;
26402
+ }
26403
+ return originalWrite(chunk, ...rest);
26404
+ }
26405
+ const reanchor = chunk.startsWith(FRAME_CHUNK_MARKER);
26406
+ rows = reanchor ? ownedRows : 0;
26407
+ column = reanchor ? ownedColumn : -1;
26408
+ return originalWrite(imeCursorRestore(ownedRows) + chunk + (reanchor ? imeCursorMove(ownedRows, ownedColumn) : ""), ...rest);
26409
+ });
26410
+ target[ANCHOR_STATE] = anchor;
26411
+ return anchor;
26412
+ }
26413
+ /**
26414
+ * Keep the real terminal cursor on the composer's caret cell while `active`
26415
+ * (the editable composer). The second effect runs after every commit without
26416
+ * a dep list: frame rewrites restore the anchor themselves, but any other
26417
+ * write (protocol push, resize replay) leaves the cursor at Ink's parked
26418
+ * position, and the next commit re-anchors it.
26419
+ */
26420
+ function useImeCursorAnchor(active, rows, column) {
26421
+ const { stdout } = useStdout();
26422
+ const anchorRef = (0, import_react.useRef)(void 0);
26423
+ (0, import_react.useEffect)(() => {
26424
+ if (stdout === void 0) return void 0;
26425
+ const installed = installImeCursorAnchor(stdout);
26426
+ anchorRef.current = installed;
26427
+ return () => {
26428
+ installed?.release();
26429
+ if (anchorRef.current === installed) anchorRef.current = void 0;
26430
+ };
26431
+ }, [stdout]);
26432
+ (0, import_react.useEffect)(() => {
26433
+ anchorRef.current?.anchor(active && rows > 0 ? rows : 0, column);
26434
+ });
26435
+ }
26436
+ //#endregion
25751
26437
  //#region src/render/markdown.ts
25752
26438
  /**
25753
26439
  * Terminal markdown renderer for assistant replies: a pure GFM-subset
@@ -26365,8 +27051,60 @@ const DEEPSEEK_WAVE_STYLES = [
26365
27051
  "aurora",
26366
27052
  "pulse"
26367
27053
  ];
26368
- /** Pulse ring half-width in columns — Codex PULSE_HALF_WIDTH (4.5). */
26369
- const PULSE_HALF_WIDTH = 4.5;
27054
+ /**
27055
+ * The water surface: ONE continuous sine line spanning the whole band,
27056
+ * mirror-symmetric about the center column, its crests flowing OUTWARD from
27057
+ * the center (phase k·|x − center| − ω·t). No sweep window, no return trip —
27058
+ * the surface fades in, flows, and fades out, symmetric in both space and
27059
+ * time. The deepseek tier adds one faster, finer HARMONIC line whose crests
27060
+ * cross the fundamental's: interleaved richness with both lines still
27061
+ * symmetric and still only ever flowing outward.
27062
+ */
27063
+ const WAVE_SURFACE_AMPLITUDE = .8;
27064
+ const WAVE_SURFACE_HARMONIC = .45;
27065
+ /** Vertical thickness in lane units — Aurora-wide: soft gradients, no hard edges. */
27066
+ const WAVE_SURFACE_THICKNESS = 1.2;
27067
+ /**
27068
+ * The mirrored second-hue profile: the space BELOW the surface carries a
27069
+ * second blue at this strength, so color (not just brightness) varies
27070
+ * continuously across the wave — Aurora-style hue mixing instead of a
27071
+ * single flat tint.
27072
+ */
27073
+ const WAVE_SURFACE_MIRROR = .6;
27074
+ /** Aurora-style soft alpha: low gain, capped well under the pulse ring's. */
27075
+ const WAVE_SURFACE_ALPHA_GAIN = .45;
27076
+ const WAVE_SURFACE_ALPHA_CAP = .68;
27077
+ /**
27078
+ * Pulse ring geometry, softened to the Wave standard: a WIDE band
27079
+ * (half-width 5.5) with a moderate peak riding the radius — all inside the
27080
+ * hue blend, no hard white line — and an inner profile one hue over at a
27081
+ * slightly smaller radius, so the ring's color grades continuously across
27082
+ * its width (the radial analog of the water surface's mirrored hues).
27083
+ */
27084
+ const PULSE_HALF_WIDTH = 5.5;
27085
+ const PULSE_PEAK_HALF_WIDTH = 1.8;
27086
+ const PULSE_PEAK_GAIN = .35;
27087
+ const PULSE_INNER_OFFSET = 2.5;
27088
+ const PULSE_INNER_STRENGTH = .6;
27089
+ /** The inner edge of each ring carries the tier's third blue. */
27090
+ const PULSE_INNER_HUE = 2;
27091
+ /**
27092
+ * The trailing echo ripple: every pulse ring drags a second, weaker ring at
27093
+ * a fraction of its radius in the NEXT hue of the tier's blues, fading in a
27094
+ * little after the primary so the center hole opens first.
27095
+ */
27096
+ const PULSE_ECHO_RADIUS = .7;
27097
+ const PULSE_ECHO_STRENGTH = .65;
27098
+ const PULSE_ECHO_DELAY = .12;
27099
+ /** Aurora-grade soft alpha for the detonation — a notch above the swell. */
27100
+ const PULSE_ALPHA_GAIN = .45;
27101
+ const PULSE_ALPHA_CAP = .72;
27102
+ /**
27103
+ * Terminal cell aspect (row height ÷ column width, ≈2.2 for common fonts).
27104
+ * A ring computed in raw cell units looks vertically squashed; weighting row
27105
+ * distance by the aspect makes the Pulse ring appear circular on screen.
27106
+ */
27107
+ const PULSE_ROW_ASPECT = 2.2;
26370
27108
  /** Sparkle start and frame cadence — Codex SPARK_START / SPARK_FRAME. */
26371
27109
  const SPARK_START_MS = 900;
26372
27110
  const SPARK_FRAME_MS = 100;
@@ -26510,14 +27248,17 @@ function deepseekWaveStyleRandom(previous) {
26510
27248
  return candidates[Math.floor(Math.random() * candidates.length)] ?? "wave";
26511
27249
  }
26512
27250
  /**
26513
- * Tier for a `provider/model` label: a model id containing `flash` runs the
27251
+ * Tier for a `provider/model` label: a MODEL ID containing `flash` runs the
26514
27252
  * single-band flash tier; everything else (pro/reasoner/chat) runs the
26515
- * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
27253
+ * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping. Only the model
27254
+ * segment (after the `/`) is matched, so a provider whose name contains
27255
+ * `flash` cannot flip an unrelated model onto the flash tier.
26516
27256
  * @param model - the `provider/model` label of the applied model.
26517
27257
  * @returns the wave tier for that model.
26518
27258
  */
26519
27259
  function deepseekWaveTier(model) {
26520
- return model.toLowerCase().includes("flash") ? "flash" : "deepseek";
27260
+ const slash = model.indexOf("/");
27261
+ return (slash < 0 ? model : model.slice(slash + 1)).toLowerCase().includes("flash") ? "flash" : "deepseek";
26521
27262
  }
26522
27263
  /**
26523
27264
  * Cosine window — Codex `crest`: 1 exactly under the wave center, 0 from
@@ -26530,18 +27271,6 @@ function crest(distance) {
26530
27271
  return .5 * (1 + Math.cos(Math.PI * distance));
26531
27272
  }
26532
27273
  /**
26533
- * Cubic ease-in-out — Codex `ease_in_out`: flat at both ends, steepest in
26534
- * the middle, so the crest accelerates and eases instead of sliding linearly.
26535
- * @param progress - raw progress (clamped to 0..1).
26536
- * @returns the eased progress in 0..1.
26537
- */
26538
- function easeInOut(progress) {
26539
- const p = Math.min(1, Math.max(0, progress));
26540
- if (p < .5) return 4 * p * p * p;
26541
- const inverse = -2 * p + 2;
26542
- return 1 - inverse * inverse * inverse / 2;
26543
- }
26544
- /**
26545
27274
  * Fade-in/fade-out envelope — Codex `envelope`: linear ramp over `fadeIn`
26546
27275
  * at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
26547
27276
  * total. The Wave style keeps the envelope at 1 (Codex paints Wave without
@@ -26559,38 +27288,98 @@ function envelope(elapsed, total, fadeIn, fadeOut) {
26559
27288
  return Math.min(Math.max(Math.min(rise, fall), 0), 1);
26560
27289
  }
26561
27290
  /**
26562
- * One band's contribution at a column — Codex `band_sample`, all three
26563
- * branches: Wave sweeps an eased crest across the row; Aurora drifts a
26564
- * sinusoidal center carrying a hue index; Pulse expands a ring from the row
26565
- * center with cubic ease and decaying strength.
27291
+ * One band's contributions at a column — Codex `band_sample`, redesigned:
27292
+ * Wave is a WATER SURFACE one continuous sine line, mirror-symmetric
27293
+ * about the center column, crests flowing outward from the center (band 1
27294
+ * of the deepseek tier is a faster harmonic line crossing it). Pulse
27295
+ * detonates in TWO dimensions: soft rings that keep expanding through a
27296
+ * symmetric fade envelope, color grading across each ring's width, with a
27297
+ * trailing echo ripple. Aurora matches Codex verbatim.
26566
27298
  * @param style - the ignition style.
26567
27299
  * @param band - the band triple (meaning depends on the style).
26568
27300
  * @param elapsed - seconds since the animation started.
26569
27301
  * @param column - column index in the content row (0..width-1).
26570
27302
  * @param width - content-row width in columns.
26571
- * @returns `[hueIndex, strength]`.
27303
+ * @param context - band geometry (row position, undulation flag, pulse span).
27304
+ * @returns one or two `[hueIndex, strength, core]` contributions.
26572
27305
  */
26573
- function bandSample(style, band, elapsed, column, width) {
27306
+ function bandSample(style, band, elapsed, column, width, context) {
26574
27307
  const [first, second, third] = band;
26575
27308
  switch (style) {
26576
27309
  case "wave": {
26577
- const progress = (elapsed - first) / second;
26578
- if (progress < 0 || progress > 1) return [0, 0];
26579
- const center = easeInOut(progress) * (width + 18) - 9;
26580
- return [0, crest(Math.abs(column - center) / 9)];
27310
+ const fadeIn = first;
27311
+ const fadeOut = Math.max(.05, context.total - (first + second));
27312
+ const fade = envelope(elapsed, context.total, fadeIn, fadeOut);
27313
+ if (fade <= .01) return [[
27314
+ 0,
27315
+ 0,
27316
+ 0
27317
+ ]];
27318
+ const harmonic = context.bandIndex % 2 === 1;
27319
+ const wavelength = harmonic ? 40 / 1.5 : 40;
27320
+ const omega = (harmonic ? 1.5 : 1) * 9;
27321
+ const amplitude = (harmonic ? WAVE_SURFACE_HARMONIC : 1) * WAVE_SURFACE_AMPLITUDE;
27322
+ const d = Math.abs(column - (width - 1) / 2);
27323
+ const surface = amplitude * Math.sin(Math.PI * 2 * d / wavelength - omega * elapsed + (harmonic ? Math.PI / 2 : 0));
27324
+ const fromSurface = Math.abs(context.u - surface);
27325
+ const vertical = context.undulating ? crest(fromSurface / WAVE_SURFACE_THICKNESS) : 1;
27326
+ const mirrorHue = harmonic ? 1 : 2;
27327
+ const below = context.undulating ? crest(Math.abs(context.u + surface) / WAVE_SURFACE_THICKNESS) : vertical;
27328
+ return [[
27329
+ 0,
27330
+ fade * vertical,
27331
+ 0
27332
+ ], [
27333
+ mirrorHue,
27334
+ fade * below * WAVE_SURFACE_MIRROR,
27335
+ 0
27336
+ ]];
26581
27337
  }
26582
27338
  case "aurora": {
26583
27339
  const center = (.5 + .38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width;
26584
27340
  const halfWidth = Math.max(width * .22, 4);
26585
- return [Math.trunc(third), crest(Math.abs(column - center) / halfWidth)];
27341
+ return [[
27342
+ Math.trunc(third),
27343
+ crest(Math.abs(column - center) / halfWidth),
27344
+ 0
27345
+ ]];
26586
27346
  }
26587
27347
  case "pulse": {
26588
- const progress = (elapsed - first) / second;
26589
- if (progress < 0 || progress > 1) return [0, 0];
26590
- const inverse = 1 - progress;
26591
- const radius = (1 - inverse * inverse * inverse) * (width / 2 + 2 * PULSE_HALF_WIDTH);
26592
- const distance = Math.abs(column - width / 2);
26593
- return [0, crest(Math.abs(distance - radius) / PULSE_HALF_WIDTH) * third * (1 - .6 * progress)];
27348
+ const launch = first;
27349
+ const travel = second;
27350
+ const fadeOut = Math.max(.05, context.total - (launch + travel));
27351
+ const fade = envelope(elapsed, context.total, launch, fadeOut);
27352
+ if (fade <= .01) return [[
27353
+ 0,
27354
+ 0,
27355
+ 0
27356
+ ]];
27357
+ const progress = (elapsed - launch) / travel;
27358
+ const radius = (1 - (1 - progress) ** 3) * context.pulseSpan;
27359
+ const decay = third * (1 - .35 * Math.min(Math.max(progress, 0), 1));
27360
+ const distance = Math.hypot(column - width / 2, context.dy);
27361
+ const fromRing = Math.abs(distance - radius);
27362
+ const band = crest(fromRing / PULSE_HALF_WIDTH) + PULSE_PEAK_GAIN * crest(fromRing / PULSE_PEAK_HALF_WIDTH);
27363
+ const inner = crest(Math.abs(distance - (radius - PULSE_INNER_OFFSET)) / PULSE_HALF_WIDTH);
27364
+ const echoGate = envelope(elapsed, context.total, launch + PULSE_ECHO_DELAY, fadeOut);
27365
+ const echo = crest(Math.abs(distance - radius * PULSE_ECHO_RADIUS) / PULSE_HALF_WIDTH) * PULSE_ECHO_STRENGTH * echoGate;
27366
+ return [
27367
+ [
27368
+ context.bandIndex,
27369
+ fade * band * decay,
27370
+ 0
27371
+ ],
27372
+ [
27373
+ PULSE_INNER_HUE,
27374
+ fade * inner * decay * PULSE_INNER_STRENGTH,
27375
+ 0
27376
+ ],
27377
+ [
27378
+ context.bandIndex + 1,
27379
+ fade * echo * decay,
27380
+ 0
27381
+ ]
27382
+ ];
26594
27383
  }
26595
27384
  }
26596
27385
  }
@@ -26603,9 +27392,10 @@ function blendRgb(fg, bg, alpha) {
26603
27392
  ];
26604
27393
  }
26605
27394
  /**
26606
- * Per-row phase share of the duration: the crest reaches the top row first
26607
- * and the bottom row last, sweeping down the band. 0.12 keeps the bottom
26608
- * row's lag inside the 200ms duration extension.
27395
+ * Aurora-only per-row phase share of the duration: its drifting bands reach
27396
+ * the top row first and the bottom row last, sweeping down the band. 0.12
27397
+ * keeps the bottom row's lag inside the 200ms duration extension. Wave and
27398
+ * Pulse deliberately share one timeline (see `deepseekWaveColumnBg`).
26609
27399
  */
26610
27400
  const DEEPSEEK_WAVE_ROW_PHASE = .12;
26611
27401
  /**
@@ -26616,9 +27406,13 @@ const DEEPSEEK_WAVE_ROW_PHASE = .12;
26616
27406
  * blends the mixed hue toward the blank-cell base at the style's alpha cap,
26617
27407
  * and Aurora applies its own fade envelope. Returns `null` when the column
26618
27408
  * should stay transparent, so the row returns to no `backgroundColor` on
26619
- * both ends. With `rows > 1` each row samples the same timeline shifted by a
26620
- * per-row phase offset, so the crest cascades down the band instead of
26621
- * painting every row identically.
27409
+ * both ends. With `rows > 1`: Wave is a water surface every column
27410
+ * lights the row nearest the surface's current height, so the light reads
27411
+ * as ONE continuous wavy line spanning the band, symmetric about the center
27412
+ * column and flowing outward (a single-row band falls back to a flat glow);
27413
+ * Pulse rings in two dimensions around the band's center cell with trailing
27414
+ * echo ripples; only Aurora samples the timeline shifted by a per-row phase
27415
+ * offset.
26622
27416
  * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
26623
27417
  * @param column - column index in the content row (0..width-1).
26624
27418
  * @param width - content-row width in columns.
@@ -26632,16 +27426,28 @@ const DEEPSEEK_WAVE_ROW_PHASE = .12;
26632
27426
  */
26633
27427
  function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row = 0, rows = 1) {
26634
27428
  const total = deepseekWaveBaseDuration(tier, style) / 1e3;
26635
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3 - (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE;
27429
+ const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3 - (style === "aurora" ? (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE : 0);
27430
+ const dy = style === "pulse" ? (row - (rows - 1) / 2) * PULSE_ROW_ASPECT : 0;
27431
+ const undulating = rows > 1;
27432
+ const u = undulating ? (row - (rows - 1) / 2) / ((rows - 1) / 2) : 0;
27433
+ const pulseSpan = Math.hypot(width / 2, (rows - 1) / 2 * PULSE_ROW_ASPECT);
26636
27434
  const fade = style === "aurora" ? envelope(elapsed, total, .25, .4) : 1;
26637
27435
  const weights = [
26638
27436
  0,
26639
27437
  0,
26640
27438
  0
26641
27439
  ];
27440
+ let bandIndex = 0;
26642
27441
  for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
26643
- const [hue, strength] = bandSample(style, band, elapsed, column, width);
26644
- weights[hue] = style === "aurora" ? weights[hue] + strength : Math.max(weights[hue], strength);
27442
+ for (const [hue, strength] of bandSample(style, band, elapsed, column, width, {
27443
+ dy,
27444
+ u,
27445
+ undulating,
27446
+ pulseSpan,
27447
+ bandIndex,
27448
+ total
27449
+ })) weights[hue] = style === "aurora" ? weights[hue] + strength : Math.max(weights[hue], strength);
27450
+ bandIndex += 1;
26645
27451
  }
26646
27452
  const weight = weights[0] + weights[1] + weights[2];
26647
27453
  if (weight <= .01) return null;
@@ -26658,7 +27464,7 @@ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row
26658
27464
  Math.round(green / weight),
26659
27465
  Math.round(blue / weight)
26660
27466
  ];
26661
- const alpha = style === "aurora" ? Math.min(weight * .4, .5) * fade : weight * .55;
27467
+ const alpha = style === "aurora" ? Math.min(weight * .4, .5) * fade : style === "wave" ? Math.min(weight * WAVE_SURFACE_ALPHA_GAIN, WAVE_SURFACE_ALPHA_CAP) : Math.min(weight * PULSE_ALPHA_GAIN, PULSE_ALPHA_CAP);
26662
27468
  if (alpha < .02) return null;
26663
27469
  return blendRgb(mixed, base, alpha);
26664
27470
  }
@@ -26743,6 +27549,32 @@ function effortAboveHigh(effort) {
26743
27549
  const rank = EFFORT_RANK[effort.trim().toLowerCase()];
26744
27550
  return rank !== void 0 && rank > 3;
26745
27551
  }
27552
+ /**
27553
+ * Parse a persisted animations preference (`animations.json`): timed
27554
+ * animations are on by default and only an explicit `false` disables them —
27555
+ * a missing key, corrupt value, or absent file all mean enabled, so the
27556
+ * /animation toggle degrades exactly like every other user preference.
27557
+ * @param value - the raw parsed JSON value (expected boolean).
27558
+ * @returns whether timed animations should run.
27559
+ */
27560
+ function parseAnimationsPref(value) {
27561
+ return value !== false;
27562
+ }
27563
+ /**
27564
+ * One parsed `/animation` argument: '' toggles, `on|true|1` enables,
27565
+ * `off|false|0` disables (case-insensitive, surrounding whitespace ignored),
27566
+ * and anything else is a usage error the caller surfaces. Kept pure so the
27567
+ * command's entire decision table is unit-testable.
27568
+ * @param argument - the raw text after `/animation`.
27569
+ * @returns `{ enabled }`, `'toggle'`, or `'usage'`.
27570
+ */
27571
+ function parseAnimationsArgument(argument) {
27572
+ const normalized = argument.trim().toLowerCase();
27573
+ if (normalized === "") return "toggle";
27574
+ if (normalized === "on" || normalized === "true" || normalized === "1") return { enabled: true };
27575
+ if (normalized === "off" || normalized === "false" || normalized === "0") return { enabled: false };
27576
+ return "usage";
27577
+ }
26746
27578
  //#endregion
26747
27579
  //#region src/commands.ts
26748
27580
  /**
@@ -26761,17 +27593,41 @@ function watchCommands(ctx) {
26761
27593
  let error;
26762
27594
  let loadedFor;
26763
27595
  const listeners = /* @__PURE__ */ new Set();
27596
+ const descriptorFingerprint = (list) => JSON.stringify(list.map((descriptor) => [
27597
+ descriptor.name,
27598
+ descriptor.description,
27599
+ descriptor.input?.hint ?? "",
27600
+ descriptor.input?.attachments === true
27601
+ ]));
27602
+ let lastFingerprint = "[]";
27603
+ let lastNotifiedError;
27604
+ const changed = (next, nextError) => descriptorFingerprint(next) !== lastFingerprint || nextError !== lastNotifiedError;
27605
+ let notifyScheduled = false;
27606
+ const notify = () => {
27607
+ if (notifyScheduled) return;
27608
+ notifyScheduled = true;
27609
+ setImmediate(() => {
27610
+ notifyScheduled = false;
27611
+ for (const listener of listeners) listener();
27612
+ });
27613
+ };
26764
27614
  const refresh = () => {
26765
27615
  if (commands === void 0 || agent === void 0) return;
27616
+ let next;
27617
+ let nextError;
26766
27618
  try {
26767
- descriptors = commands.list(agent);
27619
+ next = commands.list(agent);
26768
27620
  loadedFor = agent;
26769
- error = void 0;
26770
27621
  } catch (cause) {
26771
- descriptors = loadedFor === agent ? [...descriptors] : [];
26772
- error = cause instanceof Error ? cause.message : String(cause);
27622
+ next = loadedFor === agent ? [...descriptors] : [];
27623
+ nextError = cause instanceof Error ? cause.message : String(cause);
26773
27624
  }
26774
- for (const listener of listeners) listener();
27625
+ if (!changed(next, nextError)) return;
27626
+ descriptors = next;
27627
+ error = nextError;
27628
+ lastFingerprint = descriptorFingerprint(next);
27629
+ lastNotifiedError = nextError;
27630
+ notify();
26775
27631
  };
26776
27632
  if (commands !== void 0) ctx.on("commands/change", () => refresh());
26777
27633
  return {
@@ -26819,6 +27675,70 @@ function submissionPayload(line) {
26819
27675
  return isSlashLine(trimmed) ? trimmed : withoutTrailingNewlines;
26820
27676
  }
26821
27677
  //#endregion
27678
+ //#region src/render/fuzzy.ts
27679
+ /** Extra weight for name starts and separator boundaries. */
27680
+ function boundaryBonus(name, index) {
27681
+ return index === 0 || name.charAt(index - 1) === "-" || name.charAt(index - 1) === "_" ? 8 : 0;
27682
+ }
27683
+ /**
27684
+ * Score the strongest ordered-subsequence alignment in O(name × query).
27685
+ * Boundary and adjacent matches earn weight; skipped and leading characters
27686
+ * cost weight. Undefined when the query is not a subsequence of the name.
27687
+ */
27688
+ function alignmentScore(name, query) {
27689
+ if (query.length > name.length) return void 0;
27690
+ const noMatch = Number.NEGATIVE_INFINITY;
27691
+ let previous = Array(name.length).fill(noMatch);
27692
+ for (let index = 0; index < name.length; index++) if (name.charAt(index) === query.charAt(0)) previous[index] = 1 + boundaryBonus(name, index) - index;
27693
+ for (let queryIndex = 1; queryIndex < query.length; queryIndex++) {
27694
+ const current = Array(name.length).fill(noMatch);
27695
+ let left = noMatch;
27696
+ let leftLeft = noMatch;
27697
+ let bestGapped = noMatch;
27698
+ for (const [index, prior] of previous.entries()) {
27699
+ if (leftLeft !== noMatch) bestGapped = Math.max(bestGapped, leftLeft + index - 2);
27700
+ if (name.charAt(index) === query.charAt(queryIndex)) {
27701
+ const bonus = 1 + boundaryBonus(name, index);
27702
+ let score = noMatch;
27703
+ if (left !== noMatch) score = left + bonus + 4;
27704
+ if (bestGapped !== noMatch) score = Math.max(score, bestGapped + bonus + 1 - index);
27705
+ current[index] = score;
27706
+ }
27707
+ leftLeft = left;
27708
+ left = prior;
27709
+ }
27710
+ previous = current;
27711
+ }
27712
+ let best = noMatch;
27713
+ for (const score of previous) best = Math.max(best, score);
27714
+ return best === noMatch ? void 0 : best;
27715
+ }
27716
+ /**
27717
+ * Rank named items by a menu query.
27718
+ * @param items - candidates in source order (the caller's composition order
27719
+ * is the final tie-breaker, e.g. local commands before registry entries).
27720
+ * @param rawQuery - the text typed after the trigger, matched case-insensitively.
27721
+ * @returns the matching items: prefix hits first, then by alignment score,
27722
+ * then in source order. The input list itself for an empty query.
27723
+ */
27724
+ function rankByName(items, rawQuery) {
27725
+ const query = rawQuery.toLowerCase();
27726
+ if (query === "") return items;
27727
+ const ranked = [];
27728
+ items.forEach((item, index) => {
27729
+ const name = item.name.toLowerCase();
27730
+ const score = alignmentScore(name, query);
27731
+ if (score !== void 0) ranked.push({
27732
+ item,
27733
+ index,
27734
+ prefix: name.startsWith(query),
27735
+ score
27736
+ });
27737
+ });
27738
+ ranked.sort((left, right) => Number(right.prefix) - Number(left.prefix) || right.score - left.score || left.index - right.index);
27739
+ return ranked.map((match) => match.item);
27740
+ }
27741
+ //#endregion
26822
27742
  //#region src/provider-settings.ts
26823
27743
  /** Human text for a rejection value (mirrors the web page's `messageOf`). */
26824
27744
  function messageOf$1(error) {
@@ -27028,7 +27948,8 @@ async function loadProviderSettings(ctx) {
27028
27948
  active: active.has(entry.provider),
27029
27949
  settingsNs: entry.settingsNs,
27030
27950
  settingsPath: entry.settingsPath,
27031
- ...entry.declared === void 0 ? {} : { declared: entry.declared }
27951
+ ...entry.declared === void 0 ? {} : { declared: entry.declared },
27952
+ ...entry.error === void 0 ? {} : { error: singleLine$1(entry.error) }
27032
27953
  })), ...registered.filter((provider) => !declared.has(provider.id)).map((provider) => ({
27033
27954
  provider: provider.id,
27034
27955
  displayName: provider.name,
@@ -27053,7 +27974,8 @@ async function loadProviderSettings(ctx) {
27053
27974
  configuration: configurationOf(profile),
27054
27975
  ...credentialRef === void 0 ? {} : { credentialRef },
27055
27976
  suggestedRef: deriveCredentialRef(base.provider),
27056
- ...base.declared === void 0 ? {} : { declared: base.declared }
27977
+ ...base.declared === void 0 ? {} : { declared: base.declared },
27978
+ ...base.error === void 0 ? {} : { diagnostic: base.error }
27057
27979
  };
27058
27980
  });
27059
27981
  const refs = [...new Set(rows.flatMap((row) => row.credentialRef === void 0 ? [] : [row.credentialRef]))];
@@ -27480,9 +28402,8 @@ function mergeSessionTitles(rows, observations) {
27480
28402
  }
27481
28403
  /**
27482
28404
  * Encode a session id the way the JSONL backend does for its on-disk layout
27483
- * (`encodeSegment`: safe units literal, everything else `~XXXX`). Used ONLY to
27484
- * validate that a `locate()` path really is this session's directory before
27485
- * any deletion touches the filesystem — a local copy of the pure upstream
28405
+ * (`encodeSegment`: safe units literal, everything else `~XXXX`). Used to
28406
+ * validate and derive session directories a local copy of the pure upstream
27486
28407
  * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
27487
28408
  */
27488
28409
  function encodeSessionSegment(raw) {
@@ -27498,21 +28419,91 @@ function encodeSessionSegment(raw) {
27498
28419
  }
27499
28420
  return out;
27500
28421
  }
27501
- /** The session-log artifact names the JSONL backend may create. */
27502
- const SESSION_ARTIFACT_NAMES = ["session.jsonl", "session.jsonl.zstd"];
27503
28422
  /**
27504
- * Guard one `locate()` artifact path before deletion (codex's scoped-path
27505
- * check, adapted to the JSONL layout): the file must be a `session.jsonl`
27506
- * artifact sitting in the directory named exactly `encodeSegment(id)`.
27507
- * @param artifact - the path the persistence backend located.
27508
- * @param id - the session id the artifact claims to belong to.
27509
- * @returns the owning session directory, or undefined when the layout is unexpected.
28423
+ * Encode a project cwd the way the JSONL backend groups sessions on disk
28424
+ * (`projectKey`: separators collapse to one `-`, everything else mirrors
28425
+ * `encodeSegment`, bounded to 251 chars). A local copy of the pure upstream
28426
+ * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
28427
+ */
28428
+ function encodeProjectKey(cwd) {
28429
+ if (cwd.length === 0) throw new Error("cannot encode an empty project path");
28430
+ let readable = "";
28431
+ let separatorRun = false;
28432
+ for (let i = 0; i < cwd.length; i += 1) {
28433
+ const code = cwd.charCodeAt(i);
28434
+ const ch = String.fromCharCode(code);
28435
+ if (ch === "/" || ch === "\\" || ch === ":") {
28436
+ if (!separatorRun) readable += "-";
28437
+ separatorRun = true;
28438
+ } else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
28439
+ readable += ch;
28440
+ separatorRun = false;
28441
+ } else {
28442
+ readable += `~${code.toString(16).toUpperCase().padStart(4, "0")}`;
28443
+ separatorRun = false;
28444
+ }
28445
+ }
28446
+ return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
28447
+ }
28448
+ /** The project-level directory name the JSONL backend uses for a missing cwd. */
28449
+ const NO_CWD_DIRECTORY = "_no-cwd";
28450
+ /**
28451
+ * Derive one session's artifact directory under the JSONL backend root,
28452
+ * mirroring the upstream `<root>/<projectKey(cwd)>/<encodeSegment(id)>/`
28453
+ * layout (0.1.5 `sessionDir`/`projectDir`).
28454
+ * @param root - the JSONL backend's configured session root.
28455
+ * @param cwd - the session's pinned working directory, when the header has one.
28456
+ * @param id - the session id.
28457
+ * @returns the absolute session directory path.
27510
28458
  */
27511
- function sessionArtifactDirectory(artifact, id) {
27512
- if (basename(artifact) !== "session.jsonl" && basename(artifact) !== "session.jsonl.zstd") return void 0;
27513
- const dir = dirname(artifact);
28459
+ function sessionDirectoryFor(root, cwd, id) {
28460
+ const project = cwd === void 0 || cwd === "" ? NO_CWD_DIRECTORY : encodeProjectKey(cwd);
28461
+ return resolve(root, project, encodeSessionSegment(id));
28462
+ }
28463
+ /**
28464
+ * The canonical session-log artifact filenames the JSONL backend may create:
28465
+ * format v0 writes the bare `session.jsonl` name; v1+ write
28466
+ * `session.vN.jsonl`, each generation optionally zstd-compressed. Multiple
28467
+ * immutable generations may coexist in one session directory (0.1.5). The
28468
+ * range follows the installed session package's `SESSION_FORMAT_VERSION`, so
28469
+ * a future generation joins the enumeration with the dependency bump.
28470
+ */
28471
+ function sessionArtifactNames() {
28472
+ const names = ["session.jsonl", "session.jsonl.zstd"];
28473
+ for (let version = 1; version <= SESSION_FORMAT_VERSION; version += 1) names.push(`session.v${version}.jsonl`, `session.v${version}.jsonl.zstd`);
28474
+ return names;
28475
+ }
28476
+ /** Canonical generation-log filenames as a lookup set (bare v0 or `vN`-suffixed, ± zstd). */
28477
+ const SESSION_ARTIFACT_NAME_SET = new Set(sessionArtifactNames());
28478
+ /** True for one canonical session-log artifact filename the backend may own. */
28479
+ function isSessionArtifactName(name) {
28480
+ return SESSION_ARTIFACT_NAME_SET.has(name);
28481
+ }
28482
+ /**
28483
+ * Guard a derived session directory before deletion (codex's scoped-path
28484
+ * check, adapted to the JSONL layout): the directory's base name must be
28485
+ * exactly `encodeSegment(id)` beneath its project grouping.
28486
+ * @param dir - the derived session artifact directory.
28487
+ * @param id - the session id the directory claims to belong to.
28488
+ * @returns the guarded directory, or undefined when the layout is unexpected.
28489
+ */
28490
+ function sessionArtifactDirectory(dir, id) {
27514
28491
  if (basename(dir) !== encodeSessionSegment(id)) return void 0;
27515
- return dir;
28492
+ if (basename(dirname(dir)) === NO_CWD_DIRECTORY) return dir;
28493
+ return /^--.*--$|^~/.test(basename(dirname(dir))) ? dir : void 0;
28494
+ }
28495
+ /**
28496
+ * The JSONL backend's configured session root, when the mounted backend
28497
+ * exposes one. The upstream service contract dropped `locate()` in 0.1.5
28498
+ * (artifact paths are backend-private; only refusal diagnostics carry them),
28499
+ * so the TUI derives artifact paths from the backend's public plugin config.
28500
+ * Backends without a JSONL-style config (or a foreign shape) yield undefined
28501
+ * and callers degrade: mtime sorting falls back to createdAt and /delete
28502
+ * refuses, exactly as before.
28503
+ */
28504
+ function jsonlSessionRoot(persistence) {
28505
+ const root = persistence?.config?.root;
28506
+ return typeof root === "string" && root !== "" ? root : void 0;
27516
28507
  }
27517
28508
  /**
27518
28509
  * Collect one session's deletion subtree: the id plus every record whose
@@ -27948,14 +28939,6 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
27948
28939
  }];
27949
28940
  const right = [];
27950
28941
  const row2 = [];
27951
- if (facts.plan && enabled.has("plan")) row2.push({
27952
- group: { spans: [{
27953
- text: "⧉ plan",
27954
- tone: "accent"
27955
- }] },
27956
- rank: RANK2_PLAN,
27957
- id: "plan"
27958
- });
27959
28942
  if (stats.turns > 0 || stats.steps > 0) {
27960
28943
  if (enabled.has("turns")) {
27961
28944
  const counts = [];
@@ -28074,9 +29057,13 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
28074
29057
  });
28075
29058
  const permission = safe(facts.permission);
28076
29059
  let badge = -1;
29060
+ const planStation = facts.plan && permissionTone(permission) === "success";
28077
29061
  if (permission !== "" && enabled.has("permission")) {
28078
29062
  right.push({
28079
- span: {
29063
+ span: planStation ? {
29064
+ text: "plan on",
29065
+ tone: "plan"
29066
+ } : {
28080
29067
  text: permission,
28081
29068
  tone: permissionTone(permission)
28082
29069
  },
@@ -28085,6 +29072,14 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
28085
29072
  });
28086
29073
  badge = right.length - 1;
28087
29074
  }
29075
+ if (facts.plan && enabled.has("plan")) row2.push({
29076
+ group: { spans: [{
29077
+ text: "⧉ plan",
29078
+ tone: "accent"
29079
+ }] },
29080
+ rank: RANK2_PLAN,
29081
+ id: "plan"
29082
+ });
28088
29083
  return {
28089
29084
  left,
28090
29085
  right,
@@ -28926,14 +29921,15 @@ function composerMaxRows(terminalRows) {
28926
29921
  return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
28927
29922
  }
28928
29923
  /**
28929
- * History navigation starts with Up on an empty draft, or after visual
28930
- * movement has reached the directional text edge of an unchanged recalled
28931
- * entry. Every other position remains under textarea movement.
29924
+ * History navigation starts with Up on an empty draft, or continues from an
29925
+ * unchanged recalled entry whenever the caret sits on either text edge
29926
+ * (start or end) - moving the caret into the interior returns the keys to
29927
+ * ordinary editing until an edge is reached again.
28932
29928
  */
28933
29929
  function shouldRecallNavigate(value, cursor, lastRecalled, direction) {
28934
29930
  if (value === "") return direction < 0;
28935
29931
  if (lastRecalled !== value) return false;
28936
- return direction < 0 ? cursor === 0 : cursor === value.length;
29932
+ return cursor === 0 || cursor === value.length;
28937
29933
  }
28938
29934
  //#endregion
28939
29935
  //#region src/keyboard.ts
@@ -29274,7 +30270,8 @@ function ModePanel({ current, load, select, close }) {
29274
30270
  const visible = (0, import_react.useMemo)(() => rows.filter((row) => `${row.id} ${row.name ?? ""} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
29275
30271
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
29276
30272
  useInput((input, key) => {
29277
- if (key.escape || input === "q") return close();
30273
+ if (key.escape) return close();
30274
+ if (input === "q" && query === "") return close();
29278
30275
  if (input === "r" && query === "") return refresh();
29279
30276
  if (key.upArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length);
29280
30277
  if (key.downArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + 1) % visible.length);
@@ -29320,7 +30317,8 @@ function PermissionPanel({ current, load, select, close }) {
29320
30317
  const visible = (0, import_react.useMemo)(() => rows.filter((row) => `${row.id} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
29321
30318
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
29322
30319
  useInput((input, key) => {
29323
- if (key.escape || input === "q") return close();
30320
+ if (key.escape) return close();
30321
+ if (input === "q" && query === "") return close();
29324
30322
  if (input === "r" && query === "") return refresh();
29325
30323
  if (key.upArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length);
29326
30324
  if (key.downArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + 1) % visible.length);
@@ -29352,7 +30350,8 @@ function PluginPanel({ load, close, initialQuery = "" }) {
29352
30350
  const rows = (0, import_react.useMemo)(() => load().filter((row) => `${row.entryId} ${row.moduleName} ${row.phase ?? ""}`.toLowerCase().includes(query.toLowerCase())), [epoch, query]);
29353
30351
  (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
29354
30352
  useInput((input, key) => {
29355
- if (key.escape || input === "q") return close();
30353
+ if (key.escape) return close();
30354
+ if (input === "q" && query === "") return close();
29356
30355
  if (input === "r" && query === "") return setEpoch((value) => value + 1);
29357
30356
  if (key.upArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length);
29358
30357
  if (key.downArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + 1) % rows.length);
@@ -30033,6 +31032,69 @@ function SubagentPanel({ current, load, pick, inherit, close }) {
30033
31032
  footer: "↑↓ choose · enter apply · r refresh · esc close"
30034
31033
  });
30035
31034
  }
31035
+ /** Human frequency label: one-shot kinds read as Once, every rows carry the interval. */
31036
+ function scheduleFrequency(row) {
31037
+ if (row.kind !== "every") return "Once";
31038
+ const seconds = row.everySeconds ?? 0;
31039
+ if (seconds >= 3600 && seconds % 3600 === 0) return `Every ${seconds / 3600}h`;
31040
+ if (seconds >= 60 && seconds % 60 === 0) return `Every ${seconds / 60}m`;
31041
+ return `Every ${seconds}s`;
31042
+ }
31043
+ /** Relative label for the next target: in N unit, or N unit overdue. */
31044
+ function scheduleRelative(targetAt, now) {
31045
+ const delta = Math.max(0, Math.abs(targetAt - now));
31046
+ const minutes = Math.floor(delta / 6e4);
31047
+ const unit = minutes === 0 ? "<1m" : minutes >= 60 ? `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}` : `${minutes}m`;
31048
+ return targetAt <= now ? `${unit} overdue` : `in ${unit}`;
31049
+ }
31050
+ /** Ordered display rows: overdue first (error tone), then ascending target. */
31051
+ function scheduleDisplayRows(rows, now) {
31052
+ return [...rows].sort((left, right) => Number(left.targetAt > now) - Number(right.targetAt > now) || left.targetAt - right.targetAt).map((row) => ({
31053
+ key: row.id,
31054
+ text: `${row.prompt} · ${scheduleFrequency(row)} · ${new Date(row.targetAt).toLocaleString()} (${scheduleRelative(row.targetAt, now)})`,
31055
+ tone: row.targetAt <= now ? "error" : void 0
31056
+ }));
31057
+ }
31058
+ function SchedulePanel({ rows, close }) {
31059
+ const [, setTick] = (0, import_react.useState)(0);
31060
+ (0, import_react.useEffect)(() => {
31061
+ const id = setInterval(() => setTick((value) => value + 1), 1e3);
31062
+ return () => clearInterval(id);
31063
+ }, []);
31064
+ const display = scheduleDisplayRows(rows(), Date.now());
31065
+ const stdout = useStdout().stdout;
31066
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
31067
+ useInput((input, key) => {
31068
+ if (key.escape || input === "q") return close();
31069
+ });
31070
+ if (viewport.maxHeight === 0 || viewport.compact) {
31071
+ const summary = display.length === 0 ? "no active reminders" : singleLineText(display[0].text);
31072
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`/schedule · ${summary}`, viewport.contentColumns));
31073
+ }
31074
+ const budget = Math.max(1, viewport.bodyRows);
31075
+ const visible = display.slice(0, budget);
31076
+ const hidden = display.length - visible.length;
31077
+ return (0, import_react.createElement)(Box, {
31078
+ width: viewport.outerColumns,
31079
+ borderStyle: "round",
31080
+ borderColor: inkColor(getPalette().dim),
31081
+ flexDirection: "column",
31082
+ paddingX: 1
31083
+ }, (0, import_react.createElement)(Text, {
31084
+ color: inkColor(getPalette().brandBright),
31085
+ wrap: "truncate-end"
31086
+ }, truncateColumns(`/schedule · ${display.length} active reminder${display.length === 1 ? "" : "s"}`, viewport.contentColumns)), ...display.length === 0 ? [(0, import_react.createElement)(Text, {
31087
+ dimColor: true,
31088
+ wrap: "truncate-end"
31089
+ }, truncateColumns(" no active reminders — the model creates them with schedule_create", viewport.contentColumns))] : visible.map((row) => (0, import_react.createElement)(Text, {
31090
+ key: row.key,
31091
+ color: row.tone === "error" ? inkColor(getPalette().error) : void 0,
31092
+ wrap: "truncate-end"
31093
+ }, truncateColumns(` ${singleLineText(row.text)}`, viewport.contentColumns))), (0, import_react.createElement)(Text, {
31094
+ dimColor: true,
31095
+ wrap: "truncate-end"
31096
+ }, truncateColumns(`esc/q close${hidden > 0 ? ` · +${hidden} more` : ""} · the model schedules via schedule_create`, viewport.contentColumns)));
31097
+ }
30036
31098
  /** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
30037
31099
  function serializeHistoryEntry(text) {
30038
31100
  return JSON.stringify(text);
@@ -30586,7 +31648,14 @@ function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
30586
31648
  }
30587
31649
  //#endregion
30588
31650
  //#region src/attachments.ts
30589
- /** Terminal image-file adapter over the Harness durable attachment service. */
31651
+ /** Terminal image- and file-attachment adapter over the Harness durable attachment service. */
31652
+ /**
31653
+ * Terminal-side file admission bounds. Upstream exposes image limits through
31654
+ * the attachment service but no file limits (files ride verbatim storage);
31655
+ * these keep a dragged file from silently ingesting a disk-sized blob and
31656
+ * bound one message the way the image batch is bounded.
31657
+ */
31658
+ const MAX_FILE_BYTES = 8388608;
30590
31659
  const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
30591
31660
  ".png",
30592
31661
  ".jpg",
@@ -30608,24 +31677,58 @@ function detectImageMediaType(data) {
30608
31677
  function looksLikeImagePath(path) {
30609
31678
  return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
30610
31679
  }
30611
- /** Parse a terminal paste/drop containing only one or more image paths. */
30612
- function parsePastedImagePaths(input) {
31680
+ /**
31681
+ * Parse a paste/drop into its image and file paths: image-suffixed tokens
31682
+ * stay images, other path-shaped tokens ride as file attachments (0.1.5
31683
+ * file blocks), and anything that is neither leaves both empty — the caller
31684
+ * then treats the paste as plain text.
31685
+ *
31686
+ * File tokens are held to an absolute-path-with-shape bar (drive/backslash
31687
+ * or a dot-suffixed leaf after a separator): a dropped terminal path always
31688
+ * carries one of those, while prose, slash commands, and option flags never
31689
+ * do. A POSIX absolute path without any dot-suffixed leaf falls through as
31690
+ * text — the @ mention route still attaches such files deliberately.
31691
+ */
31692
+ function parsePastedAttachmentPaths(input) {
30613
31693
  const text = input.trim();
30614
- if (text === "") return [];
30615
- const tokens = [];
31694
+ if (text === "") return {
31695
+ images: [],
31696
+ files: []
31697
+ };
31698
+ const images = [];
31699
+ const files = [];
31700
+ const looksLikeDroppedFile = (path) => /^[A-Za-z]:[\\/]/u.test(path) || /^\\\\/u.test(path) || /^\/|^\.\.?\//u.test(path) && /\.[A-Za-z0-9]{1,16}$/u.test(path);
30616
31701
  for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
30617
31702
  const token = match[1] ?? match[2] ?? match[3];
30618
31703
  if (token === void 0) continue;
30619
31704
  let path = token;
30620
- if (path.startsWith("file://")) try {
30621
- path = fileURLToPath(path);
30622
- } catch {
30623
- return [];
31705
+ if (path.startsWith("file://")) {
31706
+ try {
31707
+ path = fileURLToPath(path);
31708
+ } catch {
31709
+ return {
31710
+ images: [],
31711
+ files: []
31712
+ };
31713
+ }
31714
+ if (looksLikeImagePath(path)) images.push(path);
31715
+ else files.push(path);
31716
+ continue;
31717
+ }
31718
+ if (looksLikeImagePath(path)) {
31719
+ images.push(path);
31720
+ continue;
30624
31721
  }
30625
- if (!looksLikeImagePath(path)) return [];
30626
- tokens.push(path);
31722
+ if (!looksLikeDroppedFile(path)) return {
31723
+ images: [],
31724
+ files: []
31725
+ };
31726
+ files.push(path);
30627
31727
  }
30628
- return tokens;
31728
+ return {
31729
+ images,
31730
+ files
31731
+ };
30629
31732
  }
30630
31733
  /** Validate path, byte size and encoded signature without writing an attachment object. */
30631
31734
  async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
@@ -30696,6 +31799,60 @@ async function saveImagePaths(paths, attachments, signal) {
30696
31799
  attachment
30697
31800
  }));
30698
31801
  }
31802
+ /** Validate path and byte size for non-image file attachments without writing. */
31803
+ async function inspectFilePaths(paths, attachments, cwd = process.cwd()) {
31804
+ if (paths.length === 0) return [];
31805
+ if (attachments === void 0) throw new Error("file attachments are unavailable in this profile");
31806
+ if (paths.length > 8) throw new Error(`too many files (${paths.length}; limit 8)`);
31807
+ const inspected = [];
31808
+ for (const raw of paths) {
31809
+ const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
31810
+ let facts;
31811
+ try {
31812
+ facts = await stat(path);
31813
+ } catch (error) {
31814
+ throw new Error(`cannot read file "${raw}": ${error instanceof Error ? error.message : String(error)}`);
31815
+ }
31816
+ if (!facts.isFile()) throw new Error(`file path is not a file: "${raw}"`);
31817
+ if (facts.size > 8388608) throw new Error(`file "${basename(path)}" is ${facts.size} bytes; limit ${MAX_FILE_BYTES}`);
31818
+ inspected.push({
31819
+ path,
31820
+ name: basename(path),
31821
+ bytes: facts.size
31822
+ });
31823
+ }
31824
+ return inspected;
31825
+ }
31826
+ /** Read and persist an ordered non-image file path list as model file blocks. */
31827
+ async function saveFilePaths(paths, attachments, signal) {
31828
+ if (paths.length === 0) return [];
31829
+ if (attachments === void 0) throw new Error("file attachments are unavailable in this profile");
31830
+ await inspectFilePaths(paths, attachments);
31831
+ const checkCancelled = () => {
31832
+ if (signal?.aborted === true) throw new Error("file submission cancelled");
31833
+ };
31834
+ const inputs = [];
31835
+ for (const path of paths) {
31836
+ checkCancelled();
31837
+ let data;
31838
+ try {
31839
+ data = await readFile(path);
31840
+ } catch (error) {
31841
+ throw new Error(`cannot read file "${path}": ${error instanceof Error ? error.message : String(error)}`);
31842
+ }
31843
+ inputs.push({
31844
+ data,
31845
+ name: basename(path)
31846
+ });
31847
+ }
31848
+ checkCancelled();
31849
+ const refs = await Promise.all(inputs.map((input) => attachments.saveFile(input)));
31850
+ checkCancelled();
31851
+ return refs.map((attachment) => ({
31852
+ type: "file",
31853
+ attachment
31854
+ }));
31855
+ }
30699
31856
  //#endregion
30700
31857
  //#region src/app.ts
30701
31858
  /**
@@ -30773,10 +31930,18 @@ const LOCAL_COMMANDS = [
30773
31930
  label: "/plugin",
30774
31931
  description: "inspect the live plugin composition"
30775
31932
  },
31933
+ {
31934
+ label: "/update",
31935
+ description: "update dsh-code, the harness host, and profile plugins in one aligned step"
31936
+ },
30776
31937
  {
30777
31938
  label: "/jobs",
30778
31939
  description: "inspect background jobs"
30779
31940
  },
31941
+ {
31942
+ label: "/schedule",
31943
+ description: "inspect active reminders (created through schedule tools)"
31944
+ },
30780
31945
  {
30781
31946
  label: "/statusline",
30782
31947
  description: "customize the status line items"
@@ -30785,6 +31950,10 @@ const LOCAL_COMMANDS = [
30785
31950
  label: "/theme",
30786
31951
  description: "switch the color theme"
30787
31952
  },
31953
+ {
31954
+ label: "/animation",
31955
+ description: "toggle timed animations (/animation [on|off])"
31956
+ },
30788
31957
  {
30789
31958
  label: "/history",
30790
31959
  description: "search and recall past prompts"
@@ -30844,12 +32013,21 @@ function padColumns(text, width) {
30844
32013
  const clipped = truncateColumns(singleLineText(text), width);
30845
32014
  return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
30846
32015
  }
30847
- /** Interval-driven frame counter for one self-contained animated leaf. */
32016
+ /**
32017
+ * Wall-clock frame counter for one self-contained animated leaf. Each fire
32018
+ * derives the tick from elapsed time instead of counting intervals, so a
32019
+ * stretched interval (busy event loop, slow SSH) skips the animation ahead
32020
+ * rather than slowing it down; the tick always tracks real time.
32021
+ */
30848
32022
  function useFrames(intervalMs, active = true) {
30849
32023
  const [tick, setTick] = (0, import_react.useState)(0);
30850
32024
  (0, import_react.useEffect)(() => {
30851
32025
  if (!active) return;
30852
- const id = setInterval(() => setTick((current) => current + 1), intervalMs);
32026
+ const startedAt = Date.now();
32027
+ setTick(0);
32028
+ const id = setInterval(() => {
32029
+ setTick(Math.max(0, Math.floor((Date.now() - startedAt) / intervalMs)));
32030
+ }, intervalMs);
30853
32031
  return () => {
30854
32032
  clearInterval(id);
30855
32033
  };
@@ -30870,14 +32048,17 @@ function useStableInput(handler, active) {
30870
32048
  }, []);
30871
32049
  useInput(stableHandler, { isActive: active });
30872
32050
  }
30873
- /** The original web StateDot chase used by the busy composer marker. */
30874
- function BusyChase() {
30875
- const tick = useFrames(125);
32051
+ /**
32052
+ * The original web StateDot chase used by the busy composer marker. With
32053
+ * animations off it freezes on the first frame (still visibly busy).
32054
+ */
32055
+ function BusyChase({ animated = true }) {
32056
+ const tick = useFrames(125, animated);
30876
32057
  return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
30877
32058
  }
30878
- /** Blinking block caret appended to streaming text. */
30879
- function Caret() {
30880
- const tick = useFrames(530);
32059
+ /** Blinking block caret appended to streaming text; solid when frozen. */
32060
+ function Caret({ animated = true }) {
32061
+ const tick = useFrames(530, animated);
30881
32062
  return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
30882
32063
  }
30883
32064
  /** One resettable input-caret phase shared by the entire composer. */
@@ -30904,17 +32085,19 @@ function useCursorBlink(active) {
30904
32085
  * One bounded line painted with the deep-diving shimmer: a continuously
30905
32086
  * moving blue gradient across graphemes, the `✻` glyph in the breathing
30906
32087
  * spark color. Shared by the busy line and the collapsed thinking marker;
30907
- * always exactly one row (truncate-end) so the live budget stays exact.
32088
+ * always exactly one row (truncate-end) so the live budget stays exact. With
32089
+ * animations off the same spans render in fixed colors — no timer, no
32090
+ * per-frame repaint, the `✻` keeps its highlight.
30908
32091
  */
30909
- function ShimmerLine({ text }) {
30910
- const tick = useFrames(33);
32092
+ function ShimmerLine({ text, animated = true }) {
32093
+ const tick = useFrames(33, animated);
30911
32094
  const palette = getPalette();
30912
32095
  const graphemes = splitGraphemes(text);
30913
32096
  return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
30914
32097
  const sparkle = grapheme.text === "✻";
30915
32098
  return (0, import_react.createElement)(Text, {
30916
32099
  key: `${grapheme.start}-${grapheme.end}`,
30917
- color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
32100
+ color: inkColor(!animated ? sparkle ? palette.brandBright : palette.brandDeep : sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
30918
32101
  bold: sparkle || void 0
30919
32102
  }, grapheme.text);
30920
32103
  }));
@@ -30925,10 +32108,13 @@ function ShimmerLine({ text }) {
30925
32108
  * only once the turn has clearly been running (15s) — anchored to `turn/start`
30926
32109
  * so a resumed mid-turn keeps the real time.
30927
32110
  */
30928
- function DeepDivingLine({ since }) {
32111
+ function DeepDivingLine({ since, animated = true }) {
30929
32112
  const elapsed = since === 0 ? 0 : Date.now() - since;
30930
32113
  const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "✻ Deep diving...";
30931
- return (0, import_react.createElement)(ShimmerLine, { text });
32114
+ return (0, import_react.createElement)(ShimmerLine, {
32115
+ text,
32116
+ animated
32117
+ });
30932
32118
  }
30933
32119
  /**
30934
32120
  * The streaming buffer rendered with a hard size cap: the live region must
@@ -31330,6 +32516,11 @@ function statusToneProps(tone) {
31330
32516
  bold: true,
31331
32517
  dimColor: void 0
31332
32518
  };
32519
+ case "plan": return {
32520
+ color: inkColor(getPalette().success),
32521
+ bold: true,
32522
+ dimColor: void 0
32523
+ };
31333
32524
  case "warn": return {
31334
32525
  color: inkColor(getPalette().warn),
31335
32526
  bold: true,
@@ -31383,7 +32574,7 @@ function deepseekWaveHues(tier) {
31383
32574
  palette.brandMid
31384
32575
  ];
31385
32576
  }
31386
- function StatusLine({ facts, stats, busy, columns, items }) {
32577
+ function StatusLine({ facts, stats, busy, columns, items, onRows }) {
31387
32578
  const layout = (0, import_react.useMemo)(() => layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
31388
32579
  busy,
31389
32580
  items,
@@ -31406,6 +32597,10 @@ function StatusLine({ facts, stats, busy, columns, items }) {
31406
32597
  columns,
31407
32598
  items
31408
32599
  ]);
32600
+ const statusRowCount = layout.row2.left.length > 0 ? 2 : 1;
32601
+ (0, import_react.useEffect)(() => {
32602
+ onRows?.(statusRowCount);
32603
+ }, [onRows, statusRowCount]);
31409
32604
  const renderRow = (row, key, indent = 0) => {
31410
32605
  const leftParts = [];
31411
32606
  row.left.forEach((group, groupIndex) => {
@@ -31946,36 +33141,44 @@ function QuestionBar({ store, snapshot, locked }) {
31946
33141
  }
31947
33142
  /** The /model panel: a scrolling list over the advisory model directory. */
31948
33143
  function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry, onClose }) {
33144
+ const [query, setQuery] = (0, import_react.useState)("");
31949
33145
  const [cursor, setCursor] = (0, import_react.useState)(0);
31950
33146
  const stdout = useStdout().stdout;
31951
33147
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
31952
33148
  const rows = directory?.rows ?? [];
33149
+ const filtered = (0, import_react.useMemo)(() => {
33150
+ if (query === "") return rows;
33151
+ const needle = query.toLowerCase();
33152
+ return rows.filter((row) => `${row.provider} ${row.providerName ?? ""} ${row.model} ${row.modelName}`.toLowerCase().includes(needle));
33153
+ }, [rows, query]);
31953
33154
  const positioned = (0, import_react.useRef)(false);
31954
33155
  (0, import_react.useEffect)(() => {
31955
33156
  if (positioned.current || rows.length === 0 || current === void 0) {
31956
- if (rows.length === 0) {
33157
+ if (filtered.length === 0) {
31957
33158
  if (cursor !== 0) setCursor(0);
31958
33159
  return;
31959
33160
  }
31960
- if (cursor >= rows.length) setCursor(rows.length - 1);
33161
+ if (cursor >= filtered.length) setCursor(filtered.length - 1);
31961
33162
  return;
31962
33163
  }
31963
33164
  const index = rows.findIndex((row) => `${row.provider}/${row.model}` === current);
31964
33165
  if (index >= 0) {
31965
33166
  positioned.current = true;
31966
- setCursor(index);
31967
- } else if (cursor >= rows.length) setCursor(Math.max(0, rows.length - 1));
33167
+ const filteredIndex = filtered.indexOf(rows[index]);
33168
+ setCursor(filteredIndex >= 0 ? filteredIndex : 0);
33169
+ } else if (cursor >= filtered.length) setCursor(Math.max(0, filtered.length - 1));
31968
33170
  }, [
31969
33171
  rows,
33172
+ filtered,
31970
33173
  cursor,
31971
33174
  current
31972
33175
  ]);
31973
33176
  useInput((input, key) => {
31974
- if (key.escape || input === "q") {
33177
+ if (key.escape || input === "q" && query === "") {
31975
33178
  onClose();
31976
33179
  return;
31977
33180
  }
31978
- if (input === "r") {
33181
+ if (input === "r" && query === "") {
31979
33182
  onRetry();
31980
33183
  return;
31981
33184
  }
@@ -31987,13 +33190,19 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
31987
33190
  onClose();
31988
33191
  return;
31989
33192
  }
31990
- if (rows.length === 0) return;
33193
+ const next = editQuery(query, input, key);
33194
+ if (next !== void 0) {
33195
+ setQuery(next);
33196
+ setCursor(0);
33197
+ return;
33198
+ }
33199
+ if (filtered.length === 0) return;
31991
33200
  if (key.upArrow) {
31992
- setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
33201
+ setCursor(cursor > 0 ? cursor - 1 : filtered.length - 1);
31993
33202
  return;
31994
33203
  }
31995
33204
  if (key.downArrow) {
31996
- setCursor(cursor < rows.length - 1 ? cursor + 1 : 0);
33205
+ setCursor(cursor < filtered.length - 1 ? cursor + 1 : 0);
31997
33206
  return;
31998
33207
  }
31999
33208
  if (key.pageUp) {
@@ -32001,23 +33210,15 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
32001
33210
  return;
32002
33211
  }
32003
33212
  if (key.pageDown) {
32004
- setCursor((current) => Math.min(rows.length - 1, current + Math.max(1, viewport.bodyRows - 1)));
32005
- return;
32006
- }
32007
- if (input === "g") {
32008
- setCursor(0);
32009
- return;
32010
- }
32011
- if (input === "G") {
32012
- setCursor(rows.length - 1);
33213
+ setCursor((current) => Math.min(filtered.length - 1, current + Math.max(1, viewport.bodyRows - 1)));
32013
33214
  return;
32014
33215
  }
32015
- if (key.return && rows[cursor] !== void 0) onSelect(rows[cursor]);
33216
+ if (key.return && filtered[cursor] !== void 0) onSelect(filtered[cursor]);
32016
33217
  });
32017
33218
  if (viewport.maxHeight === 0 || viewport.compact) {
32018
33219
  const providers = onProviders === void 0 ? "" : " · tab providers";
32019
- const state = rows.length === 0 ? directory === void 0 && error === void 0 ? "loading…" : error !== void 0 ? "error" : "no models" : `❯ ${rows[cursor]?.modelName ?? rows[cursor]?.model ?? ""}`;
32020
- return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`/model · ${state}${providers} · r retry · esc/q close`, viewport.contentColumns));
33220
+ const state = filtered.length === 0 ? directory === void 0 && error === void 0 ? "loading…" : error !== void 0 ? "error" : query === "" ? "no models" : `no match for '${singleLineText(query)}'` : `❯ ${filtered[cursor]?.modelName ?? filtered[cursor]?.model ?? ""}`;
33221
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`/model · ${state}${providers} · ${query === "" ? "type to filter · r retry · esc/q close" : "backspace edits · esc close"}`, viewport.contentColumns));
32021
33222
  }
32022
33223
  const visibleStateRows = (directory === void 0 && error === void 0 ? [(0, import_react.createElement)(Text, {
32023
33224
  key: "loading",
@@ -32035,10 +33236,14 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
32035
33236
  key: "empty",
32036
33237
  dimColor: true,
32037
33238
  wrap: "truncate-end"
32038
- }, " no models available")] : []]).slice(0, viewport.bodyRows);
33239
+ }, " no models available")] : filtered.length === 0 ? [(0, import_react.createElement)(Text, {
33240
+ key: "no-match",
33241
+ dimColor: true,
33242
+ wrap: "truncate-end"
33243
+ }, truncateColumns(` no models match '${singleLineText(query)}'`, viewport.contentColumns))] : []]).slice(0, viewport.bodyRows);
32039
33244
  const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length);
32040
- const first = selectionWindow(cursor, rows.length, rowBudget);
32041
- const visible = rowBudget === 0 ? [] : rows.slice(first, first + rowBudget);
33245
+ const first = selectionWindow(cursor, filtered.length, rowBudget);
33246
+ const visible = rowBudget === 0 ? [] : filtered.slice(first, first + rowBudget);
32042
33247
  return (0, import_react.createElement)(Box, {
32043
33248
  flexDirection: "column",
32044
33249
  width: viewport.outerColumns,
@@ -32049,8 +33254,8 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
32049
33254
  color: inkColor(getPalette().brand),
32050
33255
  bold: true,
32051
33256
  wrap: "truncate-end"
32052
- }, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
32053
- const index = rows.indexOf(row);
33257
+ }, truncateColumns(query === "" ? `/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}` : `/model — select model · ${filtered.length} of ${rows.length} match '${singleLineText(query)}'`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
33258
+ const index = filtered.indexOf(row);
32054
33259
  const capability = row.inputModalities?.includes("image") === true ? " · image" : "";
32055
33260
  const label = displayText(`${row.providerName} · ${row.modelName}${capability}`);
32056
33261
  return (0, import_react.createElement)(Text, {
@@ -32061,7 +33266,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
32061
33266
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
32062
33267
  dimColor: true,
32063
33268
  wrap: "truncate-end"
32064
- }, dim(truncateColumns(`↑↓ move · pgup/pgdn page · enter select${onProviders === void 0 ? "" : " · tab providers"} · r retry · esc/q close`, viewport.contentColumns))));
33269
+ }, dim(truncateColumns(query === "" ? `type to filter · ↑↓ move · pgup/pgdn page · enter select${onProviders === void 0 ? "" : " · tab providers"} · r retry · esc/q close` : `↑↓ move · pgup/pgdn page · enter select · backspace edits · esc close`, viewport.contentColumns))));
32065
33270
  }
32066
33271
  /** Compact provider-state copy; only value-free credential facts cross this boundary. */
32067
33272
  function providerStateLabel(row) {
@@ -32213,7 +33418,8 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
32213
33418
  const identity = row.displayName === row.provider ? row.provider : row.displayName + " (" + row.provider + ")";
32214
33419
  const authorization = authorizationForProvider(authorizations, row.provider);
32215
33420
  const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? " · " + providerAuthorizationStatus(authorization) : "";
32216
- const label = identity + " · " + providerStateLabel(row) + authLabel + (row.removable ? " · custom" : "");
33421
+ const diagnostic = row.diagnostic === void 0 ? "" : " · ! " + singleLineText(row.diagnostic);
33422
+ const label = identity + " · " + providerStateLabel(row) + authLabel + (row.removable ? " · custom" : "") + diagnostic;
32217
33423
  const idleColor = row.configured ? inkColor(getPalette().brandMid) : inkColor(getPalette().dim);
32218
33424
  itemRows.push((0, import_react.createElement)(Text, {
32219
33425
  key: row.provider,
@@ -32581,7 +33787,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
32581
33787
  color: zone === "url" ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
32582
33788
  wrap: "truncate-end"
32583
33789
  }, truncateColumns(" " + (zone === "url" ? ">" : " ") + " url " + (baseURL === "" ? "(official default)" : baseURL) + (zone === "url" ? "▏" : ""), viewport.contentColumns));
32584
- const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 3);
33790
+ const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - (target.diagnostic === void 0 ? 0 : 1) - 3);
32585
33791
  const first = selectionWindow(cursor, models.length + 1, rowBudget);
32586
33792
  const modelRows = [];
32587
33793
  for (let index = first; index < first + Math.max(0, Math.min(models.length + 1 - first, rowBudget)); index += 1) {
@@ -32614,7 +33820,11 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
32614
33820
  color: inkColor(getPalette().brand),
32615
33821
  bold: true,
32616
33822
  wrap: "truncate-end"
32617
- }, truncateColumns("/model — configure " + target.displayName, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), keyRow, urlRow, ...stateRows, ...modelRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
33823
+ }, truncateColumns("/model — configure " + target.displayName, viewport.contentColumns)), ...target.diagnostic === void 0 ? [] : [(0, import_react.createElement)(Text, {
33824
+ key: "diagnostic",
33825
+ color: inkColor(getPalette().warn),
33826
+ wrap: "truncate-end"
33827
+ }, truncateColumns("! " + displayText(singleLineText(target.diagnostic)), viewport.contentColumns))], (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), keyRow, urlRow, ...stateRows, ...modelRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
32618
33828
  color: inkColor(getPalette().dim),
32619
33829
  wrap: "truncate-end"
32620
33830
  }, truncateColumns("↑↓ move · ←→ in/out · space remove · e efforts · c copy efforts · tab discover · enter save · esc back", viewport.contentColumns)));
@@ -32954,6 +34164,167 @@ function waveRowSpans(cells) {
32954
34164
  }
32955
34165
  return spans;
32956
34166
  }
34167
+ /** Index of the cell STARTING at a display column, if one does. */
34168
+ function cellIndexAtColumn(cells, target) {
34169
+ let column = 0;
34170
+ for (let index = 0; index < cells.length; index += 1) {
34171
+ if (column === target) return index;
34172
+ column += cells[index].width ?? visibleColumns(cells[index].char);
34173
+ if (column > target) return void 0;
34174
+ }
34175
+ }
34176
+ /**
34177
+ * Wall-clock wave frames — strictly ONE sweep per MOUNT; the mount-spanning
34178
+ * one-shot latch (surviving modal unmounts) lives in Input as `wavePlayedKey`.
34179
+ * The first gate-off after the sweep has started (it completed, a turn went
34180
+ * busy, image preparation began, animations were toggled off) latches `done`
34181
+ * for this mount, so the same mount can never resume or replay. A trigger
34182
+ * that lands while the gate is already down stays pending until the gate
34183
+ * rises once, then plays.
34184
+ */
34185
+ function useWaveFrames(active, durationMs) {
34186
+ const [tick, setTick] = (0, import_react.useState)(0);
34187
+ const [done, setDone] = (0, import_react.useState)(false);
34188
+ const startedRef = (0, import_react.useRef)(false);
34189
+ (0, import_react.useEffect)(() => {
34190
+ if (done) return;
34191
+ if (!active) {
34192
+ if (startedRef.current) setDone(true);
34193
+ return;
34194
+ }
34195
+ startedRef.current = true;
34196
+ const startedAt = Date.now();
34197
+ const id = setInterval(() => {
34198
+ const elapsed = Date.now() - startedAt;
34199
+ if (elapsed >= durationMs) {
34200
+ clearInterval(id);
34201
+ setDone(true);
34202
+ return;
34203
+ }
34204
+ setTick(Math.max(0, Math.floor(elapsed / 33)));
34205
+ }, 33);
34206
+ return () => {
34207
+ clearInterval(id);
34208
+ };
34209
+ }, [
34210
+ active,
34211
+ durationMs,
34212
+ done
34213
+ ]);
34214
+ return {
34215
+ tick,
34216
+ done
34217
+ };
34218
+ }
34219
+ /**
34220
+ * The self-contained wave leaf: it owns its 33ms tick, so the sweep
34221
+ * re-renders ONLY this component at ~30fps — Input's derived editor state
34222
+ * never re-runs per frame. Graphemes stay atomic and every background sample
34223
+ * advances by terminal display columns, so CJK and emoji cannot move the
34224
+ * caret or wrap the band. The duration gate renders the fallback band on the
34225
+ * frame the sweep completes.
34226
+ */
34227
+ function ComposerWave(props) {
34228
+ const { tier, style } = props;
34229
+ const durationMs = deepseekWaveDuration(tier, style);
34230
+ const { tick, done } = useWaveFrames(props.active, durationMs);
34231
+ const settledRef = (0, import_react.useRef)(false);
34232
+ const onSettledRef = (0, import_react.useRef)(props.onSettled);
34233
+ onSettledRef.current = props.onSettled;
34234
+ const settle = () => {
34235
+ if (settledRef.current) return;
34236
+ settledRef.current = true;
34237
+ onSettledRef.current();
34238
+ };
34239
+ (0, import_react.useEffect)(() => {
34240
+ if (done) settle();
34241
+ }, [done]);
34242
+ (0, import_react.useEffect)(() => () => {
34243
+ settle();
34244
+ }, []);
34245
+ if (!props.active || done || tick * 33 >= durationMs) return props.fallback;
34246
+ const hues = deepseekWaveHues(tier);
34247
+ const bandRgb = getPalette().composerBand;
34248
+ const totalBandRows = props.rows.length + 2;
34249
+ const waveBg = (row, column) => {
34250
+ const rgb = deepseekWaveColumnBg(tick, column, props.bandWidth, tier, style, hues, bandRgb, row, totalBandRows);
34251
+ return rgb === null ? props.bandBg : inkColor(rgb);
34252
+ };
34253
+ const blankBandRow = (row) => {
34254
+ const blanks = [];
34255
+ for (let column = 0; column < props.bandWidth; column += 1) blanks.push({
34256
+ char: " ",
34257
+ width: 1,
34258
+ backgroundColor: waveBg(row, column)
34259
+ });
34260
+ return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
34261
+ };
34262
+ const editorWaveRows = props.rows.map((row, visibleIndex) => {
34263
+ const sourceIndex = props.windowStart + visibleIndex;
34264
+ const bandRow = visibleIndex + 1;
34265
+ const parts = editorRowParts(row, sourceIndex, props.caretRow, props.cursor);
34266
+ const placeholder = sourceIndex === 0 && props.value === "";
34267
+ const cells = [];
34268
+ let usedColumns = 0;
34269
+ const push = (char, extra = {}) => {
34270
+ const width = visibleColumns(char);
34271
+ cells.push({
34272
+ char,
34273
+ width,
34274
+ backgroundColor: waveBg(bandRow, usedColumns),
34275
+ ...extra
34276
+ });
34277
+ usedColumns += width;
34278
+ };
34279
+ if (sourceIndex === 0) {
34280
+ push(props.promptGlyph, {
34281
+ color: props.promptColor,
34282
+ bold: true
34283
+ });
34284
+ push(" ", { color: props.promptColor });
34285
+ } else {
34286
+ push(" ");
34287
+ push(" ");
34288
+ }
34289
+ for (const span of splitGraphemes(parts.before)) push(span.text);
34290
+ if (parts.hasCaret) push(parts.caret, { inverse: props.caretVisible });
34291
+ const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
34292
+ for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
34293
+ while (usedColumns < props.bandWidth) push(" ");
34294
+ const middleBandRow = Math.floor(totalBandRows / 2);
34295
+ if (bandRow === middleBandRow && deepseekWaveWordVisible(tick, tier, style)) {
34296
+ const word = tier === "unknown" ? "Into the Unknown" : "deepseek";
34297
+ const start = Math.max(2, Math.floor((props.bandWidth - word.length) / 2));
34298
+ const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
34299
+ if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
34300
+ const cell = cells[indices[at]];
34301
+ cell.char = word[at];
34302
+ cell.width = 1;
34303
+ cell.color = inkColor(deepseekWaveWordHue(at, hues));
34304
+ cell.bold = true;
34305
+ cell.dim = false;
34306
+ }
34307
+ }
34308
+ if (bandRow === middleBandRow && (tier === "deepseek" || tier === "unknown") && style === "wave") {
34309
+ const spark = deepseekWaveSpark(tick);
34310
+ const lastIndex = cellIndexAtColumn(cells, props.bandWidth - 1);
34311
+ if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
34312
+ cells[lastIndex].char = spark;
34313
+ cells[lastIndex].color = props.promptColor;
34314
+ cells[lastIndex].bold = true;
34315
+ cells[lastIndex].dim = false;
34316
+ }
34317
+ }
34318
+ return (0, import_react.createElement)(Text, {
34319
+ key: `editor-${sourceIndex}`,
34320
+ wrap: "truncate-end"
34321
+ }, ...waveRowSpans(cells));
34322
+ });
34323
+ return (0, import_react.createElement)(Box, {
34324
+ flexDirection: "column",
34325
+ width: props.bandWidth
34326
+ }, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
34327
+ }
32957
34328
  /**
32958
34329
  * The Ctrl+O transcript inspector: one selected durable entry at a time,
32959
34330
  * with independent history selection and content scrolling. The complete
@@ -33113,8 +34484,10 @@ function completionCandidates(value, descriptors, skills) {
33113
34484
  seen.add(name);
33114
34485
  all.push(candidate);
33115
34486
  }
33116
- if (prefix === "") return all;
33117
- return all.filter((candidate) => candidate.label.slice(1).startsWith(prefix));
34487
+ return rankByName(all.map((candidate) => ({
34488
+ name: candidate.label.slice(1),
34489
+ candidate
34490
+ })), prefix).map((entry) => entry.candidate);
33118
34491
  }
33119
34492
  /**
33120
34493
  * Shared completion-menu geometry: the menu view and the App's dynamic-row
@@ -33196,9 +34569,12 @@ function CompletionMenu({ active, mention, index, rows, error }) {
33196
34569
  * While a modal (approval / question / model panel) owns the keys, the
33197
34570
  * box passes every key through untouched.
33198
34571
  */
33199
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows, onMenuRows }) {
33200
- const columns = useStdout().stdout?.columns ?? 80;
33201
- const inputTerminalRows = useStdout().stdout?.rows ?? 30;
34572
+ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openUpdate, openSchedule, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles, cycleMode, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, animations, applyAnimations, waveTier, waveStyle, maxRows, anchorRowsBelow, tabTitle, onEditorRows, onMenuRows, sessionKey }) {
34573
+ const { stdout: inputStdout } = useStdout();
34574
+ const columns = inputStdout?.columns ?? 80;
34575
+ const inputTerminalRows = inputStdout?.rows ?? 30;
34576
+ const tabTitleRef = (0, import_react.useRef)(tabTitle);
34577
+ tabTitleRef.current = tabTitle;
33202
34578
  const editorColumns = Math.max(1, columns - 6);
33203
34579
  const stdin = useStdin().stdin;
33204
34580
  const focusReporting = isVsCodeTerminalEnv();
@@ -33211,10 +34587,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33211
34587
  const [draftImages, setDraftImages] = (0, import_react.useState)([]);
33212
34588
  const draftImagesRef = (0, import_react.useRef)(draftImages);
33213
34589
  draftImagesRef.current = draftImages;
34590
+ const [draftFiles, setDraftFiles] = (0, import_react.useState)([]);
34591
+ const draftFilesRef = (0, import_react.useRef)(draftFiles);
34592
+ draftFilesRef.current = draftFiles;
33214
34593
  const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
33215
34594
  const prepareAbortRef = (0, import_react.useRef)(void 0);
33216
34595
  const prepareEpochRef = (0, import_react.useRef)(0);
33217
- const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages);
34596
+ const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages && animations);
33218
34597
  (0, import_react.useEffect)(() => () => {
33219
34598
  prepareEpochRef.current += 1;
33220
34599
  prepareAbortRef.current?.abort();
@@ -33238,6 +34617,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33238
34617
  const safe = sanitizeDraftText(historyFill.text);
33239
34618
  draftImagesRef.current = [];
33240
34619
  setDraftImages([]);
34620
+ draftFilesRef.current = [];
34621
+ setDraftFiles([]);
33241
34622
  valueRef.current = safe;
33242
34623
  cursorRef.current = safe.length;
33243
34624
  setValue(safe);
@@ -33264,6 +34645,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33264
34645
  draftImagesRef.current = next;
33265
34646
  return next.length === current.length ? current : next;
33266
34647
  });
34648
+ setDraftFiles((current) => {
34649
+ const next = current.filter((file) => value.includes(file.marker));
34650
+ draftFilesRef.current = next;
34651
+ return next.length === current.length ? current : next;
34652
+ });
33267
34653
  }, [value]);
33268
34654
  (0, import_react.useEffect)(() => {
33269
34655
  if (stdin === void 0) return;
@@ -33274,6 +34660,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33274
34660
  const normalized = normalizeKeyboardChunk(typeof chunk === "string" ? chunk : String(chunk));
33275
34661
  const input = focusReporting ? stripTerminalFocusEvents(normalized, (focused) => {
33276
34662
  terminalFocusedRef.current = focused;
34663
+ if (focused && inputStdout !== void 0) {
34664
+ inputStdout.write(terminalTitleSequence(tabTitleRef.current));
34665
+ process.title = sanitizeTerminalTitle(tabTitleRef.current);
34666
+ }
33277
34667
  }) : normalized;
33278
34668
  rawEditorTokens.current = tokenizeRawEditorChunk(input);
33279
34669
  return input;
@@ -33307,12 +34697,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33307
34697
  const [mentionError, setMentionError] = (0, import_react.useState)(void 0);
33308
34698
  const mentionRequestRef = (0, import_react.useRef)(0);
33309
34699
  const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
33310
- const uniqueImageMarker = (name, source, reserved = []) => {
34700
+ const uniqueImageMarker = (name, source, reserved = [], kind = "image") => {
33311
34701
  const safeName = singleLineText(sanitizeDraftText(name));
33312
- let marker = source === "mention" ? `@${safeName}` : `[image: ${safeName}]`;
34702
+ const label = kind === "file" ? "file" : "image";
34703
+ let marker = source === "mention" ? `@${safeName}` : `[${label}: ${safeName}]`;
33313
34704
  let suffix = 2;
33314
- while (valueRef.current.includes(marker) || draftImagesRef.current.some((image) => image.marker === marker) || reserved.includes(marker)) {
33315
- marker = source === "mention" ? `@${safeName} (${suffix})` : `[image: ${safeName} ${suffix}]`;
34705
+ const taken = (candidate) => valueRef.current.includes(candidate) || draftImagesRef.current.some((image) => image.marker === candidate) || draftFilesRef.current.some((file) => file.marker === candidate) || reserved.includes(candidate);
34706
+ while (taken(marker)) {
34707
+ marker = source === "mention" ? `@${safeName} (${suffix})` : `[${label}: ${safeName} ${suffix}]`;
33316
34708
  suffix += 1;
33317
34709
  }
33318
34710
  return marker;
@@ -33330,24 +34722,41 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33330
34722
  setDraftImages(next);
33331
34723
  return true;
33332
34724
  };
33333
- const insertDroppedImages = (paths) => {
34725
+ /**
34726
+ * Attach a paste/drop split into image and non-image paths: images ride the
34727
+ * durable image blocks, files ride the 0.1.5 file blocks, and both register
34728
+ * visible draft markers anchored at the drop point.
34729
+ */
34730
+ const insertDroppedAttachments = (imagePaths, filePaths) => {
33334
34731
  const originalValue = valueRef.current;
33335
34732
  const originalCursor = cursorRef.current;
33336
- notify(`checking ${paths.length} image${paths.length === 1 ? "" : "s"}…`);
33337
- inspectImages(paths).then((inspected) => {
33338
- const additions = [];
34733
+ const total = imagePaths.length + filePaths.length;
34734
+ if (total === 0) return;
34735
+ notify(`checking ${total} attachment${total === 1 ? "" : "s"}…`);
34736
+ Promise.all([imagePaths.length === 0 ? Promise.resolve([]) : inspectImages(imagePaths), filePaths.length === 0 ? Promise.resolve([]) : inspectFiles(filePaths)]).then(([inspectedImages, inspectedFiles]) => {
34737
+ const imageAdditions = [];
34738
+ const fileAdditions = [];
33339
34739
  const markers = [];
33340
- for (const inspection of inspected) {
33341
- if ([...draftImagesRef.current, ...additions].some((image) => sameImagePath(image.path, inspection.path))) continue;
34740
+ for (const inspection of inspectedImages) {
34741
+ if ([...draftImagesRef.current, ...imageAdditions].some((image) => sameImagePath(image.path, inspection.path))) continue;
33342
34742
  const marker = uniqueImageMarker(inspection.name, "drop", markers);
33343
- additions.push({
34743
+ imageAdditions.push({
33344
34744
  ...inspection,
33345
34745
  marker
33346
34746
  });
33347
34747
  markers.push(marker);
33348
34748
  }
33349
- if (additions.length === 0) {
33350
- notify("those images are already attached", "warning");
34749
+ for (const inspection of inspectedFiles) {
34750
+ if ([...draftFilesRef.current, ...fileAdditions].some((file) => sameImagePath(file.path, inspection.path))) continue;
34751
+ const marker = uniqueImageMarker(inspection.name, "drop", markers, "file");
34752
+ fileAdditions.push({
34753
+ ...inspection,
34754
+ marker
34755
+ });
34756
+ markers.push(marker);
34757
+ }
34758
+ if (imageAdditions.length === 0 && fileAdditions.length === 0) {
34759
+ notify("those attachments are already attached", "warning");
33351
34760
  return;
33352
34761
  }
33353
34762
  const current = valueRef.current;
@@ -33356,7 +34765,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33356
34765
  end: originalCursor
33357
34766
  });
33358
34767
  if (anchor === void 0) {
33359
- notify("draft changed at the image drop point; drop the images again", "warning");
34768
+ notify("draft changed at the attachment drop point; drop the files again", "warning");
33360
34769
  return;
33361
34770
  }
33362
34771
  const at = anchor.start;
@@ -33368,12 +34777,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33368
34777
  setValue(edit.value);
33369
34778
  setCursor(nextCursor);
33370
34779
  resetCursorBlink();
33371
- const nextImages = [...draftImagesRef.current, ...additions];
34780
+ const nextImages = [...draftImagesRef.current, ...imageAdditions];
33372
34781
  draftImagesRef.current = nextImages;
33373
34782
  setDraftImages(nextImages);
33374
- notify(`${additions.length} image${additions.length === 1 ? "" : "s"} ready for the next message`);
34783
+ const nextFiles = [...draftFilesRef.current, ...fileAdditions];
34784
+ draftFilesRef.current = nextFiles;
34785
+ setDraftFiles(nextFiles);
34786
+ const count = imageAdditions.length + fileAdditions.length;
34787
+ notify(`${count} attachment${count === 1 ? "" : "s"} ready for the next message`);
33375
34788
  }, (reason) => {
33376
- notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
34789
+ notify(`attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
33377
34790
  });
33378
34791
  };
33379
34792
  (0, import_react.useEffect)(() => {
@@ -33408,7 +34821,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33408
34821
  ]);
33409
34822
  const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value;
33410
34823
  const visibleMentionRows = mentionToken !== void 0 && isPathLikeMentionQuery(mentionToken.query) ? mentionRows.filter((row) => row.kind !== "session") : mentionRows;
33411
- const menuRows = mentionActive ? visibleMentionRows.map((row) => ({
34824
+ let rankedMentionRows = visibleMentionRows;
34825
+ if (mentionToken !== void 0 && !isPathLikeMentionQuery(mentionToken.query) && mentionToken.query !== "") {
34826
+ const hits = rankByName(visibleMentionRows.map((row) => ({
34827
+ name: row.label.replace(/^@/u, ""),
34828
+ row
34829
+ })), mentionToken.query).map((entry) => entry.row);
34830
+ const hitSet = new Set(hits);
34831
+ rankedMentionRows = [...hits, ...visibleMentionRows.filter((row) => !hitSet.has(row))];
34832
+ }
34833
+ const menuRows = mentionActive ? rankedMentionRows.map((row) => ({
33412
34834
  label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
33413
34835
  description: row.description,
33414
34836
  origin: "mention"
@@ -33417,8 +34839,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33417
34839
  /** Accept the highlighted completion-menu candidate into the draft. */
33418
34840
  const acceptMenuCandidate = () => {
33419
34841
  if (mentionActive && mentionToken !== void 0) {
33420
- if (visibleMentionRows.length === 0) return;
33421
- const row = visibleMentionRows[completionIndex % visibleMentionRows.length];
34842
+ if (rankedMentionRows.length === 0) return;
34843
+ const row = rankedMentionRows[completionIndex % rankedMentionRows.length];
33422
34844
  if (row !== void 0) {
33423
34845
  if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
33424
34846
  const tokenText = value.slice(mentionToken.start, cursor);
@@ -33551,20 +34973,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33551
34973
  dismissNotice();
33552
34974
  notify("image submission cancelled", "warning");
33553
34975
  };
33554
- /** Move through visual rows first, then cross history at the true edge. */
34976
+ /** Cross history while an unchanged recalled draft rests its caret on
34977
+ * either text edge; between the edges (or inside ordinary drafts) the
34978
+ * arrows move through visual rows first. */
33555
34979
  const navigateVertical = (direction) => {
33556
34980
  const currentValue = valueRef.current;
33557
34981
  const currentCursor = cursorRef.current;
33558
- const model = editorModel(currentValue, editorColumns);
33559
- const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column;
33560
- const next = moveCursorVertically(model, currentCursor, preferred, direction);
33561
- if (next !== currentCursor) {
33562
- cursorRef.current = next;
33563
- setCursor(next);
33564
- resetCursorBlink();
33565
- preferredColumnRef.current = preferred;
33566
- return;
33567
- }
33568
34982
  if (recall.current.entries.length > 0 && shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
33569
34983
  const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current);
33570
34984
  recall.current = step.state;
@@ -33575,10 +34989,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33575
34989
  setValue(safe);
33576
34990
  setCursor(safe.length);
33577
34991
  preferredColumnRef.current = null;
33578
- setDismissedMenuValue(void 0);
34992
+ setDismissedMenuValue(safe);
33579
34993
  }
34994
+ resetCursorBlink();
34995
+ return;
34996
+ }
34997
+ const model = editorModel(currentValue, editorColumns);
34998
+ const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column;
34999
+ const next = moveCursorVertically(model, currentCursor, preferred, direction);
35000
+ if (next !== currentCursor) {
35001
+ cursorRef.current = next;
35002
+ setCursor(next);
35003
+ resetCursorBlink();
35004
+ preferredColumnRef.current = preferred;
33580
35005
  }
33581
- resetCursorBlink();
33582
35006
  };
33583
35007
  useStableInput((input, key) => {
33584
35008
  if (!active) return;
@@ -33595,8 +35019,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33595
35019
  }
33596
35020
  if (key.tab && key.shift) {
33597
35021
  try {
33598
- const next = cyclePermission();
33599
- if (next !== "") notify(`permission → ${next}`);
35022
+ const label = cycleMode();
35023
+ if (label !== "") notify(label);
33600
35024
  } catch (error) {
33601
35025
  notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
33602
35026
  }
@@ -33621,6 +35045,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33621
35045
  resetCursorBlink();
33622
35046
  draftImagesRef.current = [];
33623
35047
  setDraftImages([]);
35048
+ draftFilesRef.current = [];
35049
+ setDraftFiles([]);
33624
35050
  setCompletionIndex(0);
33625
35051
  setDismissedMenuValue(void 0);
33626
35052
  } else quit();
@@ -33666,15 +35092,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33666
35092
  }
33667
35093
  const trimmed = liveValue.trim();
33668
35094
  const text = submissionPayload(liveValue);
33669
- if (draftImagesRef.current.length > 0) {
35095
+ if (draftImagesRef.current.length > 0 || draftFilesRef.current.length > 0) {
35096
+ if (isSlashLine(text)) notify("commands cannot carry attachments; the line will be sent to the model as a prompt", "warning");
35097
+ const originSession = sessionKey;
33670
35098
  const controller = new AbortController();
33671
35099
  const epoch = prepareEpochRef.current + 1;
33672
35100
  prepareEpochRef.current = epoch;
33673
35101
  prepareAbortRef.current = controller;
33674
35102
  setPreparingImages(true);
33675
- notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? "" : "s"}…`);
33676
- const snapshot = draftImagesRef.current;
33677
- prepareImages(snapshot.map((image) => image.path), controller.signal).then((images) => {
35103
+ const imageSnapshot = draftImagesRef.current;
35104
+ const fileSnapshot = draftFilesRef.current;
35105
+ const total = imageSnapshot.length + fileSnapshot.length;
35106
+ notify(`processing ${total} attachment${total === 1 ? "" : "s"}…`);
35107
+ Promise.all([imageSnapshot.length === 0 ? Promise.resolve([]) : prepareImages(imageSnapshot.map((image) => image.path), controller.signal), fileSnapshot.length === 0 ? Promise.resolve([]) : prepareFiles(fileSnapshot.map((file) => file.path), controller.signal)]).then(([images, files]) => {
33678
35108
  if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
33679
35109
  prepareAbortRef.current = void 0;
33680
35110
  setPreparingImages(false);
@@ -33684,6 +35114,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33684
35114
  setCursor(0);
33685
35115
  draftImagesRef.current = [];
33686
35116
  setDraftImages([]);
35117
+ draftFilesRef.current = [];
35118
+ setDraftFiles([]);
33687
35119
  setCompletionIndex(0);
33688
35120
  setDismissedMenuValue(void 0);
33689
35121
  dismissNotice();
@@ -33692,13 +35124,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33692
35124
  recordHistory(text);
33693
35125
  }
33694
35126
  recall.current = beginRecall(recallSpace, "");
33695
- if (busy) steer(text, images);
33696
- else dispatch(text, images);
35127
+ const blocks = [...images, ...files];
35128
+ if (busy) steer(text, blocks, originSession);
35129
+ else dispatch(text, blocks, originSession);
33697
35130
  }, (reason) => {
33698
35131
  if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
33699
35132
  prepareAbortRef.current = void 0;
33700
35133
  setPreparingImages(false);
33701
- notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
35134
+ notify(`attachment submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
33702
35135
  });
33703
35136
  return;
33704
35137
  }
@@ -33711,10 +35144,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33711
35144
  setDismissedMenuValue(void 0);
33712
35145
  if (trimmed === "") return;
33713
35146
  dismissNotice();
33714
- if (!text.startsWith("/")) {
33715
- recordLocal(text);
33716
- recordHistory(text);
33717
- }
35147
+ recordLocal(text);
35148
+ recordHistory(text);
33718
35149
  recall.current = beginRecall(recallSpace, "");
33719
35150
  if (text === "/quit") {
33720
35151
  quit();
@@ -33793,6 +35224,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33793
35224
  openPlugin(text.slice(7).trim());
33794
35225
  return;
33795
35226
  }
35227
+ if (text === "/update") {
35228
+ openUpdate();
35229
+ return;
35230
+ }
35231
+ if (text === "/schedule") {
35232
+ openSchedule();
35233
+ return;
35234
+ }
33796
35235
  if (text === "/jobs" || text.startsWith("/jobs ")) {
33797
35236
  openJobs();
33798
35237
  return;
@@ -33805,6 +35244,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33805
35244
  openTheme();
33806
35245
  return;
33807
35246
  }
35247
+ if (text === "/animation" || text.startsWith("/animation ")) {
35248
+ const parsed = parseAnimationsArgument(text.slice(10));
35249
+ if (parsed === "toggle") applyAnimations(!animations);
35250
+ else if (parsed === "usage") notify("usage: /animation [on|off]", "info");
35251
+ else applyAnimations(parsed.enabled);
35252
+ return;
35253
+ }
33808
35254
  if (text === "/history") {
33809
35255
  openHistory();
33810
35256
  return;
@@ -33946,48 +35392,26 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
33946
35392
  text = text.replaceAll(PASTE_END_MARKER, "");
33947
35393
  }
33948
35394
  if (text === "") return;
33949
- const droppedPaths = text.length > 1 ? parsePastedImagePaths(text) : [];
33950
- if (droppedPaths.length > 0) {
33951
- insertDroppedImages(droppedPaths);
33952
- return;
35395
+ if (text.length > 1) {
35396
+ const dropped = parsePastedAttachmentPaths(text);
35397
+ if (dropped.images.length > 0 || dropped.files.length > 0) {
35398
+ insertDroppedAttachments(dropped.images, dropped.files);
35399
+ return;
35400
+ }
33953
35401
  }
33954
35402
  applyEdit(insertText(valueRef.current, cursorRef.current, text));
33955
35403
  }
33956
35404
  }, active);
33957
- const [waveTick, setWaveTick] = (0, import_react.useState)(null);
33958
- const wavePrevious = (0, import_react.useRef)({
33959
- tier: null,
33960
- style: null
33961
- });
35405
+ const waveKey = waveTier !== null && waveStyle !== null ? `${waveTier}:${waveStyle}` : null;
35406
+ const [wavePlayedKey, setWavePlayedKey] = (0, import_react.useState)(null);
33962
35407
  (0, import_react.useEffect)(() => {
33963
- const previous = wavePrevious.current;
33964
- wavePrevious.current = {
33965
- tier: waveTier,
33966
- style: waveStyle
33967
- };
33968
- if (waveTier === null) {
33969
- setWaveTick(null);
33970
- return;
33971
- }
33972
- if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
33973
- }, [waveTier, waveStyle]);
33974
- const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
33975
- (0, import_react.useEffect)(() => {
33976
- if (!waveActive) return;
33977
- const id = setInterval(() => {
33978
- setWaveTick((current) => current === null ? 0 : current + 1);
33979
- }, 33);
33980
- return () => {
33981
- clearInterval(id);
33982
- };
33983
- }, [waveActive]);
33984
- (0, import_react.useEffect)(() => {
33985
- if (waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 >= deepseekWaveDuration(waveTier, waveStyle)) setWaveTick(null);
35408
+ if (!animations && waveKey !== null && waveKey !== wavePlayedKey) setWavePlayedKey(waveKey);
33986
35409
  }, [
33987
- waveTick,
33988
- waveTier,
33989
- waveStyle
35410
+ animations,
35411
+ waveKey,
35412
+ wavePlayedKey
33990
35413
  ]);
35414
+ const waveArmed = waveKey !== null && waveKey !== wavePlayedKey;
33991
35415
  const tierActive = waveTier !== null;
33992
35416
  const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
33993
35417
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
@@ -34004,6 +35428,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
34004
35428
  (0, import_react.useEffect)(() => {
34005
35429
  onEditorRows(editorRowCount);
34006
35430
  }, [editorRowCount, onEditorRows]);
35431
+ const caretRowInWindow = Math.max(0, Math.min(caret.row - editorWindowStart, editorWindowRows - 1));
35432
+ useImeCursorAnchor(!frozen, imeCursorRowsUp({
35433
+ editorWindowRows,
35434
+ caretRowInWindow,
35435
+ rowsBelowComposer: anchorRowsBelow
35436
+ }), 2 + caret.column);
34007
35437
  (0, import_react.useEffect)(() => {
34008
35438
  onMenuRows(menuHeightRows);
34009
35439
  return () => onMenuRows(0);
@@ -34029,7 +35459,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
34029
35459
  bold: true
34030
35460
  }, warning), bandFill(2 + visibleColumns(warning))));
34031
35461
  }
34032
- const frozenLine = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
35462
+ const frozenLine = value === "" ? frozenHint ?? "type a message" : verboseLine(value, Math.max(1, columns - 6));
34033
35463
  return band((0, import_react.createElement)(Text, {
34034
35464
  backgroundColor: bandBg,
34035
35465
  wrap: "truncate-end"
@@ -34059,7 +35489,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
34059
35489
  }, index === 0 ? preparingImages ? (0, import_react.createElement)(Text, {
34060
35490
  color: inkColor(getPalette().warn),
34061
35491
  bold: true
34062
- }, "… ") : busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
35492
+ }, "… ") : busy ? (0, import_react.createElement)(BusyChase, { animated: animations }) : (0, import_react.createElement)(Text, {
34063
35493
  color: promptColor,
34064
35494
  bold: tierActive ? true : void 0
34065
35495
  }, `${promptGlyph} `) : " ", parts.before, parts.hasCaret ? (0, import_react.createElement)(Text, {
@@ -34068,100 +35498,26 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
34068
35498
  }, parts.caret) : null, placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : parts.after, bandFill(consumed)));
34069
35499
  }
34070
35500
  const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
34071
- const waveRow = () => {
34072
- const hues = deepseekWaveHues(waveTier);
34073
- const style = waveStyle;
34074
- const bandRgb = getPalette().composerBand;
34075
- const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows);
34076
- const totalBandRows = visibleRows.length + 2;
34077
- const waveBg = (row, column) => {
34078
- const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, totalBandRows);
34079
- return rgb === null ? bandBg : inkColor(rgb);
34080
- };
34081
- const blankBandRow = (row) => {
34082
- const blanks = [];
34083
- for (let column = 0; column < bandWidth; column += 1) blanks.push({
34084
- char: " ",
34085
- width: 1,
34086
- backgroundColor: waveBg(row, column)
34087
- });
34088
- return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
34089
- };
34090
- const cellIndexAtColumn = (cells, target) => {
34091
- let column = 0;
34092
- for (let index = 0; index < cells.length; index += 1) {
34093
- if (column === target) return index;
34094
- column += cells[index].width ?? visibleColumns(cells[index].char);
34095
- if (column > target) return void 0;
34096
- }
34097
- };
34098
- const editorWaveRows = visibleRows.map((row, visibleIndex) => {
34099
- const sourceIndex = editorWindowStart + visibleIndex;
34100
- const bandRow = visibleIndex + 1;
34101
- const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor);
34102
- const placeholder = sourceIndex === 0 && value === "" && !busy;
34103
- const cells = [];
34104
- let usedColumns = 0;
34105
- const push = (char, extra = {}) => {
34106
- const width = visibleColumns(char);
34107
- cells.push({
34108
- char,
34109
- width,
34110
- backgroundColor: waveBg(bandRow, usedColumns),
34111
- ...extra
34112
- });
34113
- usedColumns += width;
34114
- };
34115
- if (sourceIndex === 0) {
34116
- push(promptGlyph, {
34117
- color: promptColor,
34118
- bold: true
34119
- });
34120
- push(" ", { color: promptColor });
34121
- } else {
34122
- push(" ");
34123
- push(" ");
34124
- }
34125
- for (const span of splitGraphemes(parts.before)) push(span.text);
34126
- if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible });
34127
- const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
34128
- for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
34129
- while (usedColumns < bandWidth) push(" ");
34130
- const middleBandRow = Math.floor(totalBandRows / 2);
34131
- if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick, waveTier, style)) {
34132
- const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
34133
- const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
34134
- const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
34135
- if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
34136
- const cell = cells[indices[at]];
34137
- cell.char = word[at];
34138
- cell.width = 1;
34139
- cell.color = inkColor(deepseekWaveWordHue(at, hues));
34140
- cell.bold = true;
34141
- cell.dim = false;
34142
- }
34143
- }
34144
- if (bandRow === middleBandRow && (waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
34145
- const spark = deepseekWaveSpark(waveTick);
34146
- const lastIndex = cellIndexAtColumn(cells, bandWidth - 1);
34147
- if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
34148
- cells[lastIndex].char = spark;
34149
- cells[lastIndex].color = promptColor;
34150
- cells[lastIndex].bold = true;
34151
- cells[lastIndex].dim = false;
34152
- }
34153
- }
34154
- return (0, import_react.createElement)(Text, {
34155
- key: `editor-${sourceIndex}`,
34156
- wrap: "truncate-end"
34157
- }, ...waveRowSpans(cells));
34158
- });
34159
- return (0, import_react.createElement)(Box, {
34160
- flexDirection: "column",
34161
- width: bandWidth
34162
- }, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
34163
- };
34164
- return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && waveStyle !== null && !busy && !preparingImages ? waveRow() : band(staticEditor));
35501
+ return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, (0, import_react.createElement)(ComposerWave, {
35502
+ key: waveKey ?? "static",
35503
+ tier: waveTier ?? "deepseek",
35504
+ style: waveStyle ?? "wave",
35505
+ active: waveTier !== null && waveStyle !== null && !busy && !preparingImages && animations && waveArmed,
35506
+ onSettled: () => {
35507
+ if (waveKey !== null) setWavePlayedKey(waveKey);
35508
+ },
35509
+ fallback: band(staticEditor),
35510
+ bandWidth,
35511
+ bandBg,
35512
+ rows: editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows),
35513
+ windowStart: editorWindowStart,
35514
+ caretRow: caret.row,
35515
+ cursor: clampedCursor,
35516
+ caretVisible: cursorVisible,
35517
+ value,
35518
+ promptGlyph,
35519
+ promptColor
35520
+ }));
34165
35521
  }
34166
35522
  /** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
34167
35523
  function buildSettledRow(entry, index, showReasoning, columns) {
@@ -34339,12 +35695,19 @@ function App(props) {
34339
35695
  * to static while the prompt marker keeps the tier accent. The trigger
34340
35696
  * follows the applied model label (what the status bar actually shows),
34341
35697
  * never the initial paint, and the tier is derived from the label and
34342
- * cached at the switch. The 33ms tick itself lives inside Input, so the
34343
- * sweep re-renders only the composer band, not the whole tree, at 30fps;
34344
- * App owns the rarely-changing tier/style and Input starts the sweep
34345
- * whenever that pair changes. */
35698
+ * cached at the switch. The 33ms tick itself lives inside the ComposerWave
35699
+ * leaf, so the sweep re-renders only the composer band, not the whole tree,
35700
+ * at 30fps; App owns the rarely-changing tier/style and the leaf plays the
35701
+ * sweep exactly ONCE per pair change — an unchanged model+effort pair
35702
+ * (ordinary turns, image preparation, /animation toggles) never replays. */
34346
35703
  const [waveTier, setWaveTier] = (0, import_react.useState)(null);
34347
35704
  const [waveStyle, setWaveStyle] = (0, import_react.useState)(null);
35705
+ const [animations, setAnimations] = (0, import_react.useState)(props.animations ?? true);
35706
+ const applyAnimations = (enabled) => {
35707
+ setAnimations(enabled);
35708
+ props.saveAnimations?.(enabled);
35709
+ notify(`animations ${enabled ? "on" : "off"}`);
35710
+ };
34348
35711
  const previousModel = (0, import_react.useRef)(void 0);
34349
35712
  const previousEffort = (0, import_react.useRef)(props.effort);
34350
35713
  const previousStyle = (0, import_react.useRef)(void 0);
@@ -34468,6 +35831,8 @@ function App(props) {
34468
35831
  const [resumeOpen, setResumeOpen] = (0, import_react.useState)(false);
34469
35832
  const [pluginOpen, setPluginOpen] = (0, import_react.useState)(false);
34470
35833
  const [pluginQuery, setPluginQuery] = (0, import_react.useState)("");
35834
+ const [updateOpen, setUpdateOpen] = (0, import_react.useState)(false);
35835
+ const [scheduleOpen, setScheduleOpen] = (0, import_react.useState)(false);
34471
35836
  const [jobsOpen, setJobsOpen] = (0, import_react.useState)(false);
34472
35837
  const [statuslineOpen, setStatuslineOpen] = (0, import_react.useState)(false);
34473
35838
  const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
@@ -34540,7 +35905,7 @@ function App(props) {
34540
35905
  const agentRows = (0, import_react.useSyncExternalStore)(props.subagents.subscribe, props.subagents.getSnapshot);
34541
35906
  const approvalPending = approvalSnapshot.pending !== void 0;
34542
35907
  const questionPending = questionSnapshot.pending !== void 0;
34543
- const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
35908
+ const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
34544
35909
  (0, import_react.useEffect)(() => {
34545
35910
  if (!approvalPending && !questionPending) return;
34546
35911
  setModelOpen(false);
@@ -34552,6 +35917,8 @@ function App(props) {
34552
35917
  setPermissionOpen(false);
34553
35918
  setResumeOpen(false);
34554
35919
  setPluginOpen(false);
35920
+ setUpdateOpen(false);
35921
+ setScheduleOpen(false);
34555
35922
  setStatuslineOpen(false);
34556
35923
  setThemeOpen(false);
34557
35924
  setHistoryOpen(false);
@@ -34617,11 +35984,18 @@ function App(props) {
34617
35984
  const handleMenuRows = (0, import_react.useCallback)((rows) => {
34618
35985
  setMenuRows((current) => current === rows ? current : rows);
34619
35986
  }, []);
35987
+ const [statusBarRows, setStatusBarRows] = (0, import_react.useState)(1);
35988
+ const handleStatusRows = (0, import_react.useCallback)((rows) => {
35989
+ setStatusBarRows((current) => current === rows ? current : rows);
35990
+ }, []);
35991
+ const imeRowsBelowComposer = statusBarRows + 1;
34620
35992
  const composerEditorCap = composerMaxRows(terminalRows);
34621
35993
  const MENU_RESERVE_ROWS = 5;
34622
35994
  const dynamicRows = Math.max(1, terminalRows - 8 - MENU_RESERVE_ROWS - composerGutterRows - (composerRows - 1) - Math.max(0, menuRows - MENU_RESERVE_ROWS));
34623
35995
  const streamingActive = view.streaming !== "" || view.streamingReasoning !== "";
34624
35996
  const deepDivingVisible = busy && !streamingActive;
35997
+ const tabTitle = view.title === "" ? DEFAULT_TERMINAL_TITLE : view.title;
35998
+ useTerminalTitle(tabTitle);
34625
35999
  const allLiveLines = (0, import_react.useMemo)(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2), showReasoning)), [
34626
36000
  view.entries,
34627
36001
  settled,
@@ -34645,8 +36019,11 @@ function App(props) {
34645
36019
  const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length ? visibleLiveLines : visibleLiveLines.slice(-liveAudit.allocation.live);
34646
36020
  const auditedReasoningRows = liveAudit.allocation.reasoning;
34647
36021
  const auditedAnswerRows = liveAudit.allocation.answer;
34648
- const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
34649
- const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || jobsOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || todosOpen || verboseOpen && !approvalPending && !questionPending || diffView !== void 0 || approvalPending || questionPending;
36022
+ const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
36023
+ const inspectorVisible = verboseOpen && !approvalPending && !questionPending;
36024
+ const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || todosOpen || inspectorVisible || diffView !== void 0 || approvalPending || questionPending;
36025
+ const keyboardOwner = approvalPending ? "the approval prompt" : questionPending ? "the question" : diffView !== void 0 ? "the diff review" : modelOpen ? "/model" : helpOpen ? "/help" : modeOpen ? "/mode" : permissionOpen ? "/permission" : resumeOpen ? "/resume" : pluginOpen ? "/plugin" : updateOpen ? "/update" : scheduleOpen ? "/schedule" : jobsOpen ? "/jobs" : statuslineOpen ? "/statusline" : themeOpen ? "/theme" : historyOpen ? "/history" : agentsOpen ? "/agents" : subagentOpen ? "/subagent" : todosOpen ? "/todos" : inspectorVisible ? "history details" : void 0;
36026
+ const frozenHint = keyboardOwner === void 0 ? void 0 : `keys go to ${keyboardOwner} · esc ${approvalPending ? "rejects" : questionPending ? "cancels" : "closes"}`;
34650
36027
  const closeInspector = (0, import_react.useCallback)(() => {
34651
36028
  setVerboseOpen(false);
34652
36029
  }, []);
@@ -34904,7 +36281,10 @@ function App(props) {
34904
36281
  continuationPrefix: " ",
34905
36282
  dim: true,
34906
36283
  maxRows: auditedReasoningRows
34907
- }) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, { text: "✻ Thinking… (Ctrl/Alt+R to expand)" }) : (0, import_react.createElement)(StreamTail, {
36284
+ }) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, {
36285
+ text: "✻ Thinking… (Ctrl/Alt+R to expand)",
36286
+ animated: animations
36287
+ }) : (0, import_react.createElement)(StreamTail, {
34908
36288
  text: "Thinking… (Ctrl/Alt+R to expand)",
34909
36289
  prefix: "✻ ",
34910
36290
  continuationPrefix: " ",
@@ -34915,7 +36295,10 @@ function App(props) {
34915
36295
  dim: false,
34916
36296
  maxRows: auditedAnswerRows,
34917
36297
  prefix: " "
34918
- }, 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, {
36298
+ }, busy ? (0, import_react.createElement)(Caret, { animated: animations }) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, {
36299
+ since: view.busySince,
36300
+ animated: animations
36301
+ }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, {
34919
36302
  rows: agentRows,
34920
36303
  total: props.subagents.getTotalSeen()
34921
36304
  }) : void 0, todosOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoTodoListPanel, {
@@ -34987,6 +36370,14 @@ function App(props) {
34987
36370
  load: props.loadPlugins,
34988
36371
  initialQuery: pluginQuery,
34989
36372
  close: () => setPluginOpen(false)
36373
+ }) : void 0, updateOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(UpdatePanel, {
36374
+ probe: props.probeUpdate,
36375
+ apply: props.applyUpdate,
36376
+ notify: (text, tone) => notify(text, tone),
36377
+ close: () => setUpdateOpen(false)
36378
+ }) : void 0, scheduleOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(SchedulePanel, {
36379
+ rows: () => view.schedules,
36380
+ close: () => setScheduleOpen(false)
34990
36381
  }) : void 0, jobsOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(JobsPanel, {
34991
36382
  load: props.loadJobs,
34992
36383
  close: () => setJobsOpen(false)
@@ -35049,6 +36440,7 @@ function App(props) {
35049
36440
  }, (0, import_react.createElement)(Input, {
35050
36441
  active: inputActive,
35051
36442
  frozen: modalVisible,
36443
+ frozenHint,
35052
36444
  busy,
35053
36445
  descriptors,
35054
36446
  skills,
@@ -35106,6 +36498,8 @@ function App(props) {
35106
36498
  setPluginQuery(query);
35107
36499
  setPluginOpen(true);
35108
36500
  },
36501
+ openUpdate: () => setUpdateOpen(true),
36502
+ openSchedule: () => setScheduleOpen(true),
35109
36503
  openJobs: () => setJobsOpen(true),
35110
36504
  openStatusline: () => setStatuslineOpen(true),
35111
36505
  openTheme: () => setThemeOpen(true),
@@ -35153,7 +36547,10 @@ function App(props) {
35153
36547
  loadMentions: props.loadMentions,
35154
36548
  inspectImages: props.inspectImages,
35155
36549
  prepareImages: props.prepareImages,
35156
- cyclePermission: props.cyclePermission,
36550
+ inspectFiles: props.inspectFiles,
36551
+ prepareFiles: props.prepareFiles,
36552
+ sessionKey: props.sessionKey,
36553
+ cycleMode: props.cycleMode,
35157
36554
  exportTranscript: props.exportTranscript,
35158
36555
  renameTitle: props.renameTitle,
35159
36556
  copyLastResponse: props.copyLastResponse,
@@ -35164,9 +36561,13 @@ function App(props) {
35164
36561
  cancelQueued: props.cancelQueued,
35165
36562
  historyFill,
35166
36563
  historyConsumed,
36564
+ animations,
36565
+ applyAnimations,
35167
36566
  waveTier,
35168
36567
  waveStyle,
35169
36568
  maxRows: composerEditorCap,
36569
+ anchorRowsBelow: imeRowsBelowComposer,
36570
+ tabTitle,
35170
36571
  onEditorRows: handleEditorRows,
35171
36572
  onMenuRows: handleMenuRows
35172
36573
  }), (0, import_react.createElement)(StatusLine, {
@@ -35177,7 +36578,7 @@ function App(props) {
35177
36578
  branch: props.branch,
35178
36579
  sessionId: props.sessionId,
35179
36580
  title: view.title,
35180
- plan: view.plan,
36581
+ plan: view.plan || props.pendingPlan === true,
35181
36582
  permission: view.permission !== "" ? view.permission : props.permission,
35182
36583
  sandbox: view.sandbox,
35183
36584
  goal: view.goal === void 0 ? void 0 : {
@@ -35189,7 +36590,8 @@ function App(props) {
35189
36590
  stats: view.stats,
35190
36591
  busy,
35191
36592
  columns: terminalColumns,
35192
- items: statuslineItems
36593
+ items: statuslineItems,
36594
+ onRows: handleStatusRows
35193
36595
  })));
35194
36596
  }
35195
36597
  //#endregion
@@ -35441,26 +36843,37 @@ const internals = {
35441
36843
  mount: (element) => {
35442
36844
  const keyboardEnhanced = shouldEnableKeyboardEnhancement();
35443
36845
  const focusReporting = isVsCodeTerminalEnv();
35444
- process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
35445
- const tuiStdin = createSplitStdin(process.stdin);
35446
- const instance = render(element, {
35447
- exitOnCtrlC: false,
35448
- stdin: tuiStdin.stdin,
35449
- stdout: process.stdout
35450
- });
35451
- return {
35452
- rerender(element) {
35453
- instance.rerender(element);
35454
- },
35455
- unmount() {
35456
- try {
35457
- instance.unmount();
35458
- } finally {
35459
- tuiStdin.dispose();
35460
- process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
36846
+ if (process.stdin.isTTY === true) process.stdin.setRawMode?.(true);
36847
+ try {
36848
+ process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
36849
+ ensureVsCodeTabTitleSetting();
36850
+ const tuiStdin = createSplitStdin(process.stdin);
36851
+ const instance = render(element, {
36852
+ exitOnCtrlC: false,
36853
+ stdin: tuiStdin.stdin,
36854
+ stdout: process.stdout
36855
+ });
36856
+ return {
36857
+ rerender(element) {
36858
+ instance.rerender(element);
36859
+ },
36860
+ unmount() {
36861
+ try {
36862
+ process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
36863
+ } finally {
36864
+ try {
36865
+ instance.unmount();
36866
+ } finally {
36867
+ tuiStdin.dispose();
36868
+ if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false);
36869
+ }
36870
+ }
35461
36871
  }
35462
- }
35463
- };
36872
+ };
36873
+ } catch (error) {
36874
+ if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false);
36875
+ throw error;
36876
+ }
35464
36877
  },
35465
36878
  stderr: process.stderr
35466
36879
  };
@@ -35917,6 +37330,7 @@ function createTranscriptStore(replay) {
35917
37330
  const listeners = /* @__PURE__ */ new Set();
35918
37331
  let scheduled = false;
35919
37332
  let lastNotifyAt = 0;
37333
+ const attemptKeys = /* @__PURE__ */ new Map();
35920
37334
  const notify = () => {
35921
37335
  if (scheduled) return;
35922
37336
  scheduled = true;
@@ -35948,8 +37362,28 @@ function createTranscriptStore(replay) {
35948
37362
  dirty = true;
35949
37363
  notify();
35950
37364
  },
37365
+ applyStreamFrame(frame) {
37366
+ if (frame.type === "start") {
37367
+ attemptKeys.set(frame.attemptId, `${frame.turn}:${frame.step}`);
37368
+ return;
37369
+ }
37370
+ if (frame.type === "chunk") {
37371
+ const key = attemptKeys.get(frame.attemptId);
37372
+ if (key === void 0) return;
37373
+ if (!applyAssistantStreamChunk(acc, key, frame.time, frame.chunk)) return;
37374
+ dirty = true;
37375
+ notify();
37376
+ return;
37377
+ }
37378
+ attemptKeys.delete(frame.attemptId);
37379
+ if (frame.outcome.kind === "abandoned" && clearAssistantStream(acc)) {
37380
+ dirty = true;
37381
+ notify();
37382
+ }
37383
+ },
35951
37384
  reset() {
35952
37385
  acc = createReplayAccumulator();
37386
+ attemptKeys.clear();
35953
37387
  dirty = true;
35954
37388
  notify();
35955
37389
  }
@@ -36017,12 +37451,27 @@ function foldSubagentRow(previous, sessionId, event) {
36017
37451
  activity: "prompted",
36018
37452
  updatedAt: event.time
36019
37453
  };
36020
- case "assistant/chunk": return {
37454
+ case "assistant/attempt": return {
36021
37455
  ...base,
36022
37456
  state: "running",
36023
37457
  activity: "thinking…",
36024
37458
  updatedAt: event.time
36025
37459
  };
37460
+ case "subagent/catalog": {
37461
+ const mode = event.data.mode === "continuable" ? "continuable" : "one-shot";
37462
+ const label = event.data.label !== void 0 && event.data.label.trim() !== "" ? bound(event.data.label) : void 0;
37463
+ const nextLabel = label === void 0 ? base.label : label;
37464
+ const activity = label === void 0 ? `catalog · ${mode}` : `catalog · ${mode} · ${label}`;
37465
+ const state = previous === void 0 ? "idle" : base.state;
37466
+ if (nextLabel === base.label && state === base.state && activity === base.activity) return base;
37467
+ return {
37468
+ ...base,
37469
+ state,
37470
+ label: nextLabel,
37471
+ activity,
37472
+ updatedAt: event.time
37473
+ };
37474
+ }
36026
37475
  case "assistant/message": return {
36027
37476
  ...base,
36028
37477
  state: "idle",
@@ -36098,7 +37547,7 @@ function createSubagentFeed() {
36098
37547
  const counted = !seen.has(sessionId);
36099
37548
  if (counted) seen.add(sessionId);
36100
37549
  if (rows.length >= 8) {
36101
- const evict = rows.findIndex((row) => row.state === "done");
37550
+ const evict = rows.findIndex((row) => row.state !== "running");
36102
37551
  if (evict === -1) {
36103
37552
  if (counted) notify();
36104
37553
  return;
@@ -36158,8 +37607,8 @@ function watchSkills(ctx, fallbackCwd) {
36158
37607
  const listeners = /* @__PURE__ */ new Set();
36159
37608
  const reload = () => {
36160
37609
  const target = agent;
36161
- if (skills === void 0 || target === void 0) return;
36162
- Promise.resolve().then(() => skills.list({
37610
+ if (skills === void 0) return;
37611
+ Promise.resolve().then(() => skills.list(target === void 0 ? { cwd: fallbackCwd } : {
36163
37612
  cwd: target.session.header.cwd ?? fallbackCwd,
36164
37613
  scope: target
36165
37614
  })).then((summaries) => {
@@ -36174,13 +37623,18 @@ function watchSkills(ctx, fallbackCwd) {
36174
37623
  for (const listener of listeners) listener();
36175
37624
  }).catch((cause) => {
36176
37625
  if (agent !== target) return;
37626
+ const nextError = cause instanceof Error ? cause.message : String(cause);
36177
37627
  if (loadedFor !== target) rows = [];
36178
- else rows = [...rows];
36179
- error = cause instanceof Error ? cause.message : String(cause);
37628
+ const errorChanged = nextError !== error;
37629
+ error = nextError;
37630
+ if (!errorChanged) return;
36180
37631
  for (const listener of listeners) listener();
36181
37632
  });
36182
37633
  };
36183
- if (skills !== void 0) ctx.on("skills/change", reload);
37634
+ if (skills !== void 0) {
37635
+ ctx.on("skills/change", reload);
37636
+ reload();
37637
+ }
36184
37638
  return {
36185
37639
  get rows() {
36186
37640
  return rows;
@@ -36215,16 +37669,22 @@ function watchSkills(ctx, fallbackCwd) {
36215
37669
  * @param sessionId - the full session identity for the header.
36216
37670
  * @returns the complete markdown text.
36217
37671
  */
37672
+ /** Both user and queued rows export the same attachment label block. */
37673
+ const attachmentLabels = (entry) => [imageLabels(entry.images), fileLabels(entry.files)].filter((label) => label !== "").join("\n");
36218
37674
  function buildExportMarkdown(view, sessionId) {
36219
37675
  const out = [
36220
37676
  view.title === "" ? `# dsh session ${sessionId}` : `# ${view.title}`,
36221
37677
  `> session ${sessionId}`,
36222
37678
  ""
36223
37679
  ];
37680
+ if (view.systemPrompt !== "") out.push("<details><summary>system prompt</summary>", "", view.systemPrompt, "", "</details>", "");
36224
37681
  for (const entry of view.entries) switch (entry.kind) {
36225
37682
  case "user":
36226
37683
  if (entry.notice) out.push(`> ⤷ context: ${entry.text}`, "");
36227
- else out.push("## user", "", entry.text, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
37684
+ else {
37685
+ const attachments = attachmentLabels(entry);
37686
+ out.push("## user", "", entry.text, ...attachments === "" ? [] : [attachments], "");
37687
+ }
36228
37688
  break;
36229
37689
  case "assistant":
36230
37690
  if (entry.reasoning !== "") out.push("<details><summary>thinking</summary>", "", entry.reasoning, "", "</details>", "");
@@ -36257,7 +37717,7 @@ function buildExportMarkdown(view, sessionId) {
36257
37717
  out.push(`> files changed: ${entry.paths.join(", ")}`, "");
36258
37718
  break;
36259
37719
  case "pending":
36260
- out.push("## user", "", entry.text, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
37720
+ out.push("## user", "", entry.text, ...attachmentLabels(entry) === "" ? [] : [attachmentLabels(entry)], "");
36261
37721
  break;
36262
37722
  default: assertNever(entry, "transcript entry kind");
36263
37723
  }
@@ -36872,13 +38332,6 @@ function selectPermission(service, session, preset) {
36872
38332
  if (session !== void 0) service.set(session, preset);
36873
38333
  return preset;
36874
38334
  }
36875
- /** Cycle table order from the active, pending, or configured-default value. */
36876
- function cyclePermission(service, session, pending) {
36877
- if (service.names.length === 0) return "";
36878
- const at = service.names.indexOf(effectivePermission(service, session, pending));
36879
- const next = service.names[(at + 1) % service.names.length] ?? "";
36880
- return next === "" ? "" : selectPermission(service, session, next);
36881
- }
36882
38335
  /** Materialize a pre-session choice after Harness creates the first session. */
36883
38336
  function applyPendingPermission(service, session, pending) {
36884
38337
  if (pending !== void 0 && effectivePermission(service, session, void 0) !== pending) selectPermission(service, session, pending);
@@ -36927,6 +38380,120 @@ function listPluginRows(ctx) {
36927
38380
  return rows;
36928
38381
  }
36929
38382
  //#endregion
38383
+ //#region src/update.ts
38384
+ /**
38385
+ * Child-process adapter for the launcher update pipeline. The launcher
38386
+ * (bin/deepseek.mjs) stays the single owner of update semantics — plan,
38387
+ * guards, and the aligned install sequence — so the TUI only spawns
38388
+ * `update --json` (read-only probe) and `update --apply` (streamed run)
38389
+ * and never re-implements version-line decisions.
38390
+ */
38391
+ /** The launcher entrypoint that ships beside this bundle (lib/../bin). */
38392
+ function launcherUpdateCommand(args, moduleUrl = import.meta.url) {
38393
+ return {
38394
+ command: process.execPath,
38395
+ args: [fileURLToPath(new URL("../bin/deepseek.mjs", moduleUrl)), ...args]
38396
+ };
38397
+ }
38398
+ /**
38399
+ * Split a streamed chunk sequence into complete display lines: CR is
38400
+ * stripped, a chunk boundary may split a line, and the trailing partial
38401
+ * stays pending until its newline arrives (npm writes whole lines, but a
38402
+ * pipe may cut anywhere). Blank lines carry no progress information and
38403
+ * are dropped so the panel budget is not spent on gaps.
38404
+ */
38405
+ function createLineSplitter(onLine) {
38406
+ let pending = "";
38407
+ return (chunk) => {
38408
+ pending += chunk;
38409
+ for (let index = pending.indexOf("\n"); index >= 0; index = pending.indexOf("\n")) {
38410
+ const line = pending.slice(0, index).replace(/\r$/u, "");
38411
+ pending = pending.slice(index + 1);
38412
+ if (line !== "") onLine(line);
38413
+ }
38414
+ };
38415
+ }
38416
+ /**
38417
+ * Probe the aligned update status. Read-only: `update --json` never
38418
+ * installs anything. The probe is bounded (npm view may hang on a broken
38419
+ * network) and resolves with the parsed status.
38420
+ */
38421
+ async function probeLauncherUpdate(spawnProcess = spawn) {
38422
+ const command = launcherUpdateCommand(["update", "--json"]);
38423
+ return await new Promise((resolve, reject) => {
38424
+ const child = spawnProcess(command.command, command.args, {
38425
+ stdio: [
38426
+ "ignore",
38427
+ "pipe",
38428
+ "pipe"
38429
+ ],
38430
+ windowsHide: true
38431
+ });
38432
+ const timer = setTimeout(() => {
38433
+ child.kill();
38434
+ reject(/* @__PURE__ */ new Error("update probe timed out"));
38435
+ }, 3e4);
38436
+ let stdout = "";
38437
+ let stderr = "";
38438
+ child.stdout?.on("data", (chunk) => {
38439
+ stdout += chunk.toString();
38440
+ });
38441
+ child.stderr?.on("data", (chunk) => {
38442
+ stderr += chunk.toString();
38443
+ });
38444
+ child.once("error", (error) => {
38445
+ clearTimeout(timer);
38446
+ reject(/* @__PURE__ */ new Error(`update probe failed to start: ${error.message}`));
38447
+ });
38448
+ child.once("exit", (code) => {
38449
+ clearTimeout(timer);
38450
+ if (code === 0) {
38451
+ try {
38452
+ resolve(JSON.parse(stdout));
38453
+ } catch {
38454
+ reject(/* @__PURE__ */ new Error("update probe returned an unreadable status"));
38455
+ }
38456
+ return;
38457
+ }
38458
+ const tail = stderr.trim().split(/\r?\n/u).pop() ?? "";
38459
+ reject(/* @__PURE__ */ new Error(`update probe failed${tail === "" ? "" : `: ${tail}`}`));
38460
+ });
38461
+ });
38462
+ }
38463
+ /**
38464
+ * Run the aligned update (`update --apply`) as a child process and stream
38465
+ * its sanitized progress lines to the caller. Resolves with the child
38466
+ * exit code (0 success); rejects only when the process could not start.
38467
+ * No timeout: an npm install may legitimately take minutes.
38468
+ */
38469
+ function applyLauncherUpdate(onLine, spawnProcess = spawn) {
38470
+ const command = launcherUpdateCommand(["update", "--apply"]);
38471
+ return new Promise((resolve, reject) => {
38472
+ const child = spawnProcess(command.command, command.args, {
38473
+ stdio: [
38474
+ "ignore",
38475
+ "pipe",
38476
+ "pipe"
38477
+ ],
38478
+ windowsHide: true
38479
+ });
38480
+ const split = createLineSplitter(onLine);
38481
+ child.stdout?.on("data", (chunk) => {
38482
+ split(chunk.toString());
38483
+ });
38484
+ child.stderr?.on("data", (chunk) => {
38485
+ split(chunk.toString());
38486
+ });
38487
+ child.once("error", (error) => {
38488
+ reject(/* @__PURE__ */ new Error(`update failed to start: ${error.message}`));
38489
+ });
38490
+ child.once("close", (code, signal) => {
38491
+ split("\n");
38492
+ resolve(code ?? (signal === null ? 0 : 1));
38493
+ });
38494
+ });
38495
+ }
38496
+ //#endregion
36930
38497
  //#region src/settings-file.ts
36931
38498
  /**
36932
38499
  * Serialized, crash-atomic persistence for the small user-level JSON files
@@ -37108,6 +38675,43 @@ async function runQuitSequence(steps, exit, onError) {
37108
38675
  return started;
37109
38676
  }
37110
38677
  /**
38678
+ * Whether a tagged submission still belongs to the active session. Attachment
38679
+ * prepares resolve on the microtask timeline, while a queued session switch
38680
+ * remounts the app asynchronously — the composing instance's unmount cleanup
38681
+ * runs too late to abort, so the delivery itself carries the composing
38682
+ * session's full id and the runner drops it here when the world moved on.
38683
+ * An untagged (synchronous) or pending-session ('') submission always passes.
38684
+ */
38685
+ function submissionBelongsToSession(origin, activeSessionId) {
38686
+ return origin === void 0 || origin === "" || origin === activeSessionId;
38687
+ }
38688
+ /**
38689
+ * Decide the next Shift+Tab station. The cycle keeps the preset table's
38690
+ * own order (most restrictive first) and inserts ONE plan station between
38691
+ * the most restrictive preset and the wrap target: with the shipped three
38692
+ * presets the user sees workspace-write → danger-full-access → read-only
38693
+ * → plan → workspace-write. Plan IS the most restrictive preset plus the
38694
+ * plan prompt layer — entering it switches nothing (the cycle is already
38695
+ * parked on read-only), and leaving it lands on the next preset after the
38696
+ * most restrictive one. Without the /plan command the cycle is exactly the
38697
+ * preset table.
38698
+ */
38699
+ function planCycleDecision(input) {
38700
+ const names = input.names;
38701
+ if (names.length === 0) return void 0;
38702
+ const first = names[0];
38703
+ if (input.inPlan) return {
38704
+ kind: "plan-off",
38705
+ preset: names[1] ?? first
38706
+ };
38707
+ const at = names.indexOf(input.current);
38708
+ if (at === 0 && input.planAvailable) return { kind: "plan-on" };
38709
+ return {
38710
+ kind: "permission",
38711
+ preset: names[(at + 1) % names.length] ?? first
38712
+ };
38713
+ }
38714
+ /**
37111
38715
  * Order-preserving gate for composer input while the startup prompt/images
37112
38716
  * are still preparing. Anything submitted before the startup delivery settles
37113
38717
  * queues and flushes afterwards in submit order, so the initial request can
@@ -37158,7 +38762,7 @@ async function resolveTarget(startup, persistence, cwd) {
37158
38762
  };
37159
38763
  if (startup.kind === "named") {
37160
38764
  if (persistence !== void 0) {
37161
- if ((await persistence.list()).some((header) => header.id === startup.sessionId)) throw new Error(`session "${startup.sessionId}" already exists; use --resume to continue it`);
38765
+ if ((await persistence.list()).map((snapshot) => snapshot.header).some((header) => header.id === startup.sessionId)) throw new Error(`session "${startup.sessionId}" already exists; use --resume to continue it`);
37162
38766
  }
37163
38767
  return {
37164
38768
  sessionId: startup.sessionId,
@@ -37167,7 +38771,7 @@ async function resolveTarget(startup, persistence, cwd) {
37167
38771
  };
37168
38772
  }
37169
38773
  if (persistence === void 0) throw new Error("cannot resolve the requested session: session persistence is not configured");
37170
- const headers = await persistence.list();
38774
+ const headers = (await persistence.list()).map((snapshot) => snapshot.header);
37171
38775
  if (startup.kind === "resume") {
37172
38776
  const matched = matchSessionId(headers, startup.sessionId);
37173
38777
  if (isSubagentSession(matched)) throw new Error("subagent conversations are read-only; resume a root session");
@@ -37226,12 +38830,12 @@ async function run(ctx, startup, io) {
37226
38830
  const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
37227
38831
  let mode = next.resume ? next.mode : next.mode ?? pendingMode;
37228
38832
  if (!next.resume) mode = (await presets.resolve(normalizePresetId(mode ?? presets.defaultId))).id;
37229
- const setup = async (agentCtx) => {
37230
- const sessionPreset = next.resume ? resolvePreset(agentCtx.agent.session) : mode;
38833
+ const setup = async (agentCtx, agent) => {
38834
+ const sessionPreset = next.resume ? resolvePreset(agent.session) : mode;
37231
38835
  mode = (await presets.mount(agentCtx, sessionPreset)).id;
37232
38836
  installModelSelection(agentCtx, {
37233
38837
  get current() {
37234
- return resolveEffectiveSelection(selectionState.picked, agentCtx.agent?.session.requestHeader()?.config, currentDefaults());
38838
+ return resolveEffectiveSelection(selectionState.picked, agent.session.requestHeader()?.config, currentDefaults());
37235
38839
  },
37236
38840
  set current(value) {
37237
38841
  selectionState.picked = value;
@@ -37257,8 +38861,9 @@ async function run(ctx, startup, io) {
37257
38861
  cwd: nextCwd,
37258
38862
  agentPreset: mode,
37259
38863
  ...next.parentSession === void 0 ? {} : { parentSession: next.parentSession },
37260
- ...next.seedLength === void 0 ? {} : { seedLength: next.seedLength }
38864
+ ...next.seedLength === void 0 ? {} : { isSeeded: true }
37261
38865
  },
38866
+ ...next.seedLength === void 0 ? {} : { inheritedEventCount: SessionLogOffset(next.seedLength) },
37262
38867
  ...next.seed === void 0 ? {} : { seed: next.seed },
37263
38868
  agentOptions: seedOptions,
37264
38869
  signal: quitAbort.signal,
@@ -37292,6 +38897,37 @@ async function run(ctx, startup, io) {
37292
38897
  /** Permission preset selected before the first session exists. */
37293
38898
  let pendingPermission;
37294
38899
  /**
38900
+ * Plan-mode choice made before the first session exists: materialized as a
38901
+ * /plan registry command delivered ahead of the first queued input when the
38902
+ * session composes, so the first assembled step already plans.
38903
+ */
38904
+ let pendingPlan = false;
38905
+ /**
38906
+ * Whether the pre-session effective preset composes plan mode, answered by
38907
+ * the presets service composition inventory (minimal does not). Cached and
38908
+ * refreshed whenever the pending mode moves; unknown reads as unavailable
38909
+ * so one keypress at most lands before the answer arrives.
38910
+ */
38911
+ let preSessionPlanAvailable = false;
38912
+ let preSessionPlanKnown = false;
38913
+ const refreshPreSessionPlan = () => {
38914
+ if (presets === void 0) {
38915
+ preSessionPlanAvailable = false;
38916
+ preSessionPlanKnown = true;
38917
+ return;
38918
+ }
38919
+ preSessionPlanKnown = false;
38920
+ presets.compositionInventory().then((inventory) => {
38921
+ const id = pendingMode ?? normalizePresetId(presets.defaultId);
38922
+ preSessionPlanAvailable = inventory.some((composition) => composition.id === id && composition.rows.some((row) => row.moduleName === "@deepseek-ai/dsh-plan-mode" && row.enabled !== false));
38923
+ preSessionPlanKnown = true;
38924
+ }, () => {
38925
+ preSessionPlanAvailable = false;
38926
+ preSessionPlanKnown = true;
38927
+ });
38928
+ };
38929
+ refreshPreSessionPlan();
38930
+ /**
37295
38931
  * Monotonic session epoch: bumped on every successful activation, on every
37296
38932
  * first-session creation, and on quit. Async callbacks (mention prepares,
37297
38933
  * command executions) capture it at call time and drop their result when it
@@ -37335,10 +38971,15 @@ async function run(ctx, startup, io) {
37335
38971
  if (session === void 0) return;
37336
38972
  if (subject.id === session.id) {
37337
38973
  store.apply(event);
38974
+ if (event.type === "subagent/catalog" && event.data.childId !== "") subagents.apply(event.data.childId, event);
37338
38975
  return;
37339
38976
  }
37340
38977
  if (subject.header.parentSession === session.id && subject.header.origin === "subagent") subagents.apply(subject.id, event);
37341
38978
  });
38979
+ ctx.on("agent/assistant-stream", ({ agent: source, frame }) => {
38980
+ if (agent === void 0 || source.id !== agent.id) return;
38981
+ store.applyStreamFrame(frame);
38982
+ });
37342
38983
  const commands = watchCommands(ctx);
37343
38984
  if (agent !== void 0) commands.setAgent(agent);
37344
38985
  const skills = watchSkills(ctx, cwd);
@@ -37410,6 +39051,19 @@ async function run(ctx, startup, io) {
37410
39051
  bridge.notify("theme save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
37411
39052
  });
37412
39053
  };
39054
+ const animationsPath = join(homedir(), ".dsh", "dsh-code", "animations.json");
39055
+ let animationsEnabled = true;
39056
+ let animationsWarning;
39057
+ try {
39058
+ animationsEnabled = parseAnimationsPref((JSON.parse(readFileSync(animationsPath, "utf8")) ?? {}).animations);
39059
+ } catch (error) {
39060
+ if (error.code !== "ENOENT") animationsWarning = error instanceof Error ? error.message : String(error);
39061
+ }
39062
+ const saveAnimations = (enabled) => {
39063
+ settingsPersistence.save(animationsPath, JSON.stringify({ animations: enabled }, null, 2) + "\n").catch((writeError) => {
39064
+ bridge.notify("animations save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
39065
+ });
39066
+ };
37413
39067
  const historyPath = join(homedir(), ".dsh", "dsh-code", "history.jsonl");
37414
39068
  let inputHistory = [];
37415
39069
  let historyWriteChain = Promise.resolve();
@@ -37651,6 +39305,10 @@ async function run(ctx, startup, io) {
37651
39305
  abortPendingControllers();
37652
39306
  epoch += 1;
37653
39307
  const queued = pendingInputs.splice(0);
39308
+ if (pendingPlan) {
39309
+ pendingPlan = false;
39310
+ deliverLine("/plan", "followup");
39311
+ }
37654
39312
  for (const item of queued) deliverLine(item.text, item.mode, item.images);
37655
39313
  } finally {
37656
39314
  creating = false;
@@ -37697,7 +39355,8 @@ async function run(ctx, startup, io) {
37697
39355
  });
37698
39356
  };
37699
39357
  /** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
37700
- const dispatch = (text, images = []) => {
39358
+ const dispatch = (text, images = [], origin) => {
39359
+ if (!submissionBelongsToSession(origin, session?.id)) return;
37701
39360
  send(text, "followup", images);
37702
39361
  };
37703
39362
  /**
@@ -37705,7 +39364,8 @@ async function run(ctx, startup, io) {
37705
39364
  * boundary (the inbox delivers between steps); an idle driver just starts
37706
39365
  * a turn, so this doubles as the busy-state submit path.
37707
39366
  */
37708
- const steer = (text, images = []) => {
39367
+ const steer = (text, images = [], origin) => {
39368
+ if (!submissionBelongsToSession(origin, session?.id)) return;
37709
39369
  send(text, "steer", images);
37710
39370
  };
37711
39371
  /** Interrupt the running turn (Esc); true when a turn was actually cancelled. */
@@ -37732,24 +39392,56 @@ async function run(ctx, startup, io) {
37732
39392
  return selected;
37733
39393
  };
37734
39394
  /**
37735
- * Cycle to the next permission preset (Shift+Tab). Before the first session,
37736
- * the choice remains process-local and is materialized when Harness creates
37737
- * that session; afterwards the canonical service writes durable events.
39395
+ * Shift+Tab mode cycle: permission presets in table order, then the plan
39396
+ * station when the composition offers the /plan command (preset-mounted,
39397
+ * so minimal sessions and the pre-session state cycle permissions only).
39398
+ * Plan transitions submit the upstream registry command — it stays the
39399
+ * single owner of plan state; the TUI renders the durable plan/mode event
39400
+ * it appends. Returns the notice label, or '' when nothing changed.
37738
39401
  */
37739
- const cyclePermission$1 = () => {
39402
+ const cycleMode = () => {
37740
39403
  if (permissionPresets === void 0 || permissionPresets.names.length === 0) {
37741
39404
  bridge.notify("permission presets are not mounted in this composition", "warning");
37742
39405
  return "";
37743
39406
  }
37744
39407
  try {
37745
- const next = cyclePermission(permissionPresets, session, pendingPermission);
37746
- if (session === void 0 && next !== "") {
37747
- pendingPermission = next;
39408
+ const preSession = session === void 0;
39409
+ if (preSession && !preSessionPlanKnown) refreshPreSessionPlan();
39410
+ const decision = planCycleDecision({
39411
+ names: permissionPresets.names,
39412
+ current: effectivePermission(permissionPresets, session, pendingPermission),
39413
+ inPlan: preSession ? pendingPlan : store.getView().plan === true,
39414
+ planAvailable: preSession ? preSessionPlanAvailable : commands.descriptors.some((descriptor) => descriptor.name === "plan")
39415
+ });
39416
+ if (decision === void 0) return "";
39417
+ if (decision.kind === "permission") {
39418
+ const next = selectPermission(permissionPresets, session, decision.preset);
39419
+ if (preSession) {
39420
+ pendingPermission = next;
39421
+ renderCurrent();
39422
+ }
39423
+ return `permission → ${next}`;
39424
+ }
39425
+ if (decision.kind === "plan-on") {
39426
+ if (preSession) {
39427
+ pendingPlan = true;
39428
+ renderCurrent();
39429
+ return "plan → on (applies to the first session)";
39430
+ }
39431
+ send("/plan", "followup");
39432
+ return "plan → on";
39433
+ }
39434
+ if (preSession) {
39435
+ pendingPlan = false;
39436
+ pendingPermission = decision.preset;
37748
39437
  renderCurrent();
39438
+ return `plan → off · permission → ${decision.preset}`;
37749
39439
  }
37750
- return next;
39440
+ send("/plan off", "followup");
39441
+ selectPermission(permissionPresets, session, decision.preset);
39442
+ return `plan → off · permission → ${decision.preset}`;
37751
39443
  } catch (error) {
37752
- bridge.notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
39444
+ bridge.notify(`mode change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
37753
39445
  return "";
37754
39446
  }
37755
39447
  };
@@ -37833,14 +39525,17 @@ async function run(ctx, startup, io) {
37833
39525
  const loadSessions = async (options, signal) => {
37834
39526
  if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
37835
39527
  const records = await sessionQuery.listSessions(signal);
39528
+ const root = jsonlSessionRoot(persistence);
37836
39529
  const updated = /* @__PURE__ */ new Map();
37837
- for (const record of records) {
37838
- const location = persistence?.locate(record.header);
37839
- if (location === void 0) continue;
39530
+ if (root !== void 0) await Promise.all(records.map(async (record) => {
37840
39531
  try {
37841
- updated.set(record.header.id, (await stat(location.path)).mtimeMs);
39532
+ const dir = sessionDirectoryFor(root, record.header.cwd, record.header.id);
39533
+ const entries = await readdir(dir, { withFileTypes: true });
39534
+ const stats = await Promise.all(entries.filter((entry) => entry.isFile() && isSessionArtifactName(entry.name)).map((entry) => stat(join(dir, entry.name))));
39535
+ const newest = Math.max(...stats.map((info) => info.mtimeMs));
39536
+ if (Number.isFinite(newest)) updated.set(record.header.id, newest);
37842
39537
  } catch {}
37843
- }
39538
+ }));
37844
39539
  const projected = projectSessionRows(records, options, updated);
37845
39540
  const page = projected.slice(0, 32);
37846
39541
  if (page.length === 0) return projected;
@@ -37856,11 +39551,11 @@ async function run(ctx, startup, io) {
37856
39551
  * 1. `planSessionDeletion` collects the subtree and refuses when the root
37857
39552
  * or ANY member is live (a live child would outlive its deleted
37858
39553
  * parent), ordering the plan children-first.
37859
- * 2. Every plan node must locate to a guarded artifact directory
37860
- * (`encodeSegment(id)`/`session.jsonl` layout). Backends without a
37861
- * locatable artifact (SQLite) refuse the WHOLE deletion here — no
37862
- * file has been touched yet, so a backend or layout surprise can
37863
- * never strand a half-deleted subtree.
39554
+ * 2. Every plan node must derive to a guarded artifact directory
39555
+ * (`encodeSegment(id)` layout beneath the backend's config root).
39556
+ * Backends without a derivable artifact (non-JSONL) refuse the WHOLE
39557
+ * deletion here — no file has been touched yet, so a backend or layout
39558
+ * surprise can never strand a half-deleted subtree.
37864
39559
  * 3. Artifacts are removed children-first: only an I/O error mid-delete
37865
39560
  * can stop it short (reported with removed/total counts), leaving the
37866
39561
  * shallowest lineage intact.
@@ -37874,22 +39569,23 @@ async function run(ctx, startup, io) {
37874
39569
  const records = await sessionQuery.listSessions();
37875
39570
  const plan = planSessionDeletion(records, id);
37876
39571
  if (!plan.ok) return plan.reason;
39572
+ const root = jsonlSessionRoot(persistence);
39573
+ if (root === void 0) return "session backend exposes no deletable artifact (deletion is unsupported on this backend)";
37877
39574
  const byId = new Map(records.map((record) => [record.header.id, record]));
37878
39575
  const dirs = /* @__PURE__ */ new Map();
37879
39576
  for (const node of plan.nodes) {
37880
39577
  const record = byId.get(node.id);
37881
39578
  if (record === void 0) return `no persisted session matches "${node.id}"`;
37882
- const location = persistence?.locate(record.header);
37883
- if (location === void 0) return `session backend exposes no deletable artifact for ${node.id.slice(-12)} (deletion is unsupported on this backend)`;
37884
- const dir = sessionArtifactDirectory(location.path, node.id);
37885
- if (dir === void 0) return `refusing to delete: unexpected artifact layout at ${location.path}`;
39579
+ const dir = sessionArtifactDirectory(sessionDirectoryFor(root, record.header.cwd, node.id), node.id);
39580
+ if (dir === void 0) return `refusing to delete: unexpected artifact layout for ${node.id.slice(-12)}`;
37886
39581
  dirs.set(node.id, dir);
37887
39582
  }
37888
39583
  let removed = 0;
37889
39584
  for (const node of plan.nodes) {
37890
39585
  const dir = dirs.get(node.id);
37891
39586
  try {
37892
- for (const name of SESSION_ARTIFACT_NAMES) await rm(join(dir, name), { force: true });
39587
+ const entries = await readdir(dir, { withFileTypes: true });
39588
+ for (const entry of entries) if (entry.isFile() && isSessionArtifactName(entry.name)) await rm(join(dir, entry.name), { force: true });
37893
39589
  await rm(dir, {
37894
39590
  force: true,
37895
39591
  recursive: false
@@ -37958,6 +39654,7 @@ async function run(ctx, startup, io) {
37958
39654
  subagents.reset();
37959
39655
  pendingMode = void 0;
37960
39656
  pendingPermission = void 0;
39657
+ pendingPlan = false;
37961
39658
  } catch (error) {
37962
39659
  active = previous;
37963
39660
  agent = previous?.agent;
@@ -38127,6 +39824,7 @@ async function run(ctx, startup, io) {
38127
39824
  const permission = permissionPresets === void 0 ? currentView.permission : effectivePermission(permissionPresets, session, pendingPermission);
38128
39825
  return (0, import_react.createElement)(App, {
38129
39826
  key: session?.id ?? "pending",
39827
+ sessionKey: session?.id ?? "",
38130
39828
  store,
38131
39829
  approval,
38132
39830
  questions,
@@ -38142,6 +39840,8 @@ async function run(ctx, startup, io) {
38142
39840
  resumed: active?.resumed ?? false,
38143
39841
  mode: active?.mode ?? pendingMode ?? normalizePresetId(presets.defaultId),
38144
39842
  permission,
39843
+ /** Pre-session plan choice for the status badge until a session composes. */
39844
+ pendingPlan: session === void 0 && pendingPlan,
38145
39845
  dispatch,
38146
39846
  steer,
38147
39847
  interrupt,
@@ -38164,7 +39864,9 @@ async function run(ctx, startup, io) {
38164
39864
  loadMentions: (query, signal) => mentions.candidates(query, signal),
38165
39865
  inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
38166
39866
  prepareImages: (paths, signal) => saveImagePaths(paths, ctx.get("attachments"), signal),
38167
- cyclePermission: cyclePermission$1,
39867
+ inspectFiles: (paths) => inspectFilePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
39868
+ prepareFiles: (paths, signal) => saveFilePaths(paths, ctx.get("attachments"), signal),
39869
+ cycleMode,
38168
39870
  setPermission: setPermissionAction,
38169
39871
  selectModel,
38170
39872
  subagentModel: subagentModelLabel(),
@@ -38197,11 +39899,15 @@ async function run(ctx, startup, io) {
38197
39899
  switchSession,
38198
39900
  cancelSessionSwitch,
38199
39901
  loadPlugins: () => listPluginRows(ctx),
39902
+ probeUpdate: () => probeLauncherUpdate(),
39903
+ applyUpdate: (onLine) => applyLauncherUpdate(onLine),
38200
39904
  loadJobs: () => listJobs(ctx, active?.agent),
38201
39905
  statusline: statuslineItems,
38202
39906
  saveStatusline,
38203
39907
  applyEditorKeys,
38204
39908
  saveTheme,
39909
+ animations: animationsEnabled,
39910
+ saveAnimations,
38205
39911
  history: inputHistory,
38206
39912
  recordHistory,
38207
39913
  cancelQueued,
@@ -38240,6 +39946,9 @@ async function run(ctx, startup, io) {
38240
39946
  if (themeWarning !== void 0) setTimeout(() => {
38241
39947
  bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
38242
39948
  }, 50);
39949
+ if (animationsWarning !== void 0) setTimeout(() => {
39950
+ bridge.notify("animations config unreadable, animations stay on: " + animationsWarning, "warning");
39951
+ }, 50);
38243
39952
  resolveEditorKeysStartupHint(editorKeysEnv).then((hint) => {
38244
39953
  if (hint === void 0) return;
38245
39954
  setTimeout(() => {
@@ -38287,4 +39996,4 @@ function apply(ctx, config) {
38287
39996
  });
38288
39997
  }
38289
39998
  //#endregion
38290
- export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence };
39999
+ export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, planCycleDecision, resolveTarget, runQuitSequence, submissionBelongsToSession };