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.
Files changed (105) hide show
  1. package/.oesignore +17 -0
  2. package/CHANGELOG.md +95 -0
  3. package/LICENSE +21 -0
  4. package/README.md +235 -0
  5. package/oes.json +11 -0
  6. package/package.json +100 -0
  7. package/scripts/install-git-hooks.mjs +29 -0
  8. package/src/pware.oc.core/constants/index.ts +18 -0
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
  19. package/src/pware.oc.core/git/index.ts +7 -0
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
  22. package/src/pware.oc.core/index.ts +18 -0
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
  34. package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
  35. package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
  36. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
  37. package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
  38. package/src/pware.oc.omo/constants/index.ts +15 -0
  39. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
  40. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
  47. package/src/pware.oc.omo/index.ts +7 -0
  48. package/src/pware.oc.omo/resolver/index.ts +19 -0
  49. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
  50. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
  62. package/src/pware.oc.opencode/constants/index.ts +10 -0
  63. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
  64. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
  67. package/src/pware.oc.opencode/index.ts +8 -0
  68. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
  69. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
  70. package/src/pware.oc.opencode/resolver/index.ts +170 -0
  71. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
  72. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
  76. package/src/pware.oc.perf/index.ts +14 -0
  77. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
  78. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
  79. package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
  80. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
  81. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
  82. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
  85. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
  86. package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
  87. package/src/pware.oc.runtime/index.ts +13 -0
  88. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
  89. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
  90. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
  91. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
  92. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
  93. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
  95. package/src/pware.oc.runtime/resolver/index.ts +224 -0
  96. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
  97. package/src/pware.oc.ui/index.ts +12 -0
  98. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
  99. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
  100. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
  101. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
  102. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
  103. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
  104. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
  105. package/src/pware.oc.ui.tsx +98 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * pware.oc.perf.realtime
