opencode-extended-sidebar 0.2.90
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/.oesignore +17 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/oes.json +11 -0
- package/package.json +100 -0
- package/scripts/install-git-hooks.mjs +29 -0
- package/src/pware.oc.core/constants/index.ts +18 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
- package/src/pware.oc.core/git/index.ts +7 -0
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
- package/src/pware.oc.core/index.ts +18 -0
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
- package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
- package/src/pware.oc.omo/constants/index.ts +15 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
- package/src/pware.oc.omo/index.ts +7 -0
- package/src/pware.oc.omo/resolver/index.ts +19 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
- package/src/pware.oc.opencode/constants/index.ts +10 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
- package/src/pware.oc.opencode/index.ts +8 -0
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
- package/src/pware.oc.opencode/resolver/index.ts +170 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
- package/src/pware.oc.perf/index.ts +14 -0
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
- package/src/pware.oc.runtime/index.ts +13 -0
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
- package/src/pware.oc.runtime/resolver/index.ts +224 -0
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
- package/src/pware.oc.ui/index.ts +12 -0
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
- package/src/pware.oc.ui.tsx +98 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.omo.resolver.boulder
|
|
3
|
+
*
|
|
4
|
+
* Read-only boulder.json resolution: works, tasks, delegates, the active
|
|
5
|
+
* boulder mirror, and the `.omo`/`.sisyphus` layout probes (boulder path,
|
|
6
|
+
* watch dirs, presence marker, stamps).
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs"
|
|
9
|
+
import path from "node:path"
|
|
10
|
+
import { canonicalizePath, fileStamp, readJson, resolveProjectFile } from "../../pware.oc.core/pware.oc.core.paths.js"
|
|
11
|
+
import { profile } from "../../pware.oc.core/pware.oc.core.debug.js"
|
|
12
|
+
import { composeMark, formatAge, pulseAgeMs, stripSessionPrefix, toEpochMs, type AgentMark } from "../../pware.oc.core/pware.oc.core.pulse.js"
|
|
13
|
+
import {
|
|
14
|
+
normalizeStatus,
|
|
15
|
+
taskRank,
|
|
16
|
+
toWorkLabel,
|
|
17
|
+
workIsTerminal,
|
|
18
|
+
workStatusGlyph,
|
|
19
|
+
} from "../../pware.oc.core/pware.oc.core.status.js"
|
|
20
|
+
import {
|
|
21
|
+
MARK_READY,
|
|
22
|
+
} from "../../pware.oc.core/constants/pware.oc.core.constants.pulse.js"
|
|
23
|
+
import {
|
|
24
|
+
STATUS_COMPLETED,
|
|
25
|
+
STATUS_ERROR,
|
|
26
|
+
STATUS_UNKNOWN,
|
|
27
|
+
} from "../../pware.oc.core/constants/pware.oc.core.constants.status.js"
|
|
28
|
+
import { BOULDER_STATUS_PENDING, BOULDER_STATUS_RUNNING } from "../constants/pware.oc.omo.constants.boulderStatus.js"
|
|
29
|
+
import {
|
|
30
|
+
WORK_STATE_COMPLETED,
|
|
31
|
+
WORK_STATE_NOT_COMPLETED,
|
|
32
|
+
type WorkState,
|
|
33
|
+
} from "../constants/pware.oc.omo.constants.planStatus.js"
|
|
34
|
+
|
|
35
|
+
export { workIsTerminal }
|
|
36
|
+
export { toWorkLabel as workStatusLabel }
|
|
37
|
+
|
|
38
|
+
export type PlanStep = { checked: boolean; text: string }
|
|
39
|
+
|
|
40
|
+
export type Delegate = {
|
|
41
|
+
taskKey: string
|
|
42
|
+
title: string
|
|
43
|
+
sessionId: string | null
|
|
44
|
+
agent: string | null
|
|
45
|
+
status: string
|
|
46
|
+
/** Boulder updated_at / started_at — fallback pulse when no session row. */
|
|
47
|
+
updatedAt: number | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type BoulderSession = {
|
|
51
|
+
id: string
|
|
52
|
+
/** `direct` opened the work, `appended` was pulled in later. */
|
|
53
|
+
origin: "direct" | "appended" | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One `task_sessions` entry: a plan unit handed to a subagent. */
|
|
57
|
+
export type TaskView = {
|
|
58
|
+
taskKey: string
|
|
59
|
+
/** Short label from the plan (`todo:3`), when boulder recorded one. */
|
|
60
|
+
label: string | null
|
|
61
|
+
title: string
|
|
62
|
+
sessionId: string | null
|
|
63
|
+
agent: string | null
|
|
64
|
+
category: string | null
|
|
65
|
+
status: string
|
|
66
|
+
startedAt: number | null
|
|
67
|
+
endedAt: number | null
|
|
68
|
+
elapsedMs: number | null
|
|
69
|
+
updatedAt: number | null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One boulder run. Keyed by `work_id` — the same plan run twice is two works. */
|
|
73
|
+
export type WorkView = {
|
|
74
|
+
workId: string
|
|
75
|
+
name: string
|
|
76
|
+
status: string
|
|
77
|
+
sessionId: string | null
|
|
78
|
+
sessions: BoulderSession[]
|
|
79
|
+
agent: string | null
|
|
80
|
+
/** Project-relative plan markdown path, when boulder lists active_plan. */
|
|
81
|
+
planPath: string | null
|
|
82
|
+
startedAt: number | null
|
|
83
|
+
updatedAt: number | null
|
|
84
|
+
endedAt: number | null
|
|
85
|
+
elapsedMs: number | null
|
|
86
|
+
current: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The active work, as mirrored on the boulder root. Blank when nothing is live. */
|
|
90
|
+
export type BoulderView = {
|
|
91
|
+
workId: string | null
|
|
92
|
+
name: string | null
|
|
93
|
+
status: string | null
|
|
94
|
+
agent: string | null
|
|
95
|
+
startedAt: number | null
|
|
96
|
+
updatedAt: number | null
|
|
97
|
+
endedAt: number | null
|
|
98
|
+
elapsedMs: number | null
|
|
99
|
+
sessions: BoulderSession[]
|
|
100
|
+
tasks: TaskView[]
|
|
101
|
+
counts: { running: number; done: number; other: number; total: number }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type OmoSnapshot = {
|
|
105
|
+
present: boolean
|
|
106
|
+
boulderPath: string | null
|
|
107
|
+
planPath: string | null
|
|
108
|
+
status: string | null
|
|
109
|
+
agent: string | null
|
|
110
|
+
planName: string | null
|
|
111
|
+
plan: { total: number; completed: number; percent: number; steps: PlanStep[] }
|
|
112
|
+
works: WorkView[]
|
|
113
|
+
boulder: BoulderView
|
|
114
|
+
delegates: Delegate[]
|
|
115
|
+
stamp: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type RawTask = {
|
|
119
|
+
task_key?: string
|
|
120
|
+
task_label?: string
|
|
121
|
+
task_title?: string
|
|
122
|
+
session_id?: string
|
|
123
|
+
agent?: string
|
|
124
|
+
category?: string
|
|
125
|
+
status?: string
|
|
126
|
+
updated_at?: string | number
|
|
127
|
+
started_at?: string | number
|
|
128
|
+
ended_at?: string | number
|
|
129
|
+
elapsed_ms?: number
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseStamp(v: string | number | undefined): number | null {
|
|
133
|
+
return toEpochMs(v)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type RawWork = {
|
|
137
|
+
plan_name?: string
|
|
138
|
+
active_plan?: string
|
|
139
|
+
agent?: string
|
|
140
|
+
status?: string
|
|
141
|
+
updated_at?: string | number
|
|
142
|
+
started_at?: string | number
|
|
143
|
+
ended_at?: string | number
|
|
144
|
+
elapsed_ms?: number
|
|
145
|
+
session_ids?: string[]
|
|
146
|
+
session_origins?: Record<string, string>
|
|
147
|
+
task_sessions?: Record<string, RawTask>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type RawBoulder = RawWork & {
|
|
151
|
+
active_work_id?: string
|
|
152
|
+
works?: Record<string, RawWork>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The two OMO layout roots, newest-first: `.omo` and the legacy `.sisyphus`. */
|
|
156
|
+
export const OMO_DIRS = [".omo", ".sisyphus"] as const
|
|
157
|
+
|
|
158
|
+
/** An OMO layout root directory name. */
|
|
159
|
+
export type OmoDir = (typeof OMO_DIRS)[number]
|
|
160
|
+
|
|
161
|
+
/** First existing `<root>/<omoDir>/<...rel>` across the OMO dirs, or null. */
|
|
162
|
+
export function firstOmoPath(root: string, ...rel: string[]): string | null {
|
|
163
|
+
for (const dir of OMO_DIRS) {
|
|
164
|
+
const p = path.join(root, dir, ...rel)
|
|
165
|
+
if (fs.existsSync(p)) return p
|
|
166
|
+
}
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function findBoulder(projectRoot: string): string | null {
|
|
171
|
+
return firstOmoPath(projectRoot, "boulder.json")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function findOmoWatchDirs(projectRoot: string): string[] {
|
|
175
|
+
const out: string[] = []
|
|
176
|
+
for (const dir of OMO_DIRS) {
|
|
177
|
+
const p = path.join(projectRoot, dir)
|
|
178
|
+
if (fs.existsSync(p)) out.push(p)
|
|
179
|
+
}
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Installed omo means its config marker — `.omo/omo.jsonc` or `.sisyphus/omo.jsonc`. */
|
|
184
|
+
export function isOmoPresent(projectRoot: string): boolean {
|
|
185
|
+
return firstOmoPath(projectRoot, "omo.jsonc") !== null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Boulder + omo config marker. Drafts/notepads/evidence must not invalidate the
|
|
190
|
+
* hot path, but the marker (`.omo/omo.jsonc`) must — its arrival is what turns
|
|
191
|
+
* the OMO group from hidden to visible.
|
|
192
|
+
*/
|
|
193
|
+
export function omoStamp(projectRoot?: string | null): string {
|
|
194
|
+
if (!projectRoot) return "0"
|
|
195
|
+
return profile("omo.stamp", () => {
|
|
196
|
+
const root = canonicalizePath(projectRoot)
|
|
197
|
+
const parts: string[] = []
|
|
198
|
+
const boulderPath = findBoulder(root)
|
|
199
|
+
if (boulderPath) parts.push(fileStamp(boulderPath))
|
|
200
|
+
for (const dir of OMO_DIRS) {
|
|
201
|
+
const p = path.join(root, dir, "omo.jsonc")
|
|
202
|
+
if (fs.existsSync(p)) parts.push(fileStamp(p))
|
|
203
|
+
}
|
|
204
|
+
return parts.join("|") || "0"
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function lastSessionId(ids: string[] | undefined): string | null {
|
|
209
|
+
if (!ids?.length) return null
|
|
210
|
+
for (let i = ids.length - 1; i >= 0; i--) {
|
|
211
|
+
const id = stripSessionPrefix(ids[i])
|
|
212
|
+
if (id) return id
|
|
213
|
+
}
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function planLabel(name: string | null | undefined, activePlan: string | null | undefined): string {
|
|
218
|
+
const n = typeof name === "string" ? name.trim() : ""
|
|
219
|
+
if (n) return n
|
|
220
|
+
if (activePlan && typeof activePlan === "string") {
|
|
221
|
+
const base = path.basename(activePlan.replace(/\\/g, "/")).replace(/\.md$/i, "")
|
|
222
|
+
if (base) return base
|
|
223
|
+
}
|
|
224
|
+
return "plan"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** One row's mark / glyph / age suffix for the OMO works list. */
|
|
228
|
+
export function workRowView(
|
|
229
|
+
work: { status: string; updatedAt: number | null },
|
|
230
|
+
now: number,
|
|
231
|
+
seen?: number | null,
|
|
232
|
+
): { mark: AgentMark; glyph: string | null; suffix: string } {
|
|
233
|
+
const ageMs = pulseAgeMs(now, work.updatedAt, seen)
|
|
234
|
+
const mark = workIsTerminal(work.status)
|
|
235
|
+
? toWorkLabel(work.status) === STATUS_ERROR
|
|
236
|
+
? STATUS_ERROR
|
|
237
|
+
: MARK_READY
|
|
238
|
+
: composeMark({ lifecycle: work.status, ageMs })
|
|
239
|
+
return {
|
|
240
|
+
mark,
|
|
241
|
+
glyph: workStatusGlyph(work.status),
|
|
242
|
+
suffix: formatAge(ageMs),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function relativePlanPath(projectRoot: string, activePlan: string | null | undefined): string | null {
|
|
247
|
+
return resolveProjectFile(projectRoot, activePlan)?.rel ?? null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function collectSessions(
|
|
251
|
+
ids: string[] | undefined,
|
|
252
|
+
origins: Record<string, string> | undefined,
|
|
253
|
+
): BoulderSession[] {
|
|
254
|
+
if (!Array.isArray(ids)) return []
|
|
255
|
+
const out: BoulderSession[] = []
|
|
256
|
+
const seen = new Set<string>()
|
|
257
|
+
for (const raw of ids) {
|
|
258
|
+
const id = stripSessionPrefix(raw)
|
|
259
|
+
if (!id || seen.has(id)) continue
|
|
260
|
+
seen.add(id)
|
|
261
|
+
// Boulder keys origins by the stored id, prefixed or not.
|
|
262
|
+
const hint = origins?.[String(raw)] ?? origins?.[id] ?? null
|
|
263
|
+
const origin = hint === "direct" || hint === "appended" ? hint : null
|
|
264
|
+
out.push({ id, origin })
|
|
265
|
+
}
|
|
266
|
+
return out.slice(0, 24)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function asWork(
|
|
270
|
+
workId: string,
|
|
271
|
+
w: RawWork,
|
|
272
|
+
current: boolean,
|
|
273
|
+
projectRoot: string,
|
|
274
|
+
fallbackPlan?: string | null,
|
|
275
|
+
): WorkView {
|
|
276
|
+
const activePlan = w.active_plan || fallbackPlan || null
|
|
277
|
+
const sessions = collectSessions(w.session_ids, w.session_origins)
|
|
278
|
+
return {
|
|
279
|
+
workId,
|
|
280
|
+
name: planLabel(w.plan_name, activePlan),
|
|
281
|
+
status: (w.status || STATUS_UNKNOWN).toLowerCase(),
|
|
282
|
+
sessionId: lastSessionId(w.session_ids),
|
|
283
|
+
sessions,
|
|
284
|
+
agent: w.agent ?? null,
|
|
285
|
+
planPath: relativePlanPath(projectRoot, activePlan),
|
|
286
|
+
startedAt: parseStamp(w.started_at),
|
|
287
|
+
updatedAt: parseStamp(w.updated_at) ?? parseStamp(w.started_at),
|
|
288
|
+
endedAt: parseStamp(w.ended_at),
|
|
289
|
+
elapsedMs: typeof w.elapsed_ms === "number" && Number.isFinite(w.elapsed_ms) ? w.elapsed_ms : null,
|
|
290
|
+
current,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Boulder runs, newest first, active pinned to the top. Keyed by `work_id`, so
|
|
296
|
+
* two runs of the same plan stay two rows. The root fields are a mirror of the
|
|
297
|
+
* active work — they only become a work of their own on a legacy state with no
|
|
298
|
+
* `works` map.
|
|
299
|
+
*/
|
|
300
|
+
function collectWorks(raw: RawBoulder, projectRoot: string): WorkView[] {
|
|
301
|
+
const out: WorkView[] = []
|
|
302
|
+
const workId = raw.active_work_id ?? null
|
|
303
|
+
const entries =
|
|
304
|
+
raw.works && typeof raw.works === "object" && !Array.isArray(raw.works)
|
|
305
|
+
? Object.entries(raw.works)
|
|
306
|
+
: []
|
|
307
|
+
|
|
308
|
+
for (const [id, w] of entries) {
|
|
309
|
+
if (!w || typeof w !== "object") continue
|
|
310
|
+
if (!(w.plan_name || w.active_plan || w.status)) continue
|
|
311
|
+
out.push(asWork(id, w, Boolean(workId && id === workId), projectRoot, raw.active_plan))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (out.length === 0 && (raw.plan_name || raw.active_plan || raw.status)) {
|
|
315
|
+
out.push(asWork(workId || "active", raw, true, projectRoot))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
out.sort((a, b) => {
|
|
319
|
+
if (a.current !== b.current) return a.current ? -1 : 1
|
|
320
|
+
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
// A dangling active_work_id leaves nobody current; boulder itself then mirrors
|
|
324
|
+
// the most recently updated work.
|
|
325
|
+
if (out.length > 0 && !out.some((w) => w.current)) out[0]!.current = true
|
|
326
|
+
return out.slice(0, 20)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function resolvePlanPath(projectRoot: string, activePlan: string | null): string | null {
|
|
330
|
+
if (!activePlan) return null
|
|
331
|
+
const planPath = path.isAbsolute(activePlan)
|
|
332
|
+
? activePlan
|
|
333
|
+
: path.join(projectRoot, activePlan)
|
|
334
|
+
try {
|
|
335
|
+
const root = path.resolve(projectRoot)
|
|
336
|
+
const real = path.resolve(planPath)
|
|
337
|
+
if (!real.startsWith(root) || !fs.existsSync(real)) return null
|
|
338
|
+
return real
|
|
339
|
+
} catch {
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function collectTasks(raw: Record<string, RawTask> | undefined): TaskView[] {
|
|
345
|
+
if (!raw || typeof raw !== "object") return []
|
|
346
|
+
const list: TaskView[] = []
|
|
347
|
+
for (const [key, t] of Object.entries(raw)) {
|
|
348
|
+
if (!t || typeof t !== "object") continue
|
|
349
|
+
list.push({
|
|
350
|
+
taskKey: t.task_key || key,
|
|
351
|
+
label: t.task_label ?? null,
|
|
352
|
+
title: t.task_title || key,
|
|
353
|
+
sessionId: stripSessionPrefix(t.session_id),
|
|
354
|
+
agent: t.agent ?? null,
|
|
355
|
+
category: t.category ?? null,
|
|
356
|
+
status: (t.status || "").toLowerCase() || (t.started_at ? BOULDER_STATUS_RUNNING : BOULDER_STATUS_PENDING),
|
|
357
|
+
startedAt: parseStamp(t.started_at),
|
|
358
|
+
endedAt: parseStamp(t.ended_at),
|
|
359
|
+
elapsedMs: typeof t.elapsed_ms === "number" && Number.isFinite(t.elapsed_ms) ? t.elapsed_ms : null,
|
|
360
|
+
updatedAt: parseStamp(t.updated_at) ?? parseStamp(t.started_at),
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
list.sort((a, b) => taskRank(a.status) - taskRank(b.status) || a.title.localeCompare(b.title))
|
|
364
|
+
return list.slice(0, 40)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function taskToDelegate(t: TaskView): Delegate {
|
|
368
|
+
return {
|
|
369
|
+
taskKey: t.taskKey,
|
|
370
|
+
title: t.title,
|
|
371
|
+
sessionId: t.sessionId,
|
|
372
|
+
agent: t.agent,
|
|
373
|
+
status: t.status,
|
|
374
|
+
updatedAt: t.updatedAt,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function countTasks(tasks: readonly TaskView[]): BoulderView["counts"] {
|
|
379
|
+
let running = 0
|
|
380
|
+
let done = 0
|
|
381
|
+
for (const t of tasks) {
|
|
382
|
+
const r = taskRank(t.status)
|
|
383
|
+
if (r === 0) running += 1
|
|
384
|
+
else if (r === 3) done += 1
|
|
385
|
+
}
|
|
386
|
+
return { running, done, other: tasks.length - running - done, total: tasks.length }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function emptyBoulder(): BoulderView {
|
|
390
|
+
return {
|
|
391
|
+
workId: null,
|
|
392
|
+
name: null,
|
|
393
|
+
status: null,
|
|
394
|
+
agent: null,
|
|
395
|
+
startedAt: null,
|
|
396
|
+
updatedAt: null,
|
|
397
|
+
endedAt: null,
|
|
398
|
+
elapsedMs: null,
|
|
399
|
+
sessions: [],
|
|
400
|
+
tasks: [],
|
|
401
|
+
counts: { running: 0, done: 0, other: 0, total: 0 },
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** The task boulder is on right now, if any. */
|
|
406
|
+
export function currentTask(boulder: BoulderView): TaskView | null {
|
|
407
|
+
return boulder.tasks.find((t) => taskRank(t.status) === 0) ?? null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function emptyOmo(): OmoSnapshot {
|
|
411
|
+
return {
|
|
412
|
+
present: false,
|
|
413
|
+
boulderPath: null,
|
|
414
|
+
planPath: null,
|
|
415
|
+
status: null,
|
|
416
|
+
agent: null,
|
|
417
|
+
planName: null,
|
|
418
|
+
plan: { total: 0, completed: 0, percent: 0, steps: [] },
|
|
419
|
+
works: [],
|
|
420
|
+
boulder: emptyBoulder(),
|
|
421
|
+
delegates: [],
|
|
422
|
+
stamp: "0",
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Read boulder + plan for a project directory. Never throws. */
|
|
427
|
+
export function readOmo(projectRoot: string | null | undefined): OmoSnapshot {
|
|
428
|
+
if (!projectRoot) return emptyOmo()
|
|
429
|
+
return profile("omo.read", () => readOmoInner(projectRoot))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Plan name → the latest boulder work's completion state, for the My-work
|
|
434
|
+
* reconciliation. Works are sorted newest-first, so the first entry per name
|
|
435
|
+
* wins — a completed plan re-run and still running stays `not-completed`.
|
|
436
|
+
*/
|
|
437
|
+
export function planWorkStateByPlanName(
|
|
438
|
+
projectRoot: string | null | undefined,
|
|
439
|
+
): Map<string, WorkState> {
|
|
440
|
+
const out = new Map<string, WorkState>()
|
|
441
|
+
if (!projectRoot) return out
|
|
442
|
+
for (const w of readOmo(projectRoot).works) {
|
|
443
|
+
if (!w.name || out.has(w.name)) continue
|
|
444
|
+
out.set(w.name, normalizeStatus(w.status) === STATUS_COMPLETED ? WORK_STATE_COMPLETED : WORK_STATE_NOT_COMPLETED)
|
|
445
|
+
}
|
|
446
|
+
return out
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function readOmoInner(projectRoot: string): OmoSnapshot {
|
|
450
|
+
const root = canonicalizePath(projectRoot)
|
|
451
|
+
const boulderPath = findBoulder(root)
|
|
452
|
+
// Installed omo (config marker) with no active run: present, but nothing to show.
|
|
453
|
+
if (!boulderPath) {
|
|
454
|
+
if (isOmoPresent(root)) return { ...emptyOmo(), present: true }
|
|
455
|
+
return emptyOmo()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const raw = readJson(boulderPath) as RawBoulder | null
|
|
459
|
+
if (!raw) return { ...emptyOmo(), present: true, boulderPath, stamp: fileStamp(boulderPath) }
|
|
460
|
+
|
|
461
|
+
const works = collectWorks(raw, root)
|
|
462
|
+
const active = works.find((w) => w.current) ?? null
|
|
463
|
+
// Root fields mirror the active work; fall back to the work entry itself.
|
|
464
|
+
const mirror: RawWork = (active && raw.works?.[active.workId]) || raw
|
|
465
|
+
|
|
466
|
+
const planName = raw.plan_name || mirror.plan_name || null
|
|
467
|
+
const activePlan = raw.active_plan || mirror.active_plan || null
|
|
468
|
+
const agent = raw.agent || mirror.agent || null
|
|
469
|
+
const status = raw.status || mirror.status || null
|
|
470
|
+
const rootTasks = raw.task_sessions
|
|
471
|
+
const tasks = collectTasks(
|
|
472
|
+
rootTasks && Object.keys(rootTasks).length ? rootTasks : mirror.task_sessions,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
const planPath = resolvePlanPath(root, activePlan)
|
|
476
|
+
// Plan checklist is not shown in the sidebar; Docs reads the file on demand.
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
present: true,
|
|
480
|
+
boulderPath,
|
|
481
|
+
planPath,
|
|
482
|
+
status,
|
|
483
|
+
agent,
|
|
484
|
+
planName,
|
|
485
|
+
plan: { total: 0, completed: 0, percent: 0, steps: [] },
|
|
486
|
+
works,
|
|
487
|
+
boulder: {
|
|
488
|
+
workId: active?.workId ?? null,
|
|
489
|
+
name: active?.name ?? planName,
|
|
490
|
+
status: active?.status ?? status,
|
|
491
|
+
agent: active?.agent ?? agent,
|
|
492
|
+
startedAt: active?.startedAt ?? parseStamp(raw.started_at),
|
|
493
|
+
updatedAt: active?.updatedAt ?? parseStamp(raw.updated_at),
|
|
494
|
+
endedAt: active?.endedAt ?? parseStamp(raw.ended_at),
|
|
495
|
+
elapsedMs: active?.elapsedMs ?? null,
|
|
496
|
+
sessions: active?.sessions ?? collectSessions(raw.session_ids, raw.session_origins),
|
|
497
|
+
tasks,
|
|
498
|
+
counts: countTasks(tasks),
|
|
499
|
+
},
|
|
500
|
+
delegates: tasks.map(taskToDelegate),
|
|
501
|
+
stamp: fileStamp(boulderPath),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.omo.resolver.config
|
|
3
|
+
*
|
|
4
|
+
* Oh-my-openagent config resolution — team mode + agent names from the user's
|
|
5
|
+
* `oh-my-openagent.json`. Best-effort, never throws.
|
|
6
|
+
*/
|
|
7
|
+
import os from "node:os"
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import { profile } from "../../pware.oc.core/pware.oc.core.debug.js"
|
|
10
|
+
import { readJson } from "../../pware.oc.core/pware.oc.core.paths.js"
|
|
11
|
+
|
|
12
|
+
export type OmoConfigView = {
|
|
13
|
+
present: boolean
|
|
14
|
+
path: string | null
|
|
15
|
+
teamMode: boolean | null
|
|
16
|
+
agents: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readOmoConfig(): OmoConfigView {
|
|
20
|
+
return profile("omo.config", () => {
|
|
21
|
+
const home = os.homedir()
|
|
22
|
+
const candidates = [
|
|
23
|
+
path.join(process.env.XDG_CONFIG_HOME || path.join(home, ".config"), "opencode", "oh-my-openagent.json"),
|
|
24
|
+
path.join(home, ".config", "opencode", "oh-my-openagent.json"),
|
|
25
|
+
]
|
|
26
|
+
for (const p of candidates) {
|
|
27
|
+
const raw = readJson(p)
|
|
28
|
+
if (!raw) continue
|
|
29
|
+
const team = raw.team_mode as { enabled?: boolean } | undefined
|
|
30
|
+
const agents = raw.agents as Record<string, unknown> | undefined
|
|
31
|
+
return {
|
|
32
|
+
present: true,
|
|
33
|
+
path: p,
|
|
34
|
+
teamMode: team?.enabled ?? null,
|
|
35
|
+
agents: Object.keys(agents || {}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { present: false, path: null, teamMode: null, agents: [] }
|
|
39
|
+
})
|
|
40
|
+
}
|