@foae/opencode-timings 0.1.1 → 0.1.2

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 (3) hide show
  1. package/README.md +16 -11
  2. package/package.json +1 -1
  3. package/src/tui.tsx +40 -29
package/README.md CHANGED
@@ -11,20 +11,25 @@ context-window pollution**.
11
11
 
12
12
  ```
13
13
  Timing
14
- API ███████░░░ 54%
15
- 6m31s / 12m04s
16
- turns 18 · avg 21s
17
- ▁▂▅█▃▂▄▂▁▃ slow 1m12s
14
+ api/wall ██████░░ 78%
15
+ api 31s · wall 40s
16
+ turns 4 · avg 8s
17
+ slowest 19s
18
+ per-turn ▄█▂▂
18
19
  ```
19
20
 
21
+ Every row names itself, so there are no unlabeled numbers or glyphs to decode.
22
+
20
23
  ## Metrics
21
24
 
22
- | Row | Meaning |
23
- |---------|---------|
24
- | `API` | Total assistant inference time — the sum of `time.completed time.created` over every completed assistant message and its share of wall-clock. |
25
- | `wall` | Span from the first to the last message timestamp. Includes the time you spend reading/typing between turns, so `API` is always a fraction of it. |
26
- | `turns` | Number of completed assistant messages, plus the average per-turn duration. |
27
- | `slow` | The single slowest assistant message. |
25
+ | Row | Meaning |
26
+ |------------|---------|
27
+ | `api/wall` | How much of wall-clock was actual model inference, as a bar gauge and percent. |
28
+ | `api` | Total assistant inference time the sum of `time.completed time.created` over every completed assistant message. |
29
+ | `wall` | Span from the first to the last message timestamp. Includes the time you spend reading/typing between turns, so `api` is always a fraction of it. |
30
+ | `turns` | Number of completed assistant messages, plus the average per-turn duration. |
31
+ | `slowest` | The single slowest assistant message. |
32
+ | `per-turn` | Sparkline of each recent turn's duration. |
28
33
 
29
34
  The panel is always shown; before the first turn its values read zero.
30
35
 
@@ -44,7 +49,7 @@ belongs in `tui.json`, not `opencode.json`:
44
49
  OpenCode installs the plugin and its dependencies with Bun at startup. Restart
45
50
  OpenCode and open the session sidebar to see the `Timing` panel.
46
51
 
47
- You can also pin a version, e.g. `@foae/opencode-timings@0.1.1`.
52
+ You can also pin a version, e.g. `@foae/opencode-timings@0.1.2`.
48
53
 
49
54
  ## Configuration
50
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foae/opencode-timings",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "OpenCode TUI sidebar panel showing per-session timing: total API/inference time, wall-clock, turns, average and slowest turn. Zero context-window pollution.",
5
5
  "license": "MIT",
6
6
  "author": "foae",
package/src/tui.tsx CHANGED
@@ -7,13 +7,24 @@
7
7
  * state. Nothing is injected into the message stream, so there is zero
8
8
  * context-window pollution.
9
9
  *
10
+ * Fancy layout — every value names itself, all rows in the muted tone:
11
+ *
12
+ * Timing
13
+ * api/wall ███████░ 78% gauge: how much of wall-clock was model inference
14
+ * api 31s · wall 40s the two raw times behind that ratio
15
+ * turns 4 · avg 8s completed assistant turns and their average
16
+ * slowest 19s the single slowest turn
17
+ * per-turn ▄█▂▂ sparkline of each recent turn's duration
18
+ *
10
19
  * Metrics (per session):
11
- * - API total assistant inference time = sum of (time.completed - time.created)
12
- * over every completed assistant message, with its share of wall-clock.
13
- * - wall span from the first to the last message timestamp (includes the time
14
- * you spend reading/typing between turns).
15
- * - turns number of completed assistant messages, plus the average duration.
16
- * - slow the single slowest assistant message.
20
+ * - api total assistant inference time = sum of (time.completed - time.created)
21
+ * over every completed assistant message.
22
+ * - wall span from the first to the last message timestamp (includes the time
23
+ * you spend reading/typing between turns), so api is a fraction of it.
24
+ * - api/wall api's share of wall-clock, as a bar gauge and percent.
25
+ * - turns number of completed assistant messages, plus the average duration.
26
+ * - slowest the single slowest assistant message.
27
+ * - per-turn sparkline of recent per-turn durations.
17
28
  *
18
29
  * Config — pass options via the tuple form in `tui.json`:
19
30
  *
@@ -27,7 +38,7 @@
27
38
  * }]
28
39
  * ]
29
40
  *
30
- * "fancy" draws a bar gauge for the API/wall ratio and a sparkline of recent
41
+ * "fancy" draws a bar gauge for the api/wall ratio and a sparkline of recent
31
42
  * turn durations; "simple" is plain labeled rows. The panel is always shown
32
43
  * (values read zero before the first turn).
33
44
  */
