@lazyingart/agintiflow 0.12.4 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/agent-runtime-pipe.md +2 -0
- package/package.json +1 -1
- package/scripts/smoke-cli-chat.js +17 -3
- package/scripts/smoke-inbox.js +9 -5
- package/src/agent-runner.js +4 -0
- package/src/interactive-cli.js +390 -22
- package/src/session-store.js +25 -5
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ aginti
|
|
|
67
67
|
aginti chat
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/instructions` to inspect `AGINTI.md`, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. Assistant responses start on a fresh line after the `aginti>` header and render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, and tables. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
|
|
70
|
+
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/instructions` to inspect `AGINTI.md`, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. During an active run, Enter sends the draft as an ASAP pipe message (`→`) and Tab queues it for after the run (`↳`); ASAP messages are consumed first, Alt+Up edits the last piped message, and Shift+Left edits the last after-finish queued message. The input panel always shows the current `cwd` footer. Assistant responses start on a fresh line after the `aginti>` header with a colored response gutter and render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, and tables. Resuming a session prints a compact recent-chat preview before the prompt. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
|
|
71
71
|
|
|
72
72
|
`aginti init` creates `AGINTI.md` at the project root. This is the editable project-instruction file for both CLI and web runs, similar to `AGENTS.md` or project memory in other agents. Keep durable preferences, commands, and constraints there, but never secrets. You can edit it manually or ask in chat, for example: `update AGINTI.md to remember that this project uses pytest and npm run check`.
|
|
73
73
|
|
|
@@ -365,7 +365,7 @@ Package policy values:
|
|
|
365
365
|
|
|
366
366
|
Toolchain commands such as `python3 plot.py`, `latexmk -pdf paper.tex`, and `pdflatex -interaction=nonstopmode -halt-on-error paper.tex` are allowlisted only when the shell tool is enabled. In Docker mode the project folder is mounted as `/workspace`; any file written to `/workspace/report.pdf` appears on the host as `<your-project>/report.pdf`. CLI runs print both the host workspace and the Docker mapping before execution. File and canvas tools accept both normal relative paths and Docker virtual paths like `/workspace/report.pdf`, while other absolute host paths remain blocked.
|
|
367
367
|
|
|
368
|
-
The web chat mirrors the CLI session store. Enter sends, Ctrl+J inserts a newline, Tab submits/queues the message, and Esc stops the active run.
|
|
368
|
+
The web chat mirrors the CLI session store. Enter sends, Ctrl+J inserts a newline, Tab submits/queues the message, and Esc stops the active run. CLI live input has two pending lanes: ASAP pipe messages (`→`) are written to `.sessions/<session-id>/inbox.jsonl` and drained before normal queued items at safe runner boundaries, while after-finish messages (`↳`) run as the next prompt once the current task fully finishes. Generated local sites should use the built-in `preview_workspace` or `open_workspace_file` tools; AgInTiFlow avoids transient localhost servers inside Docker because those containers stop between commands and their ports are not host-published.
|
|
369
369
|
|
|
370
370
|
Safe preflight endpoints:
|
|
371
371
|
|
|
@@ -9,6 +9,8 @@ AgInTiFlow keeps CLI and web runs equivalent by using the project folder as the
|
|
|
9
9
|
|
|
10
10
|
When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
|
|
11
11
|
|
|
12
|
+
The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. The current command cwd is rendered below the input panel in both idle and running states.
|
|
13
|
+
|
|
12
14
|
Runs can be stopped without corrupting session state. The CLI listens for Esc or Ctrl+C during an active run, and the web UI exposes a Stop button plus Esc. Both paths send an abort signal, persist `session.stopped`, and leave the session resumable through `aginti resume <session-id>`.
|
|
13
15
|
|
|
14
16
|
Default execution is Docker workspace mode with package installs approved inside the sandbox. The project is mounted at `/workspace`; persistent agent toolchain folders are mounted at `/aginti-home`, `/aginti-cache`, and `/aginti-env` from `~/.agintiflow/docker/`. Python, conda, and other language-level environments should be installed under `/aginti-env` so they survive across runs. Apt/apk package changes are ephemeral unless the Docker image is rebuilt.
|
package/package.json
CHANGED
|
@@ -84,6 +84,17 @@ try {
|
|
|
84
84
|
if (hugePromptLayout.renderedRows.length > 12 || !hugePromptLayout.renderedRows.some((line) => line.includes("earlier input row"))) {
|
|
85
85
|
throw new Error("terminal prompt layout did not bound redraw size for large prompts");
|
|
86
86
|
}
|
|
87
|
+
const queuedPromptLayout = buildPromptLayout("follow up", 9, 90, 24, {
|
|
88
|
+
commandCwd: "/tmp/aginti-project",
|
|
89
|
+
pendingAsap: [{ content: "apply this to the running task" }],
|
|
90
|
+
pendingQueued: [{ content: "run this after the current task" }],
|
|
91
|
+
});
|
|
92
|
+
const queuedText = queuedPromptLayout.renderedRows
|
|
93
|
+
.map((line) => line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, ""))
|
|
94
|
+
.join("\n");
|
|
95
|
+
if (!queuedText.includes("→ apply this") || !queuedText.includes("↳ run this") || !queuedText.includes("cwd /tmp/aginti-project")) {
|
|
96
|
+
throw new Error("terminal prompt layout did not render live input queue and cwd footer");
|
|
97
|
+
}
|
|
87
98
|
|
|
88
99
|
await runCli(["init"], "");
|
|
89
100
|
const instructions = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
|
|
@@ -111,21 +122,24 @@ try {
|
|
|
111
122
|
if (!result.stdout.includes("status=running workingOn=") || !result.stdout.includes("status=idle session=")) {
|
|
112
123
|
throw new Error("interactive chat did not print simple run status updates");
|
|
113
124
|
}
|
|
114
|
-
if (!/aginti>\s
|
|
115
|
-
throw new Error("assistant response did not
|
|
125
|
+
if (!/aginti>\s*\r?\n\s*\|\s+Mock run complete\./.test(result.stdout)) {
|
|
126
|
+
throw new Error("assistant response did not render with a fresh-line response gutter");
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
const latest = await runCli(["resume"], "/exit\n");
|
|
119
130
|
if (!latest.stdout.includes("session=") || !latest.stdout.includes("Interactive agent chat")) {
|
|
120
131
|
throw new Error("bare aginti resume did not open the latest session interactively");
|
|
121
132
|
}
|
|
133
|
+
if (!latest.stdout.includes("resume history") || !latest.stdout.includes("Mock run complete")) {
|
|
134
|
+
throw new Error("bare aginti resume did not preview saved chat history");
|
|
135
|
+
}
|
|
122
136
|
|
|
123
137
|
console.log(
|
|
124
138
|
JSON.stringify(
|
|
125
139
|
{
|
|
126
140
|
ok: true,
|
|
127
141
|
projectRoot: tempRoot,
|
|
128
|
-
checks: ["markdown-render", "prompt-layout", "agent-response-
|
|
142
|
+
checks: ["markdown-render", "prompt-layout", "live-input-layout", "agent-response-gutter", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest", "resume-history-preview"],
|
|
129
143
|
},
|
|
130
144
|
null,
|
|
131
145
|
2
|
package/scripts/smoke-inbox.js
CHANGED
|
@@ -12,20 +12,24 @@ function assert(condition, message) {
|
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
14
|
const store = new SessionStore(path.join(tempRoot, ".sessions"), "inbox-smoke");
|
|
15
|
-
await store.appendInbox("first queued message", { source: "test" });
|
|
15
|
+
const first = await store.appendInbox("first queued message", { source: "test" });
|
|
16
16
|
await store.appendInbox("second queued message", { source: "test" });
|
|
17
|
+
await store.appendInbox("urgent piped message", { source: "test", priority: "asap" });
|
|
18
|
+
|
|
19
|
+
assert(first?.id, "appendInbox did not return an inbox id");
|
|
17
20
|
|
|
18
21
|
const drained = await store.drainInbox();
|
|
19
|
-
assert(drained.length ===
|
|
20
|
-
assert(drained[0].content === "
|
|
21
|
-
assert(drained[1].content === "
|
|
22
|
+
assert(drained.length === 3, "inbox did not drain all messages");
|
|
23
|
+
assert(drained[0].content === "urgent piped message", "asap message was not drained first");
|
|
24
|
+
assert(drained[1].content === "first queued message", "first queued message mismatch");
|
|
25
|
+
assert(drained[2].content === "second queued message", "second queued message mismatch");
|
|
22
26
|
assert((await store.drainInbox()).length === 0, "inbox was not cleared after drain");
|
|
23
27
|
|
|
24
28
|
console.log(
|
|
25
29
|
JSON.stringify(
|
|
26
30
|
{
|
|
27
31
|
ok: true,
|
|
28
|
-
checks: ["session-inbox-append", "session-inbox-drain"],
|
|
32
|
+
checks: ["session-inbox-append", "session-inbox-drain", "session-inbox-asap-priority"],
|
|
29
33
|
},
|
|
30
34
|
null,
|
|
31
35
|
2
|
package/src/agent-runner.js
CHANGED
|
@@ -727,12 +727,16 @@ async function injectQueuedUserMessages(store, state, observers) {
|
|
|
727
727
|
content: `Additional user message received while this run was active:\n${content}`,
|
|
728
728
|
});
|
|
729
729
|
await store.appendEvent("conversation.queued_input_applied", {
|
|
730
|
+
id: item.id || "",
|
|
730
731
|
prompt: content,
|
|
731
732
|
source: item.source || "inbox",
|
|
733
|
+
priority: item.priority || "normal",
|
|
732
734
|
});
|
|
733
735
|
observers.event("conversation.queued_input_applied", {
|
|
736
|
+
id: item.id || "",
|
|
734
737
|
prompt: content,
|
|
735
738
|
source: item.source || "inbox",
|
|
739
|
+
priority: item.priority || "normal",
|
|
736
740
|
});
|
|
737
741
|
}
|
|
738
742
|
}
|
package/src/interactive-cli.js
CHANGED
|
@@ -3,11 +3,12 @@ import { emitKeypressEvents } from "node:readline";
|
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
4
|
import { runAgent } from "./agent-runner.js";
|
|
5
5
|
import { loadConfig } from "./config.js";
|
|
6
|
-
import { initProject, listProjectSessions, providerKeyStatus, readProjectInstructions, setProviderKey } from "./project.js";
|
|
6
|
+
import { initProject, listProjectSessions, projectPaths, providerKeyStatus, readProjectInstructions, setProviderKey } from "./project.js";
|
|
7
7
|
import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
|
|
8
8
|
import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
|
|
9
9
|
import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
|
|
10
10
|
import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
|
|
11
|
+
import { SessionStore } from "./session-store.js";
|
|
11
12
|
|
|
12
13
|
const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
|
|
13
14
|
const ansi = {
|
|
@@ -26,6 +27,7 @@ const ansi = {
|
|
|
26
27
|
cursorShow: "\x1b[?25h",
|
|
27
28
|
userBg: "\x1b[48;5;24m\x1b[38;5;231m",
|
|
28
29
|
agentBg: "\x1b[48;5;29m\x1b[38;5;231m",
|
|
30
|
+
responseBg: "\x1b[48;5;25m\x1b[38;5;231m",
|
|
29
31
|
systemBg: "\x1b[48;5;236m\x1b[38;5;245m",
|
|
30
32
|
};
|
|
31
33
|
const brandPalette = ["\x1b[38;5;45m", "\x1b[38;5;81m", "\x1b[38;5;86m", "\x1b[38;5;118m", "\x1b[38;5;226m"];
|
|
@@ -56,6 +58,7 @@ const SLASH_COMMANDS = [
|
|
|
56
58
|
"/exit",
|
|
57
59
|
];
|
|
58
60
|
const promptHistory = [];
|
|
61
|
+
let activeRunInput = null;
|
|
59
62
|
|
|
60
63
|
function color(value, ...codes) {
|
|
61
64
|
if (!useColor || codes.length === 0) return String(value);
|
|
@@ -70,6 +73,14 @@ function label(name, bgCode) {
|
|
|
70
73
|
return color(` ${name} `, bgCode, ansi.bold);
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
function outputLine(line = "") {
|
|
77
|
+
if (activeRunInput) {
|
|
78
|
+
activeRunInput.printLine(String(line));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(line);
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
function terminalWidth() {
|
|
74
85
|
return Math.max(Number(output.columns) || 80, 40);
|
|
75
86
|
}
|
|
@@ -133,6 +144,11 @@ function clamp(value, min, max) {
|
|
|
133
144
|
return Math.min(Math.max(value, min), max);
|
|
134
145
|
}
|
|
135
146
|
|
|
147
|
+
function compactLine(value = "", limit = 96) {
|
|
148
|
+
const text = stripAnsi(String(value || "").replace(/\s+/g, " ").trim());
|
|
149
|
+
return text.length <= limit ? text : `${text.slice(0, Math.max(limit - 1, 1))}…`;
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
export function stripMarkdown(text) {
|
|
137
153
|
const lines = String(text || "").split(/\r?\n/);
|
|
138
154
|
let inFence = false;
|
|
@@ -260,34 +276,38 @@ function rolePrefix(name, bgCode) {
|
|
|
260
276
|
return `${label(name, bgCode)} ${color("|", bgCode)} `;
|
|
261
277
|
}
|
|
262
278
|
|
|
279
|
+
function responsePrefix() {
|
|
280
|
+
return `${color(" | ", ansi.responseBg)} `;
|
|
281
|
+
}
|
|
282
|
+
|
|
263
283
|
function printWrapped(prefix, text, { stripCode = "" } = {}) {
|
|
264
284
|
const rendered = stripMarkdown(text);
|
|
265
285
|
const lines = rendered.split("\n");
|
|
266
286
|
const visible = stripAnsi(prefix).length;
|
|
267
287
|
const gutter = `${" ".repeat(Math.max(visible - 2, 0))}${stripCode ? color("|", stripCode) : "|"} `;
|
|
268
|
-
|
|
288
|
+
outputLine(`${prefix}${lines[0] || ""}`);
|
|
269
289
|
for (const line of lines.slice(1)) {
|
|
270
|
-
|
|
290
|
+
outputLine(`${gutter}${line}`);
|
|
271
291
|
}
|
|
272
292
|
}
|
|
273
293
|
|
|
274
294
|
function printAgentMessage(text) {
|
|
275
|
-
|
|
295
|
+
outputLine(label("aginti>", ansi.agentBg).trimEnd());
|
|
276
296
|
const rendered = stripMarkdown(text);
|
|
277
297
|
const lines = rendered.split("\n");
|
|
278
|
-
for (const line of lines)
|
|
298
|
+
for (const line of lines) outputLine(`${responsePrefix()}${line}`);
|
|
279
299
|
}
|
|
280
300
|
|
|
281
301
|
function printSystemLine(text) {
|
|
282
302
|
if (!String(text || "").trim()) {
|
|
283
|
-
|
|
303
|
+
outputLine("");
|
|
284
304
|
return;
|
|
285
305
|
}
|
|
286
|
-
|
|
306
|
+
outputLine(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
|
|
287
307
|
}
|
|
288
308
|
|
|
289
309
|
function printHeading(text) {
|
|
290
|
-
|
|
310
|
+
outputLine(color(stripMarkdown(text), ansi.bold, ansi.cyan));
|
|
291
311
|
}
|
|
292
312
|
|
|
293
313
|
function shimmerText(text, frame) {
|
|
@@ -364,7 +384,9 @@ function printHelp() {
|
|
|
364
384
|
"",
|
|
365
385
|
"Type a normal request to run the agent. Example: write a Python CLI app with tests",
|
|
366
386
|
"Type / then Tab to autocomplete commands.",
|
|
367
|
-
"While a run is active,
|
|
387
|
+
"While a run is active, Enter pipes a message into the current run (→), Tab queues it after finish (↳).",
|
|
388
|
+
"Alt+Up edits the last piped message; Shift+Left edits the last queued message.",
|
|
389
|
+
"Esc or Ctrl+C stops gracefully and prints a resume command.",
|
|
368
390
|
].join("\n")
|
|
369
391
|
);
|
|
370
392
|
}
|
|
@@ -401,7 +423,7 @@ function promptVisibleWindow(rows, cursorRow, height = terminalHeight()) {
|
|
|
401
423
|
};
|
|
402
424
|
}
|
|
403
425
|
|
|
404
|
-
export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight()) {
|
|
426
|
+
export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight(), options = {}) {
|
|
405
427
|
const safeBuffer = String(buffer || "");
|
|
406
428
|
const safeCursor = clamp(Number(cursor) || 0, 0, safeBuffer.length);
|
|
407
429
|
const lineWidth = editorWidth(width);
|
|
@@ -497,6 +519,17 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
|
|
|
497
519
|
const renderedRows = [];
|
|
498
520
|
let renderedCursorRow = cursorRow - visible.start;
|
|
499
521
|
|
|
522
|
+
const pendingAsap = Array.isArray(options.pendingAsap) ? options.pendingAsap : [];
|
|
523
|
+
const pendingQueued = Array.isArray(options.pendingQueued) ? options.pendingQueued : [];
|
|
524
|
+
for (const item of pendingAsap.slice(-4)) {
|
|
525
|
+
renderedRows.push(panelLine(` → ${compactLine(item.content || item, lineWidth - 6)}`, ansi.systemBg, lineWidth));
|
|
526
|
+
renderedCursorRow += 1;
|
|
527
|
+
}
|
|
528
|
+
for (const item of pendingQueued.slice(-4)) {
|
|
529
|
+
renderedRows.push(panelLine(` ↳ ${compactLine(item.content || item, lineWidth - 6)}`, ansi.systemBg, lineWidth));
|
|
530
|
+
renderedCursorRow += 1;
|
|
531
|
+
}
|
|
532
|
+
|
|
500
533
|
if (visible.topHidden > 0) {
|
|
501
534
|
renderedRows.push(panelLine(` ... ${visible.topHidden} earlier input row${visible.topHidden === 1 ? "" : "s"}`, ansi.systemBg, lineWidth));
|
|
502
535
|
renderedCursorRow += 1;
|
|
@@ -514,6 +547,9 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
|
|
|
514
547
|
if (suggestions.length > 0) {
|
|
515
548
|
renderedRows.push(panelLine(` hint ${suggestions.join(" ")}`, ansi.systemBg, lineWidth));
|
|
516
549
|
}
|
|
550
|
+
if (options.commandCwd) {
|
|
551
|
+
renderedRows.push(panelLine(` cwd ${options.commandCwd}`, ansi.systemBg, lineWidth));
|
|
552
|
+
}
|
|
517
553
|
|
|
518
554
|
return {
|
|
519
555
|
rows,
|
|
@@ -558,8 +594,8 @@ function moveFromPromptCursorToTop(previous) {
|
|
|
558
594
|
output.write("\r");
|
|
559
595
|
}
|
|
560
596
|
|
|
561
|
-
function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0, renderedRows: [] }) {
|
|
562
|
-
const layout = buildPromptLayout(buffer, cursor);
|
|
597
|
+
function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0, renderedRows: [] }, options = {}) {
|
|
598
|
+
const layout = buildPromptLayout(buffer, cursor, terminalWidth(), terminalHeight(), options);
|
|
563
599
|
const sameShape = previous.lineCount === layout.renderedRows.length && previous.renderedRows?.length === layout.renderedRows.length;
|
|
564
600
|
const changedRows = sameShape
|
|
565
601
|
? layout.renderedRows
|
|
@@ -637,7 +673,7 @@ function createAbortError(message = "Aborted with Ctrl+C") {
|
|
|
637
673
|
return error;
|
|
638
674
|
}
|
|
639
675
|
|
|
640
|
-
function readTtyPrompt() {
|
|
676
|
+
function readTtyPrompt(options = {}) {
|
|
641
677
|
return new Promise((resolve, reject) => {
|
|
642
678
|
emitKeypressEvents(input);
|
|
643
679
|
const wasRaw = Boolean(input.isRaw);
|
|
@@ -665,14 +701,14 @@ function readTtyPrompt() {
|
|
|
665
701
|
clearImmediate(redrawHandle);
|
|
666
702
|
redrawHandle = null;
|
|
667
703
|
}
|
|
668
|
-
rendered = renderPromptBuffer(buffer, cursor, rendered);
|
|
704
|
+
rendered = renderPromptBuffer(buffer, cursor, rendered, options);
|
|
669
705
|
};
|
|
670
706
|
|
|
671
707
|
const redraw = () => {
|
|
672
708
|
if (redrawHandle) return;
|
|
673
709
|
redrawHandle = setImmediate(() => {
|
|
674
710
|
redrawHandle = null;
|
|
675
|
-
rendered = renderPromptBuffer(buffer, cursor, rendered);
|
|
711
|
+
rendered = renderPromptBuffer(buffer, cursor, rendered, options);
|
|
676
712
|
});
|
|
677
713
|
};
|
|
678
714
|
|
|
@@ -832,13 +868,294 @@ function readTtyPrompt() {
|
|
|
832
868
|
});
|
|
833
869
|
}
|
|
834
870
|
|
|
835
|
-
async function readPromptAnswer(rl) {
|
|
871
|
+
async function readPromptAnswer(rl, state = {}) {
|
|
836
872
|
if (input.isTTY && output.isTTY && typeof input.setRawMode === "function") {
|
|
837
|
-
return readTtyPrompt();
|
|
873
|
+
return readTtyPrompt({ commandCwd: state.commandCwd || process.cwd() });
|
|
838
874
|
}
|
|
839
875
|
return rl.question(userPrompt());
|
|
840
876
|
}
|
|
841
877
|
|
|
878
|
+
class LiveRunInput {
|
|
879
|
+
constructor({ state, store, controller }) {
|
|
880
|
+
this.state = state;
|
|
881
|
+
this.store = store;
|
|
882
|
+
this.controller = controller;
|
|
883
|
+
this.buffer = "";
|
|
884
|
+
this.cursor = 0;
|
|
885
|
+
this.rendered = { lineCount: 0, cursorRow: 0, renderedRows: [] };
|
|
886
|
+
this.redrawHandle = null;
|
|
887
|
+
this.preferredColumn = null;
|
|
888
|
+
this.pendingAsap = [];
|
|
889
|
+
this.pendingQueued = [];
|
|
890
|
+
this.wasRaw = Boolean(input.isRaw);
|
|
891
|
+
this.started = false;
|
|
892
|
+
this.handler = this.handleKey.bind(this);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
get commandCwd() {
|
|
896
|
+
return this.state.commandCwd || process.cwd();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
start() {
|
|
900
|
+
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return false;
|
|
901
|
+
emitKeypressEvents(input);
|
|
902
|
+
input.resume();
|
|
903
|
+
input.setRawMode(true);
|
|
904
|
+
input.on("keypress", this.handler);
|
|
905
|
+
activeRunInput = this;
|
|
906
|
+
this.started = true;
|
|
907
|
+
this.renderNow();
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async stop() {
|
|
912
|
+
if (!this.started) return [];
|
|
913
|
+
if (this.redrawHandle) {
|
|
914
|
+
clearImmediate(this.redrawHandle);
|
|
915
|
+
this.redrawHandle = null;
|
|
916
|
+
}
|
|
917
|
+
input.off("keypress", this.handler);
|
|
918
|
+
if (typeof input.setRawMode === "function") input.setRawMode(this.wasRaw);
|
|
919
|
+
this.clearForExternalOutput();
|
|
920
|
+
output.write(ansi.cursorShow);
|
|
921
|
+
if (activeRunInput === this) activeRunInput = null;
|
|
922
|
+
this.started = false;
|
|
923
|
+
|
|
924
|
+
const unappliedAsap = [...this.pendingAsap];
|
|
925
|
+
if (unappliedAsap.length > 0) {
|
|
926
|
+
await this.removeInboxItems(unappliedAsap.map((item) => item.id));
|
|
927
|
+
}
|
|
928
|
+
return [
|
|
929
|
+
...unappliedAsap.map((item) => ({ ...item, kind: "asap" })),
|
|
930
|
+
...this.pendingQueued.map((item) => ({ ...item, kind: "queued" })),
|
|
931
|
+
];
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
printLine(line = "") {
|
|
935
|
+
this.clearForExternalOutput();
|
|
936
|
+
output.write(`${line}\n`);
|
|
937
|
+
this.renderNow();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
clearForExternalOutput() {
|
|
941
|
+
if (!this.rendered.lineCount) return;
|
|
942
|
+
output.write(ansi.cursorHide);
|
|
943
|
+
clearRenderedPrompt(this.rendered);
|
|
944
|
+
this.rendered = { lineCount: 0, cursorRow: 0, renderedRows: [] };
|
|
945
|
+
output.write(ansi.cursorShow);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
renderNow() {
|
|
949
|
+
if (this.redrawHandle) {
|
|
950
|
+
clearImmediate(this.redrawHandle);
|
|
951
|
+
this.redrawHandle = null;
|
|
952
|
+
}
|
|
953
|
+
this.rendered = renderPromptBuffer(this.buffer, this.cursor, this.rendered, {
|
|
954
|
+
commandCwd: this.commandCwd,
|
|
955
|
+
pendingAsap: this.pendingAsap,
|
|
956
|
+
pendingQueued: this.pendingQueued,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
redraw() {
|
|
961
|
+
if (this.redrawHandle) return;
|
|
962
|
+
this.redrawHandle = setImmediate(() => {
|
|
963
|
+
this.redrawHandle = null;
|
|
964
|
+
this.renderNow();
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
setBuffer(nextBuffer, nextCursor = nextBuffer.length) {
|
|
969
|
+
this.buffer = String(nextBuffer || "");
|
|
970
|
+
this.cursor = clamp(nextCursor, 0, this.buffer.length);
|
|
971
|
+
this.preferredColumn = null;
|
|
972
|
+
this.redraw();
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
moveVertical(direction) {
|
|
976
|
+
const layout = buildPromptLayout(this.buffer, this.cursor, terminalWidth(), terminalHeight(), {
|
|
977
|
+
commandCwd: this.commandCwd,
|
|
978
|
+
pendingAsap: this.pendingAsap,
|
|
979
|
+
pendingQueued: this.pendingQueued,
|
|
980
|
+
});
|
|
981
|
+
const location = cursorLocation(layout, this.cursor);
|
|
982
|
+
const targetRowIndex = location.rowIndex + direction;
|
|
983
|
+
if (targetRowIndex < 0 || targetRowIndex >= layout.rows.length) return;
|
|
984
|
+
const currentColumn = this.preferredColumn ?? location.column;
|
|
985
|
+
const targetRow = layout.rows[targetRowIndex];
|
|
986
|
+
this.cursor = targetRow.start + Math.min(currentColumn, targetRow.end - targetRow.start);
|
|
987
|
+
this.preferredColumn = currentColumn;
|
|
988
|
+
this.redraw();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async removeInboxItems(ids = []) {
|
|
992
|
+
if (ids.length === 0) return;
|
|
993
|
+
const idSet = new Set(ids);
|
|
994
|
+
const inbox = await this.store.loadInbox().catch(() => []);
|
|
995
|
+
await this.store.saveInbox(inbox.filter((item) => !idSet.has(item.id))).catch(() => {});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async submitAsap() {
|
|
999
|
+
const content = this.buffer.trim();
|
|
1000
|
+
if (!content) return;
|
|
1001
|
+
const item = {
|
|
1002
|
+
id: `cli-asap-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
1003
|
+
content,
|
|
1004
|
+
priority: "asap",
|
|
1005
|
+
source: "cli-live",
|
|
1006
|
+
};
|
|
1007
|
+
this.pendingAsap.push(item);
|
|
1008
|
+
this.setBuffer("");
|
|
1009
|
+
await this.store.appendInbox(content, item).catch((error) => {
|
|
1010
|
+
this.pendingAsap = this.pendingAsap.filter((pending) => pending.id !== item.id);
|
|
1011
|
+
this.printLine(`${label("error", ansi.systemBg)} failed to pipe message: ${error.message}`);
|
|
1012
|
+
});
|
|
1013
|
+
await this.store.appendEvent("conversation.piped_input", {
|
|
1014
|
+
id: item.id,
|
|
1015
|
+
prompt: content,
|
|
1016
|
+
source: item.source,
|
|
1017
|
+
priority: item.priority,
|
|
1018
|
+
}).catch(() => {});
|
|
1019
|
+
this.redraw();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
queueAfterFinish() {
|
|
1023
|
+
const content = this.buffer.trim();
|
|
1024
|
+
if (!content) return;
|
|
1025
|
+
this.pendingQueued.push({
|
|
1026
|
+
id: `cli-queued-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
1027
|
+
content,
|
|
1028
|
+
priority: "after-finish",
|
|
1029
|
+
source: "cli-live",
|
|
1030
|
+
});
|
|
1031
|
+
this.setBuffer("");
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
editLastQueued() {
|
|
1035
|
+
const item = this.pendingQueued.pop();
|
|
1036
|
+
if (!item) return;
|
|
1037
|
+
this.setBuffer(item.content);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
editLastAsap() {
|
|
1041
|
+
const item = this.pendingAsap.pop();
|
|
1042
|
+
if (!item) return;
|
|
1043
|
+
void this.removeInboxItems([item.id]);
|
|
1044
|
+
this.setBuffer(item.content);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
markApplied(data = {}) {
|
|
1048
|
+
const id = data.id || "";
|
|
1049
|
+
const prompt = String(data.prompt || "");
|
|
1050
|
+
if (id) {
|
|
1051
|
+
this.pendingAsap = this.pendingAsap.filter((item) => item.id !== id);
|
|
1052
|
+
} else if (prompt) {
|
|
1053
|
+
const index = this.pendingAsap.findIndex((item) => item.content === prompt);
|
|
1054
|
+
if (index >= 0) this.pendingAsap.splice(index, 1);
|
|
1055
|
+
}
|
|
1056
|
+
this.redraw();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
handleKey(str = "", key = {}) {
|
|
1060
|
+
if (key.ctrl && key.name === "c") {
|
|
1061
|
+
this.controller.abort(new Error("Interrupted by ctrl-c."));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (key.name === "escape") {
|
|
1065
|
+
this.controller.abort(new Error("Interrupted by escape."));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (key.meta && key.name === "up") {
|
|
1069
|
+
this.editLastAsap();
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if ((key.shift && key.name === "left") || key.sequence === "\x1b[1;2D") {
|
|
1073
|
+
this.editLastQueued();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if ((key.ctrl && key.name === "j") || key.sequence === "\n") {
|
|
1077
|
+
({ buffer: this.buffer, cursor: this.cursor } = insertAt(this.buffer, this.cursor, "\n"));
|
|
1078
|
+
this.preferredColumn = null;
|
|
1079
|
+
this.redraw();
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (key.name === "return" || key.name === "enter" || key.sequence === "\r" || str === "\r") {
|
|
1083
|
+
void this.submitAsap();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (key.name === "tab") {
|
|
1087
|
+
this.queueAfterFinish();
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (key.name === "backspace") {
|
|
1091
|
+
({ buffer: this.buffer, cursor: this.cursor } = removeBefore(this.buffer, this.cursor));
|
|
1092
|
+
this.preferredColumn = null;
|
|
1093
|
+
this.redraw();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (key.name === "delete") {
|
|
1097
|
+
({ buffer: this.buffer, cursor: this.cursor } = removeAt(this.buffer, this.cursor));
|
|
1098
|
+
this.preferredColumn = null;
|
|
1099
|
+
this.redraw();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (key.name === "left") {
|
|
1103
|
+
this.cursor = Math.max(this.cursor - 1, 0);
|
|
1104
|
+
this.preferredColumn = null;
|
|
1105
|
+
this.redraw();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (key.name === "right") {
|
|
1109
|
+
this.cursor = Math.min(this.cursor + 1, this.buffer.length);
|
|
1110
|
+
this.preferredColumn = null;
|
|
1111
|
+
this.redraw();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (key.name === "up") {
|
|
1115
|
+
this.moveVertical(-1);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (key.name === "down") {
|
|
1119
|
+
this.moveVertical(1);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if ((key.ctrl && key.name === "a") || key.name === "home") {
|
|
1123
|
+
this.cursor = lineBounds(this.buffer, this.cursor).start;
|
|
1124
|
+
this.preferredColumn = null;
|
|
1125
|
+
this.redraw();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if ((key.ctrl && key.name === "e") || key.name === "end") {
|
|
1129
|
+
this.cursor = lineBounds(this.buffer, this.cursor).end;
|
|
1130
|
+
this.preferredColumn = null;
|
|
1131
|
+
this.redraw();
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (key.ctrl && key.name === "u") {
|
|
1135
|
+
const bounds = lineBounds(this.buffer, this.cursor);
|
|
1136
|
+
this.buffer = `${this.buffer.slice(0, bounds.start)}${this.buffer.slice(this.cursor)}`;
|
|
1137
|
+
this.cursor = bounds.start;
|
|
1138
|
+
this.preferredColumn = null;
|
|
1139
|
+
this.redraw();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (key.ctrl && key.name === "k") {
|
|
1143
|
+
const bounds = lineBounds(this.buffer, this.cursor);
|
|
1144
|
+
this.buffer = `${this.buffer.slice(0, this.cursor)}${this.buffer.slice(bounds.end)}`;
|
|
1145
|
+
this.preferredColumn = null;
|
|
1146
|
+
this.redraw();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (key.ctrl || key.meta) return;
|
|
1150
|
+
if (str && !key.sequence?.startsWith("\x1b")) {
|
|
1151
|
+
const text = str.replace(/\r/g, "");
|
|
1152
|
+
({ buffer: this.buffer, cursor: this.cursor } = insertAt(this.buffer, this.cursor, text));
|
|
1153
|
+
this.preferredColumn = null;
|
|
1154
|
+
this.redraw();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
842
1159
|
function printStatus(state) {
|
|
843
1160
|
printSystemLine(`project=${process.cwd()}`);
|
|
844
1161
|
printSystemLine(`cwd=${state.commandCwd || process.cwd()}`);
|
|
@@ -869,6 +1186,34 @@ async function latestSession() {
|
|
|
869
1186
|
return sessions[0] || null;
|
|
870
1187
|
}
|
|
871
1188
|
|
|
1189
|
+
function compactHistoryText(content = "", limit = 170) {
|
|
1190
|
+
const rendered = stripAnsi(stripMarkdown(String(content || "")));
|
|
1191
|
+
return compactLine(rendered.replace(/\s+/g, " "), limit);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function printHistoryEntry(entry) {
|
|
1195
|
+
const role = entry.role === "assistant" ? "aginti" : entry.role === "user" ? "user" : String(entry.role || "note");
|
|
1196
|
+
const bg = role === "aginti" ? ansi.agentBg : role === "user" ? ansi.userBg : ansi.systemBg;
|
|
1197
|
+
const time = entry.at ? new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
|
1198
|
+
const suffix = time ? ` ${color(time, ansi.dim)}` : "";
|
|
1199
|
+
outputLine(`${label(role, bg)} ${color("|", bg)} ${compactHistoryText(entry.content)}${suffix}`);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async function printResumeHistory(state, { limit = 8 } = {}) {
|
|
1203
|
+
if (!state.sessionId) return;
|
|
1204
|
+
const store = new SessionStore(projectPaths(process.cwd()).sessionsDir, state.sessionId);
|
|
1205
|
+
const saved = await store.loadState().catch(() => null);
|
|
1206
|
+
const chat = Array.isArray(saved?.chat) ? saved.chat.filter((entry) => entry?.content) : [];
|
|
1207
|
+
if (chat.length === 0) {
|
|
1208
|
+
printSystemLine(`resume history session=${state.sessionId} messages=0`);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const shown = chat.slice(-limit);
|
|
1213
|
+
printSystemLine(`resume history session=${state.sessionId} showing=${shown.length}/${chat.length}`);
|
|
1214
|
+
for (const entry of shown) printHistoryEntry(entry);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
872
1217
|
function printStatusEvent(state, label, details = "") {
|
|
873
1218
|
state.lastEvent = details ? `${label}: ${details}` : label;
|
|
874
1219
|
printSystemLine(`status=${state.status || "running"} ${state.lastEvent}`);
|
|
@@ -1092,10 +1437,12 @@ async function handleCommand(line, state, packageDir) {
|
|
|
1092
1437
|
} else {
|
|
1093
1438
|
state.sessionId = latest.sessionId;
|
|
1094
1439
|
printAgentMessage(`Resuming latest ${formatSessionLine(latest)}`);
|
|
1440
|
+
await printResumeHistory(state);
|
|
1095
1441
|
}
|
|
1096
1442
|
} else {
|
|
1097
1443
|
state.sessionId = value;
|
|
1098
1444
|
printAgentMessage(`Resuming ${state.sessionId}`);
|
|
1445
|
+
await printResumeHistory(state);
|
|
1099
1446
|
}
|
|
1100
1447
|
return true;
|
|
1101
1448
|
}
|
|
@@ -1253,8 +1600,12 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1253
1600
|
printSystemLine(`session=${state.sessionId}`);
|
|
1254
1601
|
printSystemLine(`status=running workingOn=${state.activeGoal}`);
|
|
1255
1602
|
|
|
1256
|
-
const
|
|
1603
|
+
const store = new SessionStore(config.sessionsDir, state.sessionId);
|
|
1604
|
+
const liveInput = new LiveRunInput({ state, store, controller });
|
|
1605
|
+
const liveStarted = liveInput.start();
|
|
1606
|
+
const detachInterrupts = liveStarted ? () => {} : attachRunInterrupts(controller);
|
|
1257
1607
|
let result;
|
|
1608
|
+
let queuedAfterFinish = [];
|
|
1258
1609
|
try {
|
|
1259
1610
|
result = await runAgent({
|
|
1260
1611
|
...config,
|
|
@@ -1267,7 +1618,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1267
1618
|
} else if (options.kind === "heading") {
|
|
1268
1619
|
printHeading(text);
|
|
1269
1620
|
} else if (options.error) {
|
|
1270
|
-
|
|
1621
|
+
outputLine(`${label("error", ansi.systemBg)} ${stripMarkdown(text)}`);
|
|
1271
1622
|
} else {
|
|
1272
1623
|
printSystemLine(text);
|
|
1273
1624
|
}
|
|
@@ -1284,7 +1635,8 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1284
1635
|
} else if (type === "loop.guard") {
|
|
1285
1636
|
printStatusEvent(state, "loop_guard", data.toolName || "");
|
|
1286
1637
|
} else if (type === "conversation.queued_input_applied") {
|
|
1287
|
-
|
|
1638
|
+
liveInput.markApplied(data);
|
|
1639
|
+
printStatusEvent(state, "queued_input_applied", data.priority === "asap" ? "asap" : "");
|
|
1288
1640
|
} else if (type === "session.finished") {
|
|
1289
1641
|
printStatusEvent(state, "finished");
|
|
1290
1642
|
} else if (type === "session.stopped") {
|
|
@@ -1296,6 +1648,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1296
1648
|
});
|
|
1297
1649
|
} finally {
|
|
1298
1650
|
detachInterrupts();
|
|
1651
|
+
queuedAfterFinish = await liveInput.stop();
|
|
1299
1652
|
}
|
|
1300
1653
|
state.sessionId = result.sessionId || state.sessionId;
|
|
1301
1654
|
state.status = result.stopped ? "stopped" : "idle";
|
|
@@ -1303,7 +1656,9 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1303
1656
|
printSystemLine(`status=${state.status} session=${state.sessionId}`);
|
|
1304
1657
|
if (result.stopped && result.reason === "user_interrupt") {
|
|
1305
1658
|
await printResumeHint(state);
|
|
1659
|
+
return [];
|
|
1306
1660
|
}
|
|
1661
|
+
return queuedAfterFinish;
|
|
1307
1662
|
}
|
|
1308
1663
|
|
|
1309
1664
|
export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
|
|
@@ -1323,12 +1678,13 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
1323
1678
|
await maybeOnboardDeepSeekKey(state);
|
|
1324
1679
|
printAgentMessage("Interactive agent chat. Type /help for commands, /exit to quit.");
|
|
1325
1680
|
printStatus(state);
|
|
1681
|
+
await printResumeHistory(state);
|
|
1326
1682
|
|
|
1327
1683
|
try {
|
|
1328
1684
|
while (true) {
|
|
1329
1685
|
let answer = "";
|
|
1330
1686
|
try {
|
|
1331
|
-
answer = await readPromptAnswer(rl);
|
|
1687
|
+
answer = await readPromptAnswer(rl, state);
|
|
1332
1688
|
} catch (error) {
|
|
1333
1689
|
if (error?.code === "ERR_USE_AFTER_CLOSE") break;
|
|
1334
1690
|
if (isAbortError(error)) {
|
|
@@ -1346,7 +1702,19 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
1346
1702
|
}
|
|
1347
1703
|
|
|
1348
1704
|
try {
|
|
1349
|
-
|
|
1705
|
+
const pendingPrompts = [{ content: line }];
|
|
1706
|
+
while (pendingPrompts.length > 0) {
|
|
1707
|
+
const nextPrompt = pendingPrompts.shift();
|
|
1708
|
+
const content = String(nextPrompt.content || "").trim();
|
|
1709
|
+
if (!content) continue;
|
|
1710
|
+
if (content.startsWith("/")) {
|
|
1711
|
+
const keepGoing = await handleCommand(content, state, packageDir);
|
|
1712
|
+
if (!keepGoing) return;
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
const queued = await runPrompt(content, state, packageDir);
|
|
1716
|
+
pendingPrompts.push(...queued.filter((item) => String(item.content || "").trim()));
|
|
1717
|
+
}
|
|
1350
1718
|
} catch (error) {
|
|
1351
1719
|
if (isAbortError(error)) {
|
|
1352
1720
|
await printResumeHint(state);
|
package/src/session-store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
|
|
4
5
|
export class SessionStore {
|
|
5
6
|
constructor(baseDir, sessionId) {
|
|
@@ -63,16 +64,20 @@ export class SessionStore {
|
|
|
63
64
|
async appendInbox(content, metadata = {}) {
|
|
64
65
|
await this.ensure();
|
|
65
66
|
const text = String(content || "").trim();
|
|
66
|
-
if (!text) return;
|
|
67
|
-
const
|
|
67
|
+
if (!text) return null;
|
|
68
|
+
const item = {
|
|
69
|
+
id: metadata.id || `inbox-${crypto.randomUUID()}`,
|
|
68
70
|
timestamp: new Date().toISOString(),
|
|
69
71
|
content: text,
|
|
72
|
+
priority: metadata.priority || "normal",
|
|
70
73
|
...metadata,
|
|
71
|
-
}
|
|
74
|
+
};
|
|
75
|
+
const line = JSON.stringify(item);
|
|
72
76
|
await fs.appendFile(this.inboxPath, `${line}\n`, "utf8");
|
|
77
|
+
return item;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
async
|
|
80
|
+
async loadInbox() {
|
|
76
81
|
await this.ensure();
|
|
77
82
|
let raw = "";
|
|
78
83
|
try {
|
|
@@ -80,7 +85,6 @@ export class SessionStore {
|
|
|
80
85
|
} catch {
|
|
81
86
|
return [];
|
|
82
87
|
}
|
|
83
|
-
await fs.writeFile(this.inboxPath, "", "utf8");
|
|
84
88
|
return raw
|
|
85
89
|
.split("\n")
|
|
86
90
|
.map((line) => line.trim())
|
|
@@ -88,6 +92,22 @@ export class SessionStore {
|
|
|
88
92
|
.map((line) => JSON.parse(line));
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
async saveInbox(items = []) {
|
|
96
|
+
await this.ensure();
|
|
97
|
+
const lines = items.filter(Boolean).map((item) => JSON.stringify(item));
|
|
98
|
+
await fs.writeFile(this.inboxPath, lines.length > 0 ? `${lines.join("\n")}\n` : "", "utf8");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async drainInbox() {
|
|
102
|
+
await this.ensure();
|
|
103
|
+
const items = await this.loadInbox();
|
|
104
|
+
await fs.writeFile(this.inboxPath, "", "utf8");
|
|
105
|
+
return items.sort((a, b) => {
|
|
106
|
+
const priority = (item) => (item.priority === "asap" ? 0 : 1);
|
|
107
|
+
return priority(a) - priority(b) || String(a.timestamp || "").localeCompare(String(b.timestamp || ""));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
async saveSnapshot(step, snapshot) {
|
|
92
112
|
await this.ensure();
|
|
93
113
|
const filename = `step-${String(step).padStart(3, "0")}.snapshot.json`;
|