@lazyingart/agintiflow 0.12.4 → 0.12.5
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 +12 -1
- package/scripts/smoke-inbox.js +9 -5
- package/src/agent-runner.js +4 -0
- package/src/interactive-cli.js +353 -21
- 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 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.
|
|
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");
|
|
@@ -125,7 +136,7 @@ try {
|
|
|
125
136
|
{
|
|
126
137
|
ok: true,
|
|
127
138
|
projectRoot: tempRoot,
|
|
128
|
-
checks: ["markdown-render", "prompt-layout", "agent-response-newline", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest"],
|
|
139
|
+
checks: ["markdown-render", "prompt-layout", "live-input-layout", "agent-response-newline", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest"],
|
|
129
140
|
},
|
|
130
141
|
null,
|
|
131
142
|
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
|
@@ -8,6 +8,7 @@ import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-p
|
|
|
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 = {
|
|
@@ -56,6 +57,7 @@ const SLASH_COMMANDS = [
|
|
|
56
57
|
"/exit",
|
|
57
58
|
];
|
|
58
59
|
const promptHistory = [];
|
|
60
|
+
let activeRunInput = null;
|
|
59
61
|
|
|
60
62
|
function color(value, ...codes) {
|
|
61
63
|
if (!useColor || codes.length === 0) return String(value);
|
|
@@ -70,6 +72,14 @@ function label(name, bgCode) {
|
|
|
70
72
|
return color(` ${name} `, bgCode, ansi.bold);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
function outputLine(line = "") {
|
|
76
|
+
if (activeRunInput) {
|
|
77
|
+
activeRunInput.printLine(String(line));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log(line);
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
function terminalWidth() {
|
|
74
84
|
return Math.max(Number(output.columns) || 80, 40);
|
|
75
85
|
}
|
|
@@ -133,6 +143,11 @@ function clamp(value, min, max) {
|
|
|
133
143
|
return Math.min(Math.max(value, min), max);
|
|
134
144
|
}
|
|
135
145
|
|
|
146
|
+
function compactLine(value = "", limit = 96) {
|
|
147
|
+
const text = stripAnsi(String(value || "").replace(/\s+/g, " ").trim());
|
|
148
|
+
return text.length <= limit ? text : `${text.slice(0, Math.max(limit - 1, 1))}…`;
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
export function stripMarkdown(text) {
|
|
137
152
|
const lines = String(text || "").split(/\r?\n/);
|
|
138
153
|
let inFence = false;
|
|
@@ -265,29 +280,29 @@ function printWrapped(prefix, text, { stripCode = "" } = {}) {
|
|
|
265
280
|
const lines = rendered.split("\n");
|
|
266
281
|
const visible = stripAnsi(prefix).length;
|
|
267
282
|
const gutter = `${" ".repeat(Math.max(visible - 2, 0))}${stripCode ? color("|", stripCode) : "|"} `;
|
|
268
|
-
|
|
283
|
+
outputLine(`${prefix}${lines[0] || ""}`);
|
|
269
284
|
for (const line of lines.slice(1)) {
|
|
270
|
-
|
|
285
|
+
outputLine(`${gutter}${line}`);
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
288
|
|
|
274
289
|
function printAgentMessage(text) {
|
|
275
|
-
|
|
290
|
+
outputLine(rolePrefix("aginti>", ansi.agentBg).trimEnd());
|
|
276
291
|
const rendered = stripMarkdown(text);
|
|
277
292
|
const lines = rendered.split("\n");
|
|
278
|
-
for (const line of lines)
|
|
293
|
+
for (const line of lines) outputLine(line);
|
|
279
294
|
}
|
|
280
295
|
|
|
281
296
|
function printSystemLine(text) {
|
|
282
297
|
if (!String(text || "").trim()) {
|
|
283
|
-
|
|
298
|
+
outputLine("");
|
|
284
299
|
return;
|
|
285
300
|
}
|
|
286
|
-
|
|
301
|
+
outputLine(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
|
|
287
302
|
}
|
|
288
303
|
|
|
289
304
|
function printHeading(text) {
|
|
290
|
-
|
|
305
|
+
outputLine(color(stripMarkdown(text), ansi.bold, ansi.cyan));
|
|
291
306
|
}
|
|
292
307
|
|
|
293
308
|
function shimmerText(text, frame) {
|
|
@@ -364,7 +379,9 @@ function printHelp() {
|
|
|
364
379
|
"",
|
|
365
380
|
"Type a normal request to run the agent. Example: write a Python CLI app with tests",
|
|
366
381
|
"Type / then Tab to autocomplete commands.",
|
|
367
|
-
"While a run is active,
|
|
382
|
+
"While a run is active, Enter pipes a message into the current run (→), Tab queues it after finish (↳).",
|
|
383
|
+
"Alt+Up edits the last piped message; Shift+Left edits the last queued message.",
|
|
384
|
+
"Esc or Ctrl+C stops gracefully and prints a resume command.",
|
|
368
385
|
].join("\n")
|
|
369
386
|
);
|
|
370
387
|
}
|
|
@@ -401,7 +418,7 @@ function promptVisibleWindow(rows, cursorRow, height = terminalHeight()) {
|
|
|
401
418
|
};
|
|
402
419
|
}
|
|
403
420
|
|
|
404
|
-
export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight()) {
|
|
421
|
+
export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight(), options = {}) {
|
|
405
422
|
const safeBuffer = String(buffer || "");
|
|
406
423
|
const safeCursor = clamp(Number(cursor) || 0, 0, safeBuffer.length);
|
|
407
424
|
const lineWidth = editorWidth(width);
|
|
@@ -497,6 +514,17 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
|
|
|
497
514
|
const renderedRows = [];
|
|
498
515
|
let renderedCursorRow = cursorRow - visible.start;
|
|
499
516
|
|
|
517
|
+
const pendingAsap = Array.isArray(options.pendingAsap) ? options.pendingAsap : [];
|
|
518
|
+
const pendingQueued = Array.isArray(options.pendingQueued) ? options.pendingQueued : [];
|
|
519
|
+
for (const item of pendingAsap.slice(-4)) {
|
|
520
|
+
renderedRows.push(panelLine(` → ${compactLine(item.content || item, lineWidth - 6)}`, ansi.systemBg, lineWidth));
|
|
521
|
+
renderedCursorRow += 1;
|
|
522
|
+
}
|
|
523
|
+
for (const item of pendingQueued.slice(-4)) {
|
|
524
|
+
renderedRows.push(panelLine(` ↳ ${compactLine(item.content || item, lineWidth - 6)}`, ansi.systemBg, lineWidth));
|
|
525
|
+
renderedCursorRow += 1;
|
|
526
|
+
}
|
|
527
|
+
|
|
500
528
|
if (visible.topHidden > 0) {
|
|
501
529
|
renderedRows.push(panelLine(` ... ${visible.topHidden} earlier input row${visible.topHidden === 1 ? "" : "s"}`, ansi.systemBg, lineWidth));
|
|
502
530
|
renderedCursorRow += 1;
|
|
@@ -514,6 +542,9 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
|
|
|
514
542
|
if (suggestions.length > 0) {
|
|
515
543
|
renderedRows.push(panelLine(` hint ${suggestions.join(" ")}`, ansi.systemBg, lineWidth));
|
|
516
544
|
}
|
|
545
|
+
if (options.commandCwd) {
|
|
546
|
+
renderedRows.push(panelLine(` cwd ${options.commandCwd}`, ansi.systemBg, lineWidth));
|
|
547
|
+
}
|
|
517
548
|
|
|
518
549
|
return {
|
|
519
550
|
rows,
|
|
@@ -558,8 +589,8 @@ function moveFromPromptCursorToTop(previous) {
|
|
|
558
589
|
output.write("\r");
|
|
559
590
|
}
|
|
560
591
|
|
|
561
|
-
function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0, renderedRows: [] }) {
|
|
562
|
-
const layout = buildPromptLayout(buffer, cursor);
|
|
592
|
+
function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0, renderedRows: [] }, options = {}) {
|
|
593
|
+
const layout = buildPromptLayout(buffer, cursor, terminalWidth(), terminalHeight(), options);
|
|
563
594
|
const sameShape = previous.lineCount === layout.renderedRows.length && previous.renderedRows?.length === layout.renderedRows.length;
|
|
564
595
|
const changedRows = sameShape
|
|
565
596
|
? layout.renderedRows
|
|
@@ -637,7 +668,7 @@ function createAbortError(message = "Aborted with Ctrl+C") {
|
|
|
637
668
|
return error;
|
|
638
669
|
}
|
|
639
670
|
|
|
640
|
-
function readTtyPrompt() {
|
|
671
|
+
function readTtyPrompt(options = {}) {
|
|
641
672
|
return new Promise((resolve, reject) => {
|
|
642
673
|
emitKeypressEvents(input);
|
|
643
674
|
const wasRaw = Boolean(input.isRaw);
|
|
@@ -665,14 +696,14 @@ function readTtyPrompt() {
|
|
|
665
696
|
clearImmediate(redrawHandle);
|
|
666
697
|
redrawHandle = null;
|
|
667
698
|
}
|
|
668
|
-
rendered = renderPromptBuffer(buffer, cursor, rendered);
|
|
699
|
+
rendered = renderPromptBuffer(buffer, cursor, rendered, options);
|
|
669
700
|
};
|
|
670
701
|
|
|
671
702
|
const redraw = () => {
|
|
672
703
|
if (redrawHandle) return;
|
|
673
704
|
redrawHandle = setImmediate(() => {
|
|
674
705
|
redrawHandle = null;
|
|
675
|
-
rendered = renderPromptBuffer(buffer, cursor, rendered);
|
|
706
|
+
rendered = renderPromptBuffer(buffer, cursor, rendered, options);
|
|
676
707
|
});
|
|
677
708
|
};
|
|
678
709
|
|
|
@@ -832,13 +863,294 @@ function readTtyPrompt() {
|
|
|
832
863
|
});
|
|
833
864
|
}
|
|
834
865
|
|
|
835
|
-
async function readPromptAnswer(rl) {
|
|
866
|
+
async function readPromptAnswer(rl, state = {}) {
|
|
836
867
|
if (input.isTTY && output.isTTY && typeof input.setRawMode === "function") {
|
|
837
|
-
return readTtyPrompt();
|
|
868
|
+
return readTtyPrompt({ commandCwd: state.commandCwd || process.cwd() });
|
|
838
869
|
}
|
|
839
870
|
return rl.question(userPrompt());
|
|
840
871
|
}
|
|
841
872
|
|
|
873
|
+
class LiveRunInput {
|
|
874
|
+
constructor({ state, store, controller }) {
|
|
875
|
+
this.state = state;
|
|
876
|
+
this.store = store;
|
|
877
|
+
this.controller = controller;
|
|
878
|
+
this.buffer = "";
|
|
879
|
+
this.cursor = 0;
|
|
880
|
+
this.rendered = { lineCount: 0, cursorRow: 0, renderedRows: [] };
|
|
881
|
+
this.redrawHandle = null;
|
|
882
|
+
this.preferredColumn = null;
|
|
883
|
+
this.pendingAsap = [];
|
|
884
|
+
this.pendingQueued = [];
|
|
885
|
+
this.wasRaw = Boolean(input.isRaw);
|
|
886
|
+
this.started = false;
|
|
887
|
+
this.handler = this.handleKey.bind(this);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
get commandCwd() {
|
|
891
|
+
return this.state.commandCwd || process.cwd();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
start() {
|
|
895
|
+
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return false;
|
|
896
|
+
emitKeypressEvents(input);
|
|
897
|
+
input.resume();
|
|
898
|
+
input.setRawMode(true);
|
|
899
|
+
input.on("keypress", this.handler);
|
|
900
|
+
activeRunInput = this;
|
|
901
|
+
this.started = true;
|
|
902
|
+
this.renderNow();
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async stop() {
|
|
907
|
+
if (!this.started) return [];
|
|
908
|
+
if (this.redrawHandle) {
|
|
909
|
+
clearImmediate(this.redrawHandle);
|
|
910
|
+
this.redrawHandle = null;
|
|
911
|
+
}
|
|
912
|
+
input.off("keypress", this.handler);
|
|
913
|
+
if (typeof input.setRawMode === "function") input.setRawMode(this.wasRaw);
|
|
914
|
+
this.clearForExternalOutput();
|
|
915
|
+
output.write(ansi.cursorShow);
|
|
916
|
+
if (activeRunInput === this) activeRunInput = null;
|
|
917
|
+
this.started = false;
|
|
918
|
+
|
|
919
|
+
const unappliedAsap = [...this.pendingAsap];
|
|
920
|
+
if (unappliedAsap.length > 0) {
|
|
921
|
+
await this.removeInboxItems(unappliedAsap.map((item) => item.id));
|
|
922
|
+
}
|
|
923
|
+
return [
|
|
924
|
+
...unappliedAsap.map((item) => ({ ...item, kind: "asap" })),
|
|
925
|
+
...this.pendingQueued.map((item) => ({ ...item, kind: "queued" })),
|
|
926
|
+
];
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
printLine(line = "") {
|
|
930
|
+
this.clearForExternalOutput();
|
|
931
|
+
output.write(`${line}\n`);
|
|
932
|
+
this.renderNow();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
clearForExternalOutput() {
|
|
936
|
+
if (!this.rendered.lineCount) return;
|
|
937
|
+
output.write(ansi.cursorHide);
|
|
938
|
+
clearRenderedPrompt(this.rendered);
|
|
939
|
+
this.rendered = { lineCount: 0, cursorRow: 0, renderedRows: [] };
|
|
940
|
+
output.write(ansi.cursorShow);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
renderNow() {
|
|
944
|
+
if (this.redrawHandle) {
|
|
945
|
+
clearImmediate(this.redrawHandle);
|
|
946
|
+
this.redrawHandle = null;
|
|
947
|
+
}
|
|
948
|
+
this.rendered = renderPromptBuffer(this.buffer, this.cursor, this.rendered, {
|
|
949
|
+
commandCwd: this.commandCwd,
|
|
950
|
+
pendingAsap: this.pendingAsap,
|
|
951
|
+
pendingQueued: this.pendingQueued,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
redraw() {
|
|
956
|
+
if (this.redrawHandle) return;
|
|
957
|
+
this.redrawHandle = setImmediate(() => {
|
|
958
|
+
this.redrawHandle = null;
|
|
959
|
+
this.renderNow();
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
setBuffer(nextBuffer, nextCursor = nextBuffer.length) {
|
|
964
|
+
this.buffer = String(nextBuffer || "");
|
|
965
|
+
this.cursor = clamp(nextCursor, 0, this.buffer.length);
|
|
966
|
+
this.preferredColumn = null;
|
|
967
|
+
this.redraw();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
moveVertical(direction) {
|
|
971
|
+
const layout = buildPromptLayout(this.buffer, this.cursor, terminalWidth(), terminalHeight(), {
|
|
972
|
+
commandCwd: this.commandCwd,
|
|
973
|
+
pendingAsap: this.pendingAsap,
|
|
974
|
+
pendingQueued: this.pendingQueued,
|
|
975
|
+
});
|
|
976
|
+
const location = cursorLocation(layout, this.cursor);
|
|
977
|
+
const targetRowIndex = location.rowIndex + direction;
|
|
978
|
+
if (targetRowIndex < 0 || targetRowIndex >= layout.rows.length) return;
|
|
979
|
+
const currentColumn = this.preferredColumn ?? location.column;
|
|
980
|
+
const targetRow = layout.rows[targetRowIndex];
|
|
981
|
+
this.cursor = targetRow.start + Math.min(currentColumn, targetRow.end - targetRow.start);
|
|
982
|
+
this.preferredColumn = currentColumn;
|
|
983
|
+
this.redraw();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async removeInboxItems(ids = []) {
|
|
987
|
+
if (ids.length === 0) return;
|
|
988
|
+
const idSet = new Set(ids);
|
|
989
|
+
const inbox = await this.store.loadInbox().catch(() => []);
|
|
990
|
+
await this.store.saveInbox(inbox.filter((item) => !idSet.has(item.id))).catch(() => {});
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async submitAsap() {
|
|
994
|
+
const content = this.buffer.trim();
|
|
995
|
+
if (!content) return;
|
|
996
|
+
const item = {
|
|
997
|
+
id: `cli-asap-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
998
|
+
content,
|
|
999
|
+
priority: "asap",
|
|
1000
|
+
source: "cli-live",
|
|
1001
|
+
};
|
|
1002
|
+
this.pendingAsap.push(item);
|
|
1003
|
+
this.setBuffer("");
|
|
1004
|
+
await this.store.appendInbox(content, item).catch((error) => {
|
|
1005
|
+
this.pendingAsap = this.pendingAsap.filter((pending) => pending.id !== item.id);
|
|
1006
|
+
this.printLine(`${label("error", ansi.systemBg)} failed to pipe message: ${error.message}`);
|
|
1007
|
+
});
|
|
1008
|
+
await this.store.appendEvent("conversation.piped_input", {
|
|
1009
|
+
id: item.id,
|
|
1010
|
+
prompt: content,
|
|
1011
|
+
source: item.source,
|
|
1012
|
+
priority: item.priority,
|
|
1013
|
+
}).catch(() => {});
|
|
1014
|
+
this.redraw();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
queueAfterFinish() {
|
|
1018
|
+
const content = this.buffer.trim();
|
|
1019
|
+
if (!content) return;
|
|
1020
|
+
this.pendingQueued.push({
|
|
1021
|
+
id: `cli-queued-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
1022
|
+
content,
|
|
1023
|
+
priority: "after-finish",
|
|
1024
|
+
source: "cli-live",
|
|
1025
|
+
});
|
|
1026
|
+
this.setBuffer("");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
editLastQueued() {
|
|
1030
|
+
const item = this.pendingQueued.pop();
|
|
1031
|
+
if (!item) return;
|
|
1032
|
+
this.setBuffer(item.content);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
editLastAsap() {
|
|
1036
|
+
const item = this.pendingAsap.pop();
|
|
1037
|
+
if (!item) return;
|
|
1038
|
+
void this.removeInboxItems([item.id]);
|
|
1039
|
+
this.setBuffer(item.content);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
markApplied(data = {}) {
|
|
1043
|
+
const id = data.id || "";
|
|
1044
|
+
const prompt = String(data.prompt || "");
|
|
1045
|
+
if (id) {
|
|
1046
|
+
this.pendingAsap = this.pendingAsap.filter((item) => item.id !== id);
|
|
1047
|
+
} else if (prompt) {
|
|
1048
|
+
const index = this.pendingAsap.findIndex((item) => item.content === prompt);
|
|
1049
|
+
if (index >= 0) this.pendingAsap.splice(index, 1);
|
|
1050
|
+
}
|
|
1051
|
+
this.redraw();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
handleKey(str = "", key = {}) {
|
|
1055
|
+
if (key.ctrl && key.name === "c") {
|
|
1056
|
+
this.controller.abort(new Error("Interrupted by ctrl-c."));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (key.name === "escape") {
|
|
1060
|
+
this.controller.abort(new Error("Interrupted by escape."));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (key.meta && key.name === "up") {
|
|
1064
|
+
this.editLastAsap();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if ((key.shift && key.name === "left") || key.sequence === "\x1b[1;2D") {
|
|
1068
|
+
this.editLastQueued();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if ((key.ctrl && key.name === "j") || key.sequence === "\n") {
|
|
1072
|
+
({ buffer: this.buffer, cursor: this.cursor } = insertAt(this.buffer, this.cursor, "\n"));
|
|
1073
|
+
this.preferredColumn = null;
|
|
1074
|
+
this.redraw();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (key.name === "return" || key.name === "enter" || key.sequence === "\r" || str === "\r") {
|
|
1078
|
+
void this.submitAsap();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (key.name === "tab") {
|
|
1082
|
+
this.queueAfterFinish();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (key.name === "backspace") {
|
|
1086
|
+
({ buffer: this.buffer, cursor: this.cursor } = removeBefore(this.buffer, this.cursor));
|
|
1087
|
+
this.preferredColumn = null;
|
|
1088
|
+
this.redraw();
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (key.name === "delete") {
|
|
1092
|
+
({ buffer: this.buffer, cursor: this.cursor } = removeAt(this.buffer, this.cursor));
|
|
1093
|
+
this.preferredColumn = null;
|
|
1094
|
+
this.redraw();
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (key.name === "left") {
|
|
1098
|
+
this.cursor = Math.max(this.cursor - 1, 0);
|
|
1099
|
+
this.preferredColumn = null;
|
|
1100
|
+
this.redraw();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (key.name === "right") {
|
|
1104
|
+
this.cursor = Math.min(this.cursor + 1, this.buffer.length);
|
|
1105
|
+
this.preferredColumn = null;
|
|
1106
|
+
this.redraw();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (key.name === "up") {
|
|
1110
|
+
this.moveVertical(-1);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (key.name === "down") {
|
|
1114
|
+
this.moveVertical(1);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if ((key.ctrl && key.name === "a") || key.name === "home") {
|
|
1118
|
+
this.cursor = lineBounds(this.buffer, this.cursor).start;
|
|
1119
|
+
this.preferredColumn = null;
|
|
1120
|
+
this.redraw();
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if ((key.ctrl && key.name === "e") || key.name === "end") {
|
|
1124
|
+
this.cursor = lineBounds(this.buffer, this.cursor).end;
|
|
1125
|
+
this.preferredColumn = null;
|
|
1126
|
+
this.redraw();
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (key.ctrl && key.name === "u") {
|
|
1130
|
+
const bounds = lineBounds(this.buffer, this.cursor);
|
|
1131
|
+
this.buffer = `${this.buffer.slice(0, bounds.start)}${this.buffer.slice(this.cursor)}`;
|
|
1132
|
+
this.cursor = bounds.start;
|
|
1133
|
+
this.preferredColumn = null;
|
|
1134
|
+
this.redraw();
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (key.ctrl && key.name === "k") {
|
|
1138
|
+
const bounds = lineBounds(this.buffer, this.cursor);
|
|
1139
|
+
this.buffer = `${this.buffer.slice(0, this.cursor)}${this.buffer.slice(bounds.end)}`;
|
|
1140
|
+
this.preferredColumn = null;
|
|
1141
|
+
this.redraw();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (key.ctrl || key.meta) return;
|
|
1145
|
+
if (str && !key.sequence?.startsWith("\x1b")) {
|
|
1146
|
+
const text = str.replace(/\r/g, "");
|
|
1147
|
+
({ buffer: this.buffer, cursor: this.cursor } = insertAt(this.buffer, this.cursor, text));
|
|
1148
|
+
this.preferredColumn = null;
|
|
1149
|
+
this.redraw();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
842
1154
|
function printStatus(state) {
|
|
843
1155
|
printSystemLine(`project=${process.cwd()}`);
|
|
844
1156
|
printSystemLine(`cwd=${state.commandCwd || process.cwd()}`);
|
|
@@ -1253,8 +1565,12 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1253
1565
|
printSystemLine(`session=${state.sessionId}`);
|
|
1254
1566
|
printSystemLine(`status=running workingOn=${state.activeGoal}`);
|
|
1255
1567
|
|
|
1256
|
-
const
|
|
1568
|
+
const store = new SessionStore(config.sessionsDir, state.sessionId);
|
|
1569
|
+
const liveInput = new LiveRunInput({ state, store, controller });
|
|
1570
|
+
const liveStarted = liveInput.start();
|
|
1571
|
+
const detachInterrupts = liveStarted ? () => {} : attachRunInterrupts(controller);
|
|
1257
1572
|
let result;
|
|
1573
|
+
let queuedAfterFinish = [];
|
|
1258
1574
|
try {
|
|
1259
1575
|
result = await runAgent({
|
|
1260
1576
|
...config,
|
|
@@ -1267,7 +1583,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1267
1583
|
} else if (options.kind === "heading") {
|
|
1268
1584
|
printHeading(text);
|
|
1269
1585
|
} else if (options.error) {
|
|
1270
|
-
|
|
1586
|
+
outputLine(`${label("error", ansi.systemBg)} ${stripMarkdown(text)}`);
|
|
1271
1587
|
} else {
|
|
1272
1588
|
printSystemLine(text);
|
|
1273
1589
|
}
|
|
@@ -1284,7 +1600,8 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1284
1600
|
} else if (type === "loop.guard") {
|
|
1285
1601
|
printStatusEvent(state, "loop_guard", data.toolName || "");
|
|
1286
1602
|
} else if (type === "conversation.queued_input_applied") {
|
|
1287
|
-
|
|
1603
|
+
liveInput.markApplied(data);
|
|
1604
|
+
printStatusEvent(state, "queued_input_applied", data.priority === "asap" ? "asap" : "");
|
|
1288
1605
|
} else if (type === "session.finished") {
|
|
1289
1606
|
printStatusEvent(state, "finished");
|
|
1290
1607
|
} else if (type === "session.stopped") {
|
|
@@ -1296,6 +1613,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1296
1613
|
});
|
|
1297
1614
|
} finally {
|
|
1298
1615
|
detachInterrupts();
|
|
1616
|
+
queuedAfterFinish = await liveInput.stop();
|
|
1299
1617
|
}
|
|
1300
1618
|
state.sessionId = result.sessionId || state.sessionId;
|
|
1301
1619
|
state.status = result.stopped ? "stopped" : "idle";
|
|
@@ -1303,7 +1621,9 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1303
1621
|
printSystemLine(`status=${state.status} session=${state.sessionId}`);
|
|
1304
1622
|
if (result.stopped && result.reason === "user_interrupt") {
|
|
1305
1623
|
await printResumeHint(state);
|
|
1624
|
+
return [];
|
|
1306
1625
|
}
|
|
1626
|
+
return queuedAfterFinish;
|
|
1307
1627
|
}
|
|
1308
1628
|
|
|
1309
1629
|
export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
|
|
@@ -1328,7 +1648,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
1328
1648
|
while (true) {
|
|
1329
1649
|
let answer = "";
|
|
1330
1650
|
try {
|
|
1331
|
-
answer = await readPromptAnswer(rl);
|
|
1651
|
+
answer = await readPromptAnswer(rl, state);
|
|
1332
1652
|
} catch (error) {
|
|
1333
1653
|
if (error?.code === "ERR_USE_AFTER_CLOSE") break;
|
|
1334
1654
|
if (isAbortError(error)) {
|
|
@@ -1346,7 +1666,19 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
1346
1666
|
}
|
|
1347
1667
|
|
|
1348
1668
|
try {
|
|
1349
|
-
|
|
1669
|
+
const pendingPrompts = [{ content: line }];
|
|
1670
|
+
while (pendingPrompts.length > 0) {
|
|
1671
|
+
const nextPrompt = pendingPrompts.shift();
|
|
1672
|
+
const content = String(nextPrompt.content || "").trim();
|
|
1673
|
+
if (!content) continue;
|
|
1674
|
+
if (content.startsWith("/")) {
|
|
1675
|
+
const keepGoing = await handleCommand(content, state, packageDir);
|
|
1676
|
+
if (!keepGoing) return;
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
const queued = await runPrompt(content, state, packageDir);
|
|
1680
|
+
pendingPrompts.push(...queued.filter((item) => String(item.content || "").trim()));
|
|
1681
|
+
}
|
|
1350
1682
|
} catch (error) {
|
|
1351
1683
|
if (isAbortError(error)) {
|
|
1352
1684
|
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`;
|