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,399 +1,399 @@
1
- /**
2
- * Session file hits: basename + diff stats only.
3
- * Never keeps path, before/after, or tool I/O.
4
- */
5
- import { readGitMarksFor, relToGitRoot, type GitLetter } from "../pware.oc.core/git/pware.oc.core.git.js"
6
- import { profile } from "../pware.oc.core/pware.oc.core.debug.js"
7
- import { ignoredByGitignore, ignoredByOesignore } from "../pware.oc.core/git/pware.oc.core.gitignore.js"
8
- import { getOes } from "../pware.oc.core/pware.oc.core.oes.js"
9
- import { ROW_LINE_FALLBACK } from "../pware.oc.core/pware.oc.core.layout.js"
10
- import { strWidth, takeCols, takeLastCols } from "../pware.oc.core/pware.oc.core.width.js"
11
- import { eventKind, eventType } from "../pware.oc.core/pware.oc.core.events.js"
12
- import {
13
- EVENT_KIND_DB_REFRESH,
14
- EVENT_KIND_FILE,
15
- EVENT_KIND_TOOL,
16
- } from "../pware.oc.core/constants/pware.oc.core.constants.eventKind.js"
17
- import { basenameOf, finiteNum } from "../pware.oc.core/pware.oc.core.paths.js"
18
- import { sessionIdFromEvent } from "../pware.oc.core/pware.oc.core.pulse.js"
19
- import { READ_TOOLS, WRITE_TOOLS } from "../pware.oc.core/constants/pware.oc.core.constants.toolName.js"
20
- import { PART_TYPE_PATCH } from "../pware.oc.core/constants/pware.oc.core.constants.partType.js"
21
- import {
22
- FILE_TOUCH_READ,
23
- FILE_TOUCH_WRITE,
24
- type FileTouch,
25
- } from "./constants/pware.oc.opencode.constants.fileTouch.js"
26
-
27
- export { basenameOf, FILE_TOUCH_READ, FILE_TOUCH_WRITE, type FileTouch }
28
-
29
- export const FILE_ROWS = 8
30
-
31
- export type FileFilter = {
32
- skipGitignore?: boolean
33
- projectRoot?: string | null
34
- }
35
-
36
- export function fileFilter(projectRoot?: string | null): FileFilter {
37
- const o = getOes(projectRoot)
38
- return { skipGitignore: o.skipGitignore, projectRoot: projectRoot ?? null }
39
- }
40
-
41
- /** Git porcelain letters, plus `V` (viewed) which git does not use. */
42
- export type FileLetter = GitLetter | "V"
43
-
44
- export type FileView = {
45
- /** Internal key (path). Never shown. */
46
- id: string
47
- /** Basename only. */
48
- name: string
49
- additions: number
50
- deletions: number
51
- at: number
52
- touch: FileTouch
53
- letter: FileLetter | null
54
- }
55
-
56
- /** Long names: start…end.ext — no directories. Column budget, not code units. */
57
- export function shortFileName(raw: string, max = ROW_LINE_FALLBACK): string {
58
- const base = basenameOf(raw)
59
- if (strWidth(base) <= max) return base
60
- const dot = base.lastIndexOf(".")
61
- const ext = dot > 0 && dot >= base.length - 8 ? base.slice(dot) : ""
62
- const stem = ext ? base.slice(0, -ext.length) : base
63
- const ellip = "…"
64
- const room = max - strWidth(ext) - strWidth(ellip)
65
- if (room < 2) return `${takeCols(base, Math.max(1, max - 1))}${ellip}`
66
- const tail = Math.min(strWidth(stem), Math.max(3, Math.ceil(room * 0.55)))
67
- const head = Math.max(1, room - tail)
68
- if (head + tail >= strWidth(stem)) return takeCols(base, max)
69
- return `${takeCols(stem, head)}${ellip}${takeLastCols(stem, tail)}${ext}`
70
- }
71
-
72
- export function formatDiffStat(add: number, del: number): string {
73
- const a = add > 0 ? `+${add}` : ""
74
- const d = del > 0 ? `−${del}` : ""
75
- return [a, d].filter(Boolean).join(" ")
76
- }
77
-
78
- function num(v: unknown): number {
79
- const n = finiteNum(v)
80
- return n > 0 ? Math.round(n) : 0
81
- }
82
-
83
- function matchInt(data: string, key: string): number {
84
- const m = data.match(new RegExp(`"${key}"\\s*:\\s*(\\d+)`))
85
- if (!m?.[1]) return 0
86
- const n = Number(m[1])
87
- return Number.isFinite(n) && n > 0 ? n : 0
88
- }
89
-
90
- function statsFromBag(o: Record<string, unknown> | null | undefined): { add: number; del: number } {
91
- if (!o) return { add: 0, del: 0 }
92
- const fd = o.filediff && typeof o.filediff === "object" ? (o.filediff as Record<string, unknown>) : null
93
- return {
94
- add: num(fd?.additions) || num(o.additions),
95
- del: num(fd?.deletions) || num(o.deletions),
96
- }
97
- }
98
-
99
- function hidden(posixPath: string, filter?: FileFilter): boolean {
100
- if (ignoredByOesignore(posixPath, filter?.projectRoot)) return true
101
- return Boolean(filter?.skipGitignore && ignoredByGitignore(posixPath, filter.projectRoot))
102
- }
103
-
104
- function asFile(
105
- pathOrName: string,
106
- add: number,
107
- del: number,
108
- at: number,
109
- filter?: FileFilter,
110
- touch: FileTouch = FILE_TOUCH_WRITE,
111
- ): FileView | null {
112
- const raw = pathOrName.replace(/\\/g, "/").trim()
113
- if (!raw || raw === "." || raw === "..") return null
114
- if (hidden(raw, filter)) return null
115
- const name = basenameOf(raw)
116
- return { id: raw, name, additions: add, deletions: del, at, touch, letter: null }
117
- }
118
-
119
- function eventBag(evt: unknown): Record<string, unknown> | null {
120
- if (!evt || typeof evt !== "object") return null
121
- const o = evt as Record<string, unknown>
122
- if (o.properties && typeof o.properties === "object") {
123
- return o.properties as Record<string, unknown>
124
- }
125
- return o
126
- }
127
-
128
- function pickPath(o: Record<string, unknown>): string | null {
129
- for (const key of ["file", "filePath", "filepath", "path"]) {
130
- const v = o[key]
131
- if (typeof v === "string" && v.trim()) return v.trim()
132
- }
133
- return null
134
- }
135
-
136
- function fileFromDiffRow(row: unknown, at: number, filter?: FileFilter): FileView | null {
137
- if (!row || typeof row !== "object") return null
138
- const o = row as Record<string, unknown>
139
- const file = pickPath(o)
140
- if (!file) return null
141
- return asFile(file, num(o.additions) || num(o.added), num(o.deletions) || num(o.removed), at, filter, FILE_TOUCH_WRITE)
142
- }
143
-
144
- /** Paths + +/- only. Drops before/after and any other fields. */
145
- export function filesFromEvent(evt: unknown, sessionId: string, filter?: FileFilter): FileView[] {
146
- const type = eventType(evt)
147
- const bag = eventBag(evt)
148
- if (!bag) return []
149
- const sid = sessionIdFromEvent(evt)
150
- if (sid && sid !== sessionId) return []
151
- const kind = eventKind(type)
152
- if (type && kind !== EVENT_KIND_FILE && kind !== EVENT_KIND_TOOL && kind !== EVENT_KIND_DB_REFRESH) return []
153
- const at = Date.now()
154
- const out: FileView[] = []
155
-
156
- if (type.includes("session.diff") || Array.isArray(bag.diff)) {
157
- const rows = Array.isArray(bag.diff) ? bag.diff : []
158
- for (const row of rows) {
159
- const f = fileFromDiffRow(row, at, filter)
160
- if (f) out.push(f)
161
- }
162
- return out
163
- }
164
-
165
- if (type.includes("file.edited")) {
166
- const p = pickPath(bag)
167
- if (p) {
168
- const f = asFile(p, 0, 0, at, filter)
169
- if (f) out.push(f)
170
- }
171
- return out
172
- }
173
-
174
- if (type.includes("session.updated") || type.includes("session.created")) {
175
- const info =
176
- bag.info && typeof bag.info === "object" ? (bag.info as Record<string, unknown>) : bag
177
- const summary =
178
- info.summary && typeof info.summary === "object"
179
- ? (info.summary as Record<string, unknown>)
180
- : null
181
- const diffs = summary && Array.isArray(summary.diffs) ? summary.diffs : []
182
- for (const row of diffs) {
183
- const f = fileFromDiffRow(row, at, filter)
184
- if (f) out.push(f)
185
- }
186
- return out
187
- }
188
-
189
- if (type.includes("tool.called") || type.includes("tool.success") || type.includes("part.updated")) {
190
- const part =
191
- bag.part && typeof bag.part === "object" ? (bag.part as Record<string, unknown>) : bag
192
- const tool = String(part.tool ?? bag.tool ?? "").toLowerCase()
193
- const touch = touchOfTool(tool)
194
- if (!touch && !type.includes("file.edited")) return []
195
- const input =
196
- part.input && typeof part.input === "object"
197
- ? (part.input as Record<string, unknown>)
198
- : bag.input && typeof bag.input === "object"
199
- ? (bag.input as Record<string, unknown>)
200
- : null
201
- const p = input ? pickPath(input) : pickPath(part) ?? pickPath(bag)
202
- if (!p) return []
203
- const meta =
204
- part.metadata && typeof part.metadata === "object"
205
- ? (part.metadata as Record<string, unknown>)
206
- : null
207
- const { add, del } = statsFromBag(meta ?? part)
208
- const f = asFile(p, add, del, at, filter, touch ?? FILE_TOUCH_WRITE)
209
- return f ? [f] : []
210
- }
211
-
212
- return out
213
- }
214
-
215
- const WRITE_TOOL_SET = new Set<string>(WRITE_TOOLS)
216
- const READ_TOOL_SET = new Set<string>(READ_TOOLS)
217
-
218
- function touchOfTool(tool: string): FileTouch | null {
219
- if (READ_TOOL_SET.has(tool)) return FILE_TOUCH_READ
220
- if (WRITE_TOOL_SET.has(tool)) return FILE_TOUCH_WRITE
221
- return null
222
- }
223
-
224
- const FILEPATH_RE = /"(?:filePath|filepath)"\s*:\s*"((?:\\.|[^"\\])+)"/
225
- const TOOL_FILE_RE = new RegExp(
226
- `"tool"\\s*:\\s*"(${[...WRITE_TOOLS, ...READ_TOOLS].join("|")})"`,
227
- "i",
228
- )
229
-
230
- function unescapeJsonString(s: string): string {
231
- try {
232
- return JSON.parse(`"${s}"`) as string
233
- } catch {
234
- return s.replace(/\\"/g, '"').replace(/\\\\/g, "\\")
235
- }
236
- }
237
-
238
- const PATCH_FILES_RE = /"files"\s*:\s*\[([^\]]*)\]/
239
-
240
- /** Paths from `part` type patch `{ files: [...] }`. No bodies. */
241
- export function filesFromPatchData(data: string, at: number, filter?: FileFilter): FileView[] {
242
- if (!new RegExp(`"type"\\s*:\\s*"${PART_TYPE_PATCH}"`).test(data)) return []
243
- const block = data.match(PATCH_FILES_RE)?.[1]
244
- if (!block) return []
245
- const out: FileView[] = []
246
- const re = /"((?:\\.|[^"\\])+)"/g
247
- let m: RegExpExecArray | null
248
- while ((m = re.exec(block)) !== null) {
249
- const f = asFile(unescapeJsonString(m[1]), 0, 0, at, filter)
250
- if (f) out.push(f)
251
- }
252
- return out
253
- }
254
-
255
- /** Pull filePath from edit/write parts without parsing I/O bodies. */
256
- export function filePathFromPartData(data: string): string | null {
257
- if (!TOOL_FILE_RE.test(data)) return null
258
- const m = data.match(FILEPATH_RE)
259
- if (!m?.[1]) return null
260
- return unescapeJsonString(m[1])
261
- }
262
-
263
- function touchFromPartData(data: string): FileTouch {
264
- const t = data.match(TOOL_FILE_RE)?.[1]?.toLowerCase() ?? ""
265
- return touchOfTool(t) ?? FILE_TOUCH_WRITE
266
- }
267
-
268
- /** Path + edit metadata +/- . Ignores patch/diff bodies. */
269
- export function fileHitFromPartData(data: string, at: number, filter?: FileFilter): FileView | null {
270
- const raw = filePathFromPartData(data)
271
- if (!raw) return null
272
- return asFile(
273
- raw,
274
- matchInt(data, "additions"),
275
- matchInt(data, "deletions"),
276
- at,
277
- filter,
278
- touchFromPartData(data),
279
- )
280
- }
281
-
282
- function firstString(...vals: unknown[]): string | null {
283
- for (const v of vals) {
284
- if (typeof v === "string" && v.trim()) return v.trim()
285
- }
286
- return null
287
- }
288
-
289
- function firstNum(...vals: unknown[]): number {
290
- for (const v of vals) {
291
- const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN
292
- if (Number.isFinite(n) && n > 0) return Math.round(n)
293
- }
294
- return 0
295
- }
296
-
297
- /** Patch `files` from json_extract — array or JSON text. No bodies. */
298
- export function filesFromPatchJson(raw: unknown, at: number, filter?: FileFilter): FileView[] {
299
- let list: unknown = raw
300
- if (typeof raw === "string") {
301
- const t = raw.trim()
302
- if (!t) return []
303
- try {
304
- list = JSON.parse(t)
305
- } catch {
306
- return []
307
- }
308
- }
309
- if (!Array.isArray(list)) return []
310
- const out: FileView[] = []
311
- for (const item of list) {
312
- const p =
313
- typeof item === "string"
314
- ? item
315
- : firstString(
316
- item && typeof item === "object" ? (item as Record<string, unknown>).filePath : null,
317
- item && typeof item === "object" ? (item as Record<string, unknown>).path : null,
318
- )
319
- if (!p) continue
320
- const f = asFile(p, 0, 0, at, filter)
321
- if (f) out.push(f)
322
- }
323
- return out
324
- }
325
-
326
- /** Path + +/- from extracted columns. Never needs the part blob. */
327
- export function fileHitFromExtracted(opts: {
328
- tool?: string | null
329
- filePath?: string | null
330
- filePathAlt?: string | null
331
- additions?: unknown
332
- deletions?: unknown
333
- at: number
334
- filter?: FileFilter
335
- }): FileView | null {
336
- const raw = firstString(opts.filePath, opts.filePathAlt)
337
- if (!raw) return null
338
- const tool = (opts.tool || "").toLowerCase()
339
- const touch = touchOfTool(tool)
340
- if (tool && !touch) return null
341
- return asFile(raw, firstNum(opts.additions), firstNum(opts.deletions), opts.at, opts.filter, touch ?? FILE_TOUCH_WRITE)
342
- }
343
-
344
- /** Git porcelain wins. `V` (viewed) only when git has no letter — git does not use V. */
345
- export function decorateFiles(
346
- files: FileView[],
347
- projectRoot?: string | null,
348
- opts?: { git?: boolean },
349
- ): FileView[] {
350
- if (!files.length) return files
351
- return profile("files.decorate", () => {
352
- if (opts?.git === false) {
353
- return files.map((f) => ({
354
- ...f,
355
- letter: f.letter ?? (f.touch === FILE_TOUCH_READ ? "V" : null),
356
- }))
357
- }
358
- const { root, marks } = readGitMarksFor(
359
- files.map((f) => f.id),
360
- projectRoot ?? null,
361
- )
362
- return files.map((f) => {
363
- const git = root
364
- ? marks.get(relToGitRoot(f.id, root)) ?? marks.get(f.id.replace(/\\/g, "/").toLowerCase()) ?? null
365
- : null
366
- return { ...f, letter: git ?? (f.touch === FILE_TOUCH_READ ? "V" : null) }
367
- })
368
- })
369
- }
370
-
371
- export function sumDiff(files: FileView[]): { additions: number; deletions: number } {
372
- let additions = 0
373
- let deletions = 0
374
- for (const f of files) {
375
- additions += f.additions
376
- deletions += f.deletions
377
- }
378
- return { additions, deletions }
379
- }
380
-
381
- export function mergeFiles(fromDb: FileView[] | null | undefined, live: Record<string, FileView>): FileView[] {
382
- const byId = new Map<string, FileView>()
383
- for (const f of fromDb ?? []) {
384
- byId.set(f.id, { ...f, touch: f.touch ?? FILE_TOUCH_WRITE, letter: f.letter ?? null })
385
- }
386
- for (const f of Object.values(live)) {
387
- const prev = byId.get(f.id)
388
- byId.set(f.id, {
389
- id: f.id,
390
- name: f.name,
391
- additions: Math.max(f.additions, prev?.additions ?? 0),
392
- deletions: Math.max(f.deletions, prev?.deletions ?? 0),
393
- at: Math.max(f.at, prev?.at ?? 0),
394
- touch: prev?.touch === FILE_TOUCH_WRITE || f.touch === FILE_TOUCH_WRITE ? FILE_TOUCH_WRITE : FILE_TOUCH_READ,
395
- letter: f.letter ?? prev?.letter ?? null,
396
- })
397
- }
398
- return [...byId.values()].sort((a, b) => b.at - a.at)
399
- }
1
+ /**
2
+ * Session file hits: basename + diff stats only.
3
+ * Never keeps path, before/after, or tool I/O.
4
+ */
5
+ import { readGitMarksFor, relToGitRoot, type GitLetter } from "../pware.oc.core/git/pware.oc.core.git.js"
6
+ import { profile } from "../pware.oc.core/pware.oc.core.debug.js"
7
+ import { ignoredByGitignore, ignoredByOesignore } from "../pware.oc.core/git/pware.oc.core.gitignore.js"
8
+ import { getOes } from "../pware.oc.core/pware.oc.core.oes.js"
9
+ import { ROW_LINE_FALLBACK } from "../pware.oc.core/pware.oc.core.layout.js"
10
+ import { strWidth, takeCols, takeLastCols } from "../pware.oc.core/pware.oc.core.width.js"
11
+ import { eventKind, eventType } from "../pware.oc.core/pware.oc.core.events.js"
12
+ import {
13
+ EVENT_KIND_DB_REFRESH,
14
+ EVENT_KIND_FILE,
15
+ EVENT_KIND_TOOL,
16
+ } from "../pware.oc.core/constants/pware.oc.core.constants.eventKind.js"
17
+ import { basenameOf, finiteNum } from "../pware.oc.core/pware.oc.core.paths.js"
18
+ import { sessionIdFromEvent } from "../pware.oc.core/pware.oc.core.pulse.js"
19
+ import { READ_TOOLS, WRITE_TOOLS } from "../pware.oc.core/constants/pware.oc.core.constants.toolName.js"
20
+ import { PART_TYPE_PATCH } from "../pware.oc.core/constants/pware.oc.core.constants.partType.js"
21
+ import {
22
+ FILE_TOUCH_READ,
23
+ FILE_TOUCH_WRITE,
24
+ type FileTouch,
25
+ } from "./constants/pware.oc.opencode.constants.fileTouch.js"
26
+
27
+ export { basenameOf, FILE_TOUCH_READ, FILE_TOUCH_WRITE, type FileTouch }
28
+
29
+ export const FILE_ROWS = 8
30
+
31
+ export type FileFilter = {
32
+ skipGitignore?: boolean
33
+ projectRoot?: string | null
34
+ }
35
+
36
+ export function fileFilter(projectRoot?: string | null): FileFilter {
37
+ const o = getOes(projectRoot)
38
+ return { skipGitignore: o.skipGitignore, projectRoot: projectRoot ?? null }
39
+ }
40
+
41
+ /** Git porcelain letters, plus `V` (viewed) which git does not use. */
42
+ export type FileLetter = GitLetter | "V"
43
+
44
+ export type FileView = {
45
+ /** Internal key (path). Never shown. */
46
+ id: string
47
+ /** Basename only. */
48
+ name: string
49
+ additions: number
50
+ deletions: number
51
+ at: number
52
+ touch: FileTouch
53
+ letter: FileLetter | null
54
+ }
55
+
56
+ /** Long names: start…end.ext — no directories. Column budget, not code units. */
57
+ export function shortFileName(raw: string, max = ROW_LINE_FALLBACK): string {
58
+ const base = basenameOf(raw)
59
+ if (strWidth(base) <= max) return base
60
+ const dot = base.lastIndexOf(".")
61
+ const ext = dot > 0 && dot >= base.length - 8 ? base.slice(dot) : ""
62
+ const stem = ext ? base.slice(0, -ext.length) : base
63
+ const ellip = "…"
64
+ const room = max - strWidth(ext) - strWidth(ellip)
65
+ if (room < 2) return `${takeCols(base, Math.max(1, max - 1))}${ellip}`
66
+ const tail = Math.min(strWidth(stem), Math.max(3, Math.ceil(room * 0.55)))
67
+ const head = Math.max(1, room - tail)
68
+ if (head + tail >= strWidth(stem)) return takeCols(base, max)
69
+ return `${takeCols(stem, head)}${ellip}${takeLastCols(stem, tail)}${ext}`
70
+ }
71
+
72
+ export function formatDiffStat(add: number, del: number): string {
73
+ const a = add > 0 ? `+${add}` : ""
74
+ const d = del > 0 ? `−${del}` : ""
75
+ return [a, d].filter(Boolean).join(" ")
76
+ }
77
+
78
+ function num(v: unknown): number {
79
+ const n = finiteNum(v)
80
+ return n > 0 ? Math.round(n) : 0
81
+ }
82
+
83
+ function matchInt(data: string, key: string): number {
84
+ const m = data.match(new RegExp(`"${key}"\\s*:\\s*(\\d+)`))
85
+ if (!m?.[1]) return 0
86
+ const n = Number(m[1])
87
+ return Number.isFinite(n) && n > 0 ? n : 0
88
+ }
89
+
90
+ function statsFromBag(o: Record<string, unknown> | null | undefined): { add: number; del: number } {
91
+ if (!o) return { add: 0, del: 0 }
92
+ const fd = o.filediff && typeof o.filediff === "object" ? (o.filediff as Record<string, unknown>) : null
93
+ return {
94
+ add: num(fd?.additions) || num(o.additions),
95
+ del: num(fd?.deletions) || num(o.deletions),
96
+ }
97
+ }
98
+
99
+ function hidden(posixPath: string, filter?: FileFilter): boolean {
100
+ if (ignoredByOesignore(posixPath, filter?.projectRoot)) return true
101
+ return Boolean(filter?.skipGitignore && ignoredByGitignore(posixPath, filter.projectRoot))
102
+ }
103
+
104
+ function asFile(
105
+ pathOrName: string,
106
+ add: number,
107
+ del: number,
108
+ at: number,
109
+ filter?: FileFilter,
110
+ touch: FileTouch = FILE_TOUCH_WRITE,
111
+ ): FileView | null {
112
+ const raw = pathOrName.replace(/\\/g, "/").trim()
113
+ if (!raw || raw === "." || raw === "..") return null
114
+ if (hidden(raw, filter)) return null
115
+ const name = basenameOf(raw)
116
+ return { id: raw, name, additions: add, deletions: del, at, touch, letter: null }
117
+ }
118
+
119
+ function eventBag(evt: unknown): Record<string, unknown> | null {
120
+ if (!evt || typeof evt !== "object") return null
121
+ const o = evt as Record<string, unknown>
122
+ if (o.properties && typeof o.properties === "object") {
123
+ return o.properties as Record<string, unknown>
124
+ }
125
+ return o
126
+ }
127
+
128
+ function pickPath(o: Record<string, unknown>): string | null {
129
+ for (const key of ["file", "filePath", "filepath", "path"]) {
130
+ const v = o[key]
131
+ if (typeof v === "string" && v.trim()) return v.trim()
132
+ }
133
+ return null
134
+ }
135
+
136
+ function fileFromDiffRow(row: unknown, at: number, filter?: FileFilter): FileView | null {
137
+ if (!row || typeof row !== "object") return null
138
+ const o = row as Record<string, unknown>
139
+ const file = pickPath(o)
140
+ if (!file) return null
141
+ return asFile(file, num(o.additions) || num(o.added), num(o.deletions) || num(o.removed), at, filter, FILE_TOUCH_WRITE)
142
+ }
143
+
144
+ /** Paths + +/- only. Drops before/after and any other fields. */
145
+ export function filesFromEvent(evt: unknown, sessionId: string, filter?: FileFilter): FileView[] {
146
+ const type = eventType(evt)
147
+ const bag = eventBag(evt)
148
+ if (!bag) return []
149
+ const sid = sessionIdFromEvent(evt)
150
+ if (sid && sid !== sessionId) return []
151
+ const kind = eventKind(type)
152
+ if (type && kind !== EVENT_KIND_FILE && kind !== EVENT_KIND_TOOL && kind !== EVENT_KIND_DB_REFRESH) return []
153
+ const at = Date.now()
154
+ const out: FileView[] = []
155
+
156
+ if (type.includes("session.diff") || Array.isArray(bag.diff)) {
157
+ const rows = Array.isArray(bag.diff) ? bag.diff : []
158
+ for (const row of rows) {
159
+ const f = fileFromDiffRow(row, at, filter)
160
+ if (f) out.push(f)
161
+ }
162
+ return out
163
+ }
164
+
165
+ if (type.includes("file.edited")) {
166
+ const p = pickPath(bag)
167
+ if (p) {
168
+ const f = asFile(p, 0, 0, at, filter)
169
+ if (f) out.push(f)
170
+ }
171
+ return out
172
+ }
173
+
174
+ if (type.includes("session.updated") || type.includes("session.created")) {
175
+ const info =
176
+ bag.info && typeof bag.info === "object" ? (bag.info as Record<string, unknown>) : bag
177
+ const summary =
178
+ info.summary && typeof info.summary === "object"
179
+ ? (info.summary as Record<string, unknown>)
180
+ : null
181
+ const diffs = summary && Array.isArray(summary.diffs) ? summary.diffs : []
182
+ for (const row of diffs) {
183
+ const f = fileFromDiffRow(row, at, filter)
184
+ if (f) out.push(f)
185
+ }
186
+ return out
187
+ }
188
+
189
+ if (type.includes("tool.called") || type.includes("tool.success") || type.includes("part.updated")) {
190
+ const part =
191
+ bag.part && typeof bag.part === "object" ? (bag.part as Record<string, unknown>) : bag
192
+ const tool = String(part.tool ?? bag.tool ?? "").toLowerCase()
193
+ const touch = touchOfTool(tool)
194
+ if (!touch && !type.includes("file.edited")) return []
195
+ const input =
196
+ part.input && typeof part.input === "object"
197
+ ? (part.input as Record<string, unknown>)
198
+ : bag.input && typeof bag.input === "object"
199
+ ? (bag.input as Record<string, unknown>)
200
+ : null
201
+ const p = input ? pickPath(input) : pickPath(part) ?? pickPath(bag)
202
+ if (!p) return []
203
+ const meta =
204
+ part.metadata && typeof part.metadata === "object"
205
+ ? (part.metadata as Record<string, unknown>)
206
+ : null
207
+ const { add, del } = statsFromBag(meta ?? part)
208
+ const f = asFile(p, add, del, at, filter, touch ?? FILE_TOUCH_WRITE)
209
+ return f ? [f] : []
210
+ }
211
+
212
+ return out
213
+ }
214
+
215
+ const WRITE_TOOL_SET = new Set<string>(WRITE_TOOLS)
216
+ const READ_TOOL_SET = new Set<string>(READ_TOOLS)
217
+
218
+ function touchOfTool(tool: string): FileTouch | null {
219
+ if (READ_TOOL_SET.has(tool)) return FILE_TOUCH_READ
220
+ if (WRITE_TOOL_SET.has(tool)) return FILE_TOUCH_WRITE
221
+ return null
222
+ }
223
+
224
+ const FILEPATH_RE = /"(?:filePath|filepath)"\s*:\s*"((?:\\.|[^"\\])+)"/
225
+ const TOOL_FILE_RE = new RegExp(
226
+ `"tool"\\s*:\\s*"(${[...WRITE_TOOLS, ...READ_TOOLS].join("|")})"`,
227
+ "i",
228
+ )
229
+
230
+ function unescapeJsonString(s: string): string {
231
+ try {
232
+ return JSON.parse(`"${s}"`) as string
233
+ } catch {
234
+ return s.replace(/\\"/g, '"').replace(/\\\\/g, "\\")
235
+ }
236
+ }
237
+
238
+ const PATCH_FILES_RE = /"files"\s*:\s*\[([^\]]*)\]/
239
+
240
+ /** Paths from `part` type patch `{ files: [...] }`. No bodies. */
241
+ export function filesFromPatchData(data: string, at: number, filter?: FileFilter): FileView[] {
242
+ if (!new RegExp(`"type"\\s*:\\s*"${PART_TYPE_PATCH}"`).test(data)) return []
243
+ const block = data.match(PATCH_FILES_RE)?.[1]
244
+ if (!block) return []
245
+ const out: FileView[] = []
246
+ const re = /"((?:\\.|[^"\\])+)"/g
247
+ let m: RegExpExecArray | null
248
+ while ((m = re.exec(block)) !== null) {
249
+ const f = asFile(unescapeJsonString(m[1]), 0, 0, at, filter)
250
+ if (f) out.push(f)
251
+ }
252
+ return out
253
+ }
254
+
255
+ /** Pull filePath from edit/write parts without parsing I/O bodies. */
256
+ export function filePathFromPartData(data: string): string | null {
257
+ if (!TOOL_FILE_RE.test(data)) return null
258
+ const m = data.match(FILEPATH_RE)
259
+ if (!m?.[1]) return null
260
+ return unescapeJsonString(m[1])
261
+ }
262
+
263
+ function touchFromPartData(data: string): FileTouch {
264
+ const t = data.match(TOOL_FILE_RE)?.[1]?.toLowerCase() ?? ""
265
+ return touchOfTool(t) ?? FILE_TOUCH_WRITE
266
+ }
267
+
268
+ /** Path + edit metadata +/- . Ignores patch/diff bodies. */
269
+ export function fileHitFromPartData(data: string, at: number, filter?: FileFilter): FileView | null {
270
+ const raw = filePathFromPartData(data)
271
+ if (!raw) return null
272
+ return asFile(
273
+ raw,
274
+ matchInt(data, "additions"),
275
+ matchInt(data, "deletions"),
276
+ at,
277
+ filter,
278
+ touchFromPartData(data),
279
+ )
280
+ }
281
+
282
+ function firstString(...vals: unknown[]): string | null {
283
+ for (const v of vals) {
284
+ if (typeof v === "string" && v.trim()) return v.trim()
285
+ }
286
+ return null
287
+ }
288
+
289
+ function firstNum(...vals: unknown[]): number {
290
+ for (const v of vals) {
291
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN
292
+ if (Number.isFinite(n) && n > 0) return Math.round(n)
293
+ }
294
+ return 0
295
+ }
296
+
297
+ /** Patch `files` from json_extract — array or JSON text. No bodies. */
298
+ export function filesFromPatchJson(raw: unknown, at: number, filter?: FileFilter): FileView[] {
299
+ let list: unknown = raw
300
+ if (typeof raw === "string") {
301
+ const t = raw.trim()
302
+ if (!t) return []
303
+ try {
304
+ list = JSON.parse(t)
305
+ } catch {
306
+ return []
307
+ }
308
+ }
309
+ if (!Array.isArray(list)) return []
310
+ const out: FileView[] = []
311
+ for (const item of list) {
312
+ const p =
313
+ typeof item === "string"
314
+ ? item
315
+ : firstString(
316
+ item && typeof item === "object" ? (item as Record<string, unknown>).filePath : null,
317
+ item && typeof item === "object" ? (item as Record<string, unknown>).path : null,
318
+ )
319
+ if (!p) continue
320
+ const f = asFile(p, 0, 0, at, filter)
321
+ if (f) out.push(f)
322
+ }
323
+ return out
324
+ }
325
+
326
+ /** Path + +/- from extracted columns. Never needs the part blob. */
327
+ export function fileHitFromExtracted(opts: {
328
+ tool?: string | null
329
+ filePath?: string | null
330
+ filePathAlt?: string | null
331
+ additions?: unknown
332
+ deletions?: unknown
333
+ at: number
334
+ filter?: FileFilter
335
+ }): FileView | null {
336
+ const raw = firstString(opts.filePath, opts.filePathAlt)
337
+ if (!raw) return null
338
+ const tool = (opts.tool || "").toLowerCase()
339
+ const touch = touchOfTool(tool)
340
+ if (tool && !touch) return null
341
+ return asFile(raw, firstNum(opts.additions), firstNum(opts.deletions), opts.at, opts.filter, touch ?? FILE_TOUCH_WRITE)
342
+ }
343
+
344
+ /** Git porcelain wins. `V` (viewed) only when git has no letter — git does not use V. */
345
+ export function decorateFiles(
346
+ files: FileView[],
347
+ projectRoot?: string | null,
348
+ opts?: { git?: boolean },
349
+ ): FileView[] {
350
+ if (!files.length) return files
351
+ return profile("files.decorate", () => {
352
+ if (opts?.git === false) {
353
+ return files.map((f) => ({
354
+ ...f,
355
+ letter: f.letter ?? (f.touch === FILE_TOUCH_READ ? "V" : null),
356
+ }))
357
+ }
358
+ const { root, marks } = readGitMarksFor(
359
+ files.map((f) => f.id),
360
+ projectRoot ?? null,
361
+ )
362
+ return files.map((f) => {
363
+ const git = root
364
+ ? marks.get(relToGitRoot(f.id, root)) ?? marks.get(f.id.replace(/\\/g, "/").toLowerCase()) ?? null
365
+ : null
366
+ return { ...f, letter: git ?? (f.touch === FILE_TOUCH_READ ? "V" : null) }
367
+ })
368
+ })
369
+ }
370
+
371
+ export function sumDiff(files: FileView[]): { additions: number; deletions: number } {
372
+ let additions = 0
373
+ let deletions = 0
374
+ for (const f of files) {
375
+ additions += f.additions
376
+ deletions += f.deletions
377
+ }
378
+ return { additions, deletions }
379
+ }
380
+
381
+ export function mergeFiles(fromDb: FileView[] | null | undefined, live: Record<string, FileView>): FileView[] {
382
+ const byId = new Map<string, FileView>()
383
+ for (const f of fromDb ?? []) {
384
+ byId.set(f.id, { ...f, touch: f.touch ?? FILE_TOUCH_WRITE, letter: f.letter ?? null })
385
+ }
386
+ for (const f of Object.values(live)) {
387
+ const prev = byId.get(f.id)
388
+ byId.set(f.id, {
389
+ id: f.id,
390
+ name: f.name,
391
+ additions: Math.max(f.additions, prev?.additions ?? 0),
392
+ deletions: Math.max(f.deletions, prev?.deletions ?? 0),
393
+ at: Math.max(f.at, prev?.at ?? 0),
394
+ touch: prev?.touch === FILE_TOUCH_WRITE || f.touch === FILE_TOUCH_WRITE ? FILE_TOUCH_WRITE : FILE_TOUCH_READ,
395
+ letter: f.letter ?? prev?.letter ?? null,
396
+ })
397
+ }
398
+ return [...byId.values()].sort((a, b) => b.at - a.at)
399
+ }