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.
- package/README.en.md +338 -286
- package/README.md +68 -16
- package/bin/deepseek.mjs +204 -4
- package/cordis.patch.yml +105 -7
- package/lib/index.mjs +2148 -439
- package/lib/session-query.mjs +149 -0
- package/lib/types/app.d.ts +34 -8
- package/lib/types/attachments.d.ts +36 -4
- package/lib/types/index.d.ts +38 -2
- package/lib/types/kernel-panels.d.ts +23 -0
- package/lib/types/provider-settings.d.ts +6 -11
- package/lib/types/render/animations.d.ts +74 -7
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/export.d.ts +0 -6
- package/lib/types/render/fuzzy.d.ts +21 -0
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/projection.d.ts +80 -4
- package/lib/types/render/status.d.ts +1 -1
- package/lib/types/session-directory.d.ts +48 -13
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/store.d.ts +3 -0
- package/lib/types/terminal-title.d.ts +58 -0
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +66 -0
- package/package.json +307 -162
- package/src/app.ts +730 -266
- package/src/attachments.ts +110 -11
- package/src/commands.ts +35 -5
- package/src/index.ts +1986 -1779
- package/src/internals.ts +66 -40
- package/src/kernel-panels.ts +89 -3
- package/src/provider-settings.ts +12 -12
- package/src/render/animations.ts +606 -403
- package/src/render/editor.ts +5 -4
- package/src/render/export.ts +13 -3
- package/src/render/fuzzy.ts +83 -0
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/projection.ts +1974 -1621
- package/src/render/status.ts +18 -4
- package/src/session-directory.ts +94 -16
- package/src/session-query.ts +235 -0
- package/src/skills.ts +23 -9
- package/src/store.ts +39 -1
- package/src/subagents.ts +26 -3
- package/src/terminal-title.ts +173 -0
- package/src/update-panel.ts +246 -0
- 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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
25351
|
-
const
|
|
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
|
-
|
|
25354
|
-
|
|
25355
|
-
const
|
|
25356
|
-
if (
|
|
25357
|
-
|
|
25358
|
-
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
26369
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
26563
|
-
*
|
|
26564
|
-
*
|
|
26565
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
26578
|
-
|
|
26579
|
-
const
|
|
26580
|
-
|
|
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 [
|
|
27341
|
+
return [[
|
|
27342
|
+
Math.trunc(third),
|
|
27343
|
+
crest(Math.abs(column - center) / halfWidth),
|
|
27344
|
+
0
|
|
27345
|
+
]];
|
|
26586
27346
|
}
|
|
26587
27347
|
case "pulse": {
|
|
26588
|
-
const
|
|
26589
|
-
|
|
26590
|
-
const
|
|
26591
|
-
const
|
|
26592
|
-
|
|
26593
|
-
|
|
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
|
-
*
|
|
26607
|
-
* and the bottom row last, sweeping down the band. 0.12
|
|
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
|
|
26620
|
-
*
|
|
26621
|
-
*
|
|
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]
|
|
26644
|
-
|
|
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 * .
|
|
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
|
-
|
|
27619
|
+
next = commands.list(agent);
|
|
26768
27620
|
loadedFor = agent;
|
|
26769
|
-
error = void 0;
|
|
26770
27621
|
} catch (cause) {
|
|
26771
|
-
|
|
26772
|
-
|
|
27622
|
+
next = loadedFor === agent ? [...descriptors] : [];
|
|
27623
|
+
nextError = cause instanceof Error ? cause.message : String(cause);
|
|
26773
27624
|
}
|
|
26774
|
-
|
|
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
|
|
27484
|
-
* validate
|
|
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
|
-
*
|
|
27505
|
-
*
|
|
27506
|
-
*
|
|
27507
|
-
*
|
|
27508
|
-
|
|
27509
|
-
|
|
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
|
|
27512
|
-
|
|
27513
|
-
|
|
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
|
|
28930
|
-
*
|
|
28931
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
30612
|
-
|
|
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
|
-
|
|
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://"))
|
|
30621
|
-
|
|
30622
|
-
|
|
30623
|
-
|
|
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 (!
|
|
30626
|
-
|
|
31722
|
+
if (!looksLikeDroppedFile(path)) return {
|
|
31723
|
+
images: [],
|
|
31724
|
+
files: []
|
|
31725
|
+
};
|
|
31726
|
+
files.push(path);
|
|
30627
31727
|
}
|
|
30628
|
-
return
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
30874
|
-
|
|
30875
|
-
|
|
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, {
|
|
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 (
|
|
33157
|
+
if (filtered.length === 0) {
|
|
31957
33158
|
if (cursor !== 0) setCursor(0);
|
|
31958
33159
|
return;
|
|
31959
33160
|
}
|
|
31960
|
-
if (cursor >=
|
|
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
|
-
|
|
31967
|
-
|
|
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
|
-
|
|
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 :
|
|
33201
|
+
setCursor(cursor > 0 ? cursor - 1 : filtered.length - 1);
|
|
31993
33202
|
return;
|
|
31994
33203
|
}
|
|
31995
33204
|
if (key.downArrow) {
|
|
31996
|
-
setCursor(cursor <
|
|
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(
|
|
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 &&
|
|
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 =
|
|
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")] : [
|
|
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,
|
|
32041
|
-
const visible = rowBudget === 0 ? [] :
|
|
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 =
|
|
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(
|
|
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
|
|
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)),
|
|
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
|
-
|
|
33117
|
-
|
|
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,
|
|
33200
|
-
const
|
|
33201
|
-
const
|
|
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
|
-
|
|
34702
|
+
const label = kind === "file" ? "file" : "image";
|
|
34703
|
+
let marker = source === "mention" ? `@${safeName}` : `[${label}: ${safeName}]`;
|
|
33313
34704
|
let suffix = 2;
|
|
33314
|
-
|
|
33315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33337
|
-
|
|
33338
|
-
|
|
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
|
|
33341
|
-
if ([...draftImagesRef.current, ...
|
|
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
|
-
|
|
34743
|
+
imageAdditions.push({
|
|
33344
34744
|
...inspection,
|
|
33345
34745
|
marker
|
|
33346
34746
|
});
|
|
33347
34747
|
markers.push(marker);
|
|
33348
34748
|
}
|
|
33349
|
-
|
|
33350
|
-
|
|
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
|
|
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, ...
|
|
34780
|
+
const nextImages = [...draftImagesRef.current, ...imageAdditions];
|
|
33372
34781
|
draftImagesRef.current = nextImages;
|
|
33373
34782
|
setDraftImages(nextImages);
|
|
33374
|
-
|
|
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(`
|
|
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
|
-
|
|
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 (
|
|
33421
|
-
const row =
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
33599
|
-
if (
|
|
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
|
-
|
|
33676
|
-
const
|
|
33677
|
-
|
|
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
|
-
|
|
33696
|
-
|
|
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(`
|
|
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
|
-
|
|
33715
|
-
|
|
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
|
-
|
|
33950
|
-
|
|
33951
|
-
|
|
33952
|
-
|
|
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
|
|
33958
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
33988
|
-
|
|
33989
|
-
|
|
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
|
-
|
|
34072
|
-
|
|
34073
|
-
|
|
34074
|
-
|
|
34075
|
-
|
|
34076
|
-
|
|
34077
|
-
|
|
34078
|
-
|
|
34079
|
-
|
|
34080
|
-
|
|
34081
|
-
|
|
34082
|
-
|
|
34083
|
-
|
|
34084
|
-
|
|
34085
|
-
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
|
|
34089
|
-
|
|
34090
|
-
|
|
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
|
|
34343
|
-
* sweep re-renders only the composer band, not the whole tree,
|
|
34344
|
-
* App owns the rarely-changing tier/style and
|
|
34345
|
-
*
|
|
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
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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.
|
|
35445
|
-
|
|
35446
|
-
|
|
35447
|
-
|
|
35448
|
-
|
|
35449
|
-
|
|
35450
|
-
|
|
35451
|
-
|
|
35452
|
-
|
|
35453
|
-
|
|
35454
|
-
|
|
35455
|
-
|
|
35456
|
-
|
|
35457
|
-
|
|
35458
|
-
|
|
35459
|
-
|
|
35460
|
-
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
36179
|
-
error =
|
|
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)
|
|
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
|
|
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, ...
|
|
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(
|
|
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,
|
|
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 ? {} : {
|
|
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
|
-
*
|
|
37736
|
-
*
|
|
37737
|
-
*
|
|
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
|
|
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
|
|
37746
|
-
if (
|
|
37747
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
37860
|
-
* (`encodeSegment(id)
|
|
37861
|
-
*
|
|
37862
|
-
* file has been touched yet, so a backend or layout
|
|
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
|
|
37883
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|