opencode-extended-sidebar 0.2.90 → 0.2.91

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.
Files changed (108) hide show
  1. package/.oesignore +17 -17
  2. package/CHANGELOG.md +96 -95
  3. package/LICENSE +21 -21
  4. package/README.md +150 -235
  5. package/oes.json +13 -11
  6. package/package.json +100 -100
  7. package/scripts/install-git-hooks.mjs +29 -29
  8. package/src/pware.oc.core/constants/index.ts +18 -18
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
  19. package/src/pware.oc.core/git/index.ts +7 -7
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
  22. package/src/pware.oc.core/index.ts +19 -18
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
  34. package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
  35. package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
  36. package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
  37. package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
  38. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
  39. package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
  40. package/src/pware.oc.omo/constants/index.ts +15 -15
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
  47. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
  48. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
  49. package/src/pware.oc.omo/index.ts +7 -7
  50. package/src/pware.oc.omo/resolver/index.ts +19 -19
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
  62. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
  63. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
  64. package/src/pware.oc.opencode/constants/index.ts +10 -10
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
  67. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
  68. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
  69. package/src/pware.oc.opencode/index.ts +8 -8
  70. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
  71. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
  72. package/src/pware.oc.opencode/resolver/index.ts +223 -170
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
  76. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
  77. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
  78. package/src/pware.oc.perf/index.ts +14 -14
  79. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
  80. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
  81. package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
  82. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
  85. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
  86. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
  87. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
  88. package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
  89. package/src/pware.oc.runtime/index.ts +13 -13
  90. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
  91. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
  92. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
  93. package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
  95. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
  96. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
  97. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
  98. package/src/pware.oc.runtime/resolver/index.ts +268 -224
  99. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
  100. package/src/pware.oc.ui/index.ts +12 -12
  101. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
  102. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
  103. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
  104. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
  105. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
  106. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
  107. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
  108. package/src/pware.oc.ui.tsx +98 -98
