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,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
|
+
}
|