agent-relay-server 0.12.3 → 0.13.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/docs/openapi.json +310 -1
- package/package.json +8 -5
- package/public/index.html +764 -56
- package/src/cli.ts +82 -0
- package/src/config-store.ts +71 -0
- package/src/db.ts +15 -0
- package/src/insights-db.ts +179 -0
- package/src/routes.ts +110 -0
- package/src/sse.ts +11 -0
package/public/index.html
CHANGED
|
@@ -10286,6 +10286,20 @@ async function apiUpload(path, file, meta) {
|
|
|
10286
10286
|
}
|
|
10287
10287
|
return response.json();
|
|
10288
10288
|
}
|
|
10289
|
+
async function apiPostAudio(path, blob) {
|
|
10290
|
+
const headers = { "Content-Type": blob.type || "application/octet-stream" };
|
|
10291
|
+
if (authToken) headers["X-Agent-Relay-Token"] = authToken;
|
|
10292
|
+
const response = await fetch(new URL("api" + path, baseUrl()), {
|
|
10293
|
+
method: "POST",
|
|
10294
|
+
headers,
|
|
10295
|
+
body: blob
|
|
10296
|
+
});
|
|
10297
|
+
if (!response.ok) {
|
|
10298
|
+
if (response.status === 401) throw makeError(401, "Authentication required");
|
|
10299
|
+
throw makeError(response.status, await responseErrorMessage(response));
|
|
10300
|
+
}
|
|
10301
|
+
return response.json();
|
|
10302
|
+
}
|
|
10289
10303
|
async function apiBlob(path) {
|
|
10290
10304
|
const opts = {
|
|
10291
10305
|
method: "GET",
|
|
@@ -10301,6 +10315,206 @@ async function apiBlob(path) {
|
|
|
10301
10315
|
return response.blob();
|
|
10302
10316
|
}
|
|
10303
10317
|
//#endregion
|
|
10318
|
+
//#region src/lib/voice.ts
|
|
10319
|
+
/**
|
|
10320
|
+
* Browser voice I/O for chat.
|
|
10321
|
+
*
|
|
10322
|
+
* TTS (output) is 100% browser-native via the Web Speech API — the store
|
|
10323
|
+
* already receives every agent response turn, so there is no backend round-trip.
|
|
10324
|
+
* STT (input) records the mic and posts the clip to the voice connector's
|
|
10325
|
+
* whisper endpoint through the relay proxy (single-origin, auth-gated).
|
|
10326
|
+
*
|
|
10327
|
+
* Speaker policy ("active chat owns the speaker"):
|
|
10328
|
+
* - One utterance at a time. Disabled => silent.
|
|
10329
|
+
* - Only the active chat speaks, and only responses that arrive while it is
|
|
10330
|
+
* active (no backlog replay).
|
|
10331
|
+
* - Switching away lets the current utterance finish but drops the now-background
|
|
10332
|
+
* chat's queued + future responses.
|
|
10333
|
+
* - The active chat preempts: if it speaks while a previous chat's audio lingers,
|
|
10334
|
+
* that audio is cancelled.
|
|
10335
|
+
*/
|
|
10336
|
+
/** Collapse markdown/code into something worth hearing (mirrors the connector's text.ts). */
|
|
10337
|
+
function speechify(markdown) {
|
|
10338
|
+
if (!markdown) return "";
|
|
10339
|
+
let text = markdown.replace(/\r\n/g, "\n");
|
|
10340
|
+
text = text.replace(/```[^\n]*\n([\s\S]*?)```/g, (_m, body) => {
|
|
10341
|
+
const lines = body.replace(/\n+$/, "").split("\n").length;
|
|
10342
|
+
return ` code block, ${lines} ${lines === 1 ? "line" : "lines"}. `;
|
|
10343
|
+
});
|
|
10344
|
+
text = text.replace(/```[^\n]*/g, " code block. ");
|
|
10345
|
+
text = text.replace(/^#{1,6}\s+/gm, "");
|
|
10346
|
+
text = text.replace(/^\s*[-*+]\s+/gm, ". ");
|
|
10347
|
+
text = text.replace(/^\s*\d+\.\s+/gm, ". ");
|
|
10348
|
+
text = text.replace(/`([^`]+)`/g, "$1");
|
|
10349
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
10350
|
+
text = text.replace(/(\*\*|__|\*|_)(.*?)\1/g, "$2");
|
|
10351
|
+
text = text.replace(/[ \t]+/g, " ");
|
|
10352
|
+
text = text.replace(/\n{2,}/g, ". ");
|
|
10353
|
+
text = text.replace(/\s*\.\s*\.\s*(\.\s*)+/g, ". ");
|
|
10354
|
+
return text.trim();
|
|
10355
|
+
}
|
|
10356
|
+
var MAX_CHUNK = 220;
|
|
10357
|
+
/** Split into utterance-sized chunks (sentence boundaries; hard-split very long runs). */
|
|
10358
|
+
function chunkForSpeech(text) {
|
|
10359
|
+
const sentences = text.match(/[^.!?]+[.!?]*\s*/g)?.map((s) => s.trim()).filter(Boolean) ?? [text];
|
|
10360
|
+
const out = [];
|
|
10361
|
+
for (const s of sentences) {
|
|
10362
|
+
if (s.length <= MAX_CHUNK) {
|
|
10363
|
+
out.push(s);
|
|
10364
|
+
continue;
|
|
10365
|
+
}
|
|
10366
|
+
let rest = s;
|
|
10367
|
+
while (rest.length > MAX_CHUNK) {
|
|
10368
|
+
let cut = rest.lastIndexOf(" ", MAX_CHUNK);
|
|
10369
|
+
if (cut <= 0) cut = MAX_CHUNK;
|
|
10370
|
+
out.push(rest.slice(0, cut).trim());
|
|
10371
|
+
rest = rest.slice(cut).trim();
|
|
10372
|
+
}
|
|
10373
|
+
if (rest) out.push(rest);
|
|
10374
|
+
}
|
|
10375
|
+
return out;
|
|
10376
|
+
}
|
|
10377
|
+
var synthAvailable = typeof window !== "undefined" && "speechSynthesis" in window;
|
|
10378
|
+
var VoiceTts = class {
|
|
10379
|
+
enabled = false;
|
|
10380
|
+
active = null;
|
|
10381
|
+
queue = [];
|
|
10382
|
+
currentChat = null;
|
|
10383
|
+
speaking = false;
|
|
10384
|
+
gen = 0;
|
|
10385
|
+
get available() {
|
|
10386
|
+
return synthAvailable;
|
|
10387
|
+
}
|
|
10388
|
+
isEnabled() {
|
|
10389
|
+
return this.enabled;
|
|
10390
|
+
}
|
|
10391
|
+
setEnabled(on) {
|
|
10392
|
+
if (on === this.enabled) return;
|
|
10393
|
+
this.enabled = on;
|
|
10394
|
+
if (!on) this.reset();
|
|
10395
|
+
}
|
|
10396
|
+
setActiveChat(chatId) {
|
|
10397
|
+
if (chatId === this.active) return;
|
|
10398
|
+
this.active = chatId;
|
|
10399
|
+
this.queue = this.queue.filter((q) => q.chatId === chatId);
|
|
10400
|
+
}
|
|
10401
|
+
/** A captured agent response turn arrived for `chatId`. */
|
|
10402
|
+
onResponse(chatId, rawText) {
|
|
10403
|
+
if (!this.enabled || !synthAvailable || !chatId || chatId !== this.active) return;
|
|
10404
|
+
const text = speechify(rawText);
|
|
10405
|
+
if (!text) return;
|
|
10406
|
+
if (this.speaking && this.currentChat && this.currentChat !== chatId) {
|
|
10407
|
+
this.queue = [];
|
|
10408
|
+
this.cancel();
|
|
10409
|
+
}
|
|
10410
|
+
this.queue.push({
|
|
10411
|
+
chatId,
|
|
10412
|
+
text
|
|
10413
|
+
});
|
|
10414
|
+
this.pump();
|
|
10415
|
+
}
|
|
10416
|
+
/** Cut all speech immediately (e.g. when the user starts talking). */
|
|
10417
|
+
bargeIn() {
|
|
10418
|
+
this.reset();
|
|
10419
|
+
}
|
|
10420
|
+
reset() {
|
|
10421
|
+
this.queue = [];
|
|
10422
|
+
this.cancel();
|
|
10423
|
+
}
|
|
10424
|
+
cancel() {
|
|
10425
|
+
this.gen++;
|
|
10426
|
+
this.currentChat = null;
|
|
10427
|
+
this.speaking = false;
|
|
10428
|
+
try {
|
|
10429
|
+
window.speechSynthesis.cancel();
|
|
10430
|
+
} catch {}
|
|
10431
|
+
}
|
|
10432
|
+
pump() {
|
|
10433
|
+
if (this.speaking) return;
|
|
10434
|
+
const item = this.queue.shift();
|
|
10435
|
+
if (!item) return;
|
|
10436
|
+
this.speaking = true;
|
|
10437
|
+
this.currentChat = item.chatId;
|
|
10438
|
+
const gen = ++this.gen;
|
|
10439
|
+
const chunks = chunkForSpeech(item.text);
|
|
10440
|
+
const speakAt = (i) => {
|
|
10441
|
+
if (gen !== this.gen) return;
|
|
10442
|
+
if (i >= chunks.length) {
|
|
10443
|
+
this.speaking = false;
|
|
10444
|
+
this.currentChat = null;
|
|
10445
|
+
this.pump();
|
|
10446
|
+
return;
|
|
10447
|
+
}
|
|
10448
|
+
const u = new SpeechSynthesisUtterance(chunks[i]);
|
|
10449
|
+
u.lang = navigator.language || "en-US";
|
|
10450
|
+
u.onend = () => speakAt(i + 1);
|
|
10451
|
+
u.onerror = () => speakAt(i + 1);
|
|
10452
|
+
window.speechSynthesis.speak(u);
|
|
10453
|
+
};
|
|
10454
|
+
speakAt(0);
|
|
10455
|
+
}
|
|
10456
|
+
};
|
|
10457
|
+
var voiceTts = new VoiceTts();
|
|
10458
|
+
var micAvailable = typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof window !== "undefined" && "MediaRecorder" in window;
|
|
10459
|
+
function pickMimeType() {
|
|
10460
|
+
for (const m of [
|
|
10461
|
+
"audio/webm;codecs=opus",
|
|
10462
|
+
"audio/webm",
|
|
10463
|
+
"audio/ogg;codecs=opus",
|
|
10464
|
+
"audio/mp4"
|
|
10465
|
+
]) if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported?.(m)) return m;
|
|
10466
|
+
}
|
|
10467
|
+
/** Hold-to-talk mic recorder. start() on press, stop() on release returns the clip. */
|
|
10468
|
+
var PttRecorder = class {
|
|
10469
|
+
rec = null;
|
|
10470
|
+
chunks = [];
|
|
10471
|
+
stream = null;
|
|
10472
|
+
async start() {
|
|
10473
|
+
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
10474
|
+
this.chunks = [];
|
|
10475
|
+
const mime = pickMimeType();
|
|
10476
|
+
this.rec = new MediaRecorder(this.stream, mime ? { mimeType: mime } : void 0);
|
|
10477
|
+
this.rec.ondataavailable = (e) => {
|
|
10478
|
+
if (e.data.size) this.chunks.push(e.data);
|
|
10479
|
+
};
|
|
10480
|
+
this.rec.start();
|
|
10481
|
+
}
|
|
10482
|
+
async stop() {
|
|
10483
|
+
const rec = this.rec;
|
|
10484
|
+
if (!rec) {
|
|
10485
|
+
this.teardown();
|
|
10486
|
+
return null;
|
|
10487
|
+
}
|
|
10488
|
+
const stopped = new Promise((resolve) => {
|
|
10489
|
+
rec.onstop = () => resolve();
|
|
10490
|
+
});
|
|
10491
|
+
try {
|
|
10492
|
+
rec.stop();
|
|
10493
|
+
} catch {}
|
|
10494
|
+
await stopped;
|
|
10495
|
+
const type = rec.mimeType || "audio/webm";
|
|
10496
|
+
const blob = new Blob(this.chunks, { type });
|
|
10497
|
+
this.teardown();
|
|
10498
|
+
return blob.size ? blob : null;
|
|
10499
|
+
}
|
|
10500
|
+
cancel() {
|
|
10501
|
+
try {
|
|
10502
|
+
this.rec?.stop();
|
|
10503
|
+
} catch {}
|
|
10504
|
+
this.teardown();
|
|
10505
|
+
}
|
|
10506
|
+
teardown() {
|
|
10507
|
+
this.stream?.getTracks().forEach((t) => t.stop());
|
|
10508
|
+
this.rec = null;
|
|
10509
|
+
this.stream = null;
|
|
10510
|
+
this.chunks = [];
|
|
10511
|
+
}
|
|
10512
|
+
};
|
|
10513
|
+
/** Send a recorded clip through the relay proxy to the voice connector's whisper STT. */
|
|
10514
|
+
async function transcribeClip(blob) {
|
|
10515
|
+
return ((await apiPostAudio("/connectors/voice/call/transcribe", blob))?.text ?? "").trim();
|
|
10516
|
+
}
|
|
10517
|
+
//#endregion
|
|
10304
10518
|
//#region src/store/clock.ts
|
|
10305
10519
|
var useClock = create$1((set, get) => ({
|
|
10306
10520
|
now: Date.now(),
|
|
@@ -10474,6 +10688,11 @@ var NAV_ITEMS = [
|
|
|
10474
10688
|
label: "Analytics",
|
|
10475
10689
|
icon: "AreaChart"
|
|
10476
10690
|
},
|
|
10691
|
+
{
|
|
10692
|
+
key: "insights",
|
|
10693
|
+
label: "Insights",
|
|
10694
|
+
icon: "Lightbulb"
|
|
10695
|
+
},
|
|
10477
10696
|
{
|
|
10478
10697
|
key: "maintenance",
|
|
10479
10698
|
label: "Maintenance",
|
|
@@ -10701,6 +10920,11 @@ function inboxPeer(msg) {
|
|
|
10701
10920
|
function isHumanInboundMessage(msg) {
|
|
10702
10921
|
return msg.to === "user" && msg.from !== "user";
|
|
10703
10922
|
}
|
|
10923
|
+
function isSessionActivityStep(msg) {
|
|
10924
|
+
if (msg.kind !== "session") return false;
|
|
10925
|
+
const type = (msg.payload?.session)?.type;
|
|
10926
|
+
return type === "reasoning" || type === "tool";
|
|
10927
|
+
}
|
|
10704
10928
|
function isClaimableTaskWaiting(task) {
|
|
10705
10929
|
return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
|
|
10706
10930
|
}
|
|
@@ -11636,6 +11860,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
11636
11860
|
showOffline: false,
|
|
11637
11861
|
showBuiltIns: false,
|
|
11638
11862
|
autoRefresh: true,
|
|
11863
|
+
voiceTtsEnabled: false,
|
|
11639
11864
|
agentSort: "status",
|
|
11640
11865
|
agentSortDir: "asc",
|
|
11641
11866
|
agentPresetFilter: "",
|
|
@@ -11823,6 +12048,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
11823
12048
|
_refreshTimer: null,
|
|
11824
12049
|
_refreshInFlight: false,
|
|
11825
12050
|
set: (partial) => set(partial),
|
|
12051
|
+
setVoiceTtsEnabled(on) {
|
|
12052
|
+
voiceTts.setEnabled(on);
|
|
12053
|
+
set({ voiceTtsEnabled: on });
|
|
12054
|
+
},
|
|
11826
12055
|
async init() {
|
|
11827
12056
|
if (!useRelayStore.persist.hasHydrated()) await new Promise((resolve) => {
|
|
11828
12057
|
const unsub = useRelayStore.persist.onFinishHydration(() => {
|
|
@@ -11832,6 +12061,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
11832
12061
|
});
|
|
11833
12062
|
const token = get().authToken;
|
|
11834
12063
|
if (token) setAuthToken(token);
|
|
12064
|
+
voiceTts.setEnabled(get().voiceTtsEnabled);
|
|
12065
|
+
syncVoiceActiveChat(get());
|
|
11835
12066
|
setUnauthorizedHandler(() => {
|
|
11836
12067
|
if (!get().authNeeded) set({
|
|
11837
12068
|
authNeeded: true,
|
|
@@ -12345,6 +12576,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12345
12576
|
const msgs = [...s.messages, msg];
|
|
12346
12577
|
if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
|
|
12347
12578
|
set({ messages: msgs });
|
|
12579
|
+
if (msg.kind === "session") {
|
|
12580
|
+
const sess = msg.payload?.session;
|
|
12581
|
+
if ((sess?.type ?? "response") === "response" && (sess?.origin ?? "provider") === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
|
|
12582
|
+
}
|
|
12348
12583
|
const peer = inboxPeer(msg);
|
|
12349
12584
|
if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
|
|
12350
12585
|
return;
|
|
@@ -13914,6 +14149,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
13914
14149
|
showOffline: state.showOffline,
|
|
13915
14150
|
showBuiltIns: state.showBuiltIns,
|
|
13916
14151
|
autoRefresh: state.autoRefresh,
|
|
14152
|
+
voiceTtsEnabled: state.voiceTtsEnabled,
|
|
13917
14153
|
agentSort: state.agentSort,
|
|
13918
14154
|
agentSortDir: state.agentSortDir,
|
|
13919
14155
|
agentPresetFilter: state.agentPresetFilter,
|
|
@@ -13953,6 +14189,14 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
13953
14189
|
spawnWorkspaceMode: state.spawnWorkspaceMode
|
|
13954
14190
|
})
|
|
13955
14191
|
}));
|
|
14192
|
+
var _voiceActiveChat = null;
|
|
14193
|
+
function syncVoiceActiveChat(s) {
|
|
14194
|
+
const active = s.voiceTtsEnabled && s.view === "chat" ? s.selectedInboxThread || null : null;
|
|
14195
|
+
if (active === _voiceActiveChat) return;
|
|
14196
|
+
_voiceActiveChat = active;
|
|
14197
|
+
voiceTts.setActiveChat(active);
|
|
14198
|
+
}
|
|
14199
|
+
useRelayStore.subscribe(syncVoiceActiveChat);
|
|
13956
14200
|
//#endregion
|
|
13957
14201
|
//#region src/lib/themes.ts
|
|
13958
14202
|
var THEMES = [
|
|
@@ -25055,7 +25299,7 @@ function useAgentAttention(agent) {
|
|
|
25055
25299
|
const peerMessages = messages.filter((m) => inboxPeer(m) === agent.id && !isReactionEventMessage(m));
|
|
25056
25300
|
const cursor = Number(readCursors[agent.id] || 0);
|
|
25057
25301
|
const archivedAt = Number(archivedThreads[agent.id] || 0);
|
|
25058
|
-
const unread = archivedAt >= peerMessages.filter(isHumanInboundMessage).reduce((max, m) => Math.max(max, m.id), 0) && archivedAt > 0 ? 0 : peerMessages.filter((m) => isHumanInboundMessage(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25302
|
+
const unread = archivedAt >= peerMessages.filter(isHumanInboundMessage).reduce((max, m) => Math.max(max, m.id), 0) && archivedAt > 0 ? 0 : peerMessages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25059
25303
|
const pendingPairInvite = pairs.find((p) => (p.status === "active" || p.status === "pending") && (p.requesterId === agent.id || p.targetId === agent.id))?.status === "pending";
|
|
25060
25304
|
const claimableTasks = tasks.filter((t) => isClaimableTaskWaiting(t) && targetMatchesAgent(t.target, agent)).length + messages.filter((m) => isClaimableMessageWaiting(m) && targetMatchesAgent(m.to, agent)).length;
|
|
25061
25305
|
return {
|
|
@@ -25154,6 +25398,7 @@ function useAllInboxThreads() {
|
|
|
25154
25398
|
peer,
|
|
25155
25399
|
messages: [],
|
|
25156
25400
|
lastMessage: null,
|
|
25401
|
+
previewMessage: null,
|
|
25157
25402
|
attention: {
|
|
25158
25403
|
unread: 0,
|
|
25159
25404
|
needsHumanResponse: false,
|
|
@@ -25167,8 +25412,17 @@ function useAllInboxThreads() {
|
|
|
25167
25412
|
for (const thread of threads.values()) {
|
|
25168
25413
|
thread.messages.sort((a, b) => a.id - b.id);
|
|
25169
25414
|
thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
|
|
25415
|
+
let preview = null;
|
|
25416
|
+
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
25417
|
+
const m = thread.messages[i];
|
|
25418
|
+
if (m && !isSessionActivityStep(m)) {
|
|
25419
|
+
preview = m;
|
|
25420
|
+
break;
|
|
25421
|
+
}
|
|
25422
|
+
}
|
|
25423
|
+
thread.previewMessage = preview || thread.lastMessage;
|
|
25170
25424
|
const cursor = Number(readCursors[thread.peer] || 0);
|
|
25171
|
-
const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25425
|
+
const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25172
25426
|
thread.attention = {
|
|
25173
25427
|
unread,
|
|
25174
25428
|
needsHumanResponse: false,
|
|
@@ -76130,7 +76384,8 @@ var iconMap = {
|
|
|
76130
76384
|
FolderTree,
|
|
76131
76385
|
Shield,
|
|
76132
76386
|
Wrench,
|
|
76133
|
-
UserCog
|
|
76387
|
+
UserCog,
|
|
76388
|
+
Lightbulb
|
|
76134
76389
|
};
|
|
76135
76390
|
function notificationTime(notification) {
|
|
76136
76391
|
return new Date(notification.createdAt).toLocaleTimeString(void 0, {
|
|
@@ -125201,7 +125456,7 @@ function AgentListPanel({ threads, onSelectAgent }) {
|
|
|
125201
125456
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sortedAgents.map((agent) => {
|
|
125202
125457
|
const thread = threadByPeer.get(agent.id);
|
|
125203
125458
|
const unread = thread?.attention.unread || 0;
|
|
125204
|
-
const lastMsg = thread?.
|
|
125459
|
+
const lastMsg = thread?.previewMessage;
|
|
125205
125460
|
const lastActivityAt = threadActivityTimestamp(thread);
|
|
125206
125461
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
125207
125462
|
className: cn$2("w-full text-left px-3 py-2.5 flex items-start gap-2.5 hover:bg-muted/50 transition-colors border-b border-border/50", selectedInboxThread === agent.id && "bg-muted/60"),
|
|
@@ -125368,54 +125623,39 @@ function BusyIndicator({ blockedLabel, onInterrupt }) {
|
|
|
125368
125623
|
});
|
|
125369
125624
|
}
|
|
125370
125625
|
function ActivityTrace({ steps }) {
|
|
125371
|
-
const [
|
|
125372
|
-
|
|
125373
|
-
|
|
125374
|
-
const summary = (last.label || last.text || "").replace(/\s+/g, " ").trim();
|
|
125626
|
+
const [hideTools, setHideTools] = (0, import_react.useState)(false);
|
|
125627
|
+
if (!steps.length) return null;
|
|
125628
|
+
const toolCount = steps.filter((s) => s.kind === "tool").length;
|
|
125375
125629
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
125376
125630
|
className: "flex justify-start mb-2",
|
|
125377
125631
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
125378
|
-
className: "max-w-[85%] md:max-w-[75%] min-w-0",
|
|
125379
|
-
children: [
|
|
125380
|
-
|
|
125381
|
-
|
|
125382
|
-
|
|
125383
|
-
|
|
125384
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
125385
|
-
className: "truncate",
|
|
125386
|
-
children: expanded || steps.length > 1 ? "Activity" : summary.length > 80 ? `${summary.slice(0, 79)}…` : summary || "thinking"
|
|
125387
|
-
}),
|
|
125388
|
-
steps.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
125389
|
-
className: "opacity-60 shrink-0",
|
|
125390
|
-
children: [
|
|
125391
|
-
"· ",
|
|
125392
|
-
steps.length,
|
|
125393
|
-
" steps"
|
|
125394
|
-
]
|
|
125395
|
-
}),
|
|
125396
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, { className: cn$2("w-3 h-3 shrink-0 transition-transform", expanded && "rotate-90") })
|
|
125397
|
-
]
|
|
125398
|
-
}), expanded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
125399
|
-
className: "mt-1.5 space-y-1.5 border-l border-foreground/10 pl-2.5",
|
|
125400
|
-
children: steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
125401
|
-
className: "text-xs leading-relaxed",
|
|
125402
|
-
children: step.kind === "reasoning" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
125403
|
-
className: "italic text-muted-foreground/80 whitespace-pre-wrap break-words",
|
|
125632
|
+
className: "max-w-[85%] md:max-w-[75%] min-w-0 space-y-1.5",
|
|
125633
|
+
children: [steps.map((step) => {
|
|
125634
|
+
if (step.kind === "reasoning") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
125635
|
+
className: "flex items-start gap-1.5 text-xs leading-relaxed text-muted-foreground/80",
|
|
125636
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Brain, { className: "w-3.5 h-3.5 mt-0.5 shrink-0 opacity-70" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
125637
|
+
className: "italic whitespace-pre-wrap break-words min-w-0",
|
|
125404
125638
|
children: step.text
|
|
125405
|
-
})
|
|
125406
|
-
|
|
125407
|
-
|
|
125408
|
-
|
|
125409
|
-
|
|
125410
|
-
|
|
125411
|
-
|
|
125412
|
-
|
|
125413
|
-
|
|
125414
|
-
|
|
125415
|
-
|
|
125416
|
-
|
|
125417
|
-
|
|
125418
|
-
|
|
125639
|
+
})]
|
|
125640
|
+
}, step.id);
|
|
125641
|
+
if (hideTools) return null;
|
|
125642
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
125643
|
+
className: "flex items-start gap-1.5 text-xs leading-relaxed text-muted-foreground",
|
|
125644
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Terminal, { className: "w-3 h-3 mt-0.5 shrink-0 opacity-70" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
125645
|
+
className: "min-w-0 break-words",
|
|
125646
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
125647
|
+
className: "font-medium",
|
|
125648
|
+
children: step.label || "tool"
|
|
125649
|
+
}), step.text ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
125650
|
+
className: "opacity-70",
|
|
125651
|
+
children: [" — ", step.text]
|
|
125652
|
+
}) : null]
|
|
125653
|
+
})]
|
|
125654
|
+
}, step.id);
|
|
125655
|
+
}), toolCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
125656
|
+
onClick: () => setHideTools((v) => !v),
|
|
125657
|
+
className: "flex items-center gap-1 text-[11px] text-muted-foreground/50 hover:text-muted-foreground transition-colors text-left",
|
|
125658
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, { className: cn$2("w-3 h-3 shrink-0 transition-transform", !hideTools && "rotate-90") }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: hideTools ? `show ${toolCount} tool step${toolCount === 1 ? "" : "s"}` : "hide tool steps" })]
|
|
125419
125659
|
})]
|
|
125420
125660
|
})
|
|
125421
125661
|
});
|
|
@@ -126357,7 +126597,11 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126357
126597
|
const showError = useRelayStore((s) => s.showError);
|
|
126358
126598
|
const orchestrators = useRelayStore((s) => s.orchestrators);
|
|
126359
126599
|
const fetchOrchestrators = useRelayStore((s) => s.fetchOrchestrators);
|
|
126600
|
+
const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
|
|
126601
|
+
const setVoiceTtsEnabled = useRelayStore((s) => s.setVoiceTtsEnabled);
|
|
126360
126602
|
const fileInputRef = (0, import_react.useRef)(null);
|
|
126603
|
+
const pttRecorderRef = (0, import_react.useRef)(null);
|
|
126604
|
+
const [micState, setMicState] = (0, import_react.useState)("idle");
|
|
126361
126605
|
const pendingAttachmentsRef = (0, import_react.useRef)([]);
|
|
126362
126606
|
const [pendingAttachments, setPendingAttachments] = (0, import_react.useState)([]);
|
|
126363
126607
|
const [dragActive, setDragActive] = (0, import_react.useState)(false);
|
|
@@ -126648,6 +126892,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126648
126892
|
e.preventDefault();
|
|
126649
126893
|
uploadFiles(files);
|
|
126650
126894
|
}
|
|
126895
|
+
async function startPtt() {
|
|
126896
|
+
if (!micAvailable || micState !== "idle" || !selectedInboxThread) return;
|
|
126897
|
+
voiceTts.bargeIn();
|
|
126898
|
+
const recorder = new PttRecorder();
|
|
126899
|
+
try {
|
|
126900
|
+
await recorder.start();
|
|
126901
|
+
pttRecorderRef.current = recorder;
|
|
126902
|
+
setMicState("recording");
|
|
126903
|
+
} catch (e) {
|
|
126904
|
+
pttRecorderRef.current = null;
|
|
126905
|
+
setMicState("idle");
|
|
126906
|
+
showError("Microphone unavailable", e?.message || "Could not access the microphone.");
|
|
126907
|
+
}
|
|
126908
|
+
}
|
|
126909
|
+
async function stopPtt() {
|
|
126910
|
+
const recorder = pttRecorderRef.current;
|
|
126911
|
+
if (!recorder || micState !== "recording") return;
|
|
126912
|
+
pttRecorderRef.current = null;
|
|
126913
|
+
setMicState("transcribing");
|
|
126914
|
+
try {
|
|
126915
|
+
const clip = await recorder.stop();
|
|
126916
|
+
if (!clip) return;
|
|
126917
|
+
const text = await transcribeClip(clip);
|
|
126918
|
+
if (text) setReplyDraft(selectedInboxThread, draft ? `${draft} ${text}` : text);
|
|
126919
|
+
} catch (e) {
|
|
126920
|
+
showError("Transcription failed", e?.message || "Could not transcribe audio.");
|
|
126921
|
+
} finally {
|
|
126922
|
+
setMicState("idle");
|
|
126923
|
+
}
|
|
126924
|
+
}
|
|
126651
126925
|
if (!selectedInboxThread) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
126652
126926
|
className: "flex flex-col items-center justify-center h-full text-center px-4",
|
|
126653
126927
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageSquare, { className: "w-12 h-12 text-zinc-600 mb-3" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
@@ -126714,6 +126988,14 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126714
126988
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
126715
126989
|
className: "flex items-center gap-0.5 md:gap-1 shrink-0",
|
|
126716
126990
|
children: agent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
126991
|
+
voiceTts.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
126992
|
+
variant: "ghost",
|
|
126993
|
+
size: "icon-sm",
|
|
126994
|
+
title: voiceTtsEnabled ? "Speaking agent responses aloud — click to mute" : "Speak agent responses aloud (active chat)",
|
|
126995
|
+
className: voiceTtsEnabled ? "text-primary" : "",
|
|
126996
|
+
onClick: () => setVoiceTtsEnabled(!voiceTtsEnabled),
|
|
126997
|
+
children: voiceTtsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Volume2, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VolumeX, { className: "w-3.5 h-3.5" })
|
|
126998
|
+
}),
|
|
126717
126999
|
canOpenTerminal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
126718
127000
|
variant: "ghost",
|
|
126719
127001
|
size: "icon-sm",
|
|
@@ -126975,6 +127257,26 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126975
127257
|
placeholder: `Message ${agent ? displayName(agent) : selectedInboxThread}…`,
|
|
126976
127258
|
className: "flex-1"
|
|
126977
127259
|
}),
|
|
127260
|
+
micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127261
|
+
variant: "ghost",
|
|
127262
|
+
size: "icon",
|
|
127263
|
+
title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
|
|
127264
|
+
disabled: chatSending || micState === "transcribing",
|
|
127265
|
+
onPointerDown: (e) => {
|
|
127266
|
+
e.preventDefault();
|
|
127267
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
127268
|
+
startPtt();
|
|
127269
|
+
},
|
|
127270
|
+
onPointerUp: (e) => {
|
|
127271
|
+
e.preventDefault();
|
|
127272
|
+
try {
|
|
127273
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
127274
|
+
} catch {}
|
|
127275
|
+
stopPtt();
|
|
127276
|
+
},
|
|
127277
|
+
className: `shrink-0 mb-0.5 rounded-xl h-[42px] w-[42px] ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
|
|
127278
|
+
children: micState === "transcribing" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Mic, { className: "w-4 h-4" })
|
|
127279
|
+
}),
|
|
126978
127280
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
126979
127281
|
size: "icon",
|
|
126980
127282
|
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
|
|
@@ -126995,14 +127297,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126995
127297
|
className: "w-full"
|
|
126996
127298
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
126997
127299
|
className: "flex items-center justify-between",
|
|
126998
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.
|
|
126999
|
-
|
|
127000
|
-
|
|
127001
|
-
|
|
127002
|
-
|
|
127003
|
-
|
|
127004
|
-
|
|
127005
|
-
|
|
127300
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
127301
|
+
className: "flex items-center gap-2",
|
|
127302
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127303
|
+
variant: "ghost",
|
|
127304
|
+
size: "icon",
|
|
127305
|
+
title: "Attach files",
|
|
127306
|
+
disabled: chatSending,
|
|
127307
|
+
onClick: () => fileInputRef.current?.click(),
|
|
127308
|
+
className: "rounded-xl h-9 w-9",
|
|
127309
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Paperclip, { className: "w-4 h-4" })
|
|
127310
|
+
}), micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127311
|
+
variant: "ghost",
|
|
127312
|
+
size: "icon",
|
|
127313
|
+
title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
|
|
127314
|
+
disabled: chatSending || micState === "transcribing",
|
|
127315
|
+
onPointerDown: (e) => {
|
|
127316
|
+
e.preventDefault();
|
|
127317
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
127318
|
+
startPtt();
|
|
127319
|
+
},
|
|
127320
|
+
onPointerUp: (e) => {
|
|
127321
|
+
e.preventDefault();
|
|
127322
|
+
try {
|
|
127323
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
127324
|
+
} catch {}
|
|
127325
|
+
stopPtt();
|
|
127326
|
+
},
|
|
127327
|
+
className: `rounded-xl h-9 w-9 ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
|
|
127328
|
+
children: micState === "transcribing" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Mic, { className: "w-4 h-4" })
|
|
127329
|
+
})]
|
|
127006
127330
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127007
127331
|
size: "icon",
|
|
127008
127332
|
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
|
|
@@ -152321,6 +152645,368 @@ function AnalyticsView() {
|
|
|
152321
152645
|
});
|
|
152322
152646
|
}
|
|
152323
152647
|
//#endregion
|
|
152648
|
+
//#region src/components/views/insights.tsx
|
|
152649
|
+
var DEFAULT_CONFIG = {
|
|
152650
|
+
enabled: true,
|
|
152651
|
+
contextRatio: { enabled: true },
|
|
152652
|
+
introspection: {
|
|
152653
|
+
enabled: true,
|
|
152654
|
+
minTurns: 4,
|
|
152655
|
+
minContextRemaining: .15
|
|
152656
|
+
}
|
|
152657
|
+
};
|
|
152658
|
+
function compactJson(value) {
|
|
152659
|
+
if (!value || Object.keys(value).length === 0) return "—";
|
|
152660
|
+
return Object.entries(value).map(([k, v]) => `${k}: ${typeof v === "number" ? Math.round(v * 1e3) / 1e3 : String(v)}`).join(" ");
|
|
152661
|
+
}
|
|
152662
|
+
function InsightsView() {
|
|
152663
|
+
const [config, setConfig] = (0, import_react.useState)(DEFAULT_CONFIG);
|
|
152664
|
+
const [version, setVersion] = (0, import_react.useState)(0);
|
|
152665
|
+
const [observations, setObservations] = (0, import_react.useState)([]);
|
|
152666
|
+
const [stats, setStats] = (0, import_react.useState)([]);
|
|
152667
|
+
const [projects, setProjects] = (0, import_react.useState)([]);
|
|
152668
|
+
const [projectFilter, setProjectFilter] = (0, import_react.useState)("");
|
|
152669
|
+
const [signalFilter, setSignalFilter] = (0, import_react.useState)("");
|
|
152670
|
+
const [saving, setSaving] = (0, import_react.useState)(false);
|
|
152671
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
152672
|
+
const [now, setNow] = (0, import_react.useState)(() => Date.now());
|
|
152673
|
+
async function refresh() {
|
|
152674
|
+
setError(null);
|
|
152675
|
+
try {
|
|
152676
|
+
const params = new URLSearchParams();
|
|
152677
|
+
if (projectFilter) params.set("project", projectFilter);
|
|
152678
|
+
if (signalFilter) params.set("signal", signalFilter);
|
|
152679
|
+
const query = params.toString();
|
|
152680
|
+
const [cfg, obs] = await Promise.all([api("GET", "/insights/config"), api("GET", `/insights/observations${query ? `?${query}` : ""}`)]);
|
|
152681
|
+
setConfig(cfg.value);
|
|
152682
|
+
setVersion(cfg.version);
|
|
152683
|
+
setObservations(obs.observations);
|
|
152684
|
+
setStats(obs.stats);
|
|
152685
|
+
setProjects(obs.projects);
|
|
152686
|
+
setNow(Date.now());
|
|
152687
|
+
} catch (e) {
|
|
152688
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
152689
|
+
}
|
|
152690
|
+
}
|
|
152691
|
+
(0, import_react.useEffect)(() => {
|
|
152692
|
+
refresh();
|
|
152693
|
+
}, [projectFilter, signalFilter]);
|
|
152694
|
+
async function saveConfig(next) {
|
|
152695
|
+
setSaving(true);
|
|
152696
|
+
setError(null);
|
|
152697
|
+
const previous = config;
|
|
152698
|
+
setConfig(next);
|
|
152699
|
+
try {
|
|
152700
|
+
const entry = await api("PUT", "/insights/config", {
|
|
152701
|
+
value: next,
|
|
152702
|
+
updatedBy: "dashboard"
|
|
152703
|
+
});
|
|
152704
|
+
setConfig(entry.value);
|
|
152705
|
+
setVersion(entry.version);
|
|
152706
|
+
} catch (e) {
|
|
152707
|
+
setConfig(previous);
|
|
152708
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
152709
|
+
} finally {
|
|
152710
|
+
setSaving(false);
|
|
152711
|
+
}
|
|
152712
|
+
}
|
|
152713
|
+
const signals = (0, import_react.useMemo)(() => {
|
|
152714
|
+
return [...new Set(stats.map((s) => s.signal))].sort();
|
|
152715
|
+
}, [stats]);
|
|
152716
|
+
const globalStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project === null), [stats]);
|
|
152717
|
+
const projectStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project !== null), [stats]);
|
|
152718
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152719
|
+
className: "space-y-4 p-4",
|
|
152720
|
+
children: [
|
|
152721
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152722
|
+
className: "flex items-center justify-between",
|
|
152723
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152724
|
+
className: "flex items-center gap-2",
|
|
152725
|
+
children: [
|
|
152726
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Lightbulb, { className: "size-5 text-primary" }),
|
|
152727
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
|
|
152728
|
+
className: "text-lg font-semibold",
|
|
152729
|
+
children: "Insights"
|
|
152730
|
+
}),
|
|
152731
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
152732
|
+
className: "text-xs text-muted-foreground",
|
|
152733
|
+
children: "continuous self-improvement · epic #183"
|
|
152734
|
+
})
|
|
152735
|
+
]
|
|
152736
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
152737
|
+
variant: "outline",
|
|
152738
|
+
size: "sm",
|
|
152739
|
+
onClick: () => void refresh(),
|
|
152740
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { className: "size-4" }), " Refresh"]
|
|
152741
|
+
})]
|
|
152742
|
+
}),
|
|
152743
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152744
|
+
className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
|
|
152745
|
+
children: error
|
|
152746
|
+
}),
|
|
152747
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
|
|
152748
|
+
className: "flex items-center justify-between",
|
|
152749
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Features" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
152750
|
+
className: "text-xs font-normal text-muted-foreground",
|
|
152751
|
+
children: [
|
|
152752
|
+
"config v",
|
|
152753
|
+
version,
|
|
152754
|
+
" · saved by ",
|
|
152755
|
+
config && "dashboard"
|
|
152756
|
+
]
|
|
152757
|
+
})]
|
|
152758
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardContent, {
|
|
152759
|
+
className: "space-y-4",
|
|
152760
|
+
children: [
|
|
152761
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152762
|
+
className: "flex items-center justify-between",
|
|
152763
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152764
|
+
className: "text-sm font-medium",
|
|
152765
|
+
children: "Insights enabled"
|
|
152766
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152767
|
+
className: "text-xs text-muted-foreground",
|
|
152768
|
+
children: "Master switch. When off, no signals are recorded regardless of the toggles below."
|
|
152769
|
+
})] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
|
|
152770
|
+
checked: config.enabled,
|
|
152771
|
+
disabled: saving,
|
|
152772
|
+
onCheckedChange: (v) => void saveConfig({
|
|
152773
|
+
...config,
|
|
152774
|
+
enabled: v
|
|
152775
|
+
})
|
|
152776
|
+
})]
|
|
152777
|
+
}),
|
|
152778
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152779
|
+
className: "flex items-center justify-between border-t border-border pt-3",
|
|
152780
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152781
|
+
className: "text-sm font-medium",
|
|
152782
|
+
children: ["Context-gathering ratio ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
152783
|
+
className: "text-xs text-muted-foreground",
|
|
152784
|
+
children: "(#184)"
|
|
152785
|
+
})]
|
|
152786
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152787
|
+
className: "text-xs text-muted-foreground",
|
|
152788
|
+
children: "Server-side: read/search vs. action before first substantive move, per session."
|
|
152789
|
+
})] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
|
|
152790
|
+
checked: config.contextRatio.enabled,
|
|
152791
|
+
disabled: saving || !config.enabled,
|
|
152792
|
+
onCheckedChange: (v) => void saveConfig({
|
|
152793
|
+
...config,
|
|
152794
|
+
contextRatio: { enabled: v }
|
|
152795
|
+
})
|
|
152796
|
+
})]
|
|
152797
|
+
}),
|
|
152798
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152799
|
+
className: "border-t border-border pt-3",
|
|
152800
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152801
|
+
className: "flex items-center justify-between",
|
|
152802
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152803
|
+
className: "text-sm font-medium",
|
|
152804
|
+
children: ["End-of-session introspection ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
152805
|
+
className: "text-xs text-muted-foreground",
|
|
152806
|
+
children: "(#185)"
|
|
152807
|
+
})]
|
|
152808
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152809
|
+
className: "text-xs text-muted-foreground",
|
|
152810
|
+
children: "Agent-authored 3-field artifact at session end. Gated below."
|
|
152811
|
+
})] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
|
|
152812
|
+
checked: config.introspection.enabled,
|
|
152813
|
+
disabled: saving || !config.enabled,
|
|
152814
|
+
onCheckedChange: (v) => void saveConfig({
|
|
152815
|
+
...config,
|
|
152816
|
+
introspection: {
|
|
152817
|
+
...config.introspection,
|
|
152818
|
+
enabled: v
|
|
152819
|
+
}
|
|
152820
|
+
})
|
|
152821
|
+
})]
|
|
152822
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152823
|
+
className: "mt-3 flex flex-wrap gap-4",
|
|
152824
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
|
|
152825
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
152826
|
+
children: ["min turns", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
152827
|
+
type: "number",
|
|
152828
|
+
className: "h-7 w-20",
|
|
152829
|
+
value: config.introspection.minTurns,
|
|
152830
|
+
disabled: saving || !config.enabled || !config.introspection.enabled,
|
|
152831
|
+
onChange: (e) => setConfig({
|
|
152832
|
+
...config,
|
|
152833
|
+
introspection: {
|
|
152834
|
+
...config.introspection,
|
|
152835
|
+
minTurns: Number(e.target.value)
|
|
152836
|
+
}
|
|
152837
|
+
}),
|
|
152838
|
+
onBlur: () => void saveConfig(config)
|
|
152839
|
+
})]
|
|
152840
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
|
|
152841
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
152842
|
+
children: ["min context remaining (0–1)", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
152843
|
+
type: "number",
|
|
152844
|
+
step: "0.05",
|
|
152845
|
+
min: "0",
|
|
152846
|
+
max: "1",
|
|
152847
|
+
className: "h-7 w-24",
|
|
152848
|
+
value: config.introspection.minContextRemaining,
|
|
152849
|
+
disabled: saving || !config.enabled || !config.introspection.enabled,
|
|
152850
|
+
onChange: (e) => setConfig({
|
|
152851
|
+
...config,
|
|
152852
|
+
introspection: {
|
|
152853
|
+
...config.introspection,
|
|
152854
|
+
minContextRemaining: Number(e.target.value)
|
|
152855
|
+
}
|
|
152856
|
+
}),
|
|
152857
|
+
onBlur: () => void saveConfig(config)
|
|
152858
|
+
})]
|
|
152859
|
+
})]
|
|
152860
|
+
})]
|
|
152861
|
+
})
|
|
152862
|
+
]
|
|
152863
|
+
})] }),
|
|
152864
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, { children: "Signals" }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: globalStats.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152865
|
+
className: "text-sm text-muted-foreground",
|
|
152866
|
+
children: "No observations yet — they appear after sessions end with Insights enabled."
|
|
152867
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152868
|
+
className: "space-y-4",
|
|
152869
|
+
children: globalStats.map((g) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152870
|
+
className: "mb-1 flex items-center gap-2",
|
|
152871
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
|
|
152872
|
+
variant: "secondary",
|
|
152873
|
+
children: g.signal
|
|
152874
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
152875
|
+
className: "text-xs text-muted-foreground",
|
|
152876
|
+
children: [
|
|
152877
|
+
g.count,
|
|
152878
|
+
" obs · ",
|
|
152879
|
+
g.avgRatio !== null ? `avg ratio ${g.avgRatio.toFixed(2)}` : "no ratio",
|
|
152880
|
+
" · ",
|
|
152881
|
+
timeAgo(now, g.lastAt)
|
|
152882
|
+
]
|
|
152883
|
+
})]
|
|
152884
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152885
|
+
className: "flex flex-wrap gap-2 pl-2",
|
|
152886
|
+
children: projectStats.filter((p) => p.signal === g.signal).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
152887
|
+
className: "rounded border border-border px-2 py-0.5 text-xs text-muted-foreground",
|
|
152888
|
+
children: [
|
|
152889
|
+
p.project,
|
|
152890
|
+
": ",
|
|
152891
|
+
p.count,
|
|
152892
|
+
p.avgRatio !== null ? ` · ${p.avgRatio.toFixed(2)}` : ""
|
|
152893
|
+
]
|
|
152894
|
+
}, `${g.signal}-${p.project}`))
|
|
152895
|
+
})] }, g.signal))
|
|
152896
|
+
}) })] }),
|
|
152897
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
|
|
152898
|
+
className: "flex flex-wrap items-center gap-3",
|
|
152899
|
+
children: [
|
|
152900
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Observations" }),
|
|
152901
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
152902
|
+
className: "h-7 rounded border border-border bg-background px-2 text-xs",
|
|
152903
|
+
value: projectFilter,
|
|
152904
|
+
onChange: (e) => setProjectFilter(e.target.value),
|
|
152905
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
152906
|
+
value: "",
|
|
152907
|
+
children: "all projects"
|
|
152908
|
+
}), projects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
152909
|
+
value: p,
|
|
152910
|
+
children: p
|
|
152911
|
+
}, p))]
|
|
152912
|
+
}),
|
|
152913
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
152914
|
+
className: "h-7 rounded border border-border bg-background px-2 text-xs",
|
|
152915
|
+
value: signalFilter,
|
|
152916
|
+
onChange: (e) => setSignalFilter(e.target.value),
|
|
152917
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
152918
|
+
value: "",
|
|
152919
|
+
children: "all signals"
|
|
152920
|
+
}), signals.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
152921
|
+
value: s,
|
|
152922
|
+
children: s
|
|
152923
|
+
}, s))]
|
|
152924
|
+
})
|
|
152925
|
+
]
|
|
152926
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: observations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152927
|
+
className: "text-sm text-muted-foreground",
|
|
152928
|
+
children: "No observations match."
|
|
152929
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152930
|
+
className: "overflow-x-auto",
|
|
152931
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
|
|
152932
|
+
className: "w-full text-left text-xs",
|
|
152933
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", {
|
|
152934
|
+
className: "text-muted-foreground",
|
|
152935
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
|
|
152936
|
+
className: "border-b border-border",
|
|
152937
|
+
children: [
|
|
152938
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
152939
|
+
className: "py-1 pr-3 font-medium",
|
|
152940
|
+
children: "when"
|
|
152941
|
+
}),
|
|
152942
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
152943
|
+
className: "py-1 pr-3 font-medium",
|
|
152944
|
+
children: "project"
|
|
152945
|
+
}),
|
|
152946
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
152947
|
+
className: "py-1 pr-3 font-medium",
|
|
152948
|
+
children: "signal"
|
|
152949
|
+
}),
|
|
152950
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
152951
|
+
className: "py-1 pr-3 font-medium",
|
|
152952
|
+
children: "src"
|
|
152953
|
+
}),
|
|
152954
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
152955
|
+
className: "py-1 pr-3 font-medium",
|
|
152956
|
+
children: "value"
|
|
152957
|
+
}),
|
|
152958
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
152959
|
+
className: "py-1 pr-3 font-medium",
|
|
152960
|
+
children: "outcome"
|
|
152961
|
+
}),
|
|
152962
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
152963
|
+
className: "py-1 pr-3 font-medium",
|
|
152964
|
+
children: "session"
|
|
152965
|
+
})
|
|
152966
|
+
]
|
|
152967
|
+
})
|
|
152968
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: observations.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
|
|
152969
|
+
className: "border-b border-border/50",
|
|
152970
|
+
children: [
|
|
152971
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
152972
|
+
className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
|
|
152973
|
+
children: timeAgo(now, o.createdAt)
|
|
152974
|
+
}),
|
|
152975
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
152976
|
+
className: "py-1 pr-3 whitespace-nowrap",
|
|
152977
|
+
children: o.project
|
|
152978
|
+
}),
|
|
152979
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
152980
|
+
className: "py-1 pr-3 whitespace-nowrap",
|
|
152981
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
|
|
152982
|
+
variant: "outline",
|
|
152983
|
+
children: o.signal
|
|
152984
|
+
})
|
|
152985
|
+
}),
|
|
152986
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
152987
|
+
className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
|
|
152988
|
+
children: o.source
|
|
152989
|
+
}),
|
|
152990
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
152991
|
+
className: "py-1 pr-3 font-mono",
|
|
152992
|
+
children: compactJson(o.value)
|
|
152993
|
+
}),
|
|
152994
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
152995
|
+
className: "py-1 pr-3 font-mono text-muted-foreground",
|
|
152996
|
+
children: compactJson(o.outcome)
|
|
152997
|
+
}),
|
|
152998
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
152999
|
+
className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
|
|
153000
|
+
children: o.agentId ?? o.sessionId.slice(0, 12)
|
|
153001
|
+
})
|
|
153002
|
+
]
|
|
153003
|
+
}, o.id)) })]
|
|
153004
|
+
})
|
|
153005
|
+
}) })] })
|
|
153006
|
+
]
|
|
153007
|
+
});
|
|
153008
|
+
}
|
|
153009
|
+
//#endregion
|
|
152324
153010
|
//#region src/components/views/maintenance.tsx
|
|
152325
153011
|
var STATUS_CLASS = {
|
|
152326
153012
|
succeeded: "bg-emerald-500/10 text-emerald-400 border-emerald-500/30",
|
|
@@ -156900,6 +157586,7 @@ var views = {
|
|
|
156900
157586
|
tasks: TasksView,
|
|
156901
157587
|
automation: AutomationView,
|
|
156902
157588
|
analytics: AnalyticsView,
|
|
157589
|
+
insights: InsightsView,
|
|
156903
157590
|
maintenance: MaintenanceView,
|
|
156904
157591
|
settings: SettingsView
|
|
156905
157592
|
};
|
|
@@ -157918,6 +158605,11 @@ if ("serviceWorker" in navigator) {
|
|
|
157918
158605
|
height: calc(var(--spacing) * 4);
|
|
157919
158606
|
}
|
|
157920
158607
|
|
|
158608
|
+
.size-5 {
|
|
158609
|
+
width: calc(var(--spacing) * 5);
|
|
158610
|
+
height: calc(var(--spacing) * 5);
|
|
158611
|
+
}
|
|
158612
|
+
|
|
157921
158613
|
.size-6 {
|
|
157922
158614
|
width: calc(var(--spacing) * 6);
|
|
157923
158615
|
height: calc(var(--spacing) * 6);
|
|
@@ -158239,6 +158931,14 @@ if ("serviceWorker" in navigator) {
|
|
|
158239
158931
|
width: calc(var(--spacing) * 14);
|
|
158240
158932
|
}
|
|
158241
158933
|
|
|
158934
|
+
.w-20 {
|
|
158935
|
+
width: calc(var(--spacing) * 20);
|
|
158936
|
+
}
|
|
158937
|
+
|
|
158938
|
+
.w-24 {
|
|
158939
|
+
width: calc(var(--spacing) * 24);
|
|
158940
|
+
}
|
|
158941
|
+
|
|
158242
158942
|
.w-40 {
|
|
158243
158943
|
width: calc(var(--spacing) * 40);
|
|
158244
158944
|
}
|
|
@@ -160147,6 +160847,10 @@ if ("serviceWorker" in navigator) {
|
|
|
160147
160847
|
padding-top: calc(var(--spacing) * 2);
|
|
160148
160848
|
}
|
|
160149
160849
|
|
|
160850
|
+
.pt-3 {
|
|
160851
|
+
padding-top: calc(var(--spacing) * 3);
|
|
160852
|
+
}
|
|
160853
|
+
|
|
160150
160854
|
.pt-4 {
|
|
160151
160855
|
padding-top: calc(var(--spacing) * 4);
|
|
160152
160856
|
}
|
|
@@ -160159,6 +160863,10 @@ if ("serviceWorker" in navigator) {
|
|
|
160159
160863
|
padding-right: calc(var(--spacing) * 2);
|
|
160160
160864
|
}
|
|
160161
160865
|
|
|
160866
|
+
.pr-3 {
|
|
160867
|
+
padding-right: calc(var(--spacing) * 3);
|
|
160868
|
+
}
|
|
160869
|
+
|
|
160162
160870
|
.pr-4 {
|
|
160163
160871
|
padding-right: calc(var(--spacing) * 4);
|
|
160164
160872
|
}
|