@lazyingart/agintiflow 0.12.3 → 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 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 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. Queued input is written to `.sessions/<session-id>/inbox.jsonl`; the running agent drains that pipe between steps and after tool calls. 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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
@@ -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,6 +122,9 @@ 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
  }
125
+ if (!/aginti>\s+\|\r?\nMock run complete\./.test(result.stdout)) {
126
+ throw new Error("assistant response did not start on a fresh line after the aginti header");
127
+ }
114
128
 
115
129
  const latest = await runCli(["resume"], "/exit\n");
116
130
  if (!latest.stdout.includes("session=") || !latest.stdout.includes("Interactive agent chat")) {
@@ -122,7 +136,7 @@ try {
122
136
  {
123
137
  ok: true,
124
138
  projectRoot: tempRoot,
125
- checks: ["markdown-render", "prompt-layout", "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"],
126
140
  },
127
141
  null,
128
142
  2
@@ -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 === 2, "inbox did not drain both messages");
20
- assert(drained[0].content === "first queued message", "first queued message mismatch");
21
- assert(drained[1].content === "second queued message", "second queued message mismatch");
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
@@ -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
  }
@@ -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,26 +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
- console.log(`${prefix}${lines[0] || ""}`);
283
+ outputLine(`${prefix}${lines[0] || ""}`);
269
284
  for (const line of lines.slice(1)) {
270
- console.log(`${gutter}${line}`);
285
+ outputLine(`${gutter}${line}`);
271
286
  }
272
287
  }
273
288
 
274
289
  function printAgentMessage(text) {
275
- printWrapped(rolePrefix("aginti>", ansi.agentBg), text, { stripCode: ansi.agentBg });
290
+ outputLine(rolePrefix("aginti>", ansi.agentBg).trimEnd());
291
+ const rendered = stripMarkdown(text);
292
+ const lines = rendered.split("\n");
293
+ for (const line of lines) outputLine(line);
276
294
  }
277
295
 
278
296
  function printSystemLine(text) {
279
297
  if (!String(text || "").trim()) {
280
- console.log("");
298
+ outputLine("");
281
299
  return;
282
300
  }
283
- console.log(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
301
+ outputLine(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
284
302
  }
285
303
 
286
304
  function printHeading(text) {
287
- console.log(color(stripMarkdown(text), ansi.bold, ansi.cyan));
305
+ outputLine(color(stripMarkdown(text), ansi.bold, ansi.cyan));
288
306
  }
289
307
 
290
308
  function shimmerText(text, frame) {
@@ -361,7 +379,9 @@ function printHelp() {
361
379
  "",
362
380
  "Type a normal request to run the agent. Example: write a Python CLI app with tests",
363
381
  "Type / then Tab to autocomplete commands.",
364
- "While a run is active, press Esc or Ctrl+C once to stop gracefully and print a resume command.",
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.",
365
385
  ].join("\n")
366
386
  );
367
387
  }
@@ -398,7 +418,7 @@ function promptVisibleWindow(rows, cursorRow, height = terminalHeight()) {
398
418
  };
399
419
  }
400
420
 
