hypercore-cli 1.1.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/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/api-XGC7D5AW.js +162 -0
- package/dist/auth-DNQWYQKT.js +21 -0
- package/dist/background-2EGCAAQH.js +14 -0
- package/dist/backlog-Q2NZCLNY.js +24 -0
- package/dist/chunk-2CMSCWQW.js +162 -0
- package/dist/chunk-2LJ2DVEB.js +167 -0
- package/dist/chunk-3RPFCQKJ.js +288 -0
- package/dist/chunk-43OLRXM5.js +263 -0
- package/dist/chunk-4DVYJAJL.js +57 -0
- package/dist/chunk-6OL3GA3P.js +173 -0
- package/dist/chunk-AUHU7ALH.js +2023 -0
- package/dist/chunk-B6A2AKLN.js +139 -0
- package/dist/chunk-BE46C7JW.js +46 -0
- package/dist/chunk-CUVAUOXL.js +58 -0
- package/dist/chunk-GH7E2OJE.js +223 -0
- package/dist/chunk-GOOTEPBK.js +271 -0
- package/dist/chunk-GPPMJYSM.js +133 -0
- package/dist/chunk-GU2FZQ6A.js +69 -0
- package/dist/chunk-IOPKN5GD.js +190 -0
- package/dist/chunk-IXOIOGR5.js +1505 -0
- package/dist/chunk-KRPOPWGA.js +251 -0
- package/dist/chunk-MGLJ53QN.js +219 -0
- package/dist/chunk-MV4TTRYX.js +533 -0
- package/dist/chunk-OPZYEVYR.js +150 -0
- package/dist/chunk-QTSLP47C.js +166 -0
- package/dist/chunk-R3GPQC7I.js +393 -0
- package/dist/chunk-RKB2JOV2.js +43 -0
- package/dist/chunk-RNG3K465.js +80 -0
- package/dist/chunk-TGTYKBGC.js +86 -0
- package/dist/chunk-U5SGAIMM.js +681 -0
- package/dist/chunk-V5UHPPSY.js +140 -0
- package/dist/chunk-WHLVZCQY.js +245 -0
- package/dist/chunk-XDRCBMZZ.js +66 -0
- package/dist/chunk-XOS6HPEF.js +134 -0
- package/dist/chunk-ZSBHUGWR.js +262 -0
- package/dist/claude-NSQ442XD.js +12 -0
- package/dist/commands-CK3WFAGI.js +128 -0
- package/dist/commands-U63OEO5J.js +1044 -0
- package/dist/commands-ZE6GD3WC.js +232 -0
- package/dist/config-4EW42BSF.js +8 -0
- package/dist/config-loader-SXO674TF.js +24 -0
- package/dist/diagnose-AFW3ZTZ4.js +12 -0
- package/dist/display-IIUBEYWN.js +58 -0
- package/dist/extractor-QV53W2YJ.js +129 -0
- package/dist/history-WMSCHERZ.js +180 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +406 -0
- package/dist/instance-registry-YSIJXSO7.js +15 -0
- package/dist/keybindings-JAAMLH3G.js +15 -0
- package/dist/loader-WHNTZTLP.js +58 -0
- package/dist/network-MM6YWPGO.js +279 -0
- package/dist/notify-HPTALZDC.js +14 -0
- package/dist/openai-compat-UQWJXBEK.js +12 -0
- package/dist/permissions-JUKXMNDH.js +10 -0
- package/dist/prompt-QV45TXRL.js +166 -0
- package/dist/quality-ST7PPNFR.js +16 -0
- package/dist/repl-RT3AHL7M.js +3375 -0
- package/dist/roadmap-5OBEKROY.js +17 -0
- package/dist/server-PORT7OEG.js +57 -0
- package/dist/session-4VUNDWLH.js +21 -0
- package/dist/skills-V4A35XKG.js +175 -0
- package/dist/store-Y4LU5QTO.js +25 -0
- package/dist/team-HO7Z4SIM.js +385 -0
- package/dist/telemetry-6R4EIE6O.js +30 -0
- package/dist/test-runner-ZQH5Y6OJ.js +619 -0
- package/dist/theme-3SYJ3UQA.js +14 -0
- package/dist/upgrade-7TGI3SXO.js +83 -0
- package/dist/verify-JUDKTPKZ.js +14 -0
- package/dist/web/static/app.js +562 -0
- package/dist/web/static/index.html +132 -0
- package/dist/web/static/mirror.css +1001 -0
- package/dist/web/static/mirror.html +184 -0
- package/dist/web/static/mirror.js +1125 -0
- package/dist/web/static/onboard.css +302 -0
- package/dist/web/static/onboard.html +140 -0
- package/dist/web/static/onboard.js +260 -0
- package/dist/web/static/style.css +602 -0
- package/dist/web/static/workspace.css +1568 -0
- package/dist/web/static/workspace.html +408 -0
- package/dist/web/static/workspace.js +1683 -0
- package/dist/web-Z5HSCQHW.js +39 -0
- package/package.json +67 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HYPERCORE_DIR
|
|
3
|
+
} from "./chunk-V5UHPPSY.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/upgrade.ts
|
|
6
|
+
import { readdir, copyFile, mkdir } from "fs/promises";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
function getTemplatesDir() {
|
|
12
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
13
|
+
const distDir = dirname(thisFile);
|
|
14
|
+
const projectRoot = join(distDir, "..");
|
|
15
|
+
return join(projectRoot, "templates");
|
|
16
|
+
}
|
|
17
|
+
async function syncDir(srcDir, destDir, category, result) {
|
|
18
|
+
if (!existsSync(srcDir)) return;
|
|
19
|
+
await mkdir(destDir, { recursive: true });
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = await readdir(srcDir);
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const file of entries) {
|
|
27
|
+
const srcPath = join(srcDir, file);
|
|
28
|
+
const destPath = join(destDir, file);
|
|
29
|
+
if (existsSync(destPath)) {
|
|
30
|
+
result.skipped.push(`${category}/${file}`);
|
|
31
|
+
} else {
|
|
32
|
+
try {
|
|
33
|
+
await copyFile(srcPath, destPath);
|
|
34
|
+
result.added.push(`${category}/${file}`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
result.errors.push(`${category}/${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function upgradeCommand() {
|
|
42
|
+
if (!existsSync(HYPERCORE_DIR)) {
|
|
43
|
+
console.log(chalk.red("\n \u274C \u8BF7\u5148\u8FD0\u884C hypercore init \u521D\u59CB\u5316\n"));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(chalk.bold("\n \u{1F504} \u68C0\u67E5\u6A21\u677F\u66F4\u65B0\u2026\n"));
|
|
47
|
+
const templatesDir = getTemplatesDir();
|
|
48
|
+
if (!existsSync(templatesDir)) {
|
|
49
|
+
console.log(chalk.red(" \u274C \u672A\u627E\u5230\u6A21\u677F\u76EE\u5F55\n"));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const result = { added: [], skipped: [], errors: [] };
|
|
53
|
+
await syncDir(join(templatesDir, "agents"), join(HYPERCORE_DIR, "agents"), "agents", result);
|
|
54
|
+
await syncDir(join(templatesDir, "skills"), join(HYPERCORE_DIR, "skills"), "skills", result);
|
|
55
|
+
await syncDir(join(templatesDir, "lines"), join(HYPERCORE_DIR, "lines"), "lines", result);
|
|
56
|
+
if (result.added.length > 0) {
|
|
57
|
+
console.log(chalk.green(` \u2705 \u65B0\u589E ${result.added.length} \u4E2A\u6A21\u677F\uFF1A`));
|
|
58
|
+
for (const f of result.added) {
|
|
59
|
+
console.log(chalk.green(` + ${f}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (result.skipped.length > 0) {
|
|
63
|
+
console.log(chalk.dim(`
|
|
64
|
+
\u23ED\uFE0F \u5DF2\u5B58\u5728 ${result.skipped.length} \u4E2A\uFF08\u672A\u8986\u76D6\uFF09\uFF1A`));
|
|
65
|
+
for (const f of result.skipped) {
|
|
66
|
+
console.log(chalk.dim(` = ${f}`));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (result.errors.length > 0) {
|
|
70
|
+
console.log(chalk.red(`
|
|
71
|
+
\u274C ${result.errors.length} \u4E2A\u9519\u8BEF\uFF1A`));
|
|
72
|
+
for (const e of result.errors) {
|
|
73
|
+
console.log(chalk.red(` ${e}`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (result.added.length === 0 && result.errors.length === 0) {
|
|
77
|
+
console.log(chalk.dim(" \u6240\u6709\u6A21\u677F\u5DF2\u662F\u6700\u65B0\uFF0C\u65E0\u9700\u66F4\u65B0\u3002"));
|
|
78
|
+
}
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
upgradeCommand
|
|
83
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getMetricValue,
|
|
3
|
+
inferBaselineKey,
|
|
4
|
+
recordBaseline,
|
|
5
|
+
verifyCompletedItems
|
|
6
|
+
} from "./chunk-IOPKN5GD.js";
|
|
7
|
+
import "./chunk-2CMSCWQW.js";
|
|
8
|
+
import "./chunk-MGLJ53QN.js";
|
|
9
|
+
export {
|
|
10
|
+
getMetricValue,
|
|
11
|
+
inferBaselineKey,
|
|
12
|
+
recordBaseline,
|
|
13
|
+
verifyCompletedItems
|
|
14
|
+
};
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hypercore Web Dashboard - Frontend Logic
|
|
3
|
+
* System-level management & monitoring panel
|
|
4
|
+
* Pure vanilla JS, no dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── State ───────────────────────────────────────────────────
|
|
8
|
+
const state = {
|
|
9
|
+
activeTab: 'overview',
|
|
10
|
+
lines: [],
|
|
11
|
+
selectedLine: null,
|
|
12
|
+
isRunning: false,
|
|
13
|
+
historyRuns: [],
|
|
14
|
+
refreshTimer: null,
|
|
15
|
+
connected: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const REFRESH_INTERVAL = 10_000; // 10s auto-refresh
|
|
19
|
+
|
|
20
|
+
// ─── DOM References ──────────────────────────────────────────
|
|
21
|
+
const $ = (sel) => document.querySelector(sel);
|
|
22
|
+
|
|
23
|
+
// ─── API Helpers ─────────────────────────────────────────────
|
|
24
|
+
const API = '/api';
|
|
25
|
+
|
|
26
|
+
async function fetchJSON(path) {
|
|
27
|
+
const res = await fetch(`${API}${path}`);
|
|
28
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
29
|
+
return res.json();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Utilities ───────────────────────────────────────────────
|
|
33
|
+
function relativeTime(isoStr) {
|
|
34
|
+
if (!isoStr) return '-';
|
|
35
|
+
const diff = Date.now() - new Date(isoStr).getTime();
|
|
36
|
+
if (diff < 0) return 'just now';
|
|
37
|
+
if (diff < 60_000) return `${Math.floor(diff / 1000)}s ago`;
|
|
38
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
39
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
40
|
+
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shortPath(fullPath) {
|
|
44
|
+
if (!fullPath) return '-';
|
|
45
|
+
const parts = fullPath.split('/');
|
|
46
|
+
return parts.length > 2 ? `~/${parts.slice(-2).join('/')}` : fullPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function updateTimestamp() {
|
|
50
|
+
const el = $('#last-updated');
|
|
51
|
+
if (el) {
|
|
52
|
+
const now = new Date();
|
|
53
|
+
el.textContent = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Connection Health Check ─────────────────────────────────
|
|
58
|
+
async function checkConnection() {
|
|
59
|
+
const dot = $('#connection-status');
|
|
60
|
+
try {
|
|
61
|
+
await fetchJSON('/config');
|
|
62
|
+
state.connected = true;
|
|
63
|
+
dot.className = 'status-dot online';
|
|
64
|
+
dot.title = 'Connected';
|
|
65
|
+
} catch {
|
|
66
|
+
state.connected = false;
|
|
67
|
+
dot.className = 'status-dot offline';
|
|
68
|
+
dot.title = 'Disconnected';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Tab Navigation ──────────────────────────────────────────
|
|
73
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
74
|
+
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function switchTab(tab) {
|
|
78
|
+
state.activeTab = tab;
|
|
79
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
|
|
80
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === `tab-${tab}`));
|
|
81
|
+
|
|
82
|
+
if (tab === 'overview') loadOverview();
|
|
83
|
+
if (tab === 'lines' && state.lines.length === 0) loadLines();
|
|
84
|
+
if (tab === 'history') loadHistory();
|
|
85
|
+
if (tab === 'settings') loadSettings();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Markdown Renderer (simple) ──────────────────────────────
|
|
89
|
+
function renderMarkdown(text) {
|
|
90
|
+
if (!text) return '';
|
|
91
|
+
let html = text
|
|
92
|
+
.replace(/&/g, '&')
|
|
93
|
+
.replace(/</g, '<')
|
|
94
|
+
.replace(/>/g, '>');
|
|
95
|
+
|
|
96
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
|
|
97
|
+
`<pre><code class="lang-${lang}">${code.trim()}</code></pre>`);
|
|
98
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
99
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
100
|
+
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
101
|
+
html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
|
|
102
|
+
html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
|
|
103
|
+
html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
|
|
104
|
+
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
105
|
+
html = html.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>');
|
|
106
|
+
html = html.replace(/<\/ul>\s*<ul>/g, '');
|
|
107
|
+
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
|
108
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
109
|
+
html = html.replace(/\n\n/g, '</p><p>');
|
|
110
|
+
html = html.replace(/\n/g, '<br>');
|
|
111
|
+
return `<p>${html}</p>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function escapeHtml(text) {
|
|
115
|
+
const div = document.createElement('div');
|
|
116
|
+
div.textContent = text;
|
|
117
|
+
return div.innerHTML;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Overview Tab ────────────────────────────────────────────
|
|
121
|
+
async function loadOverview() {
|
|
122
|
+
const [configRes, gitRes, instancesRes, linesRes, historyRes] = await Promise.allSettled([
|
|
123
|
+
fetchJSON('/config'),
|
|
124
|
+
fetchJSON('/git/status'),
|
|
125
|
+
fetchJSON('/instances'),
|
|
126
|
+
fetchJSON('/lines'),
|
|
127
|
+
fetchJSON('/history'),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
updateTimestamp();
|
|
131
|
+
|
|
132
|
+
// ── Instances card (wide, top priority) ──
|
|
133
|
+
const instancesBody = $('#card-instances-body');
|
|
134
|
+
if (instancesRes.status === 'fulfilled') {
|
|
135
|
+
const data = instancesRes.value;
|
|
136
|
+
const instances = data.instances || [];
|
|
137
|
+
if (instances.length === 0) {
|
|
138
|
+
instancesBody.innerHTML = `
|
|
139
|
+
<div class="empty-state">
|
|
140
|
+
<div class="empty-icon">\u{1F50C}</div>
|
|
141
|
+
<div class="empty-text">No active sessions</div>
|
|
142
|
+
<div class="empty-hint">Start a terminal with <code>hyper</code> to see it here</div>
|
|
143
|
+
</div>`;
|
|
144
|
+
} else {
|
|
145
|
+
let html = `<div class="instance-count">${instances.length} active</div>`;
|
|
146
|
+
html += '<div class="instances-list">';
|
|
147
|
+
for (const inst of instances) {
|
|
148
|
+
const current = inst.isCurrent;
|
|
149
|
+
const name = inst.projectName || inst.cwd.split('/').pop() || 'hyper';
|
|
150
|
+
const uptime = relativeTime(inst.startTime);
|
|
151
|
+
html += `<div class="instance-row${current ? ' current' : ''}">
|
|
152
|
+
<div class="instance-row-left">
|
|
153
|
+
<span class="instance-dot${current ? ' current' : ''}"></span>
|
|
154
|
+
<div class="instance-info">
|
|
155
|
+
<span class="instance-name">${escapeHtml(name)}${current ? ' <span class="badge-current">current</span>' : ''}</span>
|
|
156
|
+
<span class="instance-meta">${escapeHtml(inst.provider)}/${escapeHtml(inst.model)} \u00B7 ${shortPath(inst.cwd)}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="instance-row-right">
|
|
160
|
+
<span class="instance-port">:${inst.port}</span>
|
|
161
|
+
<span class="instance-uptime">${uptime}</span>
|
|
162
|
+
</div>
|
|
163
|
+
</div>`;
|
|
164
|
+
}
|
|
165
|
+
html += '</div>';
|
|
166
|
+
instancesBody.innerHTML = html;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
instancesBody.innerHTML = '<span class="text-muted">Failed to load</span>';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Model card ──
|
|
173
|
+
const modelBody = $('#card-model-body');
|
|
174
|
+
if (configRes.status === 'fulfilled') {
|
|
175
|
+
const c = configRes.value;
|
|
176
|
+
modelBody.innerHTML = `
|
|
177
|
+
<div class="card-stat"><span>Provider</span><span>${escapeHtml(c.provider)}</span></div>
|
|
178
|
+
<div class="card-stat"><span>Model</span><span>${escapeHtml(c.model)}</span></div>`;
|
|
179
|
+
} else {
|
|
180
|
+
modelBody.innerHTML = '<span class="text-muted">Failed to load</span>';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Git card ──
|
|
184
|
+
const gitBody = $('#card-git-body');
|
|
185
|
+
if (gitRes.status === 'fulfilled') {
|
|
186
|
+
const g = gitRes.value;
|
|
187
|
+
if (!g.available) {
|
|
188
|
+
gitBody.innerHTML = '<span class="text-muted">Not a git repo</span>';
|
|
189
|
+
} else {
|
|
190
|
+
gitBody.innerHTML = `
|
|
191
|
+
<div class="card-stat"><span>Branch</span><span class="git-branch">${escapeHtml(g.branch)}</span></div>
|
|
192
|
+
<div class="card-stat"><span>HEAD</span><span>${escapeHtml(g.head)}</span></div>
|
|
193
|
+
<div class="card-stat"><span>Status</span><span>${g.uncommitted
|
|
194
|
+
? '<span class="git-dirty">\u26A0 Uncommitted</span>'
|
|
195
|
+
: '<span class="git-clean">\u2713 Clean</span>'}</span></div>`;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
gitBody.innerHTML = '<span class="text-muted">Failed to load</span>';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Lines card ──
|
|
202
|
+
const linesBody = $('#card-lines-body');
|
|
203
|
+
if (linesRes.status === 'fulfilled') {
|
|
204
|
+
const l = linesRes.value;
|
|
205
|
+
const count = (l.lines || []).length;
|
|
206
|
+
linesBody.innerHTML = `
|
|
207
|
+
<div class="card-value-big">${count}</div>
|
|
208
|
+
<div class="card-value-label">available</div>`;
|
|
209
|
+
state.lines = l.lines || [];
|
|
210
|
+
} else {
|
|
211
|
+
linesBody.innerHTML = '<span class="text-muted">Failed to load</span>';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Runs card ──
|
|
215
|
+
const runsBody = $('#card-runs-body');
|
|
216
|
+
if (historyRes.status === 'fulfilled') {
|
|
217
|
+
const h = historyRes.value;
|
|
218
|
+
const runs = h.runs || [];
|
|
219
|
+
const lastRun = runs.length > 0 ? relativeTime(runs[0].startTime) : '-';
|
|
220
|
+
runsBody.innerHTML = `
|
|
221
|
+
<div class="card-value-big">${runs.length}</div>
|
|
222
|
+
<div class="card-value-label">total runs</div>
|
|
223
|
+
<div class="card-stat" style="margin-top:8px"><span>Last run</span><span>${lastRun}</span></div>`;
|
|
224
|
+
state.historyRuns = runs;
|
|
225
|
+
} else {
|
|
226
|
+
runsBody.innerHTML = '<span class="text-muted">Failed to load</span>';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Settings Tab ────────────────────────────────────────────
|
|
231
|
+
async function loadSettings() {
|
|
232
|
+
const [toolsRes, hooksRes] = await Promise.allSettled([
|
|
233
|
+
fetchJSON('/tools'),
|
|
234
|
+
fetchJSON('/hooks'),
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
const toolsEl = $('#settings-tools');
|
|
238
|
+
if (toolsRes.status === 'fulfilled') {
|
|
239
|
+
const tools = toolsRes.value.tools || [];
|
|
240
|
+
if (tools.length === 0) {
|
|
241
|
+
toolsEl.innerHTML = '<span class="text-muted">No tools registered</span>';
|
|
242
|
+
} else {
|
|
243
|
+
let html = `<div class="settings-tools-count">${tools.length} tools</div>`;
|
|
244
|
+
html += '<div class="settings-tools-grid">';
|
|
245
|
+
for (const tool of tools) {
|
|
246
|
+
html += `<span class="tool-tag" title="${escapeHtml(tool.description || '')}">${escapeHtml(tool.name)}</span>`;
|
|
247
|
+
}
|
|
248
|
+
html += '</div>';
|
|
249
|
+
toolsEl.innerHTML = html;
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
toolsEl.innerHTML = '<span class="text-muted">Failed to load</span>';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const hooksEl = $('#settings-hooks');
|
|
256
|
+
if (hooksRes.status === 'fulfilled') {
|
|
257
|
+
const data = hooksRes.value;
|
|
258
|
+
if (data.total === 0) {
|
|
259
|
+
hooksEl.innerHTML = '<span class="text-muted">No hooks configured</span>';
|
|
260
|
+
} else {
|
|
261
|
+
let html = '<div class="hooks-list">';
|
|
262
|
+
for (const [event, hooks] of Object.entries(data.byEvent)) {
|
|
263
|
+
html += `<div><span class="hook-event">${escapeHtml(event)}</span> <span class="hook-count">(${hooks.length})</span></div>`;
|
|
264
|
+
}
|
|
265
|
+
hooksEl.innerHTML = html + '</div>';
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
hooksEl.innerHTML = '<span class="text-muted">Failed to load</span>';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── Production Lines ────────────────────────────────────────
|
|
273
|
+
async function loadLines() {
|
|
274
|
+
const listEl = $('#lines-list');
|
|
275
|
+
listEl.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Loading...</div>';
|
|
276
|
+
try {
|
|
277
|
+
const data = await fetchJSON('/lines');
|
|
278
|
+
state.lines = data.lines || [];
|
|
279
|
+
renderLinesList();
|
|
280
|
+
} catch {
|
|
281
|
+
listEl.innerHTML = '<div style="padding:12px;color:var(--red)">Failed to load</div>';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function renderLinesList() {
|
|
286
|
+
const listEl = $('#lines-list');
|
|
287
|
+
listEl.innerHTML = '';
|
|
288
|
+
if (state.lines.length === 0) {
|
|
289
|
+
listEl.innerHTML = '<div style="padding:12px;color:var(--text-muted)">No production lines found</div>';
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
for (const line of state.lines) {
|
|
293
|
+
const item = document.createElement('div');
|
|
294
|
+
item.className = `line-item${state.selectedLine?.name === line.name ? ' active' : ''}`;
|
|
295
|
+
item.innerHTML = `
|
|
296
|
+
<div class="line-item-name">${line.icon || '\u{1F4CB}'} ${escapeHtml(line.name)}</div>
|
|
297
|
+
<div class="line-item-desc">${escapeHtml(line.description || '')}</div>`;
|
|
298
|
+
item.addEventListener('click', () => selectLine(line));
|
|
299
|
+
listEl.appendChild(item);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function selectLine(line) {
|
|
304
|
+
state.selectedLine = line;
|
|
305
|
+
renderLinesList();
|
|
306
|
+
$('#run-line-name').textContent = `${line.icon || '\u{1F3ED}'} ${line.name}`;
|
|
307
|
+
|
|
308
|
+
let html = `<p class="text-muted" style="margin-bottom:16px">${escapeHtml(line.description || '')}</p>`;
|
|
309
|
+
html += '<div class="run-form">';
|
|
310
|
+
if (line.inputs && line.inputs.length > 0) {
|
|
311
|
+
for (const inp of line.inputs) {
|
|
312
|
+
const req = inp.required ? ' *' : '';
|
|
313
|
+
html += `<div class="run-field">
|
|
314
|
+
<label>${escapeHtml(inp.name)}${req}</label>
|
|
315
|
+
<input type="text" name="${escapeHtml(inp.name)}" placeholder="${escapeHtml(inp.description || '')}"
|
|
316
|
+
${inp.defaultValue ? `value="${escapeHtml(inp.defaultValue)}"` : ''}>
|
|
317
|
+
${inp.description ? `<div class="field-hint">${escapeHtml(inp.description)}</div>` : ''}
|
|
318
|
+
</div>`;
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
html += '<p class="text-muted">No input parameters.</p>';
|
|
322
|
+
}
|
|
323
|
+
html += '<button class="btn-primary" id="btn-run-exec">\u25B6 Run</button></div>';
|
|
324
|
+
html += '<div id="run-output-area"></div>';
|
|
325
|
+
|
|
326
|
+
$('#run-content').innerHTML = html;
|
|
327
|
+
$('#btn-run-exec').addEventListener('click', () => runLine(line));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function runLine(line) {
|
|
331
|
+
if (state.isRunning) return;
|
|
332
|
+
state.isRunning = true;
|
|
333
|
+
const btn = $('#btn-run-exec');
|
|
334
|
+
btn.disabled = true;
|
|
335
|
+
btn.textContent = 'Running...';
|
|
336
|
+
|
|
337
|
+
const inputs = {};
|
|
338
|
+
document.querySelectorAll('#run-content .run-field input').forEach(f => {
|
|
339
|
+
if (f.value.trim()) inputs[f.name] = f.value.trim();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const outputArea = $('#run-output-area');
|
|
343
|
+
outputArea.innerHTML = '<div class="run-output-stream" id="run-stream"></div>';
|
|
344
|
+
const streamEl = $('#run-stream');
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const response = await fetch('/api/run', {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: { 'Content-Type': 'application/json' },
|
|
350
|
+
body: JSON.stringify({ lineName: line.name, inputs }),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
let errMsg = `HTTP ${response.status}`;
|
|
355
|
+
try { const errData = await response.json(); errMsg = errData.error || errMsg; } catch { /* ignore */ }
|
|
356
|
+
throw new Error(errMsg);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const reader = response.body.getReader();
|
|
360
|
+
const decoder = new TextDecoder();
|
|
361
|
+
let buffer = '';
|
|
362
|
+
|
|
363
|
+
while (true) {
|
|
364
|
+
const { done, value } = await reader.read();
|
|
365
|
+
if (done) break;
|
|
366
|
+
buffer += decoder.decode(value, { stream: true });
|
|
367
|
+
const lines = buffer.split('\n');
|
|
368
|
+
buffer = lines.pop() || '';
|
|
369
|
+
for (const rawLine of lines) {
|
|
370
|
+
if (!rawLine.startsWith('data: ')) continue;
|
|
371
|
+
try { handleRunEvent(JSON.parse(rawLine.slice(6)), streamEl); } catch { /* skip */ }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
streamEl.innerHTML += `<div class="run-event run-event-error">\u274C ${escapeHtml(String(err))}</div>`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
state.isRunning = false;
|
|
379
|
+
btn.disabled = false;
|
|
380
|
+
btn.textContent = '\u25B6 Run';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function handleRunEvent(data, streamEl) {
|
|
384
|
+
const div = document.createElement('div');
|
|
385
|
+
div.className = 'run-event';
|
|
386
|
+
|
|
387
|
+
switch (data.type) {
|
|
388
|
+
case 'run_start':
|
|
389
|
+
div.className += ' run-event-station';
|
|
390
|
+
div.textContent = `\u{1F3ED} Starting: ${data.lineName}`;
|
|
391
|
+
break;
|
|
392
|
+
case 'station_start': {
|
|
393
|
+
const ct = streamEl.querySelector('.run-text-current');
|
|
394
|
+
if (ct) ct.classList.remove('run-text-current');
|
|
395
|
+
div.className += ' run-event-station';
|
|
396
|
+
div.textContent = `\u{1F4CD} Station ${data.index}/${data.total}: ${data.name} [${data.agentName}]`;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case 'text': {
|
|
400
|
+
let textEl = streamEl.querySelector('.run-text-current');
|
|
401
|
+
if (!textEl) {
|
|
402
|
+
textEl = document.createElement('div');
|
|
403
|
+
textEl.className = 'run-event run-event-text run-text-current';
|
|
404
|
+
streamEl.appendChild(textEl);
|
|
405
|
+
}
|
|
406
|
+
textEl.textContent += data.content;
|
|
407
|
+
streamEl.scrollTop = streamEl.scrollHeight;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
case 'tool_call': {
|
|
411
|
+
const ct2 = streamEl.querySelector('.run-text-current');
|
|
412
|
+
if (ct2) ct2.classList.remove('run-text-current');
|
|
413
|
+
div.className += ' run-event-tool';
|
|
414
|
+
div.textContent = `\u{1F527} ${data.name}`;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case 'station_complete': {
|
|
418
|
+
const ct3 = streamEl.querySelector('.run-text-current');
|
|
419
|
+
if (ct3) ct3.classList.remove('run-text-current');
|
|
420
|
+
div.className += ' run-event-complete';
|
|
421
|
+
div.textContent = `\u2705 ${data.name} complete`;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case 'station_skipped':
|
|
425
|
+
div.innerHTML = `<span style="color:var(--yellow)">\u23ED Skipped: ${escapeHtml(data.name)}</span>`;
|
|
426
|
+
break;
|
|
427
|
+
case 'station_retry':
|
|
428
|
+
div.innerHTML = `<span style="color:var(--yellow)">\u{1F504} Retry ${data.attempt}/${data.maxRetry}: ${escapeHtml(data.name)}</span>`;
|
|
429
|
+
break;
|
|
430
|
+
case 'run_complete': {
|
|
431
|
+
div.className += ' run-event-complete';
|
|
432
|
+
const dur = data.duration ? `${(data.duration / 1000).toFixed(1)}s` : '';
|
|
433
|
+
const tok = data.totalTokens ? `${(data.totalTokens.inputTokens + data.totalTokens.outputTokens).toLocaleString()} tokens` : '';
|
|
434
|
+
const cost = data.cost ? `~$${data.cost.usd.toFixed(4)}` : '';
|
|
435
|
+
div.textContent = `\u2705 Complete! ${dur} \u00B7 ${tok}${cost ? ` \u00B7 ${cost}` : ''}`;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
case 'error':
|
|
439
|
+
div.className += ' run-event-error';
|
|
440
|
+
div.textContent = `\u274C ${data.content}`;
|
|
441
|
+
break;
|
|
442
|
+
default:
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
streamEl.appendChild(div);
|
|
446
|
+
streamEl.scrollTop = streamEl.scrollHeight;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ─── Run History ─────────────────────────────────────────────
|
|
450
|
+
async function loadHistory() {
|
|
451
|
+
const listEl = $('#history-list');
|
|
452
|
+
listEl.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Loading...</div>';
|
|
453
|
+
try {
|
|
454
|
+
const data = await fetchJSON('/history');
|
|
455
|
+
state.historyRuns = data.runs || [];
|
|
456
|
+
renderHistoryList();
|
|
457
|
+
} catch {
|
|
458
|
+
listEl.innerHTML = '<div style="padding:12px;color:var(--red)">Failed to load</div>';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function renderHistoryList() {
|
|
463
|
+
const listEl = $('#history-list');
|
|
464
|
+
listEl.innerHTML = '';
|
|
465
|
+
if (state.historyRuns.length === 0) {
|
|
466
|
+
listEl.innerHTML = '<div style="padding:12px;color:var(--text-muted)">No run history yet</div>';
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
for (const run of state.historyRuns) {
|
|
470
|
+
const item = document.createElement('div');
|
|
471
|
+
item.className = 'history-item';
|
|
472
|
+
const date = run.startTime ? new Date(run.startTime).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' }) : '';
|
|
473
|
+
const tokens = run.totalTokens ? (run.totalTokens.inputTokens + run.totalTokens.outputTokens).toLocaleString() : '0';
|
|
474
|
+
item.innerHTML = `
|
|
475
|
+
<div class="history-item-line">${escapeHtml(run.lineName || run.file)}</div>
|
|
476
|
+
<div class="history-item-meta">${date} \u00B7 ${run.stationCount || 0} stations \u00B7 ${tokens} tokens</div>`;
|
|
477
|
+
item.addEventListener('click', function() { loadHistoryDetail(run.file, this); });
|
|
478
|
+
listEl.appendChild(item);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function loadHistoryDetail(fileId, el) {
|
|
483
|
+
const titleEl = $('#history-detail-title');
|
|
484
|
+
const contentEl = $('#history-detail-content');
|
|
485
|
+
titleEl.textContent = 'Loading...';
|
|
486
|
+
contentEl.innerHTML = '<p class="text-muted">Loading...</p>';
|
|
487
|
+
|
|
488
|
+
document.querySelectorAll('.history-item').forEach(e => e.classList.remove('active'));
|
|
489
|
+
if (el) el.classList.add('active');
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const data = await fetchJSON(`/history/${fileId}`);
|
|
493
|
+
titleEl.textContent = data.lineName || fileId;
|
|
494
|
+
|
|
495
|
+
const startDate = data.startTime ? new Date(data.startTime).toLocaleString('zh-CN') : '';
|
|
496
|
+
const durationMs = data.startTime && data.endTime ? new Date(data.endTime) - new Date(data.startTime) : 0;
|
|
497
|
+
const duration = durationMs < 60000 ? `${(durationMs / 1000).toFixed(0)}s` : `${(durationMs / 60000).toFixed(1)}min`;
|
|
498
|
+
const totalTokens = data.totalTokens ? (data.totalTokens.inputTokens + data.totalTokens.outputTokens).toLocaleString() : '0';
|
|
499
|
+
|
|
500
|
+
let html = '<div class="detail-meta">';
|
|
501
|
+
html += `<div class="detail-meta-row"><span>Time</span><span>${escapeHtml(startDate)}</span></div>`;
|
|
502
|
+
html += `<div class="detail-meta-row"><span>Duration</span><span>${duration}</span></div>`;
|
|
503
|
+
html += `<div class="detail-meta-row"><span>Model</span><span>${escapeHtml(data.model || '')}</span></div>`;
|
|
504
|
+
html += `<div class="detail-meta-row"><span>Tokens</span><span>${totalTokens}</span></div>`;
|
|
505
|
+
if (data.userInputs) {
|
|
506
|
+
for (const [key, val] of Object.entries(data.userInputs)) {
|
|
507
|
+
html += `<div class="detail-meta-row"><span>${escapeHtml(key)}</span><span>${escapeHtml(String(val))}</span></div>`;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
html += '</div>';
|
|
511
|
+
|
|
512
|
+
if (data.stations && data.stations.length > 0) {
|
|
513
|
+
for (const s of data.stations) {
|
|
514
|
+
const sT = s.tokens ? (s.tokens.inputTokens + s.tokens.outputTokens).toLocaleString() : '0';
|
|
515
|
+
html += '<div class="detail-station">';
|
|
516
|
+
html += `<div class="detail-station-header">\u{1F4CD} Station ${s.index}: ${escapeHtml(s.name)} [${escapeHtml(s.agent)}] \u00B7 ${sT} tokens</div>`;
|
|
517
|
+
if (s.output) {
|
|
518
|
+
html += `<div class="detail-station-output">${renderMarkdown(s.output.substring(0, 500))}${s.output.length > 500 ? '<p><em>... (truncated)</em></p>' : ''}</div>`;
|
|
519
|
+
}
|
|
520
|
+
html += '</div>';
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
contentEl.innerHTML = html;
|
|
524
|
+
} catch (err) {
|
|
525
|
+
contentEl.innerHTML = `<p style="color:var(--red)">Failed to load: ${escapeHtml(String(err))}</p>`;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ─── Refresh Controls ────────────────────────────────────────
|
|
530
|
+
function refreshCurrentTab() {
|
|
531
|
+
const btn = $('#btn-refresh');
|
|
532
|
+
btn.classList.add('spinning');
|
|
533
|
+
setTimeout(() => btn.classList.remove('spinning'), 600);
|
|
534
|
+
|
|
535
|
+
switch (state.activeTab) {
|
|
536
|
+
case 'overview': loadOverview(); break;
|
|
537
|
+
case 'lines': loadLines(); break;
|
|
538
|
+
case 'history': loadHistory(); break;
|
|
539
|
+
case 'settings': loadSettings(); break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
$('#btn-refresh').addEventListener('click', refreshCurrentTab);
|
|
544
|
+
|
|
545
|
+
// Keyboard shortcut: R to refresh
|
|
546
|
+
document.addEventListener('keydown', (e) => {
|
|
547
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
548
|
+
if (e.key === 'r' || e.key === 'R') refreshCurrentTab();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ─── Initialize ──────────────────────────────────────────────
|
|
552
|
+
async function init() {
|
|
553
|
+
await Promise.all([loadOverview(), checkConnection()]);
|
|
554
|
+
|
|
555
|
+
// Auto-refresh every 10s when on Overview
|
|
556
|
+
state.refreshTimer = setInterval(() => {
|
|
557
|
+
if (state.activeTab === 'overview') loadOverview();
|
|
558
|
+
checkConnection();
|
|
559
|
+
}, REFRESH_INTERVAL);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
init();
|