3
+ *
4
+ * Real-time metric samples from the live stream. One `StatRealtimeSnapshot`
5
+ * carries the leaf series for every category the OES realtime chart renders
6
+ * (Tokens / Cache / CPU·RAM / Network); sums are derived via `sumSeries`.
7
+ * `StatRealtimeSnapshotHistory` is that sample retained in the rolling RAM
8
+ * history (see `pware.oc.perf.realtimeTimeline.ts`). Pure and host-free.
9
+ */
10
+ import { REALTIME_WINDOW_MS } from "../pware.oc.core/pware.oc.core.timing.js"
11
+
12
+ /** Token series — tok/s. `out` is visible output; `reasoning` is hidden thinking. */
13
+ export type StatRealtimeTokensSeries = {
14
+ in: number | null
15
+ out: number | null
16
+ reasoning: number | null
17
+ }
18
+
19
+ /** Token-cache series — tok/s served from / written to cache. */
20
+ export type StatRealtimeCacheSeries = {
21
+ read: number | null
22
+ write: number | null
23
+ }
24
+
25
+ /** CPU/RAM series — cpu as %, ram as MB. */
26
+ export type StatRealtimeCpuRamSeries = {
27
+ cpu: number | null
28
+ ram: number | null
29
+ }
30
+
31
+ /**
32
+ * Network series — kbit/s, estimated from the token flow (≈ `NETWORK_BYTES_PER_TOKEN`
33
+ * per token), not a real socket meter (model traffic lives in the server process).
34
+ * Network convention: `in` is data the machine receives (model output + reasoning),
35
+ * `out` is data the machine sends (input + cache). Null when idle.
36
+ */
37
+ export type StatRealtimeNetworkSeries = {
38
+ in: number | null
39
+ out: number | null
40
+ }
41
+
42
+ /** Rough bytes-per-token used to turn a token rate into a kbit/s estimate. */
43
+ export const NETWORK_BYTES_PER_TOKEN = 4
44
+
45
+ /** Token rate (tok/s) → kbit/s at `NETWORK_BYTES_PER_TOKEN`. Null stays null. */
46
+ export function tokenRateToKbit(tokPerSec: number | null): number | null {
47
+ if (tokPerSec == null) return null
48
+ return (tokPerSec * NETWORK_BYTES_PER_TOKEN * 8) / 1000
49
+ }
50
+
51
+ /** One point-in-time realtime sample across all categories. Rates are null when idle. */
52
+ export type StatRealtimeSnapshot = {
53
+ at: number
54
+ tokens: StatRealtimeTokensSeries
55
+ cache: StatRealtimeCacheSeries
56
+ cpuRam: StatRealtimeCpuRamSeries
57
+ network: StatRealtimeNetworkSeries
58
+ }
59
+
60
+ /** A realtime sample retained in the rolling history — the same shape as a snapshot. */
61
+ export type StatRealtimeSnapshotHistory = StatRealtimeSnapshot
62
+
63
+ /** Sum two nullable rates: null only when both are null. */
64
+ export function sumSeries(a: number | null, b: number | null): number | null {
65
+ if (a == null && b == null) return null
66
+ return (a ?? 0) + (b ?? 0)
67
+ }
68
+
69
+ /** An idle sample at `at` — every series null. */
70
+ export function emptyStatRealtimeSnapshot(at = Date.now()): StatRealtimeSnapshot {
71
+ return {
72
+ at,
73
+ tokens: { in: null, out: null, reasoning: null },
74
+ cache: { read: null, write: null },
75
+ cpuRam: { cpu: null, ram: null },
76
+ network: { in: null, out: null },
77
+ }
78
+ }
79
+
80
+ /** Append a sample and drop entries older than the window (immutable). */
81
+ export function pushStatRealtimeHistory(
82
+ history: readonly StatRealtimeSnapshotHistory[],
83
+ snapshot: StatRealtimeSnapshotHistory,
84
+ windowMs = REALTIME_WINDOW_MS,
85
+ ): StatRealtimeSnapshotHistory[] {
86
+ const next = [...history, snapshot]
87
+ return next.filter((s) => snapshot.at - s.at <= windowMs)
88
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * pware.oc.perf.realtimeBlock
3
+ *
4
+ * The static definition of the OES realtime widget: which category tabs exist,
5
+ * which selector rows each tab offers, and how each row reads its series from a
6
+ * `StatRealtimeSnapshot`. This is a pure, host-free model — the TUI layer
7
+ * renders it and owns the runtime state (active tab, fullscreen).
8
+ */
9
+ import { sumSeries, type StatRealtimeSnapshot, type StatRealtimeSnapshotHistory } from "./pware.oc.perf.realtime.js"
10
+
11
+ /** A selector row's series key — unique within its tab. */
12
+ export type StatRealtimeSeriesKey =
13
+ | "avg"
14
+ | "sum"
15
+ | "in"
16
+ | "out"
17
+ | "reasoning"
18
+ | "read"
19
+ | "write"
20
+ | "cpu"
21
+ | "ram"
22
+ | "tok/s"
23
+
24
+ /** One selector row: its key, label, unit, and how to read the series from a sample. */
25
+ export type StatRealtimeRowTab = {
26
+ key: StatRealtimeSeriesKey
27
+ label: string
28
+ unit: string
29
+ read: (snap: StatRealtimeSnapshot) => number | null
30
+ }
31
+
32
+ /** A category tab id. */
33
+ export type StatRealtimeTabId = "tokens" | "cache" | "cpu-ram" | "network"
34
+ /** One category tab: its id, label, unit, and selector rows. */
35
+ export type StatRealtimeTab = {
36
+ id: StatRealtimeTabId
37
+ label: string
38
+ unit: string
39
+ rows: readonly StatRealtimeRowTab[]
40
+ }
41
+
42
+ /** The whole realtime widget definition: the tabs the selector offers. */
43
+ export type StatRealtimeBlock = {
44
+ tabs: readonly StatRealtimeTab[]
45
+ }
46
+
47
+ /** Extract one selector row's series across history as a chartable `number[]` (null → 0). */
48
+ export function seriesValues(
49
+ history: readonly StatRealtimeSnapshotHistory[],
50
+ read: (snap: StatRealtimeSnapshot) => number | null,
51
+ ): number[] {
52
+ return history.map((s) => read(s) ?? 0)
53
+ }
54
+
55
+ /** The tab for `id`, or the first tab if `id` is unknown. */
56
+ export function realtimeTab(
57
+ tabs: readonly StatRealtimeTab[],
58
+ id: StatRealtimeTabId,
59
+ ): StatRealtimeTab {
60
+ return tabs.find((t) => t.id === id) ?? tabs[0]!
61
+ }
62
+
63
+ /** The selector row for `key` on `tab`, or the tab's first row if `key` is missing. */
64
+ export function realtimeRow(
65
+ tab: StatRealtimeTab,
66
+ key: StatRealtimeSeriesKey,
67
+ ): StatRealtimeRowTab {
68
+ return tab.rows.find((r) => r.key === key) ?? tab.rows[0]!
69
+ }
70
+
71
+ /** The default OES realtime block. Network is estimated from the token flow (kbit/s). */
72
+ export const STAT_REALTIME_BLOCK: StatRealtimeBlock = {
73
+ tabs: [
74
+ {
75
+ id: "tokens",
76
+ label: "Tok",
77
+ unit: "tok/s",
78
+ rows: [
79
+ // avg is the OES bar's old live reading — the chart smooths this series.
80
+ { key: "avg", label: "avg", unit: "tok/s", read: (s) => sumSeries(s.tokens.in, s.tokens.out) },
81
+ { key: "sum", label: "sum", unit: "tok/s", read: (s) => sumSeries(s.tokens.in, s.tokens.out) },
82
+ { key: "in", label: "in", unit: "tok/s", read: (s) => s.tokens.in },
83
+ { key: "out", label: "out", unit: "tok/s", read: (s) => s.tokens.out },
84
+ // tok/s duplicates sum (in+out) — kept as the bar's old label per UX.
85
+ { key: "tok/s", label: "tok/s", unit: "tok/s", read: (s) => sumSeries(s.tokens.in, s.tokens.out) },
86
+ ],
87
+ },
88
+ {
89
+ id: "cache",
90
+ label: "Cache",
91
+ unit: "tok/s",
92
+ rows: [
93
+ { key: "sum", label: "sum", unit: "tok/s", read: (s) => sumSeries(s.cache.read, s.cache.write) },
94
+ { key: "read", label: "read", unit: "tok/s", read: (s) => s.cache.read },
95
+ { key: "write", label: "write", unit: "tok/s", read: (s) => s.cache.write },
96
+ ],
97
+ },
98
+ {
99
+ id: "cpu-ram",
100
+ label: "Proc",
101
+ unit: "%/MB",
102
+ rows: [
103
+ { key: "cpu", label: "cpu", unit: "%", read: (s) => s.cpuRam.cpu },
104
+ { key: "ram", label: "ram", unit: "MB", read: (s) => s.cpuRam.ram },
105
+ ],
106
+ },
107
+ {
108
+ id: "network",
109
+ label: "Net",
110
+ unit: "kbit/s",
111
+ rows: [
112
+ { key: "in", label: "in", unit: "kbit/s", read: (s) => s.network.in },
113
+ { key: "out", label: "out", unit: "kbit/s", read: (s) => s.network.out },
114
+ { key: "sum", label: "sum", unit: "kbit/s", read: (s) => sumSeries(s.network.in, s.network.out) },
115
+ ],
116
+ },
117
+ ],
118
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * pware.oc.perf.realtimeCpuRam
3
+ *
4
+ * Process-level CPU/RAM reading for the OES realtime block. CPU% is the
5
+ * OpenCode process's load across all cores; RAM is its resident set. Both come
6
+ * from Node/Bun built-ins (`process.cpuUsage`, `process.memoryUsage`,
7
+ * `os.cpus`), so they work identically on Linux, macOS and Windows with no
8
+ * dependencies. The sampling math is pure and exported for tests; the host
9
+ * read helpers (`readCpuRam`, `cpuCores`) are thin edges the UI tick calls.
10
+ */
11
+ import os from "node:os"
12
+
13
+ /** A raw CPU/RAM reading: cumulative CPU µs (user/system) + resident bytes. */
14
+ export type CpuRamReading = {
15
+ user: number
16
+ system: number
17
+ rss: number
18
+ }
19
+
20
+ /** One raw reading from the host: process CPU µs since start + resident bytes. Never throws. */
21
+ export function readCpuRam(): CpuRamReading {
22
+ try {
23
+ const cpu = process.cpuUsage()
24
+ return { user: cpu.user, system: cpu.system, rss: process.memoryUsage().rss }
25
+ } catch {
26
+ return { user: 0, system: 0, rss: 0 }
27
+ }
28
+ }
29
+
30
+ /** Logical CPU count — divides CPU% so a full core is never above 100. */
31
+ export function cpuCores(): number {
32
+ return os.cpus().length
33
+ }
34
+
35
+ /** A reading stamped with when it was taken. */
36
+ export type CpuRamStampedReading = CpuRamReading & { at: number }
37
+
38
+ /** Process CPU% over an interval, relative to `cores` cores. Null when unmeasurable. */
39
+ export function cpuPercent(
40
+ prev: CpuRamReading,
41
+ next: CpuRamReading,
42
+ wallMs: number,
43
+ cores: number,
44
+ ): number | null {
45
+ if (wallMs <= 0 || cores <= 0) return null
46
+ const usedMs = (next.user - prev.user + next.system - prev.system) / 1000
47
+ return (usedMs / wallMs / cores) * 100
48
+ }
49
+
50
+ /**
51
+ * CPU% across a rolling window of stamped readings (oldest first): the delta
52
+ * between the newest and the first reading still inside the window, over their
53
+ * wall span. Null while fewer than two readings fit. This is what damps the
54
+ * fine sampling cadence into a stable utilisation number.
55
+ */
56
+ export function cpuPercentOverWindow(
57
+ readings: readonly CpuRamStampedReading[],
58
+ cores: number,
59
+ windowMs: number,
60
+ ): number | null {
61
+ if (cores <= 0 || windowMs <= 0 || readings.length < 2) return null
62
+ const last = readings[readings.length - 1]!
63
+ const minAt = last.at - windowMs
64
+ let first = readings[0]!
65
+ for (let i = 0; i < readings.length; i += 1) {
66
+ if (readings[i]!.at >= minAt) {
67
+ first = readings[i]!
68
+ break
69
+ }
70
+ }
71
+ const wall = last.at - first.at
72
+ if (wall <= 0) return null
73
+ return cpuPercent(first, last, wall, cores)
74
+ }
75
+
76
+ /** Resident bytes → MB. */
77
+ export function ramMb(rssBytes: number): number {
78
+ return rssBytes / (1024 * 1024)
79
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * pware.oc.perf.realtimeSampler
3
+ *
4
+ * Event-driven sampler: subscribes to `session.updated` events, extracts the
5
+ * per-session cumulative token totals, and feeds them to a
6
+ * `StatRealtimeTimeline`. The subscription is injected so the class stays
7
+ * host-free; the event parsing is a pure exported function.
8
+ */
9
+ import type { StatRealtimeEventTokens, StatRealtimeTimeline } from "./pware.oc.perf.realtimeTimeline.js"
10
+
11
+ /** Subscribe to realtime events: returns a dispose function. */
12
+ export type RealtimeEventSubscribe = (handler: (evt: unknown) => void) => () => void
13
+
14
+ /** Pull `sessionID` + cumulative `tokens` out of a `session.updated` event; null otherwise. */
15
+ export function extractSessionTokens(evt: unknown): { sessionId: string; tokens: StatRealtimeEventTokens } | null {
16
+ if (!evt || typeof evt !== "object") return null
17
+ const o = evt as Record<string, unknown>
18
+ const props = (
19
+ o.properties && typeof o.properties === "object" ? o.properties : o
20
+ ) as Record<string, unknown>
21
+ const sessionId = props.sessionID ?? props.sessionId ?? props.session_id
22
+ const info = props.info as Record<string, unknown> | undefined
23
+ const tokens = info && typeof info === "object" ? (info.tokens as Record<string, unknown> | undefined) : undefined
24
+ if (typeof sessionId !== "string" || !sessionId || !tokens || typeof tokens !== "object") return null
25
+ const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0)
26
+ const cache = (
27
+ tokens.cache && typeof tokens.cache === "object" ? tokens.cache : {}
28
+ ) as Record<string, unknown>
29
+ return {
30
+ sessionId,
31
+ tokens: {
32
+ input: num(tokens.input),
33
+ output: num(tokens.output),
34
+ reasoning: num(tokens.reasoning),
35
+ cacheRead: num(cache.read),
36
+ cacheWrite: num(cache.write),
37
+ },
38
+ }
39
+ }
40
+
41
+ export class EventDriverSampler {
42
+ static create(
43
+ timeline: StatRealtimeTimeline,
44
+ subscribe: RealtimeEventSubscribe,
45
+ now: () => number = Date.now,
46
+ ): EventDriverSampler {
47
+ return new EventDriverSampler(timeline, subscribe, now)
48
+ }
49
+
50
+ private off: (() => void) | null = null
51
+
52
+ private constructor(
53
+ private readonly timeline: StatRealtimeTimeline,
54
+ private readonly subscribe: RealtimeEventSubscribe,
55
+ private readonly now: () => number,
56
+ ) {}
57
+
58
+ start(): void {
59
+ if (this.off) return
60
+ this.off = this.subscribe((evt) => {
61
+ const hit = extractSessionTokens(evt)
62
+ if (!hit) return
63
+ this.timeline.ingest(hit.sessionId, hit.tokens, this.now())
64
+ })
65
+ }
66
+
67
+ stop(): void {
68
+ this.off?.()
69
+ this.off = null
70
+ }
71
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * pware.oc.perf.realtimeTimeline
3
+ *
4
+ * One shared realtime history for every category the OES realtime chart
5
+ * renders (Tokens / Cache / CPU·RAM / Network). A single wall-clock loop calls
6
+ * `tick(at)` on the UI heartbeat cadence (`TICK_MS`); every tick folds the
7
+ * current per-session cumulative token state and the raw CPU ring into one grid
8
+ * sample, so the chart's right edge always reaches "now" instead of waiting for
9
+ * a host event. Pure state + injectable timestamps — no host, no database.
10
+ *
11
+ * Token/cache rates are a windowed derivative over the trailing
12
+ * `REALTIME_RATE_WINDOW_MS`: `ingest` (exact `session.updated` totals) and
13
+ * `ingestEstimate` (estimated stream deltas for out/reasoning) record level
14
+ * changes, and `tick` reports the average over the window — it falls back to
15
+ * zero when nothing new arrives instead of holding a stale rate. Estimated
16
+ * out/reasoning deltas are reconciled against the exact totals on every
17
+ * `session.updated`, so a streamed token is never counted twice: the exact
18
+ * delta only adds what the estimate has not covered yet.
19
+ */
20
+ import { cpuCores, cpuPercentOverWindow, ramMb, type CpuRamReading } from "./pware.oc.perf.realtimeCpuRam.js"
21
+ import {
22
+ REALTIME_RATE_WINDOW_MS,
23
+ REALTIME_WINDOW_MS,
24
+ } from "../pware.oc.core/pware.oc.core.timing.js"
25
+ import {
26
+ emptyStatRealtimeSnapshot,
27
+ pushStatRealtimeHistory,
28
+ sumSeries,
29
+ tokenRateToKbit,
30
+ type StatRealtimeSnapshot,
31
+ type StatRealtimeSnapshotHistory,
32
+ } from "./pware.oc.perf.realtime.js"
33
+
34
+ /** Cumulative token totals as reported by one `session.updated` event. */
35
+ export type StatRealtimeEventTokens = {
36
+ input: number
37
+ output: number
38
+ reasoning: number
39
+ cacheRead: number
40
+ cacheWrite: number
41
+ }
42
+
43
+ /** Which token channel an estimated stream delta feeds. */
44
+ export type StatRealtimeEstKind = "out" | "reasoning"
45
+
46
+ /** A single cumulative-token or streamed-token level change at `at`. */
47
+ type Step = { at: number; n: number }
48
+
49
+ /** Per-session state: last exact totals + stream estimates not yet confirmed. */
50
+ type SessionState = {
51
+ exact: StatRealtimeEventTokens | null
52
+ estOut: number
53
+ estReasoning: number
54
+ lastAt: number
55
+ }
56
+
57
+ /** Channel names that map 1:1 onto a `StatRealtimeSnapshot` leaf. */
58
+ type TokenChannel = "in" | "out" | "reasoning" | "cacheRead" | "cacheWrite"
59
+
60
+ const TOKEN_CHANNELS: readonly TokenChannel[] = ["in", "out", "reasoning", "cacheRead", "cacheWrite"]
61
+
62
+ function pos(n: number): number {
63
+ return Number.isFinite(n) && n > 0 ? n : 0
64
+ }
65
+
66
+ export class StatRealtimeTimeline {
67
+ static build(scope: string | null = null, cores: number = cpuCores()): StatRealtimeTimeline {
68
+ return new StatRealtimeTimeline(scope, cores)
69
+ }
70
+
71
+ private readonly scope: string | null
72
+ private readonly cores: number
73
+ private readonly bySession = new Map<string, SessionState>()
74
+ private readonly steps = new Map<TokenChannel, Step[]>()
75
+ private cpuReadings: Array<CpuRamReading & { at: number }> = []
76
+ private history: StatRealtimeSnapshotHistory[] = []
77
+
78
+ private constructor(scope: string | null, cores: number) {
79
+ this.scope = scope
80
+ this.cores = cores
81
+ for (const c of TOKEN_CHANNELS) this.steps.set(c, [])
82
+ }
83
+
84
+ /** Exact cumulative totals from one `session.updated` event. */
85
+ ingest(sessionId: string, totals: StatRealtimeEventTokens, at: number): void {
86
+ if (this.scope !== null && sessionId !== this.scope) return
87
+ const st = this.bySession.get(sessionId) ?? {
88
+ exact: null,
89
+ estOut: 0,
90
+ estReasoning: 0,
91
+ lastAt: 0,
92
+ }
93
+ st.lastAt = at
94
+ this.bySession.set(sessionId, st)
95
+
96
+ const prev = st.exact
97
+ if (!prev) {
98
+ // First report is the baseline: estimates seen before it are already
99
+ // inside the reported cumulative, so they must not suppress the first
100
+ // post-baseline delta.
101
+ st.exact = totals
102
+ st.estOut = 0
103
+ st.estReasoning = 0
104
+ return
105
+ }
106
+ this.push("in", totals.input - prev.input, at)
107
+ this.push("cacheRead", totals.cacheRead - prev.cacheRead, at)
108
+ this.push("cacheWrite", totals.cacheWrite - prev.cacheWrite, at)
109
+ this.rebase("out", "estOut", prev.output, totals.output, st, at)
110
+ this.rebase("reasoning", "estReasoning", prev.reasoning, totals.reasoning, st, at)
111
+ st.exact = totals
112
+ }
113
+
114
+ /** Estimated streamed delta (text → out, thinking → reasoning). */
115
+ ingestEstimate(sessionId: string, kind: StatRealtimeEstKind, n: number, at: number): void {
116
+ if (this.scope !== null && sessionId !== this.scope) return
117
+ const amount = pos(n)
118
+ if (amount === 0) return
119
+ this.push(kind, amount, at)
120
+ const st = this.bySession.get(sessionId) ?? {
121
+ exact: null,
122
+ estOut: 0,
123
+ estReasoning: 0,
124
+ lastAt: 0,
125
+ }
126
+ st.lastAt = at
127
+ if (kind === "out") st.estOut += amount
128
+ else st.estReasoning += amount
129
+ this.bySession.set(sessionId, st)
130
+ }
131
+
132
+ /** A raw process CPU/RAM reading, taken on the same cadence as `tick`. */
133
+ ingestCpuRam(raw: CpuRamReading, at: number): void {
134
+ this.cpuReadings.push({ ...raw, at })
135
+ }
136
+
137
+ /** Advance the grid: fold all state into one sample at `at`, prune stale state. */
138
+ tick(at: number): void {
139
+ const cut = at - REALTIME_RATE_WINDOW_MS
140
+ const snap = emptyStatRealtimeSnapshot(at)
141
+
142
+ for (const c of TOKEN_CHANNELS) {
143
+ const kept: Step[] = []
144
+ let sum = 0
145
+ for (const s of this.steps.get(c)!) {
146
+ if (s.at <= cut) continue
147
+ kept.push(s)
148
+ sum += s.n
149
+ }
150
+ this.steps.set(c, kept)
151
+ this.setChannel(snap, c, sum === 0 ? null : (sum / REALTIME_RATE_WINDOW_MS) * 1000)
152
+ }
153
+
154
+ // Network is estimated from the same token flow: what the session sends to
155
+ // the model (input + cache) is `out`, what streams back (output + reasoning)
156
+ // is `in`. Real socket bytes never reach this plugin (model traffic lives in
157
+ // the server process), so kbit/s here means "≈ tokens × 4 bytes", not a meter.
158
+ snap.network.out = tokenRateToKbit(sumSeries(snap.tokens.in, sumSeries(snap.cache.read, snap.cache.write)))
159
+ snap.network.in = tokenRateToKbit(sumSeries(snap.tokens.out, snap.tokens.reasoning))
160
+
161
+ this.cpuReadings = this.cpuReadings.filter((r) => r.at > cut)
162
+ snap.cpuRam.cpu = cpuPercentOverWindow(this.cpuReadings, this.cores, REALTIME_RATE_WINDOW_MS)
163
+ const last = this.cpuReadings[this.cpuReadings.length - 1]
164
+ snap.cpuRam.ram = last ? ramMb(last.rss) : null
165
+
166
+ this.history = pushStatRealtimeHistory(this.history, snap, REALTIME_WINDOW_MS)
167
+ // Drop est-only transient states once idle — a session with an exact
168
+ // baseline stays, so its first post-idle burst still diffs correctly.
169
+ const stale = at - REALTIME_WINDOW_MS
170
+ for (const [sid, st] of this.bySession) {
171
+ if (st.exact === null && st.lastAt < stale) this.bySession.delete(sid)
172
+ }
173
+ }
174
+
175
+ /** The rolling grid samples in time order — already on the fixed grid. */
176
+ getTimeline(): StatRealtimeSnapshotHistory[] {
177
+ return this.history
178
+ }
179
+
180
+ reset(): void {
181
+ this.bySession.clear()
182
+ for (const c of TOKEN_CHANNELS) this.steps.set(c, [])
183
+ this.cpuReadings = []
184
+ this.history = []
185
+ }
186
+
187
+ private push(channel: TokenChannel, n: number, at: number): void {
188
+ const amount = pos(n)
189
+ if (amount === 0) return
190
+ this.steps.get(channel)!.push({ at, n: amount })
191
+ }
192
+
193
+ private rebase(
194
+ channel: "out" | "reasoning",
195
+ estKey: "estOut" | "estReasoning",
196
+ prevVal: number,
197
+ nextVal: number,
198
+ st: SessionState,
199
+ at: number,
200
+ ): void {
201
+ const delta = pos(nextVal - prevVal)
202
+ const uncovered = Math.max(0, delta - st[estKey])
203
+ st[estKey] = 0
204
+ this.push(channel, uncovered, at)
205
+ }
206
+
207
+ private setChannel(snap: StatRealtimeSnapshot, c: TokenChannel, rate: number | null): void {
208
+ if (c === "in") snap.tokens.in = rate
209
+ else if (c === "out") snap.tokens.out = rate
210
+ else if (c === "reasoning") snap.tokens.reasoning = rate
211
+ else if (c === "cacheRead") snap.cache.read = rate
212
+ else snap.cache.write = rate
213
+ }
214
+ }