reasonix 0.5.22 → 0.5.24
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/dist/cli/{chunk-ANMDY236.js → chunk-C266QOQU.js} +44 -1
- package/dist/cli/chunk-C266QOQU.js.map +1 -0
- package/dist/cli/index.js +1783 -375
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-75XLIUTO.js → prompt-OVVMCH5F.js} +2 -2
- package/dist/index.d.ts +225 -7
- package/dist/index.js +755 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-ANMDY236.js.map +0 -1
- /package/dist/cli/{prompt-75XLIUTO.js.map → prompt-OVVMCH5F.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
memoryEnabled,
|
|
8
8
|
readProjectMemory,
|
|
9
9
|
sanitizeMemoryName
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-C266QOQU.js";
|
|
11
11
|
|
|
12
12
|
// src/cli/index.ts
|
|
13
13
|
import { Command } from "commander";
|
|
@@ -67,6 +67,33 @@ function addProjectShellAllowed(rootDir, prefix, path = defaultConfigPath()) {
|
|
|
67
67
|
cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
|
|
68
68
|
writeConfig(cfg, path);
|
|
69
69
|
}
|
|
70
|
+
function loadEditMode(path = defaultConfigPath()) {
|
|
71
|
+
const v = readConfig(path).editMode;
|
|
72
|
+
return v === "auto" ? "auto" : "review";
|
|
73
|
+
}
|
|
74
|
+
function saveEditMode(mode, path = defaultConfigPath()) {
|
|
75
|
+
const cfg = readConfig(path);
|
|
76
|
+
cfg.editMode = mode;
|
|
77
|
+
writeConfig(cfg, path);
|
|
78
|
+
}
|
|
79
|
+
function editModeHintShown(path = defaultConfigPath()) {
|
|
80
|
+
return readConfig(path).editModeHintShown === true;
|
|
81
|
+
}
|
|
82
|
+
function loadReasoningEffort(path = defaultConfigPath()) {
|
|
83
|
+
const v = readConfig(path).reasoningEffort;
|
|
84
|
+
return v === "high" ? "high" : "max";
|
|
85
|
+
}
|
|
86
|
+
function saveReasoningEffort(effort, path = defaultConfigPath()) {
|
|
87
|
+
const cfg = readConfig(path);
|
|
88
|
+
cfg.reasoningEffort = effort;
|
|
89
|
+
writeConfig(cfg, path);
|
|
90
|
+
}
|
|
91
|
+
function markEditModeHintShown(path = defaultConfigPath()) {
|
|
92
|
+
const cfg = readConfig(path);
|
|
93
|
+
if (cfg.editModeHintShown === true) return;
|
|
94
|
+
cfg.editModeHintShown = true;
|
|
95
|
+
writeConfig(cfg, path);
|
|
96
|
+
}
|
|
70
97
|
function isPlausibleKey(key) {
|
|
71
98
|
const trimmed = key.trim();
|
|
72
99
|
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
@@ -126,8 +153,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
126
153
|
}
|
|
127
154
|
function sleep(ms, signal) {
|
|
128
155
|
if (ms <= 0) return Promise.resolve();
|
|
129
|
-
return new Promise((
|
|
130
|
-
const timer = setTimeout(
|
|
156
|
+
return new Promise((resolve8, reject) => {
|
|
157
|
+
const timer = setTimeout(resolve8, ms);
|
|
131
158
|
if (signal) {
|
|
132
159
|
const onAbort = () => {
|
|
133
160
|
clearTimeout(timer);
|
|
@@ -612,7 +639,7 @@ function matchesTool(hook, toolName) {
|
|
|
612
639
|
}
|
|
613
640
|
}
|
|
614
641
|
function defaultSpawner(input) {
|
|
615
|
-
return new Promise((
|
|
642
|
+
return new Promise((resolve8) => {
|
|
616
643
|
const child = spawn(input.command, {
|
|
617
644
|
cwd: input.cwd,
|
|
618
645
|
shell: true,
|
|
@@ -639,7 +666,7 @@ function defaultSpawner(input) {
|
|
|
639
666
|
});
|
|
640
667
|
child.once("error", (err) => {
|
|
641
668
|
clearTimeout(timer);
|
|
642
|
-
|
|
669
|
+
resolve8({
|
|
643
670
|
exitCode: null,
|
|
644
671
|
stdout: stdout2,
|
|
645
672
|
stderr,
|
|
@@ -649,7 +676,7 @@ function defaultSpawner(input) {
|
|
|
649
676
|
});
|
|
650
677
|
child.once("close", (code) => {
|
|
651
678
|
clearTimeout(timer);
|
|
652
|
-
|
|
679
|
+
resolve8({
|
|
653
680
|
exitCode: code,
|
|
654
681
|
stdout: stdout2.trim(),
|
|
655
682
|
stderr: stderr.trim(),
|
|
@@ -979,6 +1006,12 @@ var ToolRegistry = class {
|
|
|
979
1006
|
* bounced until the user approves a submitted plan.
|
|
980
1007
|
*/
|
|
981
1008
|
_planMode = false;
|
|
1009
|
+
/**
|
|
1010
|
+
* Optional hook run after arg parsing but before tool.fn. Lets the TUI
|
|
1011
|
+
* reroute specific tool calls (e.g. edit_file in review mode) without
|
|
1012
|
+
* modifying the tool definitions themselves.
|
|
1013
|
+
*/
|
|
1014
|
+
_interceptor = null;
|
|
982
1015
|
constructor(opts = {}) {
|
|
983
1016
|
this._autoFlatten = opts.autoFlatten !== false;
|
|
984
1017
|
}
|
|
@@ -990,6 +1023,14 @@ var ToolRegistry = class {
|
|
|
990
1023
|
get planMode() {
|
|
991
1024
|
return this._planMode;
|
|
992
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Install or clear the dispatch interceptor. At most one interceptor
|
|
1028
|
+
* is active at a time — calling twice replaces the previous. Pass
|
|
1029
|
+
* `null` to remove.
|
|
1030
|
+
*/
|
|
1031
|
+
setToolInterceptor(fn) {
|
|
1032
|
+
this._interceptor = fn;
|
|
1033
|
+
}
|
|
993
1034
|
register(def) {
|
|
994
1035
|
if (!def.name) throw new Error("tool requires a name");
|
|
995
1036
|
const internal = { ...def };
|
|
@@ -1046,6 +1087,16 @@ var ToolRegistry = class {
|
|
|
1046
1087
|
error: `${name}: unavailable in plan mode \u2014 this is a read-only exploration phase. Use read_file / list_directory / search_files / directory_tree / web_search / allowlisted shell commands to investigate. Call submit_plan with your proposed plan when you're ready for the user's review.`
|
|
1047
1088
|
});
|
|
1048
1089
|
}
|
|
1090
|
+
if (this._interceptor) {
|
|
1091
|
+
try {
|
|
1092
|
+
const short = await this._interceptor(name, args);
|
|
1093
|
+
if (typeof short === "string") return short;
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
return JSON.stringify({
|
|
1096
|
+
error: `${name}: interceptor failed \u2014 ${err.message}`
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1049
1100
|
try {
|
|
1050
1101
|
const result = await tool.fn(args, { signal: opts.signal });
|
|
1051
1102
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
@@ -1779,6 +1830,7 @@ function round(n, digits) {
|
|
|
1779
1830
|
}
|
|
1780
1831
|
|
|
1781
1832
|
// src/loop.ts
|
|
1833
|
+
var ARGS_COMPACT_THRESHOLD_TOKENS = 800;
|
|
1782
1834
|
var CacheFirstLoop = class {
|
|
1783
1835
|
client;
|
|
1784
1836
|
prefix;
|
|
@@ -1882,12 +1934,62 @@ var CacheFirstLoop = class {
|
|
|
1882
1934
|
* authored intent we can't mechanically shrink without losing
|
|
1883
1935
|
* meaning.
|
|
1884
1936
|
*/
|
|
1885
|
-
|
|
1937
|
+
/**
|
|
1938
|
+
* Conservative args-only shrink fired after every tool response —
|
|
1939
|
+
* strictly about ONE thing: stop oversized `edit_file` / `write_file`
|
|
1940
|
+
* arguments from riding every future turn's prompt.
|
|
1941
|
+
*
|
|
1942
|
+
* Why this is worth doing AUTOMATICALLY (not just on /compact):
|
|
1943
|
+
* Each tool-call arguments string sticks in the log verbatim. On a
|
|
1944
|
+
* coding session with ~10 edits, that's 20-40K tokens of stale
|
|
1945
|
+
* SEARCH/REPLACE text riding along on every turn. Even at a 98.9%
|
|
1946
|
+
* cache hit rate the input cost still adds up linearly (cache-hit
|
|
1947
|
+
* price × tokens × turns). Compacting IMMEDIATELY after the tool
|
|
1948
|
+
* responds means the next turn's prompt is already smaller — the
|
|
1949
|
+
* shrink is a one-time write that saves every future prompt.
|
|
1950
|
+
*
|
|
1951
|
+
* Threshold rationale: 800 tokens ≈ 3 KB. A typical 20-line edit's
|
|
1952
|
+
* args land well under that; massive rewrites (whole-file content,
|
|
1953
|
+
* 100+ line refactors) land above and get the compaction. Small
|
|
1954
|
+
* edits stay byte-verbatim so nothing common-case changes.
|
|
1955
|
+
*
|
|
1956
|
+
* Safety: we ONLY shrink args whose tool has ALREADY responded.
|
|
1957
|
+
* Structurally that's every call in `log.toMessages()` at this
|
|
1958
|
+
* point — the current turn's assistant/tool pairing is by
|
|
1959
|
+
* construction closed by the time we get here (append happens
|
|
1960
|
+
* AFTER dispatch). The in-flight assistant message being built
|
|
1961
|
+
* lives in scratch, not the log, so this pass can't touch it.
|
|
1962
|
+
*
|
|
1963
|
+
* Model impact: the model may occasionally want to reference the
|
|
1964
|
+
* exact SEARCH text of a prior edit — it then reads the file
|
|
1965
|
+
* directly (which shows current state) or looks at the preceding
|
|
1966
|
+
* assistant text (which has its plan). Losing the stale args is a
|
|
1967
|
+
* net win: one extra read_file vs. dragging N KB of stale text
|
|
1968
|
+
* through every subsequent turn.
|
|
1969
|
+
*/
|
|
1970
|
+
compactToolCallArgsAfterResponse() {
|
|
1886
1971
|
const before = this.log.toMessages();
|
|
1887
|
-
const { messages, healedCount
|
|
1972
|
+
const { messages, healedCount } = shrinkOversizedToolCallArgsByTokens(
|
|
1888
1973
|
before,
|
|
1889
|
-
|
|
1974
|
+
ARGS_COMPACT_THRESHOLD_TOKENS
|
|
1890
1975
|
);
|
|
1976
|
+
if (healedCount === 0) return;
|
|
1977
|
+
this.log.compactInPlace(messages);
|
|
1978
|
+
if (this.sessionName) {
|
|
1979
|
+
try {
|
|
1980
|
+
rewriteSession(this.sessionName, messages);
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
compact(maxTokens = 4e3) {
|
|
1986
|
+
const before = this.log.toMessages();
|
|
1987
|
+
const resultsPass = shrinkOversizedToolResultsByTokens(before, maxTokens);
|
|
1988
|
+
const argsPass = shrinkOversizedToolCallArgsByTokens(resultsPass.messages, maxTokens);
|
|
1989
|
+
const messages = argsPass.messages;
|
|
1990
|
+
const healedCount = resultsPass.healedCount + argsPass.healedCount;
|
|
1991
|
+
const tokensSaved = resultsPass.tokensSaved + argsPass.tokensSaved;
|
|
1992
|
+
const charsSaved = resultsPass.charsSaved + argsPass.charsSaved;
|
|
1891
1993
|
if (healedCount > 0) {
|
|
1892
1994
|
this.log.compactInPlace(messages);
|
|
1893
1995
|
if (this.sessionName) {
|
|
@@ -2130,8 +2232,8 @@ var CacheFirstLoop = class {
|
|
|
2130
2232
|
}
|
|
2131
2233
|
);
|
|
2132
2234
|
for (let k = 0; k < budget; k++) {
|
|
2133
|
-
const sample = queue.shift() ?? await new Promise((
|
|
2134
|
-
waiter =
|
|
2235
|
+
const sample = queue.shift() ?? await new Promise((resolve8) => {
|
|
2236
|
+
waiter = resolve8;
|
|
2135
2237
|
});
|
|
2136
2238
|
yield {
|
|
2137
2239
|
turn: this._turn,
|
|
@@ -2397,6 +2499,7 @@ ${reason}`;
|
|
|
2397
2499
|
name,
|
|
2398
2500
|
content: result
|
|
2399
2501
|
});
|
|
2502
|
+
this.compactToolCallArgsAfterResponse();
|
|
2400
2503
|
yield {
|
|
2401
2504
|
turn: this._turn,
|
|
2402
2505
|
role: "tool",
|
|
@@ -2582,6 +2685,56 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
|
|
|
2582
2685
|
});
|
|
2583
2686
|
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
2584
2687
|
}
|
|
2688
|
+
function shrinkOversizedToolCallArgsByTokens(messages, maxTokens) {
|
|
2689
|
+
let healedCount = 0;
|
|
2690
|
+
let tokensSaved = 0;
|
|
2691
|
+
let charsSaved = 0;
|
|
2692
|
+
const out = messages.map((msg) => {
|
|
2693
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls)) return msg;
|
|
2694
|
+
let changed = false;
|
|
2695
|
+
const newCalls = msg.tool_calls.map((call) => {
|
|
2696
|
+
const args = call.function?.arguments;
|
|
2697
|
+
if (typeof args !== "string" || args.length <= maxTokens) return call;
|
|
2698
|
+
const beforeTokens = countTokens(args);
|
|
2699
|
+
if (beforeTokens <= maxTokens) return call;
|
|
2700
|
+
const shrunk = shrinkJsonLongStrings(args);
|
|
2701
|
+
const afterTokens = countTokens(shrunk);
|
|
2702
|
+
if (afterTokens >= beforeTokens) return call;
|
|
2703
|
+
changed = true;
|
|
2704
|
+
healedCount += 1;
|
|
2705
|
+
tokensSaved += beforeTokens - afterTokens;
|
|
2706
|
+
charsSaved += args.length - shrunk.length;
|
|
2707
|
+
return { ...call, function: { ...call.function, arguments: shrunk } };
|
|
2708
|
+
});
|
|
2709
|
+
if (!changed) return msg;
|
|
2710
|
+
return { ...msg, tool_calls: newCalls };
|
|
2711
|
+
});
|
|
2712
|
+
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
2713
|
+
}
|
|
2714
|
+
function shrinkJsonLongStrings(jsonStr) {
|
|
2715
|
+
let parsed;
|
|
2716
|
+
try {
|
|
2717
|
+
parsed = JSON.parse(jsonStr);
|
|
2718
|
+
} catch {
|
|
2719
|
+
const head = jsonStr.slice(0, 200);
|
|
2720
|
+
return `${head}\u2026[shrunk: ${jsonStr.length} chars, unparsed]`;
|
|
2721
|
+
}
|
|
2722
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2723
|
+
return jsonStr;
|
|
2724
|
+
}
|
|
2725
|
+
const LONG_THRESHOLD = 300;
|
|
2726
|
+
const input = parsed;
|
|
2727
|
+
const output = {};
|
|
2728
|
+
for (const [k, v] of Object.entries(input)) {
|
|
2729
|
+
if (typeof v === "string" && v.length > LONG_THRESHOLD) {
|
|
2730
|
+
const newlines = v.match(/\n/g)?.length ?? 0;
|
|
2731
|
+
output[k] = `[\u2026shrunk: ${v.length} chars, ${newlines} lines \u2014 tool already responded, see result]`;
|
|
2732
|
+
} else {
|
|
2733
|
+
output[k] = v;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return JSON.stringify(output);
|
|
2737
|
+
}
|
|
2585
2738
|
function fixToolCallPairing(messages) {
|
|
2586
2739
|
const out = [];
|
|
2587
2740
|
let droppedAssistantCalls = 0;
|
|
@@ -2645,10 +2798,41 @@ function formatLoopError(err) {
|
|
|
2645
2798
|
if (msg.includes("maximum context length")) {
|
|
2646
2799
|
const reqMatch = msg.match(/requested\s+(\d+)\s+tokens/);
|
|
2647
2800
|
const requested = reqMatch ? `${Number(reqMatch[1]).toLocaleString()} tokens` : "too many tokens";
|
|
2648
|
-
return `Context overflow (DeepSeek 400): session history is ${requested}, past the
|
|
2801
|
+
return `Context overflow (DeepSeek 400): session history is ${requested}, past the model's prompt limit (V4: 1M tokens; legacy chat/reasoner: 131k). Usually a single tool result grew too big. Reasonix caps new tool results at 8k tokens and auto-heals oversized history on session load \u2014 a restart often clears it. If it still overflows, run /forget (delete the session) or /clear (drop the displayed history) to start fresh.`;
|
|
2802
|
+
}
|
|
2803
|
+
const m = /^DeepSeek (\d{3}):\s*([\s\S]*)$/.exec(msg);
|
|
2804
|
+
if (!m) return msg;
|
|
2805
|
+
const status = m[1] ?? "";
|
|
2806
|
+
const body = m[2] ?? "";
|
|
2807
|
+
const inner = extractDeepSeekErrorMessage(body);
|
|
2808
|
+
if (status === "401") {
|
|
2809
|
+
return `Authentication failed (DeepSeek 401): ${inner}. Your API key is rejected. Fix with \`reasonix setup\` or \`export DEEPSEEK_API_KEY=sk-...\`. Get one at https://platform.deepseek.com/api_keys.`;
|
|
2810
|
+
}
|
|
2811
|
+
if (status === "402") {
|
|
2812
|
+
return `Out of balance (DeepSeek 402): ${inner}. Top up at https://platform.deepseek.com/top_up \u2014 the panel header shows your balance once it's non-zero.`;
|
|
2813
|
+
}
|
|
2814
|
+
if (status === "422") {
|
|
2815
|
+
return `Invalid parameter (DeepSeek 422): ${inner}`;
|
|
2816
|
+
}
|
|
2817
|
+
if (status === "400") {
|
|
2818
|
+
return `Bad request (DeepSeek 400): ${inner}`;
|
|
2649
2819
|
}
|
|
2650
2820
|
return msg;
|
|
2651
2821
|
}
|
|
2822
|
+
function extractDeepSeekErrorMessage(body) {
|
|
2823
|
+
const trimmed = body.trim();
|
|
2824
|
+
if (!trimmed) return "(no message)";
|
|
2825
|
+
try {
|
|
2826
|
+
const parsed = JSON.parse(trimmed);
|
|
2827
|
+
if (parsed && typeof parsed === "object") {
|
|
2828
|
+
const obj = parsed;
|
|
2829
|
+
if (obj.error && typeof obj.error.message === "string") return obj.error.message;
|
|
2830
|
+
if (typeof obj.message === "string") return obj.message;
|
|
2831
|
+
}
|
|
2832
|
+
} catch {
|
|
2833
|
+
}
|
|
2834
|
+
return trimmed;
|
|
2835
|
+
}
|
|
2652
2836
|
|
|
2653
2837
|
// src/at-mentions.ts
|
|
2654
2838
|
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
@@ -2845,6 +3029,9 @@ import { promises as fs } from "fs";
|
|
|
2845
3029
|
import * as pathMod from "path";
|
|
2846
3030
|
var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
2847
3031
|
var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
|
|
3032
|
+
var DEFAULT_AUTO_PREVIEW_LINES = 200;
|
|
3033
|
+
var AUTO_PREVIEW_HEAD_LINES = 80;
|
|
3034
|
+
var AUTO_PREVIEW_TAIL_LINES = 40;
|
|
2848
3035
|
var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
2849
3036
|
"node_modules",
|
|
2850
3037
|
".git",
|
|
@@ -2937,14 +3124,22 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2937
3124
|
};
|
|
2938
3125
|
registry.register({
|
|
2939
3126
|
name: "read_file",
|
|
2940
|
-
description:
|
|
3127
|
+
description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
|
|
3128
|
+
- head: N \u2192 first N lines (imports, public API, small configs)
|
|
3129
|
+
- tail: N \u2192 last N lines (recently-added code, log tails)
|
|
3130
|
+
- range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
|
|
3131
|
+
When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_LINES} lines, the tool auto-returns a head+tail preview with an "N lines omitted" marker rather than dumping everything. If you need the middle, re-call with a range. Prefer search_content to locate a symbol first, then read_file with a range around the hit \u2014 one scoped read beats three full-file reads.`,
|
|
2941
3132
|
readOnly: true,
|
|
2942
3133
|
parameters: {
|
|
2943
3134
|
type: "object",
|
|
2944
3135
|
properties: {
|
|
2945
3136
|
path: { type: "string", description: "Path to read (relative to rootDir or absolute)." },
|
|
2946
3137
|
head: { type: "integer", description: "If set, return only the first N lines." },
|
|
2947
|
-
tail: { type: "integer", description: "If set, return only the last N lines." }
|
|
3138
|
+
tail: { type: "integer", description: "If set, return only the last N lines." },
|
|
3139
|
+
range: {
|
|
3140
|
+
type: "string",
|
|
3141
|
+
description: 'Inclusive line range like "50-100" or "50-50". 1-indexed. Takes precedence over head/tail when all three are set. Out-of-range requests clamp to file bounds.'
|
|
3142
|
+
}
|
|
2948
3143
|
},
|
|
2949
3144
|
required: ["path"]
|
|
2950
3145
|
},
|
|
@@ -2956,21 +3151,52 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2956
3151
|
}
|
|
2957
3152
|
const raw = await fs.readFile(abs);
|
|
2958
3153
|
if (raw.length > maxReadBytes) {
|
|
2959
|
-
const
|
|
2960
|
-
return `${
|
|
3154
|
+
const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
|
|
3155
|
+
return `${headBytes}
|
|
2961
3156
|
|
|
2962
|
-
[\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail for targeted view.]`;
|
|
3157
|
+
[\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail/range for targeted view.]`;
|
|
2963
3158
|
}
|
|
2964
3159
|
const text = raw.toString("utf8");
|
|
3160
|
+
let lines = text.split(/\r?\n/);
|
|
3161
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
|
|
3162
|
+
const totalLines = lines.length;
|
|
3163
|
+
if (typeof args.range === "string" && /^\d+\s*-\s*\d+$/.test(args.range)) {
|
|
3164
|
+
const [rawStart, rawEnd] = args.range.split("-").map((s) => Number.parseInt(s, 10));
|
|
3165
|
+
const start = Math.max(1, rawStart ?? 1);
|
|
3166
|
+
const end = Math.min(totalLines, Math.max(start, rawEnd ?? totalLines));
|
|
3167
|
+
const slice = lines.slice(start - 1, end);
|
|
3168
|
+
const label = `[range ${start}-${end} of ${totalLines} lines]`;
|
|
3169
|
+
return `${label}
|
|
3170
|
+
${slice.join("\n")}`;
|
|
3171
|
+
}
|
|
2965
3172
|
if (typeof args.head === "number" && args.head > 0) {
|
|
2966
|
-
|
|
3173
|
+
const count = Math.min(args.head, totalLines);
|
|
3174
|
+
const slice = lines.slice(0, count);
|
|
3175
|
+
const marker = count < totalLines ? `
|
|
3176
|
+
|
|
3177
|
+
[\u2026head ${count} of ${totalLines} lines \u2014 call again with range / tail for more]` : "";
|
|
3178
|
+
return slice.join("\n") + marker;
|
|
2967
3179
|
}
|
|
2968
3180
|
if (typeof args.tail === "number" && args.tail > 0) {
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3181
|
+
const count = Math.min(args.tail, totalLines);
|
|
3182
|
+
const slice = lines.slice(totalLines - count);
|
|
3183
|
+
const marker = count < totalLines ? `[\u2026tail ${count} of ${totalLines} lines \u2014 call again with range / head for more]
|
|
3184
|
+
|
|
3185
|
+
` : "";
|
|
3186
|
+
return marker + slice.join("\n");
|
|
3187
|
+
}
|
|
3188
|
+
if (totalLines <= DEFAULT_AUTO_PREVIEW_LINES) return lines.join("\n");
|
|
3189
|
+
const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
|
|
3190
|
+
const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
|
|
3191
|
+
const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
|
|
3192
|
+
return [
|
|
3193
|
+
`[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
|
|
3194
|
+
head,
|
|
3195
|
+
`
|
|
3196
|
+
[\u2026 ${omitted} lines omitted \u2014 call read_file again with range:"A-B" (1-indexed) or head / tail to get the middle]
|
|
3197
|
+
`,
|
|
3198
|
+
tail
|
|
3199
|
+
].join("\n");
|
|
2974
3200
|
}
|
|
2975
3201
|
});
|
|
2976
3202
|
registry.register({
|
|
@@ -2995,21 +3221,34 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2995
3221
|
});
|
|
2996
3222
|
registry.register({
|
|
2997
3223
|
name: "directory_tree",
|
|
2998
|
-
description:
|
|
3224
|
+
description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
|
|
3225
|
+
- maxDepth defaults to 2 (root + one level). A depth-4 tree on a real repo blew ~5K tokens in one call. If you truly need deeper, pass maxDepth:N explicitly.
|
|
3226
|
+
- Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
|
|
3227
|
+
- Large subtrees (>50 children) auto-collapse to "[N files, M dirs hidden \u2014 list_directory <path> to inspect]" so one huge folder can't dominate the output.
|
|
3228
|
+
Prefer \`list_directory\` for a single-level view, \`search_files\` to find specific paths, and \`search_content\` to find code.`,
|
|
2999
3229
|
readOnly: true,
|
|
3000
3230
|
parameters: {
|
|
3001
3231
|
type: "object",
|
|
3002
3232
|
properties: {
|
|
3003
3233
|
path: { type: "string", description: "Root of the tree (default: sandbox root)." },
|
|
3004
|
-
maxDepth: {
|
|
3234
|
+
maxDepth: {
|
|
3235
|
+
type: "integer",
|
|
3236
|
+
description: "Max recursion depth (default 2). Depth 0 shows only the top-level entries; depth 2 is usually enough to see module structure."
|
|
3237
|
+
},
|
|
3238
|
+
include_deps: {
|
|
3239
|
+
type: "boolean",
|
|
3240
|
+
description: "When true, also traverse node_modules / .git / dist / build / etc. Off by default \u2014 most exploration questions are about the user's own code."
|
|
3241
|
+
}
|
|
3005
3242
|
}
|
|
3006
3243
|
},
|
|
3007
3244
|
fn: async (args) => {
|
|
3008
3245
|
const startAbs = safePath(args.path ?? ".");
|
|
3009
|
-
const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth :
|
|
3246
|
+
const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 2;
|
|
3247
|
+
const includeDeps = args.include_deps === true;
|
|
3010
3248
|
const lines = [];
|
|
3011
3249
|
let totalBytes = 0;
|
|
3012
3250
|
let truncated = false;
|
|
3251
|
+
const PER_DIR_CHILD_CAP = 50;
|
|
3013
3252
|
const walk2 = async (dir, depth) => {
|
|
3014
3253
|
if (truncated) return;
|
|
3015
3254
|
if (depth > maxDepth) return;
|
|
@@ -3020,10 +3259,27 @@ function registerFilesystemTools(registry, opts) {
|
|
|
3020
3259
|
return;
|
|
3021
3260
|
}
|
|
3022
3261
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
3262
|
+
let emitted = 0;
|
|
3023
3263
|
for (const e of entries) {
|
|
3024
3264
|
if (truncated) return;
|
|
3265
|
+
const skip = e.isDirectory() && !includeDeps && SKIP_DIR_NAMES.has(e.name);
|
|
3266
|
+
if (emitted >= PER_DIR_CHILD_CAP) {
|
|
3267
|
+
const remaining = entries.length - emitted;
|
|
3268
|
+
let restFiles = 0;
|
|
3269
|
+
let restDirs = 0;
|
|
3270
|
+
for (const r of entries.slice(emitted)) {
|
|
3271
|
+
if (r.isDirectory()) restDirs++;
|
|
3272
|
+
else restFiles++;
|
|
3273
|
+
}
|
|
3274
|
+
const indent2 = " ".repeat(depth);
|
|
3275
|
+
lines.push(
|
|
3276
|
+
`${indent2}[\u2026 ${remaining} entries hidden (${restDirs} dirs, ${restFiles} files) \u2014 list_directory on this path to see all]`
|
|
3277
|
+
);
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3025
3280
|
const indent = " ".repeat(depth);
|
|
3026
|
-
const
|
|
3281
|
+
const suffix = skip ? " (skipped \u2014 pass include_deps:true to traverse)" : "";
|
|
3282
|
+
const line = e.isDirectory() ? `${indent}${e.name}/${suffix}` : `${indent}${e.name}`;
|
|
3027
3283
|
totalBytes += line.length + 1;
|
|
3028
3284
|
if (totalBytes > maxListBytes) {
|
|
3029
3285
|
lines.push(` [\u2026 tree truncated at ${maxListBytes} bytes \u2026]`);
|
|
@@ -3031,7 +3287,8 @@ function registerFilesystemTools(registry, opts) {
|
|
|
3031
3287
|
return;
|
|
3032
3288
|
}
|
|
3033
3289
|
lines.push(line);
|
|
3034
|
-
|
|
3290
|
+
emitted++;
|
|
3291
|
+
if (e.isDirectory() && !skip) {
|
|
3035
3292
|
await walk2(pathMod.join(dir, e.name), depth + 1);
|
|
3036
3293
|
}
|
|
3037
3294
|
}
|
|
@@ -3673,9 +3930,311 @@ function forkRegistryExcluding(parent, exclude) {
|
|
|
3673
3930
|
}
|
|
3674
3931
|
|
|
3675
3932
|
// src/tools/shell.ts
|
|
3676
|
-
import { spawn as
|
|
3933
|
+
import { spawn as spawn3 } from "child_process";
|
|
3677
3934
|
import { existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
3935
|
+
import * as pathMod3 from "path";
|
|
3936
|
+
|
|
3937
|
+
// src/tools/jobs.ts
|
|
3938
|
+
import { spawn as spawn2 } from "child_process";
|
|
3678
3939
|
import * as pathMod2 from "path";
|
|
3940
|
+
function killProcessTree(pid, signal) {
|
|
3941
|
+
if (process.platform === "win32") {
|
|
3942
|
+
const args = ["/pid", String(pid), "/T"];
|
|
3943
|
+
if (signal === "SIGKILL") args.push("/F");
|
|
3944
|
+
try {
|
|
3945
|
+
const killer = spawn2("taskkill", args, {
|
|
3946
|
+
stdio: "ignore",
|
|
3947
|
+
windowsHide: true
|
|
3948
|
+
});
|
|
3949
|
+
killer.on("error", () => {
|
|
3950
|
+
});
|
|
3951
|
+
} catch {
|
|
3952
|
+
}
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
try {
|
|
3956
|
+
process.kill(-pid, signal);
|
|
3957
|
+
return;
|
|
3958
|
+
} catch {
|
|
3959
|
+
}
|
|
3960
|
+
try {
|
|
3961
|
+
process.kill(pid, signal);
|
|
3962
|
+
} catch {
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
var DEFAULT_OUTPUT_CAP_BYTES = 64 * 1024;
|
|
3966
|
+
var READY_SIGNALS = [
|
|
3967
|
+
// HTTP server banners
|
|
3968
|
+
/\blistening on\b/i,
|
|
3969
|
+
/\blocal:\s+https?:\/\//i,
|
|
3970
|
+
/\bhttps?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?\b/i,
|
|
3971
|
+
/\b(?:ready|server started|started server|app listening)\b/i,
|
|
3972
|
+
// Bundlers / compilers
|
|
3973
|
+
/\bcompiled successfully\b/i,
|
|
3974
|
+
/\bbuild complete(?:d)?\b/i,
|
|
3975
|
+
/\bwatching for (?:file )?changes\b/i,
|
|
3976
|
+
/\bready in \d+/i,
|
|
3977
|
+
// Generic
|
|
3978
|
+
/\bstartup (?:complete|finished)\b/i
|
|
3979
|
+
];
|
|
3980
|
+
var JobRegistry = class {
|
|
3981
|
+
jobs = /* @__PURE__ */ new Map();
|
|
3982
|
+
nextId = 1;
|
|
3983
|
+
/**
|
|
3984
|
+
* Spawn a background child. Resolves after `waitSec` OR on ready
|
|
3985
|
+
* signal OR on early exit, whichever comes first. The child continues
|
|
3986
|
+
* to run (and buffer output) regardless of which path fires.
|
|
3987
|
+
*/
|
|
3988
|
+
async start(command, opts) {
|
|
3989
|
+
const trimmed = command.trim();
|
|
3990
|
+
if (!trimmed) throw new Error("run_background: empty command");
|
|
3991
|
+
const op = detectShellOperator(trimmed);
|
|
3992
|
+
if (op !== null) {
|
|
3993
|
+
throw new Error(
|
|
3994
|
+
`run_background: shell operator "${op}" is not supported \u2014 spawn one process per background job. Compose via your orchestration, not the shell.`
|
|
3995
|
+
);
|
|
3996
|
+
}
|
|
3997
|
+
const argv = tokenizeCommand(trimmed);
|
|
3998
|
+
if (argv.length === 0) throw new Error("run_background: empty command");
|
|
3999
|
+
const waitMs = Math.max(0, Math.min(30, opts.waitSec ?? 3)) * 1e3;
|
|
4000
|
+
const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
|
|
4001
|
+
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
4002
|
+
const spawnOpts = {
|
|
4003
|
+
cwd: pathMod2.resolve(opts.cwd),
|
|
4004
|
+
shell: false,
|
|
4005
|
+
windowsHide: true,
|
|
4006
|
+
env: process.env,
|
|
4007
|
+
// POSIX: detach so the child becomes its own process-group leader.
|
|
4008
|
+
// Required for `process.kill(-pid, …)` later — without it a group
|
|
4009
|
+
// kill fails and we end up only signaling the wrapper, leaving
|
|
4010
|
+
// grandchildren (node → vite → esbuild …) orphaned.
|
|
4011
|
+
// Windows: detached would spawn a new console window; leave the
|
|
4012
|
+
// default and use taskkill /T for tree termination.
|
|
4013
|
+
detached: process.platform !== "win32",
|
|
4014
|
+
...spawnOverrides
|
|
4015
|
+
};
|
|
4016
|
+
let child;
|
|
4017
|
+
try {
|
|
4018
|
+
child = spawn2(bin, args, spawnOpts);
|
|
4019
|
+
} catch (err) {
|
|
4020
|
+
const id2 = this.nextId++;
|
|
4021
|
+
const job2 = {
|
|
4022
|
+
id: id2,
|
|
4023
|
+
command: trimmed,
|
|
4024
|
+
pid: null,
|
|
4025
|
+
startedAt: Date.now(),
|
|
4026
|
+
exitCode: null,
|
|
4027
|
+
output: `[spawn failed] ${err.message}`,
|
|
4028
|
+
totalBytesWritten: 0,
|
|
4029
|
+
running: false,
|
|
4030
|
+
spawnError: err.message,
|
|
4031
|
+
child: null,
|
|
4032
|
+
readyPromise: Promise.resolve(),
|
|
4033
|
+
signalReady: () => {
|
|
4034
|
+
}
|
|
4035
|
+
};
|
|
4036
|
+
this.jobs.set(id2, job2);
|
|
4037
|
+
return {
|
|
4038
|
+
jobId: id2,
|
|
4039
|
+
pid: null,
|
|
4040
|
+
stillRunning: false,
|
|
4041
|
+
readyMatched: false,
|
|
4042
|
+
preview: job2.output,
|
|
4043
|
+
exitCode: null
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
const id = this.nextId++;
|
|
4047
|
+
let readyResolve = () => {
|
|
4048
|
+
};
|
|
4049
|
+
const readyPromise = new Promise((res) => {
|
|
4050
|
+
readyResolve = res;
|
|
4051
|
+
});
|
|
4052
|
+
const job = {
|
|
4053
|
+
id,
|
|
4054
|
+
command: trimmed,
|
|
4055
|
+
pid: child.pid ?? null,
|
|
4056
|
+
startedAt: Date.now(),
|
|
4057
|
+
exitCode: null,
|
|
4058
|
+
output: "",
|
|
4059
|
+
totalBytesWritten: 0,
|
|
4060
|
+
running: true,
|
|
4061
|
+
child,
|
|
4062
|
+
readyPromise,
|
|
4063
|
+
signalReady: readyResolve
|
|
4064
|
+
};
|
|
4065
|
+
this.jobs.set(id, job);
|
|
4066
|
+
let readyMatched = false;
|
|
4067
|
+
const onData = (chunk) => {
|
|
4068
|
+
const s = chunk.toString();
|
|
4069
|
+
job.totalBytesWritten += s.length;
|
|
4070
|
+
job.output += s;
|
|
4071
|
+
if (job.output.length > maxBytes) {
|
|
4072
|
+
const overflow = job.output.length - maxBytes;
|
|
4073
|
+
const cut = job.output.indexOf("\n", overflow);
|
|
4074
|
+
const start = cut >= 0 ? cut + 1 : overflow;
|
|
4075
|
+
job.output = `[\u2026 older output dropped \u2026]
|
|
4076
|
+
${job.output.slice(start)}`;
|
|
4077
|
+
}
|
|
4078
|
+
if (!readyMatched) {
|
|
4079
|
+
for (const re of READY_SIGNALS) {
|
|
4080
|
+
if (re.test(s) || re.test(job.output)) {
|
|
4081
|
+
readyMatched = true;
|
|
4082
|
+
job.signalReady();
|
|
4083
|
+
break;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
};
|
|
4088
|
+
child.stdout?.on("data", onData);
|
|
4089
|
+
child.stderr?.on("data", onData);
|
|
4090
|
+
child.on("error", (err) => {
|
|
4091
|
+
job.running = false;
|
|
4092
|
+
job.spawnError = err.message;
|
|
4093
|
+
job.signalReady();
|
|
4094
|
+
});
|
|
4095
|
+
child.on("close", (code) => {
|
|
4096
|
+
job.running = false;
|
|
4097
|
+
job.exitCode = code;
|
|
4098
|
+
job.signalReady();
|
|
4099
|
+
});
|
|
4100
|
+
const onAbort = () => this.stop(id, { graceMs: 100 });
|
|
4101
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
4102
|
+
let timer = null;
|
|
4103
|
+
await Promise.race([
|
|
4104
|
+
readyPromise,
|
|
4105
|
+
new Promise((res) => {
|
|
4106
|
+
timer = setTimeout(res, waitMs);
|
|
4107
|
+
})
|
|
4108
|
+
]);
|
|
4109
|
+
if (timer) clearTimeout(timer);
|
|
4110
|
+
return {
|
|
4111
|
+
jobId: id,
|
|
4112
|
+
pid: job.pid,
|
|
4113
|
+
stillRunning: job.running,
|
|
4114
|
+
readyMatched,
|
|
4115
|
+
preview: job.output,
|
|
4116
|
+
exitCode: job.exitCode
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* Read a job's accumulated output. `since` lets a caller poll
|
|
4121
|
+
* incrementally: pass the byte count returned from the last call to
|
|
4122
|
+
* get only newly-written content. Returns both full output and a
|
|
4123
|
+
* running snapshot so the caller can use whichever.
|
|
4124
|
+
*/
|
|
4125
|
+
read(id, opts = {}) {
|
|
4126
|
+
const job = this.jobs.get(id);
|
|
4127
|
+
if (!job) return null;
|
|
4128
|
+
const full = job.output;
|
|
4129
|
+
let slice = full;
|
|
4130
|
+
if (typeof opts.since === "number" && opts.since >= 0 && opts.since < full.length) {
|
|
4131
|
+
slice = full.slice(opts.since);
|
|
4132
|
+
}
|
|
4133
|
+
if (typeof opts.tailLines === "number" && opts.tailLines > 0) {
|
|
4134
|
+
const lines = slice.split("\n");
|
|
4135
|
+
const keep = lines.slice(Math.max(0, lines.length - opts.tailLines));
|
|
4136
|
+
slice = keep.join("\n");
|
|
4137
|
+
}
|
|
4138
|
+
return {
|
|
4139
|
+
output: slice,
|
|
4140
|
+
byteLength: full.length,
|
|
4141
|
+
running: job.running,
|
|
4142
|
+
exitCode: job.exitCode,
|
|
4143
|
+
command: job.command,
|
|
4144
|
+
pid: job.pid,
|
|
4145
|
+
spawnError: job.spawnError
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Send SIGTERM, wait `graceMs`, then SIGKILL if still alive. Returns
|
|
4150
|
+
* the final job record (or null when the job id is unknown). Safe to
|
|
4151
|
+
* call on an already-exited job — returns the record unchanged.
|
|
4152
|
+
*/
|
|
4153
|
+
async stop(id, opts = {}) {
|
|
4154
|
+
const job = this.jobs.get(id);
|
|
4155
|
+
if (!job) return null;
|
|
4156
|
+
if (!job.running || !job.child) return snapshot(job);
|
|
4157
|
+
const graceMs = Math.max(0, opts.graceMs ?? 2e3);
|
|
4158
|
+
if (job.pid !== null) {
|
|
4159
|
+
killProcessTree(job.pid, "SIGTERM");
|
|
4160
|
+
} else {
|
|
4161
|
+
try {
|
|
4162
|
+
job.child.kill("SIGTERM");
|
|
4163
|
+
} catch {
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
await Promise.race([job.readyPromise, new Promise((res) => setTimeout(res, graceMs))]);
|
|
4167
|
+
if (job.running) {
|
|
4168
|
+
if (job.pid !== null) {
|
|
4169
|
+
killProcessTree(job.pid, "SIGKILL");
|
|
4170
|
+
} else {
|
|
4171
|
+
try {
|
|
4172
|
+
job.child.kill("SIGKILL");
|
|
4173
|
+
} catch {
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
await new Promise((res) => setTimeout(res, 800));
|
|
4177
|
+
}
|
|
4178
|
+
return snapshot(job);
|
|
4179
|
+
}
|
|
4180
|
+
list() {
|
|
4181
|
+
return [...this.jobs.values()].map(snapshot);
|
|
4182
|
+
}
|
|
4183
|
+
/**
|
|
4184
|
+
* Best-effort kill of every still-running job. Called on TUI shutdown
|
|
4185
|
+
* so dev servers don't outlive the Reasonix process. Resolves after
|
|
4186
|
+
* every child has closed or a hard deadline passes (3s total).
|
|
4187
|
+
*/
|
|
4188
|
+
async shutdown(deadlineMs = 5e3) {
|
|
4189
|
+
const start = Date.now();
|
|
4190
|
+
const runningJobs = [...this.jobs.values()].filter((j) => j.running && j.child);
|
|
4191
|
+
if (runningJobs.length === 0) return;
|
|
4192
|
+
for (const job of runningJobs) {
|
|
4193
|
+
if (job.pid !== null) killProcessTree(job.pid, "SIGTERM");
|
|
4194
|
+
else
|
|
4195
|
+
try {
|
|
4196
|
+
job.child?.kill("SIGTERM");
|
|
4197
|
+
} catch {
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
const allClose = Promise.all(runningJobs.map((j) => j.readyPromise));
|
|
4201
|
+
const elapsed = () => Date.now() - start;
|
|
4202
|
+
const graceMs = Math.min(1500, Math.max(0, deadlineMs / 2));
|
|
4203
|
+
await Promise.race([allClose, new Promise((res) => setTimeout(res, graceMs))]);
|
|
4204
|
+
for (const job of runningJobs) {
|
|
4205
|
+
if (!job.running) continue;
|
|
4206
|
+
if (job.pid !== null) killProcessTree(job.pid, "SIGKILL");
|
|
4207
|
+
else
|
|
4208
|
+
try {
|
|
4209
|
+
job.child?.kill("SIGKILL");
|
|
4210
|
+
} catch {
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
const remaining = Math.max(800, deadlineMs - elapsed());
|
|
4214
|
+
await Promise.race([allClose, new Promise((res) => setTimeout(res, remaining))]);
|
|
4215
|
+
}
|
|
4216
|
+
/** Count of still-running jobs — drives the TUI status-bar indicator. */
|
|
4217
|
+
runningCount() {
|
|
4218
|
+
let n = 0;
|
|
4219
|
+
for (const job of this.jobs.values()) if (job.running) n++;
|
|
4220
|
+
return n;
|
|
4221
|
+
}
|
|
4222
|
+
};
|
|
4223
|
+
function snapshot(job) {
|
|
4224
|
+
return {
|
|
4225
|
+
id: job.id,
|
|
4226
|
+
command: job.command,
|
|
4227
|
+
pid: job.pid,
|
|
4228
|
+
startedAt: job.startedAt,
|
|
4229
|
+
exitCode: job.exitCode,
|
|
4230
|
+
output: job.output,
|
|
4231
|
+
totalBytesWritten: job.totalBytesWritten,
|
|
4232
|
+
running: job.running,
|
|
4233
|
+
spawnError: job.spawnError
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
// src/tools/shell.ts
|
|
3679
4238
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
3680
4239
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
3681
4240
|
var BUILTIN_ALLOWLIST = [
|
|
@@ -3844,10 +4403,10 @@ async function runCommand(cmd, opts) {
|
|
|
3844
4403
|
};
|
|
3845
4404
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
3846
4405
|
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
3847
|
-
return await new Promise((
|
|
4406
|
+
return await new Promise((resolve8, reject) => {
|
|
3848
4407
|
let child;
|
|
3849
4408
|
try {
|
|
3850
|
-
child =
|
|
4409
|
+
child = spawn3(bin, args, effectiveSpawnOpts);
|
|
3851
4410
|
} catch (err) {
|
|
3852
4411
|
reject(err);
|
|
3853
4412
|
return;
|
|
@@ -3877,7 +4436,7 @@ async function runCommand(cmd, opts) {
|
|
|
3877
4436
|
const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
|
|
3878
4437
|
|
|
3879
4438
|
[\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
|
|
3880
|
-
|
|
4439
|
+
resolve8({ exitCode: code, output, timedOut });
|
|
3881
4440
|
});
|
|
3882
4441
|
});
|
|
3883
4442
|
}
|
|
@@ -3885,16 +4444,16 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
3885
4444
|
const platform = opts.platform ?? process.platform;
|
|
3886
4445
|
if (platform !== "win32") return cmd;
|
|
3887
4446
|
if (!cmd) return cmd;
|
|
3888
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
3889
|
-
if (
|
|
4447
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod3.isAbsolute(cmd)) return cmd;
|
|
4448
|
+
if (pathMod3.extname(cmd)) return cmd;
|
|
3890
4449
|
const env = opts.env ?? process.env;
|
|
3891
4450
|
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
3892
|
-
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" :
|
|
4451
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod3.delimiter);
|
|
3893
4452
|
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
3894
4453
|
const isFile = opts.isFile ?? defaultIsFile;
|
|
3895
4454
|
for (const dir of pathDirs) {
|
|
3896
4455
|
for (const ext of pathExt) {
|
|
3897
|
-
const full =
|
|
4456
|
+
const full = pathMod3.win32.join(dir, cmd + ext);
|
|
3898
4457
|
if (isFile(full)) return full;
|
|
3899
4458
|
}
|
|
3900
4459
|
}
|
|
@@ -3964,8 +4523,8 @@ function withUtf8Codepage(cmdline) {
|
|
|
3964
4523
|
function isBareWindowsName(s) {
|
|
3965
4524
|
if (!s) return false;
|
|
3966
4525
|
if (s.includes("/") || s.includes("\\")) return false;
|
|
3967
|
-
if (
|
|
3968
|
-
if (
|
|
4526
|
+
if (pathMod3.isAbsolute(s)) return false;
|
|
4527
|
+
if (pathMod3.extname(s)) return false;
|
|
3969
4528
|
return true;
|
|
3970
4529
|
}
|
|
3971
4530
|
function quoteForCmdExe(arg) {
|
|
@@ -3984,12 +4543,13 @@ var NeedsConfirmationError = class extends Error {
|
|
|
3984
4543
|
}
|
|
3985
4544
|
};
|
|
3986
4545
|
function registerShellTools(registry, opts) {
|
|
3987
|
-
const rootDir =
|
|
4546
|
+
const rootDir = pathMod3.resolve(opts.rootDir);
|
|
3988
4547
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
3989
4548
|
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
4549
|
+
const jobs = opts.jobs ?? new JobRegistry();
|
|
3990
4550
|
const getExtraAllowed = typeof opts.extraAllowed === "function" ? opts.extraAllowed : (() => {
|
|
3991
|
-
const
|
|
3992
|
-
return () =>
|
|
4551
|
+
const snapshot2 = opts.extraAllowed ?? [];
|
|
4552
|
+
return () => snapshot2;
|
|
3993
4553
|
})();
|
|
3994
4554
|
const allowAll = opts.allowAll ?? false;
|
|
3995
4555
|
registry.register({
|
|
@@ -4035,8 +4595,126 @@ function registerShellTools(registry, opts) {
|
|
|
4035
4595
|
return formatCommandResult(cmd, result);
|
|
4036
4596
|
}
|
|
4037
4597
|
});
|
|
4598
|
+
registry.register({
|
|
4599
|
+
name: "run_background",
|
|
4600
|
+
description: "Spawn a long-running process (dev server, watcher, any command that doesn't naturally exit) and detach. Waits up to `waitSec` seconds for startup (or until the output matches a readiness signal like 'Local:', 'listening on', 'compiled successfully'), then returns the job id + startup preview. The process keeps running; call `job_output` to tail its logs, `stop_job` to kill it, `list_jobs` to see all running jobs. USE THIS \u2014 not `run_command` \u2014 for: npm/yarn/pnpm run dev, uvicorn / flask run, go run, cargo watch, tsc --watch, webpack serve, anything with 'dev' / 'serve' / 'watch' in the name.",
|
|
4601
|
+
parameters: {
|
|
4602
|
+
type: "object",
|
|
4603
|
+
properties: {
|
|
4604
|
+
command: {
|
|
4605
|
+
type: "string",
|
|
4606
|
+
description: "Full command line. Same quoting rules as run_command (no pipes / redirects / chaining)."
|
|
4607
|
+
},
|
|
4608
|
+
waitSec: {
|
|
4609
|
+
type: "integer",
|
|
4610
|
+
description: "Max seconds to wait for startup before returning. 0..30, default 3. A ready-signal match short-circuits this."
|
|
4611
|
+
}
|
|
4612
|
+
},
|
|
4613
|
+
required: ["command"]
|
|
4614
|
+
},
|
|
4615
|
+
fn: async (args, ctx) => {
|
|
4616
|
+
const cmd = args.command.trim();
|
|
4617
|
+
if (!cmd) throw new Error("run_background: empty command");
|
|
4618
|
+
if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
|
|
4619
|
+
throw new NeedsConfirmationError(cmd);
|
|
4620
|
+
}
|
|
4621
|
+
const result = await jobs.start(cmd, {
|
|
4622
|
+
cwd: rootDir,
|
|
4623
|
+
waitSec: args.waitSec,
|
|
4624
|
+
signal: ctx?.signal
|
|
4625
|
+
});
|
|
4626
|
+
return formatJobStart(result);
|
|
4627
|
+
}
|
|
4628
|
+
});
|
|
4629
|
+
registry.register({
|
|
4630
|
+
name: "job_output",
|
|
4631
|
+
description: "Read the latest output of a background job started with `run_background`. By default returns the tail of the buffer (last 80 lines). Pass `since` (the `byteLength` from a previous call) to stream only new content incrementally. Tells you whether the job is still running, so you can stop polling when it's done.",
|
|
4632
|
+
readOnly: true,
|
|
4633
|
+
parameters: {
|
|
4634
|
+
type: "object",
|
|
4635
|
+
properties: {
|
|
4636
|
+
jobId: { type: "integer", description: "Job id returned by run_background." },
|
|
4637
|
+
since: {
|
|
4638
|
+
type: "integer",
|
|
4639
|
+
description: "Return only output written past this byte offset (for incremental polling)."
|
|
4640
|
+
},
|
|
4641
|
+
tailLines: {
|
|
4642
|
+
type: "integer",
|
|
4643
|
+
description: "Cap the returned slice to the last N lines. Default 80, 0 = unlimited."
|
|
4644
|
+
}
|
|
4645
|
+
},
|
|
4646
|
+
required: ["jobId"]
|
|
4647
|
+
},
|
|
4648
|
+
fn: async (args) => {
|
|
4649
|
+
const out = jobs.read(args.jobId, {
|
|
4650
|
+
since: args.since,
|
|
4651
|
+
tailLines: args.tailLines ?? 80
|
|
4652
|
+
});
|
|
4653
|
+
if (!out) return `job ${args.jobId}: not found (use list_jobs)`;
|
|
4654
|
+
return formatJobRead(args.jobId, out);
|
|
4655
|
+
}
|
|
4656
|
+
});
|
|
4657
|
+
registry.register({
|
|
4658
|
+
name: "stop_job",
|
|
4659
|
+
description: "Stop a background job started with `run_background`. SIGTERM first; SIGKILL after a short grace period if it doesn't exit cleanly. Returns the final output + exit code. Safe to call on an already-exited job.",
|
|
4660
|
+
parameters: {
|
|
4661
|
+
type: "object",
|
|
4662
|
+
properties: {
|
|
4663
|
+
jobId: { type: "integer" }
|
|
4664
|
+
},
|
|
4665
|
+
required: ["jobId"]
|
|
4666
|
+
},
|
|
4667
|
+
fn: async (args) => {
|
|
4668
|
+
const rec = await jobs.stop(args.jobId);
|
|
4669
|
+
if (!rec) return `job ${args.jobId}: not found`;
|
|
4670
|
+
return formatJobStop(rec);
|
|
4671
|
+
}
|
|
4672
|
+
});
|
|
4673
|
+
registry.register({
|
|
4674
|
+
name: "list_jobs",
|
|
4675
|
+
description: "List every background job started this session \u2014 running and exited \u2014 with id, command, pid, status. Use when you've lost track of which job_id corresponds to which process, or to see what's still alive.",
|
|
4676
|
+
readOnly: true,
|
|
4677
|
+
parameters: { type: "object", properties: {} },
|
|
4678
|
+
fn: async () => {
|
|
4679
|
+
const all = jobs.list();
|
|
4680
|
+
if (all.length === 0) return "(no background jobs started this session)";
|
|
4681
|
+
return all.map(formatJobRow).join("\n");
|
|
4682
|
+
}
|
|
4683
|
+
});
|
|
4038
4684
|
return registry;
|
|
4039
4685
|
}
|
|
4686
|
+
function formatJobStart(r) {
|
|
4687
|
+
const header2 = r.stillRunning ? `[job ${r.jobId} started \xB7 pid ${r.pid ?? "?"} \xB7 ${r.readyMatched ? "READY signal matched" : "running (no ready signal yet)"}]` : r.exitCode !== null ? `[job ${r.jobId} exited during startup \xB7 exit ${r.exitCode}]` : `[job ${r.jobId} failed to start]`;
|
|
4688
|
+
return r.preview ? `${header2}
|
|
4689
|
+
${r.preview}` : header2;
|
|
4690
|
+
}
|
|
4691
|
+
function formatJobRead(jobId, r) {
|
|
4692
|
+
const status = r.running ? `running \xB7 pid ${r.pid ?? "?"}` : r.exitCode !== null ? `exited ${r.exitCode}` : r.spawnError ? `failed (${r.spawnError})` : "stopped";
|
|
4693
|
+
const header2 = `[job ${jobId} \xB7 ${status} \xB7 byteLength=${r.byteLength}]
|
|
4694
|
+
$ ${r.command}`;
|
|
4695
|
+
return r.output ? `${header2}
|
|
4696
|
+
${r.output}` : header2;
|
|
4697
|
+
}
|
|
4698
|
+
function formatJobStop(r) {
|
|
4699
|
+
const running = r.running ? "still running (SIGKILL may be pending)" : `exit ${r.exitCode ?? "?"}`;
|
|
4700
|
+
const tail = tailLines(r.output, 40);
|
|
4701
|
+
const header2 = `[job ${r.id} stopped \xB7 ${running}]
|
|
4702
|
+
$ ${r.command}`;
|
|
4703
|
+
return tail ? `${header2}
|
|
4704
|
+
${tail}` : header2;
|
|
4705
|
+
}
|
|
4706
|
+
function formatJobRow(r) {
|
|
4707
|
+
const age = ((Date.now() - r.startedAt) / 1e3).toFixed(1);
|
|
4708
|
+
const state = r.running ? `running \xB7 pid ${r.pid ?? "?"}` : r.exitCode !== null ? `exit ${r.exitCode}` : r.spawnError ? "failed" : "stopped";
|
|
4709
|
+
return ` ${String(r.id).padStart(3)} ${state.padEnd(24)} ${age}s ago $ ${r.command}`;
|
|
4710
|
+
}
|
|
4711
|
+
function tailLines(s, n) {
|
|
4712
|
+
if (!s) return "";
|
|
4713
|
+
const lines = s.split("\n");
|
|
4714
|
+
if (lines.length <= n) return s;
|
|
4715
|
+
const dropped = lines.length - n;
|
|
4716
|
+
return [`[\u2026 ${dropped} earlier lines \u2026]`, ...lines.slice(-n)].join("\n");
|
|
4717
|
+
}
|
|
4040
4718
|
function formatCommandResult(cmd, r) {
|
|
4041
4719
|
const header2 = r.timedOut ? `$ ${cmd}
|
|
4042
4720
|
[killed after timeout]` : `$ ${cmd}
|
|
@@ -4230,11 +4908,11 @@ ${i + 1}. ${r.title}`);
|
|
|
4230
4908
|
|
|
4231
4909
|
// src/env.ts
|
|
4232
4910
|
import { readFileSync as readFileSync6 } from "fs";
|
|
4233
|
-
import { resolve as
|
|
4911
|
+
import { resolve as resolve5 } from "path";
|
|
4234
4912
|
function loadDotenv(path = ".env") {
|
|
4235
4913
|
let raw;
|
|
4236
4914
|
try {
|
|
4237
|
-
raw = readFileSync6(
|
|
4915
|
+
raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
|
|
4238
4916
|
} catch {
|
|
4239
4917
|
return;
|
|
4240
4918
|
}
|
|
@@ -4947,7 +5625,7 @@ var McpClient = class {
|
|
|
4947
5625
|
const id = this.nextId++;
|
|
4948
5626
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
4949
5627
|
let abortHandler = null;
|
|
4950
|
-
const promise = new Promise((
|
|
5628
|
+
const promise = new Promise((resolve8, reject) => {
|
|
4951
5629
|
const timeout = setTimeout(() => {
|
|
4952
5630
|
this.pending.delete(id);
|
|
4953
5631
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -4956,7 +5634,7 @@ var McpClient = class {
|
|
|
4956
5634
|
);
|
|
4957
5635
|
}, this.requestTimeoutMs);
|
|
4958
5636
|
this.pending.set(id, {
|
|
4959
|
-
resolve:
|
|
5637
|
+
resolve: resolve8,
|
|
4960
5638
|
reject,
|
|
4961
5639
|
timeout
|
|
4962
5640
|
});
|
|
@@ -5038,7 +5716,7 @@ var McpClient = class {
|
|
|
5038
5716
|
};
|
|
5039
5717
|
|
|
5040
5718
|
// src/mcp/stdio.ts
|
|
5041
|
-
import { spawn as
|
|
5719
|
+
import { spawn as spawn4 } from "child_process";
|
|
5042
5720
|
var StdioTransport = class {
|
|
5043
5721
|
child;
|
|
5044
5722
|
queue = [];
|
|
@@ -5053,14 +5731,14 @@ var StdioTransport = class {
|
|
|
5053
5731
|
opts.command,
|
|
5054
5732
|
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
5055
5733
|
].join(" ");
|
|
5056
|
-
this.child =
|
|
5734
|
+
this.child = spawn4(line, [], {
|
|
5057
5735
|
env,
|
|
5058
5736
|
cwd: opts.cwd,
|
|
5059
5737
|
stdio: ["pipe", "pipe", "inherit"],
|
|
5060
5738
|
shell: true
|
|
5061
5739
|
});
|
|
5062
5740
|
} else {
|
|
5063
|
-
this.child =
|
|
5741
|
+
this.child = spawn4(opts.command, opts.args ?? [], {
|
|
5064
5742
|
env,
|
|
5065
5743
|
cwd: opts.cwd,
|
|
5066
5744
|
stdio: ["pipe", "pipe", "inherit"]
|
|
@@ -5079,12 +5757,12 @@ var StdioTransport = class {
|
|
|
5079
5757
|
}
|
|
5080
5758
|
async send(message) {
|
|
5081
5759
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
5082
|
-
return new Promise((
|
|
5760
|
+
return new Promise((resolve8, reject) => {
|
|
5083
5761
|
const line = `${JSON.stringify(message)}
|
|
5084
5762
|
`;
|
|
5085
5763
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
5086
5764
|
if (err) reject(err);
|
|
5087
|
-
else
|
|
5765
|
+
else resolve8();
|
|
5088
5766
|
});
|
|
5089
5767
|
});
|
|
5090
5768
|
}
|
|
@@ -5095,8 +5773,8 @@ var StdioTransport = class {
|
|
|
5095
5773
|
continue;
|
|
5096
5774
|
}
|
|
5097
5775
|
if (this.closed) return;
|
|
5098
|
-
const next = await new Promise((
|
|
5099
|
-
this.waiters.push(
|
|
5776
|
+
const next = await new Promise((resolve8) => {
|
|
5777
|
+
this.waiters.push(resolve8);
|
|
5100
5778
|
});
|
|
5101
5779
|
if (next === null) return;
|
|
5102
5780
|
yield next;
|
|
@@ -5162,8 +5840,8 @@ var SseTransport = class {
|
|
|
5162
5840
|
constructor(opts) {
|
|
5163
5841
|
this.url = opts.url;
|
|
5164
5842
|
this.headers = opts.headers ?? {};
|
|
5165
|
-
this.endpointReady = new Promise((
|
|
5166
|
-
this.resolveEndpoint =
|
|
5843
|
+
this.endpointReady = new Promise((resolve8, reject) => {
|
|
5844
|
+
this.resolveEndpoint = resolve8;
|
|
5167
5845
|
this.rejectEndpoint = reject;
|
|
5168
5846
|
});
|
|
5169
5847
|
this.endpointReady.catch(() => void 0);
|
|
@@ -5190,8 +5868,8 @@ var SseTransport = class {
|
|
|
5190
5868
|
continue;
|
|
5191
5869
|
}
|
|
5192
5870
|
if (this.closed) return;
|
|
5193
|
-
const next = await new Promise((
|
|
5194
|
-
this.waiters.push(
|
|
5871
|
+
const next = await new Promise((resolve8) => {
|
|
5872
|
+
this.waiters.push(resolve8);
|
|
5195
5873
|
});
|
|
5196
5874
|
if (next === null) return;
|
|
5197
5875
|
yield next;
|
|
@@ -5391,7 +6069,7 @@ async function trySection(load) {
|
|
|
5391
6069
|
|
|
5392
6070
|
// src/code/edit-blocks.ts
|
|
5393
6071
|
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5394
|
-
import { dirname as dirname5, resolve as
|
|
6072
|
+
import { dirname as dirname5, resolve as resolve6 } from "path";
|
|
5395
6073
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
5396
6074
|
function parseEditBlocks(text) {
|
|
5397
6075
|
const out = [];
|
|
@@ -5409,8 +6087,8 @@ function parseEditBlocks(text) {
|
|
|
5409
6087
|
return out;
|
|
5410
6088
|
}
|
|
5411
6089
|
function applyEditBlock(block, rootDir) {
|
|
5412
|
-
const absRoot =
|
|
5413
|
-
const absTarget =
|
|
6090
|
+
const absRoot = resolve6(rootDir);
|
|
6091
|
+
const absTarget = resolve6(absRoot, block.path);
|
|
5414
6092
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
5415
6093
|
return {
|
|
5416
6094
|
path: block.path,
|
|
@@ -5459,14 +6137,26 @@ function applyEditBlock(block, rootDir) {
|
|
|
5459
6137
|
function applyEditBlocks(blocks, rootDir) {
|
|
5460
6138
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
5461
6139
|
}
|
|
6140
|
+
function toWholeFileEditBlock(path, content, rootDir) {
|
|
6141
|
+
const abs = resolve6(rootDir, path);
|
|
6142
|
+
let search = "";
|
|
6143
|
+
if (existsSync6(abs)) {
|
|
6144
|
+
try {
|
|
6145
|
+
search = readFileSync8(abs, "utf8");
|
|
6146
|
+
} catch {
|
|
6147
|
+
search = "";
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
return { path, search, replace: content, offset: 0 };
|
|
6151
|
+
}
|
|
5462
6152
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
5463
|
-
const absRoot =
|
|
6153
|
+
const absRoot = resolve6(rootDir);
|
|
5464
6154
|
const seen = /* @__PURE__ */ new Set();
|
|
5465
6155
|
const snapshots = [];
|
|
5466
6156
|
for (const b of blocks) {
|
|
5467
6157
|
if (seen.has(b.path)) continue;
|
|
5468
6158
|
seen.add(b.path);
|
|
5469
|
-
const abs =
|
|
6159
|
+
const abs = resolve6(absRoot, b.path);
|
|
5470
6160
|
if (!existsSync6(abs)) {
|
|
5471
6161
|
snapshots.push({ path: b.path, prevContent: null });
|
|
5472
6162
|
continue;
|
|
@@ -5480,9 +6170,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
5480
6170
|
return snapshots;
|
|
5481
6171
|
}
|
|
5482
6172
|
function restoreSnapshots(snapshots, rootDir) {
|
|
5483
|
-
const absRoot =
|
|
6173
|
+
const absRoot = resolve6(rootDir);
|
|
5484
6174
|
return snapshots.map((snap) => {
|
|
5485
|
-
const abs =
|
|
6175
|
+
const abs = resolve6(absRoot, snap.path);
|
|
5486
6176
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
5487
6177
|
return {
|
|
5488
6178
|
path: snap.path,
|
|
@@ -5769,11 +6459,11 @@ function formatLogSize(path = defaultUsageLogPath()) {
|
|
|
5769
6459
|
// src/cli/commands/chat.tsx
|
|
5770
6460
|
import { existsSync as existsSync11, statSync as statSync6 } from "fs";
|
|
5771
6461
|
import { render } from "ink";
|
|
5772
|
-
import
|
|
6462
|
+
import React18, { useState as useState8 } from "react";
|
|
5773
6463
|
|
|
5774
6464
|
// src/cli/ui/App.tsx
|
|
5775
|
-
import { Box as
|
|
5776
|
-
import
|
|
6465
|
+
import { Box as Box14, Static, Text as Text14, useApp, useInput as useInput5 } from "ink";
|
|
6466
|
+
import React15, { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState6 } from "react";
|
|
5777
6467
|
|
|
5778
6468
|
// src/code/diff-preview.ts
|
|
5779
6469
|
function formatEditBlockDiff(block, opts = {}) {
|
|
@@ -5996,13 +6686,108 @@ function FileRow({ path, isSelected }) {
|
|
|
5996
6686
|
return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, marker, " ", base, dir ? ` ${dir}` : ""));
|
|
5997
6687
|
}
|
|
5998
6688
|
|
|
6689
|
+
// src/cli/ui/EditConfirm.tsx
|
|
6690
|
+
import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
|
|
6691
|
+
import React2, { useMemo, useState } from "react";
|
|
6692
|
+
var MODAL_OVERHEAD_ROWS = 18;
|
|
6693
|
+
var MIN_DIFF_ROWS = 8;
|
|
6694
|
+
function EditConfirm({ block, onChoose }) {
|
|
6695
|
+
const { stdout: stdout2 } = useStdout();
|
|
6696
|
+
const rows = stdout2?.rows ?? 40;
|
|
6697
|
+
const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
|
|
6698
|
+
const allLines = useMemo(
|
|
6699
|
+
() => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
|
|
6700
|
+
[block]
|
|
6701
|
+
);
|
|
6702
|
+
const [scroll, setScroll] = useState(0);
|
|
6703
|
+
const maxScroll = Math.max(0, allLines.length - budget);
|
|
6704
|
+
const effectiveScroll = Math.min(scroll, maxScroll);
|
|
6705
|
+
useInput((input, key) => {
|
|
6706
|
+
if (key.return || input === "y") {
|
|
6707
|
+
onChoose("apply");
|
|
6708
|
+
return;
|
|
6709
|
+
}
|
|
6710
|
+
if (input === "n") {
|
|
6711
|
+
onChoose("reject");
|
|
6712
|
+
return;
|
|
6713
|
+
}
|
|
6714
|
+
if (input === "a") {
|
|
6715
|
+
onChoose("apply-rest-of-turn");
|
|
6716
|
+
return;
|
|
6717
|
+
}
|
|
6718
|
+
if (input === "A") {
|
|
6719
|
+
onChoose("flip-to-auto");
|
|
6720
|
+
return;
|
|
6721
|
+
}
|
|
6722
|
+
if (key.downArrow || input === "j") {
|
|
6723
|
+
setScroll((s) => Math.min(maxScroll, s + 1));
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
6726
|
+
if (key.upArrow || input === "k") {
|
|
6727
|
+
setScroll((s) => Math.max(0, s - 1));
|
|
6728
|
+
return;
|
|
6729
|
+
}
|
|
6730
|
+
if (key.pageDown || input === " " || input === "f") {
|
|
6731
|
+
setScroll((s) => Math.min(maxScroll, s + Math.max(1, budget - 2)));
|
|
6732
|
+
return;
|
|
6733
|
+
}
|
|
6734
|
+
if (key.pageUp || input === "b") {
|
|
6735
|
+
setScroll((s) => Math.max(0, s - Math.max(1, budget - 2)));
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6738
|
+
if (input === "g") {
|
|
6739
|
+
setScroll(0);
|
|
6740
|
+
return;
|
|
6741
|
+
}
|
|
6742
|
+
if (input === "G") {
|
|
6743
|
+
setScroll(maxScroll);
|
|
6744
|
+
return;
|
|
6745
|
+
}
|
|
6746
|
+
});
|
|
6747
|
+
const isNew = block.search === "";
|
|
6748
|
+
const removed = isNew ? 0 : (block.search.match(/\n/g)?.length ?? 0) + 1;
|
|
6749
|
+
const added = block.replace === "" ? 0 : (block.replace.match(/\n/g)?.length ?? 0) + 1;
|
|
6750
|
+
const tag = isNew ? "NEW" : "EDIT";
|
|
6751
|
+
const visibleLines = allLines.slice(effectiveScroll, effectiveScroll + budget);
|
|
6752
|
+
const hiddenAbove = effectiveScroll;
|
|
6753
|
+
const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget);
|
|
6754
|
+
const totalLines = allLines.length;
|
|
6755
|
+
const showScrollHud = hiddenAbove + hiddenBelow > 0;
|
|
6756
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u25B8 model wants to edit a file")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, null, /* @__PURE__ */ React2.createElement(Text2, { color: isNew ? "green" : "yellow", bold: true }, `[${tag}] `), /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.path), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` (-${removed} +${added} lines)`), showScrollHud ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` \xB7 viewing ${effectiveScroll + 1}-${effectiveScroll + visibleLines.length}/${totalLines}`) : null)), hiddenAbove > 0 ? /* @__PURE__ */ React2.createElement(
|
|
6757
|
+
Text2,
|
|
6758
|
+
{
|
|
6759
|
+
dimColor: true
|
|
6760
|
+
},
|
|
6761
|
+
` \u2191 ${hiddenAbove} line${hiddenAbove === 1 ? "" : "s"} above (\u2191/k or PgUp)`
|
|
6762
|
+
) : null, /* @__PURE__ */ React2.createElement(Box2, { marginTop: hiddenAbove > 0 ? 0 : 1, flexDirection: "column" }, visibleLines.map((line, i) => {
|
|
6763
|
+
const trimmed = line.trimStart();
|
|
6764
|
+
const color = trimmed.startsWith("+") ? "green" : trimmed.startsWith("-") ? "red" : void 0;
|
|
6765
|
+
const dim = !color;
|
|
6766
|
+
return /* @__PURE__ */ React2.createElement(
|
|
6767
|
+
Text2,
|
|
6768
|
+
{
|
|
6769
|
+
key: `diff-${effectiveScroll}-${i}`,
|
|
6770
|
+
color,
|
|
6771
|
+
dimColor: dim
|
|
6772
|
+
},
|
|
6773
|
+
line
|
|
6774
|
+
);
|
|
6775
|
+
})), hiddenBelow > 0 ? /* @__PURE__ */ React2.createElement(
|
|
6776
|
+
Text2,
|
|
6777
|
+
{
|
|
6778
|
+
dimColor: true
|
|
6779
|
+
},
|
|
6780
|
+
` \u2193 ${hiddenBelow} line${hiddenBelow === 1 ? "" : "s"} below (\u2193/j or Space/PgDn)`
|
|
6781
|
+
) : null, /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "[", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "y"), "/Enter] apply \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "n"), "] reject \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "a"), "] apply rest \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "A"), "] flip AUTO \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "\u2191\u2193/Space"), "] scroll \xB7 [Esc] abort")));
|
|
6782
|
+
}
|
|
6783
|
+
|
|
5999
6784
|
// src/cli/ui/EventLog.tsx
|
|
6000
|
-
import { Box as
|
|
6001
|
-
import
|
|
6785
|
+
import { Box as Box5, Text as Text5, useStdout as useStdout2 } from "ink";
|
|
6786
|
+
import React6 from "react";
|
|
6002
6787
|
|
|
6003
6788
|
// src/cli/ui/PlanStateBlock.tsx
|
|
6004
|
-
import { Box as
|
|
6005
|
-
import
|
|
6789
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
6790
|
+
import React3 from "react";
|
|
6006
6791
|
function PlanStateBlock({ planState }) {
|
|
6007
6792
|
const fields = [];
|
|
6008
6793
|
if (planState.subgoals.length) fields.push(["subgoals", planState.subgoals, "cyan", false]);
|
|
@@ -6013,14 +6798,14 @@ function PlanStateBlock({ planState }) {
|
|
|
6013
6798
|
if (planState.rejectedPaths.length)
|
|
6014
6799
|
fields.push(["rejected", planState.rejectedPaths, "red", true]);
|
|
6015
6800
|
if (fields.length === 0) return null;
|
|
6016
|
-
return /* @__PURE__ */
|
|
6801
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color, dim]) => /* @__PURE__ */ React3.createElement(Text3, { key: label }, /* @__PURE__ */ React3.createElement(Text3, { color, bold: true, dimColor: dim }, "\u2039 ", label), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, ` (${items.length})`), /* @__PURE__ */ React3.createElement(Text3, null, `: ${items.join(" \xB7 ")}`))));
|
|
6017
6802
|
}
|
|
6018
6803
|
|
|
6019
6804
|
// src/cli/ui/markdown.tsx
|
|
6020
6805
|
import { readFileSync as readFileSync12, statSync as statSync5 } from "fs";
|
|
6021
6806
|
import { isAbsolute as isAbsolute4, join as join10 } from "path";
|
|
6022
|
-
import { Box as
|
|
6023
|
-
import
|
|
6807
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
6808
|
+
import React4 from "react";
|
|
6024
6809
|
var SUPERSCRIPT = {
|
|
6025
6810
|
"0": "\u2070",
|
|
6026
6811
|
"1": "\xB9",
|
|
@@ -6145,57 +6930,57 @@ function InlineMd({
|
|
|
6145
6930
|
for (const m of text.matchAll(INLINE_RE)) {
|
|
6146
6931
|
const start = m.index ?? 0;
|
|
6147
6932
|
if (start > last) {
|
|
6148
|
-
parts.push(/* @__PURE__ */
|
|
6933
|
+
parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `t${idx++}` }, text.slice(last, start)));
|
|
6149
6934
|
}
|
|
6150
6935
|
if (m[2] !== void 0 && m[3] !== void 0) {
|
|
6151
6936
|
const linkText = m[2];
|
|
6152
6937
|
const url = m[3];
|
|
6153
6938
|
if (isExternalUrl(url)) {
|
|
6154
6939
|
parts.push(
|
|
6155
|
-
/* @__PURE__ */
|
|
6940
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "blue", underline: true }, linkText)
|
|
6156
6941
|
);
|
|
6157
6942
|
} else {
|
|
6158
6943
|
const status = citations?.get(url);
|
|
6159
6944
|
if (status && !status.ok) {
|
|
6160
6945
|
parts.push(
|
|
6161
|
-
/* @__PURE__ */
|
|
6946
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
|
|
6162
6947
|
);
|
|
6163
6948
|
} else {
|
|
6164
6949
|
parts.push(
|
|
6165
|
-
/* @__PURE__ */
|
|
6950
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "cyan", underline: true }, linkText)
|
|
6166
6951
|
);
|
|
6167
6952
|
}
|
|
6168
6953
|
}
|
|
6169
6954
|
} else if (m[4] !== void 0) {
|
|
6170
6955
|
parts.push(
|
|
6171
|
-
/* @__PURE__ */
|
|
6956
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `b${idx++}`, bold: true }, m[4])
|
|
6172
6957
|
);
|
|
6173
6958
|
} else if (m[5] !== void 0) {
|
|
6174
6959
|
const stripped = m[5].replace(/^(\w+)\s+/, "");
|
|
6175
6960
|
parts.push(
|
|
6176
|
-
/* @__PURE__ */
|
|
6961
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, stripped)
|
|
6177
6962
|
);
|
|
6178
6963
|
} else if (m[6] !== void 0) {
|
|
6179
6964
|
parts.push(
|
|
6180
|
-
/* @__PURE__ */
|
|
6965
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, m[6])
|
|
6181
6966
|
);
|
|
6182
6967
|
} else if (m[7] !== void 0) {
|
|
6183
6968
|
parts.push(
|
|
6184
|
-
/* @__PURE__ */
|
|
6969
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `i${idx++}`, italic: true }, m[7])
|
|
6185
6970
|
);
|
|
6186
6971
|
}
|
|
6187
6972
|
last = start + m[0].length;
|
|
6188
6973
|
}
|
|
6189
6974
|
if (last < text.length) {
|
|
6190
|
-
parts.push(/* @__PURE__ */
|
|
6975
|
+
parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `t${idx++}` }, text.slice(last)));
|
|
6191
6976
|
}
|
|
6192
6977
|
if (padTo !== void 0) {
|
|
6193
6978
|
const seen = visibleWidth(text);
|
|
6194
6979
|
if (seen < padTo) {
|
|
6195
|
-
parts.push(/* @__PURE__ */
|
|
6980
|
+
parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `pad${idx++}` }, " ".repeat(padTo - seen)));
|
|
6196
6981
|
}
|
|
6197
6982
|
}
|
|
6198
|
-
return /* @__PURE__ */
|
|
6983
|
+
return /* @__PURE__ */ React4.createElement(Text4, null, parts);
|
|
6199
6984
|
}
|
|
6200
6985
|
function stripInlineMarkup(s) {
|
|
6201
6986
|
return s.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, "$1").replace(/\*\*([^*\n]+?)\*\*/g, "$1").replace(/```([^\n]+?)```/g, (_m, c) => c.replace(/^(\w+)\s+/, "")).replace(/`([^`\n]+?)`/g, "$1").replace(/(?<![*\w])\*([^*\n]+?)\*(?!\w)/g, "$1");
|
|
@@ -6391,19 +7176,19 @@ function parseBlocks(raw) {
|
|
|
6391
7176
|
function BlockView({ block, citations }) {
|
|
6392
7177
|
switch (block.kind) {
|
|
6393
7178
|
case "heading":
|
|
6394
|
-
return /* @__PURE__ */
|
|
7179
|
+
return /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, /* @__PURE__ */ React4.createElement(InlineMd, { text: block.text, citations }));
|
|
6395
7180
|
case "paragraph":
|
|
6396
|
-
return /* @__PURE__ */
|
|
7181
|
+
return /* @__PURE__ */ React4.createElement(InlineMd, { text: block.text, citations });
|
|
6397
7182
|
case "bullet":
|
|
6398
|
-
return /* @__PURE__ */
|
|
7183
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React4.createElement(Box4, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React4.createElement(InlineMd, { text: item, citations }))));
|
|
6399
7184
|
case "code":
|
|
6400
|
-
return /* @__PURE__ */
|
|
7185
|
+
return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, block.text));
|
|
6401
7186
|
case "edit-block":
|
|
6402
|
-
return /* @__PURE__ */
|
|
7187
|
+
return /* @__PURE__ */ React4.createElement(EditBlockRow, { block });
|
|
6403
7188
|
case "table":
|
|
6404
|
-
return /* @__PURE__ */
|
|
7189
|
+
return /* @__PURE__ */ React4.createElement(TableBlockRow, { block, citations });
|
|
6405
7190
|
case "hr":
|
|
6406
|
-
return /* @__PURE__ */
|
|
7191
|
+
return /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
6407
7192
|
}
|
|
6408
7193
|
}
|
|
6409
7194
|
function splitTableRow(line) {
|
|
@@ -6421,14 +7206,14 @@ function TableBlockRow({ block, citations }) {
|
|
|
6421
7206
|
widths.push(Math.min(40, Math.max(3, ...cellLengths)));
|
|
6422
7207
|
}
|
|
6423
7208
|
const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
|
|
6424
|
-
return /* @__PURE__ */
|
|
7209
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box4, null, block.header.map((cell, ci) => (
|
|
6425
7210
|
// biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
|
|
6426
|
-
/* @__PURE__ */
|
|
6427
|
-
))), /* @__PURE__ */
|
|
7211
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React4.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
|
|
7212
|
+
))), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, separator), block.rows.map((row2, ri) => (
|
|
6428
7213
|
// biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
|
|
6429
|
-
/* @__PURE__ */
|
|
7214
|
+
/* @__PURE__ */ React4.createElement(Box4, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
|
|
6430
7215
|
// biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
|
|
6431
|
-
/* @__PURE__ */
|
|
7216
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React4.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
|
|
6432
7217
|
)))
|
|
6433
7218
|
)));
|
|
6434
7219
|
}
|
|
@@ -6448,44 +7233,44 @@ function EditBlockRow({ block }) {
|
|
|
6448
7233
|
const isNewFile = block.search.length === 0;
|
|
6449
7234
|
const searchLines = block.search.split("\n");
|
|
6450
7235
|
const replaceLines = block.replace.split("\n");
|
|
6451
|
-
return /* @__PURE__ */
|
|
7236
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React4.createElement(Text4, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React4.createElement(Text4, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React4.createElement(Text4, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
|
|
6452
7237
|
}
|
|
6453
7238
|
function Markdown({ text, projectRoot }) {
|
|
6454
7239
|
const cleaned = stripMath(text);
|
|
6455
7240
|
const root = projectRoot ?? process.cwd();
|
|
6456
|
-
const citations =
|
|
6457
|
-
const blocks =
|
|
7241
|
+
const citations = React4.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
|
|
7242
|
+
const blocks = React4.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
6458
7243
|
const broken = [];
|
|
6459
7244
|
for (const [url, status] of citations) {
|
|
6460
7245
|
if (!status.ok) broken.push({ url, reason: status.reason });
|
|
6461
7246
|
}
|
|
6462
|
-
return /* @__PURE__ */
|
|
7247
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React4.createElement(BlockView, { key: `${i}-${b.kind}`, block: b, citations })), broken.length > 0 ? /* @__PURE__ */ React4.createElement(BrokenCitationsBlock, { items: broken }) : null);
|
|
6463
7248
|
}
|
|
6464
7249
|
function BrokenCitationsBlock({ items }) {
|
|
6465
|
-
return /* @__PURE__ */
|
|
7250
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "red", bold: true }, `\u26A0 ${items.length} broken citation${items.length > 1 ? "s" : ""} \u2014 the model referenced paths or lines that don't exist`), items.map((b, i) => (
|
|
6466
7251
|
// biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
|
|
6467
|
-
/* @__PURE__ */
|
|
7252
|
+
/* @__PURE__ */ React4.createElement(Text4, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
|
|
6468
7253
|
)));
|
|
6469
7254
|
}
|
|
6470
7255
|
|
|
6471
7256
|
// src/cli/ui/ticker.tsx
|
|
6472
|
-
import
|
|
7257
|
+
import React5, { createContext, useContext, useEffect, useState as useState2 } from "react";
|
|
6473
7258
|
var TICK_MS = 120;
|
|
6474
7259
|
var TickContext = createContext(0);
|
|
6475
7260
|
function TickerProvider({ children, disabled }) {
|
|
6476
|
-
const [tick, setTick] =
|
|
7261
|
+
const [tick, setTick] = useState2(0);
|
|
6477
7262
|
useEffect(() => {
|
|
6478
7263
|
if (disabled) return;
|
|
6479
7264
|
const id = setInterval(() => setTick((t) => t + 1), TICK_MS);
|
|
6480
7265
|
return () => clearInterval(id);
|
|
6481
7266
|
}, [disabled]);
|
|
6482
|
-
return /* @__PURE__ */
|
|
7267
|
+
return /* @__PURE__ */ React5.createElement(TickContext.Provider, { value: tick }, children);
|
|
6483
7268
|
}
|
|
6484
7269
|
function useTick() {
|
|
6485
7270
|
return useContext(TickContext);
|
|
6486
7271
|
}
|
|
6487
7272
|
function useElapsedSeconds() {
|
|
6488
|
-
const [start] =
|
|
7273
|
+
const [start] = useState2(() => Date.now());
|
|
6489
7274
|
useTick();
|
|
6490
7275
|
return Math.floor((Date.now() - start) / 1e3);
|
|
6491
7276
|
}
|
|
@@ -6505,18 +7290,18 @@ function RoleGlyph({
|
|
|
6505
7290
|
glyph,
|
|
6506
7291
|
color
|
|
6507
7292
|
}) {
|
|
6508
|
-
return /* @__PURE__ */
|
|
7293
|
+
return /* @__PURE__ */ React6.createElement(Text5, { color, bold: true }, glyph);
|
|
6509
7294
|
}
|
|
6510
|
-
var EventRow =
|
|
7295
|
+
var EventRow = React6.memo(function EventRow2({
|
|
6511
7296
|
event,
|
|
6512
7297
|
projectRoot
|
|
6513
7298
|
}) {
|
|
6514
7299
|
if (event.role === "user") {
|
|
6515
|
-
return /* @__PURE__ */
|
|
7300
|
+
return /* @__PURE__ */ React6.createElement(Box5, { marginTop: event.leadSeparator ? 1 : 0 }, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.user, color: "cyan" }), /* @__PURE__ */ React6.createElement(Text5, null, " ", event.text));
|
|
6516
7301
|
}
|
|
6517
7302
|
if (event.role === "assistant") {
|
|
6518
|
-
if (event.streaming) return /* @__PURE__ */
|
|
6519
|
-
return /* @__PURE__ */
|
|
7303
|
+
if (event.streaming) return /* @__PURE__ */ React6.createElement(StreamingAssistant, { event });
|
|
7304
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.assistant, color: "green" }), event.stats ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, ` ${event.stats.model}`) : null), /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, event.branch ? /* @__PURE__ */ React6.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React6.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React6.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React6.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React6.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React6.createElement(Text5, { color: "magenta" }, event.repair) : null));
|
|
6520
7305
|
}
|
|
6521
7306
|
if (event.role === "tool") {
|
|
6522
7307
|
const isError = event.text.startsWith("ERROR:");
|
|
@@ -6524,31 +7309,31 @@ var EventRow = React5.memo(function EventRow2({
|
|
|
6524
7309
|
const glyph = isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
|
|
6525
7310
|
const marker = isError ? "\u2717" : "\u2192";
|
|
6526
7311
|
const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
|
|
6527
|
-
return /* @__PURE__ */
|
|
7312
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph, color }), /* @__PURE__ */ React6.createElement(Text5, { color, bold: true }, ` ${event.toolName ?? "?"}`), /* @__PURE__ */ React6.createElement(Text5, { color, dimColor: true }, ` ${marker}`)), /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, isEditFile ? /* @__PURE__ */ React6.createElement(EditFileDiff, { text: event.text }) : /* @__PURE__ */ React6.createElement(Text5, { color: isError ? "red" : void 0, dimColor: !isError }, truncate2(event.text, 400))));
|
|
6528
7313
|
}
|
|
6529
7314
|
if (event.role === "error") {
|
|
6530
|
-
return /* @__PURE__ */
|
|
7315
|
+
return /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.error, color: "red" }), /* @__PURE__ */ React6.createElement(Text5, { color: "red" }, " ", event.text));
|
|
6531
7316
|
}
|
|
6532
7317
|
if (event.role === "info") {
|
|
6533
|
-
return /* @__PURE__ */
|
|
7318
|
+
return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, event.text));
|
|
6534
7319
|
}
|
|
6535
7320
|
if (event.role === "warning") {
|
|
6536
|
-
return /* @__PURE__ */
|
|
7321
|
+
return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.warning, color: "yellow" }), /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, " ", event.text));
|
|
6537
7322
|
}
|
|
6538
|
-
return /* @__PURE__ */
|
|
7323
|
+
return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, null, event.text));
|
|
6539
7324
|
});
|
|
6540
7325
|
function EditFileDiff({ text }) {
|
|
6541
7326
|
const lines = text.split(/\r?\n/);
|
|
6542
7327
|
const [statusHeader, hunkHeader, ...body] = lines;
|
|
6543
|
-
return /* @__PURE__ */
|
|
7328
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, ` ${statusHeader ?? ""}`), hunkHeader !== void 0 ? /* @__PURE__ */ React6.createElement(Text5, { color: "cyan", bold: true }, hunkHeader) : null, body.map((line, i) => {
|
|
6544
7329
|
const key = `${i}-${line.slice(0, 32)}`;
|
|
6545
7330
|
if (line.startsWith("- ")) {
|
|
6546
|
-
return /* @__PURE__ */
|
|
7331
|
+
return /* @__PURE__ */ React6.createElement(Text5, { key, color: "red" }, line);
|
|
6547
7332
|
}
|
|
6548
7333
|
if (line.startsWith("+ ")) {
|
|
6549
|
-
return /* @__PURE__ */
|
|
7334
|
+
return /* @__PURE__ */ React6.createElement(Text5, { key, color: "green" }, line);
|
|
6550
7335
|
}
|
|
6551
|
-
return /* @__PURE__ */
|
|
7336
|
+
return /* @__PURE__ */ React6.createElement(Text5, { key, dimColor: true }, line);
|
|
6552
7337
|
}));
|
|
6553
7338
|
}
|
|
6554
7339
|
function BranchBlock({ branch }) {
|
|
@@ -6557,33 +7342,33 @@ function BranchBlock({ branch }) {
|
|
|
6557
7342
|
const t = (branch.temperatures[i] ?? 0).toFixed(1);
|
|
6558
7343
|
return `${marker} #${i} T=${t} u=${u}`;
|
|
6559
7344
|
}).join(" ");
|
|
6560
|
-
return /* @__PURE__ */
|
|
7345
|
+
return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { color: "blue" }, "\u2387 branched ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, per)));
|
|
6561
7346
|
}
|
|
6562
7347
|
function ReasoningBlock({ reasoning }) {
|
|
6563
7348
|
const max = 260;
|
|
6564
7349
|
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
6565
7350
|
const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
|
|
6566
|
-
return /* @__PURE__ */
|
|
7351
|
+
return /* @__PURE__ */ React6.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u258F "), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true, italic: true }, "thinking ", preview));
|
|
6567
7352
|
}
|
|
6568
7353
|
function Elapsed() {
|
|
6569
7354
|
const s = useElapsedSeconds();
|
|
6570
7355
|
const mm = String(Math.floor(s / 60)).padStart(2, "0");
|
|
6571
7356
|
const ss = String(s % 60).padStart(2, "0");
|
|
6572
|
-
return /* @__PURE__ */
|
|
7357
|
+
return /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, `${mm}:${ss}`);
|
|
6573
7358
|
}
|
|
6574
7359
|
function PulsingAssistantGlyph() {
|
|
6575
7360
|
const tick = useTick();
|
|
6576
7361
|
const on = Math.floor(tick / 4) % 2 === 0;
|
|
6577
|
-
return /* @__PURE__ */
|
|
7362
|
+
return /* @__PURE__ */ React6.createElement(Text5, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
|
|
6578
7363
|
}
|
|
6579
7364
|
function StreamingAssistant({ event }) {
|
|
6580
7365
|
if (event.branchProgress) {
|
|
6581
7366
|
const p = event.branchProgress;
|
|
6582
7367
|
if (p.completed === 0) {
|
|
6583
|
-
return /* @__PURE__ */
|
|
7368
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React6.createElement(Text5, { color: "blue" }, " \u2387 launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026 "), /* @__PURE__ */ React6.createElement(Elapsed, null)), /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, " ", "spread across T=0.0/0.5/1.0 \xB7 reasoner typically takes 30-90s \u2014 this is normal"));
|
|
6584
7369
|
}
|
|
6585
7370
|
const pct2 = Math.round(p.completed / p.total * 100);
|
|
6586
|
-
return /* @__PURE__ */
|
|
7371
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React6.createElement(Text5, { color: "blue" }, " \u2387 branching ", p.completed, "/", p.total, " (", pct2, "%) "), /* @__PURE__ */ React6.createElement(Elapsed, null)), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
|
|
6587
7372
|
}
|
|
6588
7373
|
const tail = lastLine(event.text, 140);
|
|
6589
7374
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
@@ -6613,16 +7398,16 @@ function StreamingAssistant({ event }) {
|
|
|
6613
7398
|
label = parts.join(" \xB7 ");
|
|
6614
7399
|
labelColor = "green";
|
|
6615
7400
|
}
|
|
6616
|
-
return /* @__PURE__ */
|
|
7401
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(Pulse, null), /* @__PURE__ */ React6.createElement(Text5, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React6.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? (
|
|
6617
7402
|
// Non-dim yellow: first-time users misread the dim version as
|
|
6618
7403
|
// "app frozen". The reassurance has to be VISIBLE to do its job.
|
|
6619
|
-
/* @__PURE__ */
|
|
6620
|
-
) : reasoningOnly ? /* @__PURE__ */
|
|
7404
|
+
/* @__PURE__ */ React6.createElement(Text5, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
|
|
7405
|
+
) : reasoningOnly ? /* @__PURE__ */ React6.createElement(Text5, { color: "yellow", italic: true }, " R1 is thinking before it speaks \u2014 body text arrives when reasoning finishes (typically 20-90s, this is normal)") : toolCallOnly ? /* @__PURE__ */ React6.createElement(Text5, { color: "magenta", italic: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React6.createElement(Text5, { color: "yellow", italic: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking finishes") : null);
|
|
6621
7406
|
}
|
|
6622
7407
|
function Pulse() {
|
|
6623
7408
|
const tick = useTick();
|
|
6624
7409
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
6625
|
-
return /* @__PURE__ */
|
|
7410
|
+
return /* @__PURE__ */ React6.createElement(Text5, { color: "cyan" }, frames[Math.floor(tick / 4) % frames.length]);
|
|
6626
7411
|
}
|
|
6627
7412
|
function formatToolCallIndex(tb) {
|
|
6628
7413
|
if (!tb || tb.index === void 0) return "";
|
|
@@ -6641,7 +7426,7 @@ function lastLine(s, maxChars) {
|
|
|
6641
7426
|
}
|
|
6642
7427
|
function StatsLine({ stats }) {
|
|
6643
7428
|
const hit = (stats.cacheHitRatio * 100).toFixed(1);
|
|
6644
|
-
return /* @__PURE__ */
|
|
7429
|
+
return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u258F "), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, " \u2192 ", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6)));
|
|
6645
7430
|
}
|
|
6646
7431
|
function truncate2(s, max) {
|
|
6647
7432
|
if (s.length <= max) return s;
|
|
@@ -6664,12 +7449,12 @@ ${s.slice(-max)}`;
|
|
|
6664
7449
|
}
|
|
6665
7450
|
|
|
6666
7451
|
// src/cli/ui/PlanConfirm.tsx
|
|
6667
|
-
import { Box as
|
|
6668
|
-
import
|
|
7452
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
7453
|
+
import React8 from "react";
|
|
6669
7454
|
|
|
6670
7455
|
// src/cli/ui/Select.tsx
|
|
6671
|
-
import { Box as
|
|
6672
|
-
import
|
|
7456
|
+
import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
|
|
7457
|
+
import React7, { useState as useState3 } from "react";
|
|
6673
7458
|
function SingleSelect({
|
|
6674
7459
|
items,
|
|
6675
7460
|
initialValue,
|
|
@@ -6681,8 +7466,8 @@ function SingleSelect({
|
|
|
6681
7466
|
0,
|
|
6682
7467
|
items.findIndex((i) => i.value === initialValue && !i.disabled)
|
|
6683
7468
|
);
|
|
6684
|
-
const [index, setIndex] =
|
|
6685
|
-
|
|
7469
|
+
const [index, setIndex] = useState3(initialIndex === -1 ? 0 : initialIndex);
|
|
7470
|
+
useInput2((_input, key) => {
|
|
6686
7471
|
if (key.upArrow) {
|
|
6687
7472
|
setIndex((i) => findNextEnabled(items, i, -1));
|
|
6688
7473
|
} else if (key.downArrow) {
|
|
@@ -6694,7 +7479,7 @@ function SingleSelect({
|
|
|
6694
7479
|
onCancel();
|
|
6695
7480
|
}
|
|
6696
7481
|
});
|
|
6697
|
-
return /* @__PURE__ */
|
|
7482
|
+
return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React7.createElement(
|
|
6698
7483
|
SelectRow,
|
|
6699
7484
|
{
|
|
6700
7485
|
key: item.value,
|
|
@@ -6702,7 +7487,7 @@ function SingleSelect({
|
|
|
6702
7487
|
active: i === index,
|
|
6703
7488
|
marker: i === index ? "\u25B8" : " "
|
|
6704
7489
|
}
|
|
6705
|
-
)), footer ? /* @__PURE__ */
|
|
7490
|
+
)), footer ? /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, footer)) : null);
|
|
6706
7491
|
}
|
|
6707
7492
|
function MultiSelect({
|
|
6708
7493
|
items,
|
|
@@ -6711,12 +7496,12 @@ function MultiSelect({
|
|
|
6711
7496
|
onCancel,
|
|
6712
7497
|
footer
|
|
6713
7498
|
}) {
|
|
6714
|
-
const [index, setIndex] =
|
|
7499
|
+
const [index, setIndex] = useState3(() => {
|
|
6715
7500
|
const first = items.findIndex((i) => !i.disabled);
|
|
6716
7501
|
return first === -1 ? 0 : first;
|
|
6717
7502
|
});
|
|
6718
|
-
const [selected, setSelected] =
|
|
6719
|
-
|
|
7503
|
+
const [selected, setSelected] = useState3(new Set(initialSelected));
|
|
7504
|
+
useInput2((input, key) => {
|
|
6720
7505
|
if (key.upArrow) {
|
|
6721
7506
|
setIndex((i) => findNextEnabled(items, i, -1));
|
|
6722
7507
|
} else if (key.downArrow) {
|
|
@@ -6737,10 +7522,10 @@ function MultiSelect({
|
|
|
6737
7522
|
onCancel();
|
|
6738
7523
|
}
|
|
6739
7524
|
});
|
|
6740
|
-
return /* @__PURE__ */
|
|
7525
|
+
return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column" }, items.map((item, i) => {
|
|
6741
7526
|
const checked = selected.has(item.value);
|
|
6742
7527
|
const marker = checked ? "[x]" : "[ ]";
|
|
6743
|
-
return /* @__PURE__ */
|
|
7528
|
+
return /* @__PURE__ */ React7.createElement(
|
|
6744
7529
|
SelectRow,
|
|
6745
7530
|
{
|
|
6746
7531
|
key: item.value,
|
|
@@ -6749,7 +7534,7 @@ function MultiSelect({
|
|
|
6749
7534
|
marker: `${i === index ? "\u25B8" : " "} ${marker}`
|
|
6750
7535
|
}
|
|
6751
7536
|
);
|
|
6752
|
-
}), footer ? /* @__PURE__ */
|
|
7537
|
+
}), footer ? /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, footer)) : null);
|
|
6753
7538
|
}
|
|
6754
7539
|
function SelectRow({
|
|
6755
7540
|
item,
|
|
@@ -6757,7 +7542,7 @@ function SelectRow({
|
|
|
6757
7542
|
marker
|
|
6758
7543
|
}) {
|
|
6759
7544
|
const color = item.disabled ? "gray" : active ? "cyan" : void 0;
|
|
6760
|
-
return /* @__PURE__ */
|
|
7545
|
+
return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React7.createElement(Box6, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, item.hint)) : null);
|
|
6761
7546
|
}
|
|
6762
7547
|
function findNextEnabled(items, from, step) {
|
|
6763
7548
|
if (items.length === 0) return 0;
|
|
@@ -6800,7 +7585,7 @@ function PlanConfirmInner({
|
|
|
6800
7585
|
const sourceLineBudget = Math.max(MIN_BODY_ROWS, Math.floor(renderedBudget / MARKDOWN_EXPANSION));
|
|
6801
7586
|
const visible = clampBodyByLines(charCapped, sourceLineBudget);
|
|
6802
7587
|
const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
|
|
6803
|
-
return /* @__PURE__ */
|
|
7588
|
+
return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { color: "cyan", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React8.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React8.createElement(Text7, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(
|
|
6804
7589
|
SingleSelect,
|
|
6805
7590
|
{
|
|
6806
7591
|
initialValue: hasOpenQuestions ? "refine" : "approve",
|
|
@@ -6827,14 +7612,14 @@ function PlanConfirmInner({
|
|
|
6827
7612
|
}
|
|
6828
7613
|
)));
|
|
6829
7614
|
}
|
|
6830
|
-
var PlanConfirm =
|
|
7615
|
+
var PlanConfirm = React8.memo(PlanConfirmInner);
|
|
6831
7616
|
|
|
6832
7617
|
// src/cli/ui/PlanRefineInput.tsx
|
|
6833
|
-
import { Box as
|
|
6834
|
-
import
|
|
7618
|
+
import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
|
|
7619
|
+
import React9, { useState as useState4 } from "react";
|
|
6835
7620
|
function PlanRefineInput({ mode, onSubmit, onCancel }) {
|
|
6836
|
-
const [value, setValue] =
|
|
6837
|
-
|
|
7621
|
+
const [value, setValue] = useState4("");
|
|
7622
|
+
useInput3((input, key) => {
|
|
6838
7623
|
if (key.escape) {
|
|
6839
7624
|
onCancel();
|
|
6840
7625
|
return;
|
|
@@ -6854,12 +7639,12 @@ function PlanRefineInput({ mode, onSubmit, onCancel }) {
|
|
|
6854
7639
|
const title = mode === "approve" ? "\u25B8 approving \u2014 any last instructions or answers to open questions?" : "\u25B8 refining \u2014 what should the model change?";
|
|
6855
7640
|
const hint = mode === "approve" ? "Answer questions the plan raised, add constraints, or just press Enter to approve as-is." : "Describe what's wrong or missing, or answer questions the plan raised.";
|
|
6856
7641
|
const blankHint = mode === "approve" ? " (Enter with blank = approve without extra instructions.)" : " (Enter with blank = ask the model to list concrete questions.)";
|
|
6857
|
-
return /* @__PURE__ */
|
|
7642
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "yellow" }, title)), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, hint, " Enter to send \xB7 Esc to return to the picker.", value === "" ? blankHint : "")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u203A "), /* @__PURE__ */ React9.createElement(Text8, null, value || " "), /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u258D"))));
|
|
6858
7643
|
}
|
|
6859
7644
|
|
|
6860
7645
|
// src/cli/ui/PromptInput.tsx
|
|
6861
|
-
import { Box as
|
|
6862
|
-
import
|
|
7646
|
+
import { Box as Box9, Text as Text9, useInput as useInput4 } from "ink";
|
|
7647
|
+
import React10, { useRef, useState as useState5 } from "react";
|
|
6863
7648
|
|
|
6864
7649
|
// src/cli/ui/multiline-keys.ts
|
|
6865
7650
|
var BACKSLASH_SUFFIX = /\\$/;
|
|
@@ -6974,7 +7759,7 @@ function PromptInput({
|
|
|
6974
7759
|
disabled,
|
|
6975
7760
|
placeholder
|
|
6976
7761
|
}) {
|
|
6977
|
-
const [cursor, setCursor] =
|
|
7762
|
+
const [cursor, setCursor] = useState5(value.length);
|
|
6978
7763
|
const lastLocalValueRef = useRef(value);
|
|
6979
7764
|
if (value !== lastLocalValueRef.current) {
|
|
6980
7765
|
lastLocalValueRef.current = value;
|
|
@@ -6984,7 +7769,7 @@ function PromptInput({
|
|
|
6984
7769
|
}
|
|
6985
7770
|
const tick = useTick();
|
|
6986
7771
|
const showCursor = disabled ? false : Math.floor(tick / 4) % 2 === 0;
|
|
6987
|
-
|
|
7772
|
+
useInput4(
|
|
6988
7773
|
(input, key) => {
|
|
6989
7774
|
const ke = {
|
|
6990
7775
|
input,
|
|
@@ -7019,13 +7804,13 @@ function PromptInput({
|
|
|
7019
7804
|
const lines = value.length > 0 ? value.split("\n") : [""];
|
|
7020
7805
|
const borderColor = disabled ? "gray" : "cyan";
|
|
7021
7806
|
const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
|
|
7022
|
-
return /* @__PURE__ */
|
|
7807
|
+
return /* @__PURE__ */ React10.createElement(Box9, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
|
|
7023
7808
|
const isFirst = i === 0;
|
|
7024
7809
|
const showPlaceholder = isFirst && value.length === 0;
|
|
7025
7810
|
const isCursorLine = i === cursorLine;
|
|
7026
7811
|
return (
|
|
7027
7812
|
// biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
|
|
7028
|
-
/* @__PURE__ */
|
|
7813
|
+
/* @__PURE__ */ React10.createElement(Box9, { key: i }, isFirst ? /* @__PURE__ */ React10.createElement(Text9, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " "), showPlaceholder ? /* @__PURE__ */ React10.createElement(React10.Fragment, null, isCursorLine && !disabled ? /* @__PURE__ */ React10.createElement(Text9, { color: borderColor }, showCursor ? "\u258C" : " ") : null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, effectivePlaceholder)) : isCursorLine && !disabled ? /* @__PURE__ */ React10.createElement(
|
|
7029
7814
|
LineWithCursor,
|
|
7030
7815
|
{
|
|
7031
7816
|
line,
|
|
@@ -7033,7 +7818,7 @@ function PromptInput({
|
|
|
7033
7818
|
showCursor,
|
|
7034
7819
|
borderColor
|
|
7035
7820
|
}
|
|
7036
|
-
) : /* @__PURE__ */
|
|
7821
|
+
) : /* @__PURE__ */ React10.createElement(Text9, null, line))
|
|
7037
7822
|
);
|
|
7038
7823
|
}));
|
|
7039
7824
|
}
|
|
@@ -7047,16 +7832,17 @@ function LineWithCursor({
|
|
|
7047
7832
|
const atCursor = line.slice(col, col + 1);
|
|
7048
7833
|
const after = line.slice(col + 1);
|
|
7049
7834
|
if (atCursor.length === 0) {
|
|
7050
|
-
return /* @__PURE__ */
|
|
7835
|
+
return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(Text9, null, before), /* @__PURE__ */ React10.createElement(Text9, { color: borderColor }, showCursor ? "\u258C" : " "));
|
|
7051
7836
|
}
|
|
7052
|
-
return /* @__PURE__ */
|
|
7837
|
+
return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(Text9, null, before), /* @__PURE__ */ React10.createElement(Text9, { inverse: showCursor }, atCursor), /* @__PURE__ */ React10.createElement(Text9, null, after));
|
|
7053
7838
|
}
|
|
7054
7839
|
|
|
7055
7840
|
// src/cli/ui/ShellConfirm.tsx
|
|
7056
|
-
import { Box as
|
|
7057
|
-
import
|
|
7058
|
-
function ShellConfirm({ command, allowPrefix, onChoose }) {
|
|
7059
|
-
|
|
7841
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
7842
|
+
import React11 from "react";
|
|
7843
|
+
function ShellConfirm({ command, allowPrefix, kind, onChoose }) {
|
|
7844
|
+
const isBackground = kind === "run_background";
|
|
7845
|
+
return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { bold: true, color: "yellow" }, isBackground ? "\u25B8 model wants to start a BACKGROUND process" : "\u25B8 model wants to run a shell command")), isBackground ? /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (long-running: dev server / watcher; keeps running after approval, /kill to stop)")) : null, /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { color: "yellow", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "$ "), /* @__PURE__ */ React11.createElement(Text10, { color: "cyan" }, command))), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(
|
|
7060
7846
|
SingleSelect,
|
|
7061
7847
|
{
|
|
7062
7848
|
initialValue: "run_once",
|
|
@@ -7113,8 +7899,8 @@ function derivePrefix(command) {
|
|
|
7113
7899
|
}
|
|
7114
7900
|
|
|
7115
7901
|
// src/cli/ui/SlashArgPicker.tsx
|
|
7116
|
-
import { Box as
|
|
7117
|
-
import
|
|
7902
|
+
import { Box as Box11, Text as Text11 } from "ink";
|
|
7903
|
+
import React12 from "react";
|
|
7118
7904
|
function SlashArgPicker({
|
|
7119
7905
|
matches,
|
|
7120
7906
|
selectedIndex,
|
|
@@ -7123,11 +7909,11 @@ function SlashArgPicker({
|
|
|
7123
7909
|
partial
|
|
7124
7910
|
}) {
|
|
7125
7911
|
if (kind === "hint") {
|
|
7126
|
-
return /* @__PURE__ */
|
|
7912
|
+
return /* @__PURE__ */ React12.createElement(Box11, { paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " ", /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary));
|
|
7127
7913
|
}
|
|
7128
7914
|
if (matches === null) return null;
|
|
7129
7915
|
if (matches.length === 0) {
|
|
7130
|
-
return /* @__PURE__ */
|
|
7916
|
+
return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " ", /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), /* @__PURE__ */ React12.createElement(Text11, { color: "yellow" }, ' no match for "', partial, '" \u2014 keep typing, or Backspace to edit'));
|
|
7131
7917
|
}
|
|
7132
7918
|
const MAX = 8;
|
|
7133
7919
|
const total = matches.length;
|
|
@@ -7135,26 +7921,26 @@ function SlashArgPicker({
|
|
|
7135
7921
|
const shown = matches.slice(windowStart, windowStart + MAX);
|
|
7136
7922
|
const hiddenAbove = windowStart;
|
|
7137
7923
|
const hiddenBelow = total - windowStart - shown.length;
|
|
7138
|
-
return /* @__PURE__ */
|
|
7924
|
+
return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " ", /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), hiddenAbove > 0 ? /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((value, i) => /* @__PURE__ */ React12.createElement(ArgRow, { key: value, value, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
|
|
7139
7925
|
}
|
|
7140
7926
|
function ArgRow({ value, isSelected }) {
|
|
7141
7927
|
const marker = isSelected ? "\u25B8" : " ";
|
|
7142
7928
|
if (isSelected) {
|
|
7143
|
-
return /* @__PURE__ */
|
|
7929
|
+
return /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { bold: true, color: "cyan" }, marker, " ", value));
|
|
7144
7930
|
}
|
|
7145
|
-
return /* @__PURE__ */
|
|
7931
|
+
return /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, marker, " ", value));
|
|
7146
7932
|
}
|
|
7147
7933
|
|
|
7148
7934
|
// src/cli/ui/SlashSuggestions.tsx
|
|
7149
|
-
import { Box as
|
|
7150
|
-
import
|
|
7935
|
+
import { Box as Box12, Text as Text12 } from "ink";
|
|
7936
|
+
import React13 from "react";
|
|
7151
7937
|
function SlashSuggestions({
|
|
7152
7938
|
matches,
|
|
7153
7939
|
selectedIndex
|
|
7154
7940
|
}) {
|
|
7155
7941
|
if (matches === null) return null;
|
|
7156
7942
|
if (matches.length === 0) {
|
|
7157
|
-
return /* @__PURE__ */
|
|
7943
|
+
return /* @__PURE__ */ React13.createElement(Box12, { paddingX: 1 }, /* @__PURE__ */ React13.createElement(Text12, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
|
|
7158
7944
|
}
|
|
7159
7945
|
const MAX = 8;
|
|
7160
7946
|
const total = matches.length;
|
|
@@ -7162,21 +7948,21 @@ function SlashSuggestions({
|
|
|
7162
7948
|
const shown = matches.slice(windowStart, windowStart + MAX);
|
|
7163
7949
|
const hiddenAbove = windowStart;
|
|
7164
7950
|
const hiddenBelow = total - windowStart - shown.length;
|
|
7165
|
-
return /* @__PURE__ */
|
|
7951
|
+
return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React13.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
|
|
7166
7952
|
}
|
|
7167
7953
|
function SuggestionRow({ spec, isSelected }) {
|
|
7168
7954
|
const marker = isSelected ? "\u25B8" : " ";
|
|
7169
7955
|
const name = `/${spec.cmd}`;
|
|
7170
7956
|
const argsSuffix = spec.argsHint ? ` ${spec.argsHint}` : "";
|
|
7171
7957
|
if (isSelected) {
|
|
7172
|
-
return /* @__PURE__ */
|
|
7958
|
+
return /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Text12, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React13.createElement(Text12, { color: "cyan" }, " ", spec.summary));
|
|
7173
7959
|
}
|
|
7174
|
-
return /* @__PURE__ */
|
|
7960
|
+
return /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
|
|
7175
7961
|
}
|
|
7176
7962
|
|
|
7177
7963
|
// src/cli/ui/StatsPanel.tsx
|
|
7178
|
-
import { Box as
|
|
7179
|
-
import
|
|
7964
|
+
import { Box as Box13, Text as Text13, useStdout as useStdout3 } from "ink";
|
|
7965
|
+
import React14 from "react";
|
|
7180
7966
|
var WORDMARK_STYLES = [
|
|
7181
7967
|
{ ch: "\u25C8", color: "#5eead4", isLogo: true },
|
|
7182
7968
|
// teal — brand mark
|
|
@@ -7202,7 +7988,7 @@ function Wordmark({ busy }) {
|
|
|
7202
7988
|
const tick = useTick();
|
|
7203
7989
|
const period = busy ? 5 : 12;
|
|
7204
7990
|
const bright = Math.floor(tick / period) % 2 === 0;
|
|
7205
|
-
return /* @__PURE__ */
|
|
7991
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, WORDMARK_STYLES.map((c) => /* @__PURE__ */ React14.createElement(Text13, { key: `${c.ch}-${c.color}`, color: c.color, bold: c.isLogo ? bright : true }, c.ch)));
|
|
7206
7992
|
}
|
|
7207
7993
|
var NARROW_BREAKPOINT = 120;
|
|
7208
7994
|
var COLD_START_TURNS = 3;
|
|
@@ -7214,6 +8000,7 @@ function StatsPanel({
|
|
|
7214
8000
|
branchBudget,
|
|
7215
8001
|
reasoningEffort,
|
|
7216
8002
|
planMode,
|
|
8003
|
+
editMode,
|
|
7217
8004
|
balance,
|
|
7218
8005
|
updateAvailable,
|
|
7219
8006
|
busy
|
|
@@ -7221,11 +8008,11 @@ function StatsPanel({
|
|
|
7221
8008
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
7222
8009
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
7223
8010
|
const ctxRatio = summary.lastPromptTokens / ctxMax;
|
|
7224
|
-
const { stdout: stdout2 } =
|
|
8011
|
+
const { stdout: stdout2 } = useStdout3();
|
|
7225
8012
|
const columns = stdout2?.columns ?? 80;
|
|
7226
8013
|
const narrow = columns < NARROW_BREAKPOINT;
|
|
7227
8014
|
const coldStart = summary.turns <= COLD_START_TURNS;
|
|
7228
|
-
return /* @__PURE__ */
|
|
8015
|
+
return /* @__PURE__ */ React14.createElement(Box13, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React14.createElement(
|
|
7229
8016
|
Header,
|
|
7230
8017
|
{
|
|
7231
8018
|
model,
|
|
@@ -7235,12 +8022,13 @@ function StatsPanel({
|
|
|
7235
8022
|
branchBudget: branchBudget ?? 1,
|
|
7236
8023
|
reasoningEffort,
|
|
7237
8024
|
planMode,
|
|
8025
|
+
editMode,
|
|
7238
8026
|
turns: summary.turns,
|
|
7239
8027
|
updateAvailable,
|
|
7240
8028
|
narrow,
|
|
7241
8029
|
busy: busy ?? false
|
|
7242
8030
|
}
|
|
7243
|
-
), narrow ? /* @__PURE__ */
|
|
8031
|
+
), narrow ? /* @__PURE__ */ React14.createElement(
|
|
7244
8032
|
StackedMetrics,
|
|
7245
8033
|
{
|
|
7246
8034
|
summary,
|
|
@@ -7249,7 +8037,7 @@ function StatsPanel({
|
|
|
7249
8037
|
balance,
|
|
7250
8038
|
coldStart
|
|
7251
8039
|
}
|
|
7252
|
-
) : /* @__PURE__ */
|
|
8040
|
+
) : /* @__PURE__ */ React14.createElement(
|
|
7253
8041
|
InlineMetrics,
|
|
7254
8042
|
{
|
|
7255
8043
|
summary,
|
|
@@ -7268,12 +8056,13 @@ function Header({
|
|
|
7268
8056
|
branchBudget,
|
|
7269
8057
|
reasoningEffort,
|
|
7270
8058
|
planMode,
|
|
8059
|
+
editMode,
|
|
7271
8060
|
turns,
|
|
7272
8061
|
updateAvailable,
|
|
7273
8062
|
narrow,
|
|
7274
8063
|
busy
|
|
7275
8064
|
}) {
|
|
7276
|
-
return /* @__PURE__ */
|
|
8065
|
+
return /* @__PURE__ */ React14.createElement(Box13, { justifyContent: "space-between" }, /* @__PURE__ */ React14.createElement(Box13, null, /* @__PURE__ */ React14.createElement(Wordmark, { busy }), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React14.createElement(Text13, { color: "yellow" }, model), narrow ? null : /* @__PURE__ */ React14.createElement(React14.Fragment, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, prefixHash)), harvestOn ? /* @__PURE__ */ React14.createElement(Text13, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React14.createElement(Text13, { color: "blue" }, " \xB7 branch", branchBudget) : null, reasoningEffort === "max" ? /* @__PURE__ */ React14.createElement(Text13, { color: "green" }, " \xB7 max") : null, reasoningEffort === "high" ? /* @__PURE__ */ React14.createElement(Text13, { color: "yellow" }, " \xB7 high") : null, planMode ? /* @__PURE__ */ React14.createElement(Text13, { color: "red", bold: true }, " \xB7 PLAN") : null, editMode ? /* @__PURE__ */ React14.createElement(Text13, { color: editMode === "auto" ? "magenta" : "cyan", bold: true }, editMode === "auto" ? " \xB7 AUTO" : " \xB7 review") : null), /* @__PURE__ */ React14.createElement(Text13, null, updateAvailable ? /* @__PURE__ */ React14.createElement(Text13, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, narrow ? `turn ${turns}` : `turn ${turns} \xB7 /help`)));
|
|
7277
8066
|
}
|
|
7278
8067
|
function InlineMetrics({
|
|
7279
8068
|
summary,
|
|
@@ -7282,7 +8071,7 @@ function InlineMetrics({
|
|
|
7282
8071
|
balance,
|
|
7283
8072
|
coldStart
|
|
7284
8073
|
}) {
|
|
7285
|
-
return /* @__PURE__ */
|
|
8074
|
+
return /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React14.createElement(ContextCell, { ratio: ctxRatio, promptTokens: summary.lastPromptTokens, ctxMax }), /* @__PURE__ */ React14.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React14.createElement(CostCell, { summary, coldStart }), balance ? /* @__PURE__ */ React14.createElement(BalanceCell, { balance }) : null);
|
|
7286
8075
|
}
|
|
7287
8076
|
function StackedMetrics({
|
|
7288
8077
|
summary,
|
|
@@ -7291,7 +8080,7 @@ function StackedMetrics({
|
|
|
7291
8080
|
balance,
|
|
7292
8081
|
coldStart
|
|
7293
8082
|
}) {
|
|
7294
|
-
return /* @__PURE__ */
|
|
8083
|
+
return /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React14.createElement(
|
|
7295
8084
|
ContextCell,
|
|
7296
8085
|
{
|
|
7297
8086
|
ratio: ctxRatio,
|
|
@@ -7299,7 +8088,7 @@ function StackedMetrics({
|
|
|
7299
8088
|
ctxMax,
|
|
7300
8089
|
showBar: true
|
|
7301
8090
|
}
|
|
7302
|
-
), balance ? /* @__PURE__ */
|
|
8091
|
+
), balance ? /* @__PURE__ */ React14.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React14.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React14.createElement(CostCell, { summary, coldStart }));
|
|
7303
8092
|
}
|
|
7304
8093
|
function ContextCell({
|
|
7305
8094
|
ratio,
|
|
@@ -7308,11 +8097,11 @@ function ContextCell({
|
|
|
7308
8097
|
showBar
|
|
7309
8098
|
}) {
|
|
7310
8099
|
if (promptTokens === 0) {
|
|
7311
|
-
return /* @__PURE__ */
|
|
8100
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "ctx "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "\u2014 (no turns yet)"));
|
|
7312
8101
|
}
|
|
7313
8102
|
const color = ratio >= 0.8 ? "red" : ratio >= 0.6 ? "yellow" : "green";
|
|
7314
8103
|
const pct2 = Math.round(ratio * 100);
|
|
7315
|
-
return /* @__PURE__ */
|
|
8104
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "ctx "), showBar ? /* @__PURE__ */ React14.createElement(Bar, { ratio, color }) : null, showBar ? /* @__PURE__ */ React14.createElement(Text13, null, " ") : null, /* @__PURE__ */ React14.createElement(Text13, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React14.createElement(Text13, { color: "red", bold: true }, " \xB7 /compact") : null);
|
|
7316
8105
|
}
|
|
7317
8106
|
function CacheCell({
|
|
7318
8107
|
hitRatio,
|
|
@@ -7321,33 +8110,33 @@ function CacheCell({
|
|
|
7321
8110
|
}) {
|
|
7322
8111
|
const pct2 = (hitRatio * 100).toFixed(1);
|
|
7323
8112
|
if (turns === 0) {
|
|
7324
|
-
return /* @__PURE__ */
|
|
8113
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cache "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "\u2014"));
|
|
7325
8114
|
}
|
|
7326
8115
|
if (coldStart) {
|
|
7327
|
-
return /* @__PURE__ */
|
|
8116
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cache "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true, italic: true }, "(cold start)"));
|
|
7328
8117
|
}
|
|
7329
8118
|
const color = hitRatio >= 0.7 ? "green" : hitRatio >= 0.4 ? "yellow" : "red";
|
|
7330
|
-
return /* @__PURE__ */
|
|
8119
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cache "), /* @__PURE__ */ React14.createElement(Text13, { color, bold: true }, pct2, "%"));
|
|
7331
8120
|
}
|
|
7332
8121
|
function CostCell({
|
|
7333
8122
|
summary,
|
|
7334
8123
|
coldStart
|
|
7335
8124
|
}) {
|
|
7336
8125
|
if (summary.turns === 0) {
|
|
7337
|
-
return /* @__PURE__ */
|
|
8126
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cost "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "\u2014"));
|
|
7338
8127
|
}
|
|
7339
8128
|
const primaryColor = coldStart ? void 0 : "green";
|
|
7340
|
-
return /* @__PURE__ */
|
|
8129
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cost "), /* @__PURE__ */ React14.createElement(Text13, { color: primaryColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")"));
|
|
7341
8130
|
}
|
|
7342
8131
|
function BalanceCell({ balance }) {
|
|
7343
8132
|
const color = balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green";
|
|
7344
|
-
return /* @__PURE__ */
|
|
8133
|
+
return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "balance "), /* @__PURE__ */ React14.createElement(Text13, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
|
|
7345
8134
|
}
|
|
7346
8135
|
function Bar({ ratio, color }) {
|
|
7347
8136
|
const cells = 10;
|
|
7348
8137
|
const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
|
|
7349
8138
|
const bar = "\u2588".repeat(filled) + "\u2591".repeat(cells - filled);
|
|
7350
|
-
return /* @__PURE__ */
|
|
8139
|
+
return /* @__PURE__ */ React14.createElement(Text13, { color }, bar);
|
|
7351
8140
|
}
|
|
7352
8141
|
function formatTokens(n) {
|
|
7353
8142
|
if (n < 1024) return String(n);
|
|
@@ -7783,7 +8572,7 @@ var SLASH_COMMANDS = [
|
|
|
7783
8572
|
{
|
|
7784
8573
|
cmd: "compact",
|
|
7785
8574
|
argsHint: "[tokens]",
|
|
7786
|
-
summary: "shrink oversized tool results in the log
|
|
8575
|
+
summary: "shrink oversized tool results AND tool-call args (edit_file search/replace) in the log; cap in tokens, default 4000"
|
|
7787
8576
|
},
|
|
7788
8577
|
{ cmd: "keys", summary: "show all keyboard shortcuts and prompt prefixes" },
|
|
7789
8578
|
{ cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
|
|
@@ -7796,6 +8585,17 @@ var SLASH_COMMANDS = [
|
|
|
7796
8585
|
{ cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
|
|
7797
8586
|
{ cmd: "discard", summary: "drop pending edit blocks without writing", contextual: "code" },
|
|
7798
8587
|
{ cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
|
|
8588
|
+
{
|
|
8589
|
+
cmd: "history",
|
|
8590
|
+
summary: "list every edit batch this session (ids for /show, undone markers)",
|
|
8591
|
+
contextual: "code"
|
|
8592
|
+
},
|
|
8593
|
+
{
|
|
8594
|
+
cmd: "show",
|
|
8595
|
+
argsHint: "[id]",
|
|
8596
|
+
summary: "dump a stored edit diff (omit id for newest non-undone)",
|
|
8597
|
+
contextual: "code"
|
|
8598
|
+
},
|
|
7799
8599
|
{
|
|
7800
8600
|
cmd: "commit",
|
|
7801
8601
|
argsHint: '"msg"',
|
|
@@ -7813,6 +8613,26 @@ var SLASH_COMMANDS = [
|
|
|
7813
8613
|
cmd: "apply-plan",
|
|
7814
8614
|
summary: "force-approve a pending / in-text plan (fallback if picker was missed)",
|
|
7815
8615
|
contextual: "code"
|
|
8616
|
+
},
|
|
8617
|
+
{
|
|
8618
|
+
cmd: "mode",
|
|
8619
|
+
argsHint: "[review|auto]",
|
|
8620
|
+
summary: "edit-gate: review (queue for /apply) or auto (apply+undo banner). Shift+Tab cycles.",
|
|
8621
|
+
contextual: "code",
|
|
8622
|
+
argCompleter: ["review", "auto"]
|
|
8623
|
+
},
|
|
8624
|
+
{ cmd: "jobs", summary: "list background jobs started by run_background", contextual: "code" },
|
|
8625
|
+
{
|
|
8626
|
+
cmd: "kill",
|
|
8627
|
+
argsHint: "<id>",
|
|
8628
|
+
summary: "stop a background job by id (SIGTERM \u2192 SIGKILL after grace)",
|
|
8629
|
+
contextual: "code"
|
|
8630
|
+
},
|
|
8631
|
+
{
|
|
8632
|
+
cmd: "logs",
|
|
8633
|
+
argsHint: "<id> [lines]",
|
|
8634
|
+
summary: "tail a background job's output (default last 80 lines)",
|
|
8635
|
+
contextual: "code"
|
|
7816
8636
|
}
|
|
7817
8637
|
];
|
|
7818
8638
|
function suggestSlashCommands(prefix, codeMode = false) {
|
|
@@ -7881,6 +8701,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
7881
8701
|
" Backspace delete left; Delete delete under cursor",
|
|
7882
8702
|
" Esc abort the in-flight turn",
|
|
7883
8703
|
" y / n accept / reject pending edits (code mode)",
|
|
8704
|
+
" Shift+Tab cycle edit gate: review \u2194 AUTO (code mode, persists to config)",
|
|
8705
|
+
" u undo the latest non-undone edit batch (session-wide, not just banner)",
|
|
7884
8706
|
"",
|
|
7885
8707
|
"Prompt prefixes:",
|
|
7886
8708
|
" /<name> slash command; Tab/Enter picks from the suggestion list",
|
|
@@ -7929,10 +8751,16 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
7929
8751
|
" /retry truncate & resend your last message (fresh sample from the model)",
|
|
7930
8752
|
" /apply (code mode) commit the pending edit blocks to disk",
|
|
7931
8753
|
" /discard (code mode) drop pending edits without writing",
|
|
7932
|
-
" /undo (code mode) roll back the
|
|
8754
|
+
" /undo (code mode) roll back the latest non-undone edit batch",
|
|
8755
|
+
" /history (code mode) list every edit batch this session",
|
|
8756
|
+
" /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
|
|
7933
8757
|
' /commit "msg" (code mode) git add -A && git commit -m "msg"',
|
|
7934
8758
|
" /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
|
|
7935
8759
|
" /apply-plan (code mode) force-approve pending/in-text plan (fallback)",
|
|
8760
|
+
" /mode [review|auto] (code mode) edit-gate: queue edits for /apply or apply instantly (Shift+Tab cycles, u undoes within 5s)",
|
|
8761
|
+
" /jobs (code mode) list background processes (run_background) \u2014 running and exited",
|
|
8762
|
+
" /kill <id> (code mode) stop a background job by id (SIGTERM \u2192 SIGKILL)",
|
|
8763
|
+
" /logs <id> [lines] (code mode) tail a background job's output (default 80 lines)",
|
|
7936
8764
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
7937
8765
|
" /forget delete the current session from disk",
|
|
7938
8766
|
" /new start fresh: drop all context + clear scrollback",
|
|
@@ -8097,7 +8925,19 @@ ${entry.text}`
|
|
|
8097
8925
|
info: "/undo is only available inside `reasonix code` \u2014 chat mode doesn't apply edits."
|
|
8098
8926
|
};
|
|
8099
8927
|
}
|
|
8100
|
-
return { info: ctx.codeUndo() };
|
|
8928
|
+
return { info: ctx.codeUndo(args) };
|
|
8929
|
+
}
|
|
8930
|
+
case "history": {
|
|
8931
|
+
if (!ctx.codeHistory) {
|
|
8932
|
+
return { info: "/history is only available inside `reasonix code`." };
|
|
8933
|
+
}
|
|
8934
|
+
return { info: ctx.codeHistory() };
|
|
8935
|
+
}
|
|
8936
|
+
case "show": {
|
|
8937
|
+
if (!ctx.codeShowEdit) {
|
|
8938
|
+
return { info: "/show is only available inside `reasonix code`." };
|
|
8939
|
+
}
|
|
8940
|
+
return { info: ctx.codeShowEdit(args) };
|
|
8101
8941
|
}
|
|
8102
8942
|
case "apply": {
|
|
8103
8943
|
if (!ctx.codeApply) {
|
|
@@ -8137,6 +8977,81 @@ ${entry.text}`
|
|
|
8137
8977
|
info: "\u25B8 plan mode OFF \u2014 write tools are live again. Model can still propose plans autonomously for large tasks."
|
|
8138
8978
|
};
|
|
8139
8979
|
}
|
|
8980
|
+
case "jobs": {
|
|
8981
|
+
if (!ctx.jobs) {
|
|
8982
|
+
return { info: "/jobs is only available inside `reasonix code`." };
|
|
8983
|
+
}
|
|
8984
|
+
const rows = ctx.jobs.list();
|
|
8985
|
+
if (rows.length === 0) {
|
|
8986
|
+
return { info: "no background jobs yet \u2014 use run_background to start one" };
|
|
8987
|
+
}
|
|
8988
|
+
const lines = ["Background jobs:"];
|
|
8989
|
+
for (const r of rows) {
|
|
8990
|
+
const age = ((Date.now() - r.startedAt) / 1e3).toFixed(1);
|
|
8991
|
+
const state = r.running ? `running \xB7 pid ${r.pid ?? "?"}` : r.exitCode !== null ? `exit ${r.exitCode}` : r.spawnError ? "failed" : "stopped";
|
|
8992
|
+
lines.push(
|
|
8993
|
+
` ${String(r.id).padStart(3)} ${state.padEnd(20)} ${age.padStart(6)}s ago $ ${r.command}`
|
|
8994
|
+
);
|
|
8995
|
+
}
|
|
8996
|
+
lines.push("");
|
|
8997
|
+
lines.push("/kill <id> to stop one \xB7 /logs <id> [lines] to tail output");
|
|
8998
|
+
return { info: lines.join("\n") };
|
|
8999
|
+
}
|
|
9000
|
+
case "kill": {
|
|
9001
|
+
if (!ctx.jobs) return { info: "/kill is only available inside `reasonix code`." };
|
|
9002
|
+
const id = Number.parseInt(args[0] ?? "", 10);
|
|
9003
|
+
if (!Number.isFinite(id)) return { info: "usage: /kill <id> (see /jobs for ids)" };
|
|
9004
|
+
const rec = ctx.jobs.list().find((r) => r.id === id);
|
|
9005
|
+
if (!rec) return { info: `job ${id}: not found` };
|
|
9006
|
+
if (!rec.running) return { info: `job ${id} already exited (${rec.exitCode ?? "?"})` };
|
|
9007
|
+
const jobsRef = ctx.jobs;
|
|
9008
|
+
void (async () => {
|
|
9009
|
+
const final = await jobsRef.stop(id);
|
|
9010
|
+
if (!final) return;
|
|
9011
|
+
const status = final.running ? "still alive after SIGKILL (!) \u2014 report this as a bug" : final.exitCode !== null ? `exit ${final.exitCode}` : "stopped";
|
|
9012
|
+
ctx.postInfo?.(`\u25B8 job ${id} ${status}`);
|
|
9013
|
+
})();
|
|
9014
|
+
return {
|
|
9015
|
+
info: `\u25B8 stopping job ${id} (tree kill: SIGTERM \u2192 SIGKILL after 2s grace; Windows: taskkill /T /F)`
|
|
9016
|
+
};
|
|
9017
|
+
}
|
|
9018
|
+
case "logs": {
|
|
9019
|
+
if (!ctx.jobs) return { info: "/logs is only available inside `reasonix code`." };
|
|
9020
|
+
const id = Number.parseInt(args[0] ?? "", 10);
|
|
9021
|
+
if (!Number.isFinite(id)) {
|
|
9022
|
+
return { info: "usage: /logs <id> [lines] (default last 80 lines)" };
|
|
9023
|
+
}
|
|
9024
|
+
const requested = Number.parseInt(args[1] ?? "", 10);
|
|
9025
|
+
const tail = Number.isFinite(requested) && requested > 0 ? requested : 80;
|
|
9026
|
+
const out = ctx.jobs.read(id, { tailLines: tail });
|
|
9027
|
+
if (!out) return { info: `job ${id}: not found` };
|
|
9028
|
+
const status = out.running ? `running \xB7 pid ${out.pid ?? "?"}` : out.exitCode !== null ? `exited ${out.exitCode}` : out.spawnError ? `failed (${out.spawnError})` : "stopped";
|
|
9029
|
+
const header2 = `[job ${id} \xB7 ${status}]
|
|
9030
|
+
$ ${out.command}`;
|
|
9031
|
+
return { info: out.output ? `${header2}
|
|
9032
|
+
${out.output}` : header2 };
|
|
9033
|
+
}
|
|
9034
|
+
case "mode": {
|
|
9035
|
+
if (!ctx.setEditMode) {
|
|
9036
|
+
return {
|
|
9037
|
+
info: "/mode is only available inside `reasonix code`."
|
|
9038
|
+
};
|
|
9039
|
+
}
|
|
9040
|
+
const raw = (args[0] ?? "").toLowerCase();
|
|
9041
|
+
const current = ctx.editMode ?? "review";
|
|
9042
|
+
let target;
|
|
9043
|
+
if (raw === "review") target = "review";
|
|
9044
|
+
else if (raw === "auto") target = "auto";
|
|
9045
|
+
else if (raw === "") {
|
|
9046
|
+
target = current === "auto" ? "review" : "auto";
|
|
9047
|
+
} else {
|
|
9048
|
+
return { info: "usage: /mode <review|auto> (Shift+Tab also cycles)" };
|
|
9049
|
+
}
|
|
9050
|
+
ctx.setEditMode(target);
|
|
9051
|
+
return {
|
|
9052
|
+
info: target === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo, or /undo later" : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)"
|
|
9053
|
+
};
|
|
9054
|
+
}
|
|
8140
9055
|
case "apply-plan":
|
|
8141
9056
|
case "applyplan": {
|
|
8142
9057
|
if (!ctx.setPlanMode) {
|
|
@@ -8172,11 +9087,11 @@ ${entry.text}`
|
|
|
8172
9087
|
const { healedCount, tokensSaved, charsSaved } = loop.compact(cap);
|
|
8173
9088
|
if (healedCount === 0) {
|
|
8174
9089
|
return {
|
|
8175
|
-
info: `\u25B8 nothing to compact \u2014 no tool result in history
|
|
9090
|
+
info: `\u25B8 nothing to compact \u2014 no tool result or tool-call args in history exceed ${cap.toLocaleString()} tokens.`
|
|
8176
9091
|
};
|
|
8177
9092
|
}
|
|
8178
9093
|
return {
|
|
8179
|
-
info: `\u25B8 compacted ${healedCount}
|
|
9094
|
+
info: `\u25B8 compacted ${healedCount} payload(s) to ${cap.toLocaleString()} tokens each (tool results + tool-call args), saved ${tokensSaved.toLocaleString()} tokens (${charsSaved.toLocaleString()} chars). Session file rewritten.`
|
|
8180
9095
|
};
|
|
8181
9096
|
}
|
|
8182
9097
|
case "sessions": {
|
|
@@ -8282,6 +9197,7 @@ ${entry.text}`
|
|
|
8282
9197
|
const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
|
|
8283
9198
|
const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
|
|
8284
9199
|
const planLine = ctx.planMode ? " plan ON \u2014 writes gated (submit_plan + approval)" : "";
|
|
9200
|
+
const modeLine = ctx.editMode === "auto" ? " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)" : ctx.editMode === "review" ? " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)" : "";
|
|
8285
9201
|
const lines = [
|
|
8286
9202
|
` model ${loop.model}`,
|
|
8287
9203
|
` flags harvest=${loop.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop.stream ? "on" : "off"} \xB7 effort=${loop.reasoningEffort}`,
|
|
@@ -8291,6 +9207,7 @@ ${entry.text}`
|
|
|
8291
9207
|
];
|
|
8292
9208
|
if (pendingLine) lines.push(pendingLine);
|
|
8293
9209
|
if (planLine) lines.push(planLine);
|
|
9210
|
+
if (modeLine) lines.push(modeLine);
|
|
8294
9211
|
return { info: lines.join("\n") };
|
|
8295
9212
|
}
|
|
8296
9213
|
case "model": {
|
|
@@ -8377,14 +9294,18 @@ ${entry.text}`
|
|
|
8377
9294
|
const raw = (args[0] ?? "").toLowerCase();
|
|
8378
9295
|
if (raw === "") {
|
|
8379
9296
|
return {
|
|
8380
|
-
info: `reasoning_effort \u2192 ${loop.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default)`
|
|
9297
|
+
info: `reasoning_effort \u2192 ${loop.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)`
|
|
8381
9298
|
};
|
|
8382
9299
|
}
|
|
8383
9300
|
if (raw !== "high" && raw !== "max") {
|
|
8384
9301
|
return { info: "usage: /effort <high|max>" };
|
|
8385
9302
|
}
|
|
8386
9303
|
loop.configure({ reasoningEffort: raw });
|
|
8387
|
-
|
|
9304
|
+
try {
|
|
9305
|
+
saveReasoningEffort(raw);
|
|
9306
|
+
} catch {
|
|
9307
|
+
}
|
|
9308
|
+
return { info: `reasoning_effort \u2192 ${raw} (persisted)` };
|
|
8388
9309
|
}
|
|
8389
9310
|
default:
|
|
8390
9311
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
@@ -8796,6 +9717,14 @@ function gitTail(res) {
|
|
|
8796
9717
|
|
|
8797
9718
|
// src/cli/ui/App.tsx
|
|
8798
9719
|
var FLUSH_INTERVAL_MS = 100;
|
|
9720
|
+
function isEntryFullyUndone(e) {
|
|
9721
|
+
return e.snapshots.length > 0 && e.snapshots.every((s) => e.undoneFiles.has(s.path));
|
|
9722
|
+
}
|
|
9723
|
+
function entryStatus(e) {
|
|
9724
|
+
if (e.undoneFiles.size === 0) return "applied";
|
|
9725
|
+
if (isEntryFullyUndone(e)) return "UNDONE";
|
|
9726
|
+
return "PARTIAL";
|
|
9727
|
+
}
|
|
8799
9728
|
var PLAIN_UI = process.env.REASONIX_UI === "plain";
|
|
8800
9729
|
function App({
|
|
8801
9730
|
model,
|
|
@@ -8811,36 +9740,66 @@ function App({
|
|
|
8811
9740
|
codeMode
|
|
8812
9741
|
}) {
|
|
8813
9742
|
const { exit } = useApp();
|
|
8814
|
-
const [historical, setHistorical] =
|
|
8815
|
-
const [streaming, setStreaming] =
|
|
8816
|
-
const [input, setInput] =
|
|
8817
|
-
const [busy, setBusy] =
|
|
9743
|
+
const [historical, setHistorical] = useState6([]);
|
|
9744
|
+
const [streaming, setStreaming] = useState6(null);
|
|
9745
|
+
const [input, setInput] = useState6("");
|
|
9746
|
+
const [busy, setBusy] = useState6(false);
|
|
8818
9747
|
const abortedThisTurn = useRef2(false);
|
|
8819
|
-
const [ongoingTool, setOngoingTool] =
|
|
8820
|
-
const [toolProgress, setToolProgress] =
|
|
8821
|
-
const [subagentActivity, setSubagentActivity] =
|
|
8822
|
-
const [statusLine, setStatusLine] =
|
|
8823
|
-
const [balance, setBalance] =
|
|
8824
|
-
const [models, setModels] =
|
|
8825
|
-
const [latestVersion, setLatestVersion] =
|
|
9748
|
+
const [ongoingTool, setOngoingTool] = useState6(null);
|
|
9749
|
+
const [toolProgress, setToolProgress] = useState6(null);
|
|
9750
|
+
const [subagentActivity, setSubagentActivity] = useState6(null);
|
|
9751
|
+
const [statusLine, setStatusLine] = useState6(null);
|
|
9752
|
+
const [balance, setBalance] = useState6(null);
|
|
9753
|
+
const [models, setModels] = useState6(null);
|
|
9754
|
+
const [latestVersion, setLatestVersion] = useState6(null);
|
|
8826
9755
|
const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
|
|
8827
|
-
const [hookList, setHookList] =
|
|
9756
|
+
const [hookList, setHookList] = useState6(
|
|
8828
9757
|
() => loadHooks({ projectRoot: codeMode?.rootDir })
|
|
8829
9758
|
);
|
|
8830
9759
|
const hookCwd = codeMode?.rootDir ?? process.cwd();
|
|
8831
|
-
const
|
|
9760
|
+
const editHistory = useRef2([]);
|
|
9761
|
+
const nextHistoryId = useRef2(1);
|
|
9762
|
+
const currentTurnEntry = useRef2(null);
|
|
8832
9763
|
const pendingEdits = useRef2([]);
|
|
8833
|
-
const [
|
|
8834
|
-
const
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
const [
|
|
9764
|
+
const [pendingCount, setPendingCount] = useState6(0);
|
|
9765
|
+
const syncPendingCount = useCallback(() => {
|
|
9766
|
+
setPendingCount(pendingEdits.current.length);
|
|
9767
|
+
}, []);
|
|
9768
|
+
const [editMode, setEditMode] = useState6(() => codeMode ? loadEditMode() : "review");
|
|
9769
|
+
const editModeRef = useRef2(editMode);
|
|
9770
|
+
useEffect2(() => {
|
|
9771
|
+
editModeRef.current = editMode;
|
|
9772
|
+
if (codeMode) saveEditMode(editMode);
|
|
9773
|
+
}, [editMode, codeMode]);
|
|
9774
|
+
const [undoBanner, setUndoBanner] = useState6(null);
|
|
9775
|
+
const undoTimeoutRef = useRef2(null);
|
|
9776
|
+
const [pendingEditReview, setPendingEditReview] = useState6(null);
|
|
9777
|
+
const editReviewResolveRef = useRef2(null);
|
|
9778
|
+
const turnEditPolicyRef = useRef2("ask");
|
|
9779
|
+
const [modeFlash, setModeFlash] = useState6(false);
|
|
9780
|
+
const modeFlashTimeoutRef = useRef2(null);
|
|
9781
|
+
const prevEditModeRef = useRef2(editMode);
|
|
9782
|
+
useEffect2(() => {
|
|
9783
|
+
if (prevEditModeRef.current === editMode) return;
|
|
9784
|
+
prevEditModeRef.current = editMode;
|
|
9785
|
+
setModeFlash(true);
|
|
9786
|
+
if (modeFlashTimeoutRef.current) clearTimeout(modeFlashTimeoutRef.current);
|
|
9787
|
+
modeFlashTimeoutRef.current = setTimeout(() => {
|
|
9788
|
+
setModeFlash(false);
|
|
9789
|
+
modeFlashTimeoutRef.current = null;
|
|
9790
|
+
}, 1200);
|
|
9791
|
+
}, [editMode]);
|
|
9792
|
+
const [pendingShell, setPendingShell] = useState6(null);
|
|
9793
|
+
const [pendingPlan, setPendingPlan] = useState6(null);
|
|
9794
|
+
const [stagedInput, setStagedInput] = useState6(null);
|
|
9795
|
+
const [planMode, setPlanMode] = useState6(false);
|
|
9796
|
+
const [queuedSubmit, setQueuedSubmit] = useState6(null);
|
|
8838
9797
|
const promptHistory = useRef2([]);
|
|
8839
9798
|
const historyCursor = useRef2(-1);
|
|
8840
9799
|
const assistantIterCounter = useRef2(0);
|
|
8841
9800
|
const toolHistoryRef = useRef2([]);
|
|
8842
|
-
const [slashSelected, setSlashSelected] =
|
|
8843
|
-
const [summary, setSummary] =
|
|
9801
|
+
const [slashSelected, setSlashSelected] = useState6(0);
|
|
9802
|
+
const [summary, setSummary] = useState6({
|
|
8844
9803
|
turns: 0,
|
|
8845
9804
|
totalCostUsd: 0,
|
|
8846
9805
|
totalInputCostUsd: 0,
|
|
@@ -8864,7 +9823,7 @@ function App({
|
|
|
8864
9823
|
transcriptRef.current?.end();
|
|
8865
9824
|
};
|
|
8866
9825
|
}, []);
|
|
8867
|
-
const slashMatches =
|
|
9826
|
+
const slashMatches = useMemo2(() => {
|
|
8868
9827
|
if (!input.startsWith("/") || input.includes(" ")) return null;
|
|
8869
9828
|
return suggestSlashCommands(input.slice(1), !!codeMode);
|
|
8870
9829
|
}, [input, codeMode]);
|
|
@@ -8875,8 +9834,8 @@ function App({
|
|
|
8875
9834
|
return prev;
|
|
8876
9835
|
});
|
|
8877
9836
|
}, [slashMatches]);
|
|
8878
|
-
const [atSelected, setAtSelected] =
|
|
8879
|
-
const atFiles =
|
|
9837
|
+
const [atSelected, setAtSelected] = useState6(0);
|
|
9838
|
+
const atFiles = useMemo2(() => {
|
|
8880
9839
|
if (!codeMode?.rootDir) return [];
|
|
8881
9840
|
try {
|
|
8882
9841
|
return listFilesWithStatsSync(codeMode.rootDir, { maxResults: 500 });
|
|
@@ -8892,12 +9851,12 @@ function App({
|
|
|
8892
9851
|
list.unshift(p);
|
|
8893
9852
|
if (list.length > 20) list.length = 20;
|
|
8894
9853
|
}, []);
|
|
8895
|
-
const atPicker =
|
|
9854
|
+
const atPicker = useMemo2(() => {
|
|
8896
9855
|
if (!codeMode?.rootDir) return null;
|
|
8897
9856
|
if (slashMatches !== null) return null;
|
|
8898
9857
|
return detectAtPicker(input);
|
|
8899
9858
|
}, [codeMode?.rootDir, input, slashMatches]);
|
|
8900
|
-
const atMatches =
|
|
9859
|
+
const atMatches = useMemo2(() => {
|
|
8901
9860
|
if (!atPicker) return null;
|
|
8902
9861
|
return rankPickerCandidates(atFiles, atPicker.query, {
|
|
8903
9862
|
limit: 40,
|
|
@@ -8919,13 +9878,13 @@ function App({
|
|
|
8919
9878
|
},
|
|
8920
9879
|
[atPicker, input]
|
|
8921
9880
|
);
|
|
8922
|
-
const [slashArgSelected, setSlashArgSelected] =
|
|
8923
|
-
const slashArgContext =
|
|
9881
|
+
const [slashArgSelected, setSlashArgSelected] = useState6(0);
|
|
9882
|
+
const slashArgContext = useMemo2(() => {
|
|
8924
9883
|
if (!input.startsWith("/")) return null;
|
|
8925
9884
|
if (slashMatches !== null) return null;
|
|
8926
9885
|
return detectSlashArgContext(input, !!codeMode);
|
|
8927
9886
|
}, [input, slashMatches, codeMode]);
|
|
8928
|
-
const slashArgMatches =
|
|
9887
|
+
const slashArgMatches = useMemo2(() => {
|
|
8929
9888
|
if (!slashArgContext || slashArgContext.kind !== "picker") return null;
|
|
8930
9889
|
const completer = slashArgContext.spec.argCompleter;
|
|
8931
9890
|
const partial = slashArgContext.partial;
|
|
@@ -8982,7 +9941,7 @@ function App({
|
|
|
8982
9941
|
);
|
|
8983
9942
|
const loopRef = useRef2(null);
|
|
8984
9943
|
const subagentSinkRef = useRef2({ current: null });
|
|
8985
|
-
const loop =
|
|
9944
|
+
const loop = useMemo2(() => {
|
|
8986
9945
|
if (loopRef.current) return loopRef.current;
|
|
8987
9946
|
const client = new DeepSeekClient();
|
|
8988
9947
|
if (tools && !tools.has("run_skill")) {
|
|
@@ -9021,7 +9980,11 @@ function App({
|
|
|
9021
9980
|
branch,
|
|
9022
9981
|
session,
|
|
9023
9982
|
hooks: hookList,
|
|
9024
|
-
hookCwd
|
|
9983
|
+
hookCwd,
|
|
9984
|
+
// Restore the user's last-chosen effort cap. Without this a
|
|
9985
|
+
// `/effort high` silently reverted to `max` on relaunch — the
|
|
9986
|
+
// loop's constructor default wins over persisted state.
|
|
9987
|
+
reasoningEffort: loadReasoningEffort()
|
|
9025
9988
|
});
|
|
9026
9989
|
loopRef.current = l;
|
|
9027
9990
|
return l;
|
|
@@ -9161,6 +10124,7 @@ function App({
|
|
|
9161
10124
|
const restored = loadPendingEdits(session);
|
|
9162
10125
|
if (restored && restored.length > 0) {
|
|
9163
10126
|
pendingEdits.current = restored;
|
|
10127
|
+
syncPendingCount();
|
|
9164
10128
|
setHistorical((prev) => [
|
|
9165
10129
|
...prev,
|
|
9166
10130
|
{
|
|
@@ -9171,14 +10135,54 @@ function App({
|
|
|
9171
10135
|
]);
|
|
9172
10136
|
}
|
|
9173
10137
|
}
|
|
9174
|
-
|
|
9175
|
-
|
|
10138
|
+
if (codeMode && !editModeHintShown()) {
|
|
10139
|
+
setHistorical((prev) => [
|
|
10140
|
+
...prev,
|
|
10141
|
+
{
|
|
10142
|
+
id: `sys-edittip-${Date.now()}`,
|
|
10143
|
+
role: "info",
|
|
10144
|
+
text: "\u25B8 TIP: edit-gate keybindings\n y / n accept or drop pending edits\n Shift+Tab switch review \u2194 AUTO (persisted; AUTO applies instantly)\n u undo the last auto-applied batch (within the 5s banner)\n Current mode is shown in the bottom status bar. Run /keys anytime for the full list.\n (This tip shows once \u2014 suppressed after.)"
|
|
10145
|
+
}
|
|
10146
|
+
]);
|
|
10147
|
+
markEditModeHintShown();
|
|
10148
|
+
}
|
|
10149
|
+
}, [session, loop, codeMode, syncPendingCount]);
|
|
10150
|
+
useInput5((chKey, key) => {
|
|
9176
10151
|
if (key.escape && busy) {
|
|
9177
10152
|
if (abortedThisTurn.current) return;
|
|
9178
10153
|
abortedThisTurn.current = true;
|
|
10154
|
+
const resolve8 = editReviewResolveRef.current;
|
|
10155
|
+
if (resolve8) {
|
|
10156
|
+
editReviewResolveRef.current = null;
|
|
10157
|
+
setPendingEditReview(null);
|
|
10158
|
+
resolve8("reject");
|
|
10159
|
+
}
|
|
9179
10160
|
loop.abort();
|
|
9180
10161
|
return;
|
|
9181
10162
|
}
|
|
10163
|
+
if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview) {
|
|
10164
|
+
setEditMode((m) => {
|
|
10165
|
+
const next = m === "auto" ? "review" : "auto";
|
|
10166
|
+
setHistorical((prev) => [
|
|
10167
|
+
...prev,
|
|
10168
|
+
{
|
|
10169
|
+
id: `mode-${Date.now()}`,
|
|
10170
|
+
role: "info",
|
|
10171
|
+
text: next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo" : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)"
|
|
10172
|
+
}
|
|
10173
|
+
]);
|
|
10174
|
+
return next;
|
|
10175
|
+
});
|
|
10176
|
+
return;
|
|
10177
|
+
}
|
|
10178
|
+
if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && // Fire when EITHER the banner is up OR there's any non-undone
|
|
10179
|
+
// history entry — the keybind is useful long after the 5-second
|
|
10180
|
+
// banner expires, which users rightly want.
|
|
10181
|
+
(undoBanner || editHistory.current.some((e) => !isEntryFullyUndone(e)))) {
|
|
10182
|
+
const out = codeUndo([]);
|
|
10183
|
+
setHistorical((prev) => [...prev, { id: `undo-${Date.now()}`, role: "info", text: out }]);
|
|
10184
|
+
return;
|
|
10185
|
+
}
|
|
9182
10186
|
if (busy) return;
|
|
9183
10187
|
if (pendingShell) return;
|
|
9184
10188
|
if (atMatches && atMatches.length > 0) {
|
|
@@ -9244,16 +10248,269 @@ function App({
|
|
|
9244
10248
|
}
|
|
9245
10249
|
}
|
|
9246
10250
|
});
|
|
9247
|
-
const
|
|
10251
|
+
const recordEdit = useCallback(
|
|
10252
|
+
(source, blocks, results, snaps) => {
|
|
10253
|
+
if (snaps.length === 0) return;
|
|
10254
|
+
let entry = currentTurnEntry.current;
|
|
10255
|
+
if (!entry) {
|
|
10256
|
+
entry = {
|
|
10257
|
+
id: nextHistoryId.current++,
|
|
10258
|
+
at: Date.now(),
|
|
10259
|
+
source,
|
|
10260
|
+
blocks: [],
|
|
10261
|
+
results: [],
|
|
10262
|
+
snapshots: [],
|
|
10263
|
+
undoneFiles: /* @__PURE__ */ new Set()
|
|
10264
|
+
};
|
|
10265
|
+
currentTurnEntry.current = entry;
|
|
10266
|
+
editHistory.current.push(entry);
|
|
10267
|
+
}
|
|
10268
|
+
entry.blocks.push(...blocks);
|
|
10269
|
+
entry.results.push(...results);
|
|
10270
|
+
const seen = new Set(entry.snapshots.map((s) => s.path));
|
|
10271
|
+
for (const s of snaps) {
|
|
10272
|
+
if (!seen.has(s.path)) entry.snapshots.push(s);
|
|
10273
|
+
}
|
|
10274
|
+
},
|
|
10275
|
+
[]
|
|
10276
|
+
);
|
|
10277
|
+
const armUndoBanner = useCallback((results) => {
|
|
10278
|
+
setUndoBanner({ results, expiresAt: Date.now() + 5e3 });
|
|
10279
|
+
if (undoTimeoutRef.current) clearTimeout(undoTimeoutRef.current);
|
|
10280
|
+
undoTimeoutRef.current = setTimeout(() => {
|
|
10281
|
+
setUndoBanner(null);
|
|
10282
|
+
undoTimeoutRef.current = null;
|
|
10283
|
+
}, 5e3);
|
|
10284
|
+
}, []);
|
|
10285
|
+
const codeUndo = useCallback(
|
|
10286
|
+
(args = []) => {
|
|
10287
|
+
if (!codeMode) return "not in code mode";
|
|
10288
|
+
const root = codeMode.rootDir;
|
|
10289
|
+
const revert = (entry2, paths) => {
|
|
10290
|
+
const subset = entry2.snapshots.filter((s) => paths.includes(s.path));
|
|
10291
|
+
if (subset.length === 0) {
|
|
10292
|
+
return `batch #${entry2.id}: nothing to undo (already restored or path not in batch)`;
|
|
10293
|
+
}
|
|
10294
|
+
const results = restoreSnapshots(subset, root);
|
|
10295
|
+
for (const s of subset) entry2.undoneFiles.add(s.path);
|
|
10296
|
+
if (currentTurnEntry.current === entry2 && isEntryFullyUndone(entry2)) {
|
|
10297
|
+
currentTurnEntry.current = null;
|
|
10298
|
+
}
|
|
10299
|
+
if (undoTimeoutRef.current) {
|
|
10300
|
+
clearTimeout(undoTimeoutRef.current);
|
|
10301
|
+
undoTimeoutRef.current = null;
|
|
10302
|
+
}
|
|
10303
|
+
setUndoBanner(null);
|
|
10304
|
+
const when = new Date(entry2.at).toISOString().replace("T", " ").slice(11, 19);
|
|
10305
|
+
const scope = subset.length === 1 ? subset[0].path : `${subset.length} file(s)`;
|
|
10306
|
+
const header2 = `\u25B8 undo: reverted ${scope} from batch #${entry2.id} (${when})`;
|
|
10307
|
+
return [header2, ...formatUndoRows(results)].join("\n");
|
|
10308
|
+
};
|
|
10309
|
+
const idArg = args[0];
|
|
10310
|
+
const pathArg = args[1];
|
|
10311
|
+
if (!idArg) {
|
|
10312
|
+
for (let i = editHistory.current.length - 1; i >= 0; i--) {
|
|
10313
|
+
const e = editHistory.current[i];
|
|
10314
|
+
if (isEntryFullyUndone(e)) continue;
|
|
10315
|
+
const remaining = e.snapshots.map((s) => s.path).filter((p) => !e.undoneFiles.has(p));
|
|
10316
|
+
return revert(e, remaining);
|
|
10317
|
+
}
|
|
10318
|
+
return "nothing to undo \u2014 every batch in the session history is already undone";
|
|
10319
|
+
}
|
|
10320
|
+
const id = Number.parseInt(idArg, 10);
|
|
10321
|
+
if (!Number.isFinite(id)) {
|
|
10322
|
+
return "usage: /undo [id] [path] (omit id for newest; id from /history; path from /show <id>)";
|
|
10323
|
+
}
|
|
10324
|
+
const entry = editHistory.current.find((e) => e.id === id);
|
|
10325
|
+
if (!entry) return `no edit #${id} \u2014 run /history to see valid ids`;
|
|
10326
|
+
if (!pathArg) {
|
|
10327
|
+
const remaining = entry.snapshots.map((s) => s.path).filter((p) => !entry.undoneFiles.has(p));
|
|
10328
|
+
if (remaining.length === 0) return `batch #${id} is already fully undone`;
|
|
10329
|
+
return revert(entry, remaining);
|
|
10330
|
+
}
|
|
10331
|
+
const snap = entry.snapshots.find((s) => s.path === pathArg);
|
|
10332
|
+
if (!snap) {
|
|
10333
|
+
const files = [...new Set(entry.blocks.map((b) => b.path))];
|
|
10334
|
+
return `batch #${id} doesn't include "${pathArg}" \u2014 files in this batch: ${files.join(", ")}`;
|
|
10335
|
+
}
|
|
10336
|
+
if (entry.undoneFiles.has(pathArg)) {
|
|
10337
|
+
return `${pathArg} in batch #${id} is already undone`;
|
|
10338
|
+
}
|
|
10339
|
+
return revert(entry, [pathArg]);
|
|
10340
|
+
},
|
|
10341
|
+
[codeMode]
|
|
10342
|
+
);
|
|
10343
|
+
const codeHistory = useCallback(() => {
|
|
9248
10344
|
if (!codeMode) return "not in code mode";
|
|
9249
|
-
const
|
|
9250
|
-
if (
|
|
9251
|
-
|
|
10345
|
+
const entries = editHistory.current;
|
|
10346
|
+
if (entries.length === 0) return "no edits recorded this session yet";
|
|
10347
|
+
const lines = ["Edit history (oldest first):"];
|
|
10348
|
+
for (const e of entries) {
|
|
10349
|
+
const when = new Date(e.at).toISOString().replace("T", " ").slice(11, 19);
|
|
10350
|
+
const files = new Set(e.blocks.map((b) => b.path));
|
|
10351
|
+
const fileList = [...files].join(", ");
|
|
10352
|
+
const fileSummary = fileList.length > 60 ? `${fileList.slice(0, 60)}\u2026` : fileList;
|
|
10353
|
+
const status = entryStatus(e);
|
|
10354
|
+
const statusText = status === "applied" ? "applied" : status === "PARTIAL" ? "PARTIAL" : "UNDONE ";
|
|
10355
|
+
lines.push(
|
|
10356
|
+
` #${String(e.id).padStart(3)} ${when} ${statusText} ${e.source.padEnd(12)} ${files.size} file \xB7 ${e.blocks.length} block ${fileSummary}`
|
|
10357
|
+
);
|
|
9252
10358
|
}
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
10359
|
+
lines.push("");
|
|
10360
|
+
lines.push(
|
|
10361
|
+
"/show <id> \u2192 per-file summary \xB7 /show <id> <path> \u2192 full diff of one file"
|
|
10362
|
+
);
|
|
10363
|
+
lines.push(
|
|
10364
|
+
"/undo \u2192 newest non-undone \xB7 /undo <id> [path] \u2192 target a specific batch or file"
|
|
10365
|
+
);
|
|
10366
|
+
return lines.join("\n");
|
|
9256
10367
|
}, [codeMode]);
|
|
10368
|
+
const codeShowEdit = useCallback(
|
|
10369
|
+
(args = []) => {
|
|
10370
|
+
if (!codeMode) return "not in code mode";
|
|
10371
|
+
const entries = editHistory.current;
|
|
10372
|
+
if (entries.length === 0) return "no edits recorded this session \u2014 /history is empty";
|
|
10373
|
+
const idArg = args[0];
|
|
10374
|
+
const pathArg = args[1];
|
|
10375
|
+
let entry;
|
|
10376
|
+
if (!idArg) {
|
|
10377
|
+
entry = [...entries].reverse().find((e) => !isEntryFullyUndone(e)) ?? entries[entries.length - 1];
|
|
10378
|
+
} else {
|
|
10379
|
+
const id = Number.parseInt(idArg, 10);
|
|
10380
|
+
if (!Number.isFinite(id)) {
|
|
10381
|
+
return "usage: /show [id] [path] (omit id for newest; path from the per-file summary)";
|
|
10382
|
+
}
|
|
10383
|
+
entry = entries.find((e) => e.id === id);
|
|
10384
|
+
if (!entry) return `no edit #${id} \u2014 run /history to see valid ids`;
|
|
10385
|
+
}
|
|
10386
|
+
if (!entry) return "unexpected: history lookup failed";
|
|
10387
|
+
if (pathArg) {
|
|
10388
|
+
const fileBlocks = entry.blocks.filter((b) => b.path === pathArg);
|
|
10389
|
+
if (fileBlocks.length === 0) {
|
|
10390
|
+
const files2 = [...new Set(entry.blocks.map((b) => b.path))];
|
|
10391
|
+
return `batch #${entry.id} doesn't include "${pathArg}" \u2014 files in this batch: ${files2.join(", ")}`;
|
|
10392
|
+
}
|
|
10393
|
+
const when2 = new Date(entry.at).toISOString().replace("T", " ").slice(11, 19);
|
|
10394
|
+
const state = entry.undoneFiles.has(pathArg) ? "UNDONE" : "applied";
|
|
10395
|
+
const header3 = `\u25B8 edit #${entry.id} \xB7 ${when2} \xB7 ${pathArg} \xB7 ${state} \xB7 ${fileBlocks.length} block(s)`;
|
|
10396
|
+
const diff = formatAllBlockDiffs(fileBlocks, { maxLines: 60, contextLines: 2 });
|
|
10397
|
+
const footer = entry.undoneFiles.has(pathArg) ? "(already reverted \u2014 /history shows the batch-level status)" : `/undo ${entry.id} ${pathArg} \u2192 revert just this file`;
|
|
10398
|
+
return [header3, ...diff, "", footer].join("\n");
|
|
10399
|
+
}
|
|
10400
|
+
const when = new Date(entry.at).toISOString().replace("T", " ").slice(11, 19);
|
|
10401
|
+
const files = [...new Set(entry.blocks.map((b) => b.path))];
|
|
10402
|
+
const status = entryStatus(entry);
|
|
10403
|
+
const header2 = `\u25B8 edit #${entry.id} \xB7 ${when} \xB7 ${entry.source} \xB7 ${status} \xB7 ${files.length} file(s)`;
|
|
10404
|
+
const countLines3 = (s) => s.length === 0 ? 0 : (s.match(/\n/g)?.length ?? 0) + 1;
|
|
10405
|
+
const fileLines = files.map((path) => {
|
|
10406
|
+
const fileBlocks = entry.blocks.filter((b) => b.path === path);
|
|
10407
|
+
let removed = 0;
|
|
10408
|
+
let added = 0;
|
|
10409
|
+
for (const b of fileBlocks) {
|
|
10410
|
+
removed += countLines3(b.search);
|
|
10411
|
+
added += countLines3(b.replace);
|
|
10412
|
+
}
|
|
10413
|
+
const state = entry.undoneFiles.has(path) ? "UNDONE" : "applied";
|
|
10414
|
+
return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
|
|
10415
|
+
});
|
|
10416
|
+
return [
|
|
10417
|
+
header2,
|
|
10418
|
+
...fileLines,
|
|
10419
|
+
"",
|
|
10420
|
+
`/show ${entry.id} <path> \u2192 full diff of one file`,
|
|
10421
|
+
`/undo ${entry.id} <path> \u2192 revert just that file \xB7 /undo ${entry.id} \u2192 revert whole batch`
|
|
10422
|
+
].join("\n");
|
|
10423
|
+
},
|
|
10424
|
+
[codeMode]
|
|
10425
|
+
);
|
|
10426
|
+
useEffect2(() => {
|
|
10427
|
+
if (!tools || !codeMode) return;
|
|
10428
|
+
tools.setToolInterceptor(async (name, args) => {
|
|
10429
|
+
if (name !== "edit_file" && name !== "write_file") return null;
|
|
10430
|
+
const rawPath = typeof args.path === "string" ? args.path : "";
|
|
10431
|
+
if (!rawPath) return null;
|
|
10432
|
+
let relPath = rawPath;
|
|
10433
|
+
while (relPath.startsWith("/") || relPath.startsWith("\\")) {
|
|
10434
|
+
relPath = relPath.slice(1);
|
|
10435
|
+
}
|
|
10436
|
+
if (!relPath) return null;
|
|
10437
|
+
let block;
|
|
10438
|
+
if (name === "edit_file") {
|
|
10439
|
+
const search = typeof args.search === "string" ? args.search : "";
|
|
10440
|
+
const replace = typeof args.replace === "string" ? args.replace : "";
|
|
10441
|
+
if (!search) return null;
|
|
10442
|
+
block = { path: relPath, search, replace, offset: 0 };
|
|
10443
|
+
} else {
|
|
10444
|
+
const content = typeof args.content === "string" ? args.content : "";
|
|
10445
|
+
block = toWholeFileEditBlock(relPath, content, codeMode.rootDir);
|
|
10446
|
+
}
|
|
10447
|
+
const applyNow = () => {
|
|
10448
|
+
const snaps = snapshotBeforeEdits([block], codeMode.rootDir);
|
|
10449
|
+
const results = applyEditBlocks([block], codeMode.rootDir);
|
|
10450
|
+
const good = results.some((r) => r.status === "applied" || r.status === "created");
|
|
10451
|
+
if (good) {
|
|
10452
|
+
recordEdit("auto", [block], results, snaps);
|
|
10453
|
+
armUndoBanner(results);
|
|
10454
|
+
}
|
|
10455
|
+
setHistorical((prev) => [
|
|
10456
|
+
...prev,
|
|
10457
|
+
{
|
|
10458
|
+
id: `ae-${Date.now()}-${Math.random()}`,
|
|
10459
|
+
role: "info",
|
|
10460
|
+
text: formatEditResults(results)
|
|
10461
|
+
}
|
|
10462
|
+
]);
|
|
10463
|
+
return formatEditResults(results);
|
|
10464
|
+
};
|
|
10465
|
+
if (editModeRef.current === "auto") return applyNow();
|
|
10466
|
+
if (turnEditPolicyRef.current === "apply-all") return applyNow();
|
|
10467
|
+
const choice = await new Promise((resolveChoice) => {
|
|
10468
|
+
editReviewResolveRef.current = resolveChoice;
|
|
10469
|
+
setPendingEditReview(block);
|
|
10470
|
+
});
|
|
10471
|
+
editReviewResolveRef.current = null;
|
|
10472
|
+
setPendingEditReview(null);
|
|
10473
|
+
if (choice === "reject") {
|
|
10474
|
+
setHistorical((prev) => [
|
|
10475
|
+
...prev,
|
|
10476
|
+
{
|
|
10477
|
+
id: `er-${Date.now()}-${Math.random()}`,
|
|
10478
|
+
role: "info",
|
|
10479
|
+
text: `\u25B8 rejected edit to ${block.path}`
|
|
10480
|
+
}
|
|
10481
|
+
]);
|
|
10482
|
+
return `User rejected this edit to ${block.path}. Don't retry the same SEARCH/REPLACE \u2014 either try a different approach or ask the user what they want instead.`;
|
|
10483
|
+
}
|
|
10484
|
+
if (choice === "apply-rest-of-turn") {
|
|
10485
|
+
turnEditPolicyRef.current = "apply-all";
|
|
10486
|
+
setHistorical((prev) => [
|
|
10487
|
+
...prev,
|
|
10488
|
+
{
|
|
10489
|
+
id: `er-${Date.now()}-${Math.random()}`,
|
|
10490
|
+
role: "info",
|
|
10491
|
+
text: "\u25B8 auto-approving remaining edits for this turn"
|
|
10492
|
+
}
|
|
10493
|
+
]);
|
|
10494
|
+
return applyNow();
|
|
10495
|
+
}
|
|
10496
|
+
if (choice === "flip-to-auto") {
|
|
10497
|
+
setEditMode("auto");
|
|
10498
|
+
setHistorical((prev) => [
|
|
10499
|
+
...prev,
|
|
10500
|
+
{
|
|
10501
|
+
id: `er-${Date.now()}-${Math.random()}`,
|
|
10502
|
+
role: "info",
|
|
10503
|
+
text: "\u25B8 flipped to AUTO mode for the rest of the session (persisted)"
|
|
10504
|
+
}
|
|
10505
|
+
]);
|
|
10506
|
+
return applyNow();
|
|
10507
|
+
}
|
|
10508
|
+
return applyNow();
|
|
10509
|
+
});
|
|
10510
|
+
return () => {
|
|
10511
|
+
tools.setToolInterceptor(null);
|
|
10512
|
+
};
|
|
10513
|
+
}, [tools, codeMode, session, recordEdit, armUndoBanner, syncPendingCount, setEditMode]);
|
|
9257
10514
|
const codeApply = useCallback(() => {
|
|
9258
10515
|
if (!codeMode) return "not in code mode";
|
|
9259
10516
|
const blocks = pendingEdits.current;
|
|
@@ -9263,18 +10520,20 @@ function App({
|
|
|
9263
10520
|
const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
|
|
9264
10521
|
const results = applyEditBlocks(blocks, codeMode.rootDir);
|
|
9265
10522
|
const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
|
|
9266
|
-
if (anyApplied)
|
|
10523
|
+
if (anyApplied) recordEdit("review-apply", blocks, results, snaps);
|
|
9267
10524
|
pendingEdits.current = [];
|
|
9268
10525
|
clearPendingEdits(session ?? null);
|
|
10526
|
+
syncPendingCount();
|
|
9269
10527
|
return formatEditResults(results);
|
|
9270
|
-
}, [codeMode, session]);
|
|
10528
|
+
}, [codeMode, session, syncPendingCount, recordEdit]);
|
|
9271
10529
|
const codeDiscard = useCallback(() => {
|
|
9272
10530
|
const count = pendingEdits.current.length;
|
|
9273
10531
|
if (count === 0) return "nothing pending to discard.";
|
|
9274
10532
|
pendingEdits.current = [];
|
|
9275
10533
|
clearPendingEdits(session ?? null);
|
|
10534
|
+
syncPendingCount();
|
|
9276
10535
|
return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
|
|
9277
|
-
}, [session]);
|
|
10536
|
+
}, [session, syncPendingCount]);
|
|
9278
10537
|
const prefixHash = loop.prefix.fingerprint;
|
|
9279
10538
|
const writeTranscript = useCallback(
|
|
9280
10539
|
(ev) => {
|
|
@@ -9392,6 +10651,8 @@ function App({
|
|
|
9392
10651
|
codeUndo: codeMode ? codeUndo : void 0,
|
|
9393
10652
|
codeApply: codeMode ? codeApply : void 0,
|
|
9394
10653
|
codeDiscard: codeMode ? codeDiscard : void 0,
|
|
10654
|
+
codeHistory: codeMode ? codeHistory : void 0,
|
|
10655
|
+
codeShowEdit: codeMode ? codeShowEdit : void 0,
|
|
9395
10656
|
codeRoot: codeMode?.rootDir,
|
|
9396
10657
|
pendingEditCount: codeMode ? pendingEdits.current.length : void 0,
|
|
9397
10658
|
toolHistory: () => toolHistoryRef.current,
|
|
@@ -9399,6 +10660,13 @@ function App({
|
|
|
9399
10660
|
planMode,
|
|
9400
10661
|
setPlanMode: codeMode ? togglePlanMode : void 0,
|
|
9401
10662
|
clearPendingPlan: codeMode ? clearPendingPlan : void 0,
|
|
10663
|
+
editMode: codeMode ? editMode : void 0,
|
|
10664
|
+
setEditMode: codeMode ? setEditMode : void 0,
|
|
10665
|
+
jobs: codeMode?.jobs,
|
|
10666
|
+
postInfo: (text2) => setHistorical((prev) => [
|
|
10667
|
+
...prev,
|
|
10668
|
+
{ id: `sys-late-${Date.now()}-${Math.random()}`, role: "info", text: text2 }
|
|
10669
|
+
]),
|
|
9402
10670
|
reloadHooks: () => {
|
|
9403
10671
|
const fresh = loadHooks({ projectRoot: codeMode?.rootDir });
|
|
9404
10672
|
setHookList(fresh);
|
|
@@ -9435,6 +10703,7 @@ function App({
|
|
|
9435
10703
|
if (codeMode) {
|
|
9436
10704
|
pendingEdits.current = [];
|
|
9437
10705
|
clearPendingEdits(session ?? null);
|
|
10706
|
+
syncPendingCount();
|
|
9438
10707
|
}
|
|
9439
10708
|
return;
|
|
9440
10709
|
}
|
|
@@ -9443,6 +10712,7 @@ function App({
|
|
|
9443
10712
|
if (codeMode) {
|
|
9444
10713
|
pendingEdits.current = [];
|
|
9445
10714
|
clearPendingEdits(session ?? null);
|
|
10715
|
+
syncPendingCount();
|
|
9446
10716
|
}
|
|
9447
10717
|
return;
|
|
9448
10718
|
}
|
|
@@ -9505,6 +10775,10 @@ function App({
|
|
|
9505
10775
|
setStreaming({ id: assistantId, role: "assistant", text: "", streaming: true });
|
|
9506
10776
|
setBusy(true);
|
|
9507
10777
|
abortedThisTurn.current = false;
|
|
10778
|
+
if (codeMode) {
|
|
10779
|
+
currentTurnEntry.current = null;
|
|
10780
|
+
}
|
|
10781
|
+
turnEditPolicyRef.current = "ask";
|
|
9508
10782
|
const flush = () => {
|
|
9509
10783
|
if (!contentBuf.current && !reasoningBuf.current && !toolCallBuildBuf.current) return;
|
|
9510
10784
|
streamRef.text += contentBuf.current;
|
|
@@ -9618,16 +10892,37 @@ function App({
|
|
|
9618
10892
|
if (codeMode && finalText && !ev.forcedSummary) {
|
|
9619
10893
|
const blocks = parseEditBlocks(finalText);
|
|
9620
10894
|
if (blocks.length > 0) {
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
text
|
|
10895
|
+
if (editModeRef.current === "auto") {
|
|
10896
|
+
const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
|
|
10897
|
+
const results = applyEditBlocks(blocks, codeMode.rootDir);
|
|
10898
|
+
const good = results.some(
|
|
10899
|
+
(r) => r.status === "applied" || r.status === "created"
|
|
10900
|
+
);
|
|
10901
|
+
if (good) {
|
|
10902
|
+
recordEdit("auto-text", blocks, results, snaps);
|
|
10903
|
+
armUndoBanner(results);
|
|
9629
10904
|
}
|
|
9630
|
-
|
|
10905
|
+
setHistorical((prev) => [
|
|
10906
|
+
...prev,
|
|
10907
|
+
{
|
|
10908
|
+
id: `applied-${Date.now()}`,
|
|
10909
|
+
role: "info",
|
|
10910
|
+
text: formatEditResults(results)
|
|
10911
|
+
}
|
|
10912
|
+
]);
|
|
10913
|
+
} else {
|
|
10914
|
+
pendingEdits.current = [...pendingEdits.current, ...blocks];
|
|
10915
|
+
savePendingEdits(session ?? null, pendingEdits.current);
|
|
10916
|
+
syncPendingCount();
|
|
10917
|
+
setHistorical((prev) => [
|
|
10918
|
+
...prev,
|
|
10919
|
+
{
|
|
10920
|
+
id: `pending-${Date.now()}`,
|
|
10921
|
+
role: "info",
|
|
10922
|
+
text: formatPendingPreview(pendingEdits.current)
|
|
10923
|
+
}
|
|
10924
|
+
]);
|
|
10925
|
+
}
|
|
9631
10926
|
}
|
|
9632
10927
|
}
|
|
9633
10928
|
} else if (ev.role === "tool_start") {
|
|
@@ -9663,11 +10958,14 @@ function App({
|
|
|
9663
10958
|
toolName: ev.toolName
|
|
9664
10959
|
}
|
|
9665
10960
|
]);
|
|
9666
|
-
if (codeMode && ev.toolName === "run_command" && ev.content.includes('"NeedsConfirmationError:') && ev.toolArgs) {
|
|
10961
|
+
if (codeMode && (ev.toolName === "run_command" || ev.toolName === "run_background") && ev.content.includes('"NeedsConfirmationError:') && ev.toolArgs) {
|
|
9667
10962
|
try {
|
|
9668
10963
|
const parsed = JSON.parse(ev.toolArgs);
|
|
9669
10964
|
if (typeof parsed.command === "string" && parsed.command.trim()) {
|
|
9670
|
-
setPendingShell(
|
|
10965
|
+
setPendingShell({
|
|
10966
|
+
command: parsed.command.trim(),
|
|
10967
|
+
kind: ev.toolName
|
|
10968
|
+
});
|
|
9671
10969
|
}
|
|
9672
10970
|
} catch {
|
|
9673
10971
|
}
|
|
@@ -9738,7 +11036,9 @@ function App({
|
|
|
9738
11036
|
clearPendingPlan,
|
|
9739
11037
|
codeApply,
|
|
9740
11038
|
codeDiscard,
|
|
11039
|
+
codeHistory,
|
|
9741
11040
|
codeMode,
|
|
11041
|
+
codeShowEdit,
|
|
9742
11042
|
codeUndo,
|
|
9743
11043
|
exit,
|
|
9744
11044
|
hookCwd,
|
|
@@ -9761,13 +11061,18 @@ function App({
|
|
|
9761
11061
|
slashArgSelected,
|
|
9762
11062
|
pickSlashArg,
|
|
9763
11063
|
togglePlanMode,
|
|
9764
|
-
writeTranscript
|
|
11064
|
+
writeTranscript,
|
|
11065
|
+
recordEdit,
|
|
11066
|
+
armUndoBanner,
|
|
11067
|
+
editMode,
|
|
11068
|
+
syncPendingCount
|
|
9765
11069
|
]
|
|
9766
11070
|
);
|
|
9767
11071
|
const handleShellConfirm = useCallback(
|
|
9768
11072
|
async (choice) => {
|
|
9769
|
-
const
|
|
9770
|
-
if (!
|
|
11073
|
+
const pending = pendingShell;
|
|
11074
|
+
if (!pending || !codeMode) return;
|
|
11075
|
+
const { command: cmd, kind } = pending;
|
|
9771
11076
|
setPendingShell(null);
|
|
9772
11077
|
let synthetic;
|
|
9773
11078
|
if (choice === "deny") {
|
|
@@ -9791,23 +11096,65 @@ function App({
|
|
|
9791
11096
|
}
|
|
9792
11097
|
setHistorical((prev) => [
|
|
9793
11098
|
...prev,
|
|
9794
|
-
{
|
|
11099
|
+
{
|
|
11100
|
+
id: `sh-run-${Date.now()}`,
|
|
11101
|
+
role: "info",
|
|
11102
|
+
text: kind === "run_background" ? `\u25B8 starting (background): ${cmd}` : `\u25B8 running: ${cmd}`
|
|
11103
|
+
}
|
|
9795
11104
|
]);
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
11105
|
+
if (kind === "run_background" && codeMode.jobs) {
|
|
11106
|
+
let startedOk = false;
|
|
11107
|
+
let jobId = null;
|
|
11108
|
+
let preview = "";
|
|
11109
|
+
try {
|
|
11110
|
+
const res = await codeMode.jobs.start(cmd, { cwd: codeMode.rootDir });
|
|
11111
|
+
startedOk = true;
|
|
11112
|
+
jobId = res.jobId;
|
|
11113
|
+
preview = res.preview;
|
|
11114
|
+
const header2 = res.stillRunning ? `[job ${res.jobId} started \xB7 pid ${res.pid ?? "?"} \xB7 ${res.readyMatched ? "READY signal matched" : "running"}]` : res.exitCode !== null ? `[job ${res.jobId} exited during startup \xB7 exit ${res.exitCode}]` : `[job ${res.jobId} failed to start]`;
|
|
11115
|
+
const body = preview ? `${header2}
|
|
11116
|
+
${preview}` : header2;
|
|
11117
|
+
setHistorical((prev) => [
|
|
11118
|
+
...prev,
|
|
11119
|
+
{ id: `sh-out-${Date.now()}`, role: "info", text: body }
|
|
11120
|
+
]);
|
|
11121
|
+
synthetic = `I approved the background spawn. ${header2}
|
|
11122
|
+
|
|
11123
|
+
Startup preview:
|
|
11124
|
+
|
|
11125
|
+
${preview || "(no output yet)"}
|
|
11126
|
+
|
|
11127
|
+
The process is still running \u2014 use job_output to read newer logs, stop_job to halt it.`;
|
|
11128
|
+
} catch (err) {
|
|
11129
|
+
const msg = `$ ${cmd}
|
|
11130
|
+
[failed to start] ${err.message}`;
|
|
11131
|
+
setHistorical((prev) => [
|
|
11132
|
+
...prev,
|
|
11133
|
+
{ id: `sh-out-${Date.now()}`, role: "info", text: msg }
|
|
11134
|
+
]);
|
|
11135
|
+
synthetic = `I approved the background spawn but it failed to start:
|
|
11136
|
+
|
|
11137
|
+
${msg}`;
|
|
11138
|
+
}
|
|
11139
|
+
void startedOk;
|
|
11140
|
+
void jobId;
|
|
11141
|
+
} else {
|
|
11142
|
+
let body;
|
|
11143
|
+
try {
|
|
11144
|
+
const res = await runCommand(cmd, { cwd: codeMode.rootDir });
|
|
11145
|
+
body = formatCommandResult(cmd, res);
|
|
11146
|
+
} catch (err) {
|
|
11147
|
+
body = `$ ${cmd}
|
|
9802
11148
|
[failed to spawn] ${err.message}`;
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
11149
|
+
}
|
|
11150
|
+
setHistorical((prev) => [
|
|
11151
|
+
...prev,
|
|
11152
|
+
{ id: `sh-out-${Date.now()}`, role: "info", text: body }
|
|
11153
|
+
]);
|
|
11154
|
+
synthetic = `I ran the command you requested. Output:
|
|
9809
11155
|
|
|
9810
11156
|
${body}`;
|
|
11157
|
+
}
|
|
9811
11158
|
}
|
|
9812
11159
|
if (busy) {
|
|
9813
11160
|
loop.abort();
|
|
@@ -9918,7 +11265,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
9918
11265
|
if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
|
|
9919
11266
|
setStagedInput(null);
|
|
9920
11267
|
}, [stagedInput]);
|
|
9921
|
-
return /* @__PURE__ */
|
|
11268
|
+
return /* @__PURE__ */ React15.createElement(TickerProvider, { disabled: PLAIN_UI || !!pendingPlan || !!pendingShell || !!pendingEditReview }, /* @__PURE__ */ React15.createElement(Box14, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(
|
|
9922
11269
|
StatsPanel,
|
|
9923
11270
|
{
|
|
9924
11271
|
summary,
|
|
@@ -9928,32 +11275,56 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
9928
11275
|
branchBudget: loop.branchOptions.budget,
|
|
9929
11276
|
reasoningEffort: loop.reasoningEffort,
|
|
9930
11277
|
planMode,
|
|
11278
|
+
editMode: codeMode ? editMode : void 0,
|
|
9931
11279
|
balance,
|
|
9932
11280
|
busy,
|
|
9933
11281
|
updateAvailable
|
|
9934
11282
|
}
|
|
9935
|
-
), /* @__PURE__ */
|
|
11283
|
+
), /* @__PURE__ */ React15.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React15.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && streaming ? /* @__PURE__ */ React15.createElement(Box14, { marginY: 1 }, /* @__PURE__ */ React15.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && ongoingTool ? /* @__PURE__ */ React15.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && subagentActivity ? /* @__PURE__ */ React15.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !ongoingTool && statusLine ? /* @__PURE__ */ React15.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview ? /* @__PURE__ */ React15.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React15.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React15.createElement(
|
|
9936
11284
|
PlanRefineInput,
|
|
9937
11285
|
{
|
|
9938
11286
|
mode: stagedInput.mode,
|
|
9939
11287
|
onSubmit: handleStagedInputSubmit,
|
|
9940
11288
|
onCancel: handleStagedInputCancel
|
|
9941
11289
|
}
|
|
9942
|
-
) : pendingPlan ? /* @__PURE__ */
|
|
11290
|
+
) : pendingPlan ? /* @__PURE__ */ React15.createElement(
|
|
9943
11291
|
PlanConfirm,
|
|
9944
11292
|
{
|
|
9945
11293
|
plan: pendingPlan,
|
|
9946
11294
|
onChoose: stableHandlePlanConfirm,
|
|
9947
11295
|
projectRoot: hookCwd
|
|
9948
11296
|
}
|
|
9949
|
-
) : pendingShell ? /* @__PURE__ */
|
|
11297
|
+
) : pendingShell ? /* @__PURE__ */ React15.createElement(
|
|
9950
11298
|
ShellConfirm,
|
|
9951
11299
|
{
|
|
9952
|
-
command: pendingShell,
|
|
9953
|
-
allowPrefix: derivePrefix(pendingShell),
|
|
11300
|
+
command: pendingShell.command,
|
|
11301
|
+
allowPrefix: derivePrefix(pendingShell.command),
|
|
11302
|
+
kind: pendingShell.kind,
|
|
9954
11303
|
onChoose: handleShellConfirm
|
|
9955
11304
|
}
|
|
9956
|
-
) :
|
|
11305
|
+
) : pendingEditReview ? /* @__PURE__ */ React15.createElement(
|
|
11306
|
+
EditConfirm,
|
|
11307
|
+
{
|
|
11308
|
+
block: pendingEditReview,
|
|
11309
|
+
onChoose: (choice) => {
|
|
11310
|
+
const resolve8 = editReviewResolveRef.current;
|
|
11311
|
+
if (resolve8) {
|
|
11312
|
+
editReviewResolveRef.current = null;
|
|
11313
|
+
resolve8(choice);
|
|
11314
|
+
}
|
|
11315
|
+
}
|
|
11316
|
+
}
|
|
11317
|
+
) : /* @__PURE__ */ React15.createElement(React15.Fragment, null, codeMode ? /* @__PURE__ */ React15.createElement(
|
|
11318
|
+
ModeStatusBar,
|
|
11319
|
+
{
|
|
11320
|
+
editMode,
|
|
11321
|
+
pendingCount,
|
|
11322
|
+
flash: modeFlash,
|
|
11323
|
+
planMode,
|
|
11324
|
+
undoArmed: !!undoBanner || editHistory.current.some((e) => !isEntryFullyUndone(e)),
|
|
11325
|
+
jobs: codeMode.jobs
|
|
11326
|
+
}
|
|
11327
|
+
) : null, /* @__PURE__ */ React15.createElement(
|
|
9957
11328
|
PromptInput,
|
|
9958
11329
|
{
|
|
9959
11330
|
value: input,
|
|
@@ -9961,14 +11332,14 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
9961
11332
|
onSubmit: handleSubmit,
|
|
9962
11333
|
disabled: busy
|
|
9963
11334
|
}
|
|
9964
|
-
), /* @__PURE__ */
|
|
11335
|
+
), /* @__PURE__ */ React15.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React15.createElement(
|
|
9965
11336
|
AtMentionSuggestions,
|
|
9966
11337
|
{
|
|
9967
11338
|
matches: atMatches,
|
|
9968
11339
|
selectedIndex: atSelected,
|
|
9969
11340
|
query: atPicker?.query ?? ""
|
|
9970
11341
|
}
|
|
9971
|
-
), slashArgContext ? /* @__PURE__ */
|
|
11342
|
+
), slashArgContext ? /* @__PURE__ */ React15.createElement(
|
|
9972
11343
|
SlashArgPicker,
|
|
9973
11344
|
{
|
|
9974
11345
|
matches: slashArgMatches,
|
|
@@ -9983,14 +11354,44 @@ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834"
|
|
|
9983
11354
|
function StatusRow({ text }) {
|
|
9984
11355
|
const tick = useTick();
|
|
9985
11356
|
const elapsed = useElapsedSeconds();
|
|
9986
|
-
return /* @__PURE__ */
|
|
11357
|
+
return /* @__PURE__ */ React15.createElement(Box14, { marginY: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` ${elapsed}s`));
|
|
11358
|
+
}
|
|
11359
|
+
function ModeStatusBar({
|
|
11360
|
+
editMode,
|
|
11361
|
+
pendingCount,
|
|
11362
|
+
flash,
|
|
11363
|
+
planMode,
|
|
11364
|
+
undoArmed,
|
|
11365
|
+
jobs
|
|
11366
|
+
}) {
|
|
11367
|
+
useTick();
|
|
11368
|
+
const running = jobs?.runningCount() ?? 0;
|
|
11369
|
+
const jobsTag = running > 0 ? /* @__PURE__ */ React15.createElement(Text14, { color: "yellow", bold: true }, ` \xB7 \u23F5 ${running} job${running === 1 ? "" : "s"}`) : null;
|
|
11370
|
+
if (planMode) {
|
|
11371
|
+
return /* @__PURE__ */ React15.createElement(Box14, { paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color: "red", bold: true, inverse: flash }, "\u25B8 PLAN"), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " writes gated \u2014 submit_plan + approval required \xB7 /plan off to leave"), jobsTag);
|
|
11372
|
+
}
|
|
11373
|
+
const label = editMode === "auto" ? "AUTO" : "review";
|
|
11374
|
+
const color = editMode === "auto" ? "magenta" : "cyan";
|
|
11375
|
+
const mid = editMode === "auto" ? undoArmed ? "edits apply immediately \xB7 u = undo last batch" : "edits apply immediately \xB7 5s undo window after each batch" : pendingCount > 0 ? `${pendingCount} queued \xB7 y = /apply \xB7 n = /discard` : "edits queue for review \xB7 y = /apply \xB7 n = /discard";
|
|
11376
|
+
const flip = editMode === "auto" ? "Shift+Tab \u2192 review" : "Shift+Tab \u2192 AUTO";
|
|
11377
|
+
return /* @__PURE__ */ React15.createElement(Box14, { paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color, bold: true, inverse: flash }, `\u25B8 ${label}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` ${mid} \xB7 ${flip}`), jobsTag);
|
|
11378
|
+
}
|
|
11379
|
+
function UndoBanner({
|
|
11380
|
+
banner
|
|
11381
|
+
}) {
|
|
11382
|
+
useTick();
|
|
11383
|
+
const remainingMs = Math.max(0, banner.expiresAt - Date.now());
|
|
11384
|
+
const remainingSec = Math.ceil(remainingMs / 1e3);
|
|
11385
|
+
const ok = banner.results.filter((r) => r.status === "applied" || r.status === "created").length;
|
|
11386
|
+
const total = banner.results.length;
|
|
11387
|
+
return /* @__PURE__ */ React15.createElement(Box14, { marginY: 1, borderStyle: "round", borderColor: "magenta", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color: "magenta", bold: true }, "\u2713 auto-applied "), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, `${ok}/${total} edit${total === 1 ? "" : "s"}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " \xB7 press "), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta", bold: true }, "u"), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " to undo ("), /* @__PURE__ */ React15.createElement(Text14, { color: remainingSec <= 1 ? "red" : "magenta" }, `${remainingSec}s`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ")"));
|
|
9987
11388
|
}
|
|
9988
11389
|
function SubagentRow({
|
|
9989
11390
|
activity
|
|
9990
11391
|
}) {
|
|
9991
11392
|
const tick = useTick();
|
|
9992
11393
|
const seconds = (activity.elapsedMs / 1e3).toFixed(1);
|
|
9993
|
-
return /* @__PURE__ */
|
|
11394
|
+
return /* @__PURE__ */ React15.createElement(Box14, { paddingLeft: 2 }, /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, ` \u232C subagent: ${activity.task}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
|
|
9994
11395
|
}
|
|
9995
11396
|
function OngoingToolRow({
|
|
9996
11397
|
tool,
|
|
@@ -9999,7 +11400,7 @@ function OngoingToolRow({
|
|
|
9999
11400
|
const tick = useTick();
|
|
10000
11401
|
const elapsed = useElapsedSeconds();
|
|
10001
11402
|
const summary = summarizeToolArgs(tool.name, tool.args);
|
|
10002
|
-
return /* @__PURE__ */
|
|
11403
|
+
return /* @__PURE__ */ React15.createElement(Box14, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React15.createElement(Box14, null, /* @__PURE__ */ React15.createElement(Text14, { color: "cyan" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React15.createElement(Text14, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React15.createElement(Box14, { paddingLeft: 2 }, /* @__PURE__ */ React15.createElement(Text14, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React15.createElement(Box14, { paddingLeft: 2 }, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, summary)) : null);
|
|
10003
11404
|
}
|
|
10004
11405
|
function renderProgressLine(p) {
|
|
10005
11406
|
const msg = p.message ? ` ${p.message}` : "";
|
|
@@ -10069,13 +11470,12 @@ function formatPendingPreview(blocks) {
|
|
|
10069
11470
|
const diffLines = formatAllBlockDiffs(blocks);
|
|
10070
11471
|
return [header2, ...diffLines].join("\n");
|
|
10071
11472
|
}
|
|
10072
|
-
function
|
|
10073
|
-
|
|
11473
|
+
function formatUndoRows(results) {
|
|
11474
|
+
return results.map((r) => {
|
|
10074
11475
|
const mark = r.status === "applied" ? "\u2713" : "\u2717";
|
|
10075
11476
|
const detail = r.message ? ` (${r.message})` : "";
|
|
10076
11477
|
return ` ${mark} ${r.path}${detail}`;
|
|
10077
11478
|
});
|
|
10078
|
-
return [`\u25B8 undo: restored ${results.length} file(s) to pre-edit state`, ...lines].join("\n");
|
|
10079
11479
|
}
|
|
10080
11480
|
function describeRepair(repair) {
|
|
10081
11481
|
const parts = [];
|
|
@@ -10086,15 +11486,15 @@ function describeRepair(repair) {
|
|
|
10086
11486
|
}
|
|
10087
11487
|
|
|
10088
11488
|
// src/cli/ui/SessionPicker.tsx
|
|
10089
|
-
import { Box as
|
|
10090
|
-
import
|
|
11489
|
+
import { Box as Box15, Text as Text15 } from "ink";
|
|
11490
|
+
import React16 from "react";
|
|
10091
11491
|
function SessionPicker({
|
|
10092
11492
|
sessionName,
|
|
10093
11493
|
messageCount,
|
|
10094
11494
|
lastActive,
|
|
10095
11495
|
onChoose
|
|
10096
11496
|
}) {
|
|
10097
|
-
return /* @__PURE__ */
|
|
11497
|
+
return /* @__PURE__ */ React16.createElement(Box15, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React16.createElement(Box15, { marginBottom: 1 }, /* @__PURE__ */ React16.createElement(Text15, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, ` \xB7 last active ${relativeTime(lastActive)}`)), /* @__PURE__ */ React16.createElement(
|
|
10098
11498
|
SingleSelect,
|
|
10099
11499
|
{
|
|
10100
11500
|
initialValue: "new",
|
|
@@ -10117,7 +11517,7 @@ function SessionPicker({
|
|
|
10117
11517
|
],
|
|
10118
11518
|
onSubmit: (v) => onChoose(v)
|
|
10119
11519
|
}
|
|
10120
|
-
), /* @__PURE__ */
|
|
11520
|
+
), /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, "\u2191\u2193 to move \xB7 Enter to pick")));
|
|
10121
11521
|
}
|
|
10122
11522
|
function relativeTime(date) {
|
|
10123
11523
|
const ms = Date.now() - date.getTime();
|
|
@@ -10133,12 +11533,12 @@ function relativeTime(date) {
|
|
|
10133
11533
|
}
|
|
10134
11534
|
|
|
10135
11535
|
// src/cli/ui/Setup.tsx
|
|
10136
|
-
import { Box as
|
|
11536
|
+
import { Box as Box16, Text as Text16, useApp as useApp2 } from "ink";
|
|
10137
11537
|
import TextInput from "ink-text-input";
|
|
10138
|
-
import
|
|
11538
|
+
import React17, { useState as useState7 } from "react";
|
|
10139
11539
|
function Setup({ onReady }) {
|
|
10140
|
-
const [value, setValue] =
|
|
10141
|
-
const [error, setError] =
|
|
11540
|
+
const [value, setValue] = useState7("");
|
|
11541
|
+
const [error, setError] = useState7(null);
|
|
10142
11542
|
const { exit } = useApp2();
|
|
10143
11543
|
const handleSubmit = (raw) => {
|
|
10144
11544
|
const trimmed = raw.trim();
|
|
@@ -10159,7 +11559,7 @@ function Setup({ onReady }) {
|
|
|
10159
11559
|
}
|
|
10160
11560
|
onReady(trimmed);
|
|
10161
11561
|
};
|
|
10162
|
-
return /* @__PURE__ */
|
|
11562
|
+
return /* @__PURE__ */ React17.createElement(Box16, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text16, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React17.createElement(
|
|
10163
11563
|
TextInput,
|
|
10164
11564
|
{
|
|
10165
11565
|
value,
|
|
@@ -10168,7 +11568,7 @@ function Setup({ onReady }) {
|
|
|
10168
11568
|
mask: "\u2022",
|
|
10169
11569
|
placeholder: "sk-..."
|
|
10170
11570
|
}
|
|
10171
|
-
)), error ? /* @__PURE__ */
|
|
11571
|
+
)), error ? /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { color: "red" }, error)) : value ? /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "(Type /exit to abort.)")));
|
|
10172
11572
|
}
|
|
10173
11573
|
|
|
10174
11574
|
// src/cli/commands/chat.tsx
|
|
@@ -10181,10 +11581,10 @@ function Root({
|
|
|
10181
11581
|
sessionPreview,
|
|
10182
11582
|
...appProps
|
|
10183
11583
|
}) {
|
|
10184
|
-
const [key, setKey] =
|
|
10185
|
-
const [pending, setPending] =
|
|
11584
|
+
const [key, setKey] = useState8(initialKey);
|
|
11585
|
+
const [pending, setPending] = useState8(sessionPreview);
|
|
10186
11586
|
if (!key) {
|
|
10187
|
-
return /* @__PURE__ */
|
|
11587
|
+
return /* @__PURE__ */ React18.createElement(
|
|
10188
11588
|
Setup,
|
|
10189
11589
|
{
|
|
10190
11590
|
onReady: (k) => {
|
|
@@ -10196,7 +11596,7 @@ function Root({
|
|
|
10196
11596
|
}
|
|
10197
11597
|
process.env.DEEPSEEK_API_KEY = key;
|
|
10198
11598
|
if (pending && appProps.session) {
|
|
10199
|
-
return /* @__PURE__ */
|
|
11599
|
+
return /* @__PURE__ */ React18.createElement(
|
|
10200
11600
|
SessionPicker,
|
|
10201
11601
|
{
|
|
10202
11602
|
sessionName: appProps.session,
|
|
@@ -10211,7 +11611,7 @@ function Root({
|
|
|
10211
11611
|
}
|
|
10212
11612
|
);
|
|
10213
11613
|
}
|
|
10214
|
-
return /* @__PURE__ */
|
|
11614
|
+
return /* @__PURE__ */ React18.createElement(
|
|
10215
11615
|
App,
|
|
10216
11616
|
{
|
|
10217
11617
|
model: appProps.model,
|
|
@@ -10315,7 +11715,7 @@ async function chatCommand(opts) {
|
|
|
10315
11715
|
rewriteSession(opts.session, []);
|
|
10316
11716
|
}
|
|
10317
11717
|
const { waitUntilExit } = render(
|
|
10318
|
-
/* @__PURE__ */
|
|
11718
|
+
/* @__PURE__ */ React18.createElement(
|
|
10319
11719
|
Root,
|
|
10320
11720
|
{
|
|
10321
11721
|
initialKey,
|
|
@@ -10339,13 +11739,14 @@ async function chatCommand(opts) {
|
|
|
10339
11739
|
}
|
|
10340
11740
|
|
|
10341
11741
|
// src/cli/commands/code.tsx
|
|
10342
|
-
import { basename, resolve as
|
|
11742
|
+
import { basename, resolve as resolve7 } from "path";
|
|
10343
11743
|
async function codeCommand(opts = {}) {
|
|
10344
|
-
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-
|
|
10345
|
-
const rootDir =
|
|
11744
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-OVVMCH5F.js");
|
|
11745
|
+
const rootDir = resolve7(opts.dir ?? process.cwd());
|
|
10346
11746
|
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
|
|
10347
11747
|
const tools = new ToolRegistry();
|
|
10348
11748
|
registerFilesystemTools(tools, { rootDir });
|
|
11749
|
+
const jobs = new JobRegistry();
|
|
10349
11750
|
registerShellTools(tools, {
|
|
10350
11751
|
rootDir,
|
|
10351
11752
|
// Per-project "always allow" list persisted from prior ShellConfirm
|
|
@@ -10353,7 +11754,8 @@ async function codeCommand(opts = {}) {
|
|
|
10353
11754
|
// GETTER form — re-read every dispatch so a prefix the user adds
|
|
10354
11755
|
// via ShellConfirm mid-session takes effect on the next shell call
|
|
10355
11756
|
// instead of waiting for `/new` or a relaunch.
|
|
10356
|
-
extraAllowed: () => loadProjectShellAllowed(rootDir)
|
|
11757
|
+
extraAllowed: () => loadProjectShellAllowed(rootDir),
|
|
11758
|
+
jobs
|
|
10357
11759
|
});
|
|
10358
11760
|
registerPlanTool(tools);
|
|
10359
11761
|
registerMemoryTools(tools, { projectRoot: rootDir });
|
|
@@ -10361,6 +11763,12 @@ async function codeCommand(opts = {}) {
|
|
|
10361
11763
|
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
|
|
10362
11764
|
`
|
|
10363
11765
|
);
|
|
11766
|
+
const sigShutdown = () => {
|
|
11767
|
+
void jobs.shutdown();
|
|
11768
|
+
};
|
|
11769
|
+
process.once("SIGINT", sigShutdown);
|
|
11770
|
+
process.once("SIGTERM", sigShutdown);
|
|
11771
|
+
process.once("exit", sigShutdown);
|
|
10364
11772
|
await chatCommand({
|
|
10365
11773
|
model: opts.model ?? "deepseek-v4-pro",
|
|
10366
11774
|
harvest: opts.harvest ?? false,
|
|
@@ -10368,7 +11776,7 @@ async function codeCommand(opts = {}) {
|
|
|
10368
11776
|
transcript: opts.transcript,
|
|
10369
11777
|
session,
|
|
10370
11778
|
seedTools: tools,
|
|
10371
|
-
codeMode: { rootDir },
|
|
11779
|
+
codeMode: { rootDir, jobs },
|
|
10372
11780
|
forceResume: opts.forceResume,
|
|
10373
11781
|
forceNew: opts.forceNew
|
|
10374
11782
|
});
|
|
@@ -10378,34 +11786,34 @@ async function codeCommand(opts = {}) {
|
|
|
10378
11786
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
10379
11787
|
import { basename as basename2 } from "path";
|
|
10380
11788
|
import { render as render2 } from "ink";
|
|
10381
|
-
import
|
|
11789
|
+
import React21 from "react";
|
|
10382
11790
|
|
|
10383
11791
|
// src/cli/ui/DiffApp.tsx
|
|
10384
|
-
import { Box as
|
|
10385
|
-
import
|
|
11792
|
+
import { Box as Box18, Static as Static2, Text as Text18, useApp as useApp3, useInput as useInput6 } from "ink";
|
|
11793
|
+
import React20, { useState as useState9 } from "react";
|
|
10386
11794
|
|
|
10387
11795
|
// src/cli/ui/RecordView.tsx
|
|
10388
|
-
import { Box as
|
|
10389
|
-
import
|
|
11796
|
+
import { Box as Box17, Text as Text17 } from "ink";
|
|
11797
|
+
import React19 from "react";
|
|
10390
11798
|
function RecordView({ rec, compact = false }) {
|
|
10391
11799
|
const toolArgsMax = compact ? 120 : 200;
|
|
10392
11800
|
const toolContentMax = compact ? 200 : 400;
|
|
10393
11801
|
if (rec.role === "user") {
|
|
10394
|
-
return /* @__PURE__ */
|
|
11802
|
+
return /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React19.createElement(Text17, null, rec.content));
|
|
10395
11803
|
}
|
|
10396
11804
|
if (rec.role === "assistant_final") {
|
|
10397
|
-
return /* @__PURE__ */
|
|
11805
|
+
return /* @__PURE__ */ React19.createElement(Box17, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React19.createElement(Box17, null, /* @__PURE__ */ React19.createElement(Text17, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React19.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React19.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React19.createElement(Text17, null, rec.content) : /* @__PURE__ */ React19.createElement(Text17, { dimColor: true, italic: true }, "(tool-call response only)"));
|
|
10398
11806
|
}
|
|
10399
11807
|
if (rec.role === "tool") {
|
|
10400
|
-
return /* @__PURE__ */
|
|
11808
|
+
return /* @__PURE__ */ React19.createElement(Box17, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
|
|
10401
11809
|
}
|
|
10402
11810
|
if (rec.role === "error") {
|
|
10403
|
-
return /* @__PURE__ */
|
|
11811
|
+
return /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React19.createElement(Text17, { color: "red" }, rec.error ?? rec.content));
|
|
10404
11812
|
}
|
|
10405
11813
|
if (rec.role === "done" || rec.role === "assistant_delta") {
|
|
10406
11814
|
return null;
|
|
10407
11815
|
}
|
|
10408
|
-
return /* @__PURE__ */
|
|
11816
|
+
return /* @__PURE__ */ React19.createElement(Box17, null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, "[", rec.role, "] ", rec.content));
|
|
10409
11817
|
}
|
|
10410
11818
|
function CacheBadge({ usage }) {
|
|
10411
11819
|
const hit = usage.prompt_cache_hit_tokens ?? 0;
|
|
@@ -10414,7 +11822,7 @@ function CacheBadge({ usage }) {
|
|
|
10414
11822
|
if (total === 0) return null;
|
|
10415
11823
|
const pct2 = hit / total * 100;
|
|
10416
11824
|
const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
|
|
10417
|
-
return /* @__PURE__ */
|
|
11825
|
+
return /* @__PURE__ */ React19.createElement(Text17, null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React19.createElement(Text17, { color }, pct2.toFixed(1), "%"));
|
|
10418
11826
|
}
|
|
10419
11827
|
function truncate3(s, max) {
|
|
10420
11828
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
@@ -10425,8 +11833,8 @@ function DiffApp({ report }) {
|
|
|
10425
11833
|
const { exit } = useApp3();
|
|
10426
11834
|
const maxIdx = Math.max(0, report.pairs.length - 1);
|
|
10427
11835
|
const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
|
|
10428
|
-
const [idx, setIdx] =
|
|
10429
|
-
|
|
11836
|
+
const [idx, setIdx] = useState9(Math.max(0, initialIdx));
|
|
11837
|
+
useInput6((input, key) => {
|
|
10430
11838
|
if (input === "q" || key.ctrl && input === "c") {
|
|
10431
11839
|
exit();
|
|
10432
11840
|
return;
|
|
@@ -10448,7 +11856,7 @@ function DiffApp({ report }) {
|
|
|
10448
11856
|
}
|
|
10449
11857
|
});
|
|
10450
11858
|
const pair = report.pairs[idx];
|
|
10451
|
-
return /* @__PURE__ */
|
|
11859
|
+
return /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "column" }, /* @__PURE__ */ React20.createElement(DiffHeader, { report }), /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React20.createElement(Text18, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React20.createElement(Text18, null, pair ? /* @__PURE__ */ React20.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React20.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React20.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React20.createElement(Text18, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React20.createElement(Text18, null, pair.divergenceNote)) : null, /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "j"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "k"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "N"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "g"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "q"), " ", "quit")));
|
|
10452
11860
|
}
|
|
10453
11861
|
function DiffHeader({ report }) {
|
|
10454
11862
|
const a = report.a;
|
|
@@ -10466,15 +11874,15 @@ function DiffHeader({ report }) {
|
|
|
10466
11874
|
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
10467
11875
|
prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
|
|
10468
11876
|
}
|
|
10469
|
-
return /* @__PURE__ */
|
|
11877
|
+
return /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React20.createElement(Box18, { justifyContent: "space-between" }, /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React20.createElement(Text18, { color: "blue" }, a.label), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " vs B="), /* @__PURE__ */ React20.createElement(Text18, { color: "magenta" }, b.label)), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, "cache "), /* @__PURE__ */ React20.createElement(Text18, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React20.createElement(Text18, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React20.createElement(Text18, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, "cost "), /* @__PURE__ */ React20.createElement(Text18, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React20.createElement(Text18, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React20.createElement(Text18, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, "model calls "), /* @__PURE__ */ React20.createElement(Text18, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true, italic: true }, prefixLine)) : null);
|
|
10470
11878
|
}
|
|
10471
11879
|
function Pane({
|
|
10472
11880
|
label,
|
|
10473
11881
|
headerColor,
|
|
10474
11882
|
records
|
|
10475
11883
|
}) {
|
|
10476
|
-
return /* @__PURE__ */
|
|
10477
|
-
|
|
11884
|
+
return /* @__PURE__ */ React20.createElement(
|
|
11885
|
+
Box18,
|
|
10478
11886
|
{
|
|
10479
11887
|
flexDirection: "column",
|
|
10480
11888
|
flexGrow: 1,
|
|
@@ -10482,21 +11890,21 @@ function Pane({
|
|
|
10482
11890
|
borderStyle: "single",
|
|
10483
11891
|
borderColor: headerColor
|
|
10484
11892
|
},
|
|
10485
|
-
/* @__PURE__ */
|
|
10486
|
-
records.length === 0 ? /* @__PURE__ */
|
|
11893
|
+
/* @__PURE__ */ React20.createElement(Text18, { color: headerColor, bold: true }, label),
|
|
11894
|
+
records.length === 0 ? /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React20.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React20.createElement(RecordView, { key, rec, compact: true }))
|
|
10487
11895
|
);
|
|
10488
11896
|
}
|
|
10489
11897
|
function KindBadge({ kind }) {
|
|
10490
11898
|
if (kind === "match") {
|
|
10491
|
-
return /* @__PURE__ */
|
|
11899
|
+
return /* @__PURE__ */ React20.createElement(Text18, { color: "green" }, "\u2713 match");
|
|
10492
11900
|
}
|
|
10493
11901
|
if (kind === "diverge") {
|
|
10494
|
-
return /* @__PURE__ */
|
|
11902
|
+
return /* @__PURE__ */ React20.createElement(Text18, { color: "yellow" }, "\u2605 diverge");
|
|
10495
11903
|
}
|
|
10496
11904
|
if (kind === "only_in_a") {
|
|
10497
|
-
return /* @__PURE__ */
|
|
11905
|
+
return /* @__PURE__ */ React20.createElement(Text18, { color: "blue" }, "\u2190 only in A");
|
|
10498
11906
|
}
|
|
10499
|
-
return /* @__PURE__ */
|
|
11907
|
+
return /* @__PURE__ */ React20.createElement(Text18, { color: "magenta" }, "\u2192 only in B");
|
|
10500
11908
|
}
|
|
10501
11909
|
function paneRecords(pair, side) {
|
|
10502
11910
|
if (!pair) return [];
|
|
@@ -10527,7 +11935,7 @@ markdown report written to ${opts.mdPath}`);
|
|
|
10527
11935
|
return;
|
|
10528
11936
|
}
|
|
10529
11937
|
if (wantTui) {
|
|
10530
|
-
const { waitUntilExit } = render2(
|
|
11938
|
+
const { waitUntilExit } = render2(React21.createElement(DiffApp, { report }), {
|
|
10531
11939
|
exitOnCtrlC: true,
|
|
10532
11940
|
patchConsole: false
|
|
10533
11941
|
});
|
|
@@ -10668,16 +12076,16 @@ function pad2(s, width) {
|
|
|
10668
12076
|
|
|
10669
12077
|
// src/cli/commands/replay.ts
|
|
10670
12078
|
import { render as render3 } from "ink";
|
|
10671
|
-
import
|
|
12079
|
+
import React23 from "react";
|
|
10672
12080
|
|
|
10673
12081
|
// src/cli/ui/ReplayApp.tsx
|
|
10674
|
-
import { Box as
|
|
10675
|
-
import
|
|
12082
|
+
import { Box as Box19, Static as Static3, Text as Text19, useApp as useApp4, useInput as useInput7 } from "ink";
|
|
12083
|
+
import React22, { useMemo as useMemo3, useState as useState10 } from "react";
|
|
10676
12084
|
function ReplayApp({ meta, pages }) {
|
|
10677
12085
|
const { exit } = useApp4();
|
|
10678
12086
|
const maxIdx = Math.max(0, pages.length - 1);
|
|
10679
|
-
const [idx, setIdx] =
|
|
10680
|
-
|
|
12087
|
+
const [idx, setIdx] = useState10(maxIdx);
|
|
12088
|
+
useInput7((input, key) => {
|
|
10681
12089
|
if (input === "q" || key.ctrl && input === "c") {
|
|
10682
12090
|
exit();
|
|
10683
12091
|
return;
|
|
@@ -10696,7 +12104,7 @@ function ReplayApp({ meta, pages }) {
|
|
|
10696
12104
|
setIdx(maxIdx);
|
|
10697
12105
|
}
|
|
10698
12106
|
});
|
|
10699
|
-
const cumStats =
|
|
12107
|
+
const cumStats = useMemo3(() => computeCumulativeStats(pages, idx), [pages, idx]);
|
|
10700
12108
|
const summary = {
|
|
10701
12109
|
turns: cumStats.turns,
|
|
10702
12110
|
totalCostUsd: cumStats.totalCostUsd,
|
|
@@ -10711,14 +12119,14 @@ function ReplayApp({ meta, pages }) {
|
|
|
10711
12119
|
const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
|
|
10712
12120
|
const currentPage = pages[idx];
|
|
10713
12121
|
const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
|
|
10714
|
-
return /* @__PURE__ */
|
|
12122
|
+
return /* @__PURE__ */ React22.createElement(Box19, { flexDirection: "column" }, /* @__PURE__ */ React22.createElement(
|
|
10715
12123
|
StatsPanel,
|
|
10716
12124
|
{
|
|
10717
12125
|
summary,
|
|
10718
12126
|
model: cumStats.models[0] ?? meta?.model ?? "?",
|
|
10719
12127
|
prefixHash
|
|
10720
12128
|
}
|
|
10721
|
-
), /* @__PURE__ */
|
|
12129
|
+
), /* @__PURE__ */ React22.createElement(Box19, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React22.createElement(Box19, { justifyContent: "space-between" }, /* @__PURE__ */ React22.createElement(Text19, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React22.createElement(Text19, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React22.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React22.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React22.createElement(Text19, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React22.createElement(Text19, { dimColor: true }, /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "j"), "/", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "k"), "/", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "q"), " quit")));
|
|
10722
12130
|
}
|
|
10723
12131
|
|
|
10724
12132
|
// src/cli/commands/replay.ts
|
|
@@ -10730,7 +12138,7 @@ async function replayCommand(opts) {
|
|
|
10730
12138
|
}
|
|
10731
12139
|
const { parsed } = replayFromFile(opts.path);
|
|
10732
12140
|
const pages = groupRecordsByTurn(parsed.records);
|
|
10733
|
-
const { waitUntilExit } = render3(
|
|
12141
|
+
const { waitUntilExit } = render3(React23.createElement(ReplayApp, { meta: parsed.meta, pages }), {
|
|
10734
12142
|
exitOnCtrlC: true,
|
|
10735
12143
|
patchConsole: false
|
|
10736
12144
|
});
|
|
@@ -11035,12 +12443,12 @@ function truncate4(s, max) {
|
|
|
11035
12443
|
|
|
11036
12444
|
// src/cli/commands/setup.tsx
|
|
11037
12445
|
import { render as render4 } from "ink";
|
|
11038
|
-
import
|
|
12446
|
+
import React25 from "react";
|
|
11039
12447
|
|
|
11040
12448
|
// src/cli/ui/Wizard.tsx
|
|
11041
|
-
import { Box as
|
|
12449
|
+
import { Box as Box20, Text as Text20, useApp as useApp5, useInput as useInput8 } from "ink";
|
|
11042
12450
|
import TextInput2 from "ink-text-input";
|
|
11043
|
-
import
|
|
12451
|
+
import React24, { useState as useState11 } from "react";
|
|
11044
12452
|
|
|
11045
12453
|
// src/cli/ui/presets.ts
|
|
11046
12454
|
var PRESETS = {
|
|
@@ -11072,19 +12480,19 @@ var PRESET_DESCRIPTIONS = {
|
|
|
11072
12480
|
var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
|
|
11073
12481
|
function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
11074
12482
|
const { exit } = useApp5();
|
|
11075
|
-
const [step, setStep] =
|
|
11076
|
-
const [data, setData] =
|
|
12483
|
+
const [step, setStep] = useState11(existingApiKey ? "preset" : "apiKey");
|
|
12484
|
+
const [data, setData] = useState11({
|
|
11077
12485
|
apiKey: existingApiKey ?? "",
|
|
11078
12486
|
preset: initial?.preset ?? "fast",
|
|
11079
12487
|
selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
|
|
11080
12488
|
catalogArgs: {}
|
|
11081
12489
|
});
|
|
11082
|
-
const [error, setError] =
|
|
11083
|
-
|
|
12490
|
+
const [error, setError] = useState11(null);
|
|
12491
|
+
useInput8((_input, key) => {
|
|
11084
12492
|
if (key.escape && step !== "saved" && onCancel) onCancel();
|
|
11085
12493
|
});
|
|
11086
12494
|
if (step === "apiKey") {
|
|
11087
|
-
return /* @__PURE__ */
|
|
12495
|
+
return /* @__PURE__ */ React24.createElement(
|
|
11088
12496
|
ApiKeyStep,
|
|
11089
12497
|
{
|
|
11090
12498
|
onSubmit: (key) => {
|
|
@@ -11098,7 +12506,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
11098
12506
|
);
|
|
11099
12507
|
}
|
|
11100
12508
|
if (step === "preset") {
|
|
11101
|
-
return /* @__PURE__ */
|
|
12509
|
+
return /* @__PURE__ */ React24.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React24.createElement(
|
|
11102
12510
|
SingleSelect,
|
|
11103
12511
|
{
|
|
11104
12512
|
items: presetItems(),
|
|
@@ -11108,10 +12516,10 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
11108
12516
|
setStep("mcp");
|
|
11109
12517
|
}
|
|
11110
12518
|
}
|
|
11111
|
-
), /* @__PURE__ */
|
|
12519
|
+
), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
|
|
11112
12520
|
}
|
|
11113
12521
|
if (step === "mcp") {
|
|
11114
|
-
return /* @__PURE__ */
|
|
12522
|
+
return /* @__PURE__ */ React24.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React24.createElement(
|
|
11115
12523
|
MultiSelect,
|
|
11116
12524
|
{
|
|
11117
12525
|
items: mcpItems(),
|
|
@@ -11136,7 +12544,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
11136
12544
|
}
|
|
11137
12545
|
const currentName = pending[0];
|
|
11138
12546
|
const entry = CATALOG_BY_NAME.get(currentName);
|
|
11139
|
-
return /* @__PURE__ */
|
|
12547
|
+
return /* @__PURE__ */ React24.createElement(
|
|
11140
12548
|
McpArgsStep,
|
|
11141
12549
|
{
|
|
11142
12550
|
entry,
|
|
@@ -11154,7 +12562,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
11154
12562
|
}
|
|
11155
12563
|
if (step === "review") {
|
|
11156
12564
|
const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
|
|
11157
|
-
return /* @__PURE__ */
|
|
12565
|
+
return /* @__PURE__ */ React24.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column" }, /* @__PURE__ */ React24.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React24.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React24.createElement(
|
|
11158
12566
|
SummaryLine,
|
|
11159
12567
|
{
|
|
11160
12568
|
label: "MCP",
|
|
@@ -11162,8 +12570,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
11162
12570
|
}
|
|
11163
12571
|
), specs.map((spec, i) => (
|
|
11164
12572
|
// biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
|
|
11165
|
-
/* @__PURE__ */
|
|
11166
|
-
)), /* @__PURE__ */
|
|
12573
|
+
/* @__PURE__ */ React24.createElement(Box20, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "\xB7 ", spec))
|
|
12574
|
+
)), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { color: "red" }, error)) : null, /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React24.createElement(
|
|
11167
12575
|
ReviewConfirm,
|
|
11168
12576
|
{
|
|
11169
12577
|
onConfirm: () => {
|
|
@@ -11189,15 +12597,15 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
11189
12597
|
}
|
|
11190
12598
|
));
|
|
11191
12599
|
}
|
|
11192
|
-
return /* @__PURE__ */
|
|
12600
|
+
return /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React24.createElement(ExitOnEnter, { onExit: exit }));
|
|
11193
12601
|
}
|
|
11194
12602
|
function ApiKeyStep({
|
|
11195
12603
|
onSubmit,
|
|
11196
12604
|
error,
|
|
11197
12605
|
onError
|
|
11198
12606
|
}) {
|
|
11199
|
-
const [value, setValue] =
|
|
11200
|
-
return /* @__PURE__ */
|
|
12607
|
+
const [value, setValue] = useState11("");
|
|
12608
|
+
return /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React24.createElement(
|
|
11201
12609
|
TextInput2,
|
|
11202
12610
|
{
|
|
11203
12611
|
value,
|
|
@@ -11214,7 +12622,7 @@ function ApiKeyStep({
|
|
|
11214
12622
|
mask: "\u2022",
|
|
11215
12623
|
placeholder: "sk-..."
|
|
11216
12624
|
}
|
|
11217
|
-
)), error ? /* @__PURE__ */
|
|
12625
|
+
)), error ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { color: "red" }, error)) : value ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "preview: ", redactKey(value))) : null);
|
|
11218
12626
|
}
|
|
11219
12627
|
function McpArgsStep({
|
|
11220
12628
|
entry,
|
|
@@ -11222,8 +12630,8 @@ function McpArgsStep({
|
|
|
11222
12630
|
onSubmit,
|
|
11223
12631
|
onError
|
|
11224
12632
|
}) {
|
|
11225
|
-
const [value, setValue] =
|
|
11226
|
-
return /* @__PURE__ */
|
|
12633
|
+
const [value, setValue] = useState11("");
|
|
12634
|
+
return /* @__PURE__ */ React24.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column" }, /* @__PURE__ */ React24.createElement(Text20, null, entry.summary), entry.note ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Required parameter: "), /* @__PURE__ */ React24.createElement(Text20, { bold: true }, entry.userArgs)), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React24.createElement(
|
|
11227
12635
|
TextInput2,
|
|
11228
12636
|
{
|
|
11229
12637
|
value,
|
|
@@ -11239,16 +12647,16 @@ function McpArgsStep({
|
|
|
11239
12647
|
},
|
|
11240
12648
|
placeholder: placeholderFor(entry)
|
|
11241
12649
|
}
|
|
11242
|
-
)), error ? /* @__PURE__ */
|
|
12650
|
+
)), error ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { color: "red" }, error)) : null));
|
|
11243
12651
|
}
|
|
11244
12652
|
function ReviewConfirm({ onConfirm }) {
|
|
11245
|
-
|
|
12653
|
+
useInput8((_i, key) => {
|
|
11246
12654
|
if (key.return) onConfirm();
|
|
11247
12655
|
});
|
|
11248
12656
|
return null;
|
|
11249
12657
|
}
|
|
11250
12658
|
function ExitOnEnter({ onExit }) {
|
|
11251
|
-
|
|
12659
|
+
useInput8((_i, key) => {
|
|
11252
12660
|
if (key.return) onExit();
|
|
11253
12661
|
});
|
|
11254
12662
|
return null;
|
|
@@ -11259,10 +12667,10 @@ function StepFrame({
|
|
|
11259
12667
|
total,
|
|
11260
12668
|
children
|
|
11261
12669
|
}) {
|
|
11262
|
-
return /* @__PURE__ */
|
|
12670
|
+
return /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box20, null, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1, flexDirection: "column" }, children));
|
|
11263
12671
|
}
|
|
11264
12672
|
function SummaryLine({ label, value }) {
|
|
11265
|
-
return /* @__PURE__ */
|
|
12673
|
+
return /* @__PURE__ */ React24.createElement(Box20, null, /* @__PURE__ */ React24.createElement(Text20, null, label.padEnd(12)), /* @__PURE__ */ React24.createElement(Text20, { bold: true }, value));
|
|
11266
12674
|
}
|
|
11267
12675
|
function presetItems() {
|
|
11268
12676
|
return ["fast", "smart", "max"].map((name) => ({
|
|
@@ -11318,7 +12726,7 @@ async function setupCommand(_opts = {}) {
|
|
|
11318
12726
|
const existingKey = loadApiKey();
|
|
11319
12727
|
const existing = readConfig();
|
|
11320
12728
|
const { waitUntilExit, unmount } = render4(
|
|
11321
|
-
/* @__PURE__ */
|
|
12729
|
+
/* @__PURE__ */ React25.createElement(
|
|
11322
12730
|
Wizard,
|
|
11323
12731
|
{
|
|
11324
12732
|
existingApiKey: existingKey,
|
|
@@ -11336,7 +12744,7 @@ async function setupCommand(_opts = {}) {
|
|
|
11336
12744
|
}
|
|
11337
12745
|
|
|
11338
12746
|
// src/cli/commands/update.ts
|
|
11339
|
-
import { spawn as
|
|
12747
|
+
import { spawn as spawn5 } from "child_process";
|
|
11340
12748
|
function planUpdate(input) {
|
|
11341
12749
|
const diff = compareVersions(input.current, input.latest);
|
|
11342
12750
|
if (diff > 0) {
|
|
@@ -11366,13 +12774,13 @@ function planUpdate(input) {
|
|
|
11366
12774
|
};
|
|
11367
12775
|
}
|
|
11368
12776
|
function defaultSpawn(argv) {
|
|
11369
|
-
return new Promise((
|
|
11370
|
-
const child =
|
|
12777
|
+
return new Promise((resolve8, reject) => {
|
|
12778
|
+
const child = spawn5(argv[0], argv.slice(1), {
|
|
11371
12779
|
stdio: "inherit",
|
|
11372
12780
|
shell: process.platform === "win32"
|
|
11373
12781
|
});
|
|
11374
12782
|
child.once("error", reject);
|
|
11375
|
-
child.once("exit", (code) =>
|
|
12783
|
+
child.once("exit", (code) => resolve8(code ?? 1));
|
|
11376
12784
|
});
|
|
11377
12785
|
}
|
|
11378
12786
|
async function updateCommand(opts = {}) {
|