smolcoder 0.4.3 → 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 +79 -390
- 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
package/dist/web/page.js
CHANGED
|
@@ -1,381 +1,88 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// The
|
|
3
|
-
//
|
|
2
|
+
// The web page: one self-contained HTML document (styles + script inlined,
|
|
3
|
+
// no dependencies) served by the hub. Structure only — the styling lives in
|
|
4
|
+
// styles.ts and the behaviour in client.ts.
|
|
4
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
6
|
exports.PAGE_HTML = void 0;
|
|
7
|
+
const client_1 = require("./client");
|
|
8
|
+
const styles_1 = require("./styles");
|
|
9
|
+
const LOGO = `███████╗ ███╗ ███╗ ██████╗ ██╗
|
|
10
|
+
██╔════╝ ████╗ ████║ ██╔═══██╗ ██║
|
|
11
|
+
███████╗ ██╔████╔██║ ██║ ██║ ██║
|
|
12
|
+
╚════██║ ██║╚██╔╝██║ ██║ ██║ ██║
|
|
13
|
+
███████║ ██║ ╚═╝ ██║ ╚██████╔╝ ███████╗
|
|
14
|
+
╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝`;
|
|
15
|
+
const FAVICON = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230b0d0e'/%3E%3Ctext x='16' y='23' font-size='19' font-family='monospace' font-weight='700' text-anchor='middle' fill='%2335bfd4'%3ES%3C/text%3E%3C/svg%3E";
|
|
16
|
+
const ICON_BROWSER = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
|
17
|
+
const ICON_TERMINAL = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
|
|
6
18
|
exports.PAGE_HTML = `<!doctype html>
|
|
7
19
|
<html>
|
|
8
20
|
<head>
|
|
9
21
|
<meta charset="utf-8">
|
|
10
22
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
-
<title>
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
--bg: #0b0d0e; --fg: #d6dbde; --dim: #6b7480; --gray: #4a525c;
|
|
15
|
-
--accent: #35bfd4; --yellow: #e0af68; --red: #f7768e; --green: #9ece6a;
|
|
16
|
-
--magenta: #bb9af7; --box: #14181a; --sel: #1a7f94;
|
|
17
|
-
}
|
|
18
|
-
* { box-sizing: border-box; }
|
|
19
|
-
body {
|
|
20
|
-
margin: 0; background: var(--bg); color: var(--fg);
|
|
21
|
-
font: 14px/1.5 ui-monospace, "Cascadia Code", Consolas, monospace;
|
|
22
|
-
}
|
|
23
|
-
#wrap { max-width: 920px; margin: 0 auto; padding: 20px 16px 170px; }
|
|
24
|
-
#logo { color: var(--accent); white-space: pre; font-size: 11px; line-height: 1.15; margin: 8px 0 2px; }
|
|
25
|
-
#logo .coder { color: var(--dim); }
|
|
26
|
-
#log { margin-top: 16px; }
|
|
27
|
-
.user { border-left: 3px solid var(--accent); background: var(--box); padding: 8px 12px; margin: 18px 0 10px; font-weight: 600; white-space: pre-wrap; }
|
|
28
|
-
.thought { color: var(--gray); white-space: nowrap; overflow: hidden; margin-top: 4px; }
|
|
29
|
-
.md { white-space: normal; }
|
|
30
|
-
.md p { margin: 6px 0; }
|
|
31
|
-
.md h1, .md h2, .md h3, .md h4, .md h5, .md h6 { margin: 14px 0 6px; line-height: 1.3; color: #eef3f5; }
|
|
32
|
-
.md h1 { font-size: 1.3em; } .md h2 { font-size: 1.17em; } .md h3 { font-size: 1.06em; }
|
|
33
|
-
.md h4, .md h5, .md h6 { font-size: 1em; }
|
|
34
|
-
.md ul, .md ol { margin: 6px 0; padding-left: 22px; }
|
|
35
|
-
.md li { margin: 2px 0; }
|
|
36
|
-
.md strong { color: #eef3f5; }
|
|
37
|
-
.md code { background: #1b2124; padding: 1px 5px; border-radius: 3px; color: var(--yellow); }
|
|
38
|
-
.md pre { background: #11161a; border: 1px solid #232a2f; border-radius: 4px; padding: 10px 12px; overflow-x: auto; margin: 8px 0; }
|
|
39
|
-
.md pre code { background: none; padding: 0; color: var(--fg); }
|
|
40
|
-
.md table { border-collapse: collapse; margin: 8px 0; display: block; overflow-x: auto; max-width: 100%; }
|
|
41
|
-
.md th, .md td { border: 1px solid #232a2f; padding: 4px 10px; text-align: left; }
|
|
42
|
-
.md th { background: #171d20; color: #eef3f5; }
|
|
43
|
-
.md blockquote { border-left: 3px solid #2c343a; margin: 8px 0; padding-left: 12px; color: var(--dim); }
|
|
44
|
-
.md hr { border: 0; border-top: 1px solid #232a2f; margin: 12px 0; }
|
|
45
|
-
.md a { color: var(--accent); }
|
|
46
|
-
.tool { color: var(--dim); margin-top: 4px; }
|
|
47
|
-
.tool .name { color: var(--accent); font-weight: 600; }
|
|
48
|
-
.result { color: var(--dim); padding-left: 16px; }
|
|
49
|
-
.result.err { color: var(--red); }
|
|
50
|
-
.plan { background: var(--box); border-left: 3px solid var(--accent); padding: 8px 12px; margin: 10px 0; }
|
|
51
|
-
.plan .hdr { font-weight: 700; } .plan .hdr small { color: var(--dim); font-weight: 400; }
|
|
52
|
-
.plan .done { color: var(--gray); text-decoration: line-through; }
|
|
53
|
-
.plan .cur { color: var(--accent); font-weight: 600; }
|
|
54
|
-
.plan .todo { color: var(--dim); }
|
|
55
|
-
.turnend { color: var(--gray); margin: 8px 0 4px; }
|
|
56
|
-
.line-status { color: var(--gray); } .line-warn { color: var(--yellow); } .line-error { color: var(--red); }
|
|
57
|
-
#busy { color: var(--dim); display: none; }
|
|
58
|
-
#busy.on { display: block; }
|
|
59
|
-
#busy .spin { display: inline-block; color: var(--accent); animation: pulse 1s infinite; }
|
|
60
|
-
@keyframes pulse { 50% { opacity: .3; } }
|
|
61
|
-
.ask { background: var(--box); border-left: 3px solid var(--yellow); padding: 10px 12px; margin: 10px 0; }
|
|
62
|
-
.ask .cmd { font-weight: 700; }
|
|
63
|
-
.ask button, .ask .opt { margin: 6px 8px 0 0; background: #1e2428; color: var(--fg); border: 1px solid #2c343a; padding: 4px 12px; cursor: pointer; font: inherit; border-radius: 3px; }
|
|
64
|
-
.ask button:hover, .ask .opt:hover { border-color: var(--accent); }
|
|
65
|
-
.ask .opt.current { border-color: var(--green); }
|
|
66
|
-
.ask .opt .hint { color: var(--dim); font-size: 12px; margin-left: 8px; }
|
|
67
|
-
.ask > .hint { color: var(--dim); font-size: 12px; margin-top: 2px; }
|
|
68
|
-
#bottom { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg); padding: 8px 16px 14px; }
|
|
69
|
-
#bottom .inner { max-width: 920px; margin: 0 auto; position: relative; }
|
|
70
|
-
#menu { position: absolute; bottom: 100%; left: 0; right: 0; background: var(--box); border: 1px solid #232a2f; display: none; }
|
|
71
|
-
#menu .item { padding: 4px 10px; cursor: pointer; }
|
|
72
|
-
#menu .item .nm { font-weight: 700; } #menu .item .ds { color: var(--dim); margin-left: 10px; }
|
|
73
|
-
#menu .item.sel { background: var(--sel); color: #f2f7f8; }
|
|
74
|
-
#menu .item.sel .ds { color: #c8dde2; }
|
|
75
|
-
#inputbox { border-left: 3px solid var(--accent); background: var(--box); padding: 8px 12px; }
|
|
76
|
-
.inputrow { display: flex; align-items: flex-end; gap: 10px; }
|
|
77
|
-
#input { flex: 1; background: transparent; border: 0; outline: 0; color: var(--fg); font: inherit; resize: none; }
|
|
78
|
-
#actionbtn { flex: none; background: #1e2428; color: var(--dim); border: 1px solid #2c343a; padding: 3px 14px; cursor: pointer; font: inherit; font-size: 12px; border-radius: 3px; }
|
|
79
|
-
#actionbtn:hover { border-color: var(--accent); color: var(--accent); }
|
|
80
|
-
#actionbtn.stop { color: var(--red); border-color: #3d2d31; }
|
|
81
|
-
#actionbtn.stop:hover { border-color: var(--red); color: var(--red); }
|
|
82
|
-
#status { margin-top: 6px; font-size: 12.5px; color: var(--dim); }
|
|
83
|
-
#status .mode { font-weight: 700; }
|
|
84
|
-
#status .mode.edit { color: var(--accent); } #status .mode.bypass { color: var(--red); } #status .mode.ro { color: var(--magenta); }
|
|
85
|
-
#status .eff { color: var(--yellow); } #status .plan-chip { color: var(--accent); } #status .plan-chip.done { color: var(--green); }
|
|
86
|
-
#hint { font-size: 12px; color: var(--gray); margin-top: 4px; }
|
|
87
|
-
</style>
|
|
23
|
+
<title>smol</title>
|
|
24
|
+
<link rel="icon" href="${FAVICON}">
|
|
25
|
+
<style>${styles_1.STYLES}</style>
|
|
88
26
|
</head>
|
|
89
27
|
<body>
|
|
90
|
-
<
|
|
91
|
-
<div
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<div id="
|
|
98
|
-
<div
|
|
28
|
+
<aside id="side">
|
|
29
|
+
<div class="sidehdr">
|
|
30
|
+
<span class="brand">smol<span class="coder">coder</span></span>
|
|
31
|
+
<span class="grow"></span>
|
|
32
|
+
<button class="iconbtn" id="sidecollapse" title="hide sidebar (ctrl+b)">«</button>
|
|
33
|
+
</div>
|
|
34
|
+
<button id="openfolder" class="ghost">+ Open folder…</button>
|
|
35
|
+
<div id="wslist"></div>
|
|
36
|
+
<div class="sidefoot"><span id="ver"></span><span class="grow"></span><span title="keyboard: ctrl+b sidebar · ctrl+\` terminal · shift+tab mode · esc cancel">keys ?</span></div>
|
|
37
|
+
</aside>
|
|
38
|
+
<div id="main">
|
|
39
|
+
<div id="top">
|
|
40
|
+
<button class="iconbtn" id="sidetoggle" title="show sidebar (ctrl+b)" hidden>☰</button>
|
|
41
|
+
<div id="crumb"></div>
|
|
42
|
+
<button class="iconbtn" id="btnbrowser" title="browser panel: preview a dev server next to the chat">${ICON_BROWSER}</button>
|
|
43
|
+
<button class="iconbtn" id="btnterm" title="terminal panel (ctrl+\`)">${ICON_TERMINAL}</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div id="logwrap">
|
|
46
|
+
<div id="welcome" hidden>
|
|
47
|
+
<div id="logo">${LOGO} <span class="coder">coder — web</span></div>
|
|
48
|
+
<p>Pick a session in the sidebar, or open a folder to start one. Each workspace keeps its own sessions; you can run several at once and switch between them while they work.</p>
|
|
49
|
+
<div class="row"><button class="primary" id="welcomeopen">Open a folder…</button></div>
|
|
50
|
+
<div id="recent"></div>
|
|
51
|
+
</div>
|
|
52
|
+
<div id="logs"></div>
|
|
53
|
+
<div id="busywrap"><div id="busy"><span class="spin">⠋</span> <span id="busylabel">thinking…</span> <span id="busysecs"></span></div></div>
|
|
54
|
+
</div>
|
|
55
|
+
<div id="bottom" hidden><div class="inner">
|
|
56
|
+
<div id="menu"></div>
|
|
57
|
+
<div id="inputbox">
|
|
58
|
+
<div class="inputrow">
|
|
59
|
+
<textarea id="input" rows="1" placeholder='Ask anything… "/" for commands'></textarea>
|
|
60
|
+
<button id="actionbtn" title="send (enter)">send</button>
|
|
61
|
+
</div>
|
|
62
|
+
<div id="status">connecting…</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="hintrow"><span id="ws"></span> / commands · shift+tab mode · enter send · esc cancel · ctrl+b sidebar · ctrl+\` terminal</div>
|
|
65
|
+
</div></div>
|
|
99
66
|
</div>
|
|
100
|
-
<div id="
|
|
101
|
-
<div id="
|
|
102
|
-
<div id="
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
67
|
+
<div id="panel" hidden>
|
|
68
|
+
<div id="panelgrip" title="drag to resize"></div>
|
|
69
|
+
<div id="paneltabs"></div>
|
|
70
|
+
<div id="panelviews"></div>
|
|
71
|
+
</div>
|
|
72
|
+
<div id="modal" hidden>
|
|
73
|
+
<div class="dlg">
|
|
74
|
+
<div class="dlghdr">Open a folder as a workspace<span class="grow"></span><button class="iconbtn" id="fsclose" title="close">×</button></div>
|
|
75
|
+
<div class="pathrow"><input id="fspath" spellcheck="false" placeholder="type or paste a path"><button class="ghost" id="fsgo">go</button></div>
|
|
76
|
+
<div id="fsroots"></div>
|
|
77
|
+
<div id="fslist"></div>
|
|
78
|
+
<div class="dlgfoot">
|
|
79
|
+
<button class="primary" id="fsopen">Open this folder</button>
|
|
80
|
+
<label><input type="checkbox" id="fsstart" checked> start a session</label>
|
|
81
|
+
<span class="grow"></span>
|
|
82
|
+
<span>✦ looks like a project · double-click a folder to open it</span>
|
|
106
83
|
</div>
|
|
107
|
-
<div id="status">connecting…</div>
|
|
108
84
|
</div>
|
|
109
|
-
|
|
110
|
-
</
|
|
111
|
-
<script>
|
|
112
|
-
const k = new URLSearchParams(location.search).get("k") || "";
|
|
113
|
-
const log = document.getElementById("log");
|
|
114
|
-
const busyEl = document.getElementById("busy");
|
|
115
|
-
const actionBtn = document.getElementById("actionbtn");
|
|
116
|
-
let isBusy = false;
|
|
117
|
-
let state = { commands: [] };
|
|
118
|
-
let curText = null, curThought = null, busyTimer = null, busyStart = 0;
|
|
119
|
-
let thoughtBuf = "", thoughtStart = 0;
|
|
120
|
-
function endThought() {
|
|
121
|
-
if (curThought) {
|
|
122
|
-
curThought.textContent = "✦ thought for " + ((Date.now() - thoughtStart) / 1000).toFixed(1) + "s";
|
|
123
|
-
curThought = null; thoughtBuf = "";
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function el(tag, cls, text) { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; }
|
|
128
|
-
|
|
129
|
-
// ---- markdown ----------------------------------------------------------
|
|
130
|
-
// Model output is untrusted: escape everything first, then build tags
|
|
131
|
-
// ourselves. Nothing from the model is ever inserted as raw HTML.
|
|
132
|
-
function esc(s) {
|
|
133
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
134
|
-
}
|
|
135
|
-
function inlineMd(s) {
|
|
136
|
-
const codes = [];
|
|
137
|
-
s = s.replace(/\`([^\`]+)\`/g, (m, c) => { codes.push(c); return "\\u0000" + (codes.length - 1) + "\\u0000"; });
|
|
138
|
-
s = s.replace(/\\*\\*\\*([^*]+)\\*\\*\\*/g, "<strong><em>$1</em></strong>");
|
|
139
|
-
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>");
|
|
140
|
-
s = s.replace(/(^|[^*])\\*([^*\\n]+)\\*/g, "$1<em>$2</em>");
|
|
141
|
-
s = s.replace(/~~([^~]+)~~/g, "<del>$1</del>");
|
|
142
|
-
s = s.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)"]+)\\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
143
|
-
s = s.replace(/\\u0000(\\d+)\\u0000/g, (m, i) => "<code>" + codes[i] + "</code>");
|
|
144
|
-
return s;
|
|
145
|
-
}
|
|
146
|
-
function renderMarkdown(src) {
|
|
147
|
-
const lines = esc(src).split("\\n");
|
|
148
|
-
let out = "", i = 0, listType = null;
|
|
149
|
-
const closeList = () => { if (listType) { out += "</" + listType + ">"; listType = null; } };
|
|
150
|
-
const cells = (row) => row.trim().replace(/^\\||\\|$/g, "").split("|").map((c) => c.trim());
|
|
151
|
-
while (i < lines.length) {
|
|
152
|
-
const line = lines[i];
|
|
153
|
-
const fence = /^\\s*\`\`\`(\\w*)\\s*$/.exec(line);
|
|
154
|
-
if (fence) {
|
|
155
|
-
closeList();
|
|
156
|
-
const body = []; i++;
|
|
157
|
-
while (i < lines.length && !/^\\s*\`\`\`/.test(lines[i])) { body.push(lines[i]); i++; }
|
|
158
|
-
i++;
|
|
159
|
-
out += "<pre><code>" + body.join("\\n") + "</code></pre>";
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
if (/^\\s*\\|/.test(line) && i + 1 < lines.length && /^\\s*\\|?[\\s:|-]+\\|[\\s:|-]*$/.test(lines[i + 1])) {
|
|
163
|
-
closeList();
|
|
164
|
-
const head = cells(line); i += 2;
|
|
165
|
-
const rows = [];
|
|
166
|
-
while (i < lines.length && /^\\s*\\|/.test(lines[i])) { rows.push(cells(lines[i])); i++; }
|
|
167
|
-
out += "<table><thead><tr>" + head.map((h) => "<th>" + inlineMd(h) + "</th>").join("") + "</tr></thead><tbody>";
|
|
168
|
-
for (const r of rows) out += "<tr>" + r.map((c) => "<td>" + inlineMd(c) + "</td>").join("") + "</tr>";
|
|
169
|
-
out += "</tbody></table>";
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
const h = /^(#{1,6})\\s+(.*)$/.exec(line);
|
|
173
|
-
if (h) { closeList(); out += "<h" + h[1].length + ">" + inlineMd(h[2]) + "</h" + h[1].length + ">"; i++; continue; }
|
|
174
|
-
if (/^\\s*([-*_])\\s*\\1\\s*\\1[\\s\\-*_]*$/.test(line)) { closeList(); out += "<hr>"; i++; continue; }
|
|
175
|
-
// NB: lines are already escaped, so the blockquote marker is ">".
|
|
176
|
-
if (/^\\s*>\\s?/.test(line)) {
|
|
177
|
-
closeList();
|
|
178
|
-
const body = [];
|
|
179
|
-
while (i < lines.length && /^\\s*>\\s?/.test(lines[i])) { body.push(lines[i].replace(/^\\s*>\\s?/, "")); i++; }
|
|
180
|
-
out += "<blockquote>" + inlineMd(body.join(" ")) + "</blockquote>";
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
const ul = /^\\s*[-*+]\\s+(.*)$/.exec(line);
|
|
184
|
-
const ol = /^\\s*\\d+[.)]\\s+(.*)$/.exec(line);
|
|
185
|
-
if (ul || ol) {
|
|
186
|
-
const want = ul ? "ul" : "ol";
|
|
187
|
-
if (listType !== want) { closeList(); out += "<" + want + ">"; listType = want; }
|
|
188
|
-
out += "<li>" + inlineMd((ul || ol)[1]) + "</li>";
|
|
189
|
-
i++; continue;
|
|
190
|
-
}
|
|
191
|
-
if (!line.trim()) { closeList(); i++; continue; }
|
|
192
|
-
closeList();
|
|
193
|
-
const para = [line]; i++;
|
|
194
|
-
while (i < lines.length && lines[i].trim() &&
|
|
195
|
-
!/^(\\s*#{1,6}\\s|\\s*\`\`\`|\\s*>\\s?|\\s*[-*+]\\s|\\s*\\d+[.)]\\s|\\s*\\|)/.test(lines[i])) { para.push(lines[i]); i++; }
|
|
196
|
-
out += "<p>" + inlineMd(para.join(" ")) + "</p>";
|
|
197
|
-
}
|
|
198
|
-
closeList();
|
|
199
|
-
return out;
|
|
200
|
-
}
|
|
201
|
-
// Streaming: accumulate raw markdown on the element, re-render on a short
|
|
202
|
-
// timer. NOT requestAnimationFrame — rAF never fires in background tabs, so a
|
|
203
|
-
// response streamed while the tab is hidden would never render.
|
|
204
|
-
function scheduleMd(target) {
|
|
205
|
-
if (target._pending) return;
|
|
206
|
-
target._pending = true;
|
|
207
|
-
setTimeout(() => {
|
|
208
|
-
target._pending = false;
|
|
209
|
-
target.innerHTML = renderMarkdown(target._raw || "");
|
|
210
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
211
|
-
}, 60);
|
|
212
|
-
}
|
|
213
|
-
function add(e) { log.appendChild(e); window.scrollTo(0, document.body.scrollHeight); return e; }
|
|
214
|
-
function post(path, body) { return fetch(path + "?k=" + k, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body || {}) }); }
|
|
215
|
-
|
|
216
|
-
function setBusy(label) {
|
|
217
|
-
// The one button right of the input is send when idle, stop while running.
|
|
218
|
-
isBusy = !!label;
|
|
219
|
-
actionBtn.textContent = isBusy ? "■ stop" : "send";
|
|
220
|
-
actionBtn.className = isBusy ? "stop" : "";
|
|
221
|
-
actionBtn.title = isBusy ? "interrupt the agent (esc)" : "send (enter)";
|
|
222
|
-
if (label) {
|
|
223
|
-
busyEl.classList.add("on"); busyStart = Date.now();
|
|
224
|
-
document.getElementById("busylabel").textContent = label + "…";
|
|
225
|
-
if (!busyTimer) busyTimer = setInterval(() => {
|
|
226
|
-
const s = Math.floor((Date.now() - busyStart) / 1000);
|
|
227
|
-
document.getElementById("busysecs").textContent = s > 2 ? s + "s" : "";
|
|
228
|
-
}, 500);
|
|
229
|
-
} else {
|
|
230
|
-
busyEl.classList.remove("on");
|
|
231
|
-
if (busyTimer) { clearInterval(busyTimer); busyTimer = null; }
|
|
232
|
-
document.getElementById("busysecs").textContent = "";
|
|
233
|
-
}
|
|
234
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function renderState(s) {
|
|
238
|
-
state = Object.assign(state, s);
|
|
239
|
-
const st = document.getElementById("status");
|
|
240
|
-
st.innerHTML = "";
|
|
241
|
-
const mode = el("span", "mode " + s.mode, s.mode === "ro" ? "read-only" : s.mode === "bypass" ? "bypass permissions" : s.mode);
|
|
242
|
-
st.appendChild(mode);
|
|
243
|
-
st.appendChild(document.createTextNode(" · " + s.model + " (" + s.backend + ")"));
|
|
244
|
-
if (s.effort) { st.appendChild(document.createTextNode(" · ")); st.appendChild(el("span", "eff", s.effort)); }
|
|
245
|
-
const kt = s.ctxTokens < 1000 ? s.ctxTokens : (s.ctxTokens / 1000).toFixed(1) + "k";
|
|
246
|
-
st.appendChild(document.createTextNode(" · " + kt + " (" + s.ctxPct + "%)"));
|
|
247
|
-
if (s.plan) {
|
|
248
|
-
st.appendChild(document.createTextNode(" · "));
|
|
249
|
-
const done = s.plan.steps.filter(x => x.done).length;
|
|
250
|
-
const chip = el("span", "plan-chip" + (s.plan.current < 0 ? " done" : ""), "plan " + done + "/" + s.plan.steps.length);
|
|
251
|
-
st.appendChild(chip);
|
|
252
|
-
}
|
|
253
|
-
if (s.tasks) st.appendChild(document.createTextNode(" · " + s.tasks + " task" + (s.tasks > 1 ? "s" : "")));
|
|
254
|
-
document.getElementById("ws").textContent = s.workspace || "";
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function renderPlan(p) {
|
|
258
|
-
const box = el("div", "plan");
|
|
259
|
-
const done = p.steps.filter(x => x.done).length;
|
|
260
|
-
const hdr = el("div", "hdr", "Plan ");
|
|
261
|
-
hdr.appendChild(el("small", "", done + "/" + p.steps.length));
|
|
262
|
-
box.appendChild(hdr);
|
|
263
|
-
p.steps.forEach((s, i) => {
|
|
264
|
-
const cls = s.done ? "done" : i === p.current ? "cur" : "todo";
|
|
265
|
-
const mark = s.done ? "✔ " : i === p.current ? "▶ " : "○ ";
|
|
266
|
-
box.appendChild(el("div", cls, mark + s.text));
|
|
267
|
-
});
|
|
268
|
-
add(box);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function handle(m) {
|
|
272
|
-
switch (m.t) {
|
|
273
|
-
case "state": renderState(m.s); break;
|
|
274
|
-
case "user": endThought(); curText = null; add(el("div", "user", m.s)); break;
|
|
275
|
-
case "token":
|
|
276
|
-
endThought();
|
|
277
|
-
if (!curText) { curText = add(el("div", "md")); curText._raw = ""; }
|
|
278
|
-
curText._raw += m.s; scheduleMd(curText); break;
|
|
279
|
-
case "thinking":
|
|
280
|
-
if (!curThought) { curThought = add(el("div", "thought")); thoughtStart = Date.now(); thoughtBuf = ""; curText = null; }
|
|
281
|
-
thoughtBuf += m.s;
|
|
282
|
-
if (thoughtBuf.length > 4000) thoughtBuf = thoughtBuf.slice(-2000);
|
|
283
|
-
var tt = thoughtBuf.replace(/\\s+/g, " ").trim();
|
|
284
|
-
curThought.textContent = "✦ " + (tt.length > 160 ? "…" + tt.slice(-160) : tt);
|
|
285
|
-
window.scrollTo(0, document.body.scrollHeight); break;
|
|
286
|
-
case "tool": {
|
|
287
|
-
endThought(); curText = null;
|
|
288
|
-
const d = el("div", "tool"); d.appendChild(el("span", "name", "→ " + m.name)); d.appendChild(document.createTextNode(" " + (m.summary || "")));
|
|
289
|
-
add(d); break;
|
|
290
|
-
}
|
|
291
|
-
case "result": endThought(); curText = null; add(el("div", "result" + (m.err ? " err" : ""), (m.err ? "✗ " : "✓ ") + m.line + (m.extra ? " (+" + m.extra + " lines)" : ""))); break;
|
|
292
|
-
case "plan": endThought(); curText = null; renderPlan(m); break;
|
|
293
|
-
case "line": endThought(); curText = null; add(el("div", "line-" + m.kind, m.s)); break;
|
|
294
|
-
case "turnend": endThought(); curText = null; add(el("div", "turnend", "■ " + m.label)); break;
|
|
295
|
-
case "busy": setBusy(m.label); break;
|
|
296
|
-
case "confirm": {
|
|
297
|
-
endThought(); curText = null;
|
|
298
|
-
const box = el("div", "ask");
|
|
299
|
-
box.appendChild(el("div", "", "run?")); box.appendChild(el("div", "cmd", m.command));
|
|
300
|
-
if (m.reason) box.appendChild(el("div", "hint", m.reason));
|
|
301
|
-
["yes", "no", "always"].forEach(a => {
|
|
302
|
-
const b = el("button", "", a === "always" ? "always allow this program" : a);
|
|
303
|
-
b.onclick = () => { post("/confirm", { id: m.id, answer: a }); box.remove(); add(el("div", "line-status", a + " — " + m.command)); };
|
|
304
|
-
box.appendChild(b);
|
|
305
|
-
});
|
|
306
|
-
add(box); break;
|
|
307
|
-
}
|
|
308
|
-
case "select": {
|
|
309
|
-
endThought(); curText = null;
|
|
310
|
-
const box = el("div", "ask");
|
|
311
|
-
box.appendChild(el("div", "cmd", m.title));
|
|
312
|
-
m.options.forEach((o, i) => {
|
|
313
|
-
const b = el("button", "opt" + (o.current ? " current" : ""), (o.current ? "● " : "") + o.label);
|
|
314
|
-
if (o.hint) b.appendChild(el("span", "hint", o.hint));
|
|
315
|
-
b.onclick = () => { post("/select", { id: m.id, index: i }); box.remove(); };
|
|
316
|
-
box.appendChild(el("div")).appendChild(b);
|
|
317
|
-
});
|
|
318
|
-
const cancel = el("button", "", "cancel");
|
|
319
|
-
cancel.onclick = () => { post("/select", { id: m.id, index: null }); box.remove(); };
|
|
320
|
-
box.appendChild(cancel);
|
|
321
|
-
add(box); break;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const input = document.getElementById("input");
|
|
327
|
-
const menu = document.getElementById("menu");
|
|
328
|
-
let menuIdx = 0;
|
|
329
|
-
function menuItems() {
|
|
330
|
-
const v = input.value;
|
|
331
|
-
if (!v.startsWith("/") || v.includes(" ") || v.includes("\\n")) return [];
|
|
332
|
-
return (state.commands || []).filter(c => c.name.startsWith(v.slice(1)));
|
|
333
|
-
}
|
|
334
|
-
function renderMenu() {
|
|
335
|
-
const items = menuItems();
|
|
336
|
-
menu.style.display = items.length ? "block" : "none";
|
|
337
|
-
menu.innerHTML = "";
|
|
338
|
-
if (menuIdx >= items.length) menuIdx = 0;
|
|
339
|
-
items.forEach((c, i) => {
|
|
340
|
-
const d = el("div", "item" + (i === menuIdx ? " sel" : ""));
|
|
341
|
-
d.appendChild(el("span", "nm", "/" + c.name)); d.appendChild(el("span", "ds", c.desc));
|
|
342
|
-
d.onclick = () => { input.value = "/" + c.name; submit(); };
|
|
343
|
-
menu.appendChild(d);
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
function submit() {
|
|
347
|
-
let v = input.value;
|
|
348
|
-
const items = menuItems();
|
|
349
|
-
if (items.length) v = "/" + items[menuIdx].name;
|
|
350
|
-
v = v.trim();
|
|
351
|
-
if (!v) return;
|
|
352
|
-
input.value = ""; renderMenu();
|
|
353
|
-
post("/msg", { text: v });
|
|
354
|
-
}
|
|
355
|
-
input.addEventListener("input", () => { menuIdx = 0; renderMenu(); autoGrow(); });
|
|
356
|
-
function autoGrow() { input.rows = Math.min(6, Math.max(1, input.value.split("\\n").length)); }
|
|
357
|
-
input.addEventListener("keydown", (e) => {
|
|
358
|
-
const items = menuItems();
|
|
359
|
-
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
|
|
360
|
-
else if (e.key === "Tab" && e.shiftKey) { e.preventDefault(); post("/cycle"); }
|
|
361
|
-
else if (e.key === "Tab" && items.length) { e.preventDefault(); input.value = "/" + items[menuIdx].name + " "; renderMenu(); }
|
|
362
|
-
else if (e.key === "ArrowUp" && items.length) { e.preventDefault(); menuIdx = (menuIdx - 1 + items.length) % items.length; renderMenu(); }
|
|
363
|
-
else if (e.key === "ArrowDown" && items.length) { e.preventDefault(); menuIdx = (menuIdx + 1) % items.length; renderMenu(); }
|
|
364
|
-
else if (e.key === "Escape") { if (input.value) { input.value = ""; renderMenu(); } else post("/cancel"); }
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
actionBtn.onclick = () => { if (isBusy) post("/cancel"); else submit(); };
|
|
368
|
-
|
|
369
|
-
// Escape interrupts from anywhere on the page, not only from the input box.
|
|
370
|
-
document.addEventListener("keydown", (e) => {
|
|
371
|
-
if (e.key === "Escape" && document.activeElement !== input) post("/cancel");
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
const es = new EventSource("/events?k=" + k);
|
|
375
|
-
es.onopen = () => { log.innerHTML = ""; };
|
|
376
|
-
es.onmessage = (e) => handle(JSON.parse(e.data));
|
|
377
|
-
es.onerror = () => { document.getElementById("status").textContent = "reconnecting…"; };
|
|
378
|
-
input.focus();
|
|
379
|
-
</script>
|
|
85
|
+
</div>
|
|
86
|
+
<script>${client_1.CLIENT_JS}</script>
|
|
380
87
|
</body>
|
|
381
88
|
</html>`;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// On-disk memory for the web hub: saved sessions (so the sidebar survives a
|
|
3
|
+
// restart and any session can be resumed) and the list of workspaces the
|
|
4
|
+
// user has opened. Plain JSON files under ~/.smolcoder/, one pair per session:
|
|
5
|
+
// a small .meta.json that is read at startup, and the heavy transcript body
|
|
6
|
+
// that is only loaded when a session is resumed.
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.WorkspaceStore = exports.SessionStore = void 0;
|
|
42
|
+
exports.workspaceKey = workspaceKey;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
function writeAtomic(file, data) {
|
|
46
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
47
|
+
fs.writeFileSync(tmp, data);
|
|
48
|
+
fs.renameSync(tmp, file);
|
|
49
|
+
}
|
|
50
|
+
async function writeAtomicAsync(file, data) {
|
|
51
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
52
|
+
await fs.promises.writeFile(tmp, data);
|
|
53
|
+
await fs.promises.rename(tmp, file);
|
|
54
|
+
}
|
|
55
|
+
class SessionStore {
|
|
56
|
+
dir;
|
|
57
|
+
constructor(dataDir) {
|
|
58
|
+
this.dir = path.join(dataDir, "sessions");
|
|
59
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
metaPath(id) {
|
|
62
|
+
return path.join(this.dir, `${id}.meta.json`);
|
|
63
|
+
}
|
|
64
|
+
bodyPath(id) {
|
|
65
|
+
return path.join(this.dir, `${id}.json`);
|
|
66
|
+
}
|
|
67
|
+
listMetas() {
|
|
68
|
+
const out = [];
|
|
69
|
+
let names = [];
|
|
70
|
+
try {
|
|
71
|
+
names = fs.readdirSync(this.dir);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
for (const n of names) {
|
|
77
|
+
if (!n.endsWith(".meta.json"))
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
const m = JSON.parse(fs.readFileSync(path.join(this.dir, n), "utf8"));
|
|
81
|
+
if (m && typeof m.id === "string" && typeof m.workspace === "string") {
|
|
82
|
+
out.push({
|
|
83
|
+
id: m.id,
|
|
84
|
+
workspace: m.workspace,
|
|
85
|
+
title: String(m.title ?? ""),
|
|
86
|
+
createdAt: Number(m.createdAt) || 0,
|
|
87
|
+
updatedAt: Number(m.updatedAt) || 0,
|
|
88
|
+
model: m.model,
|
|
89
|
+
backend: m.backend,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* skip a broken file */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
saveMeta(meta) {
|
|
100
|
+
writeAtomic(this.metaPath(meta.id), JSON.stringify(meta));
|
|
101
|
+
}
|
|
102
|
+
saveBody(id, body) {
|
|
103
|
+
return writeAtomicAsync(this.bodyPath(id), JSON.stringify(body));
|
|
104
|
+
}
|
|
105
|
+
saveBodySync(id, body) {
|
|
106
|
+
writeAtomic(this.bodyPath(id), JSON.stringify(body));
|
|
107
|
+
}
|
|
108
|
+
loadBody(id) {
|
|
109
|
+
try {
|
|
110
|
+
const b = JSON.parse(fs.readFileSync(this.bodyPath(id), "utf8"));
|
|
111
|
+
if (!b || typeof b !== "object" || !b.snapshot)
|
|
112
|
+
return null;
|
|
113
|
+
return { snapshot: b.snapshot, events: Array.isArray(b.events) ? b.events : [] };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
delete(id) {
|
|
120
|
+
for (const p of [this.metaPath(id), this.bodyPath(id)]) {
|
|
121
|
+
try {
|
|
122
|
+
fs.unlinkSync(p);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
/* already gone */
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.SessionStore = SessionStore;
|
|
131
|
+
/** Normalized form for comparisons: resolved, no trailing separator, and
|
|
132
|
+
* case-folded on Windows where paths are case-insensitive. */
|
|
133
|
+
function workspaceKey(p) {
|
|
134
|
+
let r = path.resolve(p);
|
|
135
|
+
if (r.length > 1 && /[\\/]$/.test(r) && path.dirname(r) !== r)
|
|
136
|
+
r = r.slice(0, -1);
|
|
137
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
138
|
+
}
|
|
139
|
+
class WorkspaceStore {
|
|
140
|
+
file;
|
|
141
|
+
entries = [];
|
|
142
|
+
constructor(dataDir) {
|
|
143
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
144
|
+
this.file = path.join(dataDir, "workspaces.json");
|
|
145
|
+
try {
|
|
146
|
+
const raw = JSON.parse(fs.readFileSync(this.file, "utf8"));
|
|
147
|
+
if (Array.isArray(raw)) {
|
|
148
|
+
this.entries = raw
|
|
149
|
+
.filter((e) => e && typeof e.path === "string")
|
|
150
|
+
.map((e) => ({ path: e.path, addedAt: Number(e.addedAt) || 0, lastOpened: Number(e.lastOpened) || 0 }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
this.entries = [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
list() {
|
|
158
|
+
return this.entries.slice();
|
|
159
|
+
}
|
|
160
|
+
has(p) {
|
|
161
|
+
const key = workspaceKey(p);
|
|
162
|
+
return this.entries.some((e) => workspaceKey(e.path) === key);
|
|
163
|
+
}
|
|
164
|
+
add(p) {
|
|
165
|
+
const key = workspaceKey(p);
|
|
166
|
+
let e = this.entries.find((x) => workspaceKey(x.path) === key);
|
|
167
|
+
if (!e) {
|
|
168
|
+
e = { path: path.resolve(p), addedAt: Date.now(), lastOpened: Date.now() };
|
|
169
|
+
this.entries.push(e);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
e.lastOpened = Date.now();
|
|
173
|
+
}
|
|
174
|
+
this.save();
|
|
175
|
+
return e;
|
|
176
|
+
}
|
|
177
|
+
touch(p) {
|
|
178
|
+
const key = workspaceKey(p);
|
|
179
|
+
const e = this.entries.find((x) => workspaceKey(x.path) === key);
|
|
180
|
+
if (e) {
|
|
181
|
+
e.lastOpened = Date.now();
|
|
182
|
+
this.save();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
remove(p) {
|
|
186
|
+
const key = workspaceKey(p);
|
|
187
|
+
this.entries = this.entries.filter((x) => workspaceKey(x.path) !== key);
|
|
188
|
+
this.save();
|
|
189
|
+
}
|
|
190
|
+
save() {
|
|
191
|
+
try {
|
|
192
|
+
writeAtomic(this.file, JSON.stringify(this.entries, null, 2));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* non-fatal */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.WorkspaceStore = WorkspaceStore;
|