@@ -1,361 +1,363 @@
1
- /**
2
- * pware.oc.omo.resolver.planFile
3
- *
4
- * Omo-file → writer-session index. Reads the OpenCode SQLite database through
5
- * a raw `SqlDb` handle to map `.omo/` / `.sisyphus/` document files to the
6
- * session that last wrote them. Lives in the omo layer because omo may read
7
- * opencode data (one-way relaxation): the index needs the session/part rows,
8
- * not any opencode-layer code.
9
- *
10
- * One generic engine (`omoFileIndex` / `sessionForOmoFile`) serves every
11
- * document kind; the plan/draft index (`planSessionIndex` /
12
- * `sessionForPlanFile`) is a legacy specialisation that folds plans and drafts
13
- * into one map so a session's latest plan is preferred over its latest draft.
14
- */
15
- import { createStampCache } from "../../pware.oc.core/pware.oc.core.cache.js"
16
- import { basenameOf, str } from "../../pware.oc.core/pware.oc.core.paths.js"
17
- import type { SqlDb } from "../../pware.oc.core/pware.oc.core.sqlite.js"
18
- import { DOC_KIND_PLAN, type DocKind } from "../constants/pware.oc.omo.constants.docKind.js"
19
- import {
20
- listOmoFiles,
21
- type DocView,
22
- type ListOmoFilesOptions,
23
- } from "./pware.oc.omo.resolver.doc.js"
24
-
25
- /** Every OMO on-disk document kind (the subdirectory under `.omo/` / `.sisyphus/`). */
26
- export const OMO_FILE_KINDS = [
27
- "plan",
28
- "draft",
29
- "notepad",
30
- "proof",
31
- "rule",
32
- "run-continuation",
33
- ] as const
34
-
35
- /** An OMO on-disk document kind. */
36
- export type OmoFileKind = (typeof OMO_FILE_KINDS)[number]
37
-
38
- /** One case-insensitive regex per kind matching the omo/sisyphus subdir. */
39
- const OMO_FILE_KIND_RE: Record<OmoFileKind, RegExp> = {
40
- plan: /\.(omo|sisyphus)\/plans\//i,
41
- draft: /\.(omo|sisyphus)\/drafts\//i,
42
- notepad: /\.(omo|sisyphus)\/notepads\//i,
43
- proof: /\.(omo|sisyphus)\/evidence\//i,
44
- rule: /\.(omo|sisyphus)\/rules\//i,
45
- "run-continuation": /\.(omo|sisyphus)\/run-continuation\//i,
46
- }
47
-
48
- export type PlanSessionIndex = {
49
- fileWriter: Map<string, { sessionId: string; lastAt: number }>
50
- sessionPlan: Map<string, { rel: string; lastAt: number; isPlan: boolean }>
51
- }
52
-
53
- export type OmoFileIndex = {
54
- fileWriter: Map<string, { sessionId: string; lastAt: number }>
55
- sessionFiles: Map<string, { rel: string; lastAt: number }>
56
- }
57
-
58
- const PLAN_FILE_RE = /\.(omo|sisyphus)\/(drafts|plans)\//i
59
-
60
- /**
61
- * Normalise `fp` to a project-relative posix path, match it against `regex`,
62
- * and slice to the marker start. `projectRoot` strips the absolute prefix;
63
- * without it the fp is used as-is and the marker may sit mid-path. Null when
64
- * the path does not match `regex`.
65
- */
66
- function omoRelOf(
67
- fp: string,
68
- projectRoot: string | null | undefined,
69
- regex: RegExp,
70
- ): string | null {
71
- let rel = fp.replace(/\\/g, "/").trim()
72
- if (!rel) return null
73
- if (projectRoot) {
74
- const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "")
75
- if (rel.toLowerCase().startsWith(root.toLowerCase() + "/")) {
76
- rel = rel.slice(root.length + 1)
77
- } else if (rel.toLowerCase() === root.toLowerCase()) {
78
- rel = ""
79
- }
80
- }
81
- const m = rel.match(regex)
82
- if (!m) return null
83
- const start = m.index ?? 0
84
- return start > 0 ? rel.slice(start) : rel
85
- }
86
-
87
- /**
88
- * If `fp` is a plan/draft target under `.omo/` or `.sisyphus/`, return its
89
- * project-relative path and whether it is under `plans/`. `projectRoot`
90
- * strips the absolute prefix; without it the fp is used as-is and the marker
91
- * may sit mid-path. Null for anything else (plain source files included).
92
- */
93
- function planFileOf(
94
- fp: string,
95
- projectRoot: string | null | undefined,
96
- ): { rel: string; isPlan: boolean } | null {
97
- const rel = omoRelOf(fp, projectRoot, PLAN_FILE_RE)
98
- if (!rel) return null
99
- const m = rel.match(PLAN_FILE_RE)
100
- return { rel, isPlan: m?.[2]?.toLowerCase() === "plans" }
101
- }
102
-
103
- /** `MAX(part.time_updated)` over the part/session join, or null on error. */
104
- function partsStampInfo(
105
- db: SqlDb,
106
- projectId: string | null | undefined,
107
- ): { id: string; max: number } | null {
108
- try {
109
- const where = projectId ? "session.project_id = ?" : "1=1"
110
- const args = projectId ? [projectId] : []
111
- const p = db.get<{ m: number | null }>(
112
- `SELECT MAX(part.time_updated) AS m
113
- FROM part JOIN session ON session.id = part.session_id
114
- WHERE ${where}`,
115
- ...args,
116
- )
117
- return { id: projectId ?? "*", max: p?.m ?? 0 }
118
- } catch {
119
- return null
120
- }
121
- }
122
-
123
- function planIndexStamp(db: SqlDb, projectId: string | null | undefined): string {
124
- const info = partsStampInfo(db, projectId)
125
- return info ? `${info.id}|${info.max}` : "x"
126
- }
127
-
128
- function omoFileStamp(db: SqlDb, projectId: string | null | undefined, kind: OmoFileKind): string {
129
- const info = partsStampInfo(db, projectId)
130
- return info ? `${info.id}|${kind}|${info.max}` : "x"
131
- }
132
-
133
- /** One write-ish part row: session, resolved filePath, latest write time, and
134
- * whether the session ever `write`-created the file. */
135
- type FilePartRow = {
136
- session_id: string | null
137
- fp: string | null
138
- last: number | null
139
- is_writer: number | null
140
- }
141
-
142
- function queryFileParts(db: SqlDb, projectId: string | null | undefined): FilePartRow[] {
143
- const where = projectId ? "session.project_id = ? AND" : ""
144
- const args = projectId ? [projectId] : []
145
- return db.all<FilePartRow>(
146
- `SELECT part.session_id,
147
- COALESCE(
148
- json_extract(part.data,'$.state.input.filePath'),
149
- json_extract(part.data,'$.input.filePath'),
150
- json_extract(part.data,'$.filePath')
151
- ) AS fp,
152
- MAX(part.time_updated) AS last,
153
- MAX(CASE WHEN json_extract(part.data,'$.tool')='write' THEN 1 ELSE 0 END) AS is_writer
154
- FROM part
155
- JOIN session ON session.id = part.session_id
156
- WHERE ${where} json_extract(part.data,'$.type')='tool'
157
- AND json_extract(part.data,'$.tool') IN ('write','edit','multiedit','apply_edit','applyedit')
158
- GROUP BY part.session_id, fp`,
159
- ...args,
160
- )
161
- }
162
-
163
- function buildPlanSessionIndex(
164
- db: SqlDb,
165
- projectId: string | null | undefined,
166
- projectRoot: string | null | undefined,
167
- ): PlanSessionIndex {
168
- const fileWriter = new Map<string, { sessionId: string; lastAt: number }>()
169
- const writerPref = new Map<string, number>() // basename → is_writer of the current entry
170
- const sessionPlan = new Map<string, { rel: string; lastAt: number; isPlan: boolean }>()
171
- try {
172
- for (const row of queryFileParts(db, projectId)) {
173
- const sessionId = str(row.session_id)
174
- const fp = str(row.fp)
175
- const last = typeof row.last === "number" && row.last > 0 ? row.last : 0
176
- if (!sessionId || !fp) continue
177
- const info = planFileOf(fp, projectRoot)
178
- if (!info) continue
179
- const base = basenameOf(info.rel)
180
- const isWriter = row.is_writer === 1 ? 1 : 0
181
- const writer = fileWriter.get(base)
182
- if (!writer) {
183
- fileWriter.set(base, { sessionId, lastAt: last })
184
- writerPref.set(base, isWriter)
185
- } else {
186
- const cur = writerPref.get(base) ?? 0
187
- // A `write` creator beats a session that only edited the file, even
188
- // when the edit came later; equal preference falls back to the later
189
- // timestamp.
190
- if (isWriter > cur || (isWriter === cur && last > writer.lastAt)) {
191
- fileWriter.set(base, { sessionId, lastAt: last })
192
- writerPref.set(base, isWriter)
193
- }
194
- }
195
- const plan = sessionPlan.get(sessionId)
196
- if (!plan) {
197
- sessionPlan.set(sessionId, { rel: info.rel, lastAt: last, isPlan: info.isPlan })
198
- } else if (last > plan.lastAt) {
199
- sessionPlan.set(sessionId, { rel: info.rel, lastAt: last, isPlan: info.isPlan })
200
- } else if (last === plan.lastAt && info.isPlan && !plan.isPlan) {
201
- sessionPlan.set(sessionId, { rel: info.rel, lastAt: last, isPlan: info.isPlan })
202
- }
203
- }
204
- } catch {
205
- // soft-fail → empty index
206
- }
207
- return { fileWriter, sessionPlan }
208
- }
209
-
210
- function buildOmoFileIndex(
211
- db: SqlDb,
212
- projectId: string | null | undefined,
213
- projectRoot: string | null | undefined,
214
- kind: OmoFileKind,
215
- ): OmoFileIndex {
216
- const fileWriter = new Map<string, { sessionId: string; lastAt: number }>()
217
- const writerPref = new Map<string, number>() // basename → is_writer of the current entry
218
- const sessionFiles = new Map<string, { rel: string; lastAt: number }>()
219
- const regex = OMO_FILE_KIND_RE[kind]
220
- try {
221
- for (const row of queryFileParts(db, projectId)) {
222
- const sessionId = str(row.session_id)
223
- const fp = str(row.fp)
224
- const last = typeof row.last === "number" && row.last > 0 ? row.last : 0
225
- if (!sessionId || !fp) continue
226
- const rel = omoRelOf(fp, projectRoot, regex)
227
- if (!rel) continue
228
- const base = basenameOf(rel)
229
- const isWriter = row.is_writer === 1 ? 1 : 0
230
- const writer = fileWriter.get(base)
231
- if (!writer) {
232
- fileWriter.set(base, { sessionId, lastAt: last })
233
- writerPref.set(base, isWriter)
234
- } else {
235
- const cur = writerPref.get(base) ?? 0
236
- // A `write` creator beats a session that only edited the file, even
237
- // when the edit came later; equal preference falls back to the later
238
- // timestamp.
239
- if (isWriter > cur || (isWriter === cur && last > writer.lastAt)) {
240
- fileWriter.set(base, { sessionId, lastAt: last })
241
- writerPref.set(base, isWriter)
242
- }
243
- }
244
- const curFile = sessionFiles.get(sessionId)
245
- if (!curFile || last > curFile.lastAt) {
246
- sessionFiles.set(sessionId, { rel, lastAt: last })
247
- }
248
- }
249
- } catch {
250
- // soft-fail → empty index
251
- }
252
- return { fileWriter, sessionFiles }
253
- }
254
-
255
- const planIndexCache = createStampCache<PlanSessionIndex>()
256
- const omoIndexCache = createStampCache<OmoFileIndex>()
257
-
258
- /**
259
- * In-memory bidirectional plan-file index for one project, built from a single
260
- * write-ish `part` join. `fileWriter` maps a plan/draft basename to its latest
261
- * writer session; `sessionPlan` maps a session to its latest plan/draft file,
262
- * preferring `plans/` over `drafts/` on an equal timestamp. Stamp-cached on the
263
- * project's `MAX(part.time_updated)` so it rebuilds only when parts change.
264
- */
265
- export function planSessionIndex(
266
- db: SqlDb,
267
- projectId: string | null | undefined,
268
- projectRoot: string | null | undefined,
269
- ): PlanSessionIndex {
270
- return planIndexCache.get(planIndexStamp(db, projectId), () =>
271
- buildPlanSessionIndex(db, projectId, projectRoot),
272
- )
273
- }
274
-
275
- /**
276
- * The session that last wrote a `.omo/` plan/draft file, if any. Matches
277
- * write-ish tool parts by the file's basename, so the planner session that
278
- * created a pending approval is the one "Continue" should jump to.
279
- */
280
- export function sessionForPlanFile(db: SqlDb, relPath: string | null | undefined): string | null {
281
- const base = basenameOf(relPath ?? "")
282
- if (!base || base === "file") return null
283
- return planSessionIndex(db, null, null).fileWriter.get(base)?.sessionId ?? null
284
- }
285
-
286
- /**
287
- * In-memory bidirectional index for one OMO document kind, built from a single
288
- * write-ish `part` join. `fileWriter` maps a file basename to its latest writer
289
- * session; `sessionFiles` maps a session to its latest file of that kind.
290
- * Stamp-cached on the project's `MAX(part.time_updated)` so it rebuilds only
291
- * when parts change.
292
- */
293
- export function omoFileIndex(
294
- db: SqlDb,
295
- projectId: string | null | undefined,
296
- projectRoot: string | null | undefined,
297
- kind: OmoFileKind,
298
- ): OmoFileIndex {
299
- return omoIndexCache.get(omoFileStamp(db, projectId, kind), () =>
300
- buildOmoFileIndex(db, projectId, projectRoot, kind),
301
- )
302
- }
303
-
304
- /**
305
- * The writer session for a rel path from an already-built index. The per-file
306
- * half of `sessionForOmoFile`, split out so a caller can filter a whole list
307
- * against one index build instead of one stamp lookup per file.
308
- */
309
- export function omoWriterSession(
310
- index: OmoFileIndex,
311
- relPath: string | null | undefined,
312
- ): string | null {
313
- const base = basenameOf(relPath ?? "")
314
- if (!base || base === "file") return null
315
- return index.fileWriter.get(base)?.sessionId ?? null
316
- }
317
-
318
- /**
319
- * The session that last wrote a `.omo/` file of `kind`, if any. Matches
320
- * write-ish tool parts by the file's basename.
321
- */
322
- export function sessionForOmoFile(
323
- db: SqlDb,
324
- relPath: string | null | undefined,
325
- kind: OmoFileKind,
326
- ): string | null {
327
- return omoWriterSession(omoFileIndex(db, null, null, kind), relPath)
328
- }
329
-
330
- /** A per-kind omo-file resolver: the index builder + the writer-session lookup. */
331
- export function makeOmoFileResolver(kind: OmoFileKind): {
332
- sessionIndex: (
333
- db: SqlDb,
334
- projectId: string | null | undefined,
335
- projectRoot: string | null | undefined,
336
- ) => OmoFileIndex
337
- sessionForFile: (db: SqlDb, relPath: string | null | undefined) => string | null
338
- } {
339
- return {
340
- sessionIndex: (db, projectId, projectRoot) => omoFileIndex(db, projectId, projectRoot, kind),
341
- sessionForFile: (db, relPath) => sessionForOmoFile(db, relPath, kind),
342
- }
343
- }
344
-
345
- /** A per-doc-kind `.list` wrapper — the `XxxFile.list` shape. */
346
- export function makeOmoFileList(docKind: DocKind): {
347
- list: (
348
- projectRoot: string | null | undefined,
349
- sessionId?: string | null,
350
- opts?: ListOmoFilesOptions,
351
- ) => DocView[]
352
- } {
353
- return {
354
- list(projectRoot, sessionId = null, opts = {}) {
355
- return listOmoFiles(docKind, projectRoot, { ...opts, sessionId })
356
- },
357
- }
358
- }
359
-
360
- /** Plan files under `.omo/plans/`, optionally filtered by writer session. */
361
- export const PlanFile = makeOmoFileList(DOC_KIND_PLAN)
1
+ /**
2
+ * pware.oc.omo.resolver.planFile
3
+ *
4
+ * Omo-file → writer-session index. Reads the OpenCode SQLite database through
5
+ * a raw `SqlDb` handle to map `.omo/` / `.sisyphus/` document files to the
6
+ * session that last wrote them. Lives in the omo layer because omo may read
7
+ * opencode data (one-way relaxation): the index needs the session/part rows,
8
+ * not any opencode-layer code.
9
+ *
10
+ * One generic engine (`omoFileIndex` / `sessionForOmoFile`) serves every
11
+ * document kind; the plan/draft index (`planSessionIndex` /
12
+ * `sessionForPlanFile`) is a legacy specialisation that folds plans and drafts
13
+ * into one map so a session's latest plan is preferred over its latest draft.
14
+ */
15
+ import { createStampCache } from "../../pware.oc.core/pware.oc.core.cache.js"
16
+ import { basenameOf, str } from "../../pware.oc.core/pware.oc.core.paths.js"
17
+ import type { SqlDb } from "../../pware.oc.core/pware.oc.core.sqlite.js"
18
+ import { DOC_KIND_PLAN, type DocKind } from "../constants/pware.oc.omo.constants.docKind.js"
19
+ import {
20
+ listOmoFiles,
21
+ type DocView,
22
+ type ListOmoFilesOptions,
23
+ } from "./pware.oc.omo.resolver.doc.js"
24
+
25
+ /** Every OMO on-disk document kind (the subdirectory under `.omo/` / `.sisyphus/`). */
26
+ export const OMO_FILE_KINDS = [
27
+ "plan",
28
+ "draft",
29
+ "notepad",
30
+ "proof",
31
+ "rule",
32
+ "run-continuation",
33
+ ] as const
34
+
35
+ /** An OMO on-disk document kind. */
36
+ export type OmoFileKind = (typeof OMO_FILE_KINDS)[number]
37
+
38
+ /** One case-insensitive regex per kind matching the omo/sisyphus subdir. */
39
+ const OMO_FILE_KIND_RE: Record<OmoFileKind, RegExp> = {
40
+ plan: /\.(omo|sisyphus)\/plans\//i,
41
+ draft: /\.(omo|sisyphus)\/drafts\//i,
42
+ notepad: /\.(omo|sisyphus)\/notepads\//i,
43
+ proof: /\.(omo|sisyphus)\/evidence\//i,
44
+ rule: /\.(omo|sisyphus)\/rules\//i,
45
+ "run-continuation": /\.(omo|sisyphus)\/run-continuation\//i,
46
+ }
47
+
48
+ export type PlanSessionIndex = {
49
+ fileWriter: Map<string, { sessionId: string; lastAt: number }>
50
+ sessionPlan: Map<string, { rel: string; lastAt: number; isPlan: boolean }>
51
+ }
52
+
53
+ export type OmoFileIndex = {
54
+ fileWriter: Map<string, { sessionId: string; lastAt: number }>
55
+ sessionFiles: Map<string, { rel: string; lastAt: number }>
56
+ }
57
+
58
+ const PLAN_FILE_RE = /\.(omo|sisyphus)\/(drafts|plans)\//i
59
+
60
+ /**
61
+ * Normalise `fp` to a project-relative posix path, match it against `regex`,
62
+ * and slice to the marker start. `projectRoot` strips the absolute prefix;
63
+ * without it the fp is used as-is and the marker may sit mid-path. Null when
64
+ * the path does not match `regex`.
65
+ */
66
+ function omoRelOf(
67
+ fp: string,
68
+ projectRoot: string | null | undefined,
69
+ regex: RegExp,
70
+ ): string | null {
71
+ let rel = fp.replace(/\\/g, "/").trim()
72
+ if (!rel) return null
73
+ if (projectRoot) {
74
+ const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "")
75
+ if (rel.toLowerCase().startsWith(root.toLowerCase() + "/")) {
76
+ rel = rel.slice(root.length + 1)
77
+ } else if (rel.toLowerCase() === root.toLowerCase()) {
78
+ rel = ""
79
+ }
80
+ }
81
+ const m = rel.match(regex)
82
+ if (!m) return null
83
+ const start = m.index ?? 0
84
+ return start > 0 ? rel.slice(start) : rel
85
+ }
86
+
87
+ /**
88
+ * If `fp` is a plan/draft target under `.omo/` or `.sisyphus/`, return its
89
+ * project-relative path and whether it is under `plans/`. `projectRoot`
90
+ * strips the absolute prefix; without it the fp is used as-is and the marker
91
+ * may sit mid-path. Null for anything else (plain source files included).
92
+ */
93
+ function planFileOf(
94
+ fp: string,
95
+ projectRoot: string | null | undefined,
96
+ ): { rel: string; isPlan: boolean } | null {
97
+ const rel = omoRelOf(fp, projectRoot, PLAN_FILE_RE)
98
+ if (!rel) return null
99
+ const m = rel.match(PLAN_FILE_RE)
100
+ return { rel, isPlan: m?.[2]?.toLowerCase() === "plans" }
101
+ }
102
+
103
+ /** `MAX(part.time_updated)` over the part/session join, or null on error. */
104
+ function partsStampInfo(
105
+ db: SqlDb,
106
+ projectId: string | null | undefined,
107
+ ): { id: string; max: number } | null {
108
+ try {
109
+ const where = projectId ? "session.project_id = ?" : "1=1"
110
+ const args = projectId ? [projectId] : []
111
+ const p = db.get<{ m: number | null }>(
112
+ `SELECT MAX(part.time_updated) AS m
113
+ FROM part JOIN session ON session.id = part.session_id
114
+ WHERE ${where}`,
115
+ ...args,
116
+ )
117
+ return { id: projectId ?? "*", max: p?.m ?? 0 }
118
+ } catch {
119
+ return null
120
+ }
121
+ }
122
+
123
+ function planIndexStamp(db: SqlDb, projectId: string | null | undefined): string {
124
+ const info = partsStampInfo(db, projectId)
125
+ return info ? `${info.id}|${info.max}` : "x"
126
+ }
127
+
128
+ function omoFileStamp(db: SqlDb, projectId: string | null | undefined, kind: OmoFileKind): string {
129
+ const info = partsStampInfo(db, projectId)
130
+ return info ? `${info.id}|${kind}|${info.max}` : "x"
131
+ }
132
+
133
+ /** One write-ish part row: session, resolved filePath, latest write time, and
134
+ * whether the session ever `write`-created the file. */
135
+ type FilePartRow = {
136
+ session_id: string | null
137
+ fp: string | null
138
+ last: number | null
139
+ is_writer: number | null
140
+ }
141
+
142
+ function queryFileParts(db: SqlDb, projectId: string | null | undefined): FilePartRow[] {
143
+ const where = projectId ? "session.project_id = ? AND" : ""
144
+ const args = projectId ? [projectId] : []
145
+ return db.all<FilePartRow>(
146
+ `SELECT part.session_id,
147
+ COALESCE(
148
+ json_extract(part.data,'$.state.input.filePath'),
149
+ json_extract(part.data,'$.input.filePath'),
150
+ json_extract(part.data,'$.filePath')
151
+ ) AS fp,
152
+ MAX(part.time_updated) AS last,
153
+ MAX(CASE WHEN json_extract(part.data,'$.tool')='write' THEN 1 ELSE 0 END) AS is_writer
154
+ FROM part
155
+ JOIN session ON session.id = part.session_id
156
+ WHERE ${where} json_extract(part.data,'$.type')='tool'
157
+ AND json_extract(part.data,'$.tool') IN ('write','edit','multiedit','apply_edit','applyedit')
158
+ GROUP BY part.session_id, fp`,
159
+ ...args,
160
+ )
161
+ }
162
+
163
+ function buildPlanSessionIndex(
164
+ db: SqlDb,
165
+ projectId: string | null | undefined,
166
+ projectRoot: string | null | undefined,
167
+ ): PlanSessionIndex {
168
+ const fileWriter = new Map<string, { sessionId: string; lastAt: number }>()
169
+ const writerPref = new Map<string, number>() // basename → is_writer of the current entry
170
+ const sessionPlan = new Map<string, { rel: string; lastAt: number; isPlan: boolean }>()
171
+ try {
172
+ for (const row of queryFileParts(db, projectId)) {
173
+ const sessionId = str(row.session_id)
174
+ const fp = str(row.fp)
175
+ const last = typeof row.last === "number" && row.last > 0 ? row.last : 0
176
+ if (!sessionId || !fp) continue
177
+ const info = planFileOf(fp, projectRoot)
178
+ if (!info) continue
179
+ const base = basenameOf(info.rel)
180
+ const isWriter = row.is_writer === 1 ? 1 : 0
181
+ const writer = fileWriter.get(base)
182
+ if (!writer) {
183
+ fileWriter.set(base, { sessionId, lastAt: last })
184
+ writerPref.set(base, isWriter)
185
+ } else {
186
+ const cur = writerPref.get(base) ?? 0
187
+ // A `write` creator beats a session that only edited the file, even
188
+ // when the edit came later; equal preference falls back to the later
189
+ // timestamp.
190
+ if (isWriter > cur || (isWriter === cur && last > writer.lastAt)) {
191
+ fileWriter.set(base, { sessionId, lastAt: last })
192
+ writerPref.set(base, isWriter)
193
+ }
194
+ }
195
+ const plan = sessionPlan.get(sessionId)
196
+ if (!plan) {
197
+ sessionPlan.set(sessionId, { rel: info.rel, lastAt: last, isPlan: info.isPlan })
198
+ } else if (last > plan.lastAt) {
199
+ sessionPlan.set(sessionId, { rel: info.rel, lastAt: last, isPlan: info.isPlan })
200
+ } else if (last === plan.lastAt && info.isPlan && !plan.isPlan) {
201
+ sessionPlan.set(sessionId, { rel: info.rel, lastAt: last, isPlan: info.isPlan })
202
+ }
203
+ }
204
+ } catch {
205
+ // soft-fail → empty index
206
+ }
207
+ return { fileWriter, sessionPlan }
208
+ }
209
+
210
+ function buildOmoFileIndex(
211
+ db: SqlDb,
212
+ projectId: string | null | undefined,
213
+ projectRoot: string | null | undefined,
214
+ kind: OmoFileKind,
215
+ ): OmoFileIndex {
216
+ const fileWriter = new Map<string, { sessionId: string; lastAt: number }>()
217
+ const writerPref = new Map<string, number>() // basename → is_writer of the current entry
218
+ const sessionFiles = new Map<string, { rel: string; lastAt: number }>()
219
+ const regex = OMO_FILE_KIND_RE[kind]
220
+ try {
221
+ for (const row of queryFileParts(db, projectId)) {
222
+ const sessionId = str(row.session_id)
223
+ const fp = str(row.fp)
224
+ const last = typeof row.last === "number" && row.last > 0 ? row.last : 0
225
+ if (!sessionId || !fp) continue
226
+ const rel = omoRelOf(fp, projectRoot, regex)
227
+ if (!rel) continue
228
+ const base = basenameOf(rel)
229
+ const isWriter = row.is_writer === 1 ? 1 : 0
230
+ const writer = fileWriter.get(base)
231
+ if (!writer) {
232
+ fileWriter.set(base, { sessionId, lastAt: last })
233
+ writerPref.set(base, isWriter)
234
+ } else {
235
+ const cur = writerPref.get(base) ?? 0
236
+ // A `write` creator beats a session that only edited the file, even
237
+ // when the edit came later; equal preference falls back to the later
238
+ // timestamp.
239
+ if (isWriter > cur || (isWriter === cur && last > writer.lastAt)) {
240
+ fileWriter.set(base, { sessionId, lastAt: last })
241
+ writerPref.set(base, isWriter)
242
+ }
243
+ }
244
+ const curFile = sessionFiles.get(sessionId)
245
+ if (!curFile || last > curFile.lastAt) {
246
+ sessionFiles.set(sessionId, { rel, lastAt: last })
247
+ }
248
+ }
249
+ } catch {
250
+ // soft-fail → empty index
251
+ }
252
+ return { fileWriter, sessionFiles }
253
+ }
254
+
255
+ const planIndexCache = createStampCache<PlanSessionIndex>()
256
+ const omoIndexCache = createStampCache<OmoFileIndex>()
257
+
258
+ /**
259
+ * In-memory bidirectional plan-file index for one project, built from a single
260
+ * write-ish `part` join. `fileWriter` maps a plan/draft basename to its latest
261
+ * writer session; `sessionPlan` maps a session to its latest plan/draft file,
262
+ * preferring `plans/` over `drafts/` on an equal timestamp. Stamp-cached on the
263
+ * project's `MAX(part.time_updated)` so it rebuilds only when parts change.
264
+ */
265
+ export function planSessionIndex(
266
+ db: SqlDb,
267
+ projectId: string | null | undefined,
268
+ projectRoot: string | null | undefined,
269
+ ): PlanSessionIndex {
270
+ const key = `${db.path ?? ""}|${projectRoot ?? ""}|${planIndexStamp(db, projectId)}`
271
+ return planIndexCache.get(key, () =>
272
+ buildPlanSessionIndex(db, projectId, projectRoot),
273
+ )
274
+ }
275
+
276
+ /**
277
+ * The session that last wrote a `.omo/` plan/draft file, if any. Matches
278
+ * write-ish tool parts by the file's basename, so the planner session that
279
+ * created a pending approval is the one "Continue" should jump to.
280
+ */
281
+ export function sessionForPlanFile(db: SqlDb, relPath: string | null | undefined): string | null {
282
+ const base = basenameOf(relPath ?? "")
283
+ if (!base || base === "file") return null
284
+ return planSessionIndex(db, null, null).fileWriter.get(base)?.sessionId ?? null
285
+ }
286
+
287
+ /**
288
+ * In-memory bidirectional index for one OMO document kind, built from a single
289
+ * write-ish `part` join. `fileWriter` maps a file basename to its latest writer
290
+ * session; `sessionFiles` maps a session to its latest file of that kind.
291
+ * Stamp-cached on the project's `MAX(part.time_updated)` so it rebuilds only
292
+ * when parts change.
293
+ */
294
+ export function omoFileIndex(
295
+ db: SqlDb,
296
+ projectId: string | null | undefined,
297
+ projectRoot: string | null | undefined,
298
+ kind: OmoFileKind,
299
+ ): OmoFileIndex {
300
+ const key = `${db.path ?? ""}|${projectRoot ?? ""}|${omoFileStamp(db, projectId, kind)}`
301
+ return omoIndexCache.get(key, () =>
302
+ buildOmoFileIndex(db, projectId, projectRoot, kind),
303
+ )
304
+ }
305
+
306
+ /**
307
+ * The writer session for a rel path from an already-built index. The per-file
308
+ * half of `sessionForOmoFile`, split out so a caller can filter a whole list
309
+ * against one index build instead of one stamp lookup per file.
310
+ */
311
+ export function omoWriterSession(
312
+ index: OmoFileIndex,
313
+ relPath: string | null | undefined,
314
+ ): string | null {
315
+ const base = basenameOf(relPath ?? "")
316
+ if (!base || base === "file") return null
317
+ return index.fileWriter.get(base)?.sessionId ?? null
318
+ }
319
+
320
+ /**
321
+ * The session that last wrote a `.omo/` file of `kind`, if any. Matches
322
+ * write-ish tool parts by the file's basename.
323
+ */
324
+ export function sessionForOmoFile(
325
+ db: SqlDb,
326
+ relPath: string | null | undefined,
327
+ kind: OmoFileKind,
328
+ ): string | null {
329
+ return omoWriterSession(omoFileIndex(db, null, null, kind), relPath)
330
+ }
331
+
332
+ /** A per-kind omo-file resolver: the index builder + the writer-session lookup. */
333
+ export function makeOmoFileResolver(kind: OmoFileKind): {
334
+ sessionIndex: (
335
+ db: SqlDb,
336
+ projectId: string | null | undefined,
337
+ projectRoot: string | null | undefined,
338
+ ) => OmoFileIndex
339
+ sessionForFile: (db: SqlDb, relPath: string | null | undefined) => string | null
340
+ } {
341
+ return {
342
+ sessionIndex: (db, projectId, projectRoot) => omoFileIndex(db, projectId, projectRoot, kind),
343
+ sessionForFile: (db, relPath) => sessionForOmoFile(db, relPath, kind),
344
+ }
345
+ }
346
+
347
+ /** A per-doc-kind `.list` wrapper — the `XxxFile.list` shape. */
348
+ export function makeOmoFileList(docKind: DocKind): {
349
+ list: (
350
+ projectRoot: string | null | undefined,
351
+ sessionId?: string | null,
352
+ opts?: ListOmoFilesOptions,
353
+ ) => DocView[]
354
+ } {
355
+ return {
356
+ list(projectRoot, sessionId = null, opts = {}) {
357
+ return listOmoFiles(docKind, projectRoot, { ...opts, sessionId })
358
+ },
359
+ }
360
+ }
361
+
362
+ /** Plan files under `.omo/plans/`, optionally filtered by writer session. */
363
+ export const PlanFile = makeOmoFileList(DOC_KIND_PLAN)