@hueyexe/opencode-ensemble 0.9.1 → 0.10.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 +32 -5
- package/dist/index.js +589 -1
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/dashboard-html.ts +91 -0
- package/src/dashboard-js-part1.ts +97 -0
- package/src/dashboard-js-part2.ts +232 -0
- package/src/dashboard-js-part3.ts +50 -0
- package/src/dashboard.ts +166 -0
- package/src/index.ts +8 -0
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { DASHBOARD_HEAD } from "./dashboard-html"
|
|
3
|
+
import { DASHBOARD_JS_PART1 } from "./dashboard-js-part1"
|
|
4
|
+
import { DASHBOARD_JS_PART2 } from "./dashboard-js-part2"
|
|
5
|
+
import { DASHBOARD_JS_PART3 } from "./dashboard-js-part3"
|
|
6
|
+
import { log } from "./log"
|
|
7
|
+
|
|
8
|
+
/** Assemble the full dashboard HTML from parts. */
|
|
9
|
+
const DASHBOARD_HTML = DASHBOARD_HEAD + "\n<script>" + DASHBOARD_JS_PART1 + DASHBOARD_JS_PART2 + DASHBOARD_JS_PART3 + "<\/script>\n</body></html>"
|
|
10
|
+
|
|
11
|
+
interface TeamRow {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
status: string
|
|
15
|
+
lead_agent: string | null
|
|
16
|
+
time_created: number
|
|
17
|
+
time_updated: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MemberRow {
|
|
21
|
+
name: string
|
|
22
|
+
agent: string
|
|
23
|
+
status: string
|
|
24
|
+
execution_status: string
|
|
25
|
+
worktree_branch: string | null
|
|
26
|
+
prompt: string | null
|
|
27
|
+
model: string | null
|
|
28
|
+
plan_approval: string
|
|
29
|
+
time_created: number
|
|
30
|
+
time_updated: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface TaskRow {
|
|
34
|
+
id: string
|
|
35
|
+
content: string
|
|
36
|
+
status: string
|
|
37
|
+
priority: string
|
|
38
|
+
assignee: string | null
|
|
39
|
+
depends_on: string | null
|
|
40
|
+
time_created: number
|
|
41
|
+
time_updated: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface MessageRow {
|
|
45
|
+
id: string
|
|
46
|
+
from_name: string
|
|
47
|
+
to_name: string | null
|
|
48
|
+
content: string
|
|
49
|
+
delivered: number
|
|
50
|
+
read: number
|
|
51
|
+
time_created: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function jsonResponse(data: unknown, status = 200): Response {
|
|
55
|
+
return new Response(JSON.stringify(data), {
|
|
56
|
+
status,
|
|
57
|
+
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildState(db: Database): { teams: unknown[] } {
|
|
62
|
+
const teams = db.query("SELECT id, name, status, lead_agent, time_created, time_updated FROM team ORDER BY time_created DESC").all() as TeamRow[]
|
|
63
|
+
const memberStmt = db.query("SELECT name, agent, status, execution_status, worktree_branch, prompt, model, plan_approval, time_created, time_updated FROM team_member WHERE team_id = ?")
|
|
64
|
+
const taskStmt = db.query("SELECT id, content, status, priority, assignee, depends_on, time_created, time_updated FROM team_task WHERE team_id = ?")
|
|
65
|
+
const msgStmt = db.query("SELECT id, from_name, to_name, content, delivered, read, time_created FROM team_message WHERE team_id = ? ORDER BY time_created DESC LIMIT 50")
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
teams: teams.map((t) => ({
|
|
69
|
+
id: t.id,
|
|
70
|
+
name: t.name,
|
|
71
|
+
status: t.status,
|
|
72
|
+
leadAgent: t.lead_agent,
|
|
73
|
+
timeCreated: t.time_created,
|
|
74
|
+
timeUpdated: t.time_updated,
|
|
75
|
+
members: (memberStmt.all(t.id) as MemberRow[]).map((m) => ({
|
|
76
|
+
name: m.name,
|
|
77
|
+
agent: m.agent,
|
|
78
|
+
status: m.status,
|
|
79
|
+
executionStatus: m.execution_status,
|
|
80
|
+
worktreeBranch: m.worktree_branch,
|
|
81
|
+
prompt: m.prompt,
|
|
82
|
+
model: m.model,
|
|
83
|
+
planApproval: m.plan_approval,
|
|
84
|
+
timeCreated: m.time_created,
|
|
85
|
+
timeUpdated: m.time_updated,
|
|
86
|
+
})),
|
|
87
|
+
tasks: (taskStmt.all(t.id) as TaskRow[]).map((tk) => ({
|
|
88
|
+
id: tk.id,
|
|
89
|
+
content: tk.content,
|
|
90
|
+
status: tk.status,
|
|
91
|
+
priority: tk.priority,
|
|
92
|
+
assignee: tk.assignee,
|
|
93
|
+
dependsOn: tk.depends_on,
|
|
94
|
+
timeCreated: tk.time_created,
|
|
95
|
+
timeUpdated: tk.time_updated,
|
|
96
|
+
})),
|
|
97
|
+
messages: (msgStmt.all(t.id) as MessageRow[]).map((msg) => ({
|
|
98
|
+
id: msg.id,
|
|
99
|
+
fromName: msg.from_name,
|
|
100
|
+
toName: msg.to_name,
|
|
101
|
+
content: msg.content,
|
|
102
|
+
delivered: msg.delivered === 1,
|
|
103
|
+
read: msg.read === 1,
|
|
104
|
+
timeCreated: msg.time_created,
|
|
105
|
+
})),
|
|
106
|
+
})),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Start the dashboard HTTP server.
|
|
112
|
+
* Serves a JSON API for team state and the dashboard HTML.
|
|
113
|
+
* Singleton: if the port is already in use by another ensemble instance, skips silently.
|
|
114
|
+
* Returns the server instance, or null if skipped.
|
|
115
|
+
*/
|
|
116
|
+
export async function startDashboard(db: Database, port: number): Promise<ReturnType<typeof Bun.serve> | null> {
|
|
117
|
+
try {
|
|
118
|
+
const server = Bun.serve({
|
|
119
|
+
port,
|
|
120
|
+
fetch(req) {
|
|
121
|
+
const url = new URL(req.url)
|
|
122
|
+
|
|
123
|
+
if (url.pathname === "/api/health") {
|
|
124
|
+
return jsonResponse({ ensemble: true, pid: process.pid })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (url.pathname === "/api/state") {
|
|
128
|
+
return jsonResponse(buildState(db))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (url.pathname === "/") {
|
|
132
|
+
return new Response(DASHBOARD_HTML, {
|
|
133
|
+
headers: { "Content-Type": "text/html" },
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new Response("Not Found", { status: 404 })
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
log(`dashboard:started port=${port} url=http://localhost:${port}`)
|
|
141
|
+
return server
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE") {
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(`http://localhost:${port}/api/health`)
|
|
146
|
+
const data = await res.json() as { ensemble?: boolean; pid?: number }
|
|
147
|
+
if (data.ensemble && data.pid) {
|
|
148
|
+
// Check if the other process is still alive
|
|
149
|
+
let alive = false
|
|
150
|
+
try { process.kill(data.pid, 0); alive = true } catch { /* process is dead */ }
|
|
151
|
+
if (alive && data.pid !== process.pid) {
|
|
152
|
+
log(`dashboard:already-running port=${port} pid=${data.pid}`)
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
// Stale server from a dead process — warn the user
|
|
156
|
+
log(`dashboard:stale-server port=${port} stale-pid=${data.pid} — run: kill -9 ${data.pid} || lsof -ti:${port} | xargs kill -9`)
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
} catch { /* health check failed — port held by something else */ }
|
|
160
|
+
log(`dashboard:port-in-use port=${port} (not an ensemble instance)`)
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
log(`dashboard:failed err=${err instanceof Error ? err.message : String(err)}`)
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { log, initLog } from "./log"
|
|
|
15
15
|
import { findTeamBySession } from "./types"
|
|
16
16
|
import { loadConfig } from "./config"
|
|
17
17
|
import { ProgressTracker } from "./progress"
|
|
18
|
+
import { startDashboard } from "./dashboard"
|
|
18
19
|
import { executeTeamCreate } from "./tools/team-create"
|
|
19
20
|
import { executeTeamSpawn } from "./tools/team-spawn"
|
|
20
21
|
import { executeTeamMessage } from "./tools/team-message"
|
|
@@ -93,6 +94,13 @@ const plugin: Plugin = async (input) => {
|
|
|
93
94
|
log(`init:recover-branches:failed err=${err instanceof Error ? err.message : String(err)}`)
|
|
94
95
|
})
|
|
95
96
|
log("init:recovery:done")
|
|
97
|
+
|
|
98
|
+
// Start dashboard server (main instance only, not worktree instances)
|
|
99
|
+
if (config.dashboardPort !== 0) {
|
|
100
|
+
startDashboard(db, config.dashboardPort).catch((err) => {
|
|
101
|
+
log(`init:dashboard:failed err=${err instanceof Error ? err.message : String(err)}`)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
96
104
|
} else {
|
|
97
105
|
log(`init:skip-recovery (worktree instance: ${input.directory})`)
|
|
98
106
|
}
|