@@ -45,7 +56,9 @@ const SIDEBAR_ORDER = 155
45
56
  // drive updates; this interval is just a low-frequency backstop.
46
57
  const REFRESH_INTERVAL_MS = 15_000
47
58
 
48
- const BAR_WIDTH = 10
59
+ // Narrower than the simple-mode rows so the "api/wall" label + gauge + percent
60
+ // fit one sidebar line without wrapping.
61
+ const BAR_WIDTH = 8
49
62
  const SPARK_POINTS = 12
50
63
  const SPARK_LEVELS = "▁▂▃▄▅▆▇█"
51
64
 
@@ -141,9 +154,6 @@ function sparkline(values: number[]): string {
141
154
  .join("")
142
155
  }
143
156
 
144
- type Tone = "muted" | "accent"
145
- type Row = { text: string; tone: Tone }
146
-
147
157
  function turnsLine(t: Timing, fields: Fields): string {
148
158
  if (fields.turns && fields.avg) return `turns ${t.turns} · avg ${fmtDuration(t.avgMs)}`
149
159
  if (fields.turns) return `turns ${t.turns}`
@@ -151,23 +161,26 @@ function turnsLine(t: Timing, fields: Fields): string {
151
161
  return ""
152
162
  }
153
163
 
154
- function buildRows(t: Timing, mode: Mode, fields: Fields): Row[] {
155
- const rows: Row[] = []
164
+ // Every row is a labeled string in the muted tone — no value relies on the
165
+ // reader inferring what an unlabeled number or glyph means.
166
+ function buildRows(t: Timing, mode: Mode, fields: Fields): string[] {
167
+ const rows: string[] = []
156
168
  if (mode === "fancy") {
157
- if (fields.api) rows.push({ text: `API ${bar(t.apiPct, BAR_WIDTH)} ${t.apiPct}%`, tone: "accent" })
158
- if (fields.wall) rows.push({ text: `${fmtDuration(t.apiMs)} / ${fmtDuration(t.wallMs)}`, tone: "muted" })
169
+ if (fields.api) rows.push(`api/wall ${bar(t.apiPct, BAR_WIDTH)} ${t.apiPct}%`)
170
+ if (fields.wall) rows.push(`api ${fmtDuration(t.apiMs)} · wall ${fmtDuration(t.wallMs)}`)
159
171
  const ta = turnsLine(t, fields)
160
- if (ta) rows.push({ text: ta, tone: "muted" })
161
- const spark = fields.sparkline ? sparkline(t.durations) : ""
162
- const slow = fields.slow ? `slow ${fmtDuration(t.slowestMs)}` : ""
163
- const last = [spark, slow].filter(Boolean).join(" ")
164
- if (last) rows.push({ text: last, tone: "muted" })
172
+ if (ta) rows.push(ta)
173
+ if (fields.slow) rows.push(`slowest ${fmtDuration(t.slowestMs)}`)
174
+ if (fields.sparkline) {
175
+ const spark = sparkline(t.durations)
176
+ if (spark) rows.push(`per-turn ${spark}`)
177
+ }
165
178
  } else {
166
- if (fields.api) rows.push({ text: `API ${fmtDuration(t.apiMs)} ${t.apiPct}%`, tone: "muted" })
167
- if (fields.wall) rows.push({ text: `wall ${fmtDuration(t.wallMs)}`, tone: "muted" })
179
+ if (fields.api) rows.push(`api ${fmtDuration(t.apiMs)} · ${t.apiPct}% of wall`)
180
+ if (fields.wall) rows.push(`wall ${fmtDuration(t.wallMs)}`)
168
181
  const ta = turnsLine(t, fields)
169
- if (ta) rows.push({ text: ta, tone: "muted" })
170
- if (fields.slow) rows.push({ text: `slow ${fmtDuration(t.slowestMs)}`, tone: "muted" })
182
+ if (ta) rows.push(ta)
183
+ if (fields.slow) rows.push(`slowest ${fmtDuration(t.slowestMs)}`)
171
184
  }
172
185
  return rows
173
186
  }
@@ -208,9 +221,7 @@ function SidebarTimingView(props: {
208
221
  for (const unsubscribe of unsubscribers) unsubscribe()
209
222
  })
210
223
 
211
- const rows = (): Row[] => buildRows(timing(), props.mode, props.fields)
212
- const toneColor = (tone: Tone) =>
213
- tone === "accent" ? props.api.theme.current.accent : props.api.theme.current.textMuted
224
+ const rows = (): string[] => buildRows(timing(), props.mode, props.fields)
214
225
 
215
226
  return (
216
227
  <box gap={0}>
@@ -219,8 +230,8 @@ function SidebarTimingView(props: {
219
230
  </text>
220
231
  <box gap={0}>
221
232
  {rows().map((row) => (
222
- <text fg={toneColor(row.tone)} wrapMode="none">
223
- {row.text || " "}
233
+ <text fg={props.api.theme.current.textMuted} wrapMode="none">
234
+ {row || " "}
224
235
  </text>
225
236
  ))}
226
237
  </box>