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.
- package/dist/interactive-swarm.js +21 -56
- package/dist/ui/text-input.d.ts +25 -0
- package/dist/ui/text-input.js +198 -0
- package/dist/ui/theme.d.ts +2 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
541
|
+
async function cmdConfigure(config, resolved) {
|
|
542
542
|
const out = process.stderr;
|
|
543
|
-
const ask =
|
|
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
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
package/dist/ui/theme.d.ts
CHANGED
|
@@ -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