@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 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. 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.4",
3
+ "version": "0.12.6",
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,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+\|\r?\nMock run complete\./.test(result.stdout)) {
115
- throw new Error("assistant response did not start on a fresh line after the aginti header");
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-newline", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest"],
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
@@ -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
  }
@@ -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
- console.log(`${prefix}${lines[0] || ""}`);
288
+ outputLine(`${prefix}${lines[0] || ""}`);
269
289
  for (const line of lines.slice(1)) {
270
- console.log(`${gutter}${line}`);
290
+ outputLine(`${gutter}${line}`);
271
291
  }
272
292
  }
273
293
 
274
294
  function printAgentMessage(text) {
275
- console.log(rolePrefix("aginti>", ansi.agentBg).trimEnd());
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) console.log(line);
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
- console.log("");
303
+ outputLine("");
284
304
  return;
285
305
  }
286
- console.log(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
306
+ outputLine(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
287
307
  }
288
308
 
289
309
  function printHeading(text) {
290
- console.log(color(stripMarkdown(text), ansi.bold, ansi.cyan));
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, press Esc or Ctrl+C once to stop gracefully and print a resume command.",
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 detachInterrupts = attachRunInterrupts(controller);
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
- console.error(`${label("error", ansi.systemBg)} ${stripMarkdown(text)}`);
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
- printStatusEvent(state, "queued_input_applied");
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
- await runPrompt(line, state, packageDir);
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);
@@ -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`;