oh-my-opencode-dashboard 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/dist/assets/index-BFRahC0d.css +1 -0
- package/dist/assets/index-BsLpOGvG.js +40 -0
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/App.tsx +227 -4
- package/src/app-payload.test.ts +108 -1
- package/src/server/api.test.ts +19 -6
- package/src/server/api.ts +1 -1
- package/src/server/dashboard.test.ts +41 -0
- package/src/server/dashboard.ts +79 -0
- package/src/server/dev.ts +1 -1
- package/src/server/start.ts +1 -1
- package/src/styles.css +58 -0
- package/dist/assets/index--GqzhA4-.css +0 -1
- package/dist/assets/index-CiC6k4Yg.js +0 -40
package/src/App.tsx
CHANGED
|
@@ -70,6 +70,7 @@ type DashboardPayload = {
|
|
|
70
70
|
currentModel: string;
|
|
71
71
|
lastUpdatedLabel: string;
|
|
72
72
|
session: string;
|
|
73
|
+
sessionId: string | null;
|
|
73
74
|
statusPill: string;
|
|
74
75
|
};
|
|
75
76
|
planProgress: {
|
|
@@ -81,6 +82,7 @@ type DashboardPayload = {
|
|
|
81
82
|
steps?: Array<{ checked: boolean; text: string }>;
|
|
82
83
|
};
|
|
83
84
|
backgroundTasks: BackgroundTask[];
|
|
85
|
+
mainSessionTasks: BackgroundTask[];
|
|
84
86
|
timeSeries: TimeSeries;
|
|
85
87
|
raw: unknown;
|
|
86
88
|
};
|
|
@@ -200,6 +202,7 @@ const FALLBACK_DATA: DashboardPayload = {
|
|
|
200
202
|
currentModel: "anthropic/claude-opus-4-5",
|
|
201
203
|
lastUpdatedLabel: "just now",
|
|
202
204
|
session: "qa-session",
|
|
205
|
+
sessionId: null,
|
|
203
206
|
statusPill: "busy",
|
|
204
207
|
},
|
|
205
208
|
planProgress: {
|
|
@@ -223,6 +226,7 @@ const FALLBACK_DATA: DashboardPayload = {
|
|
|
223
226
|
timeline: "2026-01-01T00:00:00Z: 2m",
|
|
224
227
|
},
|
|
225
228
|
],
|
|
229
|
+
mainSessionTasks: [],
|
|
226
230
|
timeSeries: makeZeroTimeSeries({
|
|
227
231
|
windowMs: TIME_SERIES_DEFAULT_WINDOW_MS,
|
|
228
232
|
bucketMs: TIME_SERIES_DEFAULT_BUCKET_MS,
|
|
@@ -419,6 +423,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
|
|
|
419
423
|
const main = (anyJson.mainSession ?? anyJson.main_session ?? {}) as Record<string, unknown>;
|
|
420
424
|
const plan = (anyJson.planProgress ?? anyJson.plan_progress ?? {}) as Record<string, unknown>;
|
|
421
425
|
const tasks = (anyJson.backgroundTasks ?? anyJson.background_tasks ?? []) as unknown;
|
|
426
|
+
const mainTasks = (anyJson.mainSessionTasks ?? anyJson.main_session_tasks ?? []) as unknown;
|
|
422
427
|
|
|
423
428
|
function parsePlanSteps(stepsInput: unknown): Array<{ checked: boolean; text: string }> {
|
|
424
429
|
if (!Array.isArray(stepsInput)) return [];
|
|
@@ -459,6 +464,29 @@ function toDashboardPayload(json: unknown): DashboardPayload {
|
|
|
459
464
|
})
|
|
460
465
|
: FALLBACK_DATA.backgroundTasks;
|
|
461
466
|
|
|
467
|
+
const mainSessionTasks: BackgroundTask[] = Array.isArray(mainTasks)
|
|
468
|
+
? mainTasks.map((t, idx) => {
|
|
469
|
+
const rec = (t ?? {}) as Record<string, unknown>;
|
|
470
|
+
return {
|
|
471
|
+
id: String(rec.id ?? rec.taskId ?? rec.task_id ?? `main-task-${idx + 1}`),
|
|
472
|
+
description: String(rec.description ?? rec.name ?? "(no description)"),
|
|
473
|
+
subline:
|
|
474
|
+
typeof rec.subline === "string"
|
|
475
|
+
? rec.subline
|
|
476
|
+
: typeof rec.taskId === "string"
|
|
477
|
+
? rec.taskId
|
|
478
|
+
: undefined,
|
|
479
|
+
agent: String(rec.agent ?? rec.worker ?? "unknown"),
|
|
480
|
+
lastModel: toNonEmptyString(rec.lastModel ?? rec.last_model) ?? "-",
|
|
481
|
+
sessionId: toNonEmptyString(rec.sessionId ?? rec.session_id),
|
|
482
|
+
status: String(rec.status ?? "queued"),
|
|
483
|
+
toolCalls: Number(rec.toolCalls ?? rec.tool_calls ?? 0) || 0,
|
|
484
|
+
lastTool: String(rec.lastTool ?? rec.last_tool ?? "-") || "-",
|
|
485
|
+
timeline: String(rec.timeline ?? "") || "",
|
|
486
|
+
};
|
|
487
|
+
})
|
|
488
|
+
: [];
|
|
489
|
+
|
|
462
490
|
const completed = Number(plan.completed ?? plan.done ?? 0) || 0;
|
|
463
491
|
const total = Number(plan.total ?? plan.count ?? 0) || 0;
|
|
464
492
|
const steps = parsePlanSteps(plan.steps);
|
|
@@ -472,6 +500,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
|
|
|
472
500
|
currentModel: toNonEmptyString(main.currentModel ?? main.current_model) ?? "-",
|
|
473
501
|
lastUpdatedLabel: String(main.lastUpdatedLabel ?? main.last_updated ?? "just now"),
|
|
474
502
|
session: String(main.session ?? main.session_id ?? FALLBACK_DATA.mainSession.session),
|
|
503
|
+
sessionId: toNonEmptyString(main.sessionId ?? main.session_id),
|
|
475
504
|
statusPill: String(main.statusPill ?? main.status ?? FALLBACK_DATA.mainSession.statusPill),
|
|
476
505
|
},
|
|
477
506
|
planProgress: {
|
|
@@ -483,6 +512,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
|
|
|
483
512
|
steps,
|
|
484
513
|
},
|
|
485
514
|
backgroundTasks,
|
|
515
|
+
mainSessionTasks,
|
|
486
516
|
timeSeries,
|
|
487
517
|
raw: json,
|
|
488
518
|
};
|
|
@@ -500,9 +530,11 @@ export default function App() {
|
|
|
500
530
|
const [errorHint, setErrorHint] = React.useState<string | null>(null);
|
|
501
531
|
|
|
502
532
|
const [expandedBgTaskIds, setExpandedBgTaskIds] = React.useState<Set<string>>(() => new Set());
|
|
533
|
+
const [expandedMainTaskIds, setExpandedMainTaskIds] = React.useState<Set<string>>(() => new Set());
|
|
503
534
|
const [toolCallsBySession, setToolCallsBySession] = React.useState<
|
|
504
535
|
Map<string, { state: "idle" | "loading" | "ok" | "error"; data: ToolCallsResponse | null; lastFetchedAtMs: number | null }>
|
|
505
536
|
>(() => new Map());
|
|
537
|
+
const toolCallsBySessionRef = React.useRef(toolCallsBySession);
|
|
506
538
|
const toolCallsSeqRef = React.useRef<Map<string, number>>(new Map());
|
|
507
539
|
|
|
508
540
|
const timerRef = React.useRef<number | null>(null);
|
|
@@ -527,6 +559,10 @@ export default function App() {
|
|
|
527
559
|
soundEnabledRef.current = soundEnabled;
|
|
528
560
|
}, [soundEnabled]);
|
|
529
561
|
|
|
562
|
+
React.useEffect(() => {
|
|
563
|
+
toolCallsBySessionRef.current = toolCallsBySession;
|
|
564
|
+
}, [toolCallsBySession]);
|
|
565
|
+
|
|
530
566
|
React.useEffect(() => {
|
|
531
567
|
try {
|
|
532
568
|
const raw = window.localStorage.getItem("omoDashboardSoundEnabled");
|
|
@@ -706,8 +742,8 @@ export default function App() {
|
|
|
706
742
|
return map;
|
|
707
743
|
}, [data.timeSeries.series]);
|
|
708
744
|
|
|
709
|
-
|
|
710
|
-
const existing =
|
|
745
|
+
const fetchToolCalls = React.useCallback(async (sessionId: string, opts: { force: boolean }) => {
|
|
746
|
+
const existing = toolCallsBySessionRef.current.get(sessionId);
|
|
711
747
|
if (!opts.force && existing?.data?.ok) return;
|
|
712
748
|
|
|
713
749
|
const seq = (toolCallsSeqRef.current.get(sessionId) ?? 0) + 1;
|
|
@@ -747,7 +783,7 @@ export default function App() {
|
|
|
747
783
|
return next;
|
|
748
784
|
});
|
|
749
785
|
}
|
|
750
|
-
}
|
|
786
|
+
}, []);
|
|
751
787
|
|
|
752
788
|
function toggleBackgroundTaskExpanded(t: BackgroundTask) {
|
|
753
789
|
const nextExpanded = !expandedBgTaskIds.has(t.id);
|
|
@@ -764,7 +800,7 @@ export default function App() {
|
|
|
764
800
|
if (!sessionId) return;
|
|
765
801
|
|
|
766
802
|
const isRunning = String(t.status ?? "").toLowerCase().trim() === "running";
|
|
767
|
-
const cached =
|
|
803
|
+
const cached = toolCallsBySessionRef.current.get(sessionId);
|
|
768
804
|
if (isRunning) {
|
|
769
805
|
void fetchToolCalls(sessionId, { force: true });
|
|
770
806
|
return;
|
|
@@ -775,6 +811,66 @@ export default function App() {
|
|
|
775
811
|
void fetchToolCalls(sessionId, { force: false });
|
|
776
812
|
}
|
|
777
813
|
|
|
814
|
+
function toggleMainTaskExpanded(t: BackgroundTask) {
|
|
815
|
+
const nextExpanded = !expandedMainTaskIds.has(t.id);
|
|
816
|
+
setExpandedMainTaskIds((prev) => {
|
|
817
|
+
const next = new Set(prev);
|
|
818
|
+
if (nextExpanded) next.add(t.id);
|
|
819
|
+
else next.delete(t.id);
|
|
820
|
+
return next;
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
if (!nextExpanded) return;
|
|
824
|
+
|
|
825
|
+
const sessionId = toNonEmptyString(t.sessionId);
|
|
826
|
+
if (!sessionId) return;
|
|
827
|
+
|
|
828
|
+
const isRunning = String(t.status ?? "").toLowerCase().trim() === "running";
|
|
829
|
+
const cached = toolCallsBySessionRef.current.get(sessionId);
|
|
830
|
+
if (isRunning) {
|
|
831
|
+
void fetchToolCalls(sessionId, { force: true });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (cached?.data?.ok) return;
|
|
836
|
+
if (cached?.state === "loading") return;
|
|
837
|
+
void fetchToolCalls(sessionId, { force: false });
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
React.useEffect(() => {
|
|
841
|
+
if (!connected) return;
|
|
842
|
+
|
|
843
|
+
for (const t of data.backgroundTasks) {
|
|
844
|
+
const sessionId = toNonEmptyString(t.sessionId);
|
|
845
|
+
const cached = sessionId ? toolCallsBySessionRef.current.get(sessionId) : null;
|
|
846
|
+
const plan = computeToolCallsFetchPlan({
|
|
847
|
+
sessionId,
|
|
848
|
+
status: t.status,
|
|
849
|
+
cachedState: cached?.state ?? null,
|
|
850
|
+
cachedDataOk: Boolean(cached?.data?.ok),
|
|
851
|
+
isExpanded: expandedBgTaskIds.has(t.id),
|
|
852
|
+
});
|
|
853
|
+
if (plan.shouldFetch && sessionId) {
|
|
854
|
+
void fetchToolCalls(sessionId, { force: plan.force });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
for (const t of data.mainSessionTasks) {
|
|
859
|
+
const sessionId = toNonEmptyString(t.sessionId);
|
|
860
|
+
const cached = sessionId ? toolCallsBySessionRef.current.get(sessionId) : null;
|
|
861
|
+
const plan = computeToolCallsFetchPlan({
|
|
862
|
+
sessionId,
|
|
863
|
+
status: t.status,
|
|
864
|
+
cachedState: cached?.state ?? null,
|
|
865
|
+
cachedDataOk: Boolean(cached?.data?.ok),
|
|
866
|
+
isExpanded: expandedMainTaskIds.has(t.id),
|
|
867
|
+
});
|
|
868
|
+
if (plan.shouldFetch && sessionId) {
|
|
869
|
+
void fetchToolCalls(sessionId, { force: plan.force });
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}, [connected, data.backgroundTasks, data.mainSessionTasks, expandedBgTaskIds, expandedMainTaskIds, fetchToolCalls]);
|
|
873
|
+
|
|
778
874
|
const buckets = Math.max(1, data.timeSeries.buckets);
|
|
779
875
|
const bucketMs = Math.max(1, data.timeSeries.bucketMs);
|
|
780
876
|
const viewBox = `0 0 ${buckets} 28`;
|
|
@@ -1105,6 +1201,126 @@ export default function App() {
|
|
|
1105
1201
|
</article>
|
|
1106
1202
|
</section>
|
|
1107
1203
|
|
|
1204
|
+
<section className="card">
|
|
1205
|
+
<div className="cardHeader">
|
|
1206
|
+
<h2>Main session tasks</h2>
|
|
1207
|
+
<span className="badge">{data.mainSessionTasks.length}</span>
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1210
|
+
<div className="tableWrap">
|
|
1211
|
+
<table className="table">
|
|
1212
|
+
<thead>
|
|
1213
|
+
<tr>
|
|
1214
|
+
<th>DESCRIPTION</th>
|
|
1215
|
+
<th>AGENT</th>
|
|
1216
|
+
<th>LAST MODEL</th>
|
|
1217
|
+
<th>STATUS</th>
|
|
1218
|
+
<th>TOOL CALLS</th>
|
|
1219
|
+
<th>LAST TOOL</th>
|
|
1220
|
+
<th>TIMELINE</th>
|
|
1221
|
+
</tr>
|
|
1222
|
+
</thead>
|
|
1223
|
+
<tbody>
|
|
1224
|
+
{data.mainSessionTasks.length === 0 ? (
|
|
1225
|
+
<tr>
|
|
1226
|
+
<td colSpan={7} className="muted" style={{ padding: 16 }}>
|
|
1227
|
+
No main session tasks detected yet.
|
|
1228
|
+
</td>
|
|
1229
|
+
</tr>
|
|
1230
|
+
) : null}
|
|
1231
|
+
{data.mainSessionTasks.map((t) => {
|
|
1232
|
+
const expanded = expandedMainTaskIds.has(t.id);
|
|
1233
|
+
const sessionId = toNonEmptyString(t.sessionId);
|
|
1234
|
+
const detailId = `main-toolcalls-${t.id}`;
|
|
1235
|
+
const entry = sessionId ? toolCallsBySession.get(sessionId) : null;
|
|
1236
|
+
const toolCalls = entry?.data?.ok ? entry.data.toolCalls : [];
|
|
1237
|
+
const showCapped = Boolean(entry?.data?.truncated);
|
|
1238
|
+
const caps = entry?.data?.caps;
|
|
1239
|
+
const showLoading = entry?.state === "loading";
|
|
1240
|
+
const showError = entry?.state === "error" && !entry?.data?.ok;
|
|
1241
|
+
const empty = sessionId ? toolCalls.length === 0 && !showLoading && !showError : true;
|
|
1242
|
+
|
|
1243
|
+
return (
|
|
1244
|
+
<React.Fragment key={t.id}>
|
|
1245
|
+
<tr>
|
|
1246
|
+
<td>
|
|
1247
|
+
<div className="bgTaskRowTitleWrap">
|
|
1248
|
+
<button
|
|
1249
|
+
type="button"
|
|
1250
|
+
className="bgTaskToggle"
|
|
1251
|
+
onClick={() => toggleMainTaskExpanded(t)}
|
|
1252
|
+
aria-expanded={expanded}
|
|
1253
|
+
aria-controls={detailId}
|
|
1254
|
+
title={expanded ? "Collapse" : "Expand"}
|
|
1255
|
+
aria-label={expanded ? "Collapse tool calls" : "Expand tool calls"}
|
|
1256
|
+
/>
|
|
1257
|
+
<div className="bgTaskRowTitleText">
|
|
1258
|
+
<div className="taskTitle">{t.description}</div>
|
|
1259
|
+
{t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
|
|
1260
|
+
</div>
|
|
1261
|
+
</div>
|
|
1262
|
+
</td>
|
|
1263
|
+
<td className="mono">{t.agent}</td>
|
|
1264
|
+
<td className="mono">{t.lastModel}</td>
|
|
1265
|
+
<td>
|
|
1266
|
+
<span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
|
|
1267
|
+
</td>
|
|
1268
|
+
<td className="mono">{t.toolCalls}</td>
|
|
1269
|
+
<td className="mono">{t.lastTool}</td>
|
|
1270
|
+
<td className="mono muted">{formatBackgroundTaskTimelineCell(t.status, t.timeline)}</td>
|
|
1271
|
+
</tr>
|
|
1272
|
+
|
|
1273
|
+
{expanded ? (
|
|
1274
|
+
<tr>
|
|
1275
|
+
<td colSpan={7} className="bgTaskDetailCell">
|
|
1276
|
+
<section id={detailId} aria-label="Tool calls" className="bgTaskDetail">
|
|
1277
|
+
<div className="mono muted bgTaskDetailHeader">
|
|
1278
|
+
Tool calls (metadata only){showLoading && toolCalls.length > 0 ? " - refreshing" : ""}
|
|
1279
|
+
{showCapped
|
|
1280
|
+
? ` - capped${caps ? ` (max ${caps.maxMessages} messages / ${caps.maxToolCalls} tool calls)` : ""}`
|
|
1281
|
+
: ""}
|
|
1282
|
+
</div>
|
|
1283
|
+
|
|
1284
|
+
{!sessionId ? (
|
|
1285
|
+
<div className="muted bgTaskDetailEmpty">No session id available for this task.</div>
|
|
1286
|
+
) : showError ? (
|
|
1287
|
+
<div className="muted bgTaskDetailEmpty">Tool calls unavailable.</div>
|
|
1288
|
+
) : showLoading && toolCalls.length === 0 ? (
|
|
1289
|
+
<div className="muted bgTaskDetailEmpty">Loading tool calls...</div>
|
|
1290
|
+
) : empty ? (
|
|
1291
|
+
<div className="muted bgTaskDetailEmpty">No tool calls recorded.</div>
|
|
1292
|
+
) : (
|
|
1293
|
+
<div className="bgTaskToolCallsGrid">
|
|
1294
|
+
{toolCalls.map((c) => (
|
|
1295
|
+
<div key={c.callId} className="bgTaskToolCall">
|
|
1296
|
+
<div className="bgTaskToolCallRow">
|
|
1297
|
+
<div className="mono bgTaskToolCallTool" title={c.tool}>
|
|
1298
|
+
{c.tool}
|
|
1299
|
+
</div>
|
|
1300
|
+
<div className="mono muted bgTaskToolCallStatus" title={c.status}>
|
|
1301
|
+
{c.status}
|
|
1302
|
+
</div>
|
|
1303
|
+
</div>
|
|
1304
|
+
<div className="mono muted bgTaskToolCallTime">{formatTime(c.createdAtMs)}</div>
|
|
1305
|
+
<div className="mono muted bgTaskToolCallId" title={c.callId}>
|
|
1306
|
+
{c.callId}
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
))}
|
|
1310
|
+
</div>
|
|
1311
|
+
)}
|
|
1312
|
+
</section>
|
|
1313
|
+
</td>
|
|
1314
|
+
</tr>
|
|
1315
|
+
) : null}
|
|
1316
|
+
</React.Fragment>
|
|
1317
|
+
);
|
|
1318
|
+
})}
|
|
1319
|
+
</tbody>
|
|
1320
|
+
</table>
|
|
1321
|
+
</div>
|
|
1322
|
+
</section>
|
|
1323
|
+
|
|
1108
1324
|
<section className="card">
|
|
1109
1325
|
<div className="cardHeader">
|
|
1110
1326
|
<h2>Background tasks</h2>
|
|
@@ -1126,6 +1342,13 @@ export default function App() {
|
|
|
1126
1342
|
</tr>
|
|
1127
1343
|
</thead>
|
|
1128
1344
|
<tbody>
|
|
1345
|
+
{data.backgroundTasks.length === 0 ? (
|
|
1346
|
+
<tr>
|
|
1347
|
+
<td colSpan={7} className="muted" style={{ padding: 16 }}>
|
|
1348
|
+
No background tasks detected yet. When you run background agents, they will appear here.
|
|
1349
|
+
</td>
|
|
1350
|
+
</tr>
|
|
1351
|
+
) : null}
|
|
1129
1352
|
{data.backgroundTasks.map((t) => {
|
|
1130
1353
|
const expanded = expandedBgTaskIds.has(t.id);
|
|
1131
1354
|
const sessionId = toNonEmptyString(t.sessionId);
|
package/src/app-payload.test.ts
CHANGED
|
@@ -155,4 +155,111 @@ describe('toDashboardPayload', () => {
|
|
|
155
155
|
// #then: should handle non-array steps gracefully
|
|
156
156
|
expect(payload.planProgress.steps).toEqual([])
|
|
157
157
|
})
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
it('should parse mainSession.sessionId from camel or snake keys', () => {
|
|
160
|
+
// #given: server JSON with main session id in camel and snake case
|
|
161
|
+
const camelJson = {
|
|
162
|
+
mainSession: {
|
|
163
|
+
agent: "sisyphus",
|
|
164
|
+
currentTool: "dashboard_start",
|
|
165
|
+
currentModel: "anthropic/claude-opus-4-5",
|
|
166
|
+
lastUpdatedLabel: "just now",
|
|
167
|
+
session: "test-session",
|
|
168
|
+
sessionId: "ses_main",
|
|
169
|
+
statusPill: "busy",
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const snakeJson = {
|
|
174
|
+
main_session: {
|
|
175
|
+
agent: "sisyphus",
|
|
176
|
+
current_tool: "dashboard_start",
|
|
177
|
+
current_model: "anthropic/claude-opus-4-5",
|
|
178
|
+
last_updated: "just now",
|
|
179
|
+
session: "test-session",
|
|
180
|
+
session_id: "ses_snake",
|
|
181
|
+
status: "busy",
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// #when: converting to dashboard payload
|
|
186
|
+
const camelPayload = toDashboardPayload(camelJson)
|
|
187
|
+
const snakePayload = toDashboardPayload(snakeJson)
|
|
188
|
+
|
|
189
|
+
// #then: sessionId should be preserved
|
|
190
|
+
expect(camelPayload.mainSession.sessionId).toBe("ses_main")
|
|
191
|
+
expect(snakePayload.mainSession.sessionId).toBe("ses_snake")
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should preserve mainSessionTasks from server JSON', () => {
|
|
195
|
+
// #given: server JSON with mainSessionTasks
|
|
196
|
+
const serverJson = {
|
|
197
|
+
mainSession: {
|
|
198
|
+
agent: "sisyphus",
|
|
199
|
+
currentTool: "dashboard_start",
|
|
200
|
+
currentModel: "anthropic/claude-opus-4-5",
|
|
201
|
+
lastUpdatedLabel: "just now",
|
|
202
|
+
session: "test-session",
|
|
203
|
+
sessionId: "ses_main",
|
|
204
|
+
statusPill: "busy",
|
|
205
|
+
},
|
|
206
|
+
planProgress: {
|
|
207
|
+
name: "test-plan",
|
|
208
|
+
completed: 0,
|
|
209
|
+
total: 0,
|
|
210
|
+
path: "/tmp/test-plan.md",
|
|
211
|
+
statusPill: "not started",
|
|
212
|
+
steps: [],
|
|
213
|
+
},
|
|
214
|
+
mainSessionTasks: [
|
|
215
|
+
{
|
|
216
|
+
id: "main-session",
|
|
217
|
+
description: "Main session",
|
|
218
|
+
subline: "ses_main",
|
|
219
|
+
agent: "sisyphus",
|
|
220
|
+
lastModel: "anthropic/claude-opus-4-5",
|
|
221
|
+
sessionId: "ses_main",
|
|
222
|
+
status: "running",
|
|
223
|
+
toolCalls: 3,
|
|
224
|
+
lastTool: "delegate_task",
|
|
225
|
+
timeline: "2026-01-01T00:00:00Z: 2m",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
backgroundTasks: [],
|
|
229
|
+
timeSeries: {
|
|
230
|
+
windowMs: 300000,
|
|
231
|
+
buckets: 150,
|
|
232
|
+
bucketMs: 2000,
|
|
233
|
+
anchorMs: 1640995200000,
|
|
234
|
+
serverNowMs: 1640995500000,
|
|
235
|
+
series: [
|
|
236
|
+
{
|
|
237
|
+
id: "overall-main",
|
|
238
|
+
label: "Overall",
|
|
239
|
+
tone: "muted",
|
|
240
|
+
values: new Array(150).fill(0),
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// #when
|
|
247
|
+
const payload = toDashboardPayload(serverJson)
|
|
248
|
+
|
|
249
|
+
// #then
|
|
250
|
+
expect(payload.mainSessionTasks).toEqual([
|
|
251
|
+
{
|
|
252
|
+
id: "main-session",
|
|
253
|
+
description: "Main session",
|
|
254
|
+
subline: "ses_main",
|
|
255
|
+
agent: "sisyphus",
|
|
256
|
+
lastModel: "anthropic/claude-opus-4-5",
|
|
257
|
+
sessionId: "ses_main",
|
|
258
|
+
status: "running",
|
|
259
|
+
toolCalls: 3,
|
|
260
|
+
lastTool: "delegate_task",
|
|
261
|
+
timeline: "2026-01-01T00:00:00Z: 2m",
|
|
262
|
+
},
|
|
263
|
+
])
|
|
264
|
+
})
|
|
265
|
+
})
|
package/src/server/api.test.ts
CHANGED
|
@@ -15,6 +15,10 @@ function mkStorageRoot(): string {
|
|
|
15
15
|
return root
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function mkProjectRoot(): string {
|
|
19
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "omo-dashboard-project-"))
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
function writeMessageMeta(opts: {
|
|
19
23
|
storageRoot: string
|
|
20
24
|
sessionId: string
|
|
@@ -78,6 +82,7 @@ const createStore = (): DashboardStore => ({
|
|
|
78
82
|
mainSession: { agent: "x", currentModel: null, currentTool: "-", lastUpdatedLabel: "never", session: "s", statusPill: "idle" },
|
|
79
83
|
planProgress: { name: "p", completed: 0, total: 0, path: "", statusPill: "not started", steps: [] as PlanStep[] },
|
|
80
84
|
backgroundTasks: [],
|
|
85
|
+
mainSessionTasks: [],
|
|
81
86
|
timeSeries: {
|
|
82
87
|
windowMs: 0,
|
|
83
88
|
bucketMs: 0,
|
|
@@ -93,8 +98,9 @@ const createStore = (): DashboardStore => ({
|
|
|
93
98
|
describe('API Routes', () => {
|
|
94
99
|
it('should return health check', async () => {
|
|
95
100
|
const storageRoot = mkStorageRoot()
|
|
101
|
+
const projectRoot = mkProjectRoot()
|
|
96
102
|
const store = createStore()
|
|
97
|
-
const api = createApi({ store, storageRoot })
|
|
103
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
98
104
|
|
|
99
105
|
const res = await api.request("/health")
|
|
100
106
|
expect(res.status).toBe(200)
|
|
@@ -103,8 +109,9 @@ describe('API Routes', () => {
|
|
|
103
109
|
|
|
104
110
|
it('should return dashboard data without sensitive keys', async () => {
|
|
105
111
|
const storageRoot = mkStorageRoot()
|
|
112
|
+
const projectRoot = mkProjectRoot()
|
|
106
113
|
const store = createStore()
|
|
107
|
-
const api = createApi({ store, storageRoot })
|
|
114
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
108
115
|
|
|
109
116
|
const res = await api.request("/dashboard")
|
|
110
117
|
expect(res.status).toBe(200)
|
|
@@ -122,8 +129,9 @@ describe('API Routes', () => {
|
|
|
122
129
|
|
|
123
130
|
it('should reject invalid session IDs', async () => {
|
|
124
131
|
const storageRoot = mkStorageRoot()
|
|
132
|
+
const projectRoot = mkProjectRoot()
|
|
125
133
|
const store = createStore()
|
|
126
|
-
const api = createApi({ store, storageRoot })
|
|
134
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
127
135
|
|
|
128
136
|
const res = await api.request("/tool-calls/not_valid!")
|
|
129
137
|
expect(res.status).toBe(400)
|
|
@@ -132,8 +140,9 @@ describe('API Routes', () => {
|
|
|
132
140
|
|
|
133
141
|
it('should return 404 for missing sessions', async () => {
|
|
134
142
|
const storageRoot = mkStorageRoot()
|
|
143
|
+
const projectRoot = mkProjectRoot()
|
|
135
144
|
const store = createStore()
|
|
136
|
-
const api = createApi({ store, storageRoot })
|
|
145
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
137
146
|
|
|
138
147
|
const res = await api.request("/tool-calls/ses_missing")
|
|
139
148
|
expect(res.status).toBe(404)
|
|
@@ -142,9 +151,10 @@ describe('API Routes', () => {
|
|
|
142
151
|
|
|
143
152
|
it('should return empty tool calls for existing sessions', async () => {
|
|
144
153
|
const storageRoot = mkStorageRoot()
|
|
154
|
+
const projectRoot = mkProjectRoot()
|
|
145
155
|
writeMessageMeta({ storageRoot, sessionId: "ses_empty", messageId: "msg_1", created: 1000 })
|
|
146
156
|
const store = createStore()
|
|
147
|
-
const api = createApi({ store, storageRoot })
|
|
157
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
148
158
|
|
|
149
159
|
const res = await api.request("/tool-calls/ses_empty")
|
|
150
160
|
expect(res.status).toBe(200)
|
|
@@ -160,6 +170,7 @@ describe('API Routes', () => {
|
|
|
160
170
|
|
|
161
171
|
it('should redact tool call payload fields', async () => {
|
|
162
172
|
const storageRoot = mkStorageRoot()
|
|
173
|
+
const projectRoot = mkProjectRoot()
|
|
163
174
|
writeMessageMeta({ storageRoot, sessionId: "ses_redact", messageId: "msg_1", created: 1000 })
|
|
164
175
|
writeToolPart({
|
|
165
176
|
storageRoot,
|
|
@@ -175,7 +186,7 @@ describe('API Routes', () => {
|
|
|
175
186
|
},
|
|
176
187
|
})
|
|
177
188
|
const store = createStore()
|
|
178
|
-
const api = createApi({ store, storageRoot })
|
|
189
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
179
190
|
|
|
180
191
|
const res = await api.request("/tool-calls/ses_redact")
|
|
181
192
|
expect(res.status).toBe(200)
|
|
@@ -185,4 +196,6 @@ describe('API Routes', () => {
|
|
|
185
196
|
expect(data.toolCalls.length).toBe(1)
|
|
186
197
|
expect(hasSensitiveKeys(data)).toBe(false)
|
|
187
198
|
})
|
|
199
|
+
|
|
200
|
+
// /sessions was intentionally removed along with the manual session picker.
|
|
188
201
|
})
|
package/src/server/api.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { deriveToolCalls, MAX_TOOL_CALL_MESSAGES, MAX_TOOL_CALLS } from "../inge
|
|
|
6
6
|
|
|
7
7
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
|
8
8
|
|
|
9
|
-
export function createApi(opts: { store: DashboardStore; storageRoot: string }): Hono {
|
|
9
|
+
export function createApi(opts: { store: DashboardStore; storageRoot: string; projectRoot: string }): Hono {
|
|
10
10
|
const api = new Hono()
|
|
11
11
|
|
|
12
12
|
api.get("/health", (c) => {
|
|
@@ -76,9 +76,49 @@ describe("buildDashboardPayload", () => {
|
|
|
76
76
|
expect(payload.mainSession.currentTool).toBe("delegate_task")
|
|
77
77
|
expect(payload.mainSession.agent).toBe("sisyphus")
|
|
78
78
|
expect(payload.mainSession.currentModel).toBeNull()
|
|
79
|
+
expect(payload.mainSession.sessionId).toBe(sessionId)
|
|
79
80
|
|
|
80
81
|
expect(payload.raw).not.toHaveProperty("prompt")
|
|
81
82
|
expect(payload.raw).not.toHaveProperty("input")
|
|
83
|
+
|
|
84
|
+
expect(payload).toHaveProperty("mainSessionTasks")
|
|
85
|
+
expect((payload as any).mainSessionTasks).toEqual([
|
|
86
|
+
{
|
|
87
|
+
id: "main-session",
|
|
88
|
+
description: "Main session",
|
|
89
|
+
subline: sessionId,
|
|
90
|
+
agent: "sisyphus",
|
|
91
|
+
lastModel: null,
|
|
92
|
+
status: "running",
|
|
93
|
+
toolCalls: 1,
|
|
94
|
+
lastTool: "delegate_task",
|
|
95
|
+
timeline: "1970-01-01T00:00:01Z: 1s",
|
|
96
|
+
sessionId,
|
|
97
|
+
},
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
expect(payload.raw).toHaveProperty("mainSessionTasks.0.lastTool", "delegate_task")
|
|
101
|
+
} finally {
|
|
102
|
+
fs.rmSync(storageRoot, { recursive: true, force: true })
|
|
103
|
+
fs.rmSync(projectRoot, { recursive: true, force: true })
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("includes mainSessionTasks in raw payload when no sessions exist", () => {
|
|
108
|
+
const storageRoot = mkStorageRoot()
|
|
109
|
+
const storage = getStorageRoots(storageRoot)
|
|
110
|
+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omo-project-"))
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const payload = buildDashboardPayload({
|
|
114
|
+
projectRoot,
|
|
115
|
+
storage,
|
|
116
|
+
nowMs: 2000,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(payload).toHaveProperty("mainSessionTasks")
|
|
120
|
+
expect((payload as any).mainSessionTasks).toEqual([])
|
|
121
|
+
expect(payload.raw).toHaveProperty("mainSessionTasks")
|
|
82
122
|
} finally {
|
|
83
123
|
fs.rmSync(storageRoot, { recursive: true, force: true })
|
|
84
124
|
fs.rmSync(projectRoot, { recursive: true, force: true })
|
|
@@ -149,6 +189,7 @@ describe("buildDashboardPayload", () => {
|
|
|
149
189
|
expect(payload).toHaveProperty("timeSeries")
|
|
150
190
|
expect(payload.raw).toHaveProperty("timeSeries")
|
|
151
191
|
expect(payload.mainSession.currentModel).toBeNull()
|
|
192
|
+
expect(payload.mainSession.sessionId).toBeNull()
|
|
152
193
|
|
|
153
194
|
const sensitiveKeys = ["prompt", "input", "output", "error", "state"]
|
|
154
195
|
|