oh-my-opencode-dashboard 0.0.4 → 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 +512 -14
- package/src/app-payload.test.ts +108 -1
- package/src/background-task-toolcalls-policy.test.ts +191 -0
- package/src/ingest/background-tasks.test.ts +11 -2
- package/src/ingest/tool-calls.test.ts +161 -0
- package/src/ingest/tool-calls.ts +157 -0
- package/src/server/api.test.ts +175 -53
- package/src/server/api.ts +39 -2
- package/src/server/dashboard.test.ts +41 -0
- package/src/server/dashboard.ts +81 -0
- package/src/server/dev.ts +4 -2
- package/src/server/start.ts +4 -2
- package/src/styles.css +189 -0
- package/dist/assets/index-CZM2MUUs.js +0 -40
- package/dist/assets/index-RAZRO3YN.css +0 -1
package/src/server/api.test.ts
CHANGED
|
@@ -1,24 +1,106 @@
|
|
|
1
|
+
import * as fs from "node:fs"
|
|
2
|
+
import * as os from "node:os"
|
|
3
|
+
import * as path from "node:path"
|
|
1
4
|
import { describe, it, expect } from "vitest"
|
|
2
5
|
import { createApi } from "./api"
|
|
6
|
+
import type { DashboardPayload, DashboardStore } from "./dashboard"
|
|
7
|
+
import type { PlanStep } from "../ingest/boulder"
|
|
8
|
+
import type { TimeSeriesPayload } from "../ingest/timeseries"
|
|
9
|
+
|
|
10
|
+
function mkStorageRoot(): string {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "omo-dashboard-storage-"))
|
|
12
|
+
fs.mkdirSync(path.join(root, "session"), { recursive: true })
|
|
13
|
+
fs.mkdirSync(path.join(root, "message"), { recursive: true })
|
|
14
|
+
fs.mkdirSync(path.join(root, "part"), { recursive: true })
|
|
15
|
+
return root
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mkProjectRoot(): string {
|
|
19
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "omo-dashboard-project-"))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeMessageMeta(opts: {
|
|
23
|
+
storageRoot: string
|
|
24
|
+
sessionId: string
|
|
25
|
+
messageId: string
|
|
26
|
+
created?: number
|
|
27
|
+
}): void {
|
|
28
|
+
const msgDir = path.join(opts.storageRoot, "message", opts.sessionId)
|
|
29
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
30
|
+
const meta: Record<string, unknown> = {
|
|
31
|
+
id: opts.messageId,
|
|
32
|
+
sessionID: opts.sessionId,
|
|
33
|
+
role: "assistant",
|
|
34
|
+
}
|
|
35
|
+
if (typeof opts.created === "number") {
|
|
36
|
+
meta.time = { created: opts.created }
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync(path.join(msgDir, `${opts.messageId}.json`), JSON.stringify(meta), "utf8")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeToolPart(opts: {
|
|
42
|
+
storageRoot: string
|
|
43
|
+
sessionId: string
|
|
44
|
+
messageId: string
|
|
45
|
+
callId: string
|
|
46
|
+
tool: string
|
|
47
|
+
state?: Record<string, unknown>
|
|
48
|
+
}): void {
|
|
49
|
+
const partDir = path.join(opts.storageRoot, "part", opts.messageId)
|
|
50
|
+
fs.mkdirSync(partDir, { recursive: true })
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(partDir, `${opts.callId}.json`),
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
id: `part_${opts.callId}`,
|
|
55
|
+
sessionID: opts.sessionId,
|
|
56
|
+
messageID: opts.messageId,
|
|
57
|
+
type: "tool",
|
|
58
|
+
callID: opts.callId,
|
|
59
|
+
tool: opts.tool,
|
|
60
|
+
state: opts.state ?? { status: "completed", input: {} },
|
|
61
|
+
}),
|
|
62
|
+
"utf8"
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sensitiveKeys = ["prompt", "input", "output", "error", "state"]
|
|
67
|
+
|
|
68
|
+
function hasSensitiveKeys(value: unknown): boolean {
|
|
69
|
+
if (!value || typeof value !== "object") return false
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.some((item) => hasSensitiveKeys(item))
|
|
72
|
+
}
|
|
73
|
+
for (const [key, child] of Object.entries(value)) {
|
|
74
|
+
if (sensitiveKeys.includes(key)) return true
|
|
75
|
+
if (hasSensitiveKeys(child)) return true
|
|
76
|
+
}
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const createStore = (): DashboardStore => ({
|
|
81
|
+
getSnapshot: (): DashboardPayload => ({
|
|
82
|
+
mainSession: { agent: "x", currentModel: null, currentTool: "-", lastUpdatedLabel: "never", session: "s", statusPill: "idle" },
|
|
83
|
+
planProgress: { name: "p", completed: 0, total: 0, path: "", statusPill: "not started", steps: [] as PlanStep[] },
|
|
84
|
+
backgroundTasks: [],
|
|
85
|
+
mainSessionTasks: [],
|
|
86
|
+
timeSeries: {
|
|
87
|
+
windowMs: 0,
|
|
88
|
+
bucketMs: 0,
|
|
89
|
+
buckets: 0,
|
|
90
|
+
anchorMs: 0,
|
|
91
|
+
serverNowMs: 0,
|
|
92
|
+
series: [{ id: "overall-main", label: "Overall", tone: "muted" as const, values: [] as number[] }],
|
|
93
|
+
},
|
|
94
|
+
raw: null,
|
|
95
|
+
}),
|
|
96
|
+
} satisfies DashboardStore)
|
|
3
97
|
|
|
4
98
|
describe('API Routes', () => {
|
|
5
99
|
it('should return health check', async () => {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
backgroundTasks: [],
|
|
11
|
-
timeSeries: {
|
|
12
|
-
windowMs: 0,
|
|
13
|
-
bucketMs: 0,
|
|
14
|
-
buckets: 0,
|
|
15
|
-
anchorMs: 0,
|
|
16
|
-
serverNowMs: 0,
|
|
17
|
-
series: [{ id: "overall-main", label: "Overall", tone: "muted", values: [] }],
|
|
18
|
-
},
|
|
19
|
-
raw: null,
|
|
20
|
-
}),
|
|
21
|
-
})
|
|
100
|
+
const storageRoot = mkStorageRoot()
|
|
101
|
+
const projectRoot = mkProjectRoot()
|
|
102
|
+
const store = createStore()
|
|
103
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
22
104
|
|
|
23
105
|
const res = await api.request("/health")
|
|
24
106
|
expect(res.status).toBe(200)
|
|
@@ -26,22 +108,10 @@ describe('API Routes', () => {
|
|
|
26
108
|
})
|
|
27
109
|
|
|
28
110
|
it('should return dashboard data without sensitive keys', async () => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
backgroundTasks: [{ id: "1", description: "d", agent: "a", status: "queued", toolCalls: 0, lastTool: "-", timeline: "" }],
|
|
34
|
-
timeSeries: {
|
|
35
|
-
windowMs: 0,
|
|
36
|
-
bucketMs: 0,
|
|
37
|
-
buckets: 0,
|
|
38
|
-
anchorMs: 0,
|
|
39
|
-
serverNowMs: 0,
|
|
40
|
-
series: [{ id: "overall-main", label: "Overall", tone: "muted", values: [] }],
|
|
41
|
-
},
|
|
42
|
-
raw: { ok: true },
|
|
43
|
-
}),
|
|
44
|
-
})
|
|
111
|
+
const storageRoot = mkStorageRoot()
|
|
112
|
+
const projectRoot = mkProjectRoot()
|
|
113
|
+
const store = createStore()
|
|
114
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
45
115
|
|
|
46
116
|
const res = await api.request("/dashboard")
|
|
47
117
|
expect(res.status).toBe(200)
|
|
@@ -54,26 +124,78 @@ describe('API Routes', () => {
|
|
|
54
124
|
expect(data).toHaveProperty("timeSeries")
|
|
55
125
|
expect(data).toHaveProperty("raw")
|
|
56
126
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (checkForSensitiveKeys(obj[key])) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
expect(checkForSensitiveKeys(data)).toBe(false)
|
|
127
|
+
expect(hasSensitiveKeys(data)).toBe(false)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should reject invalid session IDs', async () => {
|
|
131
|
+
const storageRoot = mkStorageRoot()
|
|
132
|
+
const projectRoot = mkProjectRoot()
|
|
133
|
+
const store = createStore()
|
|
134
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
135
|
+
|
|
136
|
+
const res = await api.request("/tool-calls/not_valid!")
|
|
137
|
+
expect(res.status).toBe(400)
|
|
138
|
+
expect(await res.json()).toEqual({ ok: false, sessionId: "not_valid!", toolCalls: [] })
|
|
78
139
|
})
|
|
140
|
+
|
|
141
|
+
it('should return 404 for missing sessions', async () => {
|
|
142
|
+
const storageRoot = mkStorageRoot()
|
|
143
|
+
const projectRoot = mkProjectRoot()
|
|
144
|
+
const store = createStore()
|
|
145
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
146
|
+
|
|
147
|
+
const res = await api.request("/tool-calls/ses_missing")
|
|
148
|
+
expect(res.status).toBe(404)
|
|
149
|
+
expect(await res.json()).toEqual({ ok: false, sessionId: "ses_missing", toolCalls: [] })
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should return empty tool calls for existing sessions', async () => {
|
|
153
|
+
const storageRoot = mkStorageRoot()
|
|
154
|
+
const projectRoot = mkProjectRoot()
|
|
155
|
+
writeMessageMeta({ storageRoot, sessionId: "ses_empty", messageId: "msg_1", created: 1000 })
|
|
156
|
+
const store = createStore()
|
|
157
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
158
|
+
|
|
159
|
+
const res = await api.request("/tool-calls/ses_empty")
|
|
160
|
+
expect(res.status).toBe(200)
|
|
161
|
+
|
|
162
|
+
const data = await res.json()
|
|
163
|
+
expect(data.ok).toBe(true)
|
|
164
|
+
expect(data.sessionId).toBe("ses_empty")
|
|
165
|
+
expect(data.toolCalls).toEqual([])
|
|
166
|
+
expect(data.caps).toEqual({ maxMessages: 200, maxToolCalls: 300 })
|
|
167
|
+
expect(data.truncated).toBe(false)
|
|
168
|
+
expect(hasSensitiveKeys(data)).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should redact tool call payload fields', async () => {
|
|
172
|
+
const storageRoot = mkStorageRoot()
|
|
173
|
+
const projectRoot = mkProjectRoot()
|
|
174
|
+
writeMessageMeta({ storageRoot, sessionId: "ses_redact", messageId: "msg_1", created: 1000 })
|
|
175
|
+
writeToolPart({
|
|
176
|
+
storageRoot,
|
|
177
|
+
sessionId: "ses_redact",
|
|
178
|
+
messageId: "msg_1",
|
|
179
|
+
callId: "call_1",
|
|
180
|
+
tool: "bash",
|
|
181
|
+
state: {
|
|
182
|
+
status: "completed",
|
|
183
|
+
input: { prompt: "SECRET", nested: { output: "HIDDEN" } },
|
|
184
|
+
output: "NOPE",
|
|
185
|
+
error: "NOPE",
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
const store = createStore()
|
|
189
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
190
|
+
|
|
191
|
+
const res = await api.request("/tool-calls/ses_redact")
|
|
192
|
+
expect(res.status).toBe(200)
|
|
193
|
+
|
|
194
|
+
const data = await res.json()
|
|
195
|
+
expect(data.ok).toBe(true)
|
|
196
|
+
expect(data.toolCalls.length).toBe(1)
|
|
197
|
+
expect(hasSensitiveKeys(data)).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// /sessions was intentionally removed along with the manual session picker.
|
|
79
201
|
})
|
package/src/server/api.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Hono } from "hono"
|
|
2
2
|
import type { DashboardStore } from "./dashboard"
|
|
3
|
+
import { assertAllowedPath } from "../ingest/paths"
|
|
4
|
+
import { getMessageDir, getStorageRoots } from "../ingest/session"
|
|
5
|
+
import { deriveToolCalls, MAX_TOOL_CALL_MESSAGES, MAX_TOOL_CALLS } from "../ingest/tool-calls"
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
|
8
|
+
|
|
9
|
+
export function createApi(opts: { store: DashboardStore; storageRoot: string; projectRoot: string }): Hono {
|
|
5
10
|
const api = new Hono()
|
|
6
11
|
|
|
7
12
|
api.get("/health", (c) => {
|
|
@@ -9,7 +14,39 @@ export function createApi(store: DashboardStore): Hono {
|
|
|
9
14
|
})
|
|
10
15
|
|
|
11
16
|
api.get("/dashboard", (c) => {
|
|
12
|
-
return c.json(store.getSnapshot())
|
|
17
|
+
return c.json(opts.store.getSnapshot())
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
api.get("/tool-calls/:sessionId", (c) => {
|
|
21
|
+
const sessionId = c.req.param("sessionId")
|
|
22
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
23
|
+
return c.json({ ok: false, sessionId, toolCalls: [] }, 400)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const storage = getStorageRoots(opts.storageRoot)
|
|
27
|
+
const messageDir = getMessageDir(storage.message, sessionId)
|
|
28
|
+
if (!messageDir) {
|
|
29
|
+
return c.json({ ok: false, sessionId, toolCalls: [] }, 404)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assertAllowedPath({ candidatePath: messageDir, allowedRoots: [opts.storageRoot] })
|
|
33
|
+
|
|
34
|
+
const { toolCalls, truncated } = deriveToolCalls({
|
|
35
|
+
storage,
|
|
36
|
+
sessionId,
|
|
37
|
+
allowedRoots: [opts.storageRoot],
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return c.json({
|
|
41
|
+
ok: true,
|
|
42
|
+
sessionId,
|
|
43
|
+
toolCalls,
|
|
44
|
+
caps: {
|
|
45
|
+
maxMessages: MAX_TOOL_CALL_MESSAGES,
|
|
46
|
+
maxToolCalls: MAX_TOOL_CALLS,
|
|
47
|
+
},
|
|
48
|
+
truncated,
|
|
49
|
+
})
|
|
13
50
|
})
|
|
14
51
|
|
|
15
52
|
return api
|
|
@@ -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
|
|
package/src/server/dashboard.ts
CHANGED
|
@@ -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: {
|
|
@@ -31,6 +33,19 @@ export type DashboardPayload = {
|
|
|
31
33
|
toolCalls: number
|
|
32
34
|
lastTool: string
|
|
33
35
|
timeline: string
|
|
36
|
+
sessionId: string | null
|
|
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
|
|
34
49
|
}>
|
|
35
50
|
timeSeries: TimeSeriesPayload
|
|
36
51
|
raw: unknown
|
|
@@ -91,6 +106,33 @@ function mainStatusPill(status: string): string {
|
|
|
91
106
|
return "unknown"
|
|
92
107
|
}
|
|
93
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
|
+
|
|
94
136
|
export function buildDashboardPayload(opts: {
|
|
95
137
|
projectRoot: string
|
|
96
138
|
storage: OpenCodeStorageRoots
|
|
@@ -135,6 +177,41 @@ export function buildDashboardPayload(opts: {
|
|
|
135
177
|
const mainCurrentModel = "currentModel" in main
|
|
136
178
|
? (main as MainSessionView).currentModel
|
|
137
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
|
+
|
|
138
215
|
const payload: DashboardPayload = {
|
|
139
216
|
mainSession: {
|
|
140
217
|
agent: main.agent,
|
|
@@ -142,6 +219,7 @@ export function buildDashboardPayload(opts: {
|
|
|
142
219
|
currentTool: main.currentTool ?? "-",
|
|
143
220
|
lastUpdatedLabel: formatIso(main.lastUpdated),
|
|
144
221
|
session: main.sessionLabel,
|
|
222
|
+
sessionId: sessionId ?? null,
|
|
145
223
|
statusPill: mainStatusPill(main.status),
|
|
146
224
|
},
|
|
147
225
|
planProgress: {
|
|
@@ -161,7 +239,9 @@ export function buildDashboardPayload(opts: {
|
|
|
161
239
|
toolCalls: t.toolCalls ?? 0,
|
|
162
240
|
lastTool: t.lastTool ?? "-",
|
|
163
241
|
timeline: typeof t.timeline === "string" ? t.timeline : "",
|
|
242
|
+
sessionId: t.sessionId ?? null,
|
|
164
243
|
})),
|
|
244
|
+
mainSessionTasks,
|
|
165
245
|
timeSeries,
|
|
166
246
|
raw: null,
|
|
167
247
|
}
|
|
@@ -170,6 +250,7 @@ export function buildDashboardPayload(opts: {
|
|
|
170
250
|
mainSession: payload.mainSession,
|
|
171
251
|
planProgress: payload.planProgress,
|
|
172
252
|
backgroundTasks: payload.backgroundTasks,
|
|
253
|
+
mainSessionTasks: payload.mainSessionTasks,
|
|
173
254
|
timeSeries: payload.timeSeries,
|
|
174
255
|
}
|
|
175
256
|
return payload
|
package/src/server/dev.ts
CHANGED
|
@@ -26,14 +26,16 @@ const resolvedProjectPath = projectPath ?? process.cwd()
|
|
|
26
26
|
|
|
27
27
|
const app = new Hono()
|
|
28
28
|
|
|
29
|
+
const storageRoot = getOpenCodeStorageDir()
|
|
30
|
+
|
|
29
31
|
const store = createDashboardStore({
|
|
30
32
|
projectRoot: resolvedProjectPath,
|
|
31
|
-
storageRoot
|
|
33
|
+
storageRoot,
|
|
32
34
|
watch: true,
|
|
33
35
|
pollIntervalMs: 2000,
|
|
34
36
|
})
|
|
35
37
|
|
|
36
|
-
app.route("/api", createApi(store))
|
|
38
|
+
app.route("/api", createApi({ store, storageRoot, projectRoot: resolvedProjectPath }))
|
|
37
39
|
|
|
38
40
|
Bun.serve({
|
|
39
41
|
fetch: app.fetch,
|
package/src/server/start.ts
CHANGED
|
@@ -21,14 +21,16 @@ const port = parseInt(values.port || '51234')
|
|
|
21
21
|
|
|
22
22
|
const app = new Hono()
|
|
23
23
|
|
|
24
|
+
const storageRoot = getOpenCodeStorageDir()
|
|
25
|
+
|
|
24
26
|
const store = createDashboardStore({
|
|
25
27
|
projectRoot: project,
|
|
26
|
-
storageRoot
|
|
28
|
+
storageRoot,
|
|
27
29
|
watch: true,
|
|
28
30
|
pollIntervalMs: 2000,
|
|
29
31
|
})
|
|
30
32
|
|
|
31
|
-
app.route('/api', createApi(store))
|
|
33
|
+
app.route('/api', createApi({ store, storageRoot, projectRoot: project }))
|
|
32
34
|
|
|
33
35
|
const distRoot = join(import.meta.dir, '../../dist')
|
|
34
36
|
|