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,280 +1,280 @@
1
- /**
2
- * Optional file-based debug + profile loggers, plus a tiny on-screen event
3
- * ring for the sidebar's read-only debug console.
4
- *
5
- * Debug: OES_DEBUG_OPENCODE=1 → writes to <plugin>/logs/
6
- * Profile: OES_DEBUG_PROFILE=1 → writes to <plugin>/logs/
7
- * Either OES_DEBUG_*=/some/dir → writes to that directory
8
- *
9
- * Each session appends a single oes-debug-<date>.log / oes-profile-<date>.log
10
- * file. Lines are newline-delimited JSON:
11
- * debug → { ts, tag, msg, data? }
12
- * profile → { ts, tag, ms, data? }
13
- * All writes are silent on error — the plugin never crashes because of logging.
14
- *
15
- * The screen ring (`pushScreenLine`) is a module-level 200-line buffer of short
16
- * labels (e.g. `db open`) that the sidebar console subscribes to; it is only
17
- * fed while a logger is active, so production pays nothing.
18
- */
19
- import fs from "node:fs"
20
- import path from "node:path"
21
- import { pluginRoot } from "./pware.oc.core.paths.js"
22
-
23
- const DEBUG_ENV_KEY = "OES_DEBUG_OPENCODE"
24
- const PROFILE_ENV_KEY = "OES_DEBUG_PROFILE"
25
-
26
- function pluginLogsDir(): string {
27
- return path.join(pluginRoot(), "logs")
28
- }
29
-
30
- function resolveLogDir(
31
- envKey: string,
32
- env: Record<string, string | undefined>,
33
- defaultDir?: string,
34
- ): string | null {
35
- const val = env[envKey]
36
- if (!val) return null
37
- const trimmed = val.trim()
38
- if (!trimmed) return null
39
- const FALSY = new Set(["0", "false", "no", "off"])
40
- if (FALSY.has(trimmed.toLowerCase())) return null
41
- const TRUTHY = new Set(["1", "true", "yes", "on"])
42
- if (TRUTHY.has(trimmed.toLowerCase())) return defaultDir ?? pluginLogsDir()
43
- return trimmed
44
- }
45
-
46
- /** Returns the debug log directory, or null when debug is off. Exported for tests. */
47
- export function debugLogDir(
48
- env: Record<string, string | undefined> = process.env,
49
- defaultDir?: string,
50
- ): string | null {
51
- return resolveLogDir(DEBUG_ENV_KEY, env, defaultDir)
52
- }
53
-
54
- /** Returns the profile log directory, or null when profiling is off. Exported for tests. */
55
- export function profileLogDir(
56
- env: Record<string, string | undefined> = process.env,
57
- defaultDir?: string,
58
- ): string | null {
59
- return resolveLogDir(PROFILE_ENV_KEY, env, defaultDir)
60
- }
61
-
62
- /** True when the debug logger is active — env on AND its log dir resolved/writable. */
63
- export function debugActive(): boolean {
64
- return resolvedDir() !== null
65
- }
66
-
67
- export function debugActiveDir(): string | null {
68
- return resolvedDir()
69
- }
70
-
71
- /** True when the profile logger is active — env on AND its log dir resolved/writable. */
72
- export function profileActive(): boolean {
73
- return profileResolvedDir() !== null
74
- }
75
-
76
- export function profileActiveDir(): string | null {
77
- return profileResolvedDir()
78
- }
79
-
80
- function datedLogFile(prefix: string): string {
81
- const d = new Date()
82
- const pad = (n: number) => String(n).padStart(2, "0")
83
- return `oes-${prefix}-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.log`
84
- }
85
-
86
- let _dir: string | null | undefined = undefined // undefined = not resolved yet
87
- let _pdir: string | null | undefined = undefined
88
-
89
- function resolvedDir(): string | null {
90
- if (_dir !== undefined) return _dir
91
- _dir = debugLogDir()
92
- if (_dir) {
93
- try {
94
- fs.mkdirSync(_dir, { recursive: true })
95
- } catch {
96
- _dir = null
97
- }
98
- }
99
- return _dir
100
- }
101
-
102
- function profileResolvedDir(): string | null {
103
- if (_pdir !== undefined) return _pdir
104
- _pdir = profileLogDir()
105
- if (_pdir) {
106
- try {
107
- fs.mkdirSync(_pdir, { recursive: true })
108
- } catch {
109
- _pdir = null
110
- }
111
- }
112
- return _pdir
113
- }
114
-
115
- /** Reset cached state (test helper). */
116
- export function resetDebug(): void {
117
- _dir = undefined
118
- _pdir = undefined
119
- profileStats = {}
120
- screenLines = []
121
- }
122
-
123
- /** One on-screen console line: a timestamp plus a short label (e.g. `db open`). */
124
- export type ScreenLine = { at: number; text: string }
125
-
126
- /** Ring cap — the console shows a 5-line window over these, scrolled. */
127
- const SCREEN_MAX = 200
128
-
129
- let screenLines: ScreenLine[] = []
130
- const screenListeners = new Set<() => void>()
131
-
132
- /**
133
- * Append one short label (e.g. `db open`) to the sidebar console's screen ring
134
- * and notify subscribers. No-op unless a debug or profile logger is active, so
135
- * a normal session pays nothing for it.
136
- */
137
- export function pushScreenLine(text: string): void {
138
- if (!debugActive() && !profileActive()) return
139
- screenLines.push({ at: Date.now(), text })
140
- if (screenLines.length > SCREEN_MAX) screenLines.splice(0, screenLines.length - SCREEN_MAX)
141
- for (const listener of screenListeners) {
142
- try {
143
- listener()
144
- } catch {
145
- // a subscriber must never break the ring feed
146
- }
147
- }
148
- }
149
-
150
- /** Newest-first copy of the screen ring (empty when nothing was pushed). */
151
- export function readScreenLines(): ScreenLine[] {
152
- return screenLines.slice().reverse()
153
- }
154
-
155
- /** Subscribe to screen-ring pushes; returns an unsubscribe fn. */
156
- export function subscribeScreenLines(listener: () => void): () => void {
157
- screenListeners.add(listener)
158
- return () => {
159
- screenListeners.delete(listener)
160
- }
161
- }
162
-
163
- function appendLine(dir: string, file: string, line: Record<string, unknown>): void {
164
- try {
165
- const entry = { ts: new Date().toISOString(), ...line }
166
- fs.appendFileSync(path.join(dir, file), JSON.stringify(entry) + "\n", "utf8")
167
- } catch {
168
- // silent — logging must never crash the plugin
169
- }
170
- }
171
-
172
- /**
173
- * Append one JSON-line entry to the debug log.
174
- * No-op when OES_DEBUG_OPENCODE is not set or falsy.
175
- */
176
- export function dbg(tag: string, msg: string, data?: unknown): void {
177
- const dir = resolvedDir()
178
- if (!dir) return
179
- appendLine(dir, datedLogFile("debug"), {
180
- tag,
181
- msg,
182
- ...(data !== undefined ? { data } : {}),
183
- })
184
- }
185
-
186
- export type ProfileStats = { n: number; sum: number; max: number }
187
-
188
- /** In-memory per-tag totals, updated only while OES_DEBUG_PROFILE is on. */
189
- let profileStats: Record<string, ProfileStats> = {}
190
-
191
- /** Lazily-computed or plain profile data — a factory keeps the call site free when off. */
192
- type ProfileData = Record<string, unknown> | (() => Record<string, unknown>)
193
-
194
- function resolveData(data: ProfileData | undefined): Record<string, unknown> | undefined {
195
- const d = typeof data === "function" ? data() : data
196
- return d && Object.keys(d).length > 0 ? d : undefined
197
- }
198
-
199
- function recordProfile(tag: string, ms: number, data?: ProfileData): void {
200
- const dir = profileResolvedDir()
201
- if (!dir) return
202
- const s = (profileStats[tag] ??= { n: 0, sum: 0, max: 0 })
203
- s.n += 1
204
- s.sum += ms
205
- if (ms > s.max) s.max = ms
206
- const d = resolveData(data)
207
- appendLine(dir, datedLogFile("profile"), {
208
- tag,
209
- ms,
210
- ...(d !== undefined ? { data: d } : {}),
211
- })
212
- }
213
-
214
- /**
215
- * Measure fn with performance.now() and append one { tag, ms, data? } line to
216
- * the profile log. No-op when OES_DEBUG_PROFILE is not set or falsy. Returns
217
- * fn's result; throws propagate.
218
- */
219
- export function profile<T>(tag: string, fn: () => T, data?: ProfileData): T {
220
- if (!profileResolvedDir()) return fn()
221
- const t0 = performance.now()
222
- try {
223
- return fn()
224
- } finally {
225
- recordProfile(tag, performance.now() - t0, data)
226
- }
227
- }
228
-
229
- /**
230
- * Same as profile(), but for an async fn — measures until the returned promise
231
- * settles. The fn is invoked immediately (the promise is what is timed).
232
- */
233
- export function profileAsync<T>(
234
- tag: string,
235
- fn: () => Promise<T>,
236
- data?: ProfileData,
237
- ): Promise<T> {
238
- if (!profileResolvedDir()) return fn()
239
- const t0 = performance.now()
240
- return fn().then(
241
- (value) => {
242
- recordProfile(tag, performance.now() - t0, data)
243
- return value
244
- },
245
- (err) => {
246
- recordProfile(tag, performance.now() - t0, data)
247
- throw err
248
- },
249
- )
250
- }
251
-
252
- /** Shallow copy of the per-tag profile totals. Empty when profiling is off. */
253
- export function readProfileStats(): Record<string, ProfileStats> {
254
- const out: Record<string, ProfileStats> = {}
255
- for (const [tag, s] of Object.entries(profileStats)) out[tag] = { ...s }
256
- return out
257
- }
258
-
259
- /**
260
- * Append one `summary` line with the per-tag totals (n, total ms, avg, max) to
261
- * the profile log. No-op when profiling is off or nothing was measured.
262
- */
263
- export function writeProfileSummary(): void {
264
- const dir = profileResolvedDir()
265
- if (!dir) return
266
- const stats = readProfileStats()
267
- const tags = Object.keys(stats)
268
- if (tags.length === 0) return
269
- const summary: Record<string, { n: number; total: number; avg: number; max: number }> = {}
270
- for (const tag of tags) {
271
- const s = stats[tag]
272
- summary[tag] = {
273
- n: s.n,
274
- total: Math.round(s.sum * 10) / 10,
275
- avg: Math.round((s.sum / s.n) * 10) / 10,
276
- max: Math.round(s.max * 10) / 10,
277
- }
278
- }
279
- appendLine(dir, datedLogFile("profile"), { tag: "summary", data: summary })
280
- }
1
+ /**
2
+ * Optional file-based debug + profile loggers, plus a tiny on-screen event
3
+ * ring for the sidebar's read-only debug console.
4
+ *
5
+ * Debug: OES_DEBUG_OPENCODE=1 → writes to <plugin>/logs/
6
+ * Profile: OES_DEBUG_PROFILE=1 → writes to <plugin>/logs/
7
+ * Either OES_DEBUG_*=/some/dir → writes to that directory
8
+ *
9
+ * Each session appends a single oes-debug-<date>.log / oes-profile-<date>.log
10
+ * file. Lines are newline-delimited JSON:
11
+ * debug → { ts, tag, msg, data? }
12
+ * profile → { ts, tag, ms, data? }
13
+ * All writes are silent on error — the plugin never crashes because of logging.
14
+ *
15
+ * The screen ring (`pushScreenLine`) is a module-level 200-line buffer of short
16
+ * labels (e.g. `db open`) that the sidebar console subscribes to; it is only
17
+ * fed while a logger is active, so production pays nothing.
18
+ */
19
+ import fs from "node:fs"
20
+ import path from "node:path"
21
+ import { pluginRoot } from "./pware.oc.core.paths.js"
22
+
23
+ const DEBUG_ENV_KEY = "OES_DEBUG_OPENCODE"
24
+ const PROFILE_ENV_KEY = "OES_DEBUG_PROFILE"
25
+
26
+ function pluginLogsDir(): string {
27
+ return path.join(pluginRoot(), "logs")
28
+ }
29
+
30
+ function resolveLogDir(
31
+ envKey: string,
32
+ env: Record<string, string | undefined>,
33
+ defaultDir?: string,
34
+ ): string | null {
35
+ const val = env[envKey]
36
+ if (!val) return null
37
+ const trimmed = val.trim()
38
+ if (!trimmed) return null
39
+ const FALSY = new Set(["0", "false", "no", "off"])
40
+ if (FALSY.has(trimmed.toLowerCase())) return null
41
+ const TRUTHY = new Set(["1", "true", "yes", "on"])
42
+ if (TRUTHY.has(trimmed.toLowerCase())) return defaultDir ?? pluginLogsDir()
43
+ return trimmed
44
+ }
45
+
46
+ /** Returns the debug log directory, or null when debug is off. Exported for tests. */
47
+ export function debugLogDir(
48
+ env: Record<string, string | undefined> = process.env,
49
+ defaultDir?: string,
50
+ ): string | null {
51
+ return resolveLogDir(DEBUG_ENV_KEY, env, defaultDir)
52
+ }
53
+
54
+ /** Returns the profile log directory, or null when profiling is off. Exported for tests. */
55
+ export function profileLogDir(
56
+ env: Record<string, string | undefined> = process.env,
57
+ defaultDir?: string,
58
+ ): string | null {
59
+ return resolveLogDir(PROFILE_ENV_KEY, env, defaultDir)
60
+ }
61
+
62
+ /** True when the debug logger is active — env on AND its log dir resolved/writable. */
63
+ export function debugActive(): boolean {
64
+ return resolvedDir() !== null
65
+ }
66
+
67
+ export function debugActiveDir(): string | null {
68
+ return resolvedDir()
69
+ }
70
+
71
+ /** True when the profile logger is active — env on AND its log dir resolved/writable. */
72
+ export function profileActive(): boolean {
73
+ return profileResolvedDir() !== null
74
+ }
75
+
76
+ export function profileActiveDir(): string | null {
77
+ return profileResolvedDir()
78
+ }
79
+
80
+ function datedLogFile(prefix: string): string {
81
+ const d = new Date()
82
+ const pad = (n: number) => String(n).padStart(2, "0")
83
+ return `oes-${prefix}-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.log`
84
+ }
85
+
86
+ let _dir: string | null | undefined = undefined // undefined = not resolved yet
87
+ let _pdir: string | null | undefined = undefined
88
+
89
+ function resolvedDir(): string | null {
90
+ if (_dir !== undefined) return _dir
91
+ _dir = debugLogDir()
92
+ if (_dir) {
93
+ try {
94
+ fs.mkdirSync(_dir, { recursive: true })
95
+ } catch {
96
+ _dir = null
97
+ }
98
+ }
99
+ return _dir
100
+ }
101
+
102
+ function profileResolvedDir(): string | null {
103
+ if (_pdir !== undefined) return _pdir
104
+ _pdir = profileLogDir()
105
+ if (_pdir) {
106
+ try {
107
+ fs.mkdirSync(_pdir, { recursive: true })
108
+ } catch {
109
+ _pdir = null
110
+ }
111
+ }
112
+ return _pdir
113
+ }
114
+
115
+ /** Reset cached state (test helper). */
116
+ export function resetDebug(): void {
117
+ _dir = undefined
118
+ _pdir = undefined
119
+ profileStats = {}
120
+ screenLines = []
121
+ }
122
+
123
+ /** One on-screen console line: a timestamp plus a short label (e.g. `db open`). */
124
+ export type ScreenLine = { at: number; text: string }
125
+
126
+ /** Ring cap — the console shows a 5-line window over these, scrolled. */
127
+ const SCREEN_MAX = 200
128
+
129
+ let screenLines: ScreenLine[] = []
130
+ const screenListeners = new Set<() => void>()
131
+
132
+ /**
133
+ * Append one short label (e.g. `db open`) to the sidebar console's screen ring
134
+ * and notify subscribers. No-op unless a debug or profile logger is active, so
135
+ * a normal session pays nothing for it.
136
+ */
137
+ export function pushScreenLine(text: string): void {
138
+ if (!debugActive() && !profileActive()) return
139
+ screenLines.push({ at: Date.now(), text })
140
+ if (screenLines.length > SCREEN_MAX) screenLines.splice(0, screenLines.length - SCREEN_MAX)
141
+ for (const listener of screenListeners) {
142
+ try {
143
+ listener()
144
+ } catch {
145
+ // a subscriber must never break the ring feed
146
+ }
147
+ }
148
+ }
149
+
150
+ /** Newest-first copy of the screen ring (empty when nothing was pushed). */
151
+ export function readScreenLines(): ScreenLine[] {
152
+ return screenLines.slice().reverse()
153
+ }
154
+
155
+ /** Subscribe to screen-ring pushes; returns an unsubscribe fn. */
156
+ export function subscribeScreenLines(listener: () => void): () => void {
157
+ screenListeners.add(listener)
158
+ return () => {
159
+ screenListeners.delete(listener)
160
+ }
161
+ }
162
+
163
+ function appendLine(dir: string, file: string, line: Record<string, unknown>): void {
164
+ try {
165
+ const entry = { ts: new Date().toISOString(), ...line }
166
+ fs.appendFileSync(path.join(dir, file), JSON.stringify(entry) + "\n", "utf8")
167
+ } catch {
168
+ // silent — logging must never crash the plugin
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Append one JSON-line entry to the debug log.
174
+ * No-op when OES_DEBUG_OPENCODE is not set or falsy.
175
+ */
176
+ export function dbg(tag: string, msg: string, data?: unknown): void {
177
+ const dir = resolvedDir()
178
+ if (!dir) return
179
+ appendLine(dir, datedLogFile("debug"), {
180
+ tag,
181
+ msg,
182
+ ...(data !== undefined ? { data } : {}),
183
+ })
184
+ }
185
+
186
+ export type ProfileStats = { n: number; sum: number; max: number }
187
+
188
+ /** In-memory per-tag totals, updated only while OES_DEBUG_PROFILE is on. */
189
+ let profileStats: Record<string, ProfileStats> = {}
190
+
191
+ /** Lazily-computed or plain profile data — a factory keeps the call site free when off. */
192
+ type ProfileData = Record<string, unknown> | (() => Record<string, unknown>)
193
+
194
+ function resolveData(data: ProfileData | undefined): Record<string, unknown> | undefined {
195
+ const d = typeof data === "function" ? data() : data
196
+ return d && Object.keys(d).length > 0 ? d : undefined
197
+ }
198
+
199
+ function recordProfile(tag: string, ms: number, data?: ProfileData): void {
200
+ const dir = profileResolvedDir()
201
+ if (!dir) return
202
+ const s = (profileStats[tag] ??= { n: 0, sum: 0, max: 0 })
203
+ s.n += 1
204
+ s.sum += ms
205
+ if (ms > s.max) s.max = ms
206
+ const d = resolveData(data)
207
+ appendLine(dir, datedLogFile("profile"), {
208
+ tag,
209
+ ms,
210
+ ...(d !== undefined ? { data: d } : {}),
211
+ })
212
+ }
213
+
214
+ /**
215
+ * Measure fn with performance.now() and append one { tag, ms, data? } line to
216
+ * the profile log. No-op when OES_DEBUG_PROFILE is not set or falsy. Returns
217
+ * fn's result; throws propagate.
218
+ */
219
+ export function profile<T>(tag: string, fn: () => T, data?: ProfileData): T {
220
+ if (!profileResolvedDir()) return fn()
221
+ const t0 = performance.now()
222
+ try {
223
+ return fn()
224
+ } finally {
225
+ recordProfile(tag, performance.now() - t0, data)
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Same as profile(), but for an async fn — measures until the returned promise
231
+ * settles. The fn is invoked immediately (the promise is what is timed).
232
+ */
233
+ export function profileAsync<T>(
234
+ tag: string,
235
+ fn: () => Promise<T>,
236
+ data?: ProfileData,
237
+ ): Promise<T> {
238
+ if (!profileResolvedDir()) return fn()
239
+ const t0 = performance.now()
240
+ return fn().then(
241
+ (value) => {
242
+ recordProfile(tag, performance.now() - t0, data)
243
+ return value
244
+ },
245
+ (err) => {
246
+ recordProfile(tag, performance.now() - t0, data)
247
+ throw err
248
+ },
249
+ )
250
+ }
251
+
252
+ /** Shallow copy of the per-tag profile totals. Empty when profiling is off. */
253
+ export function readProfileStats(): Record<string, ProfileStats> {
254
+ const out: Record<string, ProfileStats> = {}
255
+ for (const [tag, s] of Object.entries(profileStats)) out[tag] = { ...s }
256
+ return out
257
+ }
258
+
259
+ /**
260
+ * Append one `summary` line with the per-tag totals (n, total ms, avg, max) to
261
+ * the profile log. No-op when profiling is off or nothing was measured.
262
+ */
263
+ export function writeProfileSummary(): void {
264
+ const dir = profileResolvedDir()
265
+ if (!dir) return
266
+ const stats = readProfileStats()
267
+ const tags = Object.keys(stats)
268
+ if (tags.length === 0) return
269
+ const summary: Record<string, { n: number; total: number; avg: number; max: number }> = {}
270
+ for (const tag of tags) {
271
+ const s = stats[tag]
272
+ summary[tag] = {
273
+ n: s.n,
274
+ total: Math.round(s.sum * 10) / 10,
275
+ avg: Math.round((s.sum / s.n) * 10) / 10,
276
+ max: Math.round(s.max * 10) / 10,
277
+ }
278
+ }
279
+ appendLine(dir, datedLogFile("profile"), { tag: "summary", data: summary })
280
+ }