401
- export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight()) {
421
+ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight(), options = {}) {
402
422
  const safeBuffer = String(buffer || "");
403
423
  const safeCursor = clamp(Number(cursor) || 0, 0, safeBuffer.length);
404
424
  const lineWidth = editorWidth(width);
@@ -494,6 +514,17 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
494
514
  const renderedRows = [];
495
515
  let renderedCursorRow = cursorRow - visible.start;
496
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
+
497
528
  if (visible.topHidden > 0) {
498
529
  renderedRows.push(panelLine(` ... ${visible.topHidden} earlier input row${visible.topHidden === 1 ? "" : "s"}`, ansi.systemBg, lineWidth));
499
530
  renderedCursorRow += 1;
@@ -511,6 +542,9 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
511
542
  if (suggestions.length > 0) {
512
543
  renderedRows.push(panelLine(` hint ${suggestions.join(" ")}`, ansi.systemBg, lineWidth));
513
544
  }
545
+ if (options.commandCwd) {
546
+ renderedRows.push(panelLine(` cwd ${options.commandCwd}`, ansi.systemBg, lineWidth));
547
+ }
514
548
 
515
549
  return {
516
550
  rows,
@@ -546,18 +580,47 @@ function clearRenderedPrompt(previous) {
546
580
  }
547
581
  }
548
582
 
549
- function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0 }) {
550
- output.write(ansi.cursorHide);
551
- clearRenderedPrompt(previous);
552
- const layout = buildPromptLayout(buffer, cursor);
553
- output.write(layout.renderedRows.join("\n"));
583
+ function moveFromPromptCursorToTop(previous) {
584
+ if (!previous.lineCount) return;
585
+ const below = previous.lineCount - 1 - previous.cursorRow;
586
+ if (below > 0) output.write(`\x1b[${below}B`);
587
+ const up = previous.lineCount - 1;
588
+ if (up > 0) output.write(`\x1b[${up}A`);
589
+ output.write("\r");
590
+ }
591
+
592
+ function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0, renderedRows: [] }, options = {}) {
593
+ const layout = buildPromptLayout(buffer, cursor, terminalWidth(), terminalHeight(), options);
594
+ const sameShape = previous.lineCount === layout.renderedRows.length && previous.renderedRows?.length === layout.renderedRows.length;
595
+ const changedRows = sameShape
596
+ ? layout.renderedRows
597
+ .map((row, index) => (row === previous.renderedRows[index] ? -1 : index))
598
+ .filter((index) => index >= 0)
599
+ : layout.renderedRows.map((_row, index) => index);
600
+
601
+ if (!sameShape) {
602
+ output.write(ansi.cursorHide);
603
+ clearRenderedPrompt(previous);
604
+ output.write(layout.renderedRows.join("\n"));
605
+ } else if (changedRows.length > 0) {
606
+ output.write(ansi.cursorHide);
607
+ moveFromPromptCursorToTop(previous);
608
+ for (let index = 0; index < layout.renderedRows.length; index += 1) {
609
+ if (changedRows.includes(index)) output.write(`\r${layout.renderedRows[index]}`);
610
+ if (index < layout.renderedRows.length - 1) output.write("\x1b[1B\r");
611
+ }
612
+ } else {
613
+ moveFromPromptCursorToTop(previous);
614
+ }
615
+
554
616
  const below = layout.renderedRows.length - 1 - layout.cursorRow;
555
617
  if (below > 0) output.write(`\x1b[${below}A`);
556
618
  output.write(`\r\x1b[${layout.cursorColumn + 1}G`);
557
- output.write(ansi.cursorShow);
619
+ if (!sameShape || changedRows.length > 0) output.write(ansi.cursorShow);
558
620
  return {
559
621
  lineCount: layout.renderedRows.length,
560
622
  cursorRow: layout.cursorRow,
623
+ renderedRows: layout.renderedRows,
561
624
  };
562
625
  }
563
626
 
@@ -605,7 +668,7 @@ function createAbortError(message = "Aborted with Ctrl+C") {
605
668
  return error;
606
669
  }
607
670
 
