swarm-code 0.1.14 → 0.1.15

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.
@@ -19,7 +19,7 @@
19
19
  import "./env.js";
20
20
  import * as fs from "node:fs";
21
21
  import * as path from "node:path";
22
- import * as readline from "node:readline";
22
+ import { readTextInput } from "./ui/text-input.js";
23
23
  // Dynamic imports — ensures env.js has set process.env BEFORE pi-ai loads
24
24
  await import("@mariozechner/pi-ai");
25
25
  const { PythonRepl } = await import("./core/repl.js");
@@ -538,9 +538,13 @@ function cmdStatus(threadManager, sessionStartTime, taskCount) {
538
538
  out.write("\n");
539
539
  }
540
540
  // ── Configure command ───────────────────────────────────────────────────────
541
- async function cmdConfigure(config, resolved, rl) {
541
+ async function cmdConfigure(config, resolved) {
542
542
  const out = process.stderr;
543
- const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
543
+ const ask = async (q) => {
544
+ const { readTextInput: readInput } = await import("./ui/text-input.js");
545
+ const result = await readInput(q);
546
+ return result.text;
547
+ };
544
548
  out.write("\n");
545
549
  out.write(` ${bold(cyan("Configuration"))}\n`);
546
550
  out.write(` ${dim(symbols.horizontal.repeat(40))}\n\n`);
@@ -835,56 +839,20 @@ export async function runInteractiveSwarm(rawArgs) {
835
839
  let taskCount = 0;
836
840
  // Start Python REPL
837
841
  await repl.start(sessionAc.signal);
838
- // Create readline interface
839
- const rl = readline.createInterface({
840
- input: process.stdin,
841
- output: process.stderr,
842
- prompt: isTTY ? ` ${coral("swarm")}${dim(">")} ` : "swarm> ",
843
- terminal: isTTY,
844
- });
845
- // SIGINT handling — first press cancels current task, second exits
846
842
  let currentTaskAc = null;
847
- let sigintCount = 0;
848
843
  let cleanupCalled = false;
849
- // Forward declarations for mutual references
850
- function handleSigint() {
851
- sigintCount++;
852
- if (sigintCount === 1 && currentTaskAc) {
853
- // Cancel current task
854
- process.stderr.write(`\n ${yellow("Cancelling current task...")} ${dim("(press Ctrl+C again to exit)")}\n`);
855
- currentTaskAc.abort();
856
- currentTaskAc = null;
857
- }
858
- else if (sigintCount >= 2) {
859
- // Force exit
860
- process.stderr.write(`\n ${yellow("Exiting...")}\n`);
861
- cleanup();
862
- }
863
- else {
864
- // No task running, treat as exit warning
865
- process.stderr.write(`\n ${dim("Press Ctrl+C again to exit, or type /quit")}\n`);
866
- sigintCount = 1;
867
- // Reset after 2 seconds
868
- setTimeout(() => {
869
- sigintCount = 0;
870
- }, 2000);
871
- }
872
- }
873
844
  async function cleanup() {
874
845
  if (cleanupCalled)
875
846
  return;
876
847
  cleanupCalled = true;
877
- rl.close();
878
848
  spinner.stop();
879
849
  dashboard.clear();
880
- process.removeListener("SIGINT", handleSigint);
881
850
  sessionAc.abort();
882
851
  repl.shutdown();
883
852
  await threadManager.cleanup();
884
853
  await opencodeMod.disableServerMode();
885
854
  process.exit(0);
886
855
  }
887
- process.on("SIGINT", handleSigint);
888
856
  // Thread handler (reused across tasks)
889
857
  const threadHandler = async (task, threadContext, agentBackend, model, files) => {
890
858
  let resolvedAgent = agentBackend || config.default_agent;
@@ -969,7 +937,6 @@ export async function runInteractiveSwarm(rawArgs) {
969
937
  // Run a task through the RLM loop
970
938
  const runTask = async (query) => {
971
939
  taskCount++;
972
- sigintCount = 0;
973
940
  currentTaskAc = new AbortController();
974
941
  // Link task abort to session abort
975
942
  const onSessionAbort = () => currentTaskAc?.abort();
@@ -1039,7 +1006,6 @@ export async function runInteractiveSwarm(rawArgs) {
1039
1006
  finally {
1040
1007
  sessionAc.signal.removeEventListener("abort", onSessionAbort);
1041
1008
  currentTaskAc = null;
1042
- sigintCount = 0;
1043
1009
  }
1044
1010
  };
1045
1011
  // Process a line of input
@@ -1087,7 +1053,7 @@ export async function runInteractiveSwarm(rawArgs) {
1087
1053
  case "/configure":
1088
1054
  case "/config":
1089
1055
  case "/c":
1090
- await cmdConfigure(config, resolved, rl);
1056
+ await cmdConfigure(config, resolved);
1091
1057
  break;
1092
1058
  case "/quit":
1093
1059
  case "/exit":
@@ -1103,13 +1069,19 @@ export async function runInteractiveSwarm(rawArgs) {
1103
1069
  await runTask(trimmed);
1104
1070
  return false;
1105
1071
  };
1106
- // REPL loop
1107
- rl.prompt();
1108
- rl.on("line", async (line) => {
1109
- // Pause readline during processing so prompt doesn't re-appear
1110
- rl.pause();
1072
+ // REPL loop — multi-line text input
1073
+ const promptStr = `${coral("swarm")}${dim(">")} `;
1074
+ while (!cleanupCalled) {
1075
+ const result = await readTextInput(promptStr);
1076
+ if (result.action === "escape") {
1077
+ await cleanup();
1078
+ return;
1079
+ }
1080
+ const text = result.text;
1081
+ if (!text)
1082
+ continue;
1111
1083
  try {
1112
- const shouldExit = await processLine(line);
1084
+ const shouldExit = await processLine(text);
1113
1085
  if (shouldExit) {
1114
1086
  await cleanup();
1115
1087
  return;
@@ -1119,13 +1091,6 @@ export async function runInteractiveSwarm(rawArgs) {
1119
1091
  const msg = err instanceof Error ? err.message : String(err);
1120
1092
  logError(`Unexpected error: ${msg}`);
1121
1093
  }
1122
- // Resume and show prompt again
1123
- rl.resume();
1124
- rl.prompt();
1125
- });
1126
- rl.on("close", async () => {
1127
- process.stderr.write("\n");
1128
- await cleanup();
1129
- });
1094
+ }
1130
1095
  }
1131
1096
  //# sourceMappingURL=interactive-swarm.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Text input for the interactive REPL with multi-line paste support.
3
+ *
4
+ * Uses raw stdin to detect paste vs keypress:
5
+ * - Pasted text arrives as a single data chunk containing newlines → preserved as multi-line
6
+ * - Typed Enter alone → submits the input
7
+ * - Escape → signals exit
8
+ * - Ctrl+D → submits current input
9
+ * - Ctrl+C → signals exit
10
+ * - Standard editing: backspace, left/right arrows, home/end
11
+ */
12
+ export interface TextInputResult {
13
+ text: string;
14
+ action: "submit" | "escape";
15
+ }
16
+ /**
17
+ * Read user input with multi-line paste support and escape-to-exit.
18
+ *
19
+ * Behavior:
20
+ * - Single Enter: submits current line(s)
21
+ * - Pasted text with newlines: captured as multi-line, then Enter submits all
22
+ * - Escape: returns action "escape" to signal exit
23
+ * - Ctrl+D: submits whatever is in the buffer
24
+ */
25
+ export declare function readTextInput(prompt: string): Promise<TextInputResult>;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Text input for the interactive REPL with multi-line paste support.
3
+ *
4
+ * Uses raw stdin to detect paste vs keypress:
5
+ * - Pasted text arrives as a single data chunk containing newlines → preserved as multi-line
6
+ * - Typed Enter alone → submits the input
7
+ * - Escape → signals exit
8
+ * - Ctrl+D → submits current input
9
+ * - Ctrl+C → signals exit
10
+ * - Standard editing: backspace, left/right arrows, home/end
11
+ */
12
+ import { dim, isTTY, stripAnsi } from "./theme.js";
13
+ /**
14
+ * Read user input with multi-line paste support and escape-to-exit.
15
+ *
16
+ * Behavior:
17
+ * - Single Enter: submits current line(s)
18
+ * - Pasted text with newlines: captured as multi-line, then Enter submits all
19
+ * - Escape: returns action "escape" to signal exit
20
+ * - Ctrl+D: submits whatever is in the buffer
21
+ */
22
+ export function readTextInput(prompt) {
23
+ if (!isTTY) {
24
+ return new Promise((resolve) => {
25
+ let data = "";
26
+ process.stdin.setEncoding("utf-8");
27
+ const onData = (chunk) => {
28
+ data += chunk;
29
+ if (data.includes("\n")) {
30
+ process.stdin.removeListener("data", onData);
31
+ resolve({ text: data.trim(), action: "submit" });
32
+ }
33
+ };
34
+ process.stdin.on("data", onData);
35
+ process.stdin.resume();
36
+ });
37
+ }
38
+ return new Promise((resolve) => {
39
+ const linesBuf = [""];
40
+ let cursorPos = 0; // position within current (last) line
41
+ const origRawMode = process.stdin.isRaw;
42
+ process.stdin.setRawMode(true);
43
+ process.stdin.resume();
44
+ process.stdin.setEncoding("utf-8");
45
+ const promptVisible = stripAnsi(prompt);
46
+ function redrawCurrentLine() {
47
+ const currentLine = linesBuf[linesBuf.length - 1];
48
+ process.stderr.write("\x1b[2K\x1b[0G"); // clear line
49
+ if (linesBuf.length === 1) {
50
+ // First line — show prompt
51
+ process.stderr.write(` ${prompt}${currentLine}`);
52
+ const col = 3 + promptVisible.length + cursorPos;
53
+ process.stderr.write(`\x1b[${col}G`);
54
+ }
55
+ else {
56
+ // Continuation line — indent to match
57
+ const pad = " ".repeat(promptVisible.length);
58
+ process.stderr.write(` ${dim(".")} ${pad.slice(2)}${currentLine}`);
59
+ const col = 3 + promptVisible.length + cursorPos;
60
+ process.stderr.write(`\x1b[${col}G`);
61
+ }
62
+ }
63
+ // Show initial prompt
64
+ process.stderr.write(` ${prompt}`);
65
+ const onData = (data) => {
66
+ // Check if this looks like a paste (multiple chars with newlines)
67
+ const hasNewlines = data.includes("\n") || data.includes("\r");
68
+ const isMultiChar = data.length > 1;
69
+ const isPaste = hasNewlines && isMultiChar;
70
+ if (isPaste) {
71
+ // Paste mode — split on newlines, add all to buffer
72
+ const pastedLines = data.split(/\r\n|\r|\n/);
73
+ // Append first fragment to current line at cursor
74
+ const currentLine = linesBuf[linesBuf.length - 1];
75
+ linesBuf[linesBuf.length - 1] = currentLine.slice(0, cursorPos) + pastedLines[0] + currentLine.slice(cursorPos);
76
+ // Redraw current line with pasted content
77
+ cursorPos = (currentLine.slice(0, cursorPos) + pastedLines[0]).length;
78
+ redrawCurrentLine();
79
+ // Add remaining lines
80
+ for (let i = 1; i < pastedLines.length; i++) {
81
+ const line = pastedLines[i];
82
+ if (i === pastedLines.length - 1 && line === "") {
83
+ // Trailing newline — don't add empty line
84
+ break;
85
+ }
86
+ process.stderr.write("\n");
87
+ linesBuf.push(line);
88
+ cursorPos = line.length;
89
+ redrawCurrentLine();
90
+ }
91
+ return;
92
+ }
93
+ // Character-by-character processing
94
+ for (let i = 0; i < data.length; i++) {
95
+ const ch = data[i];
96
+ // Escape sequences
97
+ if (ch === "\x1b") {
98
+ if (data[i + 1] === "[") {
99
+ const code = data[i + 2];
100
+ if (code === "C") {
101
+ // Right arrow
102
+ if (cursorPos < linesBuf[linesBuf.length - 1].length) {
103
+ cursorPos++;
104
+ redrawCurrentLine();
105
+ }
106
+ i += 2;
107
+ continue;
108
+ }
109
+ if (code === "D") {
110
+ // Left arrow
111
+ if (cursorPos > 0) {
112
+ cursorPos--;
113
+ redrawCurrentLine();
114
+ }
115
+ i += 2;
116
+ continue;
117
+ }
118
+ if (code === "H") {
119
+ // Home
120
+ cursorPos = 0;
121
+ redrawCurrentLine();
122
+ i += 2;
123
+ continue;
124
+ }
125
+ if (code === "F") {
126
+ // End
127
+ cursorPos = linesBuf[linesBuf.length - 1].length;
128
+ redrawCurrentLine();
129
+ i += 2;
130
+ continue;
131
+ }
132
+ // Skip other escape sequences
133
+ i += 2;
134
+ continue;
135
+ }
136
+ // Bare Escape key — exit
137
+ finish();
138
+ resolve({ text: "", action: "escape" });
139
+ return;
140
+ }
141
+ // Ctrl+D — submit
142
+ if (ch === "\x04") {
143
+ const text = linesBuf.join("\n").trim();
144
+ finish();
145
+ resolve({ text, action: "submit" });
146
+ return;
147
+ }
148
+ // Ctrl+C — exit
149
+ if (ch === "\x03") {
150
+ finish();
151
+ resolve({ text: "", action: "escape" });
152
+ return;
153
+ }
154
+ // Enter — submit
155
+ if (ch === "\r" || ch === "\n") {
156
+ const text = linesBuf.join("\n").trim();
157
+ finish();
158
+ resolve({ text, action: "submit" });
159
+ return;
160
+ }
161
+ // Backspace
162
+ if (ch === "\x7f" || ch === "\b") {
163
+ if (cursorPos > 0) {
164
+ const line = linesBuf[linesBuf.length - 1];
165
+ linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos - 1) + line.slice(cursorPos);
166
+ cursorPos--;
167
+ redrawCurrentLine();
168
+ }
169
+ continue;
170
+ }
171
+ // Tab — insert spaces
172
+ if (ch === "\t") {
173
+ const line = linesBuf[linesBuf.length - 1];
174
+ linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos) + " " + line.slice(cursorPos);
175
+ cursorPos += 2;
176
+ redrawCurrentLine();
177
+ continue;
178
+ }
179
+ // Regular printable character
180
+ if (ch >= " ") {
181
+ const line = linesBuf[linesBuf.length - 1];
182
+ linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos) + ch + line.slice(cursorPos);
183
+ cursorPos++;
184
+ redrawCurrentLine();
185
+ }
186
+ }
187
+ };
188
+ function finish() {
189
+ process.stdin.removeListener("data", onData);
190
+ if (origRawMode !== undefined) {
191
+ process.stdin.setRawMode(origRawMode);
192
+ }
193
+ process.stderr.write("\n");
194
+ }
195
+ process.stdin.on("data", onData);
196
+ });
197
+ }
198
+ //# sourceMappingURL=text-input.js.map
@@ -41,13 +41,13 @@ export declare const symbols: {
41
41
  readonly info: "●" | "[*]";
42
42
  readonly arrow: "▶" | ">";
43
43
  readonly dot: "." | "·";
44
- readonly dash: "-" | "";
44
+ readonly dash: "" | "-";
45
45
  readonly vertLine: "│" | "|";
46
46
  readonly topLeft: "╭" | "+";
47
47
  readonly topRight: "+" | "╮";
48
48
  readonly bottomLeft: "+" | "╰";
49
49
  readonly bottomRight: "+" | "╯";
50
- readonly horizontal: "-" | "";
50
+ readonly horizontal: "" | "-";
51
51
  readonly spinner: string[];
52
52
  };
53
53
  /** Get terminal width, with a sensible fallback. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarm-code",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Open-source swarm-native coding agent orchestrator — spawns parallel coding agents in isolated git worktrees, built on RLM (arXiv:2512.24601)",
5
5
  "type": "module",
6
6
  "bin": {