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.
@@ -4,6 +4,7 @@ import { readBoulderState, readPlanProgress, readPlanSteps, type PlanStep } from
4
4
  import { deriveBackgroundTasks } from "../ingest/background-tasks"
5
5
  import { deriveTimeSeriesActivity, type TimeSeriesPayload } from "../ingest/timeseries"
6
6
  import { getMainSessionView, getStorageRoots, pickActiveSessionId, readMainSessionMetas, type MainSessionView, type OpenCodeStorageRoots, type SessionMetadata } from "../ingest/session"
7
+ import { deriveToolCalls } from "../ingest/tool-calls"
7
8
 
8
9
  export type DashboardPayload = {
9
10
  mainSession: {
@@ -12,6 +13,7 @@ export type DashboardPayload = {
12
13
  currentTool: string
13
14
  lastUpdatedLabel: string
14
15
  session: string
16
+ sessionId: string | null
15
17
  statusPill: string
16
18
  }
17
19
  planProgress: {
@@ -33,6 +35,18 @@ export type DashboardPayload = {
33
35
  timeline: string
34
36
  sessionId: string | null
35
37
  }>
38
+ mainSessionTasks: Array<{
39
+ id: string
40
+ description: string
41
+ subline?: string
42
+ agent: string
43
+ lastModel: string | null
44
+ status: string
45
+ toolCalls: number
46
+ lastTool: string
47
+ timeline: string
48
+ sessionId: string | null
49
+ }>
36
50
  timeSeries: TimeSeriesPayload
37
51
  raw: unknown
38
52
  }
@@ -92,6 +106,33 @@ function mainStatusPill(status: string): string {
92
106
  return "unknown"
93
107
  }
94
108
 
109
+ function formatIsoNoMs(ts: number): string {
110
+ const iso = new Date(ts).toISOString()
111
+ return iso.replace(/\.\d{3}Z$/, "Z")
112
+ }
113
+
114
+ function formatElapsed(ms: number): string {
115
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000))
116
+ const seconds = totalSeconds % 60
117
+ const totalMinutes = Math.floor(totalSeconds / 60)
118
+ const minutes = totalMinutes % 60
119
+ const totalHours = Math.floor(totalMinutes / 60)
120
+ const hours = totalHours % 24
121
+ const days = Math.floor(totalHours / 24)
122
+
123
+ if (days > 0) return hours > 0 ? `${days}d${hours}h` : `${days}d`
124
+ if (totalHours > 0) return minutes > 0 ? `${totalHours}h${minutes}m` : `${totalHours}h`
125
+ if (totalMinutes > 0) return seconds > 0 ? `${totalMinutes}m${seconds}s` : `${totalMinutes}m`
126
+ return `${seconds}s`
127
+ }
128
+
129
+ function formatTimeline(startAt: number | null, endAtMs: number): string {
130
+ if (typeof startAt !== "number") return ""
131
+ const start = formatIsoNoMs(startAt)
132
+ const elapsed = formatElapsed(endAtMs - startAt)
133
+ return `${start}: ${elapsed}`
134
+ }
135
+
95
136
  export function buildDashboardPayload(opts: {
96
137
  projectRoot: string
97
138
  storage: OpenCodeStorageRoots
@@ -136,6 +177,41 @@ export function buildDashboardPayload(opts: {
136
177
  const mainCurrentModel = "currentModel" in main
137
178
  ? (main as MainSessionView).currentModel
138
179
  : null
180
+
181
+ const mainSessionTasks = (() => {
182
+ if (!sessionId) return []
183
+
184
+ const mainStatus = main.status
185
+ const status = mainStatus === "running_tool" || mainStatus === "thinking" || mainStatus === "busy"
186
+ ? "running"
187
+ : mainStatus === "idle"
188
+ ? "idle"
189
+ : "unknown"
190
+
191
+ const { toolCalls } = deriveToolCalls({
192
+ storage: opts.storage,
193
+ sessionId,
194
+ })
195
+
196
+ const startAt = sessionMeta?.time?.created ?? null
197
+ const endAtMs = status === "running" ? nowMs : (main.lastUpdated ?? nowMs)
198
+
199
+ return [
200
+ {
201
+ id: "main-session",
202
+ description: "Main session",
203
+ subline: sessionId,
204
+ agent: main.agent,
205
+ lastModel: mainCurrentModel,
206
+ status,
207
+ toolCalls: toolCalls.length,
208
+ lastTool: toolCalls[0]?.tool ?? "-",
209
+ timeline: formatTimeline(startAt, endAtMs),
210
+ sessionId,
211
+ },
212
+ ]
213
+ })()
214
+
139
215
  const payload: DashboardPayload = {
140
216
  mainSession: {
141
217
  agent: main.agent,
@@ -143,6 +219,7 @@ export function buildDashboardPayload(opts: {
143
219
  currentTool: main.currentTool ?? "-",
144
220
  lastUpdatedLabel: formatIso(main.lastUpdated),
145
221
  session: main.sessionLabel,
222
+ sessionId: sessionId ?? null,
146
223
  statusPill: mainStatusPill(main.status),
147
224
  },
148
225
  planProgress: {
@@ -164,6 +241,7 @@ export function buildDashboardPayload(opts: {
164
241
  timeline: typeof t.timeline === "string" ? t.timeline : "",
165
242
  sessionId: t.sessionId ?? null,
166
243
  })),
244
+ mainSessionTasks,
167
245
  timeSeries,
168
246
  raw: null,
169
247
  }
@@ -172,6 +250,7 @@ export function buildDashboardPayload(opts: {
172
250
  mainSession: payload.mainSession,
173
251
  planProgress: payload.planProgress,
174
252
  backgroundTasks: payload.backgroundTasks,
253
+ mainSessionTasks: payload.mainSessionTasks,
175
254
  timeSeries: payload.timeSeries,
176
255
  }
177
256
  return payload
package/src/server/dev.ts CHANGED
@@ -35,7 +35,7 @@ const store = createDashboardStore({
35
35
  pollIntervalMs: 2000,
36
36
  })
37
37
 
38
- app.route("/api", createApi({ store, storageRoot }))
38
+ app.route("/api", createApi({ store, storageRoot, projectRoot: resolvedProjectPath }))
39
39
 
40
40
  Bun.serve({
41
41
  fetch: app.fetch,
@@ -30,7 +30,7 @@ const store = createDashboardStore({
30
30
  pollIntervalMs: 2000,
31
31
  })
32
32
 
33
- app.route('/api', createApi({ store, storageRoot }))
33
+ app.route('/api', createApi({ store, storageRoot, projectRoot: project }))
34
34
 
35
35
  const distRoot = join(import.meta.dir, '../../dist')
36
36
 
package/src/styles.css CHANGED
@@ -161,6 +161,64 @@ body {
161
161
  transform: translateY(0px);
162
162
  }
163
163
 
164
+ .fieldRow {
165
+ display: flex;
166
+ align-items: center;
167
+ flex-wrap: wrap;
168
+ gap: 10px;
169
+ }
170
+
171
+ .field {
172
+ border: 1px solid rgba(31, 36, 38, 0.14);
173
+ background: rgba(255, 255, 255, 0.64);
174
+ padding: 9px 12px;
175
+ border-radius: 999px;
176
+ color: var(--ink);
177
+ font-size: 12px;
178
+ line-height: 1;
179
+ box-shadow: 0 6px 16px rgba(29, 32, 33, 0.06);
180
+ transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
181
+ min-width: 0;
182
+ }
183
+
184
+ .field:hover {
185
+ transform: translateY(-1px);
186
+ background: rgba(255, 255, 255, 0.78);
187
+ border-color: rgba(31, 36, 38, 0.18);
188
+ }
189
+
190
+ .field:active {
191
+ transform: translateY(0px);
192
+ }
193
+
194
+ .field:focus-visible {
195
+ outline: 2px solid rgba(15, 90, 81, 0.30);
196
+ outline-offset: 2px;
197
+ }
198
+
199
+ .field:disabled {
200
+ opacity: 0.55;
201
+ cursor: not-allowed;
202
+ }
203
+
204
+ select.field {
205
+ appearance: none;
206
+ padding-right: 34px;
207
+ background-image:
208
+ linear-gradient(45deg, transparent 50%, rgba(31, 36, 38, 0.50) 50%),
209
+ linear-gradient(135deg, rgba(31, 36, 38, 0.50) 50%, transparent 50%);
210
+ background-position:
211
+ calc(100% - 18px) 50%,
212
+ calc(100% - 13px) 50%;
213
+ background-size: 5px 5px;
214
+ background-repeat: no-repeat;
215
+ }
216
+
217
+ input.field {
218
+ appearance: none;
219
+ flex: 1 1 220px;
220
+ }
221
+
164
222
  .pill {
165
223
  display: inline-flex;
166
224
  align-items: center;
@@ -1 +0,0 @@
1
- :root{--bg-cream: #f5efe2;--bg-cream-2: #efe6d2;--paper: rgba(255, 255, 255, .52);--paper-strong: rgba(255, 255, 255, .72);--ink: #1f2426;--muted: rgba(31, 36, 38, .62);--line: rgba(31, 36, 38, .1);--shadow: 0 18px 40px rgba(29, 32, 33, .09);--teal-ink: #0f3f3a;--teal: #0f5a51;--teal-soft: rgba(15, 90, 81, .13);--green: #1f6b3d;--green-soft: rgba(31, 107, 61, .14);--sand: #6a5b3d;--sand-soft: rgba(135, 114, 71, .14);--red: #7a2b2b;--red-soft: rgba(122, 43, 43, .12);--radius: 18px}*{box-sizing:border-box}html,body{height:100%}body{margin:0;color:var(--ink);background:radial-gradient(1200px 500px at 16% 0%,rgba(15,90,81,.1),transparent 60%),radial-gradient(900px 420px at 80% 12%,rgba(135,114,71,.12),transparent 62%),linear-gradient(180deg,var(--bg-cream),var(--bg-cream-2));font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;text-rendering:geometricPrecision}.page{min-height:100%;position:relative}.page:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;background:repeating-linear-gradient(90deg,rgba(0,0,0,.012),rgba(0,0,0,.012) 1px,transparent 1px,transparent 6px),repeating-linear-gradient(0deg,rgba(0,0,0,.01),rgba(0,0,0,.01) 1px,transparent 1px,transparent 10px);mix-blend-mode:multiply;opacity:.16}.container{width:min(1120px,calc(100% - 44px));margin:0 auto;padding:28px 0 36px}.topbar{display:flex;align-items:flex-start;justify-content:space-between;gap:18px}.brand{display:flex;align-items:flex-start;gap:14px}.brandMark{width:44px;height:44px;border-radius:14px;background:radial-gradient(14px 14px at 28% 28%,#ffffff9e,#fff0 70%),linear-gradient(135deg,#0f5a51,#0d3f3a);box-shadow:0 10px 22px #0f3c372e;position:relative}.brandMark:after{content:"";position:absolute;top:7px;right:7px;bottom:7px;left:7px;border-radius:12px;border:1px solid rgba(255,255,255,.22)}.brandText h1{margin:0;font-size:34px;line-height:1.05;font-weight:650;letter-spacing:-.02em;font-family:Iowan Old Style,Palatino Linotype,Palatino,Georgia,serif}.brandText p{margin:6px 0 0;font-size:13px;color:var(--muted)}.hint{color:#7a2b2bb8}.topbarActions{display:flex;align-items:center;gap:10px;padding-top:6px}.button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid rgba(31,36,38,.14);background:#ffffffa3;padding:9px 12px;border-radius:999px;color:var(--ink);font-size:12px;line-height:1;cursor:pointer;box-shadow:0 6px 16px #1d20210f;transition:transform .12s ease,background .12s ease,border-color .12s ease}.button:hover{transform:translateY(-1px);background:#ffffffc7;border-color:#1f24262e}.button:active{transform:translateY(0)}.pill{display:inline-flex;align-items:center;gap:8px;padding:7px 12px;border-radius:999px;border:1px solid rgba(31,36,38,.12);background:var(--paper);color:var(--ink);font-size:12px;line-height:1;white-space:nowrap}.pillDot{width:8px;height:8px;border-radius:99px;background:currentColor;opacity:.82}.pill-teal{background:var(--teal-soft);border-color:#0f5a5138;color:var(--teal-ink)}.pill-sand{background:var(--sand-soft);border-color:#87724742;color:var(--sand)}.pill-red{background:var(--red-soft);border-color:#7a2b2b42;color:var(--red)}.stack{display:flex;flex-direction:column;gap:18px;margin-top:18px}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px}.card{border-radius:var(--radius);border:1px solid rgba(31,36,38,.12);background:#ffffff8f;box-shadow:var(--shadow);padding:18px;-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px)}.cardHeader{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.cardHeader h2{margin:0;font-size:16px;font-weight:680;font-family:Iowan Old Style,Palatino Linotype,Palatino,Georgia,serif}.kv{display:grid;gap:10px}.kvRow{display:grid;grid-template-columns:150px 1fr;align-items:baseline;gap:12px}.kvKey{font-size:11px;letter-spacing:.08em;color:#1f242694}.kvVal{font-size:14px;color:var(--ink)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace}.muted{color:var(--muted)}.divider{height:1px;background:var(--line);margin:14px 0}.progressWrap{margin-top:12px}.progressTrack{height:12px;border-radius:999px;border:1px solid rgba(31,36,38,.12);background:#ffffff6b;overflow:hidden}.progressFill{height:100%;width:0;background:linear-gradient(90deg,#0f5a51,#0d3f3a);border-radius:999px;box-shadow:inset 0 0 0 1px #ffffff2e;transition:width .42s ease}.path{margin-top:10px;color:#1f2426b3;font-size:12px}.badge{width:28px;height:28px;border-radius:999px;display:grid;place-items:center;background:#ffffffa8;border:1px solid rgba(31,36,38,.14);font-size:12px}.tableWrap{overflow:auto}.table{width:100%;border-collapse:collapse}.table th{text-align:left;font-size:11px;letter-spacing:.08em;color:#1f242694;padding:12px 10px;border-bottom:1px solid var(--line);white-space:nowrap}.table td{padding:14px 10px;border-bottom:1px solid rgba(31,36,38,.07);vertical-align:top;font-size:13px}.taskTitle{font-weight:630}.taskSub{margin-top:4px;font-size:12px;color:#1f24269e}.bgTaskRowTitleWrap{display:flex;align-items:flex-start;gap:10px}.bgTaskRowTitleText{min-width:0}.bgTaskToggle{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:22px;height:22px;border-radius:10px;border:1px solid rgba(31,36,38,.16);background:#ffffff9e;box-shadow:0 6px 14px #1d20210f;color:#1f2426b8;display:inline-flex;align-items:center;justify-content:center;padding:0;cursor:pointer;flex:0 0 auto;margin-top:2px;transition:transform .12s ease,background .12s ease,border-color .12s ease}.bgTaskToggle:before{content:"▸";font-size:12px;line-height:1;transform:translate(.5px)}.bgTaskToggle:hover{transform:translateY(-1px);background:#ffffffc7;border-color:#1f242633}.bgTaskToggle:active{transform:translateY(0)}.bgTaskToggle:focus-visible{outline:2px solid rgba(15,90,81,.3);outline-offset:2px}.bgTaskToggle[aria-expanded=true]{background:#0f5a511a;border-color:#0f5a5138;color:var(--teal-ink)}.bgTaskToggle[aria-expanded=true]:before{content:"▾";transform:translateY(-.5px)}.table td.bgTaskDetailCell{padding:0 10px 14px}.bgTaskDetail{padding:12px 12px 10px;border-radius:14px;border:1px solid rgba(31,36,38,.08);background:#ffffff8a;box-shadow:inset 0 1px #ffffff47,0 10px 22px #1d20210d;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.bgTaskDetailHeader{font-size:12px;margin-bottom:10px}.bgTaskDetailEmpty{font-size:13px}.bgTaskToolCallsGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px}.bgTaskToolCall{border-radius:14px;border:1px solid rgba(31,36,38,.1);background:radial-gradient(120px 50px at 12% 18%,rgba(15,90,81,.06),transparent 70%),#ffffffa3;padding:10px 11px}.bgTaskToolCallRow{display:grid;grid-template-columns:minmax(140px,1fr) max-content;gap:10px;align-items:baseline}.bgTaskToolCallTool,.bgTaskToolCallStatus,.bgTaskToolCallId{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bgTaskToolCallTime{margin-top:8px;font-size:12px}.bgTaskToolCallId{margin-top:6px;font-size:12px;opacity:.78}.details{border-radius:var(--radius);border:1px solid rgba(31,36,38,.12);background:#ffffff85;box-shadow:0 18px 40px #1d202112;overflow:hidden}.detailsSummary{list-style:none;cursor:pointer;padding:16px 18px;display:flex;align-items:center;justify-content:space-between;gap:10px}.detailsSummary::-webkit-details-marker{display:none}.detailsTitle{font-size:16px;font-weight:680;font-family:Iowan Old Style,Palatino Linotype,Palatino,Georgia,serif}.chev{width:10px;height:10px;border-right:2px solid rgba(31,36,38,.42);border-bottom:2px solid rgba(31,36,38,.42);transform:rotate(45deg);transition:transform .15s ease}.details[open] .chev{transform:rotate(-135deg)}.detailsBody{border-top:1px solid var(--line);padding:16px 18px 18px}.code{margin:0;padding:14px;border-radius:14px;border:1px solid rgba(31,36,38,.1);background:#ffffffb3;overflow:auto;font-size:12px;line-height:1.5}.footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:18px;padding:10px 2px 0;color:#1f242699;font-size:12px}.timeSeries{margin-top:18px;padding:16px 18px 12px;border-radius:var(--radius);background:var(--paper);box-shadow:var(--shadow);border:1px solid rgba(31,36,38,.06);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.timeSeriesHeader{display:flex;align-items:baseline;gap:18px;padding-bottom:8px;border-bottom:1px solid rgba(31,36,38,.06)}.timeSeriesTitle{margin:0;font-size:20px;font-weight:680;letter-spacing:-.02em;font-family:Iowan Old Style,Palatino Linotype,Palatino,Georgia,serif}.timeSeriesSub{margin:0;font-size:14px;color:var(--muted)}.timeSeriesAxisTop{display:grid;grid-template-columns:160px 1fr;gap:18px;margin-top:8px}.timeSeriesAxisTopLabels{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;font-size:12px;letter-spacing:.03em;color:#1f24269e;font-variant-numeric:tabular-nums}.timeSeriesAxisTopLabel{white-space:nowrap}.timeSeriesRows{margin-top:8px;display:grid;gap:6px}.timeSeriesRow{display:grid;grid-template-columns:160px 1fr;gap:18px;align-items:center;padding:4px 0}.timeSeriesRow+.timeSeriesRow{border-top:1px solid rgba(31,36,38,.06);padding-top:10px}.timeSeriesRowLabel{font-size:20px;line-height:1.05;font-weight:650;letter-spacing:-.01em;font-family:Iowan Old Style,Palatino Linotype,Palatino,Georgia,serif}.timeSeriesSvgWrap{position:relative;height:24px;border-radius:10px;background:radial-gradient(140px 36px at 8% 60%,rgba(255,255,255,.16),transparent 70%),linear-gradient(180deg,#ffffff1f,#ffffff0d);box-shadow:inset 0 0 0 1px #1f24260a,inset 0 -1px #ffffff1a}.timeSeriesSvg{width:100%;height:100%;display:block}.timeSeriesGridline{stroke:#1f242614;shape-rendering:crispEdges}.timeSeriesBar{shape-rendering:crispEdges}.timeSeriesBarBaseline{fill:#1f242633}.timeSeriesBar--teal,.timeSeriesRow[data-tone=teal] .timeSeriesBar{fill:var(--teal)}.timeSeriesBar--sand,.timeSeriesRow[data-tone=sand] .timeSeriesBar{fill:var(--sand)}.timeSeriesBar--red,.timeSeriesRow[data-tone=red] .timeSeriesBar{fill:var(--red)}.timeSeriesBar--green,.timeSeriesRow[data-tone=green] .timeSeriesBar{fill:var(--green)}.timeSeriesBar--muted,.timeSeriesRow[data-tone=muted] .timeSeriesBar{fill:#1f24265c}.timeSeriesAxisBottom{display:grid;grid-template-columns:160px 1fr;gap:18px;margin-top:10px;padding-top:8px;border-top:1px solid rgba(31,36,38,.06)}.timeSeriesAxisBottomLabels{display:flex;justify-content:space-between;gap:12px;font-size:12px;letter-spacing:.02em;color:#1f24269e;font-variant-numeric:tabular-nums}.timeSeriesAxisBottomLabel{white-space:nowrap}@media (max-width: 920px){.grid2{grid-template-columns:1fr}.timeSeriesAxisTop,.timeSeriesRow,.timeSeriesAxisBottom{grid-template-columns:140px 1fr}.timeSeriesRowLabel{font-size:18px}.kvRow{grid-template-columns:140px 1fr}.table th:nth-child(6),.table td:nth-child(6){display:none}}@media (max-width: 520px){.container{width:min(1120px,calc(100% - 28px));padding-top:22px}.timeSeries{padding:14px 14px 10px}.timeSeriesAxisTop{display:none}.timeSeriesRow,.timeSeriesAxisBottom{grid-template-columns:1fr;gap:6px}.timeSeriesRow+.timeSeriesRow{padding-top:8px}.timeSeriesRowLabel{font-size:16px}.timeSeriesAxisBottomLabels{font-size:11px}.topbar{flex-direction:column;align-items:stretch}.topbarActions{justify-content:flex-start;flex-wrap:wrap}.kvRow{grid-template-columns:1fr;gap:6px}.footer{flex-direction:column;align-items:flex-start}.bgTaskToolCallsGrid{grid-template-columns:1fr}}