608
- function readTtyPrompt() {
671
+ function readTtyPrompt(options = {}) {
609
672
  return new Promise((resolve, reject) => {
610
673
  emitKeypressEvents(input);
611
674
  const wasRaw = Boolean(input.isRaw);
@@ -633,14 +696,14 @@ function readTtyPrompt() {
633
696
  clearImmediate(redrawHandle);
634
697
  redrawHandle = null;
635
698
  }
636
- rendered = renderPromptBuffer(buffer, cursor, rendered);
699
+ rendered = renderPromptBuffer(buffer, cursor, rendered, options);
637
700
  };
638
701
 
639
702
  const redraw = () => {
640
703
  if (redrawHandle) return;
641
704
  redrawHandle = setImmediate(() => {
642
705
  redrawHandle = null;
643
- rendered = renderPromptBuffer(buffer, cursor, rendered);
706
+ rendered = renderPromptBuffer(buffer, cursor, rendered, options);
644
707
  });
645
708
  };
646
709
 
@@ -800,13 +863,294 @@ function readTtyPrompt() {
800
863
  });
801
864
  }
802
865
 
803
- async function readPromptAnswer(rl) {
866
+ async function readPromptAnswer(rl, state = {}) {
804
867
  if (input.isTTY && output.isTTY && typeof input.setRawMode === "function") {
805
- return readTtyPrompt();
868
+ return readTtyPrompt({ commandCwd: state.commandCwd || process.cwd() });
806
869
  }
807
870
  return rl.question(userPrompt());
808
871
  }
809
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
+
810
1154
  function printStatus(state) {
811
1155
  printSystemLine(`project=${process.cwd()}`);
812
1156
  printSystemLine(`cwd=${state.commandCwd || process.cwd()}`);
@@ -1221,8 +1565,12 @@ async function runPrompt(prompt, state, packageDir) {
1221
1565
  printSystemLine(`session=${state.sessionId}`);
1222
1566
  printSystemLine(`status=running workingOn=${state.activeGoal}`);
1223
1567
 
1224
- const detachInterrupts = attachRunInterrupts(controller);
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);
1225
1572
  let result;
1573
+ let queuedAfterFinish = [];
1226
1574
  try {
1227
1575
  result = await runAgent({
1228
1576
  ...config,
@@ -1235,7 +1583,7 @@ async function runPrompt(prompt, state, packageDir) {
1235
1583
  } else if (options.kind === "heading") {
1236
1584
  printHeading(text);
1237
1585
  } else if (options.error) {
1238
- console.error(`${label("error", ansi.systemBg)} ${stripMarkdown(text)}`);
1586
+ outputLine(`${label("error", ansi.systemBg)} ${stripMarkdown(text)}`);
1239
1587
  } else {
1240
1588
  printSystemLine(text);
1241
1589
  }
@@ -1252,7 +1600,8 @@ async function runPrompt(prompt, state, packageDir) {
1252
1600
  } else if (type === "loop.guard") {
1253
1601
  printStatusEvent(state, "loop_guard", data.toolName || "");
1254
1602
  } else if (type === "conversation.queued_input_applied") {
1255
- printStatusEvent(state, "queued_input_applied");
1603
+ liveInput.markApplied(data);
1604
+ printStatusEvent(state, "queued_input_applied", data.priority === "asap" ? "asap" : "");
1256
1605
  } else if (type === "session.finished") {
1257
1606
  printStatusEvent(state, "finished");
1258
1607
  } else if (type === "session.stopped") {
@@ -1264,6 +1613,7 @@ async function runPrompt(prompt, state, packageDir) {
1264
1613
  });
1265
1614
  } finally {
1266
1615
  detachInterrupts();
1616
+ queuedAfterFinish = await liveInput.stop();
1267
1617
  }
1268
1618
  state.sessionId = result.sessionId || state.sessionId;
1269
1619
  state.status = result.stopped ? "stopped" : "idle";
@@ -1271,7 +1621,9 @@ async function runPrompt(prompt, state, packageDir) {
1271
1621
  printSystemLine(`status=${state.status} session=${state.sessionId}`);
1272
1622
  if (result.stopped && result.reason === "user_interrupt") {
1273
1623
  await printResumeHint(state);
1624
+ return [];
1274
1625
  }
1626
+ return queuedAfterFinish;
1275
1627
  }
1276
1628
 
1277
1629
  export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
@@ -1296,7 +1648,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
1296
1648
  while (true) {
1297
1649
  let answer = "";
1298
1650
  try {
1299
- answer = await readPromptAnswer(rl);
1651
+ answer = await readPromptAnswer(rl, state);
1300
1652
  } catch (error) {
1301
1653
  if (error?.code === "ERR_USE_AFTER_CLOSE") break;
1302
1654
  if (isAbortError(error)) {
@@ -1314,7 +1666,19 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
1314
1666
  }
1315
1667
 
1316
1668
  try {
1317
- await runPrompt(line, state, packageDir);
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
+ }
1318
1682
  } catch (error) {
1319
1683
  if (isAbortError(error)) {
1320
1684
  await printResumeHint(state);
@@ -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 line = JSON.stringify({
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 drainInbox() {
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`;