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/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
- async function fetchToolCalls(sessionId: string, opts: { force: boolean }) {
710
- const existing = toolCallsBySession.get(sessionId);
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 = toolCallsBySession.get(sessionId);
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);
@@ -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
+ })
@@ -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