smolcoder 0.4.2 → 0.5.0
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 +12 -1
- package/dist/agent.js +9 -0
- package/dist/config.js +72 -0
- package/dist/index.js +85 -396
- package/dist/session.js +480 -0
- package/dist/tools/tasks.js +15 -0
- package/dist/web/channel.js +222 -0
- package/dist/web/client.js +815 -0
- package/dist/web/hub.js +786 -0
- package/dist/web/page.js +73 -366
- package/dist/web/store.js +199 -0
- package/dist/web/styles.js +222 -0
- package/dist/web/terminal.js +190 -0
- package/package.json +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SessionChannel: the SessionUI a web session talks to. Same contract the
|
|
3
|
+
// terminal Tui implements, but every event is tagged with the session id and
|
|
4
|
+
// handed to the hub, which multiplexes all sessions onto one SSE stream.
|
|
5
|
+
// Keeps a replay buffer (so a page that connects later sees the transcript),
|
|
6
|
+
// the pending input/approval promises, and a coarse status for the sidebar.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.SessionChannel = void 0;
|
|
9
|
+
exports.stripAnsi = stripAnsi;
|
|
10
|
+
const ui_1 = require("../ui");
|
|
11
|
+
const REPLAY_CAP = 2000;
|
|
12
|
+
class SessionChannel {
|
|
13
|
+
id;
|
|
14
|
+
host;
|
|
15
|
+
slashCommands = [];
|
|
16
|
+
getStatus = () => "";
|
|
17
|
+
/** Structured snapshot for the page's status bar; assigned by the hub. */
|
|
18
|
+
getState = () => ({});
|
|
19
|
+
hintLeft = "";
|
|
20
|
+
onModeCycle = null;
|
|
21
|
+
onCancel = null;
|
|
22
|
+
onExit = null;
|
|
23
|
+
phase = "starting";
|
|
24
|
+
title = "";
|
|
25
|
+
busyLabel = null;
|
|
26
|
+
replay = [];
|
|
27
|
+
closed = false;
|
|
28
|
+
pendingInput = null;
|
|
29
|
+
inputQueue = [];
|
|
30
|
+
exitRequested = false;
|
|
31
|
+
pending = new Map();
|
|
32
|
+
askId = 0;
|
|
33
|
+
constructor(id, host) {
|
|
34
|
+
this.id = id;
|
|
35
|
+
this.host = host;
|
|
36
|
+
}
|
|
37
|
+
// ---- lifecycle -----------------------------------------------------------
|
|
38
|
+
start() { }
|
|
39
|
+
close() {
|
|
40
|
+
this.closed = true;
|
|
41
|
+
this.busyLabel = null;
|
|
42
|
+
this.resolvePending(null);
|
|
43
|
+
this.host.changed(this.id);
|
|
44
|
+
}
|
|
45
|
+
/** Make the session loop's next readInput() return "/exit" without echoing
|
|
46
|
+
* it as a user message. Used when a session is closed from the sidebar. */
|
|
47
|
+
requestExit() {
|
|
48
|
+
this.exitRequested = true;
|
|
49
|
+
if (this.pendingInput) {
|
|
50
|
+
const r = this.pendingInput;
|
|
51
|
+
this.pendingInput = null;
|
|
52
|
+
r("/exit");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ---- input from the page -------------------------------------------------
|
|
56
|
+
handleMessage(text) {
|
|
57
|
+
text = text.trim();
|
|
58
|
+
if (!text || this.closed)
|
|
59
|
+
return;
|
|
60
|
+
if (this.pendingInput) {
|
|
61
|
+
const resolve = this.pendingInput;
|
|
62
|
+
this.pendingInput = null;
|
|
63
|
+
this.accept(text);
|
|
64
|
+
resolve(text);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.inputQueue.push(text);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
handleAnswer(id, value) {
|
|
71
|
+
if (!this.pending.has(id))
|
|
72
|
+
return;
|
|
73
|
+
this.setStatus("busy");
|
|
74
|
+
this.settle(id, value);
|
|
75
|
+
}
|
|
76
|
+
/** Stop button / esc: answer any open prompt with "no"/cancel, then abort
|
|
77
|
+
* the turn. Without the first step a cancel during an approval prompt
|
|
78
|
+
* would leave the agent waiting on a promise nothing will resolve. */
|
|
79
|
+
cancel() {
|
|
80
|
+
this.resolvePending(null);
|
|
81
|
+
this.onCancel?.();
|
|
82
|
+
}
|
|
83
|
+
resolvePending(value) {
|
|
84
|
+
for (const id of [...this.pending.keys()])
|
|
85
|
+
this.settle(id, value);
|
|
86
|
+
}
|
|
87
|
+
/** Resolve one prompt and record the outcome in the transcript, so a page
|
|
88
|
+
* that replays it later (reconnect, resume) sees the answer, not live
|
|
89
|
+
* buttons for a question nobody is asking any more. */
|
|
90
|
+
settle(id, value) {
|
|
91
|
+
const p = this.pending.get(id);
|
|
92
|
+
if (!p)
|
|
93
|
+
return;
|
|
94
|
+
this.pending.delete(id);
|
|
95
|
+
this.broadcast({ t: "answered", id });
|
|
96
|
+
if (p.kind === "confirm") {
|
|
97
|
+
const a = value === "always" ? "always allow" : value === "yes" ? "yes" : "no";
|
|
98
|
+
this.broadcast({ t: "line", kind: "status", s: `${a} — ${p.label}` });
|
|
99
|
+
}
|
|
100
|
+
else if (value === null || value === undefined) {
|
|
101
|
+
this.broadcast({ t: "line", kind: "status", s: `cancelled — ${p.label}` });
|
|
102
|
+
}
|
|
103
|
+
p.resolve(value);
|
|
104
|
+
}
|
|
105
|
+
accept(text) {
|
|
106
|
+
this.setStatus("busy");
|
|
107
|
+
this.broadcast({ t: "user", s: text });
|
|
108
|
+
if (!this.title && !text.startsWith("/")) {
|
|
109
|
+
this.title = text.replace(/\s+/g, " ").slice(0, 60);
|
|
110
|
+
this.host.changed(this.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
setStatus(s) {
|
|
114
|
+
if (this.phase === s)
|
|
115
|
+
return;
|
|
116
|
+
this.phase = s;
|
|
117
|
+
this.host.changed(this.id);
|
|
118
|
+
}
|
|
119
|
+
// ---- event plumbing ------------------------------------------------------
|
|
120
|
+
broadcast(ev) {
|
|
121
|
+
ev.sid = this.id;
|
|
122
|
+
if (ev.t !== "busy" && ev.t !== "state") {
|
|
123
|
+
this.replay.push(ev);
|
|
124
|
+
if (this.replay.length > REPLAY_CAP)
|
|
125
|
+
this.replay.shift();
|
|
126
|
+
this.host.touched(this.id);
|
|
127
|
+
}
|
|
128
|
+
this.host.send(ev);
|
|
129
|
+
}
|
|
130
|
+
stateEvent() {
|
|
131
|
+
return { t: "state", sid: this.id, s: { ...this.getState(), busy: this.busyLabel, title: this.title } };
|
|
132
|
+
}
|
|
133
|
+
pushState() {
|
|
134
|
+
this.host.send(this.stateEvent());
|
|
135
|
+
}
|
|
136
|
+
refresh() {
|
|
137
|
+
this.pushState();
|
|
138
|
+
}
|
|
139
|
+
// ---- SessionUI -----------------------------------------------------------
|
|
140
|
+
readInput() {
|
|
141
|
+
this.busyLabel = null;
|
|
142
|
+
this.setStatus("idle");
|
|
143
|
+
this.pushState();
|
|
144
|
+
if (this.exitRequested || this.closed)
|
|
145
|
+
return Promise.resolve("/exit");
|
|
146
|
+
if (this.inputQueue.length) {
|
|
147
|
+
const text = this.inputQueue.shift();
|
|
148
|
+
this.accept(text);
|
|
149
|
+
return Promise.resolve(text);
|
|
150
|
+
}
|
|
151
|
+
return new Promise((resolve) => (this.pendingInput = resolve));
|
|
152
|
+
}
|
|
153
|
+
select(title, options) {
|
|
154
|
+
const id = ++this.askId;
|
|
155
|
+
this.broadcast({ t: "select", id, title, options });
|
|
156
|
+
this.setStatus("waiting");
|
|
157
|
+
return new Promise((resolve) => this.pending.set(id, { kind: "select", label: title, resolve: (i) => resolve(typeof i === "number" ? i : null) }));
|
|
158
|
+
}
|
|
159
|
+
confirmCommand(command, reason) {
|
|
160
|
+
const id = ++this.askId;
|
|
161
|
+
this.broadcast({ t: "confirm", id, command, reason });
|
|
162
|
+
this.setStatus("waiting");
|
|
163
|
+
return new Promise((resolve) => this.pending.set(id, {
|
|
164
|
+
kind: "confirm",
|
|
165
|
+
label: command,
|
|
166
|
+
resolve: (a) => resolve(a === "always" ? "always" : a === "yes" ? "yes" : "no"),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
token(text) {
|
|
170
|
+
this.broadcast({ t: "token", s: text });
|
|
171
|
+
}
|
|
172
|
+
thinking(text) {
|
|
173
|
+
this.broadcast({ t: "thinking", s: text });
|
|
174
|
+
}
|
|
175
|
+
toolCall(name, args) {
|
|
176
|
+
this.broadcast({ t: "tool", name, summary: (0, ui_1.summarizeArgs)(name, args) });
|
|
177
|
+
}
|
|
178
|
+
toolResult(result) {
|
|
179
|
+
const lines = result.split("\n");
|
|
180
|
+
const first = lines[0] ?? "";
|
|
181
|
+
this.broadcast({
|
|
182
|
+
t: "result",
|
|
183
|
+
line: first.slice(0, 160),
|
|
184
|
+
err: first.startsWith("Error"),
|
|
185
|
+
extra: lines.length > 1 ? lines.length - 1 : 0,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
planUpdated(plan) {
|
|
189
|
+
this.broadcast({ t: "plan", steps: plan.steps, current: plan.currentIndex });
|
|
190
|
+
this.pushState();
|
|
191
|
+
}
|
|
192
|
+
println(s = "") {
|
|
193
|
+
if (s.trim())
|
|
194
|
+
this.broadcast({ t: "line", kind: "status", s: stripAnsi(s) });
|
|
195
|
+
}
|
|
196
|
+
status(s) {
|
|
197
|
+
this.broadcast({ t: "line", kind: "status", s: stripAnsi(s) });
|
|
198
|
+
}
|
|
199
|
+
warn(s) {
|
|
200
|
+
this.broadcast({ t: "line", kind: "warn", s: stripAnsi(s) });
|
|
201
|
+
}
|
|
202
|
+
error(s) {
|
|
203
|
+
this.broadcast({ t: "line", kind: "error", s: stripAnsi(s) });
|
|
204
|
+
}
|
|
205
|
+
turnEnd(label) {
|
|
206
|
+
this.broadcast({ t: "turnend", label });
|
|
207
|
+
this.pushState();
|
|
208
|
+
}
|
|
209
|
+
startSpinner(label) {
|
|
210
|
+
this.busyLabel = label;
|
|
211
|
+
this.broadcast({ t: "busy", label });
|
|
212
|
+
}
|
|
213
|
+
stopSpinner() {
|
|
214
|
+
this.busyLabel = null;
|
|
215
|
+
this.broadcast({ t: "busy", label: null });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
exports.SessionChannel = SessionChannel;
|
|
219
|
+
function stripAnsi(s) {
|
|
220
|
+
// eslint-disable-next-line no-control-regex
|
|
221
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
222
|
+
}
|