dsh-code 1.0.6 → 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 +60 -9
- package/README.md +60 -9
- package/bin/deepseek.mjs +86 -1
- package/cordis.patch.yml +88 -0
- package/lib/index.mjs +1002 -94
- package/lib/session-query.mjs +149 -0
- package/lib/types/app.d.ts +9 -2
- package/lib/types/index.d.ts +27 -0
- package/lib/types/kernel-panels.d.ts +23 -0
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/projection.d.ts +35 -0
- package/lib/types/render/status.d.ts +1 -1
- package/lib/types/session-query.d.ts +92 -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 +228 -89
- package/src/app.ts +252 -69
- package/src/index.ts +129 -11
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +89 -3
- package/src/render/editor.ts +5 -4
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/projection.ts +144 -3
- package/src/render/status.ts +18 -4
- package/src/session-query.ts +235 -0
- 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,7 +2,7 @@ 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
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";
|
|
@@ -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,40 @@ 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"]);
|
|
25086
25499
|
/** Assemble the effective system prompt from surface nodes: head text plus every later non-empty node. */
|
|
25087
25500
|
function assembleSystemPrompt(nodes) {
|
|
25088
25501
|
if (nodes.size === 0) return "";
|
|
@@ -25202,6 +25615,7 @@ function createReplayAccumulator() {
|
|
|
25202
25615
|
systemPrompt: "",
|
|
25203
25616
|
sandbox: "",
|
|
25204
25617
|
goal: void 0,
|
|
25618
|
+
schedules: [],
|
|
25205
25619
|
stats: {
|
|
25206
25620
|
turns: 0,
|
|
25207
25621
|
steps: 0,
|
|
@@ -25348,7 +25762,7 @@ function replayProjectEvent(acc, event) {
|
|
|
25348
25762
|
const text = textOf(message.content);
|
|
25349
25763
|
const images = imagesOf(message.content);
|
|
25350
25764
|
const files = filesOf(message.content);
|
|
25351
|
-
if (message.source.kind === "user") {
|
|
25765
|
+
if (message.source.kind === "user" || message.source.kind === "plugin" && REMINDER_PLUGINS.has(message.source.plugin)) {
|
|
25352
25766
|
appendReplayEntry(acc, {
|
|
25353
25767
|
kind: "user",
|
|
25354
25768
|
text,
|
|
@@ -25365,7 +25779,17 @@ function replayProjectEvent(acc, event) {
|
|
|
25365
25779
|
};
|
|
25366
25780
|
return true;
|
|
25367
25781
|
}
|
|
25368
|
-
|
|
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;
|
|
25369
25793
|
const summary = boundContextSummary(notice);
|
|
25370
25794
|
appendReplayEntry(acc, {
|
|
25371
25795
|
kind: "user",
|
|
@@ -25683,6 +26107,10 @@ function replayProjectEvent(acc, event) {
|
|
|
25683
26107
|
});
|
|
25684
26108
|
return true;
|
|
25685
26109
|
}
|
|
26110
|
+
case "schedule/change":
|
|
26111
|
+
acc.schedules = applyScheduleChange(acc.schedules, event.data);
|
|
26112
|
+
acc.ops += 1;
|
|
26113
|
+
return true;
|
|
25686
26114
|
case "session/title":
|
|
25687
26115
|
acc.title = event.data.title;
|
|
25688
26116
|
return true;
|
|
@@ -25787,6 +26215,7 @@ function materializeReplayView(acc, copy) {
|
|
|
25787
26215
|
systemPrompt: acc.systemPrompt,
|
|
25788
26216
|
sandbox: acc.sandbox,
|
|
25789
26217
|
goal: acc.goal,
|
|
26218
|
+
schedules: acc.schedules,
|
|
25790
26219
|
pending: {
|
|
25791
26220
|
"next-turn": [...acc.pendingTurn],
|
|
25792
26221
|
"next-step": [...acc.pendingStep]
|
|
@@ -25879,6 +26308,132 @@ function settledEntryCount(entries) {
|
|
|
25879
26308
|
return entries.length;
|
|
25880
26309
|
}
|
|
25881
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
|
|
25882
26437
|
//#region src/render/markdown.ts
|
|
25883
26438
|
/**
|
|
25884
26439
|
* Terminal markdown renderer for assistant replies: a pure GFM-subset
|
|
@@ -28384,14 +28939,6 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
|
|
|
28384
28939
|
}];
|
|
28385
28940
|
const right = [];
|
|
28386
28941
|
const row2 = [];
|
|
28387
|
-
if (facts.plan && enabled.has("plan")) row2.push({
|
|
28388
|
-
group: { spans: [{
|
|
28389
|
-
text: "⧉ plan",
|
|
28390
|
-
tone: "accent"
|
|
28391
|
-
}] },
|
|
28392
|
-
rank: RANK2_PLAN,
|
|
28393
|
-
id: "plan"
|
|
28394
|
-
});
|
|
28395
28942
|
if (stats.turns > 0 || stats.steps > 0) {
|
|
28396
28943
|
if (enabled.has("turns")) {
|
|
28397
28944
|
const counts = [];
|
|
@@ -28510,9 +29057,13 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
|
|
|
28510
29057
|
});
|
|
28511
29058
|
const permission = safe(facts.permission);
|
|
28512
29059
|
let badge = -1;
|
|
29060
|
+
const planStation = facts.plan && permissionTone(permission) === "success";
|
|
28513
29061
|
if (permission !== "" && enabled.has("permission")) {
|
|
28514
29062
|
right.push({
|
|
28515
|
-
span: {
|
|
29063
|
+
span: planStation ? {
|
|
29064
|
+
text: "plan on",
|
|
29065
|
+
tone: "plan"
|
|
29066
|
+
} : {
|
|
28516
29067
|
text: permission,
|
|
28517
29068
|
tone: permissionTone(permission)
|
|
28518
29069
|
},
|
|
@@ -28521,6 +29072,14 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
|
|
|
28521
29072
|
});
|
|
28522
29073
|
badge = right.length - 1;
|
|
28523
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
|
+
});
|
|
28524
29083
|
return {
|
|
28525
29084
|
left,
|
|
28526
29085
|
right,
|
|
@@ -29362,14 +29921,15 @@ function composerMaxRows(terminalRows) {
|
|
|
29362
29921
|
return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
|
|
29363
29922
|
}
|
|
29364
29923
|
/**
|
|
29365
|
-
* History navigation starts with Up on an empty draft, or
|
|
29366
|
-
*
|
|
29367
|
-
*
|
|
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.
|
|
29368
29928
|
*/
|
|
29369
29929
|
function shouldRecallNavigate(value, cursor, lastRecalled, direction) {
|
|
29370
29930
|
if (value === "") return direction < 0;
|
|
29371
29931
|
if (lastRecalled !== value) return false;
|
|
29372
|
-
return
|
|
29932
|
+
return cursor === 0 || cursor === value.length;
|
|
29373
29933
|
}
|
|
29374
29934
|
//#endregion
|
|
29375
29935
|
//#region src/keyboard.ts
|
|
@@ -29710,7 +30270,8 @@ function ModePanel({ current, load, select, close }) {
|
|
|
29710
30270
|
const visible = (0, import_react.useMemo)(() => rows.filter((row) => `${row.id} ${row.name ?? ""} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
|
|
29711
30271
|
(0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
|
|
29712
30272
|
useInput((input, key) => {
|
|
29713
|
-
if (key.escape
|
|
30273
|
+
if (key.escape) return close();
|
|
30274
|
+
if (input === "q" && query === "") return close();
|
|
29714
30275
|
if (input === "r" && query === "") return refresh();
|
|
29715
30276
|
if (key.upArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length);
|
|
29716
30277
|
if (key.downArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + 1) % visible.length);
|
|
@@ -29756,7 +30317,8 @@ function PermissionPanel({ current, load, select, close }) {
|
|
|
29756
30317
|
const visible = (0, import_react.useMemo)(() => rows.filter((row) => `${row.id} ${row.description ?? ""}`.toLowerCase().includes(query.toLowerCase())), [rows, query]);
|
|
29757
30318
|
(0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, visible.length - 1))), [visible.length]);
|
|
29758
30319
|
useInput((input, key) => {
|
|
29759
|
-
if (key.escape
|
|
30320
|
+
if (key.escape) return close();
|
|
30321
|
+
if (input === "q" && query === "") return close();
|
|
29760
30322
|
if (input === "r" && query === "") return refresh();
|
|
29761
30323
|
if (key.upArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length);
|
|
29762
30324
|
if (key.downArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + 1) % visible.length);
|
|
@@ -29788,7 +30350,8 @@ function PluginPanel({ load, close, initialQuery = "" }) {
|
|
|
29788
30350
|
const rows = (0, import_react.useMemo)(() => load().filter((row) => `${row.entryId} ${row.moduleName} ${row.phase ?? ""}`.toLowerCase().includes(query.toLowerCase())), [epoch, query]);
|
|
29789
30351
|
(0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
|
|
29790
30352
|
useInput((input, key) => {
|
|
29791
|
-
if (key.escape
|
|
30353
|
+
if (key.escape) return close();
|
|
30354
|
+
if (input === "q" && query === "") return close();
|
|
29792
30355
|
if (input === "r" && query === "") return setEpoch((value) => value + 1);
|
|
29793
30356
|
if (key.upArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length);
|
|
29794
30357
|
if (key.downArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + 1) % rows.length);
|
|
@@ -30469,6 +31032,69 @@ function SubagentPanel({ current, load, pick, inherit, close }) {
|
|
|
30469
31032
|
footer: "↑↓ choose · enter apply · r refresh · esc close"
|
|
30470
31033
|
});
|
|
30471
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
|
+
}
|
|
30472
31098
|
/** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
|
|
30473
31099
|
function serializeHistoryEntry(text) {
|
|
30474
31100
|
return JSON.stringify(text);
|
|
@@ -31304,10 +31930,18 @@ const LOCAL_COMMANDS = [
|
|
|
31304
31930
|
label: "/plugin",
|
|
31305
31931
|
description: "inspect the live plugin composition"
|
|
31306
31932
|
},
|
|
31933
|
+
{
|
|
31934
|
+
label: "/update",
|
|
31935
|
+
description: "update dsh-code, the harness host, and profile plugins in one aligned step"
|
|
31936
|
+
},
|
|
31307
31937
|
{
|
|
31308
31938
|
label: "/jobs",
|
|
31309
31939
|
description: "inspect background jobs"
|
|
31310
31940
|
},
|
|
31941
|
+
{
|
|
31942
|
+
label: "/schedule",
|
|
31943
|
+
description: "inspect active reminders (created through schedule tools)"
|
|
31944
|
+
},
|
|
31311
31945
|
{
|
|
31312
31946
|
label: "/statusline",
|
|
31313
31947
|
description: "customize the status line items"
|
|
@@ -31882,6 +32516,11 @@ function statusToneProps(tone) {
|
|
|
31882
32516
|
bold: true,
|
|
31883
32517
|
dimColor: void 0
|
|
31884
32518
|
};
|
|
32519
|
+
case "plan": return {
|
|
32520
|
+
color: inkColor(getPalette().success),
|
|
32521
|
+
bold: true,
|
|
32522
|
+
dimColor: void 0
|
|
32523
|
+
};
|
|
31885
32524
|
case "warn": return {
|
|
31886
32525
|
color: inkColor(getPalette().warn),
|
|
31887
32526
|
bold: true,
|
|
@@ -31935,7 +32574,7 @@ function deepseekWaveHues(tier) {
|
|
|
31935
32574
|
palette.brandMid
|
|
31936
32575
|
];
|
|
31937
32576
|
}
|
|
31938
|
-
function StatusLine({ facts, stats, busy, columns, items }) {
|
|
32577
|
+
function StatusLine({ facts, stats, busy, columns, items, onRows }) {
|
|
31939
32578
|
const layout = (0, import_react.useMemo)(() => layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
|
|
31940
32579
|
busy,
|
|
31941
32580
|
items,
|
|
@@ -31958,6 +32597,10 @@ function StatusLine({ facts, stats, busy, columns, items }) {
|
|
|
31958
32597
|
columns,
|
|
31959
32598
|
items
|
|
31960
32599
|
]);
|
|
32600
|
+
const statusRowCount = layout.row2.left.length > 0 ? 2 : 1;
|
|
32601
|
+
(0, import_react.useEffect)(() => {
|
|
32602
|
+
onRows?.(statusRowCount);
|
|
32603
|
+
}, [onRows, statusRowCount]);
|
|
31961
32604
|
const renderRow = (row, key, indent = 0) => {
|
|
31962
32605
|
const leftParts = [];
|
|
31963
32606
|
row.left.forEach((group, groupIndex) => {
|
|
@@ -32498,36 +33141,44 @@ function QuestionBar({ store, snapshot, locked }) {
|
|
|
32498
33141
|
}
|
|
32499
33142
|
/** The /model panel: a scrolling list over the advisory model directory. */
|
|
32500
33143
|
function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry, onClose }) {
|
|
33144
|
+
const [query, setQuery] = (0, import_react.useState)("");
|
|
32501
33145
|
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
32502
33146
|
const stdout = useStdout().stdout;
|
|
32503
33147
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
32504
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]);
|
|
32505
33154
|
const positioned = (0, import_react.useRef)(false);
|
|
32506
33155
|
(0, import_react.useEffect)(() => {
|
|
32507
33156
|
if (positioned.current || rows.length === 0 || current === void 0) {
|
|
32508
|
-
if (
|
|
33157
|
+
if (filtered.length === 0) {
|
|
32509
33158
|
if (cursor !== 0) setCursor(0);
|
|
32510
33159
|
return;
|
|
32511
33160
|
}
|
|
32512
|
-
if (cursor >=
|
|
33161
|
+
if (cursor >= filtered.length) setCursor(filtered.length - 1);
|
|
32513
33162
|
return;
|
|
32514
33163
|
}
|
|
32515
33164
|
const index = rows.findIndex((row) => `${row.provider}/${row.model}` === current);
|
|
32516
33165
|
if (index >= 0) {
|
|
32517
33166
|
positioned.current = true;
|
|
32518
|
-
|
|
32519
|
-
|
|
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));
|
|
32520
33170
|
}, [
|
|
32521
33171
|
rows,
|
|
33172
|
+
filtered,
|
|
32522
33173
|
cursor,
|
|
32523
33174
|
current
|
|
32524
33175
|
]);
|
|
32525
33176
|
useInput((input, key) => {
|
|
32526
|
-
if (key.escape || input === "q") {
|
|
33177
|
+
if (key.escape || input === "q" && query === "") {
|
|
32527
33178
|
onClose();
|
|
32528
33179
|
return;
|
|
32529
33180
|
}
|
|
32530
|
-
if (input === "r") {
|
|
33181
|
+
if (input === "r" && query === "") {
|
|
32531
33182
|
onRetry();
|
|
32532
33183
|
return;
|
|
32533
33184
|
}
|
|
@@ -32539,13 +33190,19 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
32539
33190
|
onClose();
|
|
32540
33191
|
return;
|
|
32541
33192
|
}
|
|
32542
|
-
|
|
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;
|
|
32543
33200
|
if (key.upArrow) {
|
|
32544
|
-
setCursor(cursor > 0 ? cursor - 1 :
|
|
33201
|
+
setCursor(cursor > 0 ? cursor - 1 : filtered.length - 1);
|
|
32545
33202
|
return;
|
|
32546
33203
|
}
|
|
32547
33204
|
if (key.downArrow) {
|
|
32548
|
-
setCursor(cursor <
|
|
33205
|
+
setCursor(cursor < filtered.length - 1 ? cursor + 1 : 0);
|
|
32549
33206
|
return;
|
|
32550
33207
|
}
|
|
32551
33208
|
if (key.pageUp) {
|
|
@@ -32553,23 +33210,15 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
32553
33210
|
return;
|
|
32554
33211
|
}
|
|
32555
33212
|
if (key.pageDown) {
|
|
32556
|
-
setCursor((current) => Math.min(
|
|
32557
|
-
return;
|
|
32558
|
-
}
|
|
32559
|
-
if (input === "g") {
|
|
32560
|
-
setCursor(0);
|
|
32561
|
-
return;
|
|
32562
|
-
}
|
|
32563
|
-
if (input === "G") {
|
|
32564
|
-
setCursor(rows.length - 1);
|
|
33213
|
+
setCursor((current) => Math.min(filtered.length - 1, current + Math.max(1, viewport.bodyRows - 1)));
|
|
32565
33214
|
return;
|
|
32566
33215
|
}
|
|
32567
|
-
if (key.return &&
|
|
33216
|
+
if (key.return && filtered[cursor] !== void 0) onSelect(filtered[cursor]);
|
|
32568
33217
|
});
|
|
32569
33218
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
32570
33219
|
const providers = onProviders === void 0 ? "" : " · tab providers";
|
|
32571
|
-
const state =
|
|
32572
|
-
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));
|
|
32573
33222
|
}
|
|
32574
33223
|
const visibleStateRows = (directory === void 0 && error === void 0 ? [(0, import_react.createElement)(Text, {
|
|
32575
33224
|
key: "loading",
|
|
@@ -32587,10 +33236,14 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
32587
33236
|
key: "empty",
|
|
32588
33237
|
dimColor: true,
|
|
32589
33238
|
wrap: "truncate-end"
|
|
32590
|
-
}, " 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);
|
|
32591
33244
|
const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length);
|
|
32592
|
-
const first = selectionWindow(cursor,
|
|
32593
|
-
const visible = rowBudget === 0 ? [] :
|
|
33245
|
+
const first = selectionWindow(cursor, filtered.length, rowBudget);
|
|
33246
|
+
const visible = rowBudget === 0 ? [] : filtered.slice(first, first + rowBudget);
|
|
32594
33247
|
return (0, import_react.createElement)(Box, {
|
|
32595
33248
|
flexDirection: "column",
|
|
32596
33249
|
width: viewport.outerColumns,
|
|
@@ -32601,8 +33254,8 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
32601
33254
|
color: inkColor(getPalette().brand),
|
|
32602
33255
|
bold: true,
|
|
32603
33256
|
wrap: "truncate-end"
|
|
32604
|
-
}, 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) => {
|
|
32605
|
-
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);
|
|
32606
33259
|
const capability = row.inputModalities?.includes("image") === true ? " · image" : "";
|
|
32607
33260
|
const label = displayText(`${row.providerName} · ${row.modelName}${capability}`);
|
|
32608
33261
|
return (0, import_react.createElement)(Text, {
|
|
@@ -32613,7 +33266,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
32613
33266
|
}), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
32614
33267
|
dimColor: true,
|
|
32615
33268
|
wrap: "truncate-end"
|
|
32616
|
-
}, 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))));
|
|
32617
33270
|
}
|
|
32618
33271
|
/** Compact provider-state copy; only value-free credential facts cross this boundary. */
|
|
32619
33272
|
function providerStateLabel(row) {
|
|
@@ -33916,9 +34569,12 @@ function CompletionMenu({ active, mention, index, rows, error }) {
|
|
|
33916
34569
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
33917
34570
|
* box passes every key through untouched.
|
|
33918
34571
|
*/
|
|
33919
|
-
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, inspectFiles, prepareFiles,
|
|
33920
|
-
const
|
|
33921
|
-
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;
|
|
33922
34578
|
const editorColumns = Math.max(1, columns - 6);
|
|
33923
34579
|
const stdin = useStdin().stdin;
|
|
33924
34580
|
const focusReporting = isVsCodeTerminalEnv();
|
|
@@ -34004,6 +34660,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34004
34660
|
const normalized = normalizeKeyboardChunk(typeof chunk === "string" ? chunk : String(chunk));
|
|
34005
34661
|
const input = focusReporting ? stripTerminalFocusEvents(normalized, (focused) => {
|
|
34006
34662
|
terminalFocusedRef.current = focused;
|
|
34663
|
+
if (focused && inputStdout !== void 0) {
|
|
34664
|
+
inputStdout.write(terminalTitleSequence(tabTitleRef.current));
|
|
34665
|
+
process.title = sanitizeTerminalTitle(tabTitleRef.current);
|
|
34666
|
+
}
|
|
34007
34667
|
}) : normalized;
|
|
34008
34668
|
rawEditorTokens.current = tokenizeRawEditorChunk(input);
|
|
34009
34669
|
return input;
|
|
@@ -34313,20 +34973,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34313
34973
|
dismissNotice();
|
|
34314
34974
|
notify("image submission cancelled", "warning");
|
|
34315
34975
|
};
|
|
34316
|
-
/**
|
|
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. */
|
|
34317
34979
|
const navigateVertical = (direction) => {
|
|
34318
34980
|
const currentValue = valueRef.current;
|
|
34319
34981
|
const currentCursor = cursorRef.current;
|
|
34320
|
-
const model = editorModel(currentValue, editorColumns);
|
|
34321
|
-
const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column;
|
|
34322
|
-
const next = moveCursorVertically(model, currentCursor, preferred, direction);
|
|
34323
|
-
if (next !== currentCursor) {
|
|
34324
|
-
cursorRef.current = next;
|
|
34325
|
-
setCursor(next);
|
|
34326
|
-
resetCursorBlink();
|
|
34327
|
-
preferredColumnRef.current = preferred;
|
|
34328
|
-
return;
|
|
34329
|
-
}
|
|
34330
34982
|
if (recall.current.entries.length > 0 && shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
|
|
34331
34983
|
const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current);
|
|
34332
34984
|
recall.current = step.state;
|
|
@@ -34337,10 +34989,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34337
34989
|
setValue(safe);
|
|
34338
34990
|
setCursor(safe.length);
|
|
34339
34991
|
preferredColumnRef.current = null;
|
|
34340
|
-
setDismissedMenuValue(
|
|
34992
|
+
setDismissedMenuValue(safe);
|
|
34341
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;
|
|
34342
35005
|
}
|
|
34343
|
-
resetCursorBlink();
|
|
34344
35006
|
};
|
|
34345
35007
|
useStableInput((input, key) => {
|
|
34346
35008
|
if (!active) return;
|
|
@@ -34357,8 +35019,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34357
35019
|
}
|
|
34358
35020
|
if (key.tab && key.shift) {
|
|
34359
35021
|
try {
|
|
34360
|
-
const
|
|
34361
|
-
if (
|
|
35022
|
+
const label = cycleMode();
|
|
35023
|
+
if (label !== "") notify(label);
|
|
34362
35024
|
} catch (error) {
|
|
34363
35025
|
notify(`permission change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
34364
35026
|
}
|
|
@@ -34482,10 +35144,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34482
35144
|
setDismissedMenuValue(void 0);
|
|
34483
35145
|
if (trimmed === "") return;
|
|
34484
35146
|
dismissNotice();
|
|
34485
|
-
|
|
34486
|
-
|
|
34487
|
-
recordHistory(text);
|
|
34488
|
-
}
|
|
35147
|
+
recordLocal(text);
|
|
35148
|
+
recordHistory(text);
|
|
34489
35149
|
recall.current = beginRecall(recallSpace, "");
|
|
34490
35150
|
if (text === "/quit") {
|
|
34491
35151
|
quit();
|
|
@@ -34564,6 +35224,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34564
35224
|
openPlugin(text.slice(7).trim());
|
|
34565
35225
|
return;
|
|
34566
35226
|
}
|
|
35227
|
+
if (text === "/update") {
|
|
35228
|
+
openUpdate();
|
|
35229
|
+
return;
|
|
35230
|
+
}
|
|
35231
|
+
if (text === "/schedule") {
|
|
35232
|
+
openSchedule();
|
|
35233
|
+
return;
|
|
35234
|
+
}
|
|
34567
35235
|
if (text === "/jobs" || text.startsWith("/jobs ")) {
|
|
34568
35236
|
openJobs();
|
|
34569
35237
|
return;
|
|
@@ -34760,6 +35428,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34760
35428
|
(0, import_react.useEffect)(() => {
|
|
34761
35429
|
onEditorRows(editorRowCount);
|
|
34762
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);
|
|
34763
35437
|
(0, import_react.useEffect)(() => {
|
|
34764
35438
|
onMenuRows(menuHeightRows);
|
|
34765
35439
|
return () => onMenuRows(0);
|
|
@@ -34785,7 +35459,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34785
35459
|
bold: true
|
|
34786
35460
|
}, warning), bandFill(2 + visibleColumns(warning))));
|
|
34787
35461
|
}
|
|
34788
|
-
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));
|
|
34789
35463
|
return band((0, import_react.createElement)(Text, {
|
|
34790
35464
|
backgroundColor: bandBg,
|
|
34791
35465
|
wrap: "truncate-end"
|
|
@@ -35157,6 +35831,8 @@ function App(props) {
|
|
|
35157
35831
|
const [resumeOpen, setResumeOpen] = (0, import_react.useState)(false);
|
|
35158
35832
|
const [pluginOpen, setPluginOpen] = (0, import_react.useState)(false);
|
|
35159
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);
|
|
35160
35836
|
const [jobsOpen, setJobsOpen] = (0, import_react.useState)(false);
|
|
35161
35837
|
const [statuslineOpen, setStatuslineOpen] = (0, import_react.useState)(false);
|
|
35162
35838
|
const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
|
|
@@ -35229,7 +35905,7 @@ function App(props) {
|
|
|
35229
35905
|
const agentRows = (0, import_react.useSyncExternalStore)(props.subagents.subscribe, props.subagents.getSnapshot);
|
|
35230
35906
|
const approvalPending = approvalSnapshot.pending !== void 0;
|
|
35231
35907
|
const questionPending = questionSnapshot.pending !== void 0;
|
|
35232
|
-
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;
|
|
35233
35909
|
(0, import_react.useEffect)(() => {
|
|
35234
35910
|
if (!approvalPending && !questionPending) return;
|
|
35235
35911
|
setModelOpen(false);
|
|
@@ -35241,6 +35917,8 @@ function App(props) {
|
|
|
35241
35917
|
setPermissionOpen(false);
|
|
35242
35918
|
setResumeOpen(false);
|
|
35243
35919
|
setPluginOpen(false);
|
|
35920
|
+
setUpdateOpen(false);
|
|
35921
|
+
setScheduleOpen(false);
|
|
35244
35922
|
setStatuslineOpen(false);
|
|
35245
35923
|
setThemeOpen(false);
|
|
35246
35924
|
setHistoryOpen(false);
|
|
@@ -35306,11 +35984,18 @@ function App(props) {
|
|
|
35306
35984
|
const handleMenuRows = (0, import_react.useCallback)((rows) => {
|
|
35307
35985
|
setMenuRows((current) => current === rows ? current : rows);
|
|
35308
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;
|
|
35309
35992
|
const composerEditorCap = composerMaxRows(terminalRows);
|
|
35310
35993
|
const MENU_RESERVE_ROWS = 5;
|
|
35311
35994
|
const dynamicRows = Math.max(1, terminalRows - 8 - MENU_RESERVE_ROWS - composerGutterRows - (composerRows - 1) - Math.max(0, menuRows - MENU_RESERVE_ROWS));
|
|
35312
35995
|
const streamingActive = view.streaming !== "" || view.streamingReasoning !== "";
|
|
35313
35996
|
const deepDivingVisible = busy && !streamingActive;
|
|
35997
|
+
const tabTitle = view.title === "" ? DEFAULT_TERMINAL_TITLE : view.title;
|
|
35998
|
+
useTerminalTitle(tabTitle);
|
|
35314
35999
|
const allLiveLines = (0, import_react.useMemo)(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2), showReasoning)), [
|
|
35315
36000
|
view.entries,
|
|
35316
36001
|
settled,
|
|
@@ -35334,8 +36019,11 @@ function App(props) {
|
|
|
35334
36019
|
const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length ? visibleLiveLines : visibleLiveLines.slice(-liveAudit.allocation.live);
|
|
35335
36020
|
const auditedReasoningRows = liveAudit.allocation.reasoning;
|
|
35336
36021
|
const auditedAnswerRows = liveAudit.allocation.answer;
|
|
35337
|
-
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
|
|
35338
|
-
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"}`;
|
|
35339
36027
|
const closeInspector = (0, import_react.useCallback)(() => {
|
|
35340
36028
|
setVerboseOpen(false);
|
|
35341
36029
|
}, []);
|
|
@@ -35682,6 +36370,14 @@ function App(props) {
|
|
|
35682
36370
|
load: props.loadPlugins,
|
|
35683
36371
|
initialQuery: pluginQuery,
|
|
35684
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)
|
|
35685
36381
|
}) : void 0, jobsOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(JobsPanel, {
|
|
35686
36382
|
load: props.loadJobs,
|
|
35687
36383
|
close: () => setJobsOpen(false)
|
|
@@ -35744,6 +36440,7 @@ function App(props) {
|
|
|
35744
36440
|
}, (0, import_react.createElement)(Input, {
|
|
35745
36441
|
active: inputActive,
|
|
35746
36442
|
frozen: modalVisible,
|
|
36443
|
+
frozenHint,
|
|
35747
36444
|
busy,
|
|
35748
36445
|
descriptors,
|
|
35749
36446
|
skills,
|
|
@@ -35801,6 +36498,8 @@ function App(props) {
|
|
|
35801
36498
|
setPluginQuery(query);
|
|
35802
36499
|
setPluginOpen(true);
|
|
35803
36500
|
},
|
|
36501
|
+
openUpdate: () => setUpdateOpen(true),
|
|
36502
|
+
openSchedule: () => setScheduleOpen(true),
|
|
35804
36503
|
openJobs: () => setJobsOpen(true),
|
|
35805
36504
|
openStatusline: () => setStatuslineOpen(true),
|
|
35806
36505
|
openTheme: () => setThemeOpen(true),
|
|
@@ -35851,7 +36550,7 @@ function App(props) {
|
|
|
35851
36550
|
inspectFiles: props.inspectFiles,
|
|
35852
36551
|
prepareFiles: props.prepareFiles,
|
|
35853
36552
|
sessionKey: props.sessionKey,
|
|
35854
|
-
|
|
36553
|
+
cycleMode: props.cycleMode,
|
|
35855
36554
|
exportTranscript: props.exportTranscript,
|
|
35856
36555
|
renameTitle: props.renameTitle,
|
|
35857
36556
|
copyLastResponse: props.copyLastResponse,
|
|
@@ -35867,6 +36566,8 @@ function App(props) {
|
|
|
35867
36566
|
waveTier,
|
|
35868
36567
|
waveStyle,
|
|
35869
36568
|
maxRows: composerEditorCap,
|
|
36569
|
+
anchorRowsBelow: imeRowsBelowComposer,
|
|
36570
|
+
tabTitle,
|
|
35870
36571
|
onEditorRows: handleEditorRows,
|
|
35871
36572
|
onMenuRows: handleMenuRows
|
|
35872
36573
|
}), (0, import_react.createElement)(StatusLine, {
|
|
@@ -35877,7 +36578,7 @@ function App(props) {
|
|
|
35877
36578
|
branch: props.branch,
|
|
35878
36579
|
sessionId: props.sessionId,
|
|
35879
36580
|
title: view.title,
|
|
35880
|
-
plan: view.plan,
|
|
36581
|
+
plan: view.plan || props.pendingPlan === true,
|
|
35881
36582
|
permission: view.permission !== "" ? view.permission : props.permission,
|
|
35882
36583
|
sandbox: view.sandbox,
|
|
35883
36584
|
goal: view.goal === void 0 ? void 0 : {
|
|
@@ -35889,7 +36590,8 @@ function App(props) {
|
|
|
35889
36590
|
stats: view.stats,
|
|
35890
36591
|
busy,
|
|
35891
36592
|
columns: terminalColumns,
|
|
35892
|
-
items: statuslineItems
|
|
36593
|
+
items: statuslineItems,
|
|
36594
|
+
onRows: handleStatusRows
|
|
35893
36595
|
})));
|
|
35894
36596
|
}
|
|
35895
36597
|
//#endregion
|
|
@@ -36144,6 +36846,7 @@ const internals = {
|
|
|
36144
36846
|
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(true);
|
|
36145
36847
|
try {
|
|
36146
36848
|
process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
|
|
36849
|
+
ensureVsCodeTabTitleSetting();
|
|
36147
36850
|
const tuiStdin = createSplitStdin(process.stdin);
|
|
36148
36851
|
const instance = render(element, {
|
|
36149
36852
|
exitOnCtrlC: false,
|
|
@@ -37629,13 +38332,6 @@ function selectPermission(service, session, preset) {
|
|
|
37629
38332
|
if (session !== void 0) service.set(session, preset);
|
|
37630
38333
|
return preset;
|
|
37631
38334
|
}
|
|
37632
|
-
/** Cycle table order from the active, pending, or configured-default value. */
|
|
37633
|
-
function cyclePermission(service, session, pending) {
|
|
37634
|
-
if (service.names.length === 0) return "";
|
|
37635
|
-
const at = service.names.indexOf(effectivePermission(service, session, pending));
|
|
37636
|
-
const next = service.names[(at + 1) % service.names.length] ?? "";
|
|
37637
|
-
return next === "" ? "" : selectPermission(service, session, next);
|
|
37638
|
-
}
|
|
37639
38335
|
/** Materialize a pre-session choice after Harness creates the first session. */
|
|
37640
38336
|
function applyPendingPermission(service, session, pending) {
|
|
37641
38337
|
if (pending !== void 0 && effectivePermission(service, session, void 0) !== pending) selectPermission(service, session, pending);
|
|
@@ -37684,6 +38380,120 @@ function listPluginRows(ctx) {
|
|
|
37684
38380
|
return rows;
|
|
37685
38381
|
}
|
|
37686
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
|
|
37687
38497
|
//#region src/settings-file.ts
|
|
37688
38498
|
/**
|
|
37689
38499
|
* Serialized, crash-atomic persistence for the small user-level JSON files
|
|
@@ -37876,6 +38686,32 @@ function submissionBelongsToSession(origin, activeSessionId) {
|
|
|
37876
38686
|
return origin === void 0 || origin === "" || origin === activeSessionId;
|
|
37877
38687
|
}
|
|
37878
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
|
+
/**
|
|
37879
38715
|
* Order-preserving gate for composer input while the startup prompt/images
|
|
37880
38716
|
* are still preparing. Anything submitted before the startup delivery settles
|
|
37881
38717
|
* queues and flushes afterwards in submit order, so the initial request can
|
|
@@ -38061,6 +38897,37 @@ async function run(ctx, startup, io) {
|
|
|
38061
38897
|
/** Permission preset selected before the first session exists. */
|
|
38062
38898
|
let pendingPermission;
|
|
38063
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
|
+
/**
|
|
38064
38931
|
* Monotonic session epoch: bumped on every successful activation, on every
|
|
38065
38932
|
* first-session creation, and on quit. Async callbacks (mention prepares,
|
|
38066
38933
|
* command executions) capture it at call time and drop their result when it
|
|
@@ -38438,6 +39305,10 @@ async function run(ctx, startup, io) {
|
|
|
38438
39305
|
abortPendingControllers();
|
|
38439
39306
|
epoch += 1;
|
|
38440
39307
|
const queued = pendingInputs.splice(0);
|
|
39308
|
+
if (pendingPlan) {
|
|
39309
|
+
pendingPlan = false;
|
|
39310
|
+
deliverLine("/plan", "followup");
|
|
39311
|
+
}
|
|
38441
39312
|
for (const item of queued) deliverLine(item.text, item.mode, item.images);
|
|
38442
39313
|
} finally {
|
|
38443
39314
|
creating = false;
|
|
@@ -38521,24 +39392,56 @@ async function run(ctx, startup, io) {
|
|
|
38521
39392
|
return selected;
|
|
38522
39393
|
};
|
|
38523
39394
|
/**
|
|
38524
|
-
*
|
|
38525
|
-
*
|
|
38526
|
-
*
|
|
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.
|
|
38527
39401
|
*/
|
|
38528
|
-
const
|
|
39402
|
+
const cycleMode = () => {
|
|
38529
39403
|
if (permissionPresets === void 0 || permissionPresets.names.length === 0) {
|
|
38530
39404
|
bridge.notify("permission presets are not mounted in this composition", "warning");
|
|
38531
39405
|
return "";
|
|
38532
39406
|
}
|
|
38533
39407
|
try {
|
|
38534
|
-
const
|
|
38535
|
-
if (
|
|
38536
|
-
|
|
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;
|
|
38537
39437
|
renderCurrent();
|
|
39438
|
+
return `plan → off · permission → ${decision.preset}`;
|
|
38538
39439
|
}
|
|
38539
|
-
|
|
39440
|
+
send("/plan off", "followup");
|
|
39441
|
+
selectPermission(permissionPresets, session, decision.preset);
|
|
39442
|
+
return `plan → off · permission → ${decision.preset}`;
|
|
38540
39443
|
} catch (error) {
|
|
38541
|
-
bridge.notify(`
|
|
39444
|
+
bridge.notify(`mode change failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
38542
39445
|
return "";
|
|
38543
39446
|
}
|
|
38544
39447
|
};
|
|
@@ -38751,6 +39654,7 @@ async function run(ctx, startup, io) {
|
|
|
38751
39654
|
subagents.reset();
|
|
38752
39655
|
pendingMode = void 0;
|
|
38753
39656
|
pendingPermission = void 0;
|
|
39657
|
+
pendingPlan = false;
|
|
38754
39658
|
} catch (error) {
|
|
38755
39659
|
active = previous;
|
|
38756
39660
|
agent = previous?.agent;
|
|
@@ -38936,6 +39840,8 @@ async function run(ctx, startup, io) {
|
|
|
38936
39840
|
resumed: active?.resumed ?? false,
|
|
38937
39841
|
mode: active?.mode ?? pendingMode ?? normalizePresetId(presets.defaultId),
|
|
38938
39842
|
permission,
|
|
39843
|
+
/** Pre-session plan choice for the status badge until a session composes. */
|
|
39844
|
+
pendingPlan: session === void 0 && pendingPlan,
|
|
38939
39845
|
dispatch,
|
|
38940
39846
|
steer,
|
|
38941
39847
|
interrupt,
|
|
@@ -38960,7 +39866,7 @@ async function run(ctx, startup, io) {
|
|
|
38960
39866
|
prepareImages: (paths, signal) => saveImagePaths(paths, ctx.get("attachments"), signal),
|
|
38961
39867
|
inspectFiles: (paths) => inspectFilePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
|
|
38962
39868
|
prepareFiles: (paths, signal) => saveFilePaths(paths, ctx.get("attachments"), signal),
|
|
38963
|
-
|
|
39869
|
+
cycleMode,
|
|
38964
39870
|
setPermission: setPermissionAction,
|
|
38965
39871
|
selectModel,
|
|
38966
39872
|
subagentModel: subagentModelLabel(),
|
|
@@ -38993,6 +39899,8 @@ async function run(ctx, startup, io) {
|
|
|
38993
39899
|
switchSession,
|
|
38994
39900
|
cancelSessionSwitch,
|
|
38995
39901
|
loadPlugins: () => listPluginRows(ctx),
|
|
39902
|
+
probeUpdate: () => probeLauncherUpdate(),
|
|
39903
|
+
applyUpdate: (onLine) => applyLauncherUpdate(onLine),
|
|
38996
39904
|
loadJobs: () => listJobs(ctx, active?.agent),
|
|
38997
39905
|
statusline: statuslineItems,
|
|
38998
39906
|
saveStatusline,
|
|
@@ -39088,4 +39996,4 @@ function apply(ctx, config) {
|
|
|
39088
39996
|
});
|
|
39089
39997
|
}
|
|
39090
39998
|
//#endregion
|
|
39091
|
-
export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence, submissionBelongsToSession };
|
|
39999
|
+
export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, planCycleDecision, resolveTarget, runQuitSequence, submissionBelongsToSession };
|