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,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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Host event type + kind. Never reads request or response bodies. */
|
|
2
|
+
import {
|
|
3
|
+
EVENT_FILE_EDITED,
|
|
4
|
+
EVENT_MESSAGE_PART_UPDATED,
|
|
5
|
+
EVENT_PART_DELTA,
|
|
6
|
+
EVENT_REASONING_DELTA,
|
|
7
|
+
EVENT_REASONING_STARTED,
|
|
8
|
+
EVENT_SESSION_CREATED,
|
|
9
|
+
EVENT_SESSION_DIFF,
|
|
10
|
+
EVENT_SESSION_IDLE,
|
|
11
|
+
EVENT_SESSION_STATUS,
|
|
12
|
+
EVENT_SESSION_UPDATED,
|
|
13
|
+
EVENT_STEP_ENDED,
|
|
14
|
+
EVENT_STEP_FAILED,
|
|
15
|
+
EVENT_STEP_STARTED,
|
|
16
|
+
EVENT_TEXT_DELTA,
|
|
17
|
+
EVENT_TOOL_CALLED,
|
|
18
|
+
EVENT_TOOL_ENDED,
|
|
19
|
+
EVENT_TOOL_FAILED,
|
|
20
|
+
EVENT_TOOL_SUCCESS,
|
|
21
|
+
} from "./constants/pware.oc.core.constants.eventType.js"
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
EVENT_KIND_DB_REFRESH,
|
|
25
|
+
EVENT_KIND_FILE,
|
|
26
|
+
EVENT_KIND_FLOW,
|
|
27
|
+
EVENT_KIND_TOOL,
|
|
28
|
+
type EventKind,
|
|
29
|
+
} from "./constants/pware.oc.core.constants.eventKind.js"
|
|
30
|
+
|
|
31
|
+
export type { EventKind }
|
|
32
|
+
|
|
33
|
+
export function eventType(evt: unknown): string {
|
|
34
|
+
if (!evt || typeof evt !== "object") return ""
|
|
35
|
+
return String((evt as Record<string, unknown>).type ?? "").toLowerCase()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function eventKind(type: string): EventKind | null {
|
|
39
|
+
const t = (type || "").toLowerCase()
|
|
40
|
+
if (!t) return null
|
|
41
|
+
if (t.includes(EVENT_FILE_EDITED) || t.includes(EVENT_SESSION_DIFF)) return EVENT_KIND_FILE
|
|
42
|
+
if (
|
|
43
|
+
t.includes(EVENT_TOOL_CALLED) ||
|
|
44
|
+
t.includes(EVENT_TOOL_SUCCESS) ||
|
|
45
|
+
t.includes(EVENT_TOOL_FAILED) ||
|
|
46
|
+
t.includes(EVENT_TOOL_ENDED)
|
|
47
|
+
) {
|
|
48
|
+
return EVENT_KIND_TOOL
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
t.includes(EVENT_SESSION_STATUS) ||
|
|
52
|
+
t.includes(EVENT_SESSION_IDLE) ||
|
|
53
|
+
t.includes(EVENT_SESSION_CREATED) ||
|
|
54
|
+
t.includes(EVENT_SESSION_UPDATED) ||
|
|
55
|
+
t.includes(EVENT_MESSAGE_PART_UPDATED) ||
|
|
56
|
+
t.includes(EVENT_STEP_STARTED) ||
|
|
57
|
+
t.includes(EVENT_STEP_ENDED) ||
|
|
58
|
+
t.includes(EVENT_STEP_FAILED)
|
|
59
|
+
) {
|
|
60
|
+
return EVENT_KIND_DB_REFRESH
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
t.includes(EVENT_TEXT_DELTA) ||
|
|
64
|
+
t.includes(EVENT_REASONING_DELTA) ||
|
|
65
|
+
t.includes(EVENT_REASONING_STARTED) ||
|
|
66
|
+
t.includes(EVENT_PART_DELTA) ||
|
|
67
|
+
t.endsWith(".delta")
|
|
68
|
+
) {
|
|
69
|
+
return EVENT_KIND_FLOW
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function shouldRefreshDb(type: string): boolean {
|
|
75
|
+
const t = (type || "").toLowerCase()
|
|
76
|
+
if (!t || t.includes(".delta")) return false
|
|
77
|
+
const kind = eventKind(t)
|
|
78
|
+
return kind === EVENT_KIND_TOOL || kind === EVENT_KIND_FILE || kind === EVENT_KIND_DB_REFRESH
|
|
79
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.glyph
|
|
3
|
+
*
|
|
4
|
+
* Every visual glyph as one complete definition: a character plus a semantic
|
|
5
|
+
* tone (and an optional blink flag). A glyph's char and colour can never drift
|
|
6
|
+
* apart because they come from the same switch. `markTone` / `stateGlyph` /
|
|
7
|
+
* `directionGlyph` are pure and theme-independent — the ui layer resolves a
|
|
8
|
+
* `ToneKey` to an actual RGBA through `toneColor` in
|
|
9
|
+
* `pware.oc.ui/pware.oc.ui.chrome.tsx`.
|
|
10
|
+
*/
|
|
11
|
+
import { BLINK_TICKS } from "./pware.oc.core.timing.js"
|
|
12
|
+
import {
|
|
13
|
+
FLOW_RECV,
|
|
14
|
+
FLOW_TOOL,
|
|
15
|
+
FLOW_WAIT,
|
|
16
|
+
MARK_QUEUED,
|
|
17
|
+
PULSE_LIVE,
|
|
18
|
+
PULSE_STALE,
|
|
19
|
+
type AgentMark,
|
|
20
|
+
type FlowDir,
|
|
21
|
+
} from "./constants/pware.oc.core.constants.pulse.js"
|
|
22
|
+
import { STATUS_ERROR } from "./constants/pware.oc.core.constants.status.js"
|
|
23
|
+
import {
|
|
24
|
+
ROW_KIND_FILE,
|
|
25
|
+
ROW_KIND_GROUP,
|
|
26
|
+
type RowKind,
|
|
27
|
+
} from "./constants/pware.oc.core.constants.rowKind.js"
|
|
28
|
+
|
|
29
|
+
/** Semantic colour of a glyph or body — no theme values, just meaning. */
|
|
30
|
+
export type ToneKey = "text" | "textMuted" | "success" | "primary" | "warning" | "error"
|
|
31
|
+
|
|
32
|
+
/** One glyph as a complete definition: the character, its tone, and blink. */
|
|
33
|
+
export type GlyphSpec = {
|
|
34
|
+
char: string
|
|
35
|
+
tone: ToneKey
|
|
36
|
+
blink?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Same braille set as OpenCode TUI thinking spinner (`opentui-spinner`). */
|
|
40
|
+
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
41
|
+
|
|
42
|
+
/** Queued — waiting for a slot: the hourglass (the wait flow owns ◷). */
|
|
43
|
+
export const QUEUED_GLYPH = "⧗"
|
|
44
|
+
|
|
45
|
+
export function spinnerFrame(frame: number): string {
|
|
46
|
+
const i = ((frame % SPINNER_FRAMES.length) + SPINNER_FRAMES.length) % SPINNER_FRAMES.length
|
|
47
|
+
return SPINNER_FRAMES[i] ?? "⠋"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Cold-start "engage" bar length in glyph frames (GLYPH_TICK_MS × 12 ≈ 1s). */
|
|
51
|
+
export const ENGAGE_MIN_FRAMES = 12
|
|
52
|
+
/** Hard ceiling so the boot bar can never strand (GLYPH_TICK_MS × 50 = 4s). */
|
|
53
|
+
export const ENGAGE_MAX_FRAMES = 50
|
|
54
|
+
|
|
55
|
+
/** Fill ratio of the cold-start engage bar: linear to 1 at `ENGAGE_MIN_FRAMES`. */
|
|
56
|
+
export function engageFill(frame: number): number {
|
|
57
|
+
return Math.min(1, Math.max(0, frame) / ENGAGE_MIN_FRAMES)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The cold-start engage bar ends once the first real snapshot has landed
|
|
62
|
+
* (`ready`) after its minimum show time — or on the hard ceiling (no DB / no
|
|
63
|
+
* snapshot ever) so the row can never strand. Negative frames count as 0.
|
|
64
|
+
*/
|
|
65
|
+
export function engageDone(frame: number, ready: boolean): boolean {
|
|
66
|
+
const n = Math.max(0, frame)
|
|
67
|
+
return (ready && n >= ENGAGE_MIN_FRAMES) || n >= ENGAGE_MAX_FRAMES
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Options for {@link sinPulseAlpha} — one full fade cycle spans `periodFrames` tick frames. */
|
|
71
|
+
export type SinPulseAlphaOpts = {
|
|
72
|
+
periodFrames?: number
|
|
73
|
+
min?: number
|
|
74
|
+
max?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sinusoidal fade pulse for glyph opacity: alpha eases `min → max → min` over one
|
|
79
|
+
* period. Defaults to `BLINK_TICKS * 2` frames (same wall-clock span as
|
|
80
|
+
* {@link flowBlinkOn}). Pure — inject the UI tick frame; the ui layer maps the
|
|
81
|
+
* result to OpenTUI `opacity` or a colour lerp.
|
|
82
|
+
*/
|
|
83
|
+
export function sinPulseAlpha(frame: number, opts: SinPulseAlphaOpts = {}): number {
|
|
84
|
+
const period = Math.max(1, Math.round(opts.periodFrames ?? BLINK_TICKS * 2))
|
|
85
|
+
const lo = Math.min(opts.min ?? 0.25, opts.max ?? 1)
|
|
86
|
+
const hi = Math.max(opts.min ?? 0.25, opts.max ?? 1)
|
|
87
|
+
const phase = (Math.abs(frame) % period) / period
|
|
88
|
+
const wave = Math.sin(phase * Math.PI)
|
|
89
|
+
return lo + wave * (hi - lo)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** ↑/↓ blink half-period in ticks (TICK_MS × BLINK_TICKS = 100ms). */
|
|
93
|
+
export function flowBlinkOn(frame: number): boolean {
|
|
94
|
+
return Math.floor(Math.abs(frame) / BLINK_TICKS) % 2 === 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The tone a state mark carries: error red, queued/stale warning, live success, else muted. */
|
|
98
|
+
export function markTone(mark: AgentMark): ToneKey {
|
|
99
|
+
if (mark === STATUS_ERROR) return "error"
|
|
100
|
+
if (mark === MARK_QUEUED || mark === PULSE_STALE) return "warning"
|
|
101
|
+
if (mark === PULSE_LIVE) return "success"
|
|
102
|
+
return "textMuted"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The state glyph for an agent mark — spinner, cross, hourglass or dot, in one spec. */
|
|
106
|
+
export function stateGlyph(mark: AgentMark, frame = 0): GlyphSpec {
|
|
107
|
+
const char =
|
|
108
|
+
mark === STATUS_ERROR
|
|
109
|
+
? "×"
|
|
110
|
+
: mark === MARK_QUEUED
|
|
111
|
+
? QUEUED_GLYPH
|
|
112
|
+
: mark === PULSE_LIVE || mark === PULSE_STALE
|
|
113
|
+
? spinnerFrame(frame)
|
|
114
|
+
: "•"
|
|
115
|
+
return { char, tone: markTone(mark) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The direction glyph for a flow — ◷ waiting, ← receiving, → tool; null when quiet. */
|
|
119
|
+
export function directionGlyph(flow: FlowDir | null): GlyphSpec | null {
|
|
120
|
+
if (!flow) return null
|
|
121
|
+
if (flow === FLOW_WAIT) return { char: "◷", tone: "warning", blink: true }
|
|
122
|
+
if (flow === FLOW_RECV) return { char: "←", tone: "success", blink: true }
|
|
123
|
+
return { char: "→", tone: "primary", blink: true }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Body text tone for a row kind: quiet for files/groups, otherwise the mark tone. */
|
|
127
|
+
export function defaultBodyTone(
|
|
128
|
+
kind: RowKind,
|
|
129
|
+
mark: AgentMark | undefined,
|
|
130
|
+
current: boolean,
|
|
131
|
+
): ToneKey {
|
|
132
|
+
if (kind === ROW_KIND_FILE || kind === ROW_KIND_GROUP) return "textMuted"
|
|
133
|
+
const tone = mark ? markTone(mark) : "textMuted"
|
|
134
|
+
return current && tone === "textMuted" ? "primary" : tone
|
|
135
|
+
}
|