@nordbyte/nordrelay 0.3.0 → 0.3.1
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/CHANGELOG.md +9 -0
- package/README.md +24 -18
- package/dist/bot.js +5 -2
- package/dist/codex-session.js +3 -1
- package/dist/context-key.js +23 -0
- package/dist/operations.js +1 -1
- package/dist/relay-runtime.js +436 -7
- package/dist/session-registry.js +3 -3
- package/dist/settings-service.js +46 -23
- package/dist/state-backend.js +17 -8
- package/dist/web-dashboard.js +159 -33
- package/dist/web-state.js +131 -0
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +42 -17
package/dist/web-dashboard.js
CHANGED
|
@@ -12,7 +12,7 @@ import { friendlyErrorText } from "./error-messages.js";
|
|
|
12
12
|
import { escapeHTML } from "./format.js";
|
|
13
13
|
import { RelayRuntime } from "./relay-runtime.js";
|
|
14
14
|
import { resolveDashboardEnvPath, SettingsService } from "./settings-service.js";
|
|
15
|
-
const DEFAULT_HOME = path.join(os.homedir(), ".
|
|
15
|
+
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
16
16
|
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
17
17
|
const options = parseOptions(process.argv.slice(2));
|
|
18
18
|
const auth = resolveDashboardAuth(options.host);
|
|
@@ -81,10 +81,15 @@ async function handleApi(req, res, url) {
|
|
|
81
81
|
channels: listChannelDescriptors(),
|
|
82
82
|
agentAdapters: listAgentAdapterDescriptors(),
|
|
83
83
|
enabledAgents: enabledAgents(config),
|
|
84
|
+
controls: await runtime.controlOptions(),
|
|
84
85
|
status: await runtime.status(),
|
|
85
86
|
});
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
89
|
+
if (req.method === "GET" && url.pathname === "/api/control-options") {
|
|
90
|
+
sendJson(res, 200, await runtime.controlOptions());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
88
93
|
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
89
94
|
sendJson(res, 200, await runtime.status());
|
|
90
95
|
return;
|
|
@@ -119,8 +124,12 @@ async function handleApi(req, res, url) {
|
|
|
119
124
|
const body = await readJsonBody(req);
|
|
120
125
|
sendJson(res, 200, {
|
|
121
126
|
session: await runtime.newSession({
|
|
127
|
+
agentId: parseAgentId(optionalStringField(body, "agentId")),
|
|
122
128
|
workspace: optionalStringField(body, "workspace"),
|
|
123
129
|
model: optionalStringField(body, "model"),
|
|
130
|
+
reasoningEffort: optionalStringField(body, "reasoningEffort"),
|
|
131
|
+
launchProfileId: optionalStringField(body, "launchProfileId"),
|
|
132
|
+
fastMode: optionalBooleanField(body, "fastMode"),
|
|
124
133
|
}),
|
|
125
134
|
});
|
|
126
135
|
return;
|
|
@@ -182,12 +191,30 @@ async function handleApi(req, res, url) {
|
|
|
182
191
|
return;
|
|
183
192
|
}
|
|
184
193
|
if (req.method === "GET" && url.pathname === "/api/queue") {
|
|
185
|
-
sendJson(res, 200, { queue: runtime.queue() });
|
|
194
|
+
sendJson(res, 200, { queue: runtime.queue(), paused: runtime.queuePaused() });
|
|
186
195
|
return;
|
|
187
196
|
}
|
|
188
197
|
if (req.method === "POST" && url.pathname === "/api/queue") {
|
|
189
198
|
const body = await readJsonBody(req);
|
|
190
|
-
sendJson(res, 200, { queue: runtime.queueAction(stringField(body, "action"), optionalStringField(body, "id")) });
|
|
199
|
+
sendJson(res, 200, { queue: runtime.queueAction(stringField(body, "action"), optionalStringField(body, "id")), paused: runtime.queuePaused() });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (req.method === "GET" && url.pathname === "/api/chat/history") {
|
|
203
|
+
sendJson(res, 200, { messages: await runtime.chatHistory(numberParam(url, "limit", 200)) });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (req.method === "DELETE" && url.pathname === "/api/chat/history") {
|
|
207
|
+
sendJson(res, 200, await runtime.clearChatHistory());
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (req.method === "GET" && url.pathname === "/api/activity") {
|
|
211
|
+
sendJson(res, 200, {
|
|
212
|
+
events: runtime.activity({
|
|
213
|
+
limit: numberParam(url, "limit", 100),
|
|
214
|
+
source: (url.searchParams.get("source") || "all"),
|
|
215
|
+
status: (url.searchParams.get("status") || "all"),
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
191
218
|
return;
|
|
192
219
|
}
|
|
193
220
|
if (req.method === "GET" && url.pathname === "/api/artifacts") {
|
|
@@ -219,12 +246,25 @@ async function handleApi(req, res, url) {
|
|
|
219
246
|
sendFile(res, artifact.localPath, artifact.name);
|
|
220
247
|
return;
|
|
221
248
|
}
|
|
249
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts/preview") {
|
|
250
|
+
const preview = await runtime.artifactPreview(requiredSearch(url, "turnId"), requiredSearch(url, "path"));
|
|
251
|
+
if (!preview) {
|
|
252
|
+
sendJson(res, 404, { error: "Artifact not found" });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
sendJson(res, 200, preview);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
222
258
|
if (req.method === "GET" && url.pathname === "/api/logs") {
|
|
223
259
|
sendJson(res, 200, await runtime.logs(url.searchParams.get("target") || "connector", numberParam(url, "lines", 120)));
|
|
224
260
|
return;
|
|
225
261
|
}
|
|
226
262
|
if (req.method === "GET" && url.pathname === "/api/diagnostics") {
|
|
227
|
-
sendJson(res, 200, await runtime.
|
|
263
|
+
sendJson(res, 200, await runtime.diagnostics());
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (req.method === "POST" && url.pathname === "/api/runtime/restart") {
|
|
267
|
+
sendJson(res, 202, runtime.restartConnector());
|
|
228
268
|
return;
|
|
229
269
|
}
|
|
230
270
|
sendJson(res, 404, { error: "Unknown endpoint" });
|
|
@@ -408,6 +448,19 @@ function optionalStringField(value, key) {
|
|
|
408
448
|
const field = value[key];
|
|
409
449
|
return typeof field === "string" && field.trim() ? field.trim() : undefined;
|
|
410
450
|
}
|
|
451
|
+
function optionalBooleanField(value, key) {
|
|
452
|
+
const field = value[key];
|
|
453
|
+
return typeof field === "boolean" ? field : undefined;
|
|
454
|
+
}
|
|
455
|
+
function parseAgentId(value) {
|
|
456
|
+
if (!value) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
if (!isAgentId(value)) {
|
|
460
|
+
throw new Error(`Invalid agent: ${value}`);
|
|
461
|
+
}
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
411
464
|
function objectRecord(value) {
|
|
412
465
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
413
466
|
return {};
|
|
@@ -528,6 +581,7 @@ function renderDashboardApp(options) {
|
|
|
528
581
|
<button data-page="chat">Chat</button>
|
|
529
582
|
<button data-page="sessions">Sessions</button>
|
|
530
583
|
<button data-page="queue">Queue</button>
|
|
584
|
+
<button data-page="activity">Activity</button>
|
|
531
585
|
<button data-page="artifacts">Artifacts</button>
|
|
532
586
|
<button data-page="settings">Settings</button>
|
|
533
587
|
<button data-page="logs">Logs</button>
|
|
@@ -561,9 +615,11 @@ function renderDashboardApp(options) {
|
|
|
561
615
|
<div class="panel chat-panel">
|
|
562
616
|
<div class="chat-toolbar">
|
|
563
617
|
<button id="newSessionBtn">New session</button>
|
|
618
|
+
<button id="clearChatBtn" class="secondary">Clear history</button>
|
|
564
619
|
<button id="abortBtn">Abort</button>
|
|
565
620
|
<button id="handbackBtn">Handback</button>
|
|
566
621
|
</div>
|
|
622
|
+
<div class="control-grid" id="sessionControls"></div>
|
|
567
623
|
<div id="messages" class="messages"></div>
|
|
568
624
|
<form id="promptForm" class="composer">
|
|
569
625
|
<div class="composer-fields">
|
|
@@ -595,21 +651,29 @@ function renderDashboardApp(options) {
|
|
|
595
651
|
|
|
596
652
|
<section class="page" id="page-queue">
|
|
597
653
|
<div class="panel">
|
|
598
|
-
<div class="row"><button data-queue="pause">Pause</button><button data-queue="resume">Resume</button><button data-queue="clear">Clear</button></div>
|
|
654
|
+
<div class="row"><button data-queue="pause">Pause</button><button data-queue="resume">Resume</button><button data-queue="clear" class="danger">Clear</button><span id="queueStatus"></span></div>
|
|
599
655
|
<div id="queueList" class="list"></div>
|
|
600
656
|
</div>
|
|
601
657
|
</section>
|
|
602
658
|
|
|
659
|
+
<section class="page" id="page-activity">
|
|
660
|
+
<div class="panel">
|
|
661
|
+
<div class="row"><select id="activitySource"><option value="all">All sources</option><option value="web">Web</option><option value="cli">CLI</option></select><select id="activityStatus"><option value="all">All statuses</option><option value="queued">Queued</option><option value="running">Running</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="aborted">Aborted</option><option value="info">Info</option></select><input id="activityLimit" type="number" value="100" min="1" max="500"><button id="loadActivityBtn">Load activity</button></div>
|
|
662
|
+
<div id="activityList" class="list"></div>
|
|
663
|
+
</div>
|
|
664
|
+
</section>
|
|
665
|
+
|
|
603
666
|
<section class="page" id="page-artifacts">
|
|
604
667
|
<div class="panel">
|
|
605
668
|
<div class="row"><button id="reloadArtifactsBtn">Reload artifacts</button></div>
|
|
606
669
|
<div id="artifactList" class="list"></div>
|
|
670
|
+
<div id="artifactPreview" class="preview"></div>
|
|
607
671
|
</div>
|
|
608
672
|
</section>
|
|
609
673
|
|
|
610
674
|
<section class="page" id="page-settings">
|
|
611
675
|
<div class="panel">
|
|
612
|
-
<div class="row"><button id="saveSettingsBtn">Save settings</button><span id="settingsStatus"></span></div>
|
|
676
|
+
<div class="row"><button id="saveSettingsBtn">Save settings</button><button id="restartBtn" class="secondary">Restart NordRelay</button><span id="settingsStatus"></span></div>
|
|
613
677
|
<div id="settingsTabs" class="tabs"></div>
|
|
614
678
|
<div id="settingsForm" class="settings-grid"></div>
|
|
615
679
|
</div>
|
|
@@ -617,13 +681,13 @@ function renderDashboardApp(options) {
|
|
|
617
681
|
|
|
618
682
|
<section class="page" id="page-logs">
|
|
619
683
|
<div class="panel">
|
|
620
|
-
<div class="row"><select id="logTarget"><option value="connector">Connector</option><option value="update">Update</option></select><input id="logLines" type="number" value="120" min="1" max="300"><button id="loadLogsBtn">Load logs</button></div>
|
|
684
|
+
<div class="row"><select id="logTarget"><option value="connector">Connector</option><option value="update">Update</option></select><select id="logLevel"><option value="all">All levels</option><option value="ERROR">Error</option><option value="WARN">Warn</option><option value="INFO">Info</option></select><input id="logSearch" placeholder="Search logs"><input id="logLines" type="number" value="120" min="1" max="300"><label class="checkbox"><input id="logAutoRefresh" type="checkbox"> Auto</label><button id="loadLogsBtn">Load logs</button><button id="downloadLogsBtn" class="secondary">Download</button></div>
|
|
621
685
|
<pre id="logs"></pre>
|
|
622
686
|
</div>
|
|
623
687
|
</section>
|
|
624
688
|
|
|
625
689
|
<section class="page" id="page-diagnostics">
|
|
626
|
-
<div class="panel"><
|
|
690
|
+
<div class="panel"><div id="diagnostics" class="list"></div></div>
|
|
627
691
|
</section>
|
|
628
692
|
|
|
629
693
|
<footer>
|
|
@@ -633,6 +697,21 @@ function renderDashboardApp(options) {
|
|
|
633
697
|
</footer>
|
|
634
698
|
</main>
|
|
635
699
|
</div>
|
|
700
|
+
<dialog id="newSessionDialog">
|
|
701
|
+
<form method="dialog" id="newSessionForm">
|
|
702
|
+
<h2>New Session</h2>
|
|
703
|
+
<div class="form-grid">
|
|
704
|
+
<label>Agent<select id="newAgent"></select></label>
|
|
705
|
+
<label>Workspace<input id="newWorkspace" list="workspaceOptions" placeholder="Current workspace"></label>
|
|
706
|
+
<label>Model<select id="newModel"></select></label>
|
|
707
|
+
<label id="newReasoningWrap">Reasoning<select id="newReasoning"></select></label>
|
|
708
|
+
<label id="newLaunchWrap">Launch profile<select id="newLaunch"></select></label>
|
|
709
|
+
<label id="newFastWrap" class="checkbox"><input id="newFast" type="checkbox"> Fast mode</label>
|
|
710
|
+
</div>
|
|
711
|
+
<datalist id="workspaceOptions"></datalist>
|
|
712
|
+
<div class="row dialog-actions"><button type="button" id="cancelSessionBtn" class="secondary">Cancel</button><button id="createSessionBtn" value="default">Create session</button></div>
|
|
713
|
+
</form>
|
|
714
|
+
</dialog>
|
|
636
715
|
<div id="toast"></div>
|
|
637
716
|
<script>${dashboardJs()}</script>
|
|
638
717
|
</body>
|
|
@@ -642,13 +721,13 @@ function dashboardCss() {
|
|
|
642
721
|
return `
|
|
643
722
|
:root{color-scheme:light;--bg:#f4f6f2;--surface:#ffffff;--surface-soft:#fbfcf8;--text:#18201b;--muted:#5d675f;--border:#dce3d9;--border-soft:#e7ede4;--sidebar:#17251d;--sidebar-text:#f4f8f2;--sidebar-muted:#aebcaf;--accent:#235c42;--accent-strong:#17452f;--accent-soft:#dff5e8;--warn:#fff7da;--danger:#9b1c1c;--pre:#111812;--pre-text:#f3f7ef;--shadow:0 8px 24px rgba(24,32,27,.04);--link:#1d6a4c}
|
|
644
723
|
:root[data-theme="dark"]{color-scheme:dark;--bg:#101411;--surface:#171d19;--surface-soft:#1d251f;--text:#edf4ee;--muted:#a7b3aa;--border:#2d3830;--border-soft:#263128;--sidebar:#0c120f;--sidebar-text:#edf7ef;--sidebar-muted:#8da091;--accent:#4fa876;--accent-strong:#64bd89;--accent-soft:#173d2a;--warn:#3b3216;--danger:#cc4b4b;--pre:#070a08;--pre-text:#e8f1ea;--shadow:0 10px 28px rgba(0,0,0,.22);--link:#75c99a}
|
|
645
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif}.app{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.sidebar{background:var(--sidebar);color:var(--sidebar-text);padding:18px;display:flex;flex-direction:column;gap:22px}.brand{display:flex;align-items:center;gap:12px}.mark{display:grid;place-items:center;width:38px;height:38px;border-radius:8px;background:#d7ffe5;color:#173d29;font-weight:800}.brand small{display:block;color:var(--sidebar-muted)}nav{display:flex;flex-direction:column;gap:6px}nav button,.menu{border:0;border-radius:6px;padding:10px 12px;background:transparent;color:inherit;text-align:left;font:inherit;cursor:pointer}nav button.active,nav button:hover{background:color-mix(in srgb,var(--accent) 35%,transparent)}main{min-width:0;display:flex;flex-direction:column}header{position:sticky;top:0;z-index:5;display:flex;justify-content:space-between;gap:16px;align-items:center;padding:16px 22px;background:color-mix(in srgb,var(--surface) 92%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--border)}h1{font-size:24px;margin:0}h2{font-size:16px;margin:0 0 12px}p{margin:4px 0 0;color:var(--muted)}a{color:var(--link)}.header-actions,.row,.chat-toolbar,.attachment-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.menu{display:none;background:var(--surface-soft);color:var(--text)}.page{display:none;padding:22px}.page.active{display:block}.stack{display:flex;flex-direction:column;gap:16px}.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:16px}.metric,.panel{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px;box-shadow:var(--shadow)}.metric .label{font-size:12px;text-transform:uppercase;color:var(--muted)}.metric .value{font-size:22px;font-weight:750;margin-top:4px;overflow:hidden;text-overflow:ellipsis}button,select,input,textarea{border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font:inherit}button{height:36px;padding:0 12px;background:var(--accent);color:white;border-color:var(--accent);cursor:pointer}button:hover{background:var(--accent-strong)}button.secondary{background:var(--surface);color:var(--text)}input,select{height:36px;padding:0 10px}textarea{width:100%;padding:10px;resize:vertical}.chat-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:16px}.chat-panel{min-height:calc(100vh - 170px);display:flex;flex-direction:column}.messages{flex:1;min-height:360px;overflow:auto;border:1px solid var(--border-soft);border-radius:8px;padding:12px;background:var(--surface-soft)}.message{margin:0 0 12px;padding:10px 12px;border-radius:8px;max-width:92%;white-space:pre-wrap;word-break:break-word}.message.user{margin-left:auto;background:var(--accent-soft)}.message.agent{background:color-mix(in srgb,var(--surface-soft) 80%,var(--border))}.message.system{background:var(--warn)}.composer{display:grid;grid-template-columns:1fr auto;gap:10px;margin-top:12px}.composer-fields{min-width:0}.composer button{height:auto;min-width:90px}.attachment-row{margin-top:8px;color:var(--muted);font-size:13px}.file-button{display:inline-flex;align-items:center;height:34px;padding:0 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);cursor:pointer}input[type=file]{display:none}.sessions-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.sessions-toolbar .search-row{flex:1 1 320px}.sessions-toolbar .attach-row{flex:1 1 360px;justify-content:flex-end;margin-left:auto}.sessions-toolbar input{min-width:220px}.copy-id{height:auto;padding:0;border:0;background:transparent;color:var(--link);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}.copy-id:hover{background:transparent;text-decoration:underline}.tool-stream{display:flex;flex-direction:column;gap:8px}.tool{border:1px solid var(--border-soft);border-radius:6px;padding:8px;background:var(--surface-soft)}.list{display:flex;flex-direction:column;gap:8px;margin-top:12px}.item{border:1px solid var(--border-soft);border-radius:8px;padding:12px;background:var(--surface-soft)}.item strong{display:block;overflow-wrap:anywhere}.item small{display:block;color:var(--muted);overflow-wrap:anywhere}.settings-grid{display:block}.setting{border:1px solid var(--border-soft);border-radius:8px;padding:12px;margin-bottom:10px;background:var(--surface-soft)}.setting label{display:block;font-size:13px;font-weight:700;margin-bottom:6px}.setting small{display:block;color:var(--muted);margin-top:6px}.setting input,.setting textarea,.setting select{width:100%}.tabs{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.tabs button{background:var(--surface);color:var(--text);border-color:var(--border);height:34px}.tabs button.active{background:var(--accent);color:white;border-color:var(--accent)}.pager{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:12px;color:var(--muted)}.pager-actions{display:flex;gap:8px}.pager button:disabled{opacity:.45;cursor:not-allowed}pre{white-space:pre-wrap;word-break:break-word;background:var(--pre);color:var(--pre-text);border-radius:8px;padding:14px;overflow:auto}footer{margin-top:auto;display:flex;gap:18px;flex-wrap:wrap;padding:14px 22px;border-top:1px solid var(--border);color:var(--muted);background:var(--surface)}#toast{position:fixed;right:18px;bottom:18px;display:none;background:var(--accent);color:white;border-radius:8px;padding:12px 14px;max-width:360px}.danger{background:var(--danger);border-color:var(--danger);color:white}@media(max-width:860px){.app{display:block}.sidebar{position:fixed;inset:0 auto 0 0;width:270px;transform:translateX(-100%);transition:.18s transform;z-index:20}.sidebar.open{transform:translateX(0)}.menu{display:inline-block}.header-actions{justify-content:flex-end}.page{padding:14px}.chat-layout{grid-template-columns:1fr}.composer{grid-template-columns:1fr}.composer button{height:40px}.side-panel{order:-1}header{align-items:flex-start}.metrics{grid-template-columns:1fr 1fr}}@media(max-width:560px){.metrics{grid-template-columns:1fr}.row{align-items:stretch}.row>*{width:100%}header{display:grid;grid-template-columns:auto 1fr}.header-actions{grid-column:1/3}.message{max-width:100%}.pager{align-items:stretch}.pager-actions,.pager button{width:100%}.attachment-row>*,.sessions-toolbar,.sessions-toolbar .row,.sessions-toolbar input,.sessions-toolbar button{width:100%}.sessions-toolbar .attach-row{margin-left:0;justify-content:stretch}}
|
|
724
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif}.app{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.sidebar{background:var(--sidebar);color:var(--sidebar-text);padding:18px;display:flex;flex-direction:column;gap:22px}.brand{display:flex;align-items:center;gap:12px}.mark{display:grid;place-items:center;width:38px;height:38px;border-radius:8px;background:#d7ffe5;color:#173d29;font-weight:800}.brand small{display:block;color:var(--sidebar-muted)}nav{display:flex;flex-direction:column;gap:6px}nav button,.menu{border:0;border-radius:6px;padding:10px 12px;background:transparent;color:inherit;text-align:left;font:inherit;cursor:pointer}nav button.active,nav button:hover{background:color-mix(in srgb,var(--accent) 35%,transparent)}main{min-width:0;display:flex;flex-direction:column}header{position:sticky;top:0;z-index:5;display:flex;justify-content:space-between;gap:16px;align-items:center;padding:16px 22px;background:color-mix(in srgb,var(--surface) 92%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--border)}h1{font-size:24px;margin:0}h2{font-size:16px;margin:0 0 12px}p{margin:4px 0 0;color:var(--muted)}a{color:var(--link)}.header-actions,.row,.chat-toolbar,.attachment-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.menu{display:none;background:var(--surface-soft);color:var(--text)}.page{display:none;padding:22px}.page.active{display:block}.stack{display:flex;flex-direction:column;gap:16px}.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:16px}.metric,.panel{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px;box-shadow:var(--shadow)}.metric .label{font-size:12px;text-transform:uppercase;color:var(--muted)}.metric .value{font-size:22px;font-weight:750;margin-top:4px;overflow:hidden;text-overflow:ellipsis}button,select,input,textarea{border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font:inherit}button{height:36px;padding:0 12px;background:var(--accent);color:white;border-color:var(--accent);cursor:pointer}button:hover{background:var(--accent-strong)}button.secondary{background:var(--surface);color:var(--text)}input,select{height:36px;padding:0 10px}textarea{width:100%;padding:10px;resize:vertical}.chat-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:16px;align-items:start}.chat-panel{min-height:calc(100vh - 170px);display:flex;flex-direction:column}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin:12px 0}.control-grid label,.form-grid label{display:grid;gap:5px;font-size:12px;color:var(--muted)}.messages{flex:1;min-height:360px;overflow:auto;border:1px solid var(--border-soft);border-radius:8px;padding:12px;background:var(--surface-soft)}.message{margin:0 0 12px;padding:10px 12px;border-radius:8px;max-width:92%;white-space:pre-wrap;word-break:break-word}.message.user{margin-left:auto;background:var(--accent-soft)}.message.agent{background:color-mix(in srgb,var(--surface-soft) 80%,var(--border))}.message.system{background:var(--warn)}.composer{display:grid;grid-template-columns:1fr auto;gap:10px;margin-top:12px}.composer-fields{min-width:0}.composer button{height:auto;min-width:90px}.attachment-row{margin-top:8px;color:var(--muted);font-size:13px}.file-button{display:inline-flex;align-items:center;height:34px;padding:0 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);cursor:pointer}input[type=file]{display:none}.sessions-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.sessions-toolbar .search-row{flex:1 1 320px}.sessions-toolbar .attach-row{flex:1 1 360px;justify-content:flex-end;margin-left:auto}.sessions-toolbar input{min-width:220px}.copy-id{height:auto;padding:0;border:0;background:transparent;color:var(--link);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}.copy-id:hover{background:transparent;text-decoration:underline}.side-panel{max-height:calc(100vh - 126px);display:flex;flex-direction:column}.tool-stream{display:flex;flex-direction:column;gap:8px;overflow:auto;max-height:calc(100vh - 190px);padding-right:4px}.tool{border:1px solid var(--border-soft);border-radius:6px;padding:8px;background:var(--surface-soft);white-space:pre-wrap;word-break:break-word}.list{display:flex;flex-direction:column;gap:8px;margin-top:12px}.item{border:1px solid var(--border-soft);border-radius:8px;padding:12px;background:var(--surface-soft)}.item strong{display:block;overflow-wrap:anywhere}.item small{display:block;color:var(--muted);overflow-wrap:anywhere}.queue-item{cursor:grab}.queue-item.dragging{opacity:.55}.badge{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;padding:2px 8px;color:var(--muted);font-size:12px}.preview{margin-top:12px}.preview img{max-width:100%;border:1px solid var(--border);border-radius:8px;background:var(--surface)}.settings-grid{display:block}.setting{border:1px solid var(--border-soft);border-radius:8px;padding:12px;margin-bottom:10px;background:var(--surface-soft)}.setting label{display:block;font-size:13px;font-weight:700;margin-bottom:6px}.setting small{display:block;color:var(--muted);margin-top:6px}.setting input,.setting textarea,.setting select{width:100%}.setting-error{color:var(--danger);font-size:12px;margin-top:6px}.checkbox{display:inline-flex!important;grid-template-columns:auto 1fr!important;align-items:center;gap:8px}.checkbox input{height:auto;width:auto}.tabs{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.tabs button{background:var(--surface);color:var(--text);border-color:var(--border);height:34px}.tabs button.active{background:var(--accent);color:white;border-color:var(--accent)}.pager{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:12px;color:var(--muted)}.pager-actions{display:flex;gap:8px}.pager button:disabled{opacity:.45;cursor:not-allowed}pre{white-space:pre-wrap;word-break:break-word;background:var(--pre);color:var(--pre-text);border-radius:8px;padding:14px;overflow:auto}footer{margin-top:auto;display:flex;gap:18px;flex-wrap:wrap;padding:14px 22px;border-top:1px solid var(--border);color:var(--muted);background:var(--surface)}dialog{border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text);width:min(720px,calc(100vw - 28px));padding:18px;box-shadow:0 18px 70px rgba(0,0,0,.22)}dialog::backdrop{background:rgba(0,0,0,.35)}.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}.dialog-actions{justify-content:flex-end;margin-top:16px}#toast{position:fixed;right:18px;bottom:18px;display:none;background:var(--accent);color:white;border-radius:8px;padding:12px 14px;max-width:360px}.danger{background:var(--danger);border-color:var(--danger);color:white}@media(max-width:860px){.app{display:block}.sidebar{position:fixed;inset:0 auto 0 0;width:270px;transform:translateX(-100%);transition:.18s transform;z-index:20}.sidebar.open{transform:translateX(0)}.menu{display:inline-block}.header-actions{justify-content:flex-end}.page{padding:14px}.chat-layout{grid-template-columns:1fr}.composer{grid-template-columns:1fr}.composer button{height:40px}.side-panel{order:-1;max-height:360px}.tool-stream{max-height:300px}header{align-items:flex-start}.metrics{grid-template-columns:1fr 1fr}}@media(max-width:560px){.metrics{grid-template-columns:1fr}.row{align-items:stretch}.row>*{width:100%}header{display:grid;grid-template-columns:auto 1fr}.header-actions{grid-column:1/3}.message{max-width:100%}.pager{align-items:stretch}.pager-actions,.pager button{width:100%}.attachment-row>*,.sessions-toolbar,.sessions-toolbar .row,.sessions-toolbar input,.sessions-toolbar button{width:100%}.sessions-toolbar .attach-row{margin-left:0;justify-content:stretch}}
|
|
646
725
|
`;
|
|
647
726
|
}
|
|
648
727
|
function dashboardJs() {
|
|
649
728
|
return `
|
|
650
729
|
const token = localStorage.getItem('nordrelayDashboardToken') || '';
|
|
651
|
-
const state = { snapshot:null, settings:[], currentPage:'overview',
|
|
730
|
+
const state = { snapshot:null, controls:null, enabledAgents:[], settings:[], currentPage:'overview', settingsGroup:null, logsPlain:'', logTimer:null, toastTimer:null, cliStatusActive:false };
|
|
652
731
|
const authHeaders = () => token ? { authorization: 'Bearer ' + token } : {};
|
|
653
732
|
async function api(path, options={}) {
|
|
654
733
|
const headers = { ...(options.body ? {'content-type':'application/json'} : {}), ...authHeaders(), ...(options.headers||{}) };
|
|
@@ -659,16 +738,21 @@ async function api(path, options={}) {
|
|
|
659
738
|
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
660
739
|
return data;
|
|
661
740
|
}
|
|
662
|
-
function toast(msg){const el=document.getElementById('toast');el.textContent=msg;el.style.display='block';setTimeout(()=>el.style.display='none',3500)}
|
|
741
|
+
function toast(msg,options={}){const el=document.getElementById('toast');el.textContent=msg;el.style.display='block';if(state.toastTimer)clearTimeout(state.toastTimer);state.toastTimer=null;if(!options.sticky){state.toastTimer=setTimeout(()=>{el.style.display='none';state.toastTimer=null},options.duration||3500)}}
|
|
663
742
|
function esc(s){return String(s??'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]))}
|
|
664
743
|
function attr(s){return esc(s).replace(/"/g,'"')}
|
|
744
|
+
function cssEscape(s){return window.CSS&&CSS.escape?CSS.escape(s):String(s).replace(/[^a-zA-Z0-9_-]/g,'\\\\$&')}
|
|
665
745
|
function short(s,max=250){const text=String(s??'');return text.length>max?text.slice(0,max-1)+'...':text}
|
|
666
746
|
async function copyText(text){if(!text)return;try{await navigator.clipboard.writeText(text)}catch{const area=document.createElement('textarea');area.value=text;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();document.execCommand('copy');area.remove()}toast('Thread ID copied')}
|
|
667
747
|
function fmtDate(s){return s?new Date(s).toLocaleString(): '-'}
|
|
748
|
+
function fmtDuration(ms){if(!ms&&ms!==0)return '-';const sec=Math.round(ms/1000);if(sec<60)return sec+'s';return Math.floor(sec/60)+'m '+(sec%60)+'s'}
|
|
668
749
|
function fmtBytes(n){if(n<1024)return n+' B';if(n<1048576)return (n/1024).toFixed(1).replace(/\\.0$/,'')+' KB';return (n/1048576).toFixed(1).replace(/\\.0$/,'')+' MB'}
|
|
750
|
+
function fmtAge(ms){const sec=Math.max(0,Math.floor(ms/1000));if(sec<60)return sec+'s ago';const min=Math.floor(sec/60);if(min<60)return min+'m ago';return Math.floor(min/60)+'h ago'}
|
|
751
|
+
function isCliRunningStatus(msg){return /^Codex CLI running\\b/.test(String(msg||''))}
|
|
752
|
+
function isCliDoneStatus(msg){return /^Codex CLI task\\b/.test(String(msg||''))}
|
|
669
753
|
function applyTheme(theme){document.documentElement.dataset.theme=theme;localStorage.setItem('nordrelayTheme',theme);document.getElementById('themeBtn').textContent=theme==='dark'?'Light':'Dark'}
|
|
670
754
|
function toggleTheme(){applyTheme(document.documentElement.dataset.theme==='dark'?'light':'dark')}
|
|
671
|
-
function page(name){state.currentPage=name;document.querySelectorAll('nav button').forEach(b=>b.classList.toggle('active',b.dataset.page===name));document.querySelectorAll('.page').forEach(p=>p.classList.toggle('active',p.id==='page-'+name));document.getElementById('pageTitle').textContent=name[0].toUpperCase()+name.slice(1);document.getElementById('sidebar').classList.remove('open'); if(name==='sessions') loadSessions(); if(name==='settings') loadSettings(); if(name==='logs') loadLogs(); if(name==='diagnostics') loadDiagnostics(); if(name==='artifacts') loadArtifacts();}
|
|
755
|
+
function page(name){state.currentPage=name;document.querySelectorAll('nav button').forEach(b=>b.classList.toggle('active',b.dataset.page===name));document.querySelectorAll('.page').forEach(p=>p.classList.toggle('active',p.id==='page-'+name));document.getElementById('pageTitle').textContent=name[0].toUpperCase()+name.slice(1);document.getElementById('sidebar').classList.remove('open'); if(name==='sessions') loadSessions(); if(name==='settings') loadSettings(); if(name==='logs') loadLogs(); if(name==='diagnostics') loadDiagnostics(); if(name==='artifacts') loadArtifacts(); if(name==='activity') loadActivity();}
|
|
672
756
|
document.querySelectorAll('nav button').forEach(b=>b.onclick=()=>page(b.dataset.page));
|
|
673
757
|
document.getElementById('menuBtn').onclick=()=>document.getElementById('sidebar').classList.toggle('open');
|
|
674
758
|
document.getElementById('refreshBtn').onclick=()=>loadBootstrap();
|
|
@@ -697,34 +781,58 @@ const sessionsPager=createPaginator('sessionsPager',()=>loadSessions(false),50);
|
|
|
697
781
|
async function loadBootstrap(){
|
|
698
782
|
const data = await api('/api/bootstrap');
|
|
699
783
|
state.snapshot = data.status.snapshot;
|
|
784
|
+
state.controls = data.controls;
|
|
785
|
+
state.enabledAgents = data.enabledAgents || [];
|
|
700
786
|
renderSnapshot(state.snapshot);
|
|
787
|
+
renderSessionControls();
|
|
788
|
+
populateNewSessionForm(data.enabledAgents);
|
|
701
789
|
renderAdapters(data.channels, data.agentAdapters);
|
|
702
790
|
document.getElementById('footerVersion').textContent='NordRelay '+(data.status.health?.version || '');
|
|
703
791
|
document.getElementById('footerHealth').textContent='Health: '+(data.status.health?.state?.status || 'unknown');
|
|
704
792
|
const agentSelect=document.getElementById('agentSelect');
|
|
705
793
|
agentSelect.innerHTML=data.enabledAgents.map(a=>'<option value="'+a+'">'+a+'</option>').join('');
|
|
706
794
|
agentSelect.value=state.snapshot.session.agentId;
|
|
707
|
-
agentSelect.onchange=async()=>{await api('/api/agent',{method:'POST',body:JSON.stringify({agentId:agentSelect.value})});toast('Agent switched');loadBootstrap()};
|
|
795
|
+
agentSelect.onchange=()=>safe(async()=>{await api('/api/agent',{method:'POST',body:JSON.stringify({agentId:agentSelect.value})});toast('Agent switched');await loadBootstrap();await loadChatHistory()});
|
|
708
796
|
}
|
|
709
797
|
function renderSnapshot(s){
|
|
710
798
|
document.getElementById('sessionLine').textContent=(s.session.agentLabel||'Agent')+' / '+(s.session.model||'default')+' / '+(s.session.threadId||'not started');
|
|
711
799
|
document.getElementById('sessionText').textContent=s.sessionText||'';
|
|
712
800
|
document.getElementById('metrics').innerHTML=[
|
|
713
|
-
['Status',s.processing?'working':'idle'],['Agent',s.session.agentLabel],['Queue',s.queue.length],['Workspace',s.session.workspace],['Thread',s.session.threadId||'not started'],['Reasoning',s.session.reasoningEffort||'default']
|
|
801
|
+
['Status',s.processing?'working':'idle'],['Agent',s.session.agentLabel],['Queue',s.queue.length],['Workspace',s.session.workspace],['Thread',s.session.threadId||'not started'],['Reasoning',s.session.reasoningEffort||'default'],['Fast',s.session.fastMode?'on':'off']
|
|
714
802
|
].map(([k,v])=>'<div class="metric"><div class="label">'+esc(k)+'</div><div class="value">'+esc(v)+'</div></div>').join('');
|
|
715
|
-
renderQueue(s.queue);
|
|
803
|
+
renderQueue(s.queue,s.queuePaused);
|
|
804
|
+
}
|
|
805
|
+
function renderSessionControls(){
|
|
806
|
+
const c=state.controls||{};const s=state.snapshot?.session||{};const caps=c.capabilities||{};
|
|
807
|
+
const modelOptions=['<option value="">Default</option>'].concat((c.models||[]).map(m=>'<option value="'+attr(m.slug)+'" '+(m.slug===s.model?'selected':'')+'>'+esc(m.displayName||m.slug)+'</option>')).join('');
|
|
808
|
+
const reasoningOptions=(c.reasoningOptions||[]).map(v=>'<option value="'+attr(v)+'" '+(v===s.reasoningEffort?'selected':'')+'>'+esc(v)+'</option>').join('');
|
|
809
|
+
const launchOptions=(c.launchProfiles||[]).map(p=>'<option value="'+attr(p.id)+'" '+(p.id===(s.nextLaunchProfileId||s.launchProfileId)?'selected':'')+'>'+esc(p.label+' - '+p.behavior+(p.unsafe?' - unsafe':''))+'</option>').join('');
|
|
810
|
+
document.getElementById('sessionControls').innerHTML=[
|
|
811
|
+
caps.modelSelection?'<label>Model<select id="controlModel">'+modelOptions+'</select></label>':'',
|
|
812
|
+
caps.reasoningSelection?'<label>'+esc(c.reasoningLabel||'Reasoning')+'<select id="controlReasoning">'+reasoningOptions+'</select></label>':'',
|
|
813
|
+
caps.launchProfiles?'<label>Launch<select id="controlLaunch">'+launchOptions+'</select></label>':'',
|
|
814
|
+
caps.fastMode?'<label class="checkbox"><input id="controlFast" type="checkbox" '+(s.fastMode?'checked':'')+'> Fast mode</label>':''
|
|
815
|
+
].join('');
|
|
816
|
+
const model=document.getElementById('controlModel'); if(model) model.onchange=()=>safe(async()=>{if(model.value){await api('/api/session/model',{method:'POST',body:JSON.stringify({model:model.value})});toast('Model updated');loadBootstrap()}});
|
|
817
|
+
const reasoning=document.getElementById('controlReasoning'); if(reasoning) reasoning.onchange=()=>safe(async()=>{await api('/api/session/reasoning',{method:'POST',body:JSON.stringify({reasoning:reasoning.value})});toast((c.reasoningLabel||'Reasoning')+' updated');loadBootstrap()});
|
|
818
|
+
const launch=document.getElementById('controlLaunch'); if(launch) launch.onchange=()=>safe(async()=>{await api('/api/session/launch',{method:'POST',body:JSON.stringify({profileId:launch.value})});toast('Launch profile updated');loadBootstrap()});
|
|
819
|
+
const fast=document.getElementById('controlFast'); if(fast) fast.onchange=()=>safe(async()=>{await api('/api/session/fast',{method:'POST',body:JSON.stringify({enabled:fast.checked})});toast('Fast mode updated');loadBootstrap()});
|
|
716
820
|
}
|
|
717
821
|
function renderAdapters(channels, agents){
|
|
718
822
|
document.getElementById('adapters').innerHTML='<div class="list">'+[...channels.map(c=>'<div class="item"><strong>'+esc(c.label)+' - '+esc(c.status)+'</strong><small>'+esc(c.capabilities.join(', '))+'</small></div>'),...agents.map(a=>'<div class="item"><strong>'+esc(a.label)+' - '+esc(a.status)+'</strong><small>'+esc(a.notes||a.envFlag||'available')+'</small></div>')].join('')+'</div>';
|
|
719
823
|
}
|
|
720
824
|
function appendMessage(cls,text){const box=document.getElementById('messages');const div=document.createElement('div');div.className='message '+cls;div.textContent=text;box.appendChild(div);box.scrollTop=box.scrollHeight;return div}
|
|
825
|
+
function renderChatMessages(messages){const box=document.getElementById('messages');box.innerHTML=(messages||[]).map(m=>'<div class="message '+esc(m.role)+'"><small>'+esc((m.source||'web')+' / '+fmtDate(m.timestamp))+'</small>\\n'+esc(m.text)+'</div>').join('');box.scrollTop=box.scrollHeight}
|
|
826
|
+
async function loadChatHistory(){const data=await api('/api/chat/history');renderChatMessages(data.messages||[])}
|
|
721
827
|
let currentAgentMessage=null;
|
|
722
828
|
function connectEvents(){
|
|
723
829
|
const qs = token ? '?token='+encodeURIComponent(token) : '';
|
|
724
830
|
const events = new EventSource('/api/events'+qs);
|
|
725
|
-
events.addEventListener('snapshot', e=>{const d=JSON.parse(e.data).data;state.snapshot=d;renderSnapshot(d)});
|
|
726
|
-
events.addEventListener('
|
|
727
|
-
events.addEventListener('
|
|
831
|
+
events.addEventListener('snapshot', e=>{const d=JSON.parse(e.data).data;state.snapshot=d;renderSnapshot(d);renderSessionControls()});
|
|
832
|
+
events.addEventListener('chat_history', e=>renderChatMessages(JSON.parse(e.data).messages||[]));
|
|
833
|
+
events.addEventListener('activity_update', e=>renderActivity(JSON.parse(e.data).events||[]));
|
|
834
|
+
events.addEventListener('session_update', e=>{loadBootstrap();loadChatHistory()});
|
|
835
|
+
events.addEventListener('queue_update', e=>{const d=JSON.parse(e.data);renderQueue(d.queue,d.paused)});
|
|
728
836
|
events.addEventListener('turn_start', e=>{const d=JSON.parse(e.data);appendMessage('user',d.prompt);currentAgentMessage=appendMessage('agent','')});
|
|
729
837
|
events.addEventListener('text_delta', e=>{const d=JSON.parse(e.data);if(!currentAgentMessage)currentAgentMessage=appendMessage('agent','');currentAgentMessage.textContent+=d.delta;currentAgentMessage.scrollIntoView({block:'end'})});
|
|
730
838
|
events.addEventListener('tool_start', e=>{const d=JSON.parse(e.data);tool('tool','Started '+d.toolName)});
|
|
@@ -733,32 +841,50 @@ function connectEvents(){
|
|
|
733
841
|
events.addEventListener('todo_update', e=>{const d=JSON.parse(e.data);tool('tool','Plan:\\n'+d.items.map(i=>(i.completed?'[x] ':'[ ] ')+i.text).join('\\n'))});
|
|
734
842
|
events.addEventListener('turn_error', e=>{const d=JSON.parse(e.data);appendMessage('system','Error: '+d.error);currentAgentMessage=null});
|
|
735
843
|
events.addEventListener('turn_complete', ()=>{currentAgentMessage=null;loadBootstrap()});
|
|
736
|
-
events.addEventListener('status', e=>{const d=JSON.parse(e.data);
|
|
844
|
+
events.addEventListener('status', e=>{const d=JSON.parse(e.data);const msg=d.message||'';if(isCliRunningStatus(msg)){state.cliStatusActive=true;toast(msg,{sticky:true});return}if(isCliDoneStatus(msg))state.cliStatusActive=false;toast(msg)});
|
|
737
845
|
events.onerror=()=>{};
|
|
738
846
|
}
|
|
739
|
-
function
|
|
847
|
+
function updateToolAgeTitles(){document.querySelectorAll('.tool[data-created-at]').forEach(el=>{const created=Number(el.dataset.createdAt||Date.now());el.title='Updated '+fmtAge(Date.now()-created)})}
|
|
848
|
+
function tool(cls,text){const div=document.createElement('div');div.className='tool '+(cls==='danger'?'danger':'');div.dataset.createdAt=String(Date.now());div.textContent=text;document.getElementById('toolStream').prepend(div);updateToolAgeTitles()}
|
|
849
|
+
setInterval(updateToolAgeTitles,30000);
|
|
740
850
|
let selectedFiles=[];
|
|
741
851
|
function renderSelectedFiles(){const summary=document.getElementById('fileSummary');if(selectedFiles.length===0){summary.textContent='No files selected';return}const names=selectedFiles.slice(0,3).map(f=>f.name || 'file').join(', ');const more=selectedFiles.length>3?' +'+(selectedFiles.length-3)+' more':'';const bytes=selectedFiles.reduce((sum,file)=>sum+file.size,0);summary.textContent=names+more+' ('+fmtBytes(bytes)+')'}
|
|
742
852
|
async function filePayload(file){return {name:file.name || 'upload',mimeType:file.type || 'application/octet-stream',dataBase64:await fileToBase64(file)}}
|
|
743
853
|
async function fileToBase64(file){const buffer=await file.arrayBuffer();const bytes=new Uint8Array(buffer);let binary='';const chunk=0x8000;for(let i=0;i<bytes.length;i+=chunk){binary+=String.fromCharCode(...bytes.subarray(i,i+chunk))}return btoa(binary)}
|
|
744
854
|
document.getElementById('fileInput').onchange=e=>{selectedFiles=Array.from(e.target.files||[]);renderSelectedFiles()};
|
|
745
855
|
document.getElementById('clearFilesBtn').onclick=()=>{selectedFiles=[];document.getElementById('fileInput').value='';renderSelectedFiles()};
|
|
746
|
-
document.getElementById('promptForm').onsubmit=
|
|
747
|
-
document.getElementById('newSessionBtn').onclick=
|
|
748
|
-
document.getElementById('
|
|
749
|
-
document.getElementById('
|
|
856
|
+
document.getElementById('promptForm').onsubmit=e=>safe(async()=>{e.preventDefault();const input=document.getElementById('promptInput');const text=input.value.trim();if(!text&&selectedFiles.length===0)return;const files=selectedFiles;input.value='';selectedFiles=[];document.getElementById('fileInput').value='';renderSelectedFiles();const payloadFiles=files.length?await Promise.all(files.map(filePayload)):[];const r=files.length?await api('/api/prompt/upload',{method:'POST',body:JSON.stringify({text,files:payloadFiles})}):await api('/api/prompt',{method:'POST',body:JSON.stringify({text})});if(r.transcribeOnly)appendMessage('system','Transcribed audio:\\n'+(r.transcript||'(empty)'));else if(r.queued)appendMessage('system','Queued prompt '+r.queueId)},e);
|
|
857
|
+
document.getElementById('newSessionBtn').onclick=()=>openNewSessionDialog();
|
|
858
|
+
document.getElementById('clearChatBtn').onclick=()=>safe(async()=>{if(confirm('Clear chat history for the current thread?')){const r=await api('/api/chat/history',{method:'DELETE'});renderChatMessages(r.messages||[]);toast('Removed '+r.removed+' messages')}});
|
|
859
|
+
document.getElementById('abortBtn').onclick=()=>safe(async()=>{await api('/api/abort',{method:'POST'});toast('Abort sent')});
|
|
860
|
+
document.getElementById('handbackBtn').onclick=()=>safe(async()=>{const r=await api('/api/handback',{method:'POST'});appendMessage('system','Handback command:\\n'+(r.command||'No command available'))});
|
|
861
|
+
function populateNewSessionForm(agents){const c=state.controls||{};const s=state.snapshot?.session||{};document.getElementById('newAgent').innerHTML=(agents||[]).map(a=>'<option value="'+attr(a)+'" '+(a===s.agentId?'selected':'')+'>'+esc(a)+'</option>').join('');document.getElementById('newWorkspace').value=s.workspace||'';document.getElementById('workspaceOptions').innerHTML=(c.workspaces||[]).map(w=>'<option value="'+attr(w)+'"></option>').join('');document.getElementById('newModel').innerHTML='<option value="">Default</option>'+((c.models||[]).map(m=>'<option value="'+attr(m.slug)+'">'+esc(m.displayName||m.slug)+'</option>').join(''));document.getElementById('newReasoning').innerHTML='<option value="">Default</option>'+((c.reasoningOptions||[]).map(v=>'<option value="'+attr(v)+'">'+esc(v)+'</option>').join(''));document.getElementById('newLaunch').innerHTML='<option value="">Default</option>'+((c.launchProfiles||[]).map(p=>'<option value="'+attr(p.id)+'">'+esc(p.label+' - '+p.behavior)+'</option>').join(''));document.getElementById('newFast').checked=Boolean(s.fastMode);document.getElementById('newLaunchWrap').style.display=(c.capabilities&&c.capabilities.launchProfiles)?'grid':'none';document.getElementById('newFastWrap').style.display=(c.capabilities&&c.capabilities.fastMode)?'inline-flex':'none'}
|
|
862
|
+
function openNewSessionDialog(){populateNewSessionForm(state.enabledAgents);document.getElementById('newSessionDialog').showModal()}
|
|
863
|
+
document.getElementById('newSessionForm').onsubmit=e=>safe(async()=>{e.preventDefault();const payload={agentId:val('newAgent'),workspace:val('newWorkspace')||undefined,model:val('newModel')||undefined,reasoningEffort:val('newReasoning')||undefined,launchProfileId:val('newLaunch')||undefined,fastMode:document.getElementById('newFast').checked};await api('/api/sessions/new',{method:'POST',body:JSON.stringify(payload)});document.getElementById('newSessionDialog').close();toast('New session started');await loadBootstrap();await loadChatHistory()},e);
|
|
864
|
+
document.getElementById('cancelSessionBtn').onclick=()=>document.getElementById('newSessionDialog').close();
|
|
865
|
+
function val(id){return document.getElementById(id).value.trim()}
|
|
750
866
|
async function loadSessions(reset=true){if(reset)sessionsPager.reset();const q=document.getElementById('sessionSearch').value||'';const data=await api('/api/sessions?query='+encodeURIComponent(q)+'&page='+sessionsPager.page+'&limit='+sessionsPager.pageSize);document.getElementById('sessionsList').innerHTML=data.sessions.map(s=>'<div class="item"><strong title="'+attr(s.title||s.firstUserMessage||s.id)+'">'+esc(short(s.title||s.firstUserMessage||s.id))+'</strong><small><button type="button" class="copy-id" data-copy-id="'+attr(s.id)+'" title="Copy thread ID">'+esc(short(s.id,64))+'</button> / '+esc(short((s.cwd||'')+' / '+fmtDate(s.updatedAt)))+'</small><div class="row"><button data-switch="'+attr(s.id)+'">Switch</button></div></div>').join('')||'<div class="item">No sessions found.</div>';sessionsPager.render(data.pagination||{});document.querySelectorAll('[data-copy-id]').forEach(b=>b.onclick=()=>copyText(b.dataset.copyId||''));document.querySelectorAll('[data-switch]').forEach(b=>b.onclick=async()=>{await api('/api/sessions/switch',{method:'POST',body:JSON.stringify({threadId:b.dataset.switch})});toast('Session switched');loadBootstrap()})}
|
|
751
867
|
document.getElementById('sessionSearchBtn').onclick=()=>loadSessions(true);document.getElementById('sessionSearch').addEventListener('keydown',e=>{if(e.key==='Enter')loadSessions(true)});document.getElementById('attachBtn').onclick=async()=>{const threadId=document.getElementById('attachInput').value.trim();if(threadId){await api('/api/sessions/attach',{method:'POST',body:JSON.stringify({threadId})});toast('Session attached');loadBootstrap()}};
|
|
752
|
-
function renderQueue(queue){document.getElementById('queueList').innerHTML=(queue||[]).map(q=>'<div class="item"><strong>'+esc(q.id
|
|
753
|
-
document.querySelectorAll('[data-queue]').forEach(b=>b.onclick=
|
|
754
|
-
async function loadArtifacts(){const data=await api('/api/artifacts');document.getElementById('artifactList').innerHTML=data.reports.map(r=>'<div class="item"><strong>'+esc(r.turnId)+' - '+r.fileCount+' files - '+fmtBytes(r.totalSizeBytes)+'</strong><small>'+fmtDate(r.updatedAt)+'</small><div class="row"><a href="/api/artifacts/zip?turnId='+encodeURIComponent(r.turnId)+(token?'&token='+encodeURIComponent(token):'')+'">Download ZIP</a><button data-del-art="'+esc(r.turnId)+'" class="danger">Delete</button></div>'+r.artifacts.slice(0,
|
|
868
|
+
function renderQueue(queue,paused){document.getElementById('queueStatus').textContent=paused?'Paused':'Running';document.getElementById('queueList').innerHTML=(queue||[]).map((q,i)=>'<div class="item queue-item" draggable="true" data-queue-id="'+attr(q.id)+'"><strong>'+esc((i+1)+'. '+q.id+' - '+q.description)+'</strong><small>Created '+fmtDate(q.createdAt)+' / attempts '+q.attempts+(q.lastError?' / '+esc(q.lastError):'')+'</small><div class="row"><button data-q="run" data-id="'+q.id+'">Run</button><button data-q="top" data-id="'+q.id+'">Top</button><button data-q="up" data-id="'+q.id+'">Up</button><button data-q="down" data-id="'+q.id+'">Down</button><button data-q="cancel" data-id="'+q.id+'" class="danger">Cancel</button></div></div>').join('')||'<div class="item">Queue is empty.</div>';document.querySelectorAll('[data-q]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/queue',{method:'POST',body:JSON.stringify({action:b.dataset.q,id:b.dataset.id})});renderQueue(r.queue,r.paused)}));let dragged=null;document.querySelectorAll('.queue-item').forEach(item=>{item.ondragstart=()=>{dragged=item.dataset.queueId;item.classList.add('dragging')};item.ondragend=()=>item.classList.remove('dragging');item.ondragover=e=>e.preventDefault();item.ondrop=()=>safe(async()=>{if(dragged&&dragged!==item.dataset.queueId){const ids=Array.from(document.querySelectorAll('.queue-item')).map(el=>el.dataset.queueId);const targetIndex=Math.max(0,ids.indexOf(item.dataset.queueId));await api('/api/queue',{method:'POST',body:JSON.stringify({action:'top',id:dragged})});for(let i=0;i<targetIndex;i++)await api('/api/queue',{method:'POST',body:JSON.stringify({action:'down',id:dragged})});const r=await api('/api/queue');renderQueue(r.queue,r.paused)}})})}
|
|
869
|
+
document.querySelectorAll('[data-queue]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/queue',{method:'POST',body:JSON.stringify({action:b.dataset.queue})});renderQueue(r.queue,r.paused)}));
|
|
870
|
+
async function loadArtifacts(){const data=await api('/api/artifacts');document.getElementById('artifactList').innerHTML=data.reports.map(r=>'<div class="item"><strong>'+esc(r.turnId)+' - '+r.fileCount+' files - '+fmtBytes(r.totalSizeBytes)+'</strong><small>'+fmtDate(r.updatedAt)+' / '+esc(r.source||'turn')+'</small><div class="row"><a href="/api/artifacts/zip?turnId='+encodeURIComponent(r.turnId)+(token?'&token='+encodeURIComponent(token):'')+'">Download ZIP</a><button data-del-art="'+esc(r.turnId)+'" class="danger">Delete</button></div>'+r.artifacts.slice(0,12).map(a=>'<small><a href="/api/artifacts/file?turnId='+encodeURIComponent(r.turnId)+'&path='+encodeURIComponent(a.relativePath)+(token?'&token='+encodeURIComponent(token):'')+'">'+esc(a.name)+'</a> '+fmtBytes(a.sizeBytes)+' <button class="secondary" data-preview-turn="'+attr(r.turnId)+'" data-preview-path="'+attr(a.relativePath)+'">Preview</button></small>').join('')+'</div>').join('')||'<div class="item">No artifacts.</div>';document.querySelectorAll('[data-del-art]').forEach(b=>b.onclick=()=>safe(async()=>{if(confirm('Delete artifact turn '+b.dataset.delArt+'?')){await api('/api/artifacts?turnId='+encodeURIComponent(b.dataset.delArt),{method:'DELETE'});loadArtifacts()}}));document.querySelectorAll('[data-preview-turn]').forEach(b=>b.onclick=()=>previewArtifact(b.dataset.previewTurn,b.dataset.previewPath))}
|
|
755
871
|
document.getElementById('reloadArtifactsBtn').onclick=loadArtifacts;
|
|
872
|
+
async function previewArtifact(turnId,path){const data=await api('/api/artifacts/preview?turnId='+encodeURIComponent(turnId)+'&path='+encodeURIComponent(path));const target=document.getElementById('artifactPreview');if(data.kind==='image'){target.innerHTML='<div class="panel"><h2>'+esc(data.name)+'</h2><img src="/api/artifacts/file?turnId='+encodeURIComponent(turnId)+'&path='+encodeURIComponent(path)+(token?'&token='+encodeURIComponent(token):'')+'"></div>';return}if(data.kind==='text'){target.innerHTML='<div class="panel"><h2>'+esc(data.name)+' '+fmtBytes(data.sizeBytes)+'</h2><pre>'+esc(data.text||'')+'</pre>'+(data.truncated?'<small>Preview truncated.</small>':'')+'</div>';return}target.innerHTML='<div class="panel"><h2>'+esc(data.name)+'</h2><p>'+esc(data.detail||'Preview unavailable')+'</p></div>'}
|
|
873
|
+
async function loadActivity(){const q='?source='+encodeURIComponent(val('activitySource'))+'&status='+encodeURIComponent(val('activityStatus'))+'&limit='+encodeURIComponent(val('activityLimit')||'100');const data=await api('/api/activity'+q);renderActivity(data.events||[])}
|
|
874
|
+
function renderActivity(events){document.getElementById('activityList').innerHTML=(events||[]).map(e=>'<div class="item"><strong>'+esc(fmtDate(e.timestamp)+' / '+e.source+' / '+e.status+' / '+e.type)+'</strong><small>'+esc(short(e.prompt||e.detail||'',220))+'</small><small>'+esc((e.threadId||'-')+' / '+(e.workspace||'-')+' / '+fmtDuration(e.durationMs))+'</small></div>').join('')||'<div class="item">No activity.</div>'}
|
|
875
|
+
document.getElementById('loadActivityBtn').onclick=()=>loadActivity();
|
|
756
876
|
async function loadSettings(){const data=await api('/api/settings');state.settings=data.settings;renderSettings()}
|
|
757
|
-
function renderSettings(){const groups={};state.settings.forEach(s=>(groups[s.group]??=[]).push(s));const names=Object.keys(groups);if(!state.settingsGroup||!groups[state.settingsGroup])state.settingsGroup=names[0];document.getElementById('settingsTabs').innerHTML=names.map(name=>'<button data-setting-tab="'+attr(name)+'" class="'+(name===state.settingsGroup?'active':'')+'">'+esc(name)+' ('+groups[name].length+')</button>').join('');document.querySelectorAll('[data-setting-tab]').forEach(b=>b.onclick=()=>{state.settingsGroup=b.dataset.settingTab;renderSettings()});const items=groups[state.settingsGroup]||[];document.getElementById('settingsForm').innerHTML='<div class="settings-section"><h2>'+esc(state.settingsGroup||'Settings')+'</h2>'+items.map(s=>'<div class="setting"><label>'+esc(s.label)+'</label>'+settingInput(s)+'<small>'+esc(s.key)+' - '+esc(s.description)+(s.restartRequired?' Restart required.':'')+'</small></div>').join('')+'</div>'}
|
|
758
|
-
function settingInput(s){const value=esc(s.value||''); if(s.kind==='boolean')return '<select data-setting="'+s.key+'"><option value=""></option><option '+(s.value==='true'?'selected':'')+'>true</option><option '+(s.value==='false'?'selected':'')+'>false</option></select>'; if(s.kind==='json')return '<textarea rows="4" data-setting="'+s.key+'">'+value+'</textarea>'; return '<input data-setting="'+s.key+'" value="'+value+'" '+(s.kind==='secret'?'type="password"':'')+'>'}
|
|
759
|
-
document.getElementById('saveSettingsBtn').onclick=async()=>{const patch={};document.querySelectorAll('[data-setting]').forEach(el=>patch[el.dataset.setting]=el.value);const r=await api('/api/settings',{method:'PATCH',body:JSON.stringify({settings:patch})});document.getElementById('settingsStatus').textContent=r.changedKeys.length?'Saved '+r.changedKeys.length+' setting(s)'+(r.restartRequired?' - restart required':''):'No changes';toast('Settings saved')};
|
|
760
|
-
|
|
761
|
-
async function
|
|
762
|
-
|
|
877
|
+
function renderSettings(){const groups={};state.settings.forEach(s=>(groups[s.group]??=[]).push(s));const names=Object.keys(groups);if(!state.settingsGroup||!groups[state.settingsGroup])state.settingsGroup=names[0];document.getElementById('settingsTabs').innerHTML=names.map(name=>'<button data-setting-tab="'+attr(name)+'" class="'+(name===state.settingsGroup?'active':'')+'">'+esc(name)+' ('+groups[name].length+')</button>').join('');document.querySelectorAll('[data-setting-tab]').forEach(b=>b.onclick=()=>{state.settingsGroup=b.dataset.settingTab;renderSettings()});const items=groups[state.settingsGroup]||[];document.getElementById('settingsForm').innerHTML='<div class="settings-section"><h2>'+esc(state.settingsGroup||'Settings')+'</h2>'+items.map(s=>'<div class="setting" data-setting-box="'+attr(s.key)+'"><label>'+esc(s.label)+'</label>'+settingInput(s)+'<small>'+esc(s.key)+' - '+esc(s.description)+(s.restartRequired?' Restart required.':'')+(s.configured?' Configured.':' Inherited/default.')+'</small><div class="setting-error"></div></div>').join('')+'</div>'}
|
|
878
|
+
function settingInput(s){const value=esc(s.value||''); if(s.options)return '<select data-setting="'+s.key+'"><option value=""></option>'+s.options.map(o=>'<option value="'+attr(o)+'" '+(s.value===o?'selected':'')+'>'+esc(o)+'</option>').join('')+'</select>'; if(s.kind==='boolean')return '<select data-setting="'+s.key+'"><option value=""></option><option value="true" '+(s.value==='true'?'selected':'')+'>true</option><option value="false" '+(s.value==='false'?'selected':'')+'>false</option></select>'; if(s.kind==='json')return '<textarea rows="4" data-setting="'+s.key+'">'+value+'</textarea>'; return '<input data-setting="'+s.key+'" value="'+value+'" '+(s.kind==='secret'?'type="password"':'')+'>'}
|
|
879
|
+
document.getElementById('saveSettingsBtn').onclick=()=>safe(async()=>{document.querySelectorAll('.setting-error').forEach(e=>e.textContent='');const patch={};document.querySelectorAll('[data-setting]').forEach(el=>patch[el.dataset.setting]=el.value);const r=await api('/api/settings',{method:'PATCH',body:JSON.stringify({settings:patch})});(r.errors||[]).forEach(err=>{const box=document.querySelector('[data-setting-box="'+cssEscape(err.key)+'"] .setting-error');if(box)box.textContent=err.message});document.getElementById('settingsStatus').textContent=(r.errors&&r.errors.length)?'Fix '+r.errors.length+' setting error(s)':(r.changedKeys.length?'Saved '+r.changedKeys.length+' setting(s)'+(r.restartRequired?' - restart required':''):'No changes');toast((r.errors&&r.errors.length)?'Settings need attention':'Settings saved')});
|
|
880
|
+
document.getElementById('restartBtn').onclick=()=>safe(async()=>{if(confirm('Restart NordRelay now?')){await api('/api/runtime/restart',{method:'POST'});toast('Restart requested')}});
|
|
881
|
+
async function loadLogs(){const target=document.getElementById('logTarget').value;const lines=document.getElementById('logLines').value;const data=await api('/api/logs?target='+target+'&lines='+lines);state.logsPlain=data.plain||'';renderLogs()}document.getElementById('loadLogsBtn').onclick=loadLogs;
|
|
882
|
+
function renderLogs(){const level=val('logLevel');const query=val('logSearch').toLowerCase();const lines=state.logsPlain.split(/\\n/).filter(line=>(level==='all'||line.includes(level))&&(!query||line.toLowerCase().includes(query)));document.getElementById('logs').textContent=lines.join('\\n')||'(empty)'}
|
|
883
|
+
document.getElementById('logLevel').onchange=renderLogs;document.getElementById('logSearch').oninput=renderLogs;document.getElementById('logAutoRefresh').onchange=e=>{clearInterval(state.logTimer);state.logTimer=null;if(e.target.checked)state.logTimer=setInterval(loadLogs,5000)};document.getElementById('downloadLogsBtn').onclick=()=>{const blob=new Blob([document.getElementById('logs').textContent||''],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='nordrelay-log.txt';a.click();URL.revokeObjectURL(a.href)};
|
|
884
|
+
async function loadDiagnostics(){const data=await api('/api/diagnostics');document.getElementById('diagnostics').innerHTML=diagnosticsHtml(data)}
|
|
885
|
+
function diagnosticsHtml(d){const h=d.health||{};const s=d.snapshot?.session||{};const vc=d.versionChecks||{};return '<div class="list">'+card('Runtime',[['Status',h.state?.status],['PID',h.state?.pid],['App PID',h.state?.appPid],['State',h.stateFile],['Log',h.logFile],['State backend',d.runtime?.stateBackend],['Uptime',h.uptimeSeconds+'s']])+card('Agent',[['Agent',s.agentLabel],['Thread',s.threadId],['Workspace',s.workspace],['Model',s.model],['Reasoning',s.reasoningEffort],['Fast',s.fastMode?'on':'off']])+card('CLI Versions',Object.values(vc).map(v=>[v.label,(v.status==='current'?'OK ':'WARN ')+(v.installedLabel||'-')+' latest '+(v.latestVersion||'-')]))+card('External Mirror',d.runtime?.externalMirror?Object.entries(d.runtime.externalMirror):[['Status','idle']])+'</div>'}
|
|
886
|
+
function card(title,rows){return '<div class="item"><strong>'+esc(title)+'</strong>'+rows.map(r=>'<small>'+esc(r[0])+': '+esc(r[1]??'-')+'</small>').join('')+'</div>'}
|
|
887
|
+
function safe(fn,event){if(event&&event.preventDefault)event.preventDefault();Promise.resolve().then(fn).catch(err=>toast(err.message||String(err)))}
|
|
888
|
+
loadBootstrap().then(()=>{connectEvents();loadChatHistory();loadSessions();loadArtifacts();loadSettings();loadLogs();loadDiagnostics();loadActivity()}).catch(err=>toast(err.message));
|
|
763
889
|
`;
|
|
764
890
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createDocumentStore } from "./state-backend.js";
|
|
3
|
+
const DEFAULT_CHAT_LIMIT = 300;
|
|
4
|
+
const DEFAULT_ACTIVITY_LIMIT = 1000;
|
|
5
|
+
export class WebChatStore {
|
|
6
|
+
store;
|
|
7
|
+
maxMessages;
|
|
8
|
+
constructor(workspace, backend = "json", maxMessages = DEFAULT_CHAT_LIMIT) {
|
|
9
|
+
this.store = createDocumentStore({
|
|
10
|
+
workspace,
|
|
11
|
+
fileName: "web-chat.json",
|
|
12
|
+
sqliteKey: "web-chat",
|
|
13
|
+
backend,
|
|
14
|
+
});
|
|
15
|
+
this.maxMessages = maxMessages;
|
|
16
|
+
}
|
|
17
|
+
append(input) {
|
|
18
|
+
const payload = this.readPayload();
|
|
19
|
+
const threadId = input.threadId || "pending";
|
|
20
|
+
const messages = payload.messagesByThread[threadId] ?? [];
|
|
21
|
+
const message = {
|
|
22
|
+
id: randomId(),
|
|
23
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
24
|
+
...input,
|
|
25
|
+
threadId,
|
|
26
|
+
};
|
|
27
|
+
messages.push(message);
|
|
28
|
+
if (messages.length > this.maxMessages) {
|
|
29
|
+
messages.splice(0, messages.length - this.maxMessages);
|
|
30
|
+
}
|
|
31
|
+
payload.messagesByThread[threadId] = messages;
|
|
32
|
+
this.store.write(payload);
|
|
33
|
+
return message;
|
|
34
|
+
}
|
|
35
|
+
list(threadId, limit = 200) {
|
|
36
|
+
const messages = this.readPayload().messagesByThread[threadId || "pending"] ?? [];
|
|
37
|
+
return messages.slice(-Math.max(1, Math.min(this.maxMessages, limit)));
|
|
38
|
+
}
|
|
39
|
+
clear(threadId) {
|
|
40
|
+
const payload = this.readPayload();
|
|
41
|
+
const key = threadId || "pending";
|
|
42
|
+
const count = payload.messagesByThread[key]?.length ?? 0;
|
|
43
|
+
delete payload.messagesByThread[key];
|
|
44
|
+
this.store.write(payload);
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
47
|
+
readPayload() {
|
|
48
|
+
const payload = this.store.read();
|
|
49
|
+
if (!payload || payload.version !== 1 || !payload.messagesByThread || typeof payload.messagesByThread !== "object") {
|
|
50
|
+
return { version: 1, messagesByThread: {} };
|
|
51
|
+
}
|
|
52
|
+
const messagesByThread = {};
|
|
53
|
+
for (const [threadId, messages] of Object.entries(payload.messagesByThread)) {
|
|
54
|
+
if (Array.isArray(messages)) {
|
|
55
|
+
messagesByThread[threadId] = messages.filter(isWebChatMessage).slice(-this.maxMessages);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { version: 1, messagesByThread };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class WebActivityStore {
|
|
62
|
+
store;
|
|
63
|
+
maxEvents;
|
|
64
|
+
constructor(workspace, backend = "json", maxEvents = DEFAULT_ACTIVITY_LIMIT) {
|
|
65
|
+
this.store = createDocumentStore({
|
|
66
|
+
workspace,
|
|
67
|
+
fileName: "web-activity.json",
|
|
68
|
+
sqliteKey: "web-activity",
|
|
69
|
+
backend,
|
|
70
|
+
});
|
|
71
|
+
this.maxEvents = maxEvents;
|
|
72
|
+
}
|
|
73
|
+
append(input) {
|
|
74
|
+
const payload = this.readPayload();
|
|
75
|
+
const event = {
|
|
76
|
+
id: randomId(),
|
|
77
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
78
|
+
...input,
|
|
79
|
+
};
|
|
80
|
+
payload.events.push(event);
|
|
81
|
+
if (payload.events.length > this.maxEvents) {
|
|
82
|
+
payload.events.splice(0, payload.events.length - this.maxEvents);
|
|
83
|
+
}
|
|
84
|
+
this.store.write(payload);
|
|
85
|
+
return event;
|
|
86
|
+
}
|
|
87
|
+
list(options = {}) {
|
|
88
|
+
const limit = Math.max(1, Math.min(500, options.limit ?? 100));
|
|
89
|
+
return this.readPayload().events
|
|
90
|
+
.filter((event) => !options.source || options.source === "all" || event.source === options.source)
|
|
91
|
+
.filter((event) => !options.status || options.status === "all" || event.status === options.status)
|
|
92
|
+
.slice(-limit)
|
|
93
|
+
.reverse();
|
|
94
|
+
}
|
|
95
|
+
readPayload() {
|
|
96
|
+
const payload = this.store.read();
|
|
97
|
+
if (!payload || payload.version !== 1 || !Array.isArray(payload.events)) {
|
|
98
|
+
return { version: 1, events: [] };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
version: 1,
|
|
102
|
+
events: payload.events.filter(isWebActivityEvent).slice(-this.maxEvents),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function isWebChatMessage(value) {
|
|
107
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const candidate = value;
|
|
111
|
+
return typeof candidate.id === "string" &&
|
|
112
|
+
typeof candidate.threadId === "string" &&
|
|
113
|
+
typeof candidate.text === "string" &&
|
|
114
|
+
typeof candidate.timestamp === "string" &&
|
|
115
|
+
["user", "agent", "system", "tool"].includes(candidate.role) &&
|
|
116
|
+
["web", "cli"].includes(candidate.source);
|
|
117
|
+
}
|
|
118
|
+
function isWebActivityEvent(value) {
|
|
119
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const candidate = value;
|
|
123
|
+
return typeof candidate.id === "string" &&
|
|
124
|
+
typeof candidate.timestamp === "string" &&
|
|
125
|
+
typeof candidate.type === "string" &&
|
|
126
|
+
["web", "cli"].includes(candidate.source) &&
|
|
127
|
+
["queued", "running", "completed", "failed", "aborted", "info"].includes(candidate.status);
|
|
128
|
+
}
|
|
129
|
+
function randomId() {
|
|
130
|
+
return randomUUID().replace(/-/g, "").slice(0, 12);
|
|
131
|
+
}
|