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.
- package/.oesignore +17 -17
- package/CHANGELOG.md +96 -95
- package/LICENSE +21 -21
- package/README.md +150 -235
- package/oes.json +13 -11
- package/package.json +100 -100
- package/scripts/install-git-hooks.mjs +29 -29
- package/src/pware.oc.core/constants/index.ts +18 -18
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
- package/src/pware.oc.core/git/index.ts +7 -7
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
- package/src/pware.oc.core/index.ts +19 -18
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
- package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
- package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
- package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
- package/src/pware.oc.omo/constants/index.ts +15 -15
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
- package/src/pware.oc.omo/index.ts +7 -7
- package/src/pware.oc.omo/resolver/index.ts +19 -19
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
- package/src/pware.oc.opencode/constants/index.ts +10 -10
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
- package/src/pware.oc.opencode/index.ts +8 -8
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
- package/src/pware.oc.opencode/resolver/index.ts +223 -170
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
- package/src/pware.oc.perf/index.ts +14 -14
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
- package/src/pware.oc.runtime/index.ts +13 -13
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
- package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
- package/src/pware.oc.runtime/resolver/index.ts +268 -224
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
- package/src/pware.oc.ui/index.ts +12 -12
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
- 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
|
+
}
|