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,462 +1,463 @@
1
- /** @jsxImportSource @opentui/solid */
2
- /** Perf tab: where a session's wall clock goes, per model and per tool. */
3
- import { createMemo, For, Show, type JSX } from "solid-js"
4
- import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
5
- import { ClickText, toneColor, type ThemeColors } from "../pware.oc.ui/pware.oc.ui.chrome.js"
6
- import { FoldSection, RowList, useFold, useReveal } from "../pware.oc.ui/pware.oc.ui.sections.js"
7
- import { openPerfCharts, openPerfLog } from "../pware.oc.ui/pware.oc.ui.menudialogs.js"
8
- import type { PerfLogKind } from "./pware.oc.perf.reader.js"
9
- import {
10
- type ModelPerf,
11
- type PerfSnapshot,
12
- type SessionPerf,
13
- type ToolPerf,
14
- } from "./pware.oc.perf.reader.js"
15
- import { THINK_GLYPH, directionGlyph, spinnerFrame, type GlyphSpec } from "../pware.oc.ui/pware.oc.ui.glyphs.js"
16
- import {
17
- PERF_LOG_KIND_MODELS,
18
- PERF_LOG_KIND_TIME,
19
- PERF_PHASE_IDLE,
20
- PERF_PHASE_RECV,
21
- PERF_PHASE_THINK,
22
- PERF_PHASE_TOOL,
23
- PERF_PHASE_WAIT,
24
- type PerfPhase,
25
- } from "../pware.oc.core/constants/pware.oc.core.constants.phase.js"
26
- import {
27
- FLOW_RECV,
28
- FLOW_TOOL,
29
- FLOW_WAIT,
30
- } from "../pware.oc.core/constants/pware.oc.core.constants.pulse.js"
31
- import {
32
- formatDuration,
33
- formatPercent,
34
- formatRate,
35
- formatSpan,
36
- packChips,
37
- packStackedRow,
38
- timeSummary,
39
- tokenSummary,
40
- type Chip,
41
- type FlowDir,
42
- } from "../pware.oc.core/pware.oc.core.pulse.js"
43
- import { clipMiddleWidth, strWidth } from "../pware.oc.core/pware.oc.core.width.js"
44
- import { asciiTrend, shareBar } from "./pware.oc.perf.charts.js"
45
-
46
- const KV_FOLD_MODELS = "oes.fold.perf.models"
47
- const KV_FOLD_TIME = "oes.fold.perf.time"
48
- const KV_FOLD_TOOLS = "oes.fold.perf.tools"
49
- const KV_FOLD_TREND = "oes.fold.perf.trend"
50
- const KV_FOLD_HISTORY = "oes.fold.perf.history"
51
-
52
- const PHASES: Array<{ key: PerfPhase; spec: GlyphSpec; label: string }> = [
53
- { key: PERF_PHASE_WAIT, spec: directionGlyph(FLOW_WAIT)!, label: "wait" },
54
- { key: PERF_PHASE_THINK, spec: { char: THINK_GLYPH, tone: "primary" }, label: "think" },
55
- { key: PERF_PHASE_RECV, spec: directionGlyph(FLOW_RECV)!, label: "recv" },
56
- { key: PERF_PHASE_TOOL, spec: directionGlyph(FLOW_TOOL)!, label: "tools" },
57
- { key: PERF_PHASE_IDLE, spec: { char: "·", tone: "textMuted" }, label: "idle" },
58
- ]
59
-
60
- function MetricRow(props: {
61
- glyph: string
62
- glyphFg: string
63
- name: string
64
- nameFg: string
65
- chips: Chip[]
66
- chipFg?: (chip: Chip) => string
67
- lineMax: number
68
- bold?: boolean
69
- minName?: number
70
- onSelect?: () => void
71
- }): JSX.Element {
72
- const body = createMemo(() => {
73
- const room = Math.max(4, props.lineMax - 2)
74
- const minName = props.minName ?? 6
75
- const chips = packChips(minName, props.chips, room)
76
- const used = chips.reduce((sum, c) => sum + strWidth(c.text) + 1, 0)
77
- const name = clipMiddleWidth(props.name, Math.max(minName, room - used))
78
- return { name, chips }
79
- })
80
- return (
81
- <box flexDirection="row" onMouseUp={props.onSelect}>
82
- <text fg={props.glyphFg}>{`${props.glyph} `}</text>
83
- <ClickText fg={props.nameFg} bold={Boolean(props.bold)} underline={Boolean(props.onSelect)}>
84
- {body().name}
85
- </ClickText>
86
- <For each={body().chips}>
87
- {(chip) => (
88
- <text fg={props.chipFg ? props.chipFg(chip) : props.nameFg}>{` ${chip.text}`}</text>
89
- )}
90
- </For>
91
- </box>
92
- )
93
- }
94
-
95
- function PhaseRow(props: {
96
- spec: GlyphSpec
97
- label: string
98
- ms: number
99
- share: number | null
100
- colors: ThemeColors
101
- lineMax: number
102
- onSelect?: () => void
103
- }): JSX.Element {
104
- const fg = () => toneColor(props.spec.tone, props.colors)
105
- // Fixed columns keep every bar starting and ending on the same screen column.
106
- const barWidth = () => Math.max(4, props.lineMax - 18)
107
- return (
108
- <box flexDirection="row" onMouseUp={props.onSelect}>
109
- <text fg={fg()}>{`${props.spec.char} `}</text>
110
- <ClickText fg={props.colors.textMuted} underline={Boolean(props.onSelect)}>
111
- {props.label.padEnd(5)}
112
- </ClickText>
113
- <text fg={fg()}>{` ${shareBar(props.share, barWidth())}`}</text>
114
- <text fg={props.colors.textMuted}>
115
- {` ${formatPercent(props.share).padStart(4)} ${formatSpan(props.ms).padStart(4)}`}
116
- </text>
117
- </box>
118
- )
119
- }
120
-
121
- function ModelRow(props: {
122
- model: ModelPerf
123
- colors: ThemeColors
124
- lineMax: number
125
- /** Fast glyph tick — read only in the live glyph. */
126
- glyphFrame: () => number
127
- live: boolean
128
- }): JSX.Element {
129
- const m = () => props.model
130
- const stacked = () =>
131
- packStackedRow(
132
- m().model,
133
- [
134
- { text: `${m().turns}×`, rank: 2 },
135
- { text: m().waitMs != null ? `↑${formatDuration(m().waitMs)}` : "", rank: 0 },
136
- { text: m().thinkMs != null ? `∴${formatDuration(m().thinkMs)}` : "", rank: 3 },
137
- { text: m().recvMs != null ? `↓${formatDuration(m().recvMs)}` : "", rank: 1 },
138
- { text: formatRate(m().tokensPerSec), rank: 4 },
139
- ],
140
- props.lineMax,
141
- )
142
- const name = () => stacked().name
143
- const chips = () => stacked().chips
144
- const chipFg = (_chip: Chip): string => props.colors.textMuted
145
- return (
146
- <box flexDirection="column">
147
- <box flexDirection="row">
148
- <text fg={props.live ? props.colors.success : props.colors.textMuted}>
149
- {`${props.live ? spinnerFrame(props.glyphFrame()) : "•"} `}
150
- </text>
151
- <text fg={props.colors.text}>{name()}</text>
152
- </box>
153
- <box flexDirection="row" paddingLeft={2}>
154
- <For each={chips()}>
155
- {(chip, i) => (
156
- <text fg={chipFg(chip)}>{i() === 0 ? chip.text : ` ${chip.text}`}</text>
157
- )}
158
- </For>
159
- </box>
160
- </box>
161
- )
162
- }
163
-
164
- function ToolRow(props: {
165
- tool: ToolPerf
166
- share: number | null
167
- colors: ThemeColors
168
- lineMax: number
169
- onSelect?: () => void
170
- }): JSX.Element {
171
- const t = () => props.tool
172
- const chips = (): Chip[] => [
173
- { text: `${t().count}×`, rank: 2 },
174
- { text: formatDuration(t().avgMs), rank: 0 },
175
- { text: t().errors > 0 ? `×${t().errors}` : "", rank: 1 },
176
- { text: shareBar(props.share, 4), rank: 3 },
177
- ]
178
- const chipFg = (chip: Chip): string => {
179
- if (chip.text.startsWith("×")) return props.colors.error || props.colors.text
180
- return props.colors.textMuted
181
- }
182
- return (
183
- <MetricRow
184
- glyph={t().errors > 0 ? "×" : "→"}
185
- glyphFg={t().errors > 0 ? props.colors.error || props.colors.text : props.colors.textMuted}
186
- name={t().name}
187
- nameFg={props.colors.text}
188
- chips={chips()}
189
- chipFg={chipFg}
190
- lineMax={props.lineMax}
191
- minName={6}
192
- onSelect={props.onSelect}
193
- />
194
- )
195
- }
196
-
197
- function HistoryRow(props: {
198
- row: SessionPerf
199
- colors: ThemeColors
200
- lineMax: number
201
- current: boolean
202
- onSelect: () => void
203
- }): JSX.Element {
204
- const r = () => props.row
205
- const chips = (): Chip[] => [
206
- { text: `${r().turns}×`, rank: 2 },
207
- { text: r().waitMs != null ? `↑${formatDuration(r().waitMs)}` : "", rank: 0 },
208
- { text: r().toolShare != null ? `→${formatPercent(r().toolShare)}` : "", rank: 1 },
209
- ]
210
- const chipFg = (_chip: Chip): string => props.colors.textMuted
211
- return (
212
- <MetricRow
213
- glyph="•"
214
- glyphFg={props.colors.textMuted}
215
- name={r().title}
216
- nameFg={props.colors.text}
217
- bold={props.current}
218
- chips={chips()}
219
- chipFg={chipFg}
220
- lineMax={props.lineMax}
221
- minName={8}
222
- onSelect={props.onSelect}
223
- />
224
- )
225
- }
226
-
227
- function TrendChart(props: {
228
- label: string
229
- values: Array<number | null>
230
- fg: string
231
- colors: ThemeColors
232
- lineMax: number
233
- }): JSX.Element {
234
- const lines = () =>
235
- asciiTrend(props.values, { width: Math.max(8, props.lineMax - 10), height: 3 }).split("\n")
236
- return (
237
- <box flexDirection="column">
238
- <text fg={props.colors.textMuted}>{props.label}</text>
239
- <For each={lines()}>
240
- {(line) => <text fg={props.fg}>{line || " "}</text>}
241
- </For>
242
- </box>
243
- )
244
- }
245
-
246
- export type PerfPanelProps = {
247
- api: TuiPluginApi
248
- perf: PerfSnapshot
249
- colors: ThemeColors
250
- lineMax: number
251
- rows: number
252
- /** Fast glyph tick — read only in the live glyph, so the panel is not rebuilt per tick. */
253
- glyphFrame: () => number
254
- /** Phase the watched session is in right now, plus how long it has lasted. */
255
- livePhase: FlowDir | null
256
- livePhaseMs: number | null
257
- currentSessionId: string
258
- dbPath: string
259
- turns: number
260
- onSelect: (sessionId: string) => void
261
- }
262
-
263
- export function PerfPanel(props: PerfPanelProps): JSX.Element {
264
- const foldModels = useFold(props.api, KV_FOLD_MODELS)
265
- const foldTime = useFold(props.api, KV_FOLD_TIME)
266
- const foldTools = useFold(props.api, KV_FOLD_TOOLS)
267
- const foldTrend = useFold(props.api, KV_FOLD_TREND)
268
- const foldHistory = useFold(props.api, KV_FOLD_HISTORY)
269
-
270
- const totals = () => props.perf.totals
271
- const wall = () => Math.max(1, totals().wallMs)
272
- const activeTimeMs = () => Math.max(0, totals().wallMs - totals().phases.idle)
273
- const phases = () =>
274
- PHASES.map((p) => ({ ...p, ms: totals().phases[p.key] })).filter(
275
- (p) => p.ms > 0 || p.key === PERF_PHASE_WAIT || p.key === PERF_PHASE_TOOL,
276
- )
277
- const modelsReveal = useReveal(props.rows)
278
- const toolsReveal = useReveal(props.rows)
279
- const historyReveal = useReveal(props.rows)
280
- const toolTotal = () => props.perf.tools.reduce((sum, t) => sum + t.totalMs, 0)
281
- const trendWait = () => props.perf.trend.map((p) => p.waitMs)
282
- const trendRate = () => props.perf.trend.map((p) => p.tokensPerSec)
283
- const hasTrend = () =>
284
- props.perf.trend.length > 1 &&
285
- (trendWait().some((v) => v != null) || trendRate().some((v) => v != null))
286
-
287
- const openLog = (kind: PerfLogKind, toolFilter?: string) =>
288
- openPerfLog(props.api, props.colors, {
289
- dbPath: props.dbPath,
290
- sessionId: props.currentSessionId,
291
- turns: props.turns,
292
- kind,
293
- toolFilter,
294
- })
295
-
296
- const openCharts = () =>
297
- openPerfCharts(props.api, props.colors, {
298
- perf: props.perf,
299
- currentSessionId: props.currentSessionId,
300
- })
301
-
302
- const liveLabel = () => {
303
- const dir = props.livePhase
304
- if (!dir) return null
305
- const label = dir === FLOW_WAIT ? "wait" : dir === FLOW_RECV ? "recv" : "tool"
306
- const fg = toneColor(directionGlyph(dir)!.tone, props.colors)
307
- return { label, fg, ms: props.livePhaseMs }
308
- }
309
-
310
- return (
311
- <box flexDirection="column" gap={1}>
312
- <Show when={liveLabel()}>
313
- {(live) => (
314
- <box flexDirection="row">
315
- <text fg={live().fg}>{`${spinnerFrame(props.glyphFrame())} ${live().label}`}</text>
316
- <text fg={props.colors.textMuted}>
317
- {live().ms != null ? ` ${formatDuration(live().ms)}` : ""}
318
- </text>
319
- </box>
320
- )}
321
- </Show>
322
-
323
- <Show when={props.perf.totals.turns > 0} fallback={null}>
324
- <box flexDirection="column" gap={1}>
325
- <FoldSection
326
- title="Models"
327
- open={foldModels.open()}
328
- count={props.perf.models.length}
329
- suffix={tokenSummary(totals())}
330
- colors={props.colors}
331
- onToggle={foldModels.toggle}
332
- onDetail={() => openLog(PERF_LOG_KIND_MODELS)}
333
- >
334
- <RowList
335
- items={props.perf.models}
336
- budget={props.rows + modelsReveal.more()}
337
- colors={props.colors}
338
- renderItem={(m) => (
339
- <ModelRow
340
- model={m}
341
- colors={props.colors}
342
- lineMax={props.lineMax}
343
- glyphFrame={props.glyphFrame}
344
- live={Boolean(props.livePhase) && m === props.perf.models[0]}
345
- />
346
- )}
347
- more={{ onReveal: modelsReveal.reveal }}
348
- />
349
- </FoldSection>
350
-
351
- <FoldSection
352
- title="Time"
353
- open={foldTime.open()}
354
- suffix={timeSummary({
355
- turns: totals().turns,
356
- durationMs: activeTimeMs(),
357
- errors: totals().errors,
358
- aborts: totals().aborts,
359
- })}
360
- colors={props.colors}
361
- onToggle={foldTime.toggle}
362
- onDetail={() => openLog(PERF_LOG_KIND_TIME)}
363
- >
364
- <For each={phases()}>
365
- {(p) => (
366
- <PhaseRow
367
- spec={p.spec}
368
- label={p.label}
369
- ms={p.ms}
370
- share={p.ms / wall()}
371
- colors={props.colors}
372
- lineMax={props.lineMax}
373
- onSelect={() => openLog(p.key)}
374
- />
375
- )}
376
- </For>
377
- </FoldSection>
378
-
379
- <Show when={props.perf.tools.length > 0}>
380
- <FoldSection
381
- title="Slow tools"
382
- open={foldTools.open()}
383
- count={props.perf.tools.length}
384
- suffix={formatSpan(toolTotal())}
385
- colors={props.colors}
386
- onToggle={foldTools.toggle}
387
- onDetail={() => openLog(PERF_PHASE_TOOL)}
388
- >
389
- <RowList
390
- items={props.perf.tools}
391
- budget={props.rows + toolsReveal.more()}
392
- colors={props.colors}
393
- renderItem={(t) => (
394
- <ToolRow
395
- tool={t}
396
- share={toolTotal() > 0 ? t.totalMs / toolTotal() : null}
397
- colors={props.colors}
398
- lineMax={props.lineMax}
399
- onSelect={() => openLog(PERF_PHASE_TOOL, t.name)}
400
- />
401
- )}
402
- more={{ onReveal: toolsReveal.reveal }}
403
- />
404
- </FoldSection>
405
- </Show>
406
-
407
- <Show when={hasTrend()}>
408
- <FoldSection
409
- title="Trend"
410
- open={foldTrend.open()}
411
- count={props.perf.trend.length}
412
- colors={props.colors}
413
- onToggle={foldTrend.toggle}
414
- onDetail={() => openCharts()}
415
- >
416
- <TrendChart
417
- label={PERF_PHASE_WAIT}
418
- values={trendWait()}
419
- fg={props.colors.warning || props.colors.text}
420
- colors={props.colors}
421
- lineMax={props.lineMax}
422
- />
423
- <TrendChart
424
- label="tok/s"
425
- values={trendRate()}
426
- fg={props.colors.success}
427
- colors={props.colors}
428
- lineMax={props.lineMax}
429
- />
430
- </FoldSection>
431
- </Show>
432
-
433
- <Show when={props.perf.history.length > 0}>
434
- <FoldSection
435
- title="History"
436
- open={foldHistory.open()}
437
- count={props.perf.history.length}
438
- colors={props.colors}
439
- onToggle={foldHistory.toggle}
440
- >
441
- <RowList
442
- items={props.perf.history}
443
- budget={props.rows + historyReveal.more()}
444
- colors={props.colors}
445
- renderItem={(row) => (
446
- <HistoryRow
447
- row={row}
448
- colors={props.colors}
449
- lineMax={props.lineMax}
450
- current={row.id === props.currentSessionId}
451
- onSelect={() => props.onSelect(row.id)}
452
- />
453
- )}
454
- more={{ onReveal: historyReveal.reveal }}
455
- />
456
- </FoldSection>
457
- </Show>
458
- </box>
459
- </Show>
460
- </box>
461
- )
462
- }
1
+ /** @jsxImportSource @opentui/solid */
2
+ /** Perf tab: where a session's wall clock goes, per model and per tool. */
3
+ import { createMemo, For, Show, type JSX } from "solid-js"
4
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
5
+ import { ClickText, toneColor, type ThemeColors } from "../pware.oc.ui/pware.oc.ui.chrome.js"
6
+ import { FoldSection, RowList, useFold, useReveal } from "../pware.oc.ui/pware.oc.ui.sections.js"
7
+ import { openPerfCharts, openPerfLog } from "../pware.oc.ui/pware.oc.ui.menudialogs.js"
8
+ import type { PerfLogKind } from "./pware.oc.perf.reader.js"
9
+ import {
10
+ type ModelPerf,
11
+ type PerfSnapshot,
12
+ type SessionPerf,
13
+ type ToolPerf,
14
+ } from "./pware.oc.perf.reader.js"
15
+ import { THINK_GLYPH, directionGlyph, spinnerFrame, type GlyphSpec } from "../pware.oc.ui/pware.oc.ui.glyphs.js"
16
+ import {
17
+ PERF_LOG_KIND_MODELS,
18
+ PERF_LOG_KIND_TIME,
19
+ PERF_PHASE_IDLE,
20
+ PERF_PHASE_RECV,
21
+ PERF_PHASE_THINK,
22
+ PERF_PHASE_TOOL,
23
+ PERF_PHASE_WAIT,
24
+ type PerfPhase,
25
+ } from "../pware.oc.core/constants/pware.oc.core.constants.phase.js"
26
+ import {
27
+ FLOW_RECV,
28
+ FLOW_TOOL,
29
+ FLOW_WAIT,
30
+ } from "../pware.oc.core/constants/pware.oc.core.constants.pulse.js"
31
+ import {
32
+ formatDuration,
33
+ formatPercent,
34
+ formatRate,
35
+ formatSpan,
36
+ packChips,
37
+ packStackedRow,
38
+ timeSummary,
39
+ tokenSummary,
40
+ type Chip,
41
+ type FlowDir,
42
+ } from "../pware.oc.core/pware.oc.core.pulse.js"
43
+ import { clipMiddleWidth, strWidth } from "../pware.oc.core/pware.oc.core.width.js"
44
+ import { asciiTrend, shareBar } from "./pware.oc.perf.charts.js"
45
+
46
+ const KV_FOLD_MODELS = "oes.fold.perf.models"
47
+ const KV_FOLD_TIME = "oes.fold.perf.time"
48
+ const KV_FOLD_TOOLS = "oes.fold.perf.tools"
49
+ const KV_FOLD_TREND = "oes.fold.perf.trend"
50
+ const KV_FOLD_HISTORY = "oes.fold.perf.history"
51
+
52
+ const PHASES: Array<{ key: PerfPhase; spec: GlyphSpec; label: string }> = [
53
+ { key: PERF_PHASE_WAIT, spec: directionGlyph(FLOW_WAIT)!, label: "wait" },
54
+ { key: PERF_PHASE_THINK, spec: { char: THINK_GLYPH, tone: "primary" }, label: "think" },
55
+ { key: PERF_PHASE_RECV, spec: directionGlyph(FLOW_RECV)!, label: "recv" },
56
+ { key: PERF_PHASE_TOOL, spec: directionGlyph(FLOW_TOOL)!, label: "tools" },
57
+ { key: PERF_PHASE_IDLE, spec: { char: "·", tone: "textMuted" }, label: "idle" },
58
+ ]
59
+
60
+ function MetricRow(props: {
61
+ glyph: string
62
+ glyphFg: string
63
+ name: string
64
+ nameFg: string
65
+ chips: Chip[]
66
+ chipFg?: (chip: Chip) => string
67
+ lineMax: number
68
+ bold?: boolean
69
+ minName?: number
70
+ onSelect?: () => void
71
+ }): JSX.Element {
72
+ const body = createMemo(() => {
73
+ const room = Math.max(4, props.lineMax - 2)
74
+ const minName = props.minName ?? 6
75
+ const chips = packChips(minName, props.chips, room)
76
+ const used = chips.reduce((sum, c) => sum + strWidth(c.text) + 1, 0)
77
+ const name = clipMiddleWidth(props.name, Math.max(minName, room - used))
78
+ return { name, chips }
79
+ })
80
+ return (
81
+ <box flexDirection="row" onMouseUp={props.onSelect}>
82
+ <text fg={props.glyphFg}>{`${props.glyph} `}</text>
83
+ <ClickText fg={props.nameFg} bold={Boolean(props.bold)} underline={Boolean(props.onSelect)}>
84
+ {body().name}
85
+ </ClickText>
86
+ <For each={body().chips}>
87
+ {(chip) => (
88
+ <text fg={props.chipFg ? props.chipFg(chip) : props.nameFg}>{` ${chip.text}`}</text>
89
+ )}
90
+ </For>
91
+ </box>
92
+ )
93
+ }
94
+
95
+ function PhaseRow(props: {
96
+ spec: GlyphSpec
97
+ label: string
98
+ ms: number
99
+ share: number | null
100
+ colors: ThemeColors
101
+ lineMax: number
102
+ onSelect?: () => void
103
+ }): JSX.Element {
104
+ const fg = () => toneColor(props.spec.tone, props.colors)
105
+ // Fixed columns keep every bar starting and ending on the same screen column.
106
+ const barWidth = () => Math.max(4, props.lineMax - 18)
107
+ return (
108
+ <box flexDirection="row" onMouseUp={props.onSelect}>
109
+ <text fg={fg()}>{`${props.spec.char} `}</text>
110
+ <ClickText fg={props.colors.textMuted} underline={Boolean(props.onSelect)}>
111
+ {props.label.padEnd(5)}
112
+ </ClickText>
113
+ <text fg={fg()}>{` ${shareBar(props.share, barWidth())}`}</text>
114
+ <text fg={props.colors.textMuted}>
115
+ {` ${formatPercent(props.share).padStart(4)} ${formatSpan(props.ms).padStart(4)}`}
116
+ </text>
117
+ </box>
118
+ )
119
+ }
120
+
121
+ function ModelRow(props: {
122
+ model: ModelPerf
123
+ colors: ThemeColors
124
+ lineMax: number
125
+ /** Fast glyph tick — read only in the live glyph. */
126
+ glyphFrame: () => number
127
+ live: boolean
128
+ }): JSX.Element {
129
+ const m = () => props.model
130
+ const stacked = () =>
131
+ packStackedRow(
132
+ m().model,
133
+ [
134
+ { text: `${m().turns}×`, rank: 2 },
135
+ { text: m().waitMs != null ? `↑${formatDuration(m().waitMs)}` : "", rank: 0 },
136
+ { text: m().thinkMs != null ? `∴${formatDuration(m().thinkMs)}` : "", rank: 3 },
137
+ { text: m().recvMs != null ? `↓${formatDuration(m().recvMs)}` : "", rank: 1 },
138
+ { text: formatRate(m().tokensPerSec), rank: 4 },
139
+ ],
140
+ props.lineMax,
141
+ )
142
+ const name = () => stacked().name
143
+ const chips = () => stacked().chips
144
+ const chipFg = (_chip: Chip): string => props.colors.textMuted
145
+ return (
146
+ <box flexDirection="column">
147
+ <box flexDirection="row">
148
+ <text fg={props.live ? props.colors.success : props.colors.textMuted}>
149
+ {`${props.live ? spinnerFrame(props.glyphFrame()) : "•"} `}
150
+ </text>
151
+ <text fg={props.colors.text}>{name()}</text>
152
+ </box>
153
+ <box flexDirection="row" paddingLeft={2}>
154
+ <For each={chips()}>
155
+ {(chip, i) => (
156
+ <text fg={chipFg(chip)}>{i() === 0 ? chip.text : ` ${chip.text}`}</text>
157
+ )}
158
+ </For>
159
+ </box>
160
+ </box>
161
+ )
162
+ }
163
+
164
+ function ToolRow(props: {
165
+ tool: ToolPerf
166
+ share: number | null
167
+ colors: ThemeColors
168
+ lineMax: number
169
+ onSelect?: () => void
170
+ }): JSX.Element {
171
+ const t = () => props.tool
172
+ const chips = (): Chip[] => [
173
+ { text: `${t().count}×`, rank: 2 },
174
+ { text: formatDuration(t().avgMs), rank: 0 },
175
+ { text: t().errors > 0 ? `×${t().errors}` : "", rank: 1 },
176
+ { text: shareBar(props.share, 4), rank: 3 },
177
+ ]
178
+ const chipFg = (chip: Chip): string => {
179
+ if (chip.text.startsWith("×")) return props.colors.error || props.colors.text
180
+ return props.colors.textMuted
181
+ }
182
+ return (
183
+ <MetricRow
184
+ glyph={t().errors > 0 ? "×" : "→"}
185
+ glyphFg={t().errors > 0 ? props.colors.error || props.colors.text : props.colors.textMuted}
186
+ name={t().name}
187
+ nameFg={props.colors.text}
188
+ chips={chips()}
189
+ chipFg={chipFg}
190
+ lineMax={props.lineMax}
191
+ minName={6}
192
+ onSelect={props.onSelect}
193
+ />
194
+ )
195
+ }
196
+
197
+ function HistoryRow(props: {
198
+ row: SessionPerf
199
+ colors: ThemeColors
200
+ lineMax: number
201
+ current: boolean
202
+ onSelect: () => void
203
+ }): JSX.Element {
204
+ const r = () => props.row
205
+ const chips = (): Chip[] => [
206
+ { text: `${r().turns}×`, rank: 2 },
207
+ { text: r().waitMs != null ? `↑${formatDuration(r().waitMs)}` : "", rank: 0 },
208
+ { text: r().toolShare != null ? `→${formatPercent(r().toolShare)}` : "", rank: 1 },
209
+ ]
210
+ const chipFg = (_chip: Chip): string => props.colors.textMuted
211
+ return (
212
+ <MetricRow
213
+ glyph="•"
214
+ glyphFg={props.colors.textMuted}
215
+ name={r().title}
216
+ nameFg={props.colors.text}
217
+ bold={props.current}
218
+ chips={chips()}
219
+ chipFg={chipFg}
220
+ lineMax={props.lineMax}
221
+ minName={8}
222
+ onSelect={props.onSelect}
223
+ />
224
+ )
225
+ }
226
+
227
+ function TrendChart(props: {
228
+ label: string
229
+ values: Array<number | null>
230
+ fg: string
231
+ colors: ThemeColors
232
+ lineMax: number
233
+ }): JSX.Element {
234
+ const lines = () =>
235
+ asciiTrend(props.values, { width: Math.max(8, props.lineMax - 10), height: 3 }).split("\n")
236
+ return (
237
+ <box flexDirection="column">
238
+ <text fg={props.colors.textMuted}>{props.label}</text>
239
+ <For each={lines()}>
240
+ {(line) => <text fg={props.fg}>{line || " "}</text>}
241
+ </For>
242
+ </box>
243
+ )
244
+ }
245
+
246
+ export type PerfPanelProps = {
247
+ api: TuiPluginApi
248
+ perf: PerfSnapshot
249
+ colors: ThemeColors
250
+ lineMax: number
251
+ rows: number
252
+ /** Fast glyph tick — read only in the live glyph, so the panel is not rebuilt per tick. */
253
+ glyphFrame: () => number
254
+ /** Phase the watched session is in right now, plus how long it has lasted. */
255
+ livePhase: FlowDir | null
256
+ livePhaseMs: number | null
257
+ currentSessionId: string
258
+ dbPath: string
259
+ turns: number
260
+ onSelect: (sessionId: string) => void
261
+ }
262
+
263
+ export function PerfPanel(props: PerfPanelProps): JSX.Element {
264
+ const foldModels = useFold(props.api, KV_FOLD_MODELS)
265
+ const foldTime = useFold(props.api, KV_FOLD_TIME)
266
+ const foldTools = useFold(props.api, KV_FOLD_TOOLS)
267
+ const foldTrend = useFold(props.api, KV_FOLD_TREND)
268
+ const foldHistory = useFold(props.api, KV_FOLD_HISTORY)
269
+
270
+ const totals = () => props.perf.totals
271
+ const wall = () => Math.max(1, totals().wallMs)
272
+ const activeTimeMs = () => Math.max(0, totals().wallMs - totals().phases.idle)
273
+ const phases = () =>
274
+ PHASES.map((p) => ({ ...p, ms: totals().phases[p.key] })).filter(
275
+ (p) => p.ms > 0 || p.key === PERF_PHASE_WAIT || p.key === PERF_PHASE_TOOL,
276
+ )
277
+ const modelsReveal = useReveal(props.rows)
278
+ const toolsReveal = useReveal(props.rows)
279
+ const historyReveal = useReveal(props.rows)
280
+ const toolTotal = () => props.perf.tools.reduce((sum, t) => sum + t.totalMs, 0)
281
+ const trendWait = () => props.perf.trend.map((p) => p.waitMs)
282
+ const trendRate = () => props.perf.trend.map((p) => p.tokensPerSec)
283
+ const hasTrend = () =>
284
+ props.perf.trend.length > 1 &&
285
+ (trendWait().some((v) => v != null) || trendRate().some((v) => v != null))
286
+
287
+ const openLog = (kind: PerfLogKind, toolFilter?: string) => {
288
+ void openPerfLog(props.api, props.colors, {
289
+ dbPath: props.dbPath,
290
+ sessionId: props.currentSessionId,
291
+ turns: props.turns,
292
+ kind,
293
+ toolFilter,
294
+ })
295
+ }
296
+
297
+ const openCharts = () =>
298
+ openPerfCharts(props.api, props.colors, {
299
+ perf: props.perf,
300
+ currentSessionId: props.currentSessionId,
301
+ })
302
+
303
+ const liveLabel = () => {
304
+ const dir = props.livePhase
305
+ if (!dir) return null
306
+ const label = dir === FLOW_WAIT ? "wait" : dir === FLOW_RECV ? "recv" : "tool"
307
+ const fg = toneColor(directionGlyph(dir)!.tone, props.colors)
308
+ return { label, fg, ms: props.livePhaseMs }
309
+ }
310
+
311
+ return (
312
+ <box flexDirection="column" gap={1}>
313
+ <Show when={liveLabel()}>
314
+ {(live) => (
315
+ <box flexDirection="row">
316
+ <text fg={live().fg}>{`${spinnerFrame(props.glyphFrame())} ${live().label}`}</text>
317
+ <text fg={props.colors.textMuted}>
318
+ {live().ms != null ? ` ${formatDuration(live().ms)}` : ""}
319
+ </text>
320
+ </box>
321
+ )}
322
+ </Show>
323
+
324
+ <Show when={props.perf.totals.turns > 0} fallback={null}>
325
+ <box flexDirection="column" gap={1}>
326
+ <FoldSection
327
+ title="Models"
328
+ open={foldModels.open()}
329
+ count={props.perf.models.length}
330
+ suffix={tokenSummary(totals())}
331
+ colors={props.colors}
332
+ onToggle={foldModels.toggle}
333
+ onDetail={() => openLog(PERF_LOG_KIND_MODELS)}
334
+ >
335
+ <RowList
336
+ items={props.perf.models}
337
+ budget={props.rows + modelsReveal.more()}
338
+ colors={props.colors}
339
+ renderItem={(m) => (
340
+ <ModelRow
341
+ model={m}
342
+ colors={props.colors}
343
+ lineMax={props.lineMax}
344
+ glyphFrame={props.glyphFrame}
345
+ live={Boolean(props.livePhase) && m === props.perf.models[0]}
346
+ />
347
+ )}
348
+ more={{ onReveal: modelsReveal.reveal }}
349
+ />
350
+ </FoldSection>
351
+
352
+ <FoldSection
353
+ title="Time"
354
+ open={foldTime.open()}
355
+ suffix={timeSummary({
356
+ turns: totals().turns,
357
+ durationMs: activeTimeMs(),
358
+ errors: totals().errors,
359
+ aborts: totals().aborts,
360
+ })}
361
+ colors={props.colors}
362
+ onToggle={foldTime.toggle}
363
+ onDetail={() => openLog(PERF_LOG_KIND_TIME)}
364
+ >
365
+ <For each={phases()}>
366
+ {(p) => (
367
+ <PhaseRow
368
+ spec={p.spec}
369
+ label={p.label}
370
+ ms={p.ms}
371
+ share={p.ms / wall()}
372
+ colors={props.colors}
373
+ lineMax={props.lineMax}
374
+ onSelect={() => openLog(p.key)}
375
+ />
376
+ )}
377
+ </For>
378
+ </FoldSection>
379
+
380
+ <Show when={props.perf.tools.length > 0}>
381
+ <FoldSection
382
+ title="Slow tools"
383
+ open={foldTools.open()}
384
+ count={props.perf.tools.length}
385
+ suffix={formatSpan(toolTotal())}
386
+ colors={props.colors}
387
+ onToggle={foldTools.toggle}
388
+ onDetail={() => openLog(PERF_PHASE_TOOL)}
389
+ >
390
+ <RowList
391
+ items={props.perf.tools}
392
+ budget={props.rows + toolsReveal.more()}
393
+ colors={props.colors}
394
+ renderItem={(t) => (
395
+ <ToolRow
396
+ tool={t}
397
+ share={toolTotal() > 0 ? t.totalMs / toolTotal() : null}
398
+ colors={props.colors}
399
+ lineMax={props.lineMax}
400
+ onSelect={() => openLog(PERF_PHASE_TOOL, t.name)}
401
+ />
402
+ )}
403
+ more={{ onReveal: toolsReveal.reveal }}
404
+ />
405
+ </FoldSection>
406
+ </Show>
407
+
408
+ <Show when={hasTrend()}>
409
+ <FoldSection
410
+ title="Trend"
411
+ open={foldTrend.open()}
412
+ count={props.perf.trend.length}
413
+ colors={props.colors}
414
+ onToggle={foldTrend.toggle}
415
+ onDetail={() => openCharts()}
416
+ >
417
+ <TrendChart
418
+ label={PERF_PHASE_WAIT}
419
+ values={trendWait()}
420
+ fg={props.colors.warning || props.colors.text}
421
+ colors={props.colors}
422
+ lineMax={props.lineMax}
423
+ />
424
+ <TrendChart
425
+ label="tok/s"
426
+ values={trendRate()}
427
+ fg={props.colors.success}
428
+ colors={props.colors}
429
+ lineMax={props.lineMax}
430
+ />
431
+ </FoldSection>
432
+ </Show>
433
+
434
+ <Show when={props.perf.history.length > 0}>
435
+ <FoldSection
436
+ title="History"
437
+ open={foldHistory.open()}
438
+ count={props.perf.history.length}
439
+ colors={props.colors}
440
+ onToggle={foldHistory.toggle}
441
+ >
442
+ <RowList
443
+ items={props.perf.history}
444
+ budget={props.rows + historyReveal.more()}
445
+ colors={props.colors}
446
+ renderItem={(row) => (
447
+ <HistoryRow
448
+ row={row}
449
+ colors={props.colors}
450
+ lineMax={props.lineMax}
451
+ current={row.id === props.currentSessionId}
452
+ onSelect={() => props.onSelect(row.id)}
453
+ />
454
+ )}
455
+ more={{ onReveal: historyReveal.reveal }}
456
+ />
457
+ </FoldSection>
458
+ </Show>
459
+ </box>
460
+ </Show>
461
+ </box>
462
+ )
463
+ }