oh-my-opencode-dashboard 0.0.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/README.md +100 -0
- package/dashboard-ui.png +0 -0
- package/dist/assets/index-D6OVzN1o.css +1 -0
- package/dist/assets/index-SEmwze_4.js +40 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +51 -0
- package/src/App.tsx +518 -0
- package/src/cli/dev.ts +139 -0
- package/src/cli/ports.test.ts +40 -0
- package/src/cli/ports.ts +43 -0
- package/src/ding-policy.test.ts +48 -0
- package/src/ding-policy.ts +39 -0
- package/src/ingest/background-tasks.test.ts +707 -0
- package/src/ingest/background-tasks.ts +317 -0
- package/src/ingest/boulder.test.ts +77 -0
- package/src/ingest/boulder.ts +71 -0
- package/src/ingest/paths.test.ts +82 -0
- package/src/ingest/paths.ts +76 -0
- package/src/ingest/session.test.ts +220 -0
- package/src/ingest/session.ts +283 -0
- package/src/main.tsx +10 -0
- package/src/server/api.test.ts +62 -0
- package/src/server/api.ts +16 -0
- package/src/server/build.ts +5 -0
- package/src/server/dashboard.test.ts +135 -0
- package/src/server/dashboard.ts +191 -0
- package/src/server/dev.ts +44 -0
- package/src/server/start.ts +93 -0
- package/src/sound.test.ts +55 -0
- package/src/sound.ts +89 -0
- package/src/styles.css +457 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oh-my-opencode-dashboard",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Local-only, read-only dashboard for viewing OhMyOpenCode agent progress",
|
|
5
|
+
"license": "SUL-1.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"oh-my-opencode-dashboard": "./src/server/start.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"index.html",
|
|
14
|
+
"vite.config.ts",
|
|
15
|
+
"tsconfig.json",
|
|
16
|
+
"dashboard-ui.png",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"bun": ">=1.1.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "bun run src/cli/dev.ts",
|
|
27
|
+
"dev:api": "bun run src/server/dev.ts",
|
|
28
|
+
"dev:ui": "vite",
|
|
29
|
+
"build": "bun run build:ui && bun run build:api",
|
|
30
|
+
"build:ui": "vite build",
|
|
31
|
+
"build:api": "bun run src/server/build.ts",
|
|
32
|
+
"prepublishOnly": "bun run build && bun test",
|
|
33
|
+
"start": "bun run src/server/start.ts",
|
|
34
|
+
"test": "vitest",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"hono": "^4.4.12",
|
|
39
|
+
"react": "^18.2.0",
|
|
40
|
+
"react-dom": "^18.2.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/react": "^18.2.72",
|
|
44
|
+
"@types/react-dom": "^18.2.24",
|
|
45
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
46
|
+
"bun-types": "^1.1.20",
|
|
47
|
+
"typescript": "^5.5.4",
|
|
48
|
+
"vite": "^5.4.2",
|
|
49
|
+
"vitest": "^1.6.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { computeWaitingDing } from "./ding-policy";
|
|
3
|
+
import { playDing, unlockAudio } from "./sound";
|
|
4
|
+
|
|
5
|
+
type BackgroundTask = {
|
|
6
|
+
id: string;
|
|
7
|
+
description: string;
|
|
8
|
+
subline?: string;
|
|
9
|
+
agent: string;
|
|
10
|
+
status: "queued" | "running" | "done" | "error" | "cancelled" | string;
|
|
11
|
+
toolCalls: number;
|
|
12
|
+
lastTool: string;
|
|
13
|
+
timeline: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type DashboardPayload = {
|
|
17
|
+
mainSession: {
|
|
18
|
+
agent: string;
|
|
19
|
+
currentTool: string;
|
|
20
|
+
lastUpdatedLabel: string;
|
|
21
|
+
session: string;
|
|
22
|
+
statusPill: string;
|
|
23
|
+
};
|
|
24
|
+
planProgress: {
|
|
25
|
+
name: string;
|
|
26
|
+
completed: number;
|
|
27
|
+
total: number;
|
|
28
|
+
path: string;
|
|
29
|
+
statusPill: string;
|
|
30
|
+
};
|
|
31
|
+
backgroundTasks: BackgroundTask[];
|
|
32
|
+
raw: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const FALLBACK_DATA: DashboardPayload = {
|
|
36
|
+
mainSession: {
|
|
37
|
+
agent: "sisyphus",
|
|
38
|
+
currentTool: "dashboard_start",
|
|
39
|
+
lastUpdatedLabel: "just now",
|
|
40
|
+
session: "qa-session",
|
|
41
|
+
statusPill: "busy",
|
|
42
|
+
},
|
|
43
|
+
planProgress: {
|
|
44
|
+
name: "agent-dashboard",
|
|
45
|
+
completed: 4,
|
|
46
|
+
total: 7,
|
|
47
|
+
path: "/tmp/agent-dashboard.md",
|
|
48
|
+
statusPill: "in progress",
|
|
49
|
+
},
|
|
50
|
+
backgroundTasks: [
|
|
51
|
+
{
|
|
52
|
+
id: "task-1",
|
|
53
|
+
description: "Explore: find HTTP/SSE patterns",
|
|
54
|
+
subline: "task-1",
|
|
55
|
+
agent: "explore",
|
|
56
|
+
status: "running",
|
|
57
|
+
toolCalls: 3,
|
|
58
|
+
lastTool: "grep",
|
|
59
|
+
timeline: "2026-01-01T00:00:00Z: 2m",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
raw: {
|
|
63
|
+
ok: false,
|
|
64
|
+
hint: "API not reachable yet. Using placeholder data.",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function clampPercent(value: number): number {
|
|
69
|
+
if (Number.isNaN(value)) return 0;
|
|
70
|
+
return Math.max(0, Math.min(100, value));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatTime(ts: number | null): string {
|
|
74
|
+
if (!ts) return "never";
|
|
75
|
+
try {
|
|
76
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
77
|
+
year: "numeric",
|
|
78
|
+
month: "short",
|
|
79
|
+
day: "2-digit",
|
|
80
|
+
hour: "2-digit",
|
|
81
|
+
minute: "2-digit",
|
|
82
|
+
second: "2-digit",
|
|
83
|
+
}).format(new Date(ts));
|
|
84
|
+
} catch {
|
|
85
|
+
return new Date(ts).toLocaleString();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function statusTone(status: string): "teal" | "sand" | "red" {
|
|
90
|
+
const s = status.toLowerCase();
|
|
91
|
+
if (s.includes("error") || s.includes("fail")) return "red";
|
|
92
|
+
if (s.includes("run") || s.includes("progress") || s.includes("busy") || s.includes("think")) return "teal";
|
|
93
|
+
return "sand";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function safeFetchJson(url: string): Promise<unknown> {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const t = window.setTimeout(() => controller.abort(), 1600);
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
method: "GET",
|
|
102
|
+
headers: { Accept: "application/json" },
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
106
|
+
return await res.json();
|
|
107
|
+
} finally {
|
|
108
|
+
window.clearTimeout(t);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toDashboardPayload(json: unknown): DashboardPayload {
|
|
113
|
+
if (!json || typeof json !== "object") {
|
|
114
|
+
return { ...FALLBACK_DATA, raw: json };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const anyJson = json as Record<string, unknown>;
|
|
118
|
+
|
|
119
|
+
const main = (anyJson.mainSession ?? anyJson.main_session ?? {}) as Record<string, unknown>;
|
|
120
|
+
const plan = (anyJson.planProgress ?? anyJson.plan_progress ?? {}) as Record<string, unknown>;
|
|
121
|
+
const tasks = (anyJson.backgroundTasks ?? anyJson.background_tasks ?? []) as unknown;
|
|
122
|
+
|
|
123
|
+
const backgroundTasks: BackgroundTask[] = Array.isArray(tasks)
|
|
124
|
+
? tasks.map((t, idx) => {
|
|
125
|
+
const rec = (t ?? {}) as Record<string, unknown>;
|
|
126
|
+
return {
|
|
127
|
+
id: String(rec.id ?? rec.taskId ?? rec.task_id ?? `task-${idx + 1}`),
|
|
128
|
+
description: String(rec.description ?? rec.name ?? "(no description)"),
|
|
129
|
+
subline:
|
|
130
|
+
typeof rec.subline === "string"
|
|
131
|
+
? rec.subline
|
|
132
|
+
: typeof rec.taskId === "string"
|
|
133
|
+
? rec.taskId
|
|
134
|
+
: undefined,
|
|
135
|
+
agent: String(rec.agent ?? rec.worker ?? "unknown"),
|
|
136
|
+
status: String(rec.status ?? "queued"),
|
|
137
|
+
toolCalls: Number(rec.toolCalls ?? rec.tool_calls ?? 0) || 0,
|
|
138
|
+
lastTool: String(rec.lastTool ?? rec.last_tool ?? "-") || "-",
|
|
139
|
+
timeline: String(rec.timeline ?? "") || "",
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
: FALLBACK_DATA.backgroundTasks;
|
|
143
|
+
|
|
144
|
+
const completed = Number(plan.completed ?? plan.done ?? 0) || 0;
|
|
145
|
+
const total = Number(plan.total ?? plan.count ?? 0) || 0;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
mainSession: {
|
|
149
|
+
agent: String(main.agent ?? FALLBACK_DATA.mainSession.agent),
|
|
150
|
+
currentTool: String(main.currentTool ?? main.current_tool ?? FALLBACK_DATA.mainSession.currentTool),
|
|
151
|
+
lastUpdatedLabel: String(main.lastUpdatedLabel ?? main.last_updated ?? "just now"),
|
|
152
|
+
session: String(main.session ?? main.session_id ?? FALLBACK_DATA.mainSession.session),
|
|
153
|
+
statusPill: String(main.statusPill ?? main.status ?? FALLBACK_DATA.mainSession.statusPill),
|
|
154
|
+
},
|
|
155
|
+
planProgress: {
|
|
156
|
+
name: String(plan.name ?? FALLBACK_DATA.planProgress.name),
|
|
157
|
+
completed: total ? Math.min(completed, total) : completed,
|
|
158
|
+
total,
|
|
159
|
+
path: String(plan.path ?? FALLBACK_DATA.planProgress.path),
|
|
160
|
+
statusPill: String(plan.statusPill ?? plan.status ?? FALLBACK_DATA.planProgress.statusPill),
|
|
161
|
+
},
|
|
162
|
+
backgroundTasks,
|
|
163
|
+
raw: json,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default function App() {
|
|
168
|
+
const [connected, setConnected] = React.useState(false);
|
|
169
|
+
const [data, setData] = React.useState<DashboardPayload>(FALLBACK_DATA);
|
|
170
|
+
const [lastUpdate, setLastUpdate] = React.useState<number | null>(null);
|
|
171
|
+
const [copyState, setCopyState] = React.useState<"idle" | "ok" | "err">("idle");
|
|
172
|
+
const [soundEnabled, setSoundEnabled] = React.useState(false);
|
|
173
|
+
const [soundUnlocked, setSoundUnlocked] = React.useState(false);
|
|
174
|
+
const [errorHint, setErrorHint] = React.useState<string | null>(null);
|
|
175
|
+
|
|
176
|
+
const timerRef = React.useRef<number | null>(null);
|
|
177
|
+
const hadSuccessRef = React.useRef(false);
|
|
178
|
+
const soundEnabledRef = React.useRef(false);
|
|
179
|
+
const prevWaitingRef = React.useRef<boolean | null>(null);
|
|
180
|
+
const lastLeftWaitingAtRef = React.useRef<number | null>(null);
|
|
181
|
+
const prevPlanCompletedRef = React.useRef<number | null>(null);
|
|
182
|
+
const prevPlanTotalRef = React.useRef<number | null>(null);
|
|
183
|
+
|
|
184
|
+
const servedFrom = React.useMemo(() => {
|
|
185
|
+
if (typeof window === "undefined") return "";
|
|
186
|
+
return window.location.origin;
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
React.useEffect(() => {
|
|
190
|
+
soundEnabledRef.current = soundEnabled;
|
|
191
|
+
}, [soundEnabled]);
|
|
192
|
+
|
|
193
|
+
React.useEffect(() => {
|
|
194
|
+
try {
|
|
195
|
+
const raw = window.localStorage.getItem("omoDashboardSoundEnabled");
|
|
196
|
+
if (raw === "1") {
|
|
197
|
+
setSoundEnabled(true);
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// ignore
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
async function enableSound(next: boolean) {
|
|
205
|
+
if (!next) {
|
|
206
|
+
setSoundEnabled(false);
|
|
207
|
+
setSoundUnlocked(false);
|
|
208
|
+
try {
|
|
209
|
+
window.localStorage.setItem("omoDashboardSoundEnabled", "0");
|
|
210
|
+
} catch {
|
|
211
|
+
// ignore
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const ok = await unlockAudio();
|
|
217
|
+
setSoundUnlocked(ok);
|
|
218
|
+
setSoundEnabled(ok);
|
|
219
|
+
try {
|
|
220
|
+
window.localStorage.setItem("omoDashboardSoundEnabled", ok ? "1" : "0");
|
|
221
|
+
} catch {
|
|
222
|
+
// ignore
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isWaitingForUser(payload: DashboardPayload): boolean {
|
|
227
|
+
const status = payload.mainSession.statusPill.toLowerCase();
|
|
228
|
+
const hasSession = payload.mainSession.session !== "(no session)" && payload.mainSession.session !== "";
|
|
229
|
+
const idle = status.includes("idle");
|
|
230
|
+
const noTool = payload.mainSession.currentTool === "-" || payload.mainSession.currentTool === "";
|
|
231
|
+
return hasSession && idle && noTool;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function maybePlayDings(next: DashboardPayload) {
|
|
235
|
+
if (!soundEnabledRef.current) return;
|
|
236
|
+
if (!hadSuccessRef.current) return;
|
|
237
|
+
|
|
238
|
+
const nowMs = Date.now();
|
|
239
|
+
const suppressFastIdleRoundTripMs = 20_000;
|
|
240
|
+
|
|
241
|
+
const waiting = isWaitingForUser(next);
|
|
242
|
+
const prevWaiting = prevWaitingRef.current;
|
|
243
|
+
|
|
244
|
+
const completed = next.planProgress.completed;
|
|
245
|
+
const total = next.planProgress.total;
|
|
246
|
+
const prevCompleted = prevPlanCompletedRef.current;
|
|
247
|
+
const prevTotal = prevPlanTotalRef.current;
|
|
248
|
+
|
|
249
|
+
const wasComplete = typeof prevCompleted === "number" && typeof prevTotal === "number" && prevTotal > 0 && prevCompleted >= prevTotal;
|
|
250
|
+
const isComplete = total > 0 && completed >= total;
|
|
251
|
+
|
|
252
|
+
if (!wasComplete && isComplete) {
|
|
253
|
+
void playDing("all");
|
|
254
|
+
} else if (typeof prevCompleted === "number" && typeof prevTotal === "number") {
|
|
255
|
+
const samePlan = total === prevTotal;
|
|
256
|
+
if (samePlan && completed > prevCompleted) {
|
|
257
|
+
void playDing("task");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const waitingDecision = computeWaitingDing({
|
|
262
|
+
prev: {
|
|
263
|
+
prevWaiting,
|
|
264
|
+
lastLeftWaitingAtMs: lastLeftWaitingAtRef.current,
|
|
265
|
+
},
|
|
266
|
+
waiting,
|
|
267
|
+
nowMs,
|
|
268
|
+
suppressMs: suppressFastIdleRoundTripMs,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (waitingDecision.play) {
|
|
272
|
+
void playDing("waiting");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
prevWaitingRef.current = waitingDecision.next.prevWaiting;
|
|
276
|
+
lastLeftWaitingAtRef.current = waitingDecision.next.lastLeftWaitingAtMs;
|
|
277
|
+
prevPlanCompletedRef.current = completed;
|
|
278
|
+
prevPlanTotalRef.current = total;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const planPercent = React.useMemo(() => {
|
|
282
|
+
if (!data.planProgress.total) return 0;
|
|
283
|
+
return clampPercent((data.planProgress.completed / data.planProgress.total) * 100);
|
|
284
|
+
}, [data.planProgress.completed, data.planProgress.total]);
|
|
285
|
+
|
|
286
|
+
const rawJsonText = React.useMemo(() => {
|
|
287
|
+
return JSON.stringify(data.raw, null, 2);
|
|
288
|
+
}, [data.raw]);
|
|
289
|
+
|
|
290
|
+
React.useEffect(() => {
|
|
291
|
+
let alive = true;
|
|
292
|
+
|
|
293
|
+
async function tick() {
|
|
294
|
+
let nextConnected = false;
|
|
295
|
+
try {
|
|
296
|
+
const json = await safeFetchJson("/api/dashboard");
|
|
297
|
+
if (!alive) return;
|
|
298
|
+
nextConnected = true;
|
|
299
|
+
hadSuccessRef.current = true;
|
|
300
|
+
setConnected(true);
|
|
301
|
+
setErrorHint(null);
|
|
302
|
+
const next = toDashboardPayload(json);
|
|
303
|
+
maybePlayDings(next);
|
|
304
|
+
setData(next);
|
|
305
|
+
setLastUpdate(Date.now());
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (!alive) return;
|
|
308
|
+
nextConnected = false;
|
|
309
|
+
setConnected(false);
|
|
310
|
+
const msg = err instanceof Error ? err.message : "disconnected";
|
|
311
|
+
setErrorHint(msg);
|
|
312
|
+
setData((prev) => {
|
|
313
|
+
if (!hadSuccessRef.current) return FALLBACK_DATA;
|
|
314
|
+
return {
|
|
315
|
+
...prev,
|
|
316
|
+
raw: {
|
|
317
|
+
ok: false,
|
|
318
|
+
disconnected: true,
|
|
319
|
+
error: msg,
|
|
320
|
+
note: "Showing last known UI values.",
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
} finally {
|
|
325
|
+
const delay = nextConnected ? 2200 : 3600;
|
|
326
|
+
timerRef.current = window.setTimeout(tick, delay);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
tick();
|
|
331
|
+
|
|
332
|
+
return () => {
|
|
333
|
+
alive = false;
|
|
334
|
+
if (timerRef.current) window.clearTimeout(timerRef.current);
|
|
335
|
+
};
|
|
336
|
+
}, []);
|
|
337
|
+
|
|
338
|
+
async function onCopyRawJson() {
|
|
339
|
+
setCopyState("idle");
|
|
340
|
+
try {
|
|
341
|
+
await navigator.clipboard.writeText(rawJsonText);
|
|
342
|
+
setCopyState("ok");
|
|
343
|
+
window.setTimeout(() => setCopyState("idle"), 1200);
|
|
344
|
+
} catch {
|
|
345
|
+
window.prompt("Copy raw JSON:", rawJsonText);
|
|
346
|
+
setCopyState("ok");
|
|
347
|
+
window.setTimeout(() => setCopyState("idle"), 1200);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const liveLabel = connected ? "Live" : "Disconnected";
|
|
352
|
+
const liveTone = connected ? "teal" : "sand";
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div className="page">
|
|
356
|
+
<div className="container">
|
|
357
|
+
<header className="topbar">
|
|
358
|
+
<div className="brand">
|
|
359
|
+
<div className="brandMark" aria-hidden="true" />
|
|
360
|
+
<div className="brandText">
|
|
361
|
+
<h1>Agent Dashboard</h1>
|
|
362
|
+
<p>
|
|
363
|
+
Live view (no prompts or tool arguments rendered).
|
|
364
|
+
{!connected && errorHint ? <span className="hint"> - {errorHint}</span> : null}
|
|
365
|
+
</p>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
<div className="topbarActions">
|
|
369
|
+
<span className={`pill pill-${liveTone}`}>
|
|
370
|
+
<span className="pillDot" aria-hidden="true" />
|
|
371
|
+
{liveLabel}
|
|
372
|
+
</span>
|
|
373
|
+
<button
|
|
374
|
+
className="button"
|
|
375
|
+
type="button"
|
|
376
|
+
onClick={() => void enableSound(!soundEnabled)}
|
|
377
|
+
aria-pressed={soundEnabled}
|
|
378
|
+
title={soundEnabled ? "Disable sound" : "Enable sound"}
|
|
379
|
+
>
|
|
380
|
+
Sound {soundEnabled ? (soundUnlocked ? "On" : "On") : "Off"}
|
|
381
|
+
</button>
|
|
382
|
+
<button
|
|
383
|
+
className="button"
|
|
384
|
+
type="button"
|
|
385
|
+
onClick={() => void playDing("task")}
|
|
386
|
+
title="Play ding"
|
|
387
|
+
aria-label="Play ding sound"
|
|
388
|
+
>
|
|
389
|
+
Ding
|
|
390
|
+
</button>
|
|
391
|
+
<button className="button" type="button" onClick={onCopyRawJson}>
|
|
392
|
+
{copyState === "ok" ? "Copied" : copyState === "err" ? "Copy failed" : "Copy raw JSON"}
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
</header>
|
|
396
|
+
|
|
397
|
+
<main className="stack">
|
|
398
|
+
<section className="grid2">
|
|
399
|
+
<article className="card">
|
|
400
|
+
<div className="cardHeader">
|
|
401
|
+
<h2>Main session</h2>
|
|
402
|
+
<span className={`pill pill-${statusTone(data.mainSession.statusPill)}`}>{data.mainSession.statusPill}</span>
|
|
403
|
+
</div>
|
|
404
|
+
<div className="kv">
|
|
405
|
+
<div className="kvRow">
|
|
406
|
+
<div className="kvKey">AGENT</div>
|
|
407
|
+
<div className="kvVal mono">{data.mainSession.agent}</div>
|
|
408
|
+
</div>
|
|
409
|
+
<div className="kvRow">
|
|
410
|
+
<div className="kvKey">CURRENT TOOL</div>
|
|
411
|
+
<div className="kvVal mono">{data.mainSession.currentTool}</div>
|
|
412
|
+
</div>
|
|
413
|
+
<div className="kvRow">
|
|
414
|
+
<div className="kvKey">LAST UPDATED</div>
|
|
415
|
+
<div className="kvVal">{data.mainSession.lastUpdatedLabel}</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
<div className="divider" />
|
|
419
|
+
<div className="kvRow">
|
|
420
|
+
<div className="kvKey">SESSION</div>
|
|
421
|
+
<div className="kvVal mono">{data.mainSession.session}</div>
|
|
422
|
+
</div>
|
|
423
|
+
</article>
|
|
424
|
+
|
|
425
|
+
<article className="card">
|
|
426
|
+
<div className="cardHeader">
|
|
427
|
+
<h2>Plan progress</h2>
|
|
428
|
+
<span className={`pill pill-${statusTone(data.planProgress.statusPill)}`}>{data.planProgress.statusPill}</span>
|
|
429
|
+
</div>
|
|
430
|
+
<div className="kv">
|
|
431
|
+
<div className="kvRow">
|
|
432
|
+
<div className="kvKey">NAME</div>
|
|
433
|
+
<div className="kvVal mono">{data.planProgress.name}</div>
|
|
434
|
+
</div>
|
|
435
|
+
<div className="kvRow">
|
|
436
|
+
<div className="kvKey">PROGRESS</div>
|
|
437
|
+
<div className="kvVal">
|
|
438
|
+
<span className="mono">
|
|
439
|
+
{data.planProgress.completed}/{data.planProgress.total || "?"}
|
|
440
|
+
</span>
|
|
441
|
+
<span className="muted"> - {Math.round(planPercent)}%</span>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
<div className="progressWrap" aria-label="progress">
|
|
446
|
+
<div className="progressTrack">
|
|
447
|
+
<div className="progressFill" style={{ width: `${planPercent}%` }} />
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
<div className="mono path">{data.planProgress.path}</div>
|
|
451
|
+
</article>
|
|
452
|
+
</section>
|
|
453
|
+
|
|
454
|
+
<section className="card">
|
|
455
|
+
<div className="cardHeader">
|
|
456
|
+
<h2>Background tasks</h2>
|
|
457
|
+
<span className="badge" aria-label="count">
|
|
458
|
+
{data.backgroundTasks.length}
|
|
459
|
+
</span>
|
|
460
|
+
</div>
|
|
461
|
+
<div className="tableWrap">
|
|
462
|
+
<table className="table">
|
|
463
|
+
<thead>
|
|
464
|
+
<tr>
|
|
465
|
+
<th>DESCRIPTION</th>
|
|
466
|
+
<th>AGENT</th>
|
|
467
|
+
<th>STATUS</th>
|
|
468
|
+
<th>TOOL CALLS</th>
|
|
469
|
+
<th>LAST TOOL</th>
|
|
470
|
+
<th>TIMELINE</th>
|
|
471
|
+
</tr>
|
|
472
|
+
</thead>
|
|
473
|
+
<tbody>
|
|
474
|
+
{data.backgroundTasks.map((t) => (
|
|
475
|
+
<tr key={t.id}>
|
|
476
|
+
<td>
|
|
477
|
+
<div className="taskTitle">{t.description}</div>
|
|
478
|
+
{t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
|
|
479
|
+
</td>
|
|
480
|
+
<td className="mono">{t.agent}</td>
|
|
481
|
+
<td>
|
|
482
|
+
<span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
|
|
483
|
+
</td>
|
|
484
|
+
<td className="mono">{t.toolCalls}</td>
|
|
485
|
+
<td className="mono">{t.lastTool}</td>
|
|
486
|
+
<td className="mono muted">{t.timeline || "-"}</td>
|
|
487
|
+
</tr>
|
|
488
|
+
))}
|
|
489
|
+
</tbody>
|
|
490
|
+
</table>
|
|
491
|
+
</div>
|
|
492
|
+
</section>
|
|
493
|
+
|
|
494
|
+
<details className="details">
|
|
495
|
+
<summary className="detailsSummary">
|
|
496
|
+
<span className="detailsTitle">Raw JSON</span>
|
|
497
|
+
<span className="chev" aria-hidden="true" />
|
|
498
|
+
</summary>
|
|
499
|
+
<div className="detailsBody">
|
|
500
|
+
<pre className="code">
|
|
501
|
+
<code>{rawJsonText}</code>
|
|
502
|
+
</pre>
|
|
503
|
+
</div>
|
|
504
|
+
</details>
|
|
505
|
+
</main>
|
|
506
|
+
|
|
507
|
+
<footer className="footer">
|
|
508
|
+
<div className="footerLeft">
|
|
509
|
+
Local-only dashboard. Served from <span className="mono">{servedFrom}</span>
|
|
510
|
+
</div>
|
|
511
|
+
<div className="footerRight">
|
|
512
|
+
Last update: <span className="mono">{formatTime(lastUpdate)}</span>
|
|
513
|
+
</div>
|
|
514
|
+
</footer>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
}
|