opencode-extended-sidebar 0.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oesignore +17 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/oes.json +11 -0
- package/package.json +100 -0
- package/scripts/install-git-hooks.mjs +29 -0
- package/src/pware.oc.core/constants/index.ts +18 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
- package/src/pware.oc.core/git/index.ts +7 -0
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
- package/src/pware.oc.core/index.ts +18 -0
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
- package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
- package/src/pware.oc.omo/constants/index.ts +15 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
- package/src/pware.oc.omo/index.ts +7 -0
- package/src/pware.oc.omo/resolver/index.ts +19 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
- package/src/pware.oc.opencode/constants/index.ts +10 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
- package/src/pware.oc.opencode/index.ts +8 -0
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
- package/src/pware.oc.opencode/resolver/index.ts +170 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
- package/src/pware.oc.perf/index.ts +14 -0
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
- package/src/pware.oc.runtime/index.ts +13 -0
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
- package/src/pware.oc.runtime/resolver/index.ts +224 -0
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
- package/src/pware.oc.ui/index.ts +12 -0
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
- package/src/pware.oc.ui.tsx +98 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deltaKindFromEvent,
|
|
3
|
+
deltaTextFromEvent,
|
|
4
|
+
estimateTokens,
|
|
5
|
+
flowFromEvent,
|
|
6
|
+
sessionBusyFromEvent,
|
|
7
|
+
sessionIdFromEvent,
|
|
8
|
+
toolHitFromEvent,
|
|
9
|
+
type FlowHint,
|
|
10
|
+
type ToolHit,
|
|
11
|
+
} from "../pware.oc.core/pware.oc.core.pulse.js"
|
|
12
|
+
import { eventType } from "../pware.oc.core/pware.oc.core.events.js"
|
|
13
|
+
import {
|
|
14
|
+
fileFilter,
|
|
15
|
+
filesFromEvent,
|
|
16
|
+
type FileView,
|
|
17
|
+
} from "./pware.oc.opencode.files.js"
|
|
18
|
+
import {
|
|
19
|
+
EV_OC_FILES_TOUCHED,
|
|
20
|
+
EV_OC_FLOW,
|
|
21
|
+
EV_OC_SESSION_ACTIVITY,
|
|
22
|
+
EV_OC_TOKENS_DELTA,
|
|
23
|
+
EV_OC_TOOL_HIT,
|
|
24
|
+
} from "./constants/pware.oc.opencode.constants.eventName.js"
|
|
25
|
+
|
|
26
|
+
export type OcSessionActivityEvent = {
|
|
27
|
+
type: typeof EV_OC_SESSION_ACTIVITY
|
|
28
|
+
ts: number
|
|
29
|
+
data: {
|
|
30
|
+
sessionId: string | null
|
|
31
|
+
busy: boolean
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type OcFlowEvent = {
|
|
36
|
+
type: typeof EV_OC_FLOW
|
|
37
|
+
ts: number
|
|
38
|
+
data: {
|
|
39
|
+
sessionId: string
|
|
40
|
+
dir: FlowHint
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type OcToolHitEvent = {
|
|
45
|
+
type: typeof EV_OC_TOOL_HIT
|
|
46
|
+
ts: number
|
|
47
|
+
data: {
|
|
48
|
+
hit: ToolHit
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type OcFilesTouchedEvent = {
|
|
53
|
+
type: typeof EV_OC_FILES_TOUCHED
|
|
54
|
+
ts: number
|
|
55
|
+
data: {
|
|
56
|
+
sessionId: string
|
|
57
|
+
files: FileView[]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type OcTokensDeltaEvent = {
|
|
62
|
+
type: typeof EV_OC_TOKENS_DELTA
|
|
63
|
+
ts: number
|
|
64
|
+
data: {
|
|
65
|
+
sessionId: string
|
|
66
|
+
/** Which stream produced the delta — `out` (text) or `reasoning` (thinking). */
|
|
67
|
+
kind: "out" | "reasoning"
|
|
68
|
+
tokens: number
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type OcEvent =
|
|
73
|
+
| OcSessionActivityEvent
|
|
74
|
+
| OcFlowEvent
|
|
75
|
+
| OcToolHitEvent
|
|
76
|
+
| OcFilesTouchedEvent
|
|
77
|
+
| OcTokensDeltaEvent
|
|
78
|
+
|
|
79
|
+
export function hostEventToOcEvents(
|
|
80
|
+
evt: unknown,
|
|
81
|
+
opts: {
|
|
82
|
+
sessionId: string
|
|
83
|
+
projectRoot: string | null
|
|
84
|
+
},
|
|
85
|
+
): OcEvent[] {
|
|
86
|
+
const ts = Date.now()
|
|
87
|
+
const out: OcEvent[] = []
|
|
88
|
+
const eventSessionId = sessionIdFromEvent(evt) ?? opts.sessionId
|
|
89
|
+
|
|
90
|
+
const busy = sessionBusyFromEvent(evt)
|
|
91
|
+
if (busy.busy != null) {
|
|
92
|
+
out.push({
|
|
93
|
+
type: EV_OC_SESSION_ACTIVITY,
|
|
94
|
+
ts,
|
|
95
|
+
data: {
|
|
96
|
+
sessionId: busy.id ?? eventSessionId,
|
|
97
|
+
busy: busy.busy,
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const flow = flowFromEvent(evt)
|
|
103
|
+
const flowSessionId = flow.id ?? eventSessionId
|
|
104
|
+
if (flow.dir && flowSessionId) {
|
|
105
|
+
out.push({
|
|
106
|
+
type: EV_OC_FLOW,
|
|
107
|
+
ts,
|
|
108
|
+
data: {
|
|
109
|
+
sessionId: flowSessionId,
|
|
110
|
+
dir: flow.dir,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const hit = toolHitFromEvent(evt)
|
|
116
|
+
if (hit && (!hit.sessionId || hit.sessionId === opts.sessionId)) {
|
|
117
|
+
out.push({
|
|
118
|
+
type: EV_OC_TOOL_HIT,
|
|
119
|
+
ts,
|
|
120
|
+
data: { hit },
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const files = filesFromEvent(evt, opts.sessionId, fileFilter(opts.projectRoot))
|
|
125
|
+
if (files.length > 0) {
|
|
126
|
+
out.push({
|
|
127
|
+
type: EV_OC_FILES_TOUCHED,
|
|
128
|
+
ts,
|
|
129
|
+
data: {
|
|
130
|
+
sessionId: eventSessionId,
|
|
131
|
+
files,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const delta = deltaTextFromEvent(evt)
|
|
137
|
+
if (delta) {
|
|
138
|
+
const tokens = estimateTokens(delta)
|
|
139
|
+
if (tokens > 0) {
|
|
140
|
+
out.push({
|
|
141
|
+
type: EV_OC_TOKENS_DELTA,
|
|
142
|
+
ts,
|
|
143
|
+
data: {
|
|
144
|
+
sessionId: eventSessionId,
|
|
145
|
+
kind: deltaKindFromEvent(evt),
|
|
146
|
+
tokens,
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const QUESTION_EVENT_HINTS = ["question.asked", "question.replied", "question.rejected"] as const
|
|
156
|
+
|
|
157
|
+
function isQuestionToolBag(bag: unknown): boolean {
|
|
158
|
+
if (!bag || typeof bag !== "object") return false
|
|
159
|
+
const b = bag as Record<string, unknown>
|
|
160
|
+
const tool = b.tool
|
|
161
|
+
if (typeof tool !== "string" || tool.toLowerCase() !== "question") return false
|
|
162
|
+
const type = b.type
|
|
163
|
+
return type == null || String(type).toLowerCase() === "tool"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Session id when a host event signals a `question` tool part changed state, else `null`. */
|
|
167
|
+
export function questionSessionFromEvent(evt: unknown): string | null {
|
|
168
|
+
const type = eventType(evt)
|
|
169
|
+
if (QUESTION_EVENT_HINTS.some((hint) => type.includes(hint))) {
|
|
170
|
+
return sessionIdFromEvent(evt)
|
|
171
|
+
}
|
|
172
|
+
if (!evt || typeof evt !== "object") return null
|
|
173
|
+
const o = evt as Record<string, unknown>
|
|
174
|
+
const props =
|
|
175
|
+
o.properties && typeof o.properties === "object"
|
|
176
|
+
? (o.properties as Record<string, unknown>)
|
|
177
|
+
: null
|
|
178
|
+
for (const bag of [o.part, props?.part, props]) {
|
|
179
|
+
if (isQuestionToolBag(bag)) return sessionIdFromEvent(evt)
|
|
180
|
+
}
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
@@ -0,0 +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
|
+
}
|