sapper-iq 1.1.40 → 1.2.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 +224 -158
- package/package.json +7 -3
- package/sapper-ui.mjs +1577 -1863
- package/sapper.mjs +1 -1
package/sapper-ui.mjs
CHANGED
|
@@ -1,2017 +1,1732 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Sapper
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Sapper Web — Runs the real sapper.mjs inside a browser terminal,
|
|
4
|
+
* with a sidebar (Files / Config / Agents / Skills) and a document
|
|
5
|
+
* viewer/editor that auto-refreshes when Sapper modifies a file.
|
|
6
|
+
*
|
|
7
|
+
* Browser <--WebSocket--> Node <--pty--> sapper.mjs
|
|
8
|
+
* <--WebSocket--> Node <--fs.watch--> workspace
|
|
9
|
+
* <--REST--> Node <--fs--> files / config
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import http from 'http';
|
|
9
13
|
import fs from 'fs';
|
|
10
|
-
import
|
|
14
|
+
import os from 'os';
|
|
11
15
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { dirname, join, resolve,
|
|
13
|
-
import
|
|
16
|
+
import { dirname, join, resolve as pathResolve, relative, sep } from 'path';
|
|
17
|
+
import { spawn as ptySpawn } from 'node-pty';
|
|
18
|
+
import { WebSocketServer } from 'ws';
|
|
19
|
+
import { spawn } from 'child_process';
|
|
14
20
|
|
|
15
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
22
|
const __dirname = dirname(__filename);
|
|
17
23
|
|
|
18
|
-
const PORT = 3777;
|
|
19
|
-
const
|
|
24
|
+
const PORT = parseInt(process.env.SAPPER_UI_PORT || '3777', 10);
|
|
25
|
+
const SAPPER_BIN = join(__dirname, 'sapper.mjs');
|
|
26
|
+
const workingDir = process.cwd();
|
|
27
|
+
const SAPPER_DIR = join(workingDir, '.sapper');
|
|
28
|
+
const CONFIG_FILE = join(SAPPER_DIR, 'config.json');
|
|
20
29
|
const AGENTS_DIR = join(SAPPER_DIR, 'agents');
|
|
21
30
|
const SKILLS_DIR = join(SAPPER_DIR, 'skills');
|
|
22
|
-
const SESSIONS_DIR = join(SAPPER_DIR, 'sessions');
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
const IGNORE_NAMES = new Set([
|
|
33
|
+
'.git', 'node_modules', '.next', 'dist', 'build', '.cache',
|
|
34
|
+
'.DS_Store', '__pycache__', '.venv', 'venv',
|
|
35
|
+
]);
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
const DEBUG = !!process.env.SAPPER_UI_DEBUG;
|
|
38
|
+
const dbg = (...a) => { if (DEBUG) console.log('[ui]', ...a); };
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
30
|
-
}
|
|
40
|
+
// ─── Path safety ─────────────────────────────────────────────────
|
|
31
41
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
name: meta.name || name,
|
|
65
|
-
description: meta.description || name,
|
|
66
|
-
tools: meta.tools || null,
|
|
67
|
-
content: body,
|
|
68
|
-
};
|
|
42
|
+
function safePath(p) {
|
|
43
|
+
if (typeof p !== 'string') return null;
|
|
44
|
+
const cleaned = p.replace(/^\/+/, '');
|
|
45
|
+
const abs = pathResolve(workingDir, cleaned || '.');
|
|
46
|
+
if (abs !== workingDir && !abs.startsWith(workingDir + sep)) return null;
|
|
47
|
+
return abs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stripJSONC(src) {
|
|
51
|
+
// Remove // line comments and /* ... */ block comments outside strings.
|
|
52
|
+
let out = '';
|
|
53
|
+
let i = 0;
|
|
54
|
+
const n = src.length;
|
|
55
|
+
while (i < n) {
|
|
56
|
+
const c = src[i];
|
|
57
|
+
const c2 = src[i + 1];
|
|
58
|
+
if (c === '"' || c === "'") {
|
|
59
|
+
const quote = c;
|
|
60
|
+
out += c; i++;
|
|
61
|
+
while (i < n) {
|
|
62
|
+
const ch = src[i];
|
|
63
|
+
out += ch; i++;
|
|
64
|
+
if (ch === '\\' && i < n) { out += src[i]; i++; continue; }
|
|
65
|
+
if (ch === quote) break;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === '/' && c2 === '/') { while (i < n && src[i] !== '\n') i++; continue; }
|
|
70
|
+
if (c === '/' && c2 === '*') {
|
|
71
|
+
i += 2;
|
|
72
|
+
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) i++;
|
|
73
|
+
i += 2; continue;
|
|
69
74
|
}
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
out += c; i++;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
function
|
|
75
|
-
ensureDir(SKILLS_DIR);
|
|
76
|
-
const skills = {};
|
|
80
|
+
function readJSON(file, fallback = null) {
|
|
77
81
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
skills[name] = { name: meta.name || name, description: meta.description || name, content: body };
|
|
82
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
83
|
+
try { return JSON.parse(raw); }
|
|
84
|
+
catch {
|
|
85
|
+
const cleaned = stripJSONC(raw).replace(/,(\s*[}\]])/g, '$1'); // also tolerate trailing commas
|
|
86
|
+
return JSON.parse(cleaned);
|
|
84
87
|
}
|
|
85
|
-
} catch
|
|
86
|
-
return skills;
|
|
88
|
+
} catch { return fallback; }
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
function ensureDir(d) { try { fs.mkdirSync(d, { recursive: true }); } catch {} }
|
|
90
92
|
|
|
91
|
-
// ───
|
|
92
|
-
const SAPPERIGNORE_FILE = '.sapperignore';
|
|
93
|
+
// ─── Markdown frontmatter (for agents/skills) ────────────────────
|
|
93
94
|
|
|
94
|
-
function
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
95
|
+
function parseFrontmatter(raw) {
|
|
96
|
+
const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
97
|
+
if (!m) return { meta: {}, body: raw };
|
|
98
|
+
const meta = {};
|
|
99
|
+
for (const line of m[1].split('\n')) {
|
|
100
|
+
const i = line.indexOf(':');
|
|
101
|
+
if (i === -1) continue;
|
|
102
|
+
let k = line.slice(0, i).trim();
|
|
103
|
+
let v = line.slice(i + 1).trim();
|
|
104
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
105
|
+
if (v.startsWith('[') && v.endsWith(']')) {
|
|
106
|
+
v = v.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
return patterns;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let _sapperIgnorePatterns = null;
|
|
113
|
-
function getSapperIgnorePatterns() {
|
|
114
|
-
if (_sapperIgnorePatterns === null) _sapperIgnorePatterns = loadSapperIgnorePatterns();
|
|
115
|
-
return _sapperIgnorePatterns;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function ignorePatternToRegex(pattern) {
|
|
119
|
-
try {
|
|
120
|
-
let p = pattern.replace(/\/+$/, '');
|
|
121
|
-
p = p.replace(/([.+^${}()|[\]\\])/g, '\\$1');
|
|
122
|
-
p = p.replace(/\*\*/g, '<<<GLOBSTAR>>>');
|
|
123
|
-
p = p.replace(/\*/g, '[^/]*');
|
|
124
|
-
p = p.replace(/<<<GLOBSTAR>>>/g, '.*');
|
|
125
|
-
p = p.replace(/\?/g, '[^/]');
|
|
126
|
-
return new RegExp(`(^|/)${p}($|/)`, 'i');
|
|
127
|
-
} catch (e) {
|
|
128
|
-
return /^$/; // Return a regex that never matches on error
|
|
108
|
+
meta[k] = v;
|
|
129
109
|
}
|
|
110
|
+
return { meta, body: m[2] };
|
|
130
111
|
}
|
|
131
112
|
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
113
|
+
function listMdDir(dir) {
|
|
114
|
+
if (!fs.existsSync(dir)) return [];
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const f of fs.readdirSync(dir)) {
|
|
117
|
+
if (!f.endsWith('.md')) continue;
|
|
118
|
+
const full = join(dir, f);
|
|
119
|
+
try {
|
|
120
|
+
const raw = fs.readFileSync(full, 'utf8');
|
|
121
|
+
const { meta } = parseFrontmatter(raw);
|
|
122
|
+
out.push({
|
|
123
|
+
key: f.replace(/\.md$/, ''),
|
|
124
|
+
file: f,
|
|
125
|
+
name: meta.name || f.replace(/\.md$/, ''),
|
|
126
|
+
description: meta.description || '',
|
|
127
|
+
path: relative(workingDir, full),
|
|
128
|
+
});
|
|
129
|
+
} catch {}
|
|
141
130
|
}
|
|
142
|
-
return
|
|
131
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
132
|
}
|
|
144
133
|
|
|
145
|
-
|
|
146
|
-
const resolved = resolve(workingDir, p || '.');
|
|
147
|
-
if (!resolved.startsWith(workingDir)) return null;
|
|
148
|
-
return resolved;
|
|
149
|
-
}
|
|
134
|
+
// ─── HTML page ───────────────────────────────────────────────────
|
|
150
135
|
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
- [TOOL:MKDIR]path[/TOOL] - Create directory
|
|
177
|
-
|
|
178
|
-
PATCH TIPS:
|
|
179
|
-
- PREFER LINE:number mode. Always READ first.
|
|
180
|
-
- If PATCH fails, switch to LINE:number or WRITE.
|
|
181
|
-
|
|
182
|
-
You MUST use [TOOL:...][/TOOL] syntax to perform actions.
|
|
183
|
-
Do NOT show tool syntax as examples to the user — only use them to perform real actions.`;
|
|
184
|
-
|
|
185
|
-
if (agentContent) {
|
|
186
|
-
prompt += `\n\n═══ ACTIVE AGENT ═══\n${agentContent}\n═══ END AGENT ═══`;
|
|
187
|
-
if (agentTools && agentTools.length > 0) {
|
|
188
|
-
const allTools = ['READ', 'WRITE', 'PATCH', 'LIST', 'SEARCH', 'SHELL', 'MKDIR'];
|
|
189
|
-
const forbidden = allTools.filter(t => !agentTools.includes(t));
|
|
190
|
-
prompt += `\nTOOL RESTRICTION: ONLY use: ${agentTools.join(', ')}. FORBIDDEN: ${forbidden.join(', ')}.`;
|
|
191
|
-
}
|
|
136
|
+
function buildHTML() {
|
|
137
|
+
return `<!doctype html>
|
|
138
|
+
<html lang="en">
|
|
139
|
+
<head>
|
|
140
|
+
<meta charset="utf-8" />
|
|
141
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
142
|
+
<title>Sapper</title>
|
|
143
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" />
|
|
144
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" />
|
|
145
|
+
<style>
|
|
146
|
+
:root {
|
|
147
|
+
--bg: #0a0e14;
|
|
148
|
+
--panel: #0d1117;
|
|
149
|
+
--panel2: #11161d;
|
|
150
|
+
--border: #21262d;
|
|
151
|
+
--border2: #30363d;
|
|
152
|
+
--fg: #e6edf3;
|
|
153
|
+
--muted: #8b949e;
|
|
154
|
+
--dim: #6e7681;
|
|
155
|
+
--accent: #58a6ff;
|
|
156
|
+
--accent2: #79c0ff;
|
|
157
|
+
--green: #3fb950;
|
|
158
|
+
--red: #f85149;
|
|
159
|
+
--yellow: #d29922;
|
|
160
|
+
--purple: #bc8cff;
|
|
192
161
|
}
|
|
193
|
-
|
|
194
|
-
|
|
162
|
+
* { box-sizing: border-box; }
|
|
163
|
+
html, body { margin: 0; height: 100%; width: 100%; max-width: 100vw; overflow: hidden;
|
|
164
|
+
background: var(--bg); color: var(--fg);
|
|
165
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; }
|
|
166
|
+
button { font-family: inherit; }
|
|
167
|
+
|
|
168
|
+
#app { display: flex; flex-direction: column; height: 100vh; width: 100vw;
|
|
169
|
+
max-width: 100vw; overflow: hidden; }
|
|
170
|
+
|
|
171
|
+
/* ─── Top bar ─── */
|
|
172
|
+
#bar {
|
|
173
|
+
height: 38px; flex-shrink: 0; display: flex; align-items: center;
|
|
174
|
+
padding: 0 12px; gap: 10px;
|
|
175
|
+
background: var(--panel); border-bottom: 1px solid var(--border);
|
|
176
|
+
font-size: 12px;
|
|
195
177
|
}
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} catch (e) { return `Error: ${e.message}`; }
|
|
209
|
-
},
|
|
210
|
-
read: (path) => {
|
|
211
|
-
try { return fs.readFileSync(resolve(workingDir, path.trim()), 'utf8'); }
|
|
212
|
-
catch (e) { return `Error: ${e.message}`; }
|
|
213
|
-
},
|
|
214
|
-
write: (path, content) => {
|
|
215
|
-
try {
|
|
216
|
-
const p = resolve(workingDir, path.trim());
|
|
217
|
-
fs.mkdirSync(dirname(p), { recursive: true });
|
|
218
|
-
fs.writeFileSync(p, content);
|
|
219
|
-
return `Successfully saved ${path.trim()}`;
|
|
220
|
-
} catch (e) { return `Error: ${e.message}`; }
|
|
221
|
-
},
|
|
222
|
-
mkdir: (path) => {
|
|
223
|
-
try { fs.mkdirSync(resolve(workingDir, path.trim()), { recursive: true }); return `Created ${path}`; }
|
|
224
|
-
catch (e) { return `Error: ${e.message}`; }
|
|
225
|
-
},
|
|
226
|
-
patch: (path, oldText, newText) => {
|
|
227
|
-
const p = resolve(workingDir, path.trim());
|
|
228
|
-
try {
|
|
229
|
-
const content = fs.readFileSync(p, 'utf8');
|
|
230
|
-
const lineMatch = oldText.match(/^LINE:(\d+)$/);
|
|
231
|
-
if (lineMatch) {
|
|
232
|
-
const n = parseInt(lineMatch[1], 10);
|
|
233
|
-
const lines = content.split('\n');
|
|
234
|
-
if (n < 1 || n > lines.length) return `Error: Line ${n} out of range (${lines.length} lines)`;
|
|
235
|
-
const old = lines[n - 1];
|
|
236
|
-
lines[n - 1] = newText;
|
|
237
|
-
fs.writeFileSync(p, lines.join('\n'));
|
|
238
|
-
return `Patched line ${n}: "${old}" → "${newText}"`;
|
|
239
|
-
}
|
|
240
|
-
if (content.includes(oldText)) {
|
|
241
|
-
fs.writeFileSync(p, content.replace(oldText, newText));
|
|
242
|
-
return `Successfully patched ${path.trim()}`;
|
|
243
|
-
}
|
|
244
|
-
if (content.includes(oldText.trim())) {
|
|
245
|
-
fs.writeFileSync(p, content.replace(oldText.trim(), newText.trim()));
|
|
246
|
-
return `Successfully patched ${path.trim()} (trimmed match)`;
|
|
247
|
-
}
|
|
248
|
-
const normalize = s => s.replace(/[\u{1F000}-\u{1FFFF}]/gu, '').replace(/\s+/g, ' ').trim();
|
|
249
|
-
const normOld = normalize(oldText);
|
|
250
|
-
const lines = content.split('\n');
|
|
251
|
-
const oldLines = oldText.trim().split('\n');
|
|
252
|
-
for (let i = 0; i <= lines.length - oldLines.length; i++) {
|
|
253
|
-
const win = lines.slice(i, i + oldLines.length).join('\n');
|
|
254
|
-
if (normalize(win) === normOld) {
|
|
255
|
-
const newContent = content.replace(win, newText.trim());
|
|
256
|
-
fs.writeFileSync(p, newContent);
|
|
257
|
-
return `Successfully patched ${path.trim()} (fuzzy match at line ${i + 1})`;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return `Error: Text not found in ${path.trim()}. Use LINE:number mode instead.`;
|
|
261
|
-
} catch (e) { return `Error: ${e.message}`; }
|
|
262
|
-
},
|
|
263
|
-
search: (pattern) => {
|
|
264
|
-
return new Promise((res) => {
|
|
265
|
-
const allIgnoreDirs = new Set(IGNORE_DIRS);
|
|
266
|
-
for (const { pattern: p, negate } of getSapperIgnorePatterns()) {
|
|
267
|
-
if (!negate && p.endsWith('/')) allIgnoreDirs.add(p.replace(/\/+$/, ''));
|
|
268
|
-
}
|
|
269
|
-
// Use args array to avoid command injection
|
|
270
|
-
const args = ['-rEin', pattern, '.'];
|
|
271
|
-
for (const dir of allIgnoreDirs) {
|
|
272
|
-
args.push(`--exclude-dir=${dir}`);
|
|
273
|
-
}
|
|
274
|
-
args.push('--include=*.js', '--include=*.ts', '--include=*.jsx', '--include=*.tsx',
|
|
275
|
-
'--include=*.py', '--include=*.java', '--include=*.go', '--include=*.rs',
|
|
276
|
-
'--include=*.rb', '--include=*.php', '--include=*.c', '--include=*.cpp',
|
|
277
|
-
'--include=*.h', '--include=*.css', '--include=*.scss', '--include=*.html',
|
|
278
|
-
'--include=*.json', '--include=*.md', '--include=*.txt', '--include=*.yml',
|
|
279
|
-
'--include=*.yaml', '--include=*.toml', '--include=*.sh');
|
|
280
|
-
const proc = spawn('grep', args, { cwd: workingDir });
|
|
281
|
-
let out = '';
|
|
282
|
-
let lineCount = 0;
|
|
283
|
-
proc.stdout.on('data', d => {
|
|
284
|
-
const text = d.toString();
|
|
285
|
-
const lines = text.split('\n');
|
|
286
|
-
for (const line of lines) {
|
|
287
|
-
if (lineCount >= 50) { proc.kill(); return; }
|
|
288
|
-
if (line) { out += line + '\n'; lineCount++; }
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
proc.stderr.on('data', () => {});
|
|
292
|
-
proc.on('error', (err) => res(`Error searching: ${err.message}`));
|
|
293
|
-
proc.on('close', () => res(out.trim() || `No matches for: ${pattern}`));
|
|
294
|
-
});
|
|
295
|
-
},
|
|
296
|
-
shell: (cmd) => {
|
|
297
|
-
return new Promise((res) => {
|
|
298
|
-
const proc = spawn('sh', ['-c', cmd], { cwd: workingDir });
|
|
299
|
-
let out = '';
|
|
300
|
-
proc.stdout.on('data', d => out += d);
|
|
301
|
-
proc.stderr.on('data', d => out += d);
|
|
302
|
-
proc.on('error', (err) => {
|
|
303
|
-
res(`Shell error: ${err.message}`);
|
|
304
|
-
});
|
|
305
|
-
proc.on('close', code => {
|
|
306
|
-
let result = out.trim();
|
|
307
|
-
if (result.length > 10000) result = result.substring(0, 10000) + '\n...(truncated)';
|
|
308
|
-
res(result || `Completed (exit ${code})`);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
},
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
async function executeTool(type, path, content, agentTools) {
|
|
315
|
-
if (agentTools && !agentTools.includes(type.toUpperCase())) {
|
|
316
|
-
return { result: `Error: Tool ${type.toUpperCase()} not allowed. Allowed: ${agentTools.join(', ')}`, blocked: true };
|
|
178
|
+
#bar .title { font-weight: 700; letter-spacing: .3px; user-select: none; }
|
|
179
|
+
#bar .title span { color: var(--accent); }
|
|
180
|
+
#bar .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); transition: background .2s; }
|
|
181
|
+
#bar .dot.on { background: var(--green); }
|
|
182
|
+
#bar .dot.err { background: var(--red); }
|
|
183
|
+
#bar .cwd { color: var(--muted); font-family: ui-monospace, 'SF Mono', monospace;
|
|
184
|
+
font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 35vw; }
|
|
185
|
+
#bar .spacer { flex: 1; }
|
|
186
|
+
#bar button {
|
|
187
|
+
background: transparent; color: var(--muted); border: 1px solid var(--border);
|
|
188
|
+
border-radius: 6px; padding: 4px 10px; font-size: 11px; cursor: pointer;
|
|
189
|
+
transition: all .12s;
|
|
317
190
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
191
|
+
#bar button:hover { color: var(--accent); border-color: var(--accent); }
|
|
192
|
+
#bar button.toggle.on { color: var(--accent); border-color: var(--accent); background: rgba(88,166,255,.08); }
|
|
193
|
+
|
|
194
|
+
/* live system stats */
|
|
195
|
+
#stats { display: grid; grid-template-columns: auto auto auto; gap: 4px 10px;
|
|
196
|
+
align-items: center; padding: 4px 10px; margin: 0 8px;
|
|
197
|
+
background: var(--panel2); border: 1px solid var(--border); border-radius: 6px;
|
|
198
|
+
font-family: ui-monospace, 'SF Mono', monospace; font-size: 10px; line-height: 1; }
|
|
199
|
+
#stats .srow { display: contents; }
|
|
200
|
+
#stats .slbl { color: var(--dim); font-size: 9px; letter-spacing: .5px; min-width: 22px; }
|
|
201
|
+
#stats .sbar { width: 60px; height: 5px; background: var(--bg); border-radius: 3px;
|
|
202
|
+
overflow: hidden; }
|
|
203
|
+
#stats .sbar i { display: block; height: 100%; width: 0%;
|
|
204
|
+
background: linear-gradient(90deg, var(--green), var(--accent));
|
|
205
|
+
transition: width .35s ease, background .25s; }
|
|
206
|
+
#stats .sbar i.warn { background: linear-gradient(90deg, var(--yellow), #ff9b3f); }
|
|
207
|
+
#stats .sbar i.crit { background: linear-gradient(90deg, #ff7b72, var(--red)); }
|
|
208
|
+
#stats .sval { color: var(--fg); min-width: 60px; text-align: right; font-variant-numeric: tabular-nums; }
|
|
209
|
+
@media (max-width: 1100px) {
|
|
210
|
+
#stats .sbar { width: 40px; }
|
|
211
|
+
#stats .sval { min-width: 48px; font-size: 9px; }
|
|
212
|
+
#bar .cwd { display: none; }
|
|
333
213
|
}
|
|
334
|
-
|
|
335
|
-
else if (t === 'shell') result = await tools.shell(path);
|
|
336
|
-
else result = `Unknown tool: ${type}`;
|
|
337
|
-
return { result, blocked: false };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ─── Chat Engine ───────────────────────────────────────────
|
|
341
|
-
|
|
342
|
-
let abortFlag = false;
|
|
343
|
-
|
|
344
|
-
async function* chatStream(messages, model, agentTools) {
|
|
345
|
-
const MAX_TOOL_ROUNDS = 15;
|
|
346
|
-
let rounds = 0;
|
|
347
|
-
const patchFails = {};
|
|
348
|
-
|
|
349
|
-
while (true) {
|
|
350
|
-
if (abortFlag) { abortFlag = false; yield { type: 'system', data: 'Generation stopped' }; break; }
|
|
214
|
+
@media (max-width: 820px) { #stats { display: none; } }
|
|
351
215
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
for await (const chunk of response) {
|
|
355
|
-
if (abortFlag) { abortFlag = false; yield { type: 'system', data: 'Generation stopped' }; messages.push({ role: 'assistant', content: fullMsg }); return; }
|
|
356
|
-
const token = chunk.message?.content || '';
|
|
357
|
-
fullMsg += token;
|
|
358
|
-
yield { type: 'token', data: token };
|
|
359
|
-
}
|
|
360
|
-
messages.push({ role: 'assistant', content: fullMsg });
|
|
361
|
-
|
|
362
|
-
const clean = fullMsg.replace(/```[\s\S]*?```/g, '');
|
|
363
|
-
const toolMatches = [...clean.matchAll(/\[TOOL:(\w+)\]([^:\]]*?)(?:(?:::|\])([\s\S]*?))?\[\/TOOL\]/g)];
|
|
216
|
+
/* ─── Body layout ─── */
|
|
217
|
+
#body { flex: 1; min-height: 0; min-width: 0; display: flex; overflow: hidden; }
|
|
364
218
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
219
|
+
/* ─── Sidebar ─── */
|
|
220
|
+
#side {
|
|
221
|
+
width: 280px; flex-shrink: 0; display: flex; flex-direction: column;
|
|
222
|
+
background: var(--panel); border-right: 1px solid var(--border);
|
|
223
|
+
min-width: 0; overflow: hidden;
|
|
224
|
+
}
|
|
225
|
+
#side.hidden { display: none; }
|
|
226
|
+
.tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
227
|
+
.tabs button {
|
|
228
|
+
flex: 1; padding: 8px 4px; background: none; border: none;
|
|
229
|
+
border-bottom: 2px solid transparent; color: var(--dim); font-size: 11px;
|
|
230
|
+
font-weight: 600; cursor: pointer; text-transform: uppercase; letter-spacing: .5px;
|
|
231
|
+
}
|
|
232
|
+
.tabs button:hover { color: var(--muted); }
|
|
233
|
+
.tabs button.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
234
|
+
.pane { flex: 1; min-height: 0; min-width: 0; overflow-x: hidden; overflow-y: auto; display: none; padding: 6px 0; }
|
|
235
|
+
.pane.active { display: block; }
|
|
236
|
+
.pane::-webkit-scrollbar { width: 6px; }
|
|
237
|
+
.pane::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
238
|
+
|
|
239
|
+
/* Files tree */
|
|
240
|
+
.files-toolbar { display: flex; align-items: center; gap: 4px; padding: 6px 8px;
|
|
241
|
+
border-bottom: 1px solid var(--border); background: var(--panel); }
|
|
242
|
+
.files-toolbar .ftb-spacer { flex: 1; }
|
|
243
|
+
.files-toolbar .ftb { background: transparent; color: var(--muted); border: 1px solid transparent;
|
|
244
|
+
border-radius: 4px; padding: 3px 7px; font-size: 13px; cursor: pointer; line-height: 1;
|
|
245
|
+
position: relative; }
|
|
246
|
+
.files-toolbar .ftb sup { font-size: 9px; color: var(--green); margin-left: 1px; }
|
|
247
|
+
.files-toolbar .ftb:hover { background: rgba(255,255,255,.05); color: var(--fg); border-color: var(--border); }
|
|
248
|
+
.tree { font-family: ui-monospace, 'SF Mono', monospace; font-size: 12px; padding-bottom: 12px; }
|
|
249
|
+
.row { display: flex; align-items: center; gap: 4px; padding: 3px 8px; cursor: pointer; color: var(--muted);
|
|
250
|
+
white-space: nowrap; user-select: none; position: relative; }
|
|
251
|
+
.row:hover { background: rgba(255,255,255,.04); color: var(--fg); }
|
|
252
|
+
.row.active { background: rgba(88,166,255,.12); color: var(--accent); }
|
|
253
|
+
.row .chev { width: 12px; display: inline-block; color: var(--dim); font-size: 9px; flex-shrink: 0; text-align: center; }
|
|
254
|
+
.row .ico { width: 14px; flex-shrink: 0; }
|
|
255
|
+
.row .name { overflow: hidden; text-overflow: ellipsis; }
|
|
256
|
+
.row .badge { margin-left: auto; font-size: 9px; color: var(--yellow); opacity: 0; transition: opacity .2s; }
|
|
257
|
+
.row.changed .badge { opacity: 1; }
|
|
258
|
+
.row .rmenu { margin-left: auto; color: var(--dim); font-size: 14px; padding: 0 4px;
|
|
259
|
+
opacity: 0; flex-shrink: 0; line-height: 1; border-radius: 3px; }
|
|
260
|
+
.row.changed .rmenu { margin-left: 4px; }
|
|
261
|
+
.row:hover .rmenu, .row .rmenu.open { opacity: 1; }
|
|
262
|
+
.row .rmenu:hover { color: var(--fg); background: rgba(255,255,255,.08); }
|
|
263
|
+
|
|
264
|
+
/* Context menu */
|
|
265
|
+
.ctx-menu { position: fixed; z-index: 9999; min-width: 180px;
|
|
266
|
+
background: var(--panel2); border: 1px solid var(--border); border-radius: 6px;
|
|
267
|
+
padding: 4px 0; box-shadow: 0 8px 24px rgba(0,0,0,.5); font-size: 12px;
|
|
268
|
+
color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
269
|
+
.ctx-menu .ci { padding: 6px 14px; cursor: pointer; display: flex; align-items: center;
|
|
270
|
+
gap: 10px; color: var(--muted); }
|
|
271
|
+
.ctx-menu .ci:hover { background: rgba(88,166,255,.12); color: var(--accent); }
|
|
272
|
+
.ctx-menu .ci.danger:hover { background: rgba(248,81,73,.15); color: var(--red); }
|
|
273
|
+
.ctx-menu .ci .k { margin-left: auto; color: var(--dim); font-size: 10px; }
|
|
274
|
+
.ctx-menu .sep { height: 1px; background: var(--border); margin: 4px 0; }
|
|
275
|
+
|
|
276
|
+
/* Modal */
|
|
277
|
+
.modal-bd { position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 10000;
|
|
278
|
+
display: flex; align-items: center; justify-content: center; }
|
|
279
|
+
.modal { background: var(--panel2); border: 1px solid var(--border); border-radius: 8px;
|
|
280
|
+
padding: 18px 18px 14px; width: 460px; max-width: 92vw;
|
|
281
|
+
font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
282
|
+
.modal h3 { margin: 0 0 12px; font-size: 14px; color: var(--fg); font-weight: 600; }
|
|
283
|
+
.modal label { display: block; font-size: 11px; color: var(--dim); margin: 8px 0 4px;
|
|
284
|
+
text-transform: uppercase; letter-spacing: .5px; }
|
|
285
|
+
.modal input[type=text] { width: 100%; box-sizing: border-box; background: var(--bg);
|
|
286
|
+
color: var(--fg); border: 1px solid var(--border); border-radius: 4px;
|
|
287
|
+
padding: 7px 9px; font-family: ui-monospace, 'SF Mono', monospace; font-size: 12px; outline: none; }
|
|
288
|
+
.modal input[type=text]:focus { border-color: var(--accent); }
|
|
289
|
+
.modal .hint { font-size: 11px; color: var(--dim); margin-top: 4px; }
|
|
290
|
+
.modal .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
|
|
291
|
+
.modal .actions button { background: transparent; color: var(--muted); border: 1px solid var(--border);
|
|
292
|
+
border-radius: 5px; padding: 6px 14px; font-size: 12px; cursor: pointer; }
|
|
293
|
+
.modal .actions button:hover { color: var(--fg); border-color: var(--accent); }
|
|
294
|
+
.modal .actions button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
295
|
+
.modal .actions button.primary:hover { filter: brightness(1.1); }
|
|
296
|
+
.modal .actions button.danger { background: var(--red); color: #fff; border-color: var(--red); }
|
|
297
|
+
|
|
298
|
+
/* Config / Agents / Skills lists */
|
|
299
|
+
.pane-section { padding: 10px 14px; }
|
|
300
|
+
.pane-section h4 { font-size: 11px; color: var(--dim); text-transform: uppercase;
|
|
301
|
+
letter-spacing: .5px; margin: 0 0 8px; font-weight: 600; }
|
|
302
|
+
.pane-section p { font-size: 12px; color: var(--muted); margin: 4px 0 12px; line-height: 1.4; }
|
|
303
|
+
.pane-section label { display: block; font-size: 11px; color: var(--muted); margin: 8px 0 4px; font-weight: 500; }
|
|
304
|
+
.pane-section input[type=text], .pane-section input[type=number], .pane-section select {
|
|
305
|
+
width: 100%; background: var(--panel2); border: 1px solid var(--border2); border-radius: 5px;
|
|
306
|
+
padding: 5px 8px; color: var(--fg); font-size: 12px; outline: none; font-family: inherit;
|
|
307
|
+
}
|
|
308
|
+
.pane-section input:focus, .pane-section select:focus { border-color: var(--accent); }
|
|
309
|
+
.pane-section .toggle-row { display: flex; align-items: center; justify-content: space-between;
|
|
310
|
+
padding: 6px 0; border-bottom: 1px solid var(--border); }
|
|
311
|
+
.pane-section .toggle-row span { font-size: 12px; color: var(--muted); }
|
|
312
|
+
.switch { position: relative; width: 30px; height: 16px; background: var(--border2); border-radius: 8px;
|
|
313
|
+
cursor: pointer; transition: background .15s; flex-shrink: 0; }
|
|
314
|
+
.switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px;
|
|
315
|
+
background: var(--muted); border-radius: 50%; transition: all .15s; }
|
|
316
|
+
.switch.on { background: var(--accent); }
|
|
317
|
+
.switch.on::after { background: white; left: 16px; }
|
|
318
|
+
|
|
319
|
+
.json-edit {
|
|
320
|
+
width: 100%; max-width: 100%; height: 320px; background: var(--bg);
|
|
321
|
+
border: 1px solid var(--border2); border-radius: 6px; padding: 8px 10px; color: var(--fg);
|
|
322
|
+
font-family: ui-monospace, 'SF Mono', monospace; font-size: 11px; line-height: 1.45;
|
|
323
|
+
resize: vertical; outline: none; display: block;
|
|
324
|
+
white-space: pre; overflow: auto;
|
|
325
|
+
}
|
|
326
|
+
.json-edit:focus { border-color: var(--accent); }
|
|
327
|
+
.row-btns { display: flex; gap: 6px; margin-top: 8px; }
|
|
328
|
+
.row-btns button {
|
|
329
|
+
flex: 1; padding: 6px 10px; border-radius: 5px; border: 1px solid var(--border2);
|
|
330
|
+
background: var(--panel2); color: var(--muted); font-size: 11px; cursor: pointer;
|
|
331
|
+
transition: all .12s;
|
|
332
|
+
}
|
|
333
|
+
.row-btns button:hover { color: var(--accent); border-color: var(--accent); }
|
|
334
|
+
.row-btns button.primary { background: var(--accent); color: white; border-color: var(--accent); }
|
|
335
|
+
.row-btns button.primary:hover { background: var(--accent2); }
|
|
336
|
+
.row-btns button.danger:hover { color: var(--red); border-color: var(--red); }
|
|
337
|
+
|
|
338
|
+
.item { padding: 8px 14px; cursor: pointer; border-left: 2px solid transparent; }
|
|
339
|
+
.item:hover { background: rgba(255,255,255,.03); border-left-color: var(--border2); }
|
|
340
|
+
.item .ti { font-size: 13px; color: var(--fg); display: flex; align-items: center; gap: 6px; }
|
|
341
|
+
.item .ti .b { background: var(--accent); color: white; font-size: 9px; padding: 1px 5px;
|
|
342
|
+
border-radius: 8px; text-transform: uppercase; letter-spacing: .3px; }
|
|
343
|
+
.item .ds { font-size: 11px; color: var(--dim); margin-top: 2px; line-height: 1.35; }
|
|
344
|
+
|
|
345
|
+
/* ─── Terminal area ─── */
|
|
346
|
+
#center { flex: 1; min-width: 0; min-height: 0; display: flex;
|
|
347
|
+
flex-direction: column; background: var(--bg); overflow: hidden; }
|
|
348
|
+
#term-wrap { flex: 1; min-height: 0; min-width: 0; padding: 6px 0 0 10px;
|
|
349
|
+
overflow: hidden; position: relative; }
|
|
350
|
+
#term-wrap .terminal, #term-wrap .xterm { height: 100% !important; width: 100% !important; }
|
|
351
|
+
.xterm-screen, .xterm-viewport { max-width: 100% !important; }
|
|
352
|
+
.xterm .xterm-viewport { background-color: var(--bg) !important; }
|
|
353
|
+
.xterm-viewport::-webkit-scrollbar { width: 8px; }
|
|
354
|
+
.xterm-viewport::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
355
|
+
.xterm-viewport::-webkit-scrollbar-track { background: transparent; }
|
|
356
|
+
|
|
357
|
+
/* ─── Preview panel ─── */
|
|
358
|
+
#preview {
|
|
359
|
+
width: 480px; flex-shrink: 0; display: flex; flex-direction: column;
|
|
360
|
+
background: var(--panel); border-left: 1px solid var(--border);
|
|
361
|
+
min-width: 0; overflow: hidden;
|
|
362
|
+
}
|
|
363
|
+
#preview.hidden { display: none; }
|
|
364
|
+
#preview .ph {
|
|
365
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
|
|
366
|
+
background: var(--panel2); border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
367
|
+
}
|
|
368
|
+
#preview .ph .pp { flex: 1; font-family: ui-monospace, 'SF Mono', monospace;
|
|
369
|
+
font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
370
|
+
#preview .ph button {
|
|
371
|
+
background: transparent; color: var(--muted); border: 1px solid var(--border2);
|
|
372
|
+
border-radius: 5px; padding: 3px 9px; font-size: 11px; cursor: pointer;
|
|
373
|
+
}
|
|
374
|
+
#preview .ph button:hover { color: var(--accent); border-color: var(--accent); }
|
|
375
|
+
#preview .ph button.primary { background: var(--accent); color: white; border-color: var(--accent); }
|
|
376
|
+
#preview .ph button.primary:hover { background: var(--accent2); }
|
|
395
377
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
378
|
+
#preview .ind {
|
|
379
|
+
display: none; padding: 4px 12px; background: rgba(210,153,34,.12);
|
|
380
|
+
color: var(--yellow); font-size: 11px; border-bottom: 1px solid rgba(210,153,34,.3);
|
|
399
381
|
}
|
|
400
|
-
}
|
|
382
|
+
#preview .ind.show { display: block; }
|
|
383
|
+
|
|
384
|
+
#pview { flex: 1; min-height: 0; overflow: auto; padding: 14px 18px; font-size: 13.5px; line-height: 1.6; }
|
|
385
|
+
#pview::-webkit-scrollbar { width: 8px; }
|
|
386
|
+
#pview::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
|
387
|
+
#pview pre { background: var(--bg); border: 1px solid var(--border);
|
|
388
|
+
border-radius: 6px; padding: 10px 12px; overflow-x: auto;
|
|
389
|
+
font-family: ui-monospace, 'SF Mono', monospace; font-size: 12px; line-height: 1.5; }
|
|
390
|
+
#pview code { font-family: ui-monospace, 'SF Mono', monospace; font-size: 12px; }
|
|
391
|
+
#pview :not(pre) > code { background: var(--panel2); padding: 1px 5px; border-radius: 3px; }
|
|
392
|
+
#pview h1, #pview h2, #pview h3 { color: var(--accent); margin-top: 1.2em; }
|
|
393
|
+
#pview h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: .3em; }
|
|
394
|
+
#pview h2 { font-size: 18px; }
|
|
395
|
+
#pview h3 { font-size: 15px; }
|
|
396
|
+
#pview a { color: var(--accent); }
|
|
397
|
+
#pview blockquote { border-left: 3px solid var(--accent); padding-left: 12px; color: var(--muted); margin: 8px 0; }
|
|
398
|
+
#pview table { border-collapse: collapse; margin: 8px 0; }
|
|
399
|
+
#pview th, #pview td { border: 1px solid var(--border); padding: 5px 8px; }
|
|
400
|
+
#pview hr { border: none; border-top: 1px solid var(--border); margin: 14px 0; }
|
|
401
|
+
#pview img { max-width: 100%; border-radius: 4px; }
|
|
402
|
+
#pview iframe.html-preview { width: 100%; height: 100%; border: 0; background: #fff;
|
|
403
|
+
border-radius: 4px; display: block; }
|
|
404
|
+
|
|
405
|
+
#pview.code { padding: 0; }
|
|
406
|
+
#pview.code pre { margin: 0; border: none; border-radius: 0; min-height: 100%; }
|
|
407
|
+
|
|
408
|
+
#pedit {
|
|
409
|
+
flex: 1; min-height: 0; width: 100%; padding: 12px 14px;
|
|
410
|
+
background: var(--bg); border: none; color: var(--fg);
|
|
411
|
+
font-family: ui-monospace, 'SF Mono', monospace; font-size: 12.5px; line-height: 1.5;
|
|
412
|
+
resize: none; outline: none; display: none;
|
|
413
|
+
}
|
|
414
|
+
#pedit.show { display: block; }
|
|
415
|
+
#pview.hide { display: none; }
|
|
416
|
+
|
|
417
|
+
#empty { padding: 40px 20px; text-align: center; color: var(--dim); font-size: 13px; }
|
|
418
|
+
#empty .lg { font-size: 36px; margin-bottom: 8px; }
|
|
419
|
+
|
|
420
|
+
/* Toast for fs events */
|
|
421
|
+
#toast { position: fixed; bottom: 14px; right: 14px; z-index: 100;
|
|
422
|
+
display: flex; flex-direction: column; gap: 6px; pointer-events: none; }
|
|
423
|
+
.tmsg { background: rgba(13,17,23,.95); color: var(--fg); border: 1px solid var(--border2);
|
|
424
|
+
border-radius: 6px; padding: 8px 12px; font-size: 12px; pointer-events: auto;
|
|
425
|
+
animation: slideIn .2s ease; max-width: 360px; box-shadow: 0 4px 16px rgba(0,0,0,.4); }
|
|
426
|
+
.tmsg.warn { border-color: rgba(210,153,34,.5); }
|
|
427
|
+
.tmsg.err { border-color: var(--red); }
|
|
428
|
+
@keyframes slideIn { from { transform: translateX(10px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
429
|
+
</style>
|
|
430
|
+
</head>
|
|
431
|
+
<body>
|
|
432
|
+
<div id="app">
|
|
433
|
+
<div id="bar">
|
|
434
|
+
<span class="dot" id="dot"></span>
|
|
435
|
+
<span class="title">⚡ <span>Sapper</span></span>
|
|
436
|
+
<span class="cwd" id="cwd"></span>
|
|
437
|
+
<div id="stats" title="Live system stats">
|
|
438
|
+
<div class="srow"><span class="slbl">CPU</span><div class="sbar"><i id="bCpu"></i></div><span class="sval" id="vCpu">—</span></div>
|
|
439
|
+
<div class="srow"><span class="slbl">RAM</span><div class="sbar"><i id="bRam"></i></div><span class="sval" id="vRam">—</span></div>
|
|
440
|
+
<div class="srow"><span class="slbl">GPU</span><div class="sbar"><i id="bGpu"></i></div><span class="sval" id="vGpu">—</span></div>
|
|
441
|
+
</div>
|
|
442
|
+
<span class="spacer"></span>
|
|
443
|
+
<button id="btnSide" class="toggle on" onclick="toggleSide()">Sidebar</button>
|
|
444
|
+
<button id="btnPrev" class="toggle" onclick="togglePreview()">Preview</button>
|
|
445
|
+
<button onclick="sendCmd('/help')">/help</button>
|
|
446
|
+
<button onclick="sendCmd('/agents')">agents</button>
|
|
447
|
+
<button onclick="sendCmd('/model')">model</button>
|
|
448
|
+
<button onclick="sendCmd('/clear')">clear</button>
|
|
449
|
+
<button onclick="restartSapper()">restart</button>
|
|
450
|
+
</div>
|
|
401
451
|
|
|
402
|
-
|
|
452
|
+
<div id="body">
|
|
453
|
+
<!-- Sidebar -->
|
|
454
|
+
<aside id="side">
|
|
455
|
+
<div class="tabs">
|
|
456
|
+
<button class="active" data-tab="files" onclick="switchTab('files')">Files</button>
|
|
457
|
+
<button data-tab="config" onclick="switchTab('config')">Config</button>
|
|
458
|
+
<button data-tab="agents" onclick="switchTab('agents')">Agents</button>
|
|
459
|
+
<button data-tab="skills" onclick="switchTab('skills')">Skills</button>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="pane active" id="pane-files">
|
|
462
|
+
<div class="files-toolbar">
|
|
463
|
+
<button class="ftb" title="New file" onclick="newItemPrompt('file','')">🗎<sup>+</sup></button>
|
|
464
|
+
<button class="ftb" title="New folder" onclick="newItemPrompt('folder','')">📁<sup>+</sup></button>
|
|
465
|
+
<span class="ftb-spacer"></span>
|
|
466
|
+
<button class="ftb" title="Refresh tree" onclick="loadTree()">↺</button>
|
|
467
|
+
<button class="ftb" title="Collapse all" onclick="collapseAll()">⇤</button>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="tree" id="tree"></div>
|
|
470
|
+
</div>
|
|
471
|
+
<div class="pane" id="pane-config">
|
|
472
|
+
<div class="pane-section" id="cfgQuick">
|
|
473
|
+
<h4>Quick settings</h4>
|
|
474
|
+
<div id="cfgQuickBody"></div>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="pane-section">
|
|
477
|
+
<h4>Raw config.json</h4>
|
|
478
|
+
<p>Full <code>.sapper/config.json</code> — every Sapper option lives here.</p>
|
|
479
|
+
<textarea class="json-edit" id="cfgJson" spellcheck="false"></textarea>
|
|
480
|
+
<div class="row-btns">
|
|
481
|
+
<button onclick="reloadConfig()">Reload</button>
|
|
482
|
+
<button class="primary" onclick="saveConfig()">Save</button>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
<div class="pane" id="pane-agents">
|
|
487
|
+
<div class="pane-section">
|
|
488
|
+
<h4>Available agents</h4>
|
|
489
|
+
<p>Click any agent to open its <code>.md</code> file in preview.</p>
|
|
490
|
+
</div>
|
|
491
|
+
<div id="agentsList"></div>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="pane" id="pane-skills">
|
|
494
|
+
<div class="pane-section">
|
|
495
|
+
<h4>Available skills</h4>
|
|
496
|
+
<p>Click a skill to open it. Use <code>/use name</code> in the terminal to load.</p>
|
|
497
|
+
</div>
|
|
498
|
+
<div id="skillsList"></div>
|
|
499
|
+
</div>
|
|
500
|
+
</aside>
|
|
501
|
+
|
|
502
|
+
<!-- Center: terminal -->
|
|
503
|
+
<main id="center">
|
|
504
|
+
<div id="term-wrap"></div>
|
|
505
|
+
</main>
|
|
506
|
+
|
|
507
|
+
<!-- Right: preview -->
|
|
508
|
+
<aside id="preview" class="hidden">
|
|
509
|
+
<div class="ph">
|
|
510
|
+
<span class="pp" id="pPath">No file open</span>
|
|
511
|
+
<button id="pEdit" onclick="startEdit()" style="display:none">Edit</button>
|
|
512
|
+
<button id="pSave" onclick="saveEdit()" class="primary" style="display:none">Save</button>
|
|
513
|
+
<button id="pCancel" onclick="cancelEdit()" style="display:none">Cancel</button>
|
|
514
|
+
<button id="pSrc" onclick="toggleSource()" style="display:none">Source</button>
|
|
515
|
+
<button id="pReload" onclick="reloadPreview()" style="display:none">Reload</button>
|
|
516
|
+
<button onclick="closePreview()">×</button>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="ind" id="pInd">File changed on disk — reload to view latest.</div>
|
|
519
|
+
<div id="pview"><div id="empty"><div class="lg">📄</div>Open a file from the sidebar.</div></div>
|
|
520
|
+
<textarea id="pedit" spellcheck="false"></textarea>
|
|
521
|
+
</aside>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
<div id="toast"></div>
|
|
403
525
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
} catch { return null; }
|
|
414
|
-
})
|
|
415
|
-
.filter(Boolean)
|
|
416
|
-
.sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
417
|
-
} catch { return []; }
|
|
418
|
-
}
|
|
526
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
527
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
528
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
|
|
529
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
|
|
530
|
+
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
|
531
|
+
<script>
|
|
532
|
+
/* ─────────────────────────────────────────────────────────────── */
|
|
533
|
+
/* Sapper Web — frontend */
|
|
534
|
+
/* ─────────────────────────────────────────────────────────────── */
|
|
419
535
|
|
|
420
|
-
|
|
421
|
-
ensureDir(SESSIONS_DIR);
|
|
422
|
-
const data = { name, created: new Date().toISOString(), model, agent: agentKey, messages };
|
|
423
|
-
fs.writeFileSync(join(SESSIONS_DIR, `${id}.json`), JSON.stringify(data));
|
|
424
|
-
}
|
|
536
|
+
var BT = String.fromCharCode(96);
|
|
425
537
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
538
|
+
// ─── State ────────────────────────────────────────────────────
|
|
539
|
+
var state = {
|
|
540
|
+
cwd: '',
|
|
541
|
+
currentFile: null, // workspace-relative path currently in preview
|
|
542
|
+
fileOnDisk: '', // last loaded content from server
|
|
543
|
+
editing: false,
|
|
544
|
+
expanded: { '': true },
|
|
545
|
+
fsWS: null,
|
|
546
|
+
};
|
|
430
547
|
|
|
431
|
-
function
|
|
432
|
-
|
|
433
|
-
|
|
548
|
+
function esc(s) {
|
|
549
|
+
if (s == null) return '';
|
|
550
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
551
|
+
.replace(/"/g,'"').replace(/'/g,''');
|
|
434
552
|
}
|
|
435
553
|
|
|
436
|
-
function
|
|
554
|
+
function showToast(msg, kind) {
|
|
555
|
+
var el = document.createElement('div');
|
|
556
|
+
el.className = 'tmsg' + (kind ? ' ' + kind : '');
|
|
557
|
+
el.textContent = msg;
|
|
558
|
+
document.getElementById('toast').appendChild(el);
|
|
559
|
+
setTimeout(function(){ el.style.opacity = '0'; el.style.transition = 'opacity .3s'; }, 2200);
|
|
560
|
+
setTimeout(function(){ el.remove(); }, 2700);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ─── Terminal & pty WS ───────────────────────────────────────
|
|
564
|
+
var term = new Terminal({
|
|
565
|
+
fontFamily: '"SF Mono","Fira Code","JetBrains Mono",Menlo,ui-monospace,monospace',
|
|
566
|
+
fontSize: 13, lineHeight: 1.25, cursorBlink: true, cursorStyle: 'bar',
|
|
567
|
+
scrollback: 10000, allowProposedApi: true, macOptionIsMeta: true,
|
|
568
|
+
theme: {
|
|
569
|
+
background:'#0a0e14', foreground:'#e6edf3', cursor:'#58a6ff', cursorAccent:'#0a0e14',
|
|
570
|
+
selectionBackground:'rgba(88,166,255,0.35)',
|
|
571
|
+
black:'#484f58', red:'#ff7b72', green:'#3fb950', yellow:'#d29922',
|
|
572
|
+
blue:'#58a6ff', magenta:'#bc8cff', cyan:'#39c5cf', white:'#e6edf3',
|
|
573
|
+
brightBlack:'#6e7681', brightRed:'#ffa198', brightGreen:'#56d364',
|
|
574
|
+
brightYellow:'#e3b341', brightBlue:'#79c0ff', brightMagenta:'#d2a8ff',
|
|
575
|
+
brightCyan:'#56d4dd', brightWhite:'#f0f6fc'
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
var fit = new FitAddon.FitAddon();
|
|
579
|
+
term.loadAddon(fit);
|
|
580
|
+
try { term.loadAddon(new WebLinksAddon.WebLinksAddon()); } catch(e){}
|
|
581
|
+
term.open(document.getElementById('term-wrap'));
|
|
582
|
+
setTimeout(function(){ try { fit.fit(); } catch(e){} }, 30);
|
|
583
|
+
|
|
584
|
+
var ws = null, reconnectTimer = null;
|
|
585
|
+
|
|
586
|
+
function connectPty() {
|
|
587
|
+
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
588
|
+
ws = new WebSocket(proto + '//' + location.host + '/ws');
|
|
589
|
+
ws.binaryType = 'arraybuffer';
|
|
590
|
+
ws.onopen = function() {
|
|
591
|
+
document.getElementById('dot').className = 'dot on';
|
|
592
|
+
var d = fit.proposeDimensions() || { cols: 100, rows: 30 };
|
|
593
|
+
ws.send(JSON.stringify({ type:'init', cols:d.cols, rows:d.rows }));
|
|
594
|
+
term.focus();
|
|
595
|
+
};
|
|
596
|
+
ws.onmessage = function(ev) {
|
|
597
|
+
if (typeof ev.data === 'string') {
|
|
598
|
+
try {
|
|
599
|
+
var m = JSON.parse(ev.data);
|
|
600
|
+
if (m.type === 'cwd') { state.cwd = m.path; document.getElementById('cwd').textContent = m.path; }
|
|
601
|
+
else if (m.type === 'exit') {
|
|
602
|
+
term.writeln('\\r\\n\\x1b[33m[sapper exited — click "restart" to relaunch]\\x1b[0m');
|
|
603
|
+
document.getElementById('dot').className = 'dot err';
|
|
604
|
+
}
|
|
605
|
+
} catch(e){}
|
|
606
|
+
} else {
|
|
607
|
+
term.write(new Uint8Array(ev.data));
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
ws.onclose = function() {
|
|
611
|
+
document.getElementById('dot').className = 'dot err';
|
|
612
|
+
clearTimeout(reconnectTimer);
|
|
613
|
+
reconnectTimer = setTimeout(connectPty, 1200);
|
|
614
|
+
};
|
|
615
|
+
ws.onerror = function(){};
|
|
616
|
+
}
|
|
617
|
+
term.onData(function(d){ if (ws && ws.readyState === 1) ws.send(d); });
|
|
618
|
+
function doFit() {
|
|
437
619
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
620
|
+
fit.fit();
|
|
621
|
+
var d = fit.proposeDimensions();
|
|
622
|
+
if (d && ws && ws.readyState === 1) ws.send(JSON.stringify({ type:'resize', cols:d.cols, rows:d.rows }));
|
|
623
|
+
} catch(e){}
|
|
624
|
+
}
|
|
625
|
+
var rTimer = null;
|
|
626
|
+
window.addEventListener('resize', function(){ clearTimeout(rTimer); rTimer = setTimeout(doFit, 80); });
|
|
627
|
+
|
|
628
|
+
window.sendCmd = function(cmd) { if (ws && ws.readyState === 1) ws.send(cmd + '\\r'); term.focus(); };
|
|
629
|
+
window.restartSapper = function() { if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type:'restart' })); };
|
|
630
|
+
document.getElementById('term-wrap').addEventListener('click', function(){ term.focus(); });
|
|
631
|
+
|
|
632
|
+
// ─── FS events WS ────────────────────────────────────────────
|
|
633
|
+
function connectEvents() {
|
|
634
|
+
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
635
|
+
state.fsWS = new WebSocket(proto + '//' + location.host + '/events');
|
|
636
|
+
state.fsWS.onmessage = function(ev) {
|
|
637
|
+
var msg = null;
|
|
638
|
+
try { msg = JSON.parse(ev.data); } catch(e) { return; }
|
|
639
|
+
if (msg.type === 'stats') { handleStats(msg); return; }
|
|
640
|
+
handleFsEvent(msg);
|
|
641
|
+
};
|
|
642
|
+
state.fsWS.onclose = function() { setTimeout(connectEvents, 2000); };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function fmtBytes(b) {
|
|
646
|
+
if (b == null) return '—';
|
|
647
|
+
var u = ['B','KB','MB','GB','TB']; var i = 0;
|
|
648
|
+
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
|
|
649
|
+
return (i >= 3 ? b.toFixed(1) : Math.round(b)) + ' ' + u[i];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function setBar(id, pct) {
|
|
653
|
+
var el = document.getElementById(id);
|
|
654
|
+
if (!el) return;
|
|
655
|
+
pct = Math.max(0, Math.min(100, pct || 0));
|
|
656
|
+
el.style.width = pct + '%';
|
|
657
|
+
el.classList.remove('warn', 'crit');
|
|
658
|
+
if (pct >= 85) el.classList.add('crit');
|
|
659
|
+
else if (pct >= 65) el.classList.add('warn');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function handleStats(msg) {
|
|
663
|
+
if (msg.cpu) {
|
|
664
|
+
setBar('bCpu', msg.cpu.percent);
|
|
665
|
+
document.getElementById('vCpu').textContent = msg.cpu.percent + '%';
|
|
666
|
+
}
|
|
667
|
+
if (msg.mem) {
|
|
668
|
+
setBar('bRam', msg.mem.percent);
|
|
669
|
+
document.getElementById('vRam').textContent = fmtBytes(msg.mem.used) + '/' + fmtBytes(msg.mem.total);
|
|
670
|
+
} else if (msg.totalMem) {
|
|
671
|
+
document.getElementById('vRam').textContent = fmtBytes(msg.totalMem);
|
|
672
|
+
}
|
|
673
|
+
if (msg.gpu) {
|
|
674
|
+
setBar('bGpu', msg.gpu.percent);
|
|
675
|
+
document.getElementById('vGpu').textContent = msg.gpu.percent + '%';
|
|
676
|
+
} else {
|
|
677
|
+
document.getElementById('vGpu').textContent = 'n/a';
|
|
455
678
|
}
|
|
456
|
-
fm += '---\n\n';
|
|
457
|
-
fs.writeFileSync(join(AGENTS_DIR, filename), fm + content);
|
|
458
|
-
return filename;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function deleteAgentFile(key) {
|
|
462
|
-
try { fs.unlinkSync(join(AGENTS_DIR, key + '.md')); return true; }
|
|
463
|
-
catch { return false; }
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function createSkillFile(name, description, content) {
|
|
467
|
-
ensureDir(SKILLS_DIR);
|
|
468
|
-
const filename = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + '.md';
|
|
469
|
-
let fm = '---\n';
|
|
470
|
-
fm += `name: "${name}"\n`;
|
|
471
|
-
fm += `description: "${description}"\n`;
|
|
472
|
-
fm += '---\n\n';
|
|
473
|
-
fs.writeFileSync(join(SKILLS_DIR, filename), fm + content);
|
|
474
|
-
return filename;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function deleteSkillFile(key) {
|
|
478
|
-
try { fs.unlinkSync(join(SKILLS_DIR, key + '.md')); return true; }
|
|
479
|
-
catch { return false; }
|
|
480
679
|
}
|
|
481
680
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
681
|
+
function handleFsEvent(msg) {
|
|
682
|
+
if (!msg || !msg.path) return;
|
|
683
|
+
// Flag the file in the tree
|
|
684
|
+
var row = document.querySelector('.row[data-path="' + cssEscape(msg.path) + '"]');
|
|
685
|
+
if (row) {
|
|
686
|
+
row.classList.add('changed');
|
|
687
|
+
setTimeout(function(){ row.classList.remove('changed'); }, 4000);
|
|
688
|
+
}
|
|
689
|
+
// Refresh tree (parent dir) if a file was added/removed
|
|
690
|
+
if (msg.event === 'rename') {
|
|
691
|
+
var parent = msg.path.split('/').slice(0, -1).join('/');
|
|
692
|
+
refreshDir(parent);
|
|
693
|
+
}
|
|
694
|
+
// If the current preview file changed, auto-refresh (or show indicator if editing)
|
|
695
|
+
if (state.currentFile === msg.path) {
|
|
696
|
+
if (state.editing) {
|
|
697
|
+
document.getElementById('pInd').classList.add('show');
|
|
698
|
+
} else {
|
|
699
|
+
openFile(msg.path, true);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
503
702
|
}
|
|
504
703
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
let serverMessages = [];
|
|
508
|
-
let serverModel = '';
|
|
509
|
-
let serverAgent = null;
|
|
510
|
-
let serverAgentKey = null;
|
|
511
|
-
let serverAgentTools = null;
|
|
512
|
-
let currentSessionId = null;
|
|
513
|
-
|
|
514
|
-
function resetChat() {
|
|
515
|
-
const skills = loadSkills();
|
|
516
|
-
const skillContents = Object.values(skills).map(s => s.content);
|
|
517
|
-
serverMessages = [{
|
|
518
|
-
role: 'system',
|
|
519
|
-
content: buildSystemPrompt(serverAgent?.content || null, serverAgentTools, skillContents)
|
|
520
|
-
}];
|
|
521
|
-
}
|
|
704
|
+
function cssEscape(s) { return s.replace(/(["\\\\])/g, '\\\\$1'); }
|
|
522
705
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
706
|
+
// ─── Sidebar tabs ────────────────────────────────────────────
|
|
707
|
+
window.switchTab = function(name) {
|
|
708
|
+
document.querySelectorAll('.tabs button').forEach(function(b){
|
|
709
|
+
b.classList.toggle('active', b.dataset.tab === name);
|
|
710
|
+
});
|
|
711
|
+
document.querySelectorAll('.pane').forEach(function(p){
|
|
712
|
+
p.classList.toggle('active', p.id === 'pane-' + name);
|
|
713
|
+
});
|
|
714
|
+
if (name === 'config' && !document.getElementById('cfgJson').value) reloadConfig();
|
|
715
|
+
if (name === 'agents') loadAgents();
|
|
716
|
+
if (name === 'skills') loadSkills();
|
|
717
|
+
};
|
|
718
|
+
window.toggleSide = function() {
|
|
719
|
+
var s = document.getElementById('side');
|
|
720
|
+
s.classList.toggle('hidden');
|
|
721
|
+
document.getElementById('btnSide').classList.toggle('on', !s.classList.contains('hidden'));
|
|
722
|
+
setTimeout(doFit, 50);
|
|
723
|
+
};
|
|
724
|
+
window.togglePreview = function() {
|
|
725
|
+
var p = document.getElementById('preview');
|
|
726
|
+
p.classList.toggle('hidden');
|
|
727
|
+
document.getElementById('btnPrev').classList.toggle('on', !p.classList.contains('hidden'));
|
|
728
|
+
setTimeout(doFit, 50);
|
|
729
|
+
};
|
|
527
730
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
731
|
+
// ─── File tree ───────────────────────────────────────────────
|
|
732
|
+
function fileIcon(name, isDir) {
|
|
733
|
+
if (isDir) return '📁';
|
|
734
|
+
var ext = name.split('.').pop().toLowerCase();
|
|
735
|
+
if (['md','markdown'].indexOf(ext) >= 0) return '📝';
|
|
736
|
+
if (['png','jpg','jpeg','gif','svg','webp'].indexOf(ext) >= 0) return '📷';
|
|
737
|
+
if (['json','yml','yaml','toml'].indexOf(ext) >= 0) return '⚙';
|
|
738
|
+
return '📄';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function loadTree() {
|
|
742
|
+
fetch('/api/tree?path=').then(function(r){return r.json();}).then(function(d){
|
|
743
|
+
var root = document.getElementById('tree');
|
|
744
|
+
root.innerHTML = '';
|
|
745
|
+
renderDir(root, '', d.entries, 0);
|
|
746
|
+
}).catch(function(e){ showToast('Tree error: ' + e.message, 'err'); });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function refreshDir(path) {
|
|
750
|
+
// Re-fetch the directory contents and re-render in place if expanded
|
|
751
|
+
var key = path || '';
|
|
752
|
+
if (!state.expanded[key]) return;
|
|
753
|
+
fetch('/api/tree?path=' + encodeURIComponent(path)).then(function(r){return r.json();}).then(function(d){
|
|
754
|
+
// Find parent row, then rebuild its children
|
|
755
|
+
if (key === '') { loadTree(); return; }
|
|
756
|
+
var parentRow = document.querySelector('.row[data-path="' + cssEscape(key) + '"]');
|
|
757
|
+
if (!parentRow) return;
|
|
758
|
+
var depth = parseInt(parentRow.dataset.depth || '0', 10);
|
|
759
|
+
var next = parentRow.nextSibling;
|
|
760
|
+
while (next && parseInt(next.dataset.depth || '-1', 10) > depth) {
|
|
761
|
+
var rem = next; next = next.nextSibling; rem.remove();
|
|
762
|
+
}
|
|
763
|
+
// Re-insert children
|
|
764
|
+
var container = document.createDocumentFragment();
|
|
765
|
+
renderEntries(container, key, d.entries, depth + 1);
|
|
766
|
+
parentRow.parentNode.insertBefore(container, parentRow.nextSibling);
|
|
767
|
+
});
|
|
531
768
|
}
|
|
532
769
|
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
770
|
+
function renderDir(container, basePath, entries, depth) {
|
|
771
|
+
renderEntries(container, basePath, entries, depth);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function renderEntries(container, basePath, entries, depth) {
|
|
775
|
+
entries.forEach(function(entry){
|
|
776
|
+
var path = basePath ? (basePath + '/' + entry.name) : entry.name;
|
|
777
|
+
var row = document.createElement('div');
|
|
778
|
+
row.className = 'row';
|
|
779
|
+
row.dataset.path = path;
|
|
780
|
+
row.dataset.depth = depth;
|
|
781
|
+
row.dataset.isdir = entry.isDir ? '1' : '0';
|
|
782
|
+
row.style.paddingLeft = (8 + depth * 14) + 'px';
|
|
783
|
+
var chev = entry.isDir ? (state.expanded[path] ? '▾' : '▸') : '';
|
|
784
|
+
row.innerHTML =
|
|
785
|
+
'<span class="chev">' + chev + '</span>' +
|
|
786
|
+
'<span class="ico">' + fileIcon(entry.name, entry.isDir) + '</span>' +
|
|
787
|
+
'<span class="name">' + esc(entry.name) + '</span>' +
|
|
788
|
+
'<span class="badge">●</span>' +
|
|
789
|
+
'<span class="rmenu" title="Options">⋯</span>';
|
|
790
|
+
row.addEventListener('click', function(ev){
|
|
791
|
+
if (ev.target && ev.target.classList && ev.target.classList.contains('rmenu')) {
|
|
792
|
+
ev.stopPropagation();
|
|
793
|
+
openRowMenu(ev.target, path, entry.isDir);
|
|
543
794
|
return;
|
|
544
795
|
}
|
|
545
|
-
|
|
796
|
+
if (entry.isDir) toggleDir(row, path);
|
|
797
|
+
else openFile(path);
|
|
546
798
|
});
|
|
547
|
-
|
|
548
|
-
|
|
799
|
+
row.addEventListener('contextmenu', function(ev){
|
|
800
|
+
ev.preventDefault();
|
|
801
|
+
openRowMenu({ getBoundingClientRect: function(){ return { left: ev.clientX, bottom: ev.clientY, right: ev.clientX, top: ev.clientY }; } }, path, entry.isDir);
|
|
802
|
+
});
|
|
803
|
+
container.appendChild(row);
|
|
804
|
+
if (entry.isDir && state.expanded[path]) {
|
|
805
|
+
// Load children if not already loaded
|
|
806
|
+
fetch('/api/tree?path=' + encodeURIComponent(path)).then(function(r){return r.json();}).then(function(d){
|
|
807
|
+
var frag = document.createDocumentFragment();
|
|
808
|
+
renderEntries(frag, path, d.entries, depth + 1);
|
|
809
|
+
row.parentNode.insertBefore(frag, row.nextSibling);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
549
812
|
});
|
|
550
813
|
}
|
|
551
814
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
815
|
+
function toggleDir(row, path) {
|
|
816
|
+
var depth = parseInt(row.dataset.depth, 10);
|
|
817
|
+
var isExpanded = !!state.expanded[path];
|
|
818
|
+
if (isExpanded) {
|
|
819
|
+
// Collapse: remove all following rows with greater depth
|
|
820
|
+
var next = row.nextSibling;
|
|
821
|
+
while (next && parseInt(next.dataset.depth || '-1', 10) > depth) {
|
|
822
|
+
var rem = next; next = next.nextSibling; rem.remove();
|
|
823
|
+
}
|
|
824
|
+
delete state.expanded[path];
|
|
825
|
+
row.querySelector('.chev').innerHTML = '▸';
|
|
826
|
+
} else {
|
|
827
|
+
state.expanded[path] = true;
|
|
828
|
+
row.querySelector('.chev').innerHTML = '▾';
|
|
829
|
+
fetch('/api/tree?path=' + encodeURIComponent(path)).then(function(r){return r.json();}).then(function(d){
|
|
830
|
+
var frag = document.createDocumentFragment();
|
|
831
|
+
renderEntries(frag, path, d.entries, depth + 1);
|
|
832
|
+
row.parentNode.insertBefore(frag, row.nextSibling);
|
|
833
|
+
});
|
|
834
|
+
}
|
|
568
835
|
}
|
|
569
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
570
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; background: var(--bg1); color: var(--fg1); height: 100vh; display: flex; overflow: hidden; }
|
|
571
|
-
|
|
572
|
-
/* ── Sidebar ── */
|
|
573
|
-
.sidebar { width: 270px; background: var(--bg2); border-right: 1px solid var(--bg4); display: flex; flex-direction: column; flex-shrink: 0; }
|
|
574
|
-
.sidebar-header { padding: 16px 18px; border-bottom: 1px solid var(--bg4); display: flex; align-items: center; gap: 10px; }
|
|
575
|
-
.sidebar-header h1 { font-size: 18px; font-weight: 700; }
|
|
576
|
-
.sidebar-header h1 span { color: var(--accent); }
|
|
577
|
-
.sidebar-tabs { display: flex; border-bottom: 1px solid var(--bg4); }
|
|
578
|
-
.sidebar-tabs button { flex: 1; padding: 10px 4px; background: none; border: none; border-bottom: 2px solid transparent; color: var(--fg3); font-size: 11px; font-weight: 600; cursor: pointer; text-transform: uppercase; letter-spacing: .5px; transition: all .15s; }
|
|
579
|
-
.sidebar-tabs button:hover { color: var(--fg2); }
|
|
580
|
-
.sidebar-tabs button.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
581
|
-
.tab-panel { display: none; flex: 1; overflow-y: auto; padding: 8px; }
|
|
582
|
-
.tab-panel.active { display: flex; flex-direction: column; }
|
|
583
|
-
.tab-panel::-webkit-scrollbar { width: 5px; }
|
|
584
|
-
.tab-panel::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
|
585
|
-
.tab-create-btn { display: flex; align-items: center; gap: 6px; padding: 8px 12px; margin: 4px; background: var(--bg3); border: 1px dashed var(--bg5); border-radius: var(--radius-sm); color: var(--fg2); font-size: 12px; cursor: pointer; transition: all .15s; }
|
|
586
|
-
.tab-create-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(88,166,255,.08); }
|
|
587
|
-
.s-item { padding: 10px 12px; margin: 2px 4px; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--fg2); transition: all .12s; border-radius: var(--radius-sm); border: 1px solid transparent; }
|
|
588
|
-
.s-item:hover { background: var(--bg3); color: var(--fg1); }
|
|
589
|
-
.s-item.active { background: rgba(88,166,255,.1); color: var(--accent); border-color: rgba(88,166,255,.2); }
|
|
590
|
-
.s-item .s-icon { width: 18px; text-align: center; flex-shrink: 0; font-size: 14px; }
|
|
591
|
-
.s-item .s-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
592
|
-
.s-item .s-meta { font-size: 10px; color: var(--fg3); }
|
|
593
|
-
.s-item .s-del { display: none; background: none; border: none; color: var(--fg3); cursor: pointer; font-size: 14px; padding: 2px 4px; border-radius: 4px; }
|
|
594
|
-
.s-item:hover .s-del { display: block; }
|
|
595
|
-
.s-item .s-del:hover { color: var(--red); background: rgba(248,81,73,.15); }
|
|
596
|
-
|
|
597
|
-
/* ── Quick Actions ── */
|
|
598
|
-
.qa-section { padding: 4px 8px; margin-top: auto; border-top: 1px solid var(--bg4); }
|
|
599
|
-
.qa-title { font-size: 10px; font-weight: 600; color: var(--fg3); text-transform: uppercase; letter-spacing: .8px; padding: 8px 8px 4px; }
|
|
600
|
-
.qa-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 4px; }
|
|
601
|
-
.qa-btn { padding: 7px 6px; background: var(--bg3); border: 1px solid transparent; border-radius: var(--radius-sm); color: var(--fg2); font-size: 11px; cursor: pointer; transition: all .12s; display: flex; align-items: center; gap: 4px; white-space: nowrap; overflow: hidden; }
|
|
602
|
-
.qa-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(88,166,255,.06); }
|
|
603
|
-
.qa-btn .qa-icon { font-size: 12px; flex-shrink: 0; }
|
|
604
|
-
|
|
605
|
-
/* ── Sidebar Footer ── */
|
|
606
|
-
.sidebar-footer { padding: 12px; border-top: 1px solid var(--bg4); }
|
|
607
|
-
.sidebar-footer select { width: 100%; background: var(--bg3); color: var(--fg1); border: 1px solid var(--bg4); border-radius: var(--radius-sm); padding: 6px 8px; font-size: 12px; cursor: pointer; outline: none; }
|
|
608
|
-
.sidebar-footer select:focus { border-color: var(--accent); }
|
|
609
|
-
.sidebar-footer .sf-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 11px; color: var(--fg3); }
|
|
610
|
-
.sidebar-footer .sf-model { background: var(--bg3); padding: 3px 8px; border-radius: 12px; font-size: 10px; color: var(--accent); }
|
|
611
|
-
|
|
612
|
-
/* ── Main ── */
|
|
613
|
-
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
614
|
-
|
|
615
|
-
/* ── Topbar ── */
|
|
616
|
-
.topbar { height: 48px; background: var(--bg2); border-bottom: 1px solid var(--bg4); display: flex; align-items: center; padding: 0 16px; gap: 10px; flex-shrink: 0; }
|
|
617
|
-
.topbar .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); flex-shrink: 0; }
|
|
618
|
-
.topbar .agent-badge { background: linear-gradient(135deg, var(--accent2), var(--purple)); color: #fff; padding: 3px 10px; border-radius: 14px; font-size: 11px; font-weight: 600; }
|
|
619
|
-
.topbar .session-name { color: var(--fg2); font-size: 12px; cursor: pointer; padding: 2px 6px; border-radius: 4px; }
|
|
620
|
-
.topbar .session-name:hover { background: var(--bg3); }
|
|
621
|
-
.topbar .spacer { flex: 1; }
|
|
622
|
-
.topbar .tb-btn { background: var(--bg3); border: 1px solid var(--bg4); border-radius: var(--radius-sm); color: var(--fg2); padding: 5px 10px; font-size: 11px; cursor: pointer; display: flex; align-items: center; gap: 5px; transition: all .12s; }
|
|
623
|
-
.topbar .tb-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
624
|
-
.topbar .cwd { color: var(--fg3); font-size: 11px; font-family: 'SF Mono', 'Fira Code', monospace; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
625
|
-
|
|
626
|
-
/* ── Chat ── */
|
|
627
|
-
.chat { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 6px; scroll-behavior: smooth; }
|
|
628
|
-
.chat::-webkit-scrollbar { width: 6px; }
|
|
629
|
-
.chat::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
|
630
|
-
.msg { max-width: 82%; padding: 12px 16px; border-radius: var(--radius); font-size: 14px; line-height: 1.6; word-break: break-word; animation: fadeIn .2s; }
|
|
631
|
-
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
632
|
-
.msg.user { background: var(--accent2); color: #fff; align-self: flex-end; border-bottom-right-radius: 3px; }
|
|
633
|
-
.msg.ai { background: var(--bg2); border: 1px solid var(--bg4); align-self: flex-start; border-bottom-left-radius: 3px; }
|
|
634
|
-
.msg.ai pre { background: var(--bg0); border: 1px solid var(--bg4); border-radius: var(--radius-sm); padding: 10px 12px; overflow-x: auto; margin: 8px 0; font-size: 13px; }
|
|
635
|
-
.msg.ai code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; }
|
|
636
|
-
.msg.ai :not(pre) > code { background: var(--bg3); padding: 1px 5px; border-radius: 3px; font-size: 12px; }
|
|
637
|
-
.msg.ai h1, .msg.ai h2, .msg.ai h3, .msg.ai h4 { margin: 10px 0 4px; color: var(--accent); }
|
|
638
|
-
.msg.ai ul, .msg.ai ol { padding-left: 20px; margin: 4px 0; }
|
|
639
|
-
.msg.ai li { margin: 2px 0; }
|
|
640
|
-
.msg.ai blockquote { border-left: 3px solid var(--accent); padding-left: 12px; color: var(--fg2); margin: 6px 0; }
|
|
641
|
-
.msg.ai a { color: var(--accent); }
|
|
642
|
-
.msg.ai p { margin: 4px 0; }
|
|
643
|
-
.msg.ai hr { border: none; border-top: 1px solid var(--bg4); margin: 8px 0; }
|
|
644
|
-
.msg.ai table { border-collapse: collapse; margin: 8px 0; width: 100%; }
|
|
645
|
-
.msg.ai th, .msg.ai td { border: 1px solid var(--bg4); padding: 5px 8px; text-align: left; font-size: 13px; }
|
|
646
|
-
.msg.ai th { background: var(--bg3); }
|
|
647
|
-
.msg.system { background: transparent; color: var(--fg3); font-size: 12px; align-self: center; padding: 3px 10px; }
|
|
648
|
-
|
|
649
|
-
/* ── Thinking Block ── */
|
|
650
|
-
.think-block { background: rgba(188,140,255,.07); border: 1px solid rgba(188,140,255,.18); border-radius: 8px; margin: 8px 0; overflow: hidden; }
|
|
651
|
-
.think-header { padding: 7px 12px; cursor: pointer; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--purple); user-select: none; }
|
|
652
|
-
.think-header:hover { background: rgba(188,140,255,.05); }
|
|
653
|
-
.think-chevron { font-size: 10px; transition: transform .15s; }
|
|
654
|
-
.think-block.open .think-chevron { transform: rotate(90deg); }
|
|
655
|
-
.think-content { padding: 8px 12px; font-size: 13px; color: var(--fg2); line-height: 1.5; border-top: 1px solid rgba(188,140,255,.12); max-height: 250px; overflow-y: auto; display: none; }
|
|
656
|
-
.think-block.open .think-content { display: block; }
|
|
657
|
-
.think-block.streaming .think-header .think-label { animation: pulse 1.5s infinite; }
|
|
658
|
-
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .5; } }
|
|
659
|
-
|
|
660
|
-
/* ── AI Exchange Container ── */
|
|
661
|
-
.ai-exchange { display: flex; flex-direction: column; gap: 4px; width: 100%; }
|
|
662
|
-
|
|
663
|
-
/* ── Tool Card ── */
|
|
664
|
-
.tool-card { background: var(--bg3); border: 1px solid var(--bg4); border-radius: 8px; margin: 4px 0; overflow: hidden; font-size: 13px; align-self: flex-start; max-width: 82%; }
|
|
665
|
-
.tool-card.done .tc-spinner { display: none; }
|
|
666
|
-
.tool-card-header { padding: 8px 12px; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: background .12s; }
|
|
667
|
-
.tool-card-header:hover { background: var(--bg4); }
|
|
668
|
-
.tc-icon { font-size: 13px; flex-shrink: 0; }
|
|
669
|
-
.tc-name { color: var(--orange); font-weight: 600; font-family: 'SF Mono', monospace; font-size: 12px; }
|
|
670
|
-
.tc-path { color: var(--fg2); font-family: 'SF Mono', monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
|
|
671
|
-
.tc-status { margin-left: auto; font-size: 12px; flex-shrink: 0; }
|
|
672
|
-
.tc-chevron { font-size: 10px; color: var(--fg3); transition: transform .15s; flex-shrink: 0; }
|
|
673
|
-
.tool-card.expanded .tc-chevron { transform: rotate(90deg); }
|
|
674
|
-
.tool-card-body { display: none; padding: 8px 12px; border-top: 1px solid var(--bg4); }
|
|
675
|
-
.tool-card.expanded .tool-card-body { display: block; }
|
|
676
|
-
.tool-card-body pre { background: var(--bg1); padding: 8px; border-radius: 4px; white-space: pre-wrap; word-break: break-all; font-size: 11px; max-height: 200px; overflow-y: auto; margin: 0; color: var(--fg2); }
|
|
677
|
-
.tool-card.error { border-color: rgba(248,81,73,.3); }
|
|
678
|
-
.tool-card.error .tc-name { color: var(--red); }
|
|
679
|
-
|
|
680
|
-
/* ── Thinking Dots ── */
|
|
681
|
-
.thinking-dots { align-self: flex-start; display: flex; gap: 4px; padding: 14px 16px; }
|
|
682
|
-
.thinking-dots span { width: 7px; height: 7px; background: var(--fg3); border-radius: 50%; animation: bounce .6s ease-in-out infinite; }
|
|
683
|
-
.thinking-dots span:nth-child(2) { animation-delay: .1s; }
|
|
684
|
-
.thinking-dots span:nth-child(3) { animation-delay: .2s; }
|
|
685
|
-
@keyframes bounce { 0%,80%,100% { transform: scale(.7); opacity: .4; } 40% { transform: scale(1); opacity: 1; } }
|
|
686
|
-
|
|
687
|
-
/* ── Input ── */
|
|
688
|
-
.input-area { background: var(--bg2); border-top: 1px solid var(--bg4); padding: 12px 16px; flex-shrink: 0; }
|
|
689
|
-
.input-row { display: flex; gap: 8px; align-items: flex-end; }
|
|
690
|
-
.input-row textarea { flex: 1; background: var(--bg1); border: 1px solid var(--bg4); border-radius: var(--radius); padding: 10px 14px; color: var(--fg1); font-size: 14px; font-family: inherit; resize: none; outline: none; min-height: 44px; max-height: 150px; line-height: 1.5; transition: border-color .15s; }
|
|
691
|
-
.input-row textarea:focus { border-color: var(--accent); }
|
|
692
|
-
.input-row textarea::placeholder { color: var(--fg3); }
|
|
693
|
-
.in-btn { width: 42px; height: 42px; border-radius: var(--radius); border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .12s; flex-shrink: 0; }
|
|
694
|
-
.in-btn.send { background: var(--accent2); }
|
|
695
|
-
.in-btn.send:hover { background: var(--accent); }
|
|
696
|
-
.in-btn.send:disabled { opacity: .3; cursor: default; }
|
|
697
|
-
.in-btn.stop { background: var(--red); display: none; }
|
|
698
|
-
.in-btn.stop:hover { background: #da3633; }
|
|
699
|
-
.in-btn.stop.visible { display: flex; }
|
|
700
|
-
.input-hint { display: flex; gap: 14px; margin-top: 6px; font-size: 10px; color: var(--fg3); }
|
|
701
|
-
.input-hint kbd { background: var(--bg3); padding: 1px 4px; border-radius: 3px; font-family: monospace; font-size: 9px; }
|
|
702
|
-
|
|
703
|
-
/* ── Welcome ── */
|
|
704
|
-
.welcome { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--fg3); gap: 10px; padding: 40px; text-align: center; }
|
|
705
|
-
.welcome .logo { font-size: 44px; margin-bottom: 6px; }
|
|
706
|
-
.welcome h2 { color: var(--fg1); font-size: 20px; }
|
|
707
|
-
.welcome p { max-width: 380px; line-height: 1.5; font-size: 14px; }
|
|
708
|
-
.welcome .chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; justify-content: center; }
|
|
709
|
-
.welcome .chip { background: var(--bg3); border: 1px solid var(--bg4); border-radius: 18px; padding: 7px 14px; font-size: 12px; color: var(--fg2); cursor: pointer; transition: all .12s; }
|
|
710
|
-
.welcome .chip:hover { border-color: var(--accent); color: var(--accent); }
|
|
711
|
-
|
|
712
|
-
/* ── Right Panel ── */
|
|
713
|
-
.right-panel { width: 0; background: var(--bg2); border-left: 1px solid var(--bg4); display: flex; flex-direction: column; flex-shrink: 0; transition: width .2s; overflow: hidden; }
|
|
714
|
-
.right-panel.open { width: 380px; }
|
|
715
|
-
.rp-header { padding: 12px 16px; border-bottom: 1px solid var(--bg4); display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
716
|
-
.rp-header h3 { font-size: 13px; flex: 1; }
|
|
717
|
-
.rp-close { background: none; border: none; color: var(--fg2); cursor: pointer; font-size: 16px; padding: 2px 6px; border-radius: 4px; }
|
|
718
|
-
.rp-close:hover { background: var(--bg3); color: var(--fg1); }
|
|
719
|
-
.rp-tabs { display: flex; border-bottom: 1px solid var(--bg4); flex-shrink: 0; }
|
|
720
|
-
.rp-tabs button { flex: 1; padding: 8px; background: none; border: none; border-bottom: 2px solid transparent; color: var(--fg3); font-size: 11px; cursor: pointer; }
|
|
721
|
-
.rp-tabs button.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
722
|
-
.file-tree { flex: 1; overflow-y: auto; padding: 6px; font-size: 13px; }
|
|
723
|
-
.file-tree::-webkit-scrollbar { width: 5px; }
|
|
724
|
-
.file-tree::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
|
725
|
-
.ft-item { padding: 5px 8px; cursor: pointer; border-radius: 4px; color: var(--fg2); display: flex; align-items: center; gap: 6px; font-family: 'SF Mono', monospace; font-size: 12px; }
|
|
726
|
-
.ft-item:hover { background: var(--bg3); color: var(--fg1); }
|
|
727
|
-
.ft-item.dir { color: var(--accent); }
|
|
728
|
-
.ft-item.active { background: rgba(88,166,255,.1); color: var(--accent); }
|
|
729
|
-
.ft-indent { padding-left: 16px; }
|
|
730
|
-
.ft-icon { flex-shrink: 0; font-size: 13px; }
|
|
731
|
-
.ft-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
732
|
-
|
|
733
|
-
/* ── File Editor ── */
|
|
734
|
-
.file-editor { display: none; flex-direction: column; border-top: 1px solid var(--bg4); }
|
|
735
|
-
.file-editor.open { display: flex; flex: 1; min-height: 200px; }
|
|
736
|
-
.fe-header { padding: 8px 12px; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--bg4); background: var(--bg3); flex-shrink: 0; }
|
|
737
|
-
.fe-path { font-family: 'SF Mono', monospace; font-size: 11px; color: var(--fg2); flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
|
738
|
-
.fe-btn { padding: 4px 10px; border: 1px solid var(--bg4); border-radius: 4px; background: var(--bg3); color: var(--fg2); font-size: 11px; cursor: pointer; }
|
|
739
|
-
.fe-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
740
|
-
.fe-btn.save { background: var(--accent2); border-color: var(--accent2); color: #fff; }
|
|
741
|
-
.fe-btn.save:hover { background: var(--accent); }
|
|
742
|
-
.fe-content { flex: 1; overflow: auto; }
|
|
743
|
-
.fe-content pre { padding: 10px 12px; margin: 0; font-family: 'SF Mono', monospace; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; color: var(--fg1); min-height: 100%; }
|
|
744
|
-
.fe-content textarea { width: 100%; height: 100%; padding: 10px 12px; background: var(--bg1); border: none; color: var(--fg1); font-family: 'SF Mono', monospace; font-size: 12px; line-height: 1.5; resize: none; outline: none; }
|
|
745
|
-
|
|
746
|
-
/* ── Modal ── */
|
|
747
|
-
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 200; align-items: center; justify-content: center; }
|
|
748
|
-
.modal-overlay.visible { display: flex; }
|
|
749
|
-
.modal { background: var(--bg2); border: 1px solid var(--bg4); border-radius: 12px; width: 480px; max-width: 90vw; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,.5); }
|
|
750
|
-
.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--bg4); display: flex; align-items: center; }
|
|
751
|
-
.modal-header h3 { flex: 1; font-size: 15px; }
|
|
752
|
-
.modal-close { background: none; border: none; color: var(--fg2); cursor: pointer; font-size: 18px; padding: 2px 6px; border-radius: 4px; }
|
|
753
|
-
.modal-close:hover { background: var(--bg3); }
|
|
754
|
-
.modal-body { padding: 16px 20px; overflow-y: auto; flex: 1; }
|
|
755
|
-
.modal-footer { padding: 12px 20px; border-top: 1px solid var(--bg4); display: flex; justify-content: flex-end; gap: 8px; }
|
|
756
|
-
.m-field { margin-bottom: 14px; }
|
|
757
|
-
.m-field label { display: block; font-size: 12px; color: var(--fg2); margin-bottom: 5px; font-weight: 500; }
|
|
758
|
-
.m-field input, .m-field textarea { width: 100%; background: var(--bg1); border: 1px solid var(--bg4); border-radius: var(--radius-sm); padding: 8px 10px; color: var(--fg1); font-size: 13px; outline: none; font-family: inherit; }
|
|
759
|
-
.m-field input:focus, .m-field textarea:focus { border-color: var(--accent); }
|
|
760
|
-
.m-field textarea { min-height: 120px; resize: vertical; font-family: 'SF Mono', monospace; font-size: 12px; }
|
|
761
|
-
.m-field .checkbox-group { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
762
|
-
.m-field .checkbox-group label { display: flex; align-items: center; gap: 4px; background: var(--bg3); padding: 4px 10px; border-radius: 14px; font-size: 12px; cursor: pointer; color: var(--fg2); border: 1px solid var(--bg4); }
|
|
763
|
-
.m-field .checkbox-group label:has(input:checked) { background: rgba(88,166,255,.12); border-color: var(--accent); color: var(--accent); }
|
|
764
|
-
.m-field .checkbox-group input { display: none; }
|
|
765
|
-
.m-btn { padding: 8px 18px; border-radius: var(--radius-sm); border: 1px solid var(--bg4); font-size: 13px; cursor: pointer; font-weight: 500; }
|
|
766
|
-
.m-btn.primary { background: var(--accent2); border-color: var(--accent2); color: #fff; }
|
|
767
|
-
.m-btn.primary:hover { background: var(--accent); }
|
|
768
|
-
.m-btn.secondary { background: var(--bg3); color: var(--fg2); }
|
|
769
|
-
.m-btn.secondary:hover { background: var(--bg4); color: var(--fg1); }
|
|
770
|
-
</style>
|
|
771
|
-
</head>
|
|
772
|
-
<body>
|
|
773
|
-
|
|
774
|
-
<!-- Sidebar -->
|
|
775
|
-
<div class="sidebar">
|
|
776
|
-
<div class="sidebar-header">
|
|
777
|
-
<h1>⚡ <span>Sapper</span></h1>
|
|
778
|
-
</div>
|
|
779
|
-
<div class="sidebar-tabs">
|
|
780
|
-
<button class="active" onclick="switchTab('sessions')">Sessions</button>
|
|
781
|
-
<button onclick="switchTab('agents')">Agents</button>
|
|
782
|
-
<button onclick="switchTab('skills')">Skills</button>
|
|
783
|
-
</div>
|
|
784
|
-
<div class="tab-panel active" id="sessionsPanel">
|
|
785
|
-
<div class="tab-create-btn" onclick="createNewChat()">✚ New Chat</div>
|
|
786
|
-
<div id="sessionList"></div>
|
|
787
|
-
</div>
|
|
788
|
-
<div class="tab-panel" id="agentsPanel">
|
|
789
|
-
<div class="tab-create-btn" onclick="openCreateAgent()">✚ Create Agent</div>
|
|
790
|
-
<div id="agentList"></div>
|
|
791
|
-
</div>
|
|
792
|
-
<div class="tab-panel" id="skillsPanel">
|
|
793
|
-
<div class="tab-create-btn" onclick="openCreateSkill()">✚ Create Skill</div>
|
|
794
|
-
<div id="skillList"></div>
|
|
795
|
-
</div>
|
|
796
|
-
<div class="qa-section">
|
|
797
|
-
<div class="qa-title">Quick Actions</div>
|
|
798
|
-
<div class="qa-grid">
|
|
799
|
-
<div class="qa-btn" onclick="qaAction('list')"><span class="qa-icon">📂</span> Browse Dir</div>
|
|
800
|
-
<div class="qa-btn" onclick="qaAction('read')"><span class="qa-icon">📄</span> Read File</div>
|
|
801
|
-
<div class="qa-btn" onclick="qaAction('write')"><span class="qa-icon">✎</span> Create File</div>
|
|
802
|
-
<div class="qa-btn" onclick="qaAction('search')"><span class="qa-icon">🔍</span> Search</div>
|
|
803
|
-
<div class="qa-btn" onclick="qaAction('shell')"><span class="qa-icon">▶</span> Terminal</div>
|
|
804
|
-
<div class="qa-btn" onclick="qaAction('mkdir')"><span class="qa-icon">📁</span> New Dir</div>
|
|
805
|
-
<div class="qa-btn" onclick="qaAction('review')"><span class="qa-icon">🔎</span> Review</div>
|
|
806
|
-
<div class="qa-btn" onclick="qaAction('scan')"><span class="qa-icon">📊</span> Scan</div>
|
|
807
|
-
</div>
|
|
808
|
-
</div>
|
|
809
|
-
<div class="sidebar-footer">
|
|
810
|
-
<div class="sf-row"><span>Model</span><span class="sf-model" id="modelTag">loading...</span></div>
|
|
811
|
-
<select id="modelSelect" onchange="selectModel()"></select>
|
|
812
|
-
</div>
|
|
813
|
-
</div>
|
|
814
836
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
<div class="agent-badge" id="agentBadge">Sapper</div>
|
|
820
|
-
<div class="session-name" id="sessionName" onclick="renameCurrentSession()" title="Click to rename">New Chat</div>
|
|
821
|
-
<div class="spacer"></div>
|
|
822
|
-
<div class="cwd" id="cwdDisplay"></div>
|
|
823
|
-
<button class="tb-btn" onclick="toggleFilePanel()">📂 Files</button>
|
|
824
|
-
</div>
|
|
825
|
-
<div class="chat" id="chat">
|
|
826
|
-
<div class="welcome" id="welcome">
|
|
827
|
-
<div class="logo">⚡</div>
|
|
828
|
-
<h2>Sapper</h2>
|
|
829
|
-
<p>AI assistant with full filesystem access. Ask anything — code, write, analyze, build.</p>
|
|
830
|
-
<div class="chips">
|
|
831
|
-
<div class="chip" onclick="sendQuick('What files are in this project?')">📁 Explore project</div>
|
|
832
|
-
<div class="chip" onclick="sendQuick('Help me fix bugs in the codebase')">🐛 Find bugs</div>
|
|
833
|
-
<div class="chip" onclick="sendQuick('Write a README for this project')">📝 Write docs</div>
|
|
834
|
-
<div class="chip" onclick="sendQuick('What are my tasks for today?')">📋 Today tasks</div>
|
|
835
|
-
</div>
|
|
836
|
-
</div>
|
|
837
|
-
</div>
|
|
838
|
-
<div class="input-area">
|
|
839
|
-
<div class="input-row">
|
|
840
|
-
<textarea id="input" placeholder="Message Sapper..." rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
|
|
841
|
-
<button class="in-btn send" id="sendBtn" onclick="send()" title="Send">
|
|
842
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
843
|
-
</button>
|
|
844
|
-
<button class="in-btn stop" id="stopBtn" onclick="stopGeneration()" title="Stop">
|
|
845
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>
|
|
846
|
-
</button>
|
|
847
|
-
</div>
|
|
848
|
-
<div class="input-hint">
|
|
849
|
-
<span><kbd>Enter</kbd> send</span>
|
|
850
|
-
<span><kbd>Shift+Enter</kbd> new line</span>
|
|
851
|
-
</div>
|
|
852
|
-
</div>
|
|
853
|
-
</div>
|
|
837
|
+
window.collapseAll = function() {
|
|
838
|
+
state.expanded = {};
|
|
839
|
+
loadTree();
|
|
840
|
+
};
|
|
854
841
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
<div class="file-tree" id="fileTree"></div>
|
|
862
|
-
<div class="file-editor" id="fileEditor">
|
|
863
|
-
<div class="fe-header">
|
|
864
|
-
<span class="fe-path" id="fePath"></span>
|
|
865
|
-
<button class="fe-btn" id="feEditBtn" onclick="startEditing()">Edit</button>
|
|
866
|
-
<button class="fe-btn save" id="feSaveBtn" onclick="saveFileEdit()" style="display:none">Save</button>
|
|
867
|
-
<button class="fe-btn" id="feCancelBtn" onclick="cancelEdit()" style="display:none">Cancel</button>
|
|
868
|
-
</div>
|
|
869
|
-
<div class="fe-content" id="feContent"></div>
|
|
870
|
-
</div>
|
|
871
|
-
</div>
|
|
842
|
+
// ─── Context menu + file actions ─────────────────────────────
|
|
843
|
+
function closeCtxMenu() {
|
|
844
|
+
var m = document.getElementById('ctxMenu');
|
|
845
|
+
if (m) m.remove();
|
|
846
|
+
document.querySelectorAll('.rmenu.open').forEach(function(e){ e.classList.remove('open'); });
|
|
847
|
+
}
|
|
872
848
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
<div class="modal-body" id="modalBody"></div>
|
|
881
|
-
<div class="modal-footer" id="modalFooter"></div>
|
|
882
|
-
</div>
|
|
883
|
-
</div>
|
|
849
|
+
document.addEventListener('click', function(e){
|
|
850
|
+
if (e.target.closest && e.target.closest('#ctxMenu')) return;
|
|
851
|
+
closeCtxMenu();
|
|
852
|
+
});
|
|
853
|
+
document.addEventListener('keydown', function(e){
|
|
854
|
+
if (e.key === 'Escape') closeCtxMenu();
|
|
855
|
+
});
|
|
884
856
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
var
|
|
888
|
-
|
|
889
|
-
var
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
var
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
var
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
function
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
857
|
+
function openRowMenu(anchor, path, isDir) {
|
|
858
|
+
closeCtxMenu();
|
|
859
|
+
var rect = anchor.getBoundingClientRect();
|
|
860
|
+
if (anchor.classList) anchor.classList.add('open');
|
|
861
|
+
var menu = document.createElement('div');
|
|
862
|
+
menu.id = 'ctxMenu';
|
|
863
|
+
menu.className = 'ctx-menu';
|
|
864
|
+
var items = [];
|
|
865
|
+
if (isDir) {
|
|
866
|
+
items.push({ label: '🗎 New file inside', fn: function(){ newItemPrompt('file', path); } });
|
|
867
|
+
items.push({ label: '📁 New folder inside', fn: function(){ newItemPrompt('folder', path); } });
|
|
868
|
+
items.push({ sep: true });
|
|
869
|
+
items.push({ label: 'Expand / Collapse', fn: function(){
|
|
870
|
+
var row = document.querySelector('.row[data-path="' + cssEscape(path) + '"]');
|
|
871
|
+
if (row) toggleDir(row, path);
|
|
872
|
+
}});
|
|
873
|
+
} else {
|
|
874
|
+
items.push({ label: 'Open', fn: function(){ openFile(path); } });
|
|
875
|
+
}
|
|
876
|
+
items.push({ sep: true });
|
|
877
|
+
items.push({ label: 'Rename\u2026', fn: function(){ renamePrompt(path); } });
|
|
878
|
+
items.push({ label: 'Duplicate', fn: function(){ duplicateItem(path); } });
|
|
879
|
+
items.push({ label: 'Copy path', fn: function(){ copyText(path); showToast('Path copied'); } });
|
|
880
|
+
items.push({ label: 'Copy name', fn: function(){ copyText(path.split('/').pop()); showToast('Name copied'); } });
|
|
881
|
+
items.push({ sep: true });
|
|
882
|
+
items.push({ label: 'Reveal in Finder', fn: function(){
|
|
883
|
+
fetch('/api/fs/reveal', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ path: path }) });
|
|
884
|
+
}});
|
|
885
|
+
if (!isDir) {
|
|
886
|
+
items.push({ label: 'Send path to terminal', fn: function(){ sendCmd(path); } });
|
|
887
|
+
items.push({ label: 'Use as preview', fn: function(){ openFile(path); } });
|
|
888
|
+
}
|
|
889
|
+
items.push({ sep: true });
|
|
890
|
+
items.push({ label: 'Delete (move to .sapper/.trash)', danger: true, fn: function(){ deleteItem(path, false); } });
|
|
891
|
+
|
|
892
|
+
items.forEach(function(it){
|
|
893
|
+
if (it.sep) { var s = document.createElement('div'); s.className = 'sep'; menu.appendChild(s); return; }
|
|
894
|
+
var el = document.createElement('div');
|
|
895
|
+
el.className = 'ci' + (it.danger ? ' danger' : '');
|
|
896
|
+
el.innerHTML = '<span>' + it.label + '</span>';
|
|
897
|
+
el.addEventListener('click', function(e){ e.stopPropagation(); closeCtxMenu(); it.fn(); });
|
|
898
|
+
menu.appendChild(el);
|
|
927
899
|
});
|
|
928
|
-
}
|
|
929
900
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
901
|
+
document.body.appendChild(menu);
|
|
902
|
+
// Position
|
|
903
|
+
var mw = menu.offsetWidth, mh = menu.offsetHeight;
|
|
904
|
+
var x = rect.right + 4, y = rect.top;
|
|
905
|
+
if (x + mw > window.innerWidth - 8) x = Math.max(8, rect.left - mw - 4);
|
|
906
|
+
if (y + mh > window.innerHeight - 8) y = Math.max(8, window.innerHeight - mh - 8);
|
|
907
|
+
menu.style.left = x + 'px';
|
|
908
|
+
menu.style.top = y + 'px';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function copyText(t) {
|
|
912
|
+
try { navigator.clipboard.writeText(t); }
|
|
913
|
+
catch(e) {
|
|
914
|
+
var ta = document.createElement('textarea');
|
|
915
|
+
ta.value = t; document.body.appendChild(ta); ta.select();
|
|
916
|
+
try { document.execCommand('copy'); } catch(_){}
|
|
917
|
+
ta.remove();
|
|
942
918
|
}
|
|
943
|
-
if (name === 'sessions') refreshSessions();
|
|
944
|
-
if (name === 'agents') refreshAgents();
|
|
945
|
-
if (name === 'skills') refreshSkills();
|
|
946
919
|
}
|
|
947
920
|
|
|
948
|
-
// ───
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
item.innerHTML = '<span class="s-icon">💬</span>' +
|
|
957
|
-
'<span class="s-label">' + esc(s.name) + '</span>' +
|
|
958
|
-
'<span class="s-meta">' + s.msgCount + '</span>' +
|
|
959
|
-
'<button class="s-del" onclick="event.stopPropagation(); deleteSession('' + esc(s.id) + '')" title="Delete">✕</button>';
|
|
960
|
-
item.setAttribute('data-id', s.id);
|
|
961
|
-
item.onclick = (function(sid) { return function() { loadSessionById(sid); }; })(s.id);
|
|
962
|
-
el.appendChild(item);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
function refreshSessions() {
|
|
966
|
-
fetch('/api/sessions').then(function(r){return r.json();}).then(function(d){renderSessionList(d.sessions);});
|
|
967
|
-
}
|
|
968
|
-
function createNewChat() {
|
|
969
|
-
currentSessionId = null;
|
|
970
|
-
currentSessionName = 'New Chat';
|
|
971
|
-
document.getElementById('sessionName').textContent = 'New Chat';
|
|
972
|
-
chatEl.innerHTML = '';
|
|
973
|
-
chatEl.appendChild(welcomeEl);
|
|
974
|
-
welcomeEl.style.display = 'flex';
|
|
975
|
-
fetch('/api/clear', {method: 'POST'});
|
|
976
|
-
refreshSessions();
|
|
977
|
-
}
|
|
978
|
-
function loadSessionById(id) {
|
|
979
|
-
fetch('/api/sessions/load', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id:id})})
|
|
980
|
-
.then(function(r){return r.json();}).then(function(d) {
|
|
981
|
-
if (!d.session) return;
|
|
982
|
-
currentSessionId = id;
|
|
983
|
-
currentSessionName = d.session.name || 'Chat';
|
|
984
|
-
document.getElementById('sessionName').textContent = currentSessionName;
|
|
985
|
-
if (d.session.model) {
|
|
986
|
-
currentModel = d.session.model;
|
|
987
|
-
document.getElementById('modelSelect').value = currentModel;
|
|
988
|
-
document.getElementById('modelTag').textContent = shortModel(currentModel);
|
|
921
|
+
// ─── Modal prompt ─────────────────────────────────────────────
|
|
922
|
+
function showModal(opts) {
|
|
923
|
+
return new Promise(function(resolve){
|
|
924
|
+
var bd = document.createElement('div'); bd.className = 'modal-bd';
|
|
925
|
+
var html = '<div class="modal"><h3>' + esc(opts.title) + '</h3>';
|
|
926
|
+
if (opts.label) html += '<label>' + esc(opts.label) + '</label>';
|
|
927
|
+
if (opts.input !== false) {
|
|
928
|
+
html += '<input type="text" id="mdInput" value="' + esc(opts.value || '') + '" placeholder="' + esc(opts.placeholder || '') + '">';
|
|
989
929
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
930
|
+
if (opts.hint) html += '<div class="hint">' + esc(opts.hint) + '</div>';
|
|
931
|
+
html += '<div class="actions">' +
|
|
932
|
+
'<button id="mdCancel">' + (opts.cancelLabel || 'Cancel') + '</button>' +
|
|
933
|
+
'<button id="mdOk" class="' + (opts.danger ? 'danger' : 'primary') + '">' + (opts.okLabel || 'OK') + '</button>' +
|
|
934
|
+
'</div></div>';
|
|
935
|
+
bd.innerHTML = html;
|
|
936
|
+
document.body.appendChild(bd);
|
|
937
|
+
var input = bd.querySelector('#mdInput');
|
|
938
|
+
var ok = function(){ var v = input ? input.value : ''; bd.remove(); resolve(v); };
|
|
939
|
+
var cancel = function(){ bd.remove(); resolve(null); };
|
|
940
|
+
bd.querySelector('#mdOk').addEventListener('click', ok);
|
|
941
|
+
bd.querySelector('#mdCancel').addEventListener('click', cancel);
|
|
942
|
+
bd.addEventListener('click', function(e){ if (e.target === bd) cancel(); });
|
|
943
|
+
if (input) {
|
|
944
|
+
input.focus();
|
|
945
|
+
// Select stem (before last dot) for renames
|
|
946
|
+
if (opts.selectStem && input.value) {
|
|
947
|
+
var dot = input.value.lastIndexOf('.');
|
|
948
|
+
if (dot > 0) input.setSelectionRange(0, dot);
|
|
949
|
+
else input.select();
|
|
950
|
+
} else if (input.value) {
|
|
951
|
+
input.select();
|
|
1003
952
|
}
|
|
953
|
+
input.addEventListener('keydown', function(e){
|
|
954
|
+
if (e.key === 'Enter') ok();
|
|
955
|
+
if (e.key === 'Escape') cancel();
|
|
956
|
+
});
|
|
1004
957
|
}
|
|
1005
|
-
scrollDown();
|
|
1006
|
-
refreshSessions();
|
|
1007
958
|
});
|
|
1008
959
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
960
|
+
|
|
961
|
+
window.newItemPrompt = async function(kind, parentDir) {
|
|
962
|
+
var defPath = parentDir ? (parentDir + '/') : '';
|
|
963
|
+
var v = await showModal({
|
|
964
|
+
title: kind === 'folder' ? 'New folder' : 'New file',
|
|
965
|
+
label: 'Path (relative to workspace)',
|
|
966
|
+
value: defPath,
|
|
967
|
+
placeholder: kind === 'folder' ? 'src/utils' : 'src/index.ts',
|
|
968
|
+
hint: 'Use "/" for subdirectories. Intermediate folders are created automatically.',
|
|
969
|
+
okLabel: 'Create',
|
|
1014
970
|
});
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (!
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (msgs.length > 0) firstUser = msgs[0].textContent;
|
|
1022
|
-
if (firstUser) currentSessionName = firstUser.substring(0, 50);
|
|
1023
|
-
document.getElementById('sessionName').textContent = currentSessionName;
|
|
1024
|
-
}
|
|
1025
|
-
fetch('/api/sessions/save', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: currentSessionId, name: currentSessionName})});
|
|
1026
|
-
}
|
|
1027
|
-
function renameCurrentSession() {
|
|
1028
|
-
if (!currentSessionId) return;
|
|
1029
|
-
showPromptModal('Rename Session', 'Session name', currentSessionName, function(val) {
|
|
1030
|
-
if (!val) return;
|
|
1031
|
-
currentSessionName = val;
|
|
1032
|
-
document.getElementById('sessionName').textContent = val;
|
|
1033
|
-
fetch('/api/sessions/rename', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: currentSessionId, name: val})})
|
|
1034
|
-
.then(function() { refreshSessions(); });
|
|
971
|
+
if (v == null) return;
|
|
972
|
+
v = v.replace(/^[\\/]+/, '').trim();
|
|
973
|
+
if (!v) return;
|
|
974
|
+
var r = await fetch('/api/fs/new', {
|
|
975
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
976
|
+
body: JSON.stringify({ path: v, kind: kind })
|
|
1035
977
|
});
|
|
1036
|
-
|
|
978
|
+
var d = await r.json();
|
|
979
|
+
if (d.error) { showToast('Create failed: ' + d.error, 'err'); return; }
|
|
980
|
+
showToast(kind === 'folder' ? 'Folder created' : 'File created');
|
|
981
|
+
// expand parent + refresh
|
|
982
|
+
if (parentDir) state.expanded[parentDir] = true;
|
|
983
|
+
loadTree();
|
|
984
|
+
if (kind === 'file') setTimeout(function(){ openFile(v); }, 200);
|
|
985
|
+
};
|
|
1037
986
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
var
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
var keys = Object.keys(agents);
|
|
1048
|
-
for (var i = 0; i < keys.length; i++) {
|
|
1049
|
-
var k = keys[i], a = agents[k];
|
|
1050
|
-
var item = document.createElement('div');
|
|
1051
|
-
item.className = 's-item' + (currentAgentKey === k ? ' active' : '');
|
|
1052
|
-
item.innerHTML = '<span class="s-icon">🤖</span>' +
|
|
1053
|
-
'<span class="s-label">' + esc(a.name) + '</span>' +
|
|
1054
|
-
'<button class="s-del" onclick="event.stopPropagation(); deleteAgent('' + esc(k) + '')" title="Delete">✕</button>';
|
|
1055
|
-
item.title = a.description || '';
|
|
1056
|
-
item.onclick = (function(key, name) { return function() { switchAgent(key, name); }; })(k, a.name);
|
|
1057
|
-
el.appendChild(item);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
function refreshAgents() {
|
|
1061
|
-
fetch('/api/agents').then(function(r){return r.json();}).then(function(d){renderAgentList(d.agents);});
|
|
1062
|
-
}
|
|
1063
|
-
function switchAgent(key, name) {
|
|
1064
|
-
currentAgentKey = key;
|
|
1065
|
-
currentAgent = key;
|
|
1066
|
-
document.getElementById('agentBadge').textContent = name;
|
|
1067
|
-
fetch('/api/agent', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({agent: key})});
|
|
1068
|
-
addSystem('Switched to ' + name);
|
|
1069
|
-
refreshAgents();
|
|
1070
|
-
}
|
|
1071
|
-
function deleteAgent(key) {
|
|
1072
|
-
if (!confirm('Delete agent "' + key + '"?')) return;
|
|
1073
|
-
fetch('/api/agents/delete', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({key: key})})
|
|
1074
|
-
.then(function() {
|
|
1075
|
-
if (currentAgentKey === key) switchAgent(null, 'Sapper');
|
|
1076
|
-
refreshAgents();
|
|
987
|
+
async function renamePrompt(path) {
|
|
988
|
+
var name = path.split('/').pop();
|
|
989
|
+
var parent = path.split('/').slice(0, -1).join('/');
|
|
990
|
+
var v = await showModal({
|
|
991
|
+
title: 'Rename',
|
|
992
|
+
label: 'New name',
|
|
993
|
+
value: name,
|
|
994
|
+
selectStem: true,
|
|
995
|
+
okLabel: 'Rename',
|
|
1077
996
|
});
|
|
997
|
+
if (v == null) return;
|
|
998
|
+
v = v.trim();
|
|
999
|
+
if (!v || v === name) return;
|
|
1000
|
+
var to = parent ? (parent + '/' + v) : v;
|
|
1001
|
+
var r = await fetch('/api/fs/rename', {
|
|
1002
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
1003
|
+
body: JSON.stringify({ from: path, to: to })
|
|
1004
|
+
});
|
|
1005
|
+
var d = await r.json();
|
|
1006
|
+
if (d.error) { showToast('Rename failed: ' + d.error, 'err'); return; }
|
|
1007
|
+
showToast('Renamed');
|
|
1008
|
+
if (state.currentFile === path) state.currentFile = to;
|
|
1009
|
+
loadTree();
|
|
1078
1010
|
}
|
|
1079
|
-
function openCreateAgent() {
|
|
1080
|
-
var allTools = ['read', 'write', 'patch', 'list', 'search', 'shell', 'mkdir'];
|
|
1081
|
-
var body = '<div class="m-field"><label>Agent Name</label><input id="maName" placeholder="e.g. Code Reviewer"></div>' +
|
|
1082
|
-
'<div class="m-field"><label>Description</label><input id="maDesc" placeholder="What does this agent do?"></div>' +
|
|
1083
|
-
'<div class="m-field"><label>Allowed Tools</label><div class="checkbox-group" id="maTools">';
|
|
1084
|
-
for (var i = 0; i < allTools.length; i++) {
|
|
1085
|
-
body += '<label><input type="checkbox" value="' + allTools[i] + '" checked> ' + allTools[i].toUpperCase() + '</label>';
|
|
1086
|
-
}
|
|
1087
|
-
body += '</div></div>' +
|
|
1088
|
-
'<div class="m-field"><label>Agent Instructions (Markdown)</label><textarea id="maContent" placeholder="# Agent Name' + NL + NL + 'Instructions for the agent..."></textarea></div>';
|
|
1089
|
-
showModal('Create Agent', body,
|
|
1090
|
-
'<button class="m-btn secondary" onclick="closeModal()">Cancel</button>' +
|
|
1091
|
-
'<button class="m-btn primary" onclick="submitCreateAgent()">Create</button>');
|
|
1092
|
-
}
|
|
1093
|
-
function submitCreateAgent() {
|
|
1094
|
-
var name = document.getElementById('maName').value.trim();
|
|
1095
|
-
var desc = document.getElementById('maDesc').value.trim();
|
|
1096
|
-
var content = document.getElementById('maContent').value;
|
|
1097
|
-
if (!name) { alert('Name is required'); return; }
|
|
1098
|
-
var checkboxes = document.querySelectorAll('#maTools input:checked');
|
|
1099
|
-
var tools = [];
|
|
1100
|
-
for (var i = 0; i < checkboxes.length; i++) tools.push(checkboxes[i].value);
|
|
1101
|
-
fetch('/api/agents/create', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({name:name, description:desc, tools:tools, content:content})})
|
|
1102
|
-
.then(function(r){return r.json();}).then(function() { closeModal(); refreshAgents(); });
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// ─── Skills ────────────────────────────────────────────────
|
|
1106
|
-
function renderSkillList(skills) {
|
|
1107
|
-
var el = document.getElementById('skillList');
|
|
1108
|
-
el.innerHTML = '';
|
|
1109
|
-
var keys = Object.keys(skills);
|
|
1110
|
-
if (keys.length === 0) {
|
|
1111
|
-
el.innerHTML = '<div style="padding:12px;color:var(--fg3);font-size:12px;">No skills yet</div>';
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
for (var i = 0; i < keys.length; i++) {
|
|
1115
|
-
var k = keys[i], s = skills[k];
|
|
1116
|
-
var item = document.createElement('div');
|
|
1117
|
-
item.className = 's-item';
|
|
1118
|
-
item.innerHTML = '<span class="s-icon">📚</span>' +
|
|
1119
|
-
'<span class="s-label">' + esc(s.name) + '</span>' +
|
|
1120
|
-
'<button class="s-del" onclick="event.stopPropagation(); deleteSkill('' + esc(k) + '')" title="Delete">✕</button>';
|
|
1121
|
-
item.title = s.description || '';
|
|
1122
|
-
el.appendChild(item);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
function refreshSkills() {
|
|
1126
|
-
fetch('/api/skills').then(function(r){return r.json();}).then(function(d){renderSkillList(d.skills);});
|
|
1127
|
-
}
|
|
1128
|
-
function deleteSkill(key) {
|
|
1129
|
-
if (!confirm('Delete skill "' + key + '"?')) return;
|
|
1130
|
-
fetch('/api/skills/delete', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({key: key})})
|
|
1131
|
-
.then(function() { refreshSkills(); });
|
|
1132
|
-
}
|
|
1133
|
-
function openCreateSkill() {
|
|
1134
|
-
var body = '<div class="m-field"><label>Skill Name</label><input id="msName" placeholder="e.g. Testing"></div>' +
|
|
1135
|
-
'<div class="m-field"><label>Description</label><input id="msDesc" placeholder="What does this skill teach?"></div>' +
|
|
1136
|
-
'<div class="m-field"><label>Skill Content (Markdown)</label><textarea id="msContent" placeholder="# Skill Name' + NL + NL + 'Knowledge and instructions..."></textarea></div>';
|
|
1137
|
-
showModal('Create Skill', body,
|
|
1138
|
-
'<button class="m-btn secondary" onclick="closeModal()">Cancel</button>' +
|
|
1139
|
-
'<button class="m-btn primary" onclick="submitCreateSkill()">Create</button>');
|
|
1140
|
-
}
|
|
1141
|
-
function submitCreateSkill() {
|
|
1142
|
-
var name = document.getElementById('msName').value.trim();
|
|
1143
|
-
var desc = document.getElementById('msDesc').value.trim();
|
|
1144
|
-
var content = document.getElementById('msContent').value;
|
|
1145
|
-
if (!name) { alert('Name is required'); return; }
|
|
1146
|
-
fetch('/api/skills/create', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({name:name, description:desc, content:content})})
|
|
1147
|
-
.then(function(r){return r.json();}).then(function() { closeModal(); refreshSkills(); });
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// ─── Model ─────────────────────────────────────────────────
|
|
1151
|
-
function selectModel() {
|
|
1152
|
-
currentModel = document.getElementById('modelSelect').value;
|
|
1153
|
-
document.getElementById('modelTag').textContent = shortModel(currentModel);
|
|
1154
|
-
fetch('/api/model', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({model: currentModel})});
|
|
1155
|
-
addSystem('Model: ' + currentModel);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// ─── Chat ──────────────────────────────────────────────────
|
|
1159
|
-
function handleKey(e) {
|
|
1160
|
-
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
|
1161
|
-
}
|
|
1162
|
-
function autoResize(el) {
|
|
1163
|
-
el.style.height = 'auto';
|
|
1164
|
-
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
|
1165
|
-
}
|
|
1166
|
-
function send(text) {
|
|
1167
|
-
var msg = text || inputEl.value.trim();
|
|
1168
|
-
if (!msg || isStreaming) return;
|
|
1169
|
-
inputEl.value = ''; inputEl.style.height = 'auto';
|
|
1170
|
-
welcomeEl.style.display = 'none';
|
|
1171
|
-
addMsg('user', msg);
|
|
1172
|
-
isStreaming = true;
|
|
1173
|
-
document.getElementById('sendBtn').disabled = true;
|
|
1174
|
-
document.getElementById('stopBtn').classList.add('visible');
|
|
1175
|
-
document.getElementById('statusDot').style.background = 'var(--orange)';
|
|
1176
|
-
|
|
1177
|
-
// Container holds all AI rounds + tool cards for this exchange
|
|
1178
|
-
var container = document.createElement('div');
|
|
1179
|
-
container.className = 'ai-exchange';
|
|
1180
|
-
chatEl.appendChild(container);
|
|
1181
|
-
|
|
1182
|
-
var dots = document.createElement('div');
|
|
1183
|
-
dots.className = 'thinking-dots';
|
|
1184
|
-
dots.innerHTML = '<span></span><span></span><span></span>';
|
|
1185
|
-
container.appendChild(dots);
|
|
1186
|
-
|
|
1187
|
-
fetch('/api/chat', {
|
|
1188
|
-
method: 'POST',
|
|
1189
|
-
headers: {'Content-Type': 'application/json'},
|
|
1190
|
-
body: JSON.stringify({message: msg})
|
|
1191
|
-
}).then(function(res) {
|
|
1192
|
-
var reader = res.body.getReader();
|
|
1193
|
-
var decoder = new TextDecoder();
|
|
1194
|
-
var buffer = '';
|
|
1195
|
-
var removedDots = false;
|
|
1196
|
-
|
|
1197
|
-
// Track current AI text segment
|
|
1198
|
-
var currentTextEl = null;
|
|
1199
|
-
var currentText = '';
|
|
1200
|
-
var renderTimer = null;
|
|
1201
|
-
var thinkEl = null;
|
|
1202
|
-
var thinkTextEl = null;
|
|
1203
|
-
var inThink = false;
|
|
1204
|
-
var thinkContent = '';
|
|
1205
|
-
var doneThinkContent = '';
|
|
1206
|
-
var lastRenderTime = 0;
|
|
1207
|
-
|
|
1208
|
-
function ensureTextEl() {
|
|
1209
|
-
if (!currentTextEl) {
|
|
1210
|
-
currentTextEl = document.createElement('div');
|
|
1211
|
-
currentTextEl.className = 'msg ai';
|
|
1212
|
-
container.appendChild(currentTextEl);
|
|
1213
|
-
currentText = '';
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
1011
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1012
|
+
async function duplicateItem(path) {
|
|
1013
|
+
var r = await fetch('/api/fs/duplicate', {
|
|
1014
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
1015
|
+
body: JSON.stringify({ from: path })
|
|
1016
|
+
});
|
|
1017
|
+
var d = await r.json();
|
|
1018
|
+
if (d.error) { showToast('Duplicate failed: ' + d.error, 'err'); return; }
|
|
1019
|
+
showToast('Duplicated to ' + d.path);
|
|
1020
|
+
loadTree();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function deleteItem(path, hard) {
|
|
1024
|
+
var v = await showModal({
|
|
1025
|
+
title: 'Delete?',
|
|
1026
|
+
label: path,
|
|
1027
|
+
input: false,
|
|
1028
|
+
hint: hard ? 'This permanently removes the item. This cannot be undone.'
|
|
1029
|
+
: 'Item will be moved to .sapper/.trash/ so you can restore it manually.',
|
|
1030
|
+
okLabel: 'Delete',
|
|
1031
|
+
danger: true,
|
|
1032
|
+
});
|
|
1033
|
+
if (v == null) return;
|
|
1034
|
+
var r = await fetch('/api/fs/delete', {
|
|
1035
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
1036
|
+
body: JSON.stringify({ path: path, hard: !!hard })
|
|
1037
|
+
});
|
|
1038
|
+
var d = await r.json();
|
|
1039
|
+
if (d.error) { showToast('Delete failed: ' + d.error, 'err'); return; }
|
|
1040
|
+
showToast('Deleted');
|
|
1041
|
+
if (state.currentFile === path) closePreview();
|
|
1042
|
+
loadTree();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ─── Preview / editor ────────────────────────────────────────
|
|
1046
|
+
function isTextLikeExt(ext) {
|
|
1047
|
+
return /^(md|markdown|txt|json|jsonc|yml|yaml|toml|js|mjs|cjs|ts|tsx|jsx|css|scss|html|htm|xml|py|rb|go|rs|java|c|cpp|h|hpp|sh|bash|zsh|fish|env|gitignore|conf|ini|sql|graphql|svelte|vue|astro|lock|log)$/i.test(ext);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
window.openFile = function(path, isReload) {
|
|
1051
|
+
// Ensure preview is open
|
|
1052
|
+
var prev = document.getElementById('preview');
|
|
1053
|
+
if (prev.classList.contains('hidden')) togglePreview();
|
|
1054
|
+
// Mark active row
|
|
1055
|
+
document.querySelectorAll('.row.active').forEach(function(r){ r.classList.remove('active'); });
|
|
1056
|
+
var row = document.querySelector('.row[data-path="' + cssEscape(path) + '"]');
|
|
1057
|
+
if (row) row.classList.add('active');
|
|
1058
|
+
|
|
1059
|
+
state.currentFile = path;
|
|
1060
|
+
state.editing = false;
|
|
1061
|
+
state.showSource = false;
|
|
1062
|
+
document.getElementById('pPath').textContent = path;
|
|
1063
|
+
document.getElementById('pInd').classList.remove('show');
|
|
1064
|
+
document.getElementById('pEdit').style.display = 'none';
|
|
1065
|
+
document.getElementById('pSave').style.display = 'none';
|
|
1066
|
+
document.getElementById('pCancel').style.display = 'none';
|
|
1067
|
+
document.getElementById('pSrc').style.display = 'none';
|
|
1068
|
+
document.getElementById('pReload').style.display = 'inline-block';
|
|
1069
|
+
document.getElementById('pedit').classList.remove('show');
|
|
1070
|
+
document.getElementById('pview').classList.remove('hide');
|
|
1071
|
+
|
|
1072
|
+
fetch('/api/file?path=' + encodeURIComponent(path)).then(function(r){return r.json();}).then(function(d){
|
|
1073
|
+
if (d.error) {
|
|
1074
|
+
document.getElementById('pview').innerHTML = '<div id="empty"><div class="lg">⚠</div>' + esc(d.error) + '</div>';
|
|
1075
|
+
document.getElementById('pview').className = '';
|
|
1076
|
+
return;
|
|
1222
1077
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1078
|
+
state.fileOnDisk = d.content || '';
|
|
1079
|
+
var ext = (path.split('.').pop() || '').toLowerCase();
|
|
1080
|
+
var view = document.getElementById('pview');
|
|
1081
|
+
if (d.binary) {
|
|
1082
|
+
view.className = '';
|
|
1083
|
+
if (/^(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(ext)) {
|
|
1084
|
+
view.innerHTML = '<img src="/api/file/raw?path=' + encodeURIComponent(path) + '" alt="' + esc(path) + '">';
|
|
1085
|
+
} else {
|
|
1086
|
+
view.innerHTML = '<div id="empty"><div class="lg">💾</div>Binary file (' + d.size + ' bytes)</div>';
|
|
1087
|
+
}
|
|
1088
|
+
document.getElementById('pEdit').style.display = 'none';
|
|
1089
|
+
} else if (ext === 'md' || ext === 'markdown') {
|
|
1090
|
+
view.className = '';
|
|
1091
|
+
try {
|
|
1092
|
+
marked.setOptions({ breaks: false, gfm: true });
|
|
1093
|
+
view.innerHTML = marked.parse(d.content || '');
|
|
1094
|
+
view.querySelectorAll('pre code').forEach(function(b){ try { hljs.highlightElement(b); } catch(e){} });
|
|
1095
|
+
} catch(e) {
|
|
1096
|
+
view.innerHTML = '<pre>' + esc(d.content || '') + '</pre>';
|
|
1230
1097
|
}
|
|
1098
|
+
document.getElementById('pEdit').style.display = 'inline-block';
|
|
1099
|
+
} else if (ext === 'html' || ext === 'htm') {
|
|
1100
|
+
renderHtmlPreview(d.content || '');
|
|
1101
|
+
document.getElementById('pEdit').style.display = 'inline-block';
|
|
1102
|
+
document.getElementById('pSrc').style.display = 'inline-block';
|
|
1103
|
+
document.getElementById('pSrc').textContent = 'Source';
|
|
1104
|
+
} else if (isTextLikeExt(ext) || d.text) {
|
|
1105
|
+
view.className = 'code';
|
|
1106
|
+
var langClass = ext ? ' class="language-' + esc(ext) + '"' : '';
|
|
1107
|
+
view.innerHTML = '<pre><code' + langClass + '>' + esc(d.content || '') + '</code></pre>';
|
|
1108
|
+
try { hljs.highlightElement(view.querySelector('code')); } catch(e){}
|
|
1109
|
+
document.getElementById('pEdit').style.display = 'inline-block';
|
|
1110
|
+
} else {
|
|
1111
|
+
view.className = '';
|
|
1112
|
+
view.innerHTML = '<pre>' + esc(d.content || '') + '</pre>';
|
|
1113
|
+
document.getElementById('pEdit').style.display = 'inline-block';
|
|
1231
1114
|
}
|
|
1115
|
+
if (isReload) showToast('Reloaded ' + path);
|
|
1116
|
+
}).catch(function(e){ showToast('Read failed: ' + e.message, 'err'); });
|
|
1117
|
+
};
|
|
1232
1118
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1119
|
+
function renderHtmlPreview(content) {
|
|
1120
|
+
var view = document.getElementById('pview');
|
|
1121
|
+
view.className = '';
|
|
1122
|
+
view.innerHTML = '';
|
|
1123
|
+
var iframe = document.createElement('iframe');
|
|
1124
|
+
iframe.className = 'html-preview';
|
|
1125
|
+
iframe.setAttribute('sandbox', 'allow-same-origin allow-popups');
|
|
1126
|
+
iframe.srcdoc = content;
|
|
1127
|
+
view.appendChild(iframe);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function renderHtmlSource(content) {
|
|
1131
|
+
var view = document.getElementById('pview');
|
|
1132
|
+
view.className = 'code';
|
|
1133
|
+
view.innerHTML = '<pre><code class="language-html">' + esc(content) + '</code></pre>';
|
|
1134
|
+
try { hljs.highlightElement(view.querySelector('code')); } catch(e){}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
window.toggleSource = function() {
|
|
1138
|
+
if (!state.currentFile) return;
|
|
1139
|
+
state.showSource = !state.showSource;
|
|
1140
|
+
var btn = document.getElementById('pSrc');
|
|
1141
|
+
if (state.showSource) {
|
|
1142
|
+
renderHtmlSource(state.fileOnDisk || '');
|
|
1143
|
+
btn.textContent = 'Rendered';
|
|
1144
|
+
} else {
|
|
1145
|
+
renderHtmlPreview(state.fileOnDisk || '');
|
|
1146
|
+
btn.textContent = 'Source';
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1246
1149
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1150
|
+
window.reloadPreview = function() { if (state.currentFile) openFile(state.currentFile, true); };
|
|
1151
|
+
window.closePreview = function() {
|
|
1152
|
+
state.currentFile = null; state.editing = false;
|
|
1153
|
+
document.getElementById('pview').innerHTML = '<div id="empty"><div class="lg">📄</div>Open a file from the sidebar.</div>';
|
|
1154
|
+
document.getElementById('pview').className = '';
|
|
1155
|
+
document.getElementById('pPath').textContent = 'No file open';
|
|
1156
|
+
document.getElementById('pEdit').style.display = 'none';
|
|
1157
|
+
document.getElementById('pSave').style.display = 'none';
|
|
1158
|
+
document.getElementById('pCancel').style.display = 'none';
|
|
1159
|
+
document.getElementById('pSrc').style.display = 'none';
|
|
1160
|
+
document.getElementById('pReload').style.display = 'none';
|
|
1161
|
+
document.getElementById('pedit').classList.remove('show');
|
|
1162
|
+
document.getElementById('pview').classList.remove('hide');
|
|
1163
|
+
};
|
|
1164
|
+
window.startEdit = function() {
|
|
1165
|
+
if (!state.currentFile) return;
|
|
1166
|
+
state.editing = true;
|
|
1167
|
+
document.getElementById('pedit').value = state.fileOnDisk;
|
|
1168
|
+
document.getElementById('pedit').classList.add('show');
|
|
1169
|
+
document.getElementById('pview').classList.add('hide');
|
|
1170
|
+
document.getElementById('pEdit').style.display = 'none';
|
|
1171
|
+
document.getElementById('pSave').style.display = 'inline-block';
|
|
1172
|
+
document.getElementById('pCancel').style.display = 'inline-block';
|
|
1173
|
+
};
|
|
1174
|
+
window.cancelEdit = function() {
|
|
1175
|
+
state.editing = false;
|
|
1176
|
+
document.getElementById('pedit').classList.remove('show');
|
|
1177
|
+
document.getElementById('pview').classList.remove('hide');
|
|
1178
|
+
document.getElementById('pEdit').style.display = 'inline-block';
|
|
1179
|
+
document.getElementById('pSave').style.display = 'none';
|
|
1180
|
+
document.getElementById('pCancel').style.display = 'none';
|
|
1181
|
+
document.getElementById('pInd').classList.remove('show');
|
|
1182
|
+
};
|
|
1183
|
+
window.saveEdit = function() {
|
|
1184
|
+
if (!state.currentFile) return;
|
|
1185
|
+
var content = document.getElementById('pedit').value;
|
|
1186
|
+
fetch('/api/file', {
|
|
1187
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1188
|
+
body: JSON.stringify({ path: state.currentFile, content: content })
|
|
1189
|
+
}).then(function(r){return r.json();}).then(function(d){
|
|
1190
|
+
if (d.error) { showToast('Save failed: ' + d.error, 'err'); return; }
|
|
1191
|
+
showToast('Saved ' + state.currentFile);
|
|
1192
|
+
state.editing = false;
|
|
1193
|
+
openFile(state.currentFile, false);
|
|
1194
|
+
}).catch(function(e){ showToast('Save failed: ' + e.message, 'err'); });
|
|
1195
|
+
};
|
|
1250
1196
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1197
|
+
// ─── Config tab ──────────────────────────────────────────────
|
|
1198
|
+
window.reloadConfig = function() {
|
|
1199
|
+
fetch('/api/config').then(function(r){return r.json();}).then(function(d){
|
|
1200
|
+
document.getElementById('cfgJson').value = JSON.stringify(d.config || {}, null, 2);
|
|
1201
|
+
renderQuickConfig(d.config || {});
|
|
1202
|
+
}).catch(function(e){ showToast('Config read failed: ' + e.message, 'err'); });
|
|
1203
|
+
};
|
|
1204
|
+
window.saveConfig = function() {
|
|
1205
|
+
var raw = document.getElementById('cfgJson').value;
|
|
1206
|
+
var parsed;
|
|
1207
|
+
try { parsed = JSON.parse(raw); }
|
|
1208
|
+
catch(e) { showToast('Invalid JSON: ' + e.message, 'err'); return; }
|
|
1209
|
+
fetch('/api/config', {
|
|
1210
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
1211
|
+
body: JSON.stringify({ config: parsed })
|
|
1212
|
+
}).then(function(r){return r.json();}).then(function(d){
|
|
1213
|
+
if (d.error) { showToast('Save failed: ' + d.error, 'err'); return; }
|
|
1214
|
+
showToast('Config saved');
|
|
1215
|
+
renderQuickConfig(parsed);
|
|
1216
|
+
});
|
|
1217
|
+
};
|
|
1261
1218
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
var before = remaining.substring(0, openIdx);
|
|
1302
|
-
if (before.length > 0) {
|
|
1303
|
-
ensureTextEl();
|
|
1304
|
-
currentText += before;
|
|
1305
|
-
scheduleRender();
|
|
1306
|
-
}
|
|
1307
|
-
// Save what we have so far as done text
|
|
1308
|
-
if (currentText.length > 0) {
|
|
1309
|
-
renderCurrentText();
|
|
1310
|
-
doneThinkContent += currentText;
|
|
1311
|
-
currentText = '';
|
|
1312
|
-
currentTextEl = null;
|
|
1313
|
-
}
|
|
1314
|
-
startThinkBlock();
|
|
1315
|
-
remaining = remaining.substring(openIdx + 7);
|
|
1316
|
-
} else {
|
|
1317
|
-
ensureTextEl();
|
|
1318
|
-
currentText += remaining;
|
|
1319
|
-
scheduleRender();
|
|
1320
|
-
remaining = '';
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1219
|
+
function renderQuickConfig(cfg) {
|
|
1220
|
+
var host = document.getElementById('cfgQuickBody');
|
|
1221
|
+
host.innerHTML = '';
|
|
1222
|
+
function add(html) { host.insertAdjacentHTML('beforeend', html); }
|
|
1223
|
+
add('<label>Default model</label><input type="text" id="qDefMod" placeholder="auto" value="' + esc(cfg.defaultModel || '') + '">');
|
|
1224
|
+
add('<label>Default agent</label><input type="text" id="qDefAgent" placeholder="(none)" value="' + esc(cfg.defaultAgent || '') + '">');
|
|
1225
|
+
add('<label>Context limit (tokens, blank = model default)</label><input type="number" id="qCtxLim" value="' + esc(cfg.contextLimit == null ? '' : cfg.contextLimit) + '">');
|
|
1226
|
+
add('<label>Tool round limit</label><input type="number" id="qToolRnd" value="' + esc(cfg.toolRoundLimit != null ? cfg.toolRoundLimit : 40) + '">');
|
|
1227
|
+
add('<div class="toggle-row"><span>Summary phases</span><div class="switch ' + (cfg.summaryPhases ? 'on' : '') + '" id="qSumPh"></div></div>');
|
|
1228
|
+
add('<label>Summary trigger %</label><input type="number" id="qSumTr" value="' + esc(cfg.summarizeTriggerPercent != null ? cfg.summarizeTriggerPercent : 65) + '">');
|
|
1229
|
+
add('<div class="toggle-row"><span>Debug mode</span><div class="switch ' + (cfg.debug ? 'on' : '') + '" id="qDebug"></div></div>');
|
|
1230
|
+
add('<div class="toggle-row"><span>Auto-attach files</span><div class="switch ' + (cfg.autoAttach !== false ? 'on' : '') + '" id="qAutoAtt"></div></div>');
|
|
1231
|
+
add('<div class="row-btns"><button class="primary" onclick="saveQuickConfig()">Apply quick changes</button></div>');
|
|
1232
|
+
|
|
1233
|
+
function bindSwitch(id) {
|
|
1234
|
+
var el = document.getElementById(id);
|
|
1235
|
+
if (el) el.addEventListener('click', function(){ el.classList.toggle('on'); });
|
|
1236
|
+
}
|
|
1237
|
+
bindSwitch('qSumPh'); bindSwitch('qDebug'); bindSwitch('qAutoAtt');
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
window.saveQuickConfig = function() {
|
|
1241
|
+
var current;
|
|
1242
|
+
try { current = JSON.parse(document.getElementById('cfgJson').value || '{}'); }
|
|
1243
|
+
catch(e) { current = {}; }
|
|
1244
|
+
function val(id) { var el = document.getElementById(id); return el ? el.value : ''; }
|
|
1245
|
+
function on(id) { var el = document.getElementById(id); return el && el.classList.contains('on'); }
|
|
1246
|
+
var v;
|
|
1247
|
+
v = val('qDefMod').trim(); current.defaultModel = v || null;
|
|
1248
|
+
v = val('qDefAgent').trim(); current.defaultAgent = v || null;
|
|
1249
|
+
v = val('qCtxLim').trim(); current.contextLimit = v === '' ? null : parseInt(v, 10);
|
|
1250
|
+
v = val('qToolRnd').trim(); current.toolRoundLimit = v === '' ? 40 : parseInt(v, 10);
|
|
1251
|
+
v = val('qSumTr').trim(); current.summarizeTriggerPercent = v === '' ? 65 : parseInt(v, 10);
|
|
1252
|
+
current.summaryPhases = on('qSumPh');
|
|
1253
|
+
current.debug = on('qDebug');
|
|
1254
|
+
current.autoAttach = on('qAutoAtt');
|
|
1255
|
+
document.getElementById('cfgJson').value = JSON.stringify(current, null, 2);
|
|
1256
|
+
saveConfig();
|
|
1257
|
+
};
|
|
1325
1258
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
if (line.indexOf('data: ') !== 0) continue;
|
|
1335
|
-
var raw = line.slice(6);
|
|
1336
|
-
if (raw === '[DONE]') continue;
|
|
1337
|
-
var evt;
|
|
1338
|
-
try { evt = JSON.parse(raw); } catch(e) { continue; }
|
|
1339
|
-
if (!removedDots) { dots.remove(); removedDots = true; }
|
|
1340
|
-
if (evt.type === 'token') {
|
|
1341
|
-
processToken(evt.data);
|
|
1342
|
-
scrollDown();
|
|
1343
|
-
} else if (evt.type === 'tool_start') {
|
|
1344
|
-
// Finish current text segment before showing tool
|
|
1345
|
-
if (currentText) { renderCurrentText(); doneThinkContent += currentText; currentText = ''; }
|
|
1346
|
-
currentTextEl = null;
|
|
1347
|
-
if (inThink) endThinkBlock();
|
|
1348
|
-
|
|
1349
|
-
var card = document.createElement('div');
|
|
1350
|
-
card.className = 'tool-card';
|
|
1351
|
-
card.setAttribute('data-tool', evt.data.tool);
|
|
1352
|
-
card.setAttribute('data-path', evt.data.path);
|
|
1353
|
-
card.innerHTML = '<div class="tool-card-header" onclick="this.parentElement.classList.toggle('expanded')">' +
|
|
1354
|
-
'<span class="tc-icon">⚙</span>' +
|
|
1355
|
-
'<span class="tc-name">' + esc(evt.data.tool) + '</span>' +
|
|
1356
|
-
'<span class="tc-path">' + esc(evt.data.path) + '</span>' +
|
|
1357
|
-
'<span class="tc-status tc-spinner">⌛</span>' +
|
|
1358
|
-
'<span class="tc-chevron">▶</span></div>' +
|
|
1359
|
-
'<div class="tool-card-body"><pre>Running...</pre></div>';
|
|
1360
|
-
container.appendChild(card);
|
|
1361
|
-
scrollDown();
|
|
1362
|
-
} else if (evt.type === 'tool_result') {
|
|
1363
|
-
// Try to update the last pending tool card
|
|
1364
|
-
var pendingCards = container.querySelectorAll('.tool-card:not(.done)');
|
|
1365
|
-
var isErr = (evt.data.result && evt.data.result.indexOf('Error') !== -1) || evt.data.blocked;
|
|
1366
|
-
if (pendingCards.length > 0) {
|
|
1367
|
-
var lastCard = pendingCards[pendingCards.length - 1];
|
|
1368
|
-
lastCard.classList.add('done');
|
|
1369
|
-
if (isErr) lastCard.classList.add('error');
|
|
1370
|
-
var hdr = lastCard.querySelector('.tool-card-header');
|
|
1371
|
-
var statusSpan = lastCard.querySelector('.tc-status');
|
|
1372
|
-
if (statusSpan) statusSpan.innerHTML = isErr ? '❌' : '✅';
|
|
1373
|
-
var body = lastCard.querySelector('.tool-card-body pre');
|
|
1374
|
-
if (body) body.textContent = evt.data.result || '(no output)';
|
|
1375
|
-
} else {
|
|
1376
|
-
// Fallback: create result card
|
|
1377
|
-
var card = document.createElement('div');
|
|
1378
|
-
card.className = 'tool-card done' + (isErr ? ' error' : '');
|
|
1379
|
-
card.innerHTML = '<div class="tool-card-header" onclick="this.parentElement.classList.toggle('expanded')">' +
|
|
1380
|
-
'<span class="tc-icon">' + (isErr ? '❌' : '✅') + '</span>' +
|
|
1381
|
-
'<span class="tc-name">' + esc(evt.data.tool) + '</span>' +
|
|
1382
|
-
'<span class="tc-path">' + esc(evt.data.path) + '</span>' +
|
|
1383
|
-
'<span class="tc-chevron">▶</span></div>' +
|
|
1384
|
-
'<div class="tool-card-body"><pre>' + esc(evt.data.result || '') + '</pre></div>';
|
|
1385
|
-
container.appendChild(card);
|
|
1386
|
-
}
|
|
1387
|
-
scrollDown();
|
|
1388
|
-
} else if (evt.type === 'system') {
|
|
1389
|
-
addSystem(evt.data);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
processStream();
|
|
1393
|
-
}).catch(function(e) {
|
|
1394
|
-
if (!removedDots) dots.remove();
|
|
1395
|
-
ensureTextEl();
|
|
1396
|
-
currentTextEl.innerHTML = '<span style="color:var(--red)">Stream error: ' + esc(e.message) + '</span>';
|
|
1397
|
-
finishStream();
|
|
1398
|
-
});
|
|
1259
|
+
// ─── Agents & Skills tabs ────────────────────────────────────
|
|
1260
|
+
function loadAgents() {
|
|
1261
|
+
fetch('/api/agents').then(function(r){return r.json();}).then(function(d){
|
|
1262
|
+
var host = document.getElementById('agentsList');
|
|
1263
|
+
host.innerHTML = '';
|
|
1264
|
+
if (!d.agents || d.agents.length === 0) {
|
|
1265
|
+
host.innerHTML = '<div class="pane-section"><p>No agents yet. Create one with <code>/newagent</code>.</p></div>';
|
|
1266
|
+
return;
|
|
1399
1267
|
}
|
|
1268
|
+
d.agents.forEach(function(a){
|
|
1269
|
+
var div = document.createElement('div');
|
|
1270
|
+
div.className = 'item';
|
|
1271
|
+
div.innerHTML = '<div class="ti">' + esc(a.name) + '</div>' +
|
|
1272
|
+
(a.description ? '<div class="ds">' + esc(a.description) + '</div>' : '');
|
|
1273
|
+
div.addEventListener('click', function(){
|
|
1274
|
+
openFile(a.path);
|
|
1275
|
+
sendCmd('/' + a.key);
|
|
1276
|
+
});
|
|
1277
|
+
host.appendChild(div);
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1400
1281
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
// If nothing was rendered at all
|
|
1409
|
-
if (!container.querySelector('.msg.ai') && !container.querySelector('.tool-card') && !container.querySelector('.think-block')) {
|
|
1410
|
-
var empty = document.createElement('div');
|
|
1411
|
-
empty.className = 'msg ai';
|
|
1412
|
-
empty.innerHTML = '<em style="color:var(--fg3)">No response</em>';
|
|
1413
|
-
container.appendChild(empty);
|
|
1414
|
-
}
|
|
1415
|
-
isStreaming = false;
|
|
1416
|
-
document.getElementById('sendBtn').disabled = false;
|
|
1417
|
-
document.getElementById('stopBtn').classList.remove('visible');
|
|
1418
|
-
document.getElementById('statusDot').style.background = 'var(--green)';
|
|
1419
|
-
scrollDown();
|
|
1420
|
-
inputEl.focus();
|
|
1421
|
-
autoSaveSession();
|
|
1282
|
+
function loadSkills() {
|
|
1283
|
+
fetch('/api/skills').then(function(r){return r.json();}).then(function(d){
|
|
1284
|
+
var host = document.getElementById('skillsList');
|
|
1285
|
+
host.innerHTML = '';
|
|
1286
|
+
if (!d.skills || d.skills.length === 0) {
|
|
1287
|
+
host.innerHTML = '<div class="pane-section"><p>No skills yet. Create one with <code>/newskill</code>.</p></div>';
|
|
1288
|
+
return;
|
|
1422
1289
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
isStreaming = false;
|
|
1432
|
-
document.getElementById('sendBtn').disabled = false;
|
|
1433
|
-
document.getElementById('stopBtn').classList.remove('visible');
|
|
1434
|
-
document.getElementById('statusDot').style.background = 'var(--green)';
|
|
1290
|
+
d.skills.forEach(function(s){
|
|
1291
|
+
var div = document.createElement('div');
|
|
1292
|
+
div.className = 'item';
|
|
1293
|
+
div.innerHTML = '<div class="ti">' + esc(s.name) + '</div>' +
|
|
1294
|
+
(s.description ? '<div class="ds">' + esc(s.description) + '</div>' : '');
|
|
1295
|
+
div.addEventListener('click', function(){ openFile(s.path); });
|
|
1296
|
+
host.appendChild(div);
|
|
1297
|
+
});
|
|
1435
1298
|
});
|
|
1436
1299
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1300
|
+
|
|
1301
|
+
// ─── Boot ────────────────────────────────────────────────────
|
|
1302
|
+
connectPty();
|
|
1303
|
+
connectEvents();
|
|
1304
|
+
loadTree();
|
|
1305
|
+
</script>
|
|
1306
|
+
</body>
|
|
1307
|
+
</html>`;
|
|
1440
1308
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
chatEl.appendChild(el);
|
|
1448
|
-
scrollDown();
|
|
1449
|
-
return el;
|
|
1309
|
+
|
|
1310
|
+
// ─── HTTP routes ─────────────────────────────────────────────────
|
|
1311
|
+
|
|
1312
|
+
function json(res, data, code = 200) {
|
|
1313
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
1314
|
+
res.end(JSON.stringify(data));
|
|
1450
1315
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1316
|
+
|
|
1317
|
+
function readReqJSON(req) {
|
|
1318
|
+
return new Promise((resolve) => {
|
|
1319
|
+
let body = ''; let size = 0;
|
|
1320
|
+
req.on('data', (c) => { size += c.length; if (size > 5 * 1024 * 1024) { req.destroy(); resolve({ _err: 'too large' }); return; } body += c; });
|
|
1321
|
+
req.on('end', () => { try { resolve(JSON.parse(body || '{}')); } catch { resolve({}); } });
|
|
1322
|
+
req.on('error', () => resolve({}));
|
|
1323
|
+
});
|
|
1457
1324
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
if (end === -1) break;
|
|
1468
|
-
result = result.substring(0, start) + result.substring(end + 7);
|
|
1469
|
-
}
|
|
1470
|
-
return result.trim();
|
|
1325
|
+
|
|
1326
|
+
function listEntries(dirPath) {
|
|
1327
|
+
try {
|
|
1328
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
1329
|
+
return entries
|
|
1330
|
+
.filter(e => !IGNORE_NAMES.has(e.name))
|
|
1331
|
+
.map(e => ({ name: e.name, isDir: e.isDirectory() }))
|
|
1332
|
+
.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : (a.isDir ? -1 : 1)));
|
|
1333
|
+
} catch { return []; }
|
|
1471
1334
|
}
|
|
1472
1335
|
|
|
1473
|
-
function
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (
|
|
1480
|
-
var cls = p.done ? 'think-block' : 'think-block open streaming';
|
|
1481
|
-
html += '<div class="' + cls + '">';
|
|
1482
|
-
html += '<div class="think-header" onclick="this.parentElement.classList.toggle('open')">';
|
|
1483
|
-
html += '<span class="think-chevron">▶</span>';
|
|
1484
|
-
html += '<span class="think-label">🧠 Thinking' + (p.done ? '' : '...') + '</span></div>';
|
|
1485
|
-
html += '<div class="think-content">' + renderMarkdown(p.content) + '</div></div>';
|
|
1486
|
-
} else {
|
|
1487
|
-
html += renderMarkdown(p.content);
|
|
1488
|
-
}
|
|
1336
|
+
function looksBinary(buf) {
|
|
1337
|
+
const len = Math.min(buf.length, 4096);
|
|
1338
|
+
let nonText = 0;
|
|
1339
|
+
for (let i = 0; i < len; i++) {
|
|
1340
|
+
const c = buf[i];
|
|
1341
|
+
if (c === 0) return true;
|
|
1342
|
+
if ((c < 32 && c !== 9 && c !== 10 && c !== 13) || c >= 127) nonText++;
|
|
1489
1343
|
}
|
|
1490
|
-
return
|
|
1344
|
+
return nonText / Math.max(len, 1) > 0.3;
|
|
1491
1345
|
}
|
|
1492
1346
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1347
|
+
const server = http.createServer(async (req, res) => {
|
|
1348
|
+
try {
|
|
1349
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1350
|
+
const path = url.pathname;
|
|
1351
|
+
|
|
1352
|
+
// Pages
|
|
1353
|
+
if (req.method === 'GET' && (path === '/' || path === '/index.html')) {
|
|
1354
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1355
|
+
res.end(buildHTML());
|
|
1356
|
+
return;
|
|
1501
1357
|
}
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1358
|
+
if (req.method === 'GET' && path === '/health') return json(res, { ok: true, cwd: workingDir });
|
|
1359
|
+
|
|
1360
|
+
// ── Tree
|
|
1361
|
+
if (req.method === 'GET' && path === '/api/tree') {
|
|
1362
|
+
const rel = url.searchParams.get('path') || '';
|
|
1363
|
+
const abs = safePath(rel);
|
|
1364
|
+
if (!abs) return json(res, { error: 'invalid path' }, 400);
|
|
1365
|
+
return json(res, { path: rel, entries: listEntries(abs) });
|
|
1507
1366
|
}
|
|
1508
|
-
parts.push({type: 'thinking', content: remaining.substring(s + 7, e), done: true});
|
|
1509
|
-
remaining = remaining.substring(e + 8);
|
|
1510
|
-
}
|
|
1511
|
-
return parts;
|
|
1512
|
-
}
|
|
1513
1367
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (trimmed.indexOf(BT+BT+BT) === 0) {
|
|
1528
|
-
if (!inCode) {
|
|
1529
|
-
if (inList) { html += '</ul>'; inList = false; }
|
|
1530
|
-
inCode = true;
|
|
1531
|
-
codeLang = trimmed.slice(3).trim();
|
|
1532
|
-
codeLines = [];
|
|
1533
|
-
} else {
|
|
1534
|
-
html += '<pre><code class="lang-' + esc(codeLang) + '">' + esc(codeLines.join(NL)) + '</code></pre>';
|
|
1535
|
-
inCode = false;
|
|
1536
|
-
}
|
|
1537
|
-
continue;
|
|
1368
|
+
// ── File read (text)
|
|
1369
|
+
if (req.method === 'GET' && path === '/api/file') {
|
|
1370
|
+
const rel = url.searchParams.get('path') || '';
|
|
1371
|
+
const abs = safePath(rel);
|
|
1372
|
+
if (!abs) return json(res, { error: 'invalid path' }, 400);
|
|
1373
|
+
try {
|
|
1374
|
+
const stat = fs.statSync(abs);
|
|
1375
|
+
if (stat.isDirectory()) return json(res, { error: 'is a directory' }, 400);
|
|
1376
|
+
if (stat.size > 2 * 1024 * 1024) return json(res, { error: 'file too large (>2MB)', size: stat.size, binary: true }, 200);
|
|
1377
|
+
const buf = fs.readFileSync(abs);
|
|
1378
|
+
if (looksBinary(buf)) return json(res, { binary: true, size: stat.size });
|
|
1379
|
+
return json(res, { content: buf.toString('utf8'), size: stat.size });
|
|
1380
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1538
1381
|
}
|
|
1539
|
-
if (inCode) { codeLines.push(line); continue; }
|
|
1540
1382
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1383
|
+
// ── File raw (images)
|
|
1384
|
+
if (req.method === 'GET' && path === '/api/file/raw') {
|
|
1385
|
+
const rel = url.searchParams.get('path') || '';
|
|
1386
|
+
const abs = safePath(rel);
|
|
1387
|
+
if (!abs) { res.writeHead(400); return res.end('invalid path'); }
|
|
1388
|
+
try {
|
|
1389
|
+
const ext = (rel.split('.').pop() || '').toLowerCase();
|
|
1390
|
+
const mime = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', bmp: 'image/bmp', ico: 'image/x-icon' }[ext] || 'application/octet-stream';
|
|
1391
|
+
const buf = fs.readFileSync(abs);
|
|
1392
|
+
res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'no-store' });
|
|
1393
|
+
return res.end(buf);
|
|
1394
|
+
} catch (e) { res.writeHead(500); return res.end(e.message); }
|
|
1545
1395
|
}
|
|
1546
1396
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
if (trimmed.indexOf('> ') === 0) { html += '<blockquote>' + inlineFmt(esc(trimmed.slice(2))) + '</blockquote>'; continue; }
|
|
1558
|
-
if (trimmed === '---' || trimmed === '***' || trimmed === '___') { html += '<hr>'; continue; }
|
|
1559
|
-
|
|
1560
|
-
if (isLi) {
|
|
1561
|
-
if (!inList) { html += '<ul>'; inList = true; }
|
|
1562
|
-
html += '<li>' + inlineFmt(esc(trimmed.slice(2))) + '</li>';
|
|
1563
|
-
continue;
|
|
1397
|
+
// ── File write
|
|
1398
|
+
if (req.method === 'POST' && path === '/api/file') {
|
|
1399
|
+
const body = await readReqJSON(req);
|
|
1400
|
+
const abs = safePath(body.path);
|
|
1401
|
+
if (!abs) return json(res, { error: 'invalid path' }, 400);
|
|
1402
|
+
try {
|
|
1403
|
+
ensureDir(dirname(abs));
|
|
1404
|
+
fs.writeFileSync(abs, body.content == null ? '' : String(body.content));
|
|
1405
|
+
return json(res, { ok: true });
|
|
1406
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1564
1407
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1408
|
+
|
|
1409
|
+
// ── Create file or folder
|
|
1410
|
+
if (req.method === 'POST' && path === '/api/fs/new') {
|
|
1411
|
+
const body = await readReqJSON(req);
|
|
1412
|
+
const abs = safePath(body.path);
|
|
1413
|
+
if (!abs) return json(res, { error: 'invalid path' }, 400);
|
|
1414
|
+
try {
|
|
1415
|
+
if (fs.existsSync(abs)) return json(res, { error: 'already exists' }, 409);
|
|
1416
|
+
if (body.kind === 'folder') {
|
|
1417
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
1418
|
+
} else {
|
|
1419
|
+
ensureDir(dirname(abs));
|
|
1420
|
+
fs.writeFileSync(abs, body.content == null ? '' : String(body.content));
|
|
1421
|
+
}
|
|
1422
|
+
return json(res, { ok: true, path: body.path });
|
|
1423
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1570
1424
|
}
|
|
1571
1425
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
}
|
|
1426
|
+
// ── Rename / move
|
|
1427
|
+
if (req.method === 'POST' && path === '/api/fs/rename') {
|
|
1428
|
+
const body = await readReqJSON(req);
|
|
1429
|
+
const from = safePath(body.from);
|
|
1430
|
+
const to = safePath(body.to);
|
|
1431
|
+
if (!from || !to) return json(res, { error: 'invalid path' }, 400);
|
|
1432
|
+
try {
|
|
1433
|
+
if (!fs.existsSync(from)) return json(res, { error: 'source not found' }, 404);
|
|
1434
|
+
if (fs.existsSync(to)) return json(res, { error: 'destination exists' }, 409);
|
|
1435
|
+
ensureDir(dirname(to));
|
|
1436
|
+
fs.renameSync(from, to);
|
|
1437
|
+
return json(res, { ok: true });
|
|
1438
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1439
|
+
}
|
|
1578
1440
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1441
|
+
// ── Duplicate
|
|
1442
|
+
if (req.method === 'POST' && path === '/api/fs/duplicate') {
|
|
1443
|
+
const body = await readReqJSON(req);
|
|
1444
|
+
const from = safePath(body.from);
|
|
1445
|
+
if (!from) return json(res, { error: 'invalid path' }, 400);
|
|
1446
|
+
try {
|
|
1447
|
+
if (!fs.existsSync(from)) return json(res, { error: 'source not found' }, 404);
|
|
1448
|
+
// build target name: foo.txt -> foo copy.txt, foo copy.txt -> foo copy 2.txt
|
|
1449
|
+
const dir = dirname(from);
|
|
1450
|
+
const base = from.slice(dir.length + 1);
|
|
1451
|
+
const dot = base.lastIndexOf('.');
|
|
1452
|
+
const stem = (dot > 0) ? base.slice(0, dot) : base;
|
|
1453
|
+
const ext = (dot > 0) ? base.slice(dot) : '';
|
|
1454
|
+
let target = '';
|
|
1455
|
+
for (let i = 0; i < 1000; i++) {
|
|
1456
|
+
const suffix = (i === 0) ? ' copy' : (' copy ' + (i + 1));
|
|
1457
|
+
const candidate = dir + '/' + stem + suffix + ext;
|
|
1458
|
+
if (!fs.existsSync(candidate)) { target = candidate; break; }
|
|
1459
|
+
}
|
|
1460
|
+
if (!target) return json(res, { error: 'too many copies' }, 500);
|
|
1461
|
+
fs.cpSync(from, target, { recursive: true });
|
|
1462
|
+
return json(res, { ok: true, path: relative(workingDir, target).split(sep).join('/') });
|
|
1463
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1589
1464
|
}
|
|
1590
|
-
}
|
|
1591
|
-
return result;
|
|
1592
|
-
}
|
|
1593
1465
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1466
|
+
// ── Delete (move to .sapper/.trash for safety)
|
|
1467
|
+
if (req.method === 'POST' && path === '/api/fs/delete') {
|
|
1468
|
+
const body = await readReqJSON(req);
|
|
1469
|
+
const abs = safePath(body.path);
|
|
1470
|
+
if (!abs) return json(res, { error: 'invalid path' }, 400);
|
|
1471
|
+
if (abs === workingDir) return json(res, { error: 'cannot delete workspace root' }, 400);
|
|
1472
|
+
try {
|
|
1473
|
+
if (!fs.existsSync(abs)) return json(res, { error: 'not found' }, 404);
|
|
1474
|
+
if (body.hard) {
|
|
1475
|
+
fs.rmSync(abs, { recursive: true, force: true });
|
|
1476
|
+
} else {
|
|
1477
|
+
const trashDir = join(SAPPER_DIR, '.trash');
|
|
1478
|
+
ensureDir(trashDir);
|
|
1479
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1480
|
+
const name = body.path.split('/').pop() || 'item';
|
|
1481
|
+
const target = join(trashDir, stamp + '__' + name);
|
|
1482
|
+
fs.renameSync(abs, target);
|
|
1483
|
+
}
|
|
1484
|
+
return json(res, { ok: true });
|
|
1485
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1486
|
+
}
|
|
1607
1487
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1488
|
+
// ── Reveal in OS file manager
|
|
1489
|
+
if (req.method === 'POST' && path === '/api/fs/reveal') {
|
|
1490
|
+
const body = await readReqJSON(req);
|
|
1491
|
+
const abs = safePath(body.path);
|
|
1492
|
+
if (!abs) return json(res, { error: 'invalid path' }, 400);
|
|
1493
|
+
try {
|
|
1494
|
+
if (process.platform === 'darwin') spawn('open', ['-R', abs]);
|
|
1495
|
+
else if (process.platform === 'win32') spawn('explorer', ['/select,' + abs]);
|
|
1496
|
+
else spawn('xdg-open', [dirname(abs)]);
|
|
1497
|
+
return json(res, { ok: true });
|
|
1498
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1499
|
+
}
|
|
1612
1500
|
|
|
1613
|
-
//
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if (rp.classList.contains('open')) { rp.classList.remove('open'); }
|
|
1617
|
-
else { rp.classList.add('open'); loadFileTree('.'); }
|
|
1618
|
-
}
|
|
1619
|
-
function closeFilePanel() { document.getElementById('rightPanel').classList.remove('open'); }
|
|
1620
|
-
|
|
1621
|
-
function loadFileTree(dirPath) {
|
|
1622
|
-
fetch('/api/tree?path=' + encodeURIComponent(dirPath)).then(function(r){return r.json();}).then(function(d) {
|
|
1623
|
-
var el = document.getElementById('fileTree');
|
|
1624
|
-
el.innerHTML = '';
|
|
1625
|
-
if (dirPath !== '.') {
|
|
1626
|
-
var up = document.createElement('div');
|
|
1627
|
-
up.className = 'ft-item';
|
|
1628
|
-
up.innerHTML = '<span class="ft-icon">⬅</span><span class="ft-name">..</span>';
|
|
1629
|
-
var parent = dirPath.split('/').slice(0, -1).join('/') || '.';
|
|
1630
|
-
up.onclick = function() { loadFileTree(parent); };
|
|
1631
|
-
el.appendChild(up);
|
|
1501
|
+
// ── Config read/write
|
|
1502
|
+
if (req.method === 'GET' && path === '/api/config') {
|
|
1503
|
+
return json(res, { config: readJSON(CONFIG_FILE, {}), path: relative(workingDir, CONFIG_FILE) });
|
|
1632
1504
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
var subPath = (dirPath === '.' ? '' : dirPath + '/') + entry.name;
|
|
1641
|
-
item.onclick = (function(p) { return function() { loadFileTree(p); }; })(subPath);
|
|
1642
|
-
} else {
|
|
1643
|
-
var filePath = (dirPath === '.' ? '' : dirPath + '/') + entry.name;
|
|
1644
|
-
item.onclick = (function(p) { return function() { viewFile(p); }; })(filePath);
|
|
1645
|
-
}
|
|
1646
|
-
el.appendChild(item);
|
|
1505
|
+
if (req.method === 'POST' && path === '/api/config') {
|
|
1506
|
+
const body = await readReqJSON(req);
|
|
1507
|
+
try {
|
|
1508
|
+
ensureDir(SAPPER_DIR);
|
|
1509
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(body.config || {}, null, 2));
|
|
1510
|
+
return json(res, { ok: true });
|
|
1511
|
+
} catch (e) { return json(res, { error: e.message }, 500); }
|
|
1647
1512
|
}
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
function viewFile(path) {
|
|
1652
|
-
editMode = false;
|
|
1653
|
-
editingFilePath = path;
|
|
1654
|
-
fetch('/api/file/read?path=' + encodeURIComponent(path)).then(function(r){return r.json();}).then(function(d) {
|
|
1655
|
-
var editor = document.getElementById('fileEditor');
|
|
1656
|
-
editor.classList.add('open');
|
|
1657
|
-
document.getElementById('fePath').textContent = path;
|
|
1658
|
-
var content = document.getElementById('feContent');
|
|
1659
|
-
content.innerHTML = '<pre>' + esc(d.content || '') + '</pre>';
|
|
1660
|
-
document.getElementById('feEditBtn').style.display = '';
|
|
1661
|
-
document.getElementById('feSaveBtn').style.display = 'none';
|
|
1662
|
-
document.getElementById('feCancelBtn').style.display = 'none';
|
|
1663
|
-
});
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
function startEditing() {
|
|
1667
|
-
if (!editingFilePath) return;
|
|
1668
|
-
editMode = true;
|
|
1669
|
-
var content = document.getElementById('feContent');
|
|
1670
|
-
var pre = content.querySelector('pre');
|
|
1671
|
-
var text = pre ? pre.textContent : '';
|
|
1672
|
-
content.innerHTML = '';
|
|
1673
|
-
var ta = document.createElement('textarea');
|
|
1674
|
-
ta.value = text;
|
|
1675
|
-
ta.id = 'feTextarea';
|
|
1676
|
-
content.appendChild(ta);
|
|
1677
|
-
document.getElementById('feEditBtn').style.display = 'none';
|
|
1678
|
-
document.getElementById('feSaveBtn').style.display = '';
|
|
1679
|
-
document.getElementById('feCancelBtn').style.display = '';
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function saveFileEdit() {
|
|
1683
|
-
var ta = document.getElementById('feTextarea');
|
|
1684
|
-
if (!ta || !editingFilePath) return;
|
|
1685
|
-
fetch('/api/file/write', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({path: editingFilePath, content: ta.value})})
|
|
1686
|
-
.then(function(r){return r.json();}).then(function(d) {
|
|
1687
|
-
if (d.ok) { viewFile(editingFilePath); addSystem('Saved ' + editingFilePath); }
|
|
1688
|
-
else addSystem('Error saving: ' + (d.error || 'unknown'));
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
1513
|
|
|
1692
|
-
|
|
1514
|
+
// ── Agents / Skills
|
|
1515
|
+
if (req.method === 'GET' && path === '/api/agents') return json(res, { agents: listMdDir(AGENTS_DIR) });
|
|
1516
|
+
if (req.method === 'GET' && path === '/api/skills') return json(res, { skills: listMdDir(SKILLS_DIR) });
|
|
1693
1517
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
sendQuick('List the contents of the directory: ' + v);
|
|
1699
|
-
});
|
|
1700
|
-
} else if (type === 'read') {
|
|
1701
|
-
showPromptModal('Read File', 'File path (e.g. src/index.js)', '', function(v) {
|
|
1702
|
-
if (v) sendQuick('Read and show me the file: ' + v);
|
|
1703
|
-
});
|
|
1704
|
-
} else if (type === 'write') {
|
|
1705
|
-
showPromptModal('Create File', 'File path to create', '', function(v) {
|
|
1706
|
-
if (v) sendQuick('Create a new file at ' + v + ' with appropriate starter content');
|
|
1707
|
-
});
|
|
1708
|
-
} else if (type === 'search') {
|
|
1709
|
-
showPromptModal('Search Files', 'Search pattern', '', function(v) {
|
|
1710
|
-
if (v) sendQuick('Search the codebase for: ' + v);
|
|
1711
|
-
});
|
|
1712
|
-
} else if (type === 'shell') {
|
|
1713
|
-
showPromptModal('Run Command', 'Shell command', '', function(v) {
|
|
1714
|
-
if (v) sendQuick('Run this command: ' + v);
|
|
1715
|
-
});
|
|
1716
|
-
} else if (type === 'mkdir') {
|
|
1717
|
-
showPromptModal('Create Directory', 'Directory path', '', function(v) {
|
|
1718
|
-
if (v) sendQuick('Create directory: ' + v);
|
|
1719
|
-
});
|
|
1720
|
-
} else if (type === 'review') {
|
|
1721
|
-
sendQuick('Review the codebase for bugs, issues, and improvements');
|
|
1722
|
-
} else if (type === 'scan') {
|
|
1723
|
-
sendQuick('List the project files and give me a complete overview of the project structure and purpose');
|
|
1518
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1519
|
+
res.end('Not found');
|
|
1520
|
+
} catch (e) {
|
|
1521
|
+
json(res, { error: e.message }, 500);
|
|
1724
1522
|
}
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
// ─── Modal ─────────────────────────────────────────────────
|
|
1728
|
-
function showModal(title, bodyHtml, footerHtml) {
|
|
1729
|
-
document.getElementById('modalTitle').textContent = title;
|
|
1730
|
-
document.getElementById('modalBody').innerHTML = bodyHtml;
|
|
1731
|
-
document.getElementById('modalFooter').innerHTML = footerHtml;
|
|
1732
|
-
document.getElementById('modalOverlay').classList.add('visible');
|
|
1733
|
-
}
|
|
1734
|
-
function closeModal() {
|
|
1735
|
-
document.getElementById('modalOverlay').classList.remove('visible');
|
|
1736
|
-
}
|
|
1737
|
-
function showPromptModal(title, placeholder, defaultVal, callback) {
|
|
1738
|
-
var body = '<div class="m-field"><label>' + esc(placeholder) + '</label><input id="promptInput" value="' + esc(defaultVal || '') + '"></div>';
|
|
1739
|
-
showModal(title, body,
|
|
1740
|
-
'<button class="m-btn secondary" onclick="closeModal()">Cancel</button>' +
|
|
1741
|
-
'<button class="m-btn primary" id="promptOk">OK</button>');
|
|
1742
|
-
var inp = document.getElementById('promptInput');
|
|
1743
|
-
inp.focus();
|
|
1744
|
-
inp.select();
|
|
1745
|
-
document.getElementById('promptOk').onclick = function() { closeModal(); callback(inp.value); };
|
|
1746
|
-
inp.onkeydown = function(e) { if (e.key === 'Enter') { closeModal(); callback(inp.value); } };
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
// ─── Escape modal on Escape key ────────────────────────────
|
|
1750
|
-
document.addEventListener('keydown', function(e) {
|
|
1751
|
-
if (e.key === 'Escape') closeModal();
|
|
1752
1523
|
});
|
|
1753
1524
|
|
|
1754
|
-
|
|
1755
|
-
</script>
|
|
1756
|
-
</body>
|
|
1757
|
-
</html>`;
|
|
1758
|
-
}
|
|
1525
|
+
// ─── WebSockets: /ws (pty) and /events (fs watcher) ──────────────
|
|
1759
1526
|
|
|
1760
|
-
|
|
1527
|
+
const wssPty = new WebSocketServer({ noServer: true });
|
|
1528
|
+
const wssEvents = new WebSocketServer({ noServer: true });
|
|
1761
1529
|
|
|
1762
|
-
|
|
1763
|
-
req.
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
// ── API: models ──
|
|
1774
|
-
if (req.method === 'GET' && url.pathname === '/api/models') {
|
|
1775
|
-
try {
|
|
1776
|
-
const list = await ollama.list();
|
|
1777
|
-
const names = (list.models || []).map(m => m.name);
|
|
1778
|
-
if (names.length > 0 && !serverModel) serverModel = names[0];
|
|
1779
|
-
json(res, { models: names });
|
|
1780
|
-
} catch (e) { json(res, { models: [], error: e.message }, 500); }
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
// ── API: agents ──
|
|
1785
|
-
if (req.method === 'GET' && url.pathname === '/api/agents') {
|
|
1786
|
-
json(res, { agents: loadAgents() });
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// ── API: skills ──
|
|
1791
|
-
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
1792
|
-
json(res, { skills: loadSkills() });
|
|
1793
|
-
return;
|
|
1530
|
+
server.on('upgrade', (req, sock, head) => {
|
|
1531
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1532
|
+
if (url.pathname === '/ws') {
|
|
1533
|
+
wssPty.handleUpgrade(req, sock, head, (ws) => wssPty.emit('connection', ws, req));
|
|
1534
|
+
} else if (url.pathname === '/events') {
|
|
1535
|
+
wssEvents.handleUpgrade(req, sock, head, (ws) => wssEvents.emit('connection', ws, req));
|
|
1536
|
+
} else {
|
|
1537
|
+
sock.destroy();
|
|
1794
1538
|
}
|
|
1539
|
+
});
|
|
1795
1540
|
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1541
|
+
function spawnSapper(cols, rows) {
|
|
1542
|
+
return ptySpawn(process.execPath, [SAPPER_BIN], {
|
|
1543
|
+
name: 'xterm-256color',
|
|
1544
|
+
cols: cols || 100, rows: rows || 30,
|
|
1545
|
+
cwd: workingDir,
|
|
1546
|
+
env: { ...process.env, TERM: 'xterm-256color', FORCE_COLOR: '1', COLORTERM: 'truecolor' },
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1801
1549
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1550
|
+
wssPty.on('connection', (ws) => {
|
|
1551
|
+
dbg('pty client connected');
|
|
1552
|
+
let pty = null; let initialized = false;
|
|
1807
1553
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1554
|
+
function start(cols, rows) {
|
|
1555
|
+
if (pty) { try { pty.kill(); } catch {} }
|
|
1556
|
+
try { pty = spawnSapper(cols, rows); }
|
|
1557
|
+
catch (e) {
|
|
1558
|
+
console.error('[ui] spawn failed:', e.message);
|
|
1559
|
+
try { ws.send(Buffer.from('\x1b[31mFailed to spawn sapper: ' + e.message + '\x1b[0m\r\n', 'utf8')); } catch {}
|
|
1560
|
+
return;
|
|
1814
1561
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1562
|
+
dbg('pty pid=' + pty.pid + ' ' + cols + 'x' + rows);
|
|
1563
|
+
pty.onData((d) => { if (ws.readyState === ws.OPEN) ws.send(Buffer.from(d, 'utf8')); });
|
|
1564
|
+
pty.onExit(({ exitCode, signal }) => {
|
|
1565
|
+
dbg('pty exit code=' + exitCode);
|
|
1566
|
+
if (ws.readyState === ws.OPEN) { try { ws.send(JSON.stringify({ type: 'exit', code: exitCode, signal })); } catch {} }
|
|
1567
|
+
});
|
|
1568
|
+
try { ws.send(JSON.stringify({ type: 'cwd', path: workingDir })); } catch {}
|
|
1817
1569
|
}
|
|
1818
1570
|
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const agents = loadAgents();
|
|
1829
|
-
if (agents[session.agent]) {
|
|
1830
|
-
serverAgent = agents[session.agent];
|
|
1831
|
-
serverAgentKey = session.agent;
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1571
|
+
ws.on('message', (raw, isBinary) => {
|
|
1572
|
+
const str = raw.toString('utf8');
|
|
1573
|
+
if (!isBinary && str.startsWith('{')) {
|
|
1574
|
+
try {
|
|
1575
|
+
const m = JSON.parse(str);
|
|
1576
|
+
if (m.type === 'init') { if (!initialized) { initialized = true; start(m.cols, m.rows); } return; }
|
|
1577
|
+
if (m.type === 'resize' && pty) { try { pty.resize(m.cols || 100, m.rows || 30); } catch {} return; }
|
|
1578
|
+
if (m.type === 'restart') { initialized = true; start(100, 30); return; }
|
|
1579
|
+
} catch {}
|
|
1834
1580
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
}
|
|
1581
|
+
if (pty) pty.write(str);
|
|
1582
|
+
});
|
|
1838
1583
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
const body = await readBody(req);
|
|
1842
|
-
deleteSessionFile(body.id);
|
|
1843
|
-
json(res, { ok: true });
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1584
|
+
ws.on('close', () => { if (pty) { try { pty.kill(); } catch {} pty = null; } });
|
|
1585
|
+
});
|
|
1846
1586
|
|
|
1847
|
-
|
|
1848
|
-
if (req.method === 'POST' && url.pathname === '/api/sessions/rename') {
|
|
1849
|
-
const body = await readBody(req);
|
|
1850
|
-
renameSession(body.id, body.name);
|
|
1851
|
-
json(res, { ok: true });
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1587
|
+
// ── FS watcher: broadcast to all /events clients ─────────────────
|
|
1854
1588
|
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
const file = createAgentFile(body.name, body.description, body.tools, body.content || '');
|
|
1859
|
-
json(res, { ok: true, file });
|
|
1860
|
-
return;
|
|
1861
|
-
}
|
|
1589
|
+
let watcher = null;
|
|
1590
|
+
const eventsClients = new Set();
|
|
1591
|
+
const recentEvents = new Map(); // path -> timestamp (dedupe burst events)
|
|
1862
1592
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1593
|
+
function startWatcher() {
|
|
1594
|
+
try {
|
|
1595
|
+
watcher = fs.watch(workingDir, { recursive: true }, (event, filename) => {
|
|
1596
|
+
if (!filename) return;
|
|
1597
|
+
// Normalize to forward slashes, skip ignored
|
|
1598
|
+
const rel = filename.split(sep).join('/');
|
|
1599
|
+
const top = rel.split('/')[0];
|
|
1600
|
+
if (IGNORE_NAMES.has(top)) return;
|
|
1601
|
+
const now = Date.now();
|
|
1602
|
+
const last = recentEvents.get(rel) || 0;
|
|
1603
|
+
if (now - last < 250) return; // dedupe
|
|
1604
|
+
recentEvents.set(rel, now);
|
|
1605
|
+
if (recentEvents.size > 500) { // bounded
|
|
1606
|
+
const cutoff = now - 10000;
|
|
1607
|
+
for (const [k, t] of recentEvents) if (t < cutoff) recentEvents.delete(k);
|
|
1608
|
+
}
|
|
1609
|
+
const payload = JSON.stringify({ event, path: rel, ts: now });
|
|
1610
|
+
for (const c of eventsClients) {
|
|
1611
|
+
if (c.readyState === c.OPEN) { try { c.send(payload); } catch {} }
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
dbg('fs.watch started on', workingDir);
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
console.error('[ui] fs.watch failed:', e.message);
|
|
1869
1617
|
}
|
|
1618
|
+
}
|
|
1870
1619
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
}
|
|
1620
|
+
wssEvents.on('connection', (ws) => {
|
|
1621
|
+
eventsClients.add(ws);
|
|
1622
|
+
dbg('events client connected (total=' + eventsClients.size + ')');
|
|
1623
|
+
if (lastStats) { try { ws.send(lastStats); } catch {} }
|
|
1624
|
+
ws.on('close', () => { eventsClients.delete(ws); });
|
|
1625
|
+
});
|
|
1878
1626
|
|
|
1879
|
-
|
|
1880
|
-
if (req.method === 'POST' && url.pathname === '/api/skills/delete') {
|
|
1881
|
-
const body = await readBody(req);
|
|
1882
|
-
deleteSkillFile(body.key);
|
|
1883
|
-
json(res, { ok: true });
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1627
|
+
// ── System stats poller (RAM + GPU on macOS) ─────────────────────
|
|
1886
1628
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
json(res, { entries: getTreeEntries(dirPath) });
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1629
|
+
let lastStats = null;
|
|
1630
|
+
let statsTimer = null;
|
|
1631
|
+
let lastCpuSample = null;
|
|
1893
1632
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const safe = safePath(filePath);
|
|
1898
|
-
if (!safe) { json(res, { error: 'Invalid path' }, 400); return; }
|
|
1899
|
-
try {
|
|
1900
|
-
const content = fs.readFileSync(safe, 'utf8');
|
|
1901
|
-
json(res, { content, path: filePath });
|
|
1902
|
-
} catch (e) { json(res, { error: e.message }, 500); }
|
|
1903
|
-
return;
|
|
1633
|
+
function broadcastEvents(payload) {
|
|
1634
|
+
for (const c of eventsClients) {
|
|
1635
|
+
if (c.readyState === c.OPEN) { try { c.send(payload); } catch {} }
|
|
1904
1636
|
}
|
|
1637
|
+
}
|
|
1905
1638
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
const safe = safePath(body.path);
|
|
1910
|
-
if (!safe) { json(res, { error: 'Invalid path' }, 400); return; }
|
|
1639
|
+
function runCmd(cmd, args, timeoutMs = 1500) {
|
|
1640
|
+
return new Promise((resolve) => {
|
|
1641
|
+
let out = ''; let done = false;
|
|
1911
1642
|
try {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
if (req.method === 'POST' && url.pathname === '/api/model') {
|
|
1921
|
-
const body = await readBody(req);
|
|
1922
|
-
serverModel = body.model || serverModel;
|
|
1923
|
-
json(res, { ok: true, model: serverModel });
|
|
1924
|
-
return;
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
// ── API: select agent ──
|
|
1928
|
-
if (req.method === 'POST' && url.pathname === '/api/agent') {
|
|
1929
|
-
const body = await readBody(req);
|
|
1930
|
-
const agentKey = body.agent;
|
|
1931
|
-
if (agentKey === null) {
|
|
1932
|
-
serverAgent = null;
|
|
1933
|
-
serverAgentKey = null;
|
|
1934
|
-
serverAgentTools = null;
|
|
1935
|
-
} else {
|
|
1936
|
-
const agents = loadAgents();
|
|
1937
|
-
if (agents[agentKey]) {
|
|
1938
|
-
serverAgent = agents[agentKey];
|
|
1939
|
-
serverAgentKey = agentKey;
|
|
1940
|
-
const toolMap = { read: 'READ', write: 'WRITE', edit: 'PATCH', patch: 'PATCH', list: 'LIST', search: 'SEARCH', shell: 'SHELL', mkdir: 'MKDIR' };
|
|
1941
|
-
serverAgentTools = serverAgent.tools
|
|
1942
|
-
? (Array.isArray(serverAgent.tools) ? serverAgent.tools : [serverAgent.tools])
|
|
1943
|
-
.map(t => toolMap[t.toLowerCase()] || t.toUpperCase()).filter(Boolean)
|
|
1944
|
-
: null;
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
resetChat();
|
|
1948
|
-
json(res, { ok: true });
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1643
|
+
const p = spawn(cmd, args);
|
|
1644
|
+
const t = setTimeout(() => { if (!done) { done = true; try { p.kill(); } catch {} resolve(''); } }, timeoutMs);
|
|
1645
|
+
p.stdout.on('data', (d) => { out += d.toString(); });
|
|
1646
|
+
p.on('error', () => { if (!done) { done = true; clearTimeout(t); resolve(''); } });
|
|
1647
|
+
p.on('close', () => { if (!done) { done = true; clearTimeout(t); resolve(out); } });
|
|
1648
|
+
} catch { resolve(''); }
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1951
1651
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1652
|
+
async function readMemMac() {
|
|
1653
|
+
// Parse vm_stat. On Apple Silicon page size = 16384, on Intel = 4096.
|
|
1654
|
+
const out = await runCmd('vm_stat', []);
|
|
1655
|
+
if (!out) return null;
|
|
1656
|
+
const pageMatch = out.match(/page size of (\d+)/);
|
|
1657
|
+
const pageSize = pageMatch ? parseInt(pageMatch[1], 10) : 4096;
|
|
1658
|
+
function pages(name) {
|
|
1659
|
+
const m = out.match(new RegExp(name + '[^\\d]+(\\d+)'));
|
|
1660
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
1958
1661
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
const stream = chatStream(serverMessages, serverModel, serverAgentTools);
|
|
1987
|
-
let clientClosed = false;
|
|
1988
|
-
res.on('close', () => { clientClosed = true; });
|
|
1989
|
-
for await (const evt of stream) {
|
|
1990
|
-
if (clientClosed) break;
|
|
1991
|
-
const ok = res.write(`data: ${JSON.stringify(evt)}\n\n`);
|
|
1992
|
-
if (!ok && !clientClosed) await new Promise(r => {
|
|
1993
|
-
res.once('drain', r);
|
|
1994
|
-
res.once('close', r);
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
} catch (e) {
|
|
1998
|
-
try { res.write(`data: ${JSON.stringify({ type: 'system', data: 'Error: ' + e.message })}\n\n`); } catch {}
|
|
1999
|
-
}
|
|
2000
|
-
res.write('data: [DONE]\n\n');
|
|
2001
|
-
res.end();
|
|
2002
|
-
return;
|
|
1662
|
+
const wired = pages('Pages wired down');
|
|
1663
|
+
const active = pages('Pages active');
|
|
1664
|
+
const compressed = pages('Pages occupied by compressor');
|
|
1665
|
+
const used = (wired + active + compressed) * pageSize;
|
|
1666
|
+
const total = os.totalmem();
|
|
1667
|
+
return { used, total, percent: total > 0 ? Math.round((used / total) * 100) : 0 };
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
async function readGPUMac() {
|
|
1671
|
+
// ioreg dumps GPU performance stats including "Device Utilization %"
|
|
1672
|
+
const out = await runCmd('ioreg', ['-r', '-c', 'IOAccelerator', '-d', '1', '-w', '0']);
|
|
1673
|
+
if (!out) return null;
|
|
1674
|
+
const m = out.match(/"Device Utilization %"\s*=\s*(\d+)/);
|
|
1675
|
+
if (!m) return null;
|
|
1676
|
+
const memMatch = out.match(/"In use system memory"\s*=\s*(\d+)/);
|
|
1677
|
+
return {
|
|
1678
|
+
percent: parseInt(m[1], 10),
|
|
1679
|
+
memBytes: memMatch ? parseInt(memMatch[1], 10) : null,
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function readCPU() {
|
|
1684
|
+
const cpus = os.cpus();
|
|
1685
|
+
let idle = 0; let total = 0;
|
|
1686
|
+
for (const c of cpus) {
|
|
1687
|
+
for (const k of Object.keys(c.times)) total += c.times[k];
|
|
1688
|
+
idle += c.times.idle;
|
|
2003
1689
|
}
|
|
1690
|
+
if (!lastCpuSample) { lastCpuSample = { idle, total }; return null; }
|
|
1691
|
+
const di = idle - lastCpuSample.idle;
|
|
1692
|
+
const dt = total - lastCpuSample.total;
|
|
1693
|
+
lastCpuSample = { idle, total };
|
|
1694
|
+
if (dt <= 0) return null;
|
|
1695
|
+
return { percent: Math.max(0, Math.min(100, Math.round((1 - di / dt) * 100))), cores: cpus.length };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
async function pollStats() {
|
|
1699
|
+
const isMac = process.platform === 'darwin';
|
|
1700
|
+
const [mem, gpu] = await Promise.all([
|
|
1701
|
+
isMac ? readMemMac() : null,
|
|
1702
|
+
isMac ? readGPUMac() : null,
|
|
1703
|
+
]);
|
|
1704
|
+
const cpu = readCPU();
|
|
1705
|
+
const payload = JSON.stringify({
|
|
1706
|
+
type: 'stats', ts: Date.now(),
|
|
1707
|
+
platform: process.platform,
|
|
1708
|
+
cpu, mem, gpu,
|
|
1709
|
+
totalMem: os.totalmem(),
|
|
1710
|
+
});
|
|
1711
|
+
lastStats = payload;
|
|
1712
|
+
if (eventsClients.size > 0) broadcastEvents(payload);
|
|
1713
|
+
}
|
|
2004
1714
|
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
}
|
|
1715
|
+
function startStatsPoll() {
|
|
1716
|
+
pollStats(); // first sample primes cpu delta
|
|
1717
|
+
statsTimer = setInterval(pollStats, 1500);
|
|
1718
|
+
}
|
|
2009
1719
|
|
|
2010
|
-
// ─── Launch
|
|
1720
|
+
// ─── Launch ──────────────────────────────────────────────────────
|
|
2011
1721
|
|
|
2012
1722
|
server.listen(PORT, () => {
|
|
2013
1723
|
const url = `http://localhost:${PORT}`;
|
|
2014
|
-
console.log(`\n ⚡ Sapper
|
|
1724
|
+
console.log(`\n \x1b[36m⚡ Sapper Web\x1b[0m running at \x1b[1m${url}\x1b[0m`);
|
|
1725
|
+
console.log(` Working dir: ${workingDir}\n`);
|
|
1726
|
+
startWatcher();
|
|
1727
|
+
startStatsPoll();
|
|
1728
|
+
|
|
1729
|
+
if (process.env.SAPPER_UI_NO_OPEN) return;
|
|
2015
1730
|
|
|
2016
1731
|
const browsers = [
|
|
2017
1732
|
['open', ['-na', 'Google Chrome', '--args', `--app=${url}`, '--new-window']],
|
|
@@ -2019,16 +1734,15 @@ server.listen(PORT, () => {
|
|
|
2019
1734
|
['open', ['-na', 'Brave Browser', '--args', `--app=${url}`]],
|
|
2020
1735
|
['open', [url]],
|
|
2021
1736
|
];
|
|
2022
|
-
|
|
2023
|
-
let opened = false;
|
|
2024
1737
|
for (const [cmd, args] of browsers) {
|
|
2025
|
-
if (opened) break;
|
|
2026
1738
|
try {
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
1739
|
+
const p = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
1740
|
+
p.unref();
|
|
1741
|
+
p.on('error', () => {});
|
|
1742
|
+
break;
|
|
2031
1743
|
} catch {}
|
|
2032
1744
|
}
|
|
2033
|
-
if (!opened) console.log(` Open manually: ${url}`);
|
|
2034
1745
|
});
|
|
1746
|
+
|
|
1747
|
+
process.on('SIGINT', () => { console.log('\nShutting down…'); try { watcher && watcher.close(); } catch {} process.exit(0); });
|
|
1748
|
+
process.on('SIGTERM', () => process.exit(0));
|