opencode-top 3.3.0 → 3.3.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.
package/README.md CHANGED
@@ -1,35 +1,75 @@
1
- # OCMonitor
1
+ # opencode-top
2
2
 
3
- Monitor OpenCode AI coding sessions with a modern TUI.
3
+ Monitor your [OpenCode](https://opencode.ai) AI coding sessions in real-time token usage, costs, agent chains, tool calls, and more.
4
+
5
+ ![opencode-top TUI](https://raw.githubusercontent.com/Nielk74/ocmonitor-share/main/screenshot.png)
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- npm install
9
- npm run build
10
+ npm install -g opencode-top
10
11
  ```
11
12
 
12
13
  ## Usage
13
14
 
14
15
  ```bash
15
- npm run start live # Live monitoring dashboard
16
- npm run start sessions # List all sessions
16
+ opencode-top live # Live monitoring dashboard (default)
17
+ opencode-top sessions # Print session table and exit
17
18
  ```
18
19
 
19
- ### Keyboard shortcuts (live mode)
20
+ Requires OpenCode to have been run at least once (reads from `~/.local/share/opencode/opencode.db`).
20
21
 
21
- - `j/k` or arrows: Navigate
22
- - `e` or enter: Expand/collapse
23
- - `h/l`: Resize panes
24
- - `r`: Refresh
25
- - `q`: Quit
22
+ ## Screens
26
23
 
27
- ## Structure
24
+ | Key | Screen | Description |
25
+ |-----|--------|-------------|
26
+ | `1` | Sessions | Browse sessions and agent trees, view stats and messages |
27
+ | `2` | Tools | Tool usage analytics — call counts, error rates, avg duration |
28
+ | `3` | Overview | Aggregate stats, 7-day trends, hourly activity heatmap |
28
29
 
29
- ```
30
- src/
31
- ├── core/ # Domain models & business logic
32
- ├── data/ # SQLite loader, pricing data
33
- ├── ui/ # Ink components (React TUI)
34
- └── cli.ts # Entry point
30
+ ## Keyboard shortcuts
31
+
32
+ ### Sessions screen
33
+ | Key | Action |
34
+ |-----|--------|
35
+ | `j` / `k` | Navigate session list |
36
+ | `g` / `G` | Jump to top / bottom |
37
+ | `Tab` | Switch between Stats and Messages view |
38
+
39
+ ### Messages view
40
+ | Key | Action |
41
+ |-----|--------|
42
+ | `j` / `k` | Move cursor line by line |
43
+ | `d` / `u` | Scroll half page down / up |
44
+ | `g` / `G` | Jump to top / bottom |
45
+ | `Enter` | Expand / collapse tool call (shows input params + output) |
46
+ | `[` / `]` | Previous / next session |
47
+
48
+ ### Global
49
+ | Key | Action |
50
+ |-----|--------|
51
+ | `1` `2` `3` | Switch screens |
52
+ | `r` | Force refresh |
53
+ | `q` | Quit |
54
+
55
+ ## What you see
56
+
57
+ - **Session list** — title, date, token count, cost per session and sub-agent
58
+ - **Stats panel** — tokens, cost, duration, output rate, context usage, top tools, agent chain graph
59
+ - **Messages panel** — chronological tool calls with ✓/✗ status, duration, expand for full input/output; interaction headers show `↓in ↑out` token counts and cumulative token progress
60
+ - **Tools screen** — ranked tool list sortable by calls / failures / avg time
61
+ - **Overview screen** — cross-session totals, model breakdown, 7-day spark charts, hourly heatmap
62
+
63
+ ## Requirements
64
+
65
+ - Node.js >= 20
66
+ - OpenCode installed and used at least once
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ git clone https://github.com/Nielk74/ocmonitor-share
72
+ cd ocmonitor-share
73
+ npm install
74
+ npm start live # run from source with tsx
35
75
  ```
package/dist/cli.mjs CHANGED
@@ -21256,9 +21256,13 @@ var import_react27 = __toESM(require_react(), 1);
21256
21256
  var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1);
21257
21257
  function buildLines(session, contentWidth, expandedIds) {
21258
21258
  const lines = [];
21259
+ let cumTokens = 0;
21259
21260
  for (const interaction of session.interactions) {
21260
21261
  if (interaction.role !== "assistant") continue;
21262
+ cumTokens += interaction.tokens.input + interaction.tokens.cacheRead + interaction.tokens.output;
21261
21263
  const dur = interaction.time.completed && interaction.time.created ? formatDuration2(interaction.time.completed - interaction.time.created) : "";
21264
+ const tokensIn = interaction.tokens.input + interaction.tokens.cacheRead;
21265
+ const tokensOut = interaction.tokens.output;
21262
21266
  lines.push({
21263
21267
  id: `h-${interaction.id}`,
21264
21268
  kind: "header",
@@ -21266,8 +21270,9 @@ function buildLines(session, contentWidth, expandedIds) {
21266
21270
  agent: interaction.agent ?? null,
21267
21271
  duration: dur,
21268
21272
  time: formatTime(interaction.time.created),
21269
- tokensIn: interaction.tokens.input + interaction.tokens.cacheRead,
21270
- tokensOut: interaction.tokens.output
21273
+ tokensIn,
21274
+ tokensOut,
21275
+ cumTokens
21271
21276
  });
21272
21277
  for (const part of interaction.parts) {
21273
21278
  if (part.type === "tool") {
@@ -21286,24 +21291,25 @@ function buildLines(session, contentWidth, expandedIds) {
21286
21291
  name: truncate4(p.toolName, 18),
21287
21292
  title: p.title ? truncate4(p.title, 28) : "",
21288
21293
  right: exitStr + dur2,
21289
- expanded
21294
+ expanded,
21295
+ cumTokens
21290
21296
  });
21291
21297
  if (expanded) {
21292
21298
  const inputKeys = Object.keys(p.input);
21293
21299
  if (inputKeys.length > 0) {
21294
- lines.push({ id: `td-${p.callId}-in`, kind: "tool-detail", label: "input", value: "", isSection: true });
21300
+ lines.push({ id: `td-${p.callId}-in`, kind: "tool-detail", label: "input", value: "", isSection: true, cumTokens });
21295
21301
  for (const key of inputKeys) {
21296
21302
  const val = formatParamValue(p.input[key], contentWidth - key.length - 6);
21297
- lines.push({ id: `td-${p.callId}-in-${key}`, kind: "tool-detail", label: key, value: val, isSection: false });
21303
+ lines.push({ id: `td-${p.callId}-in-${key}`, kind: "tool-detail", label: key, value: val, isSection: false, cumTokens });
21298
21304
  }
21299
21305
  }
21300
21306
  if (p.output?.trim()) {
21301
- lines.push({ id: `td-${p.callId}-out`, kind: "tool-detail", label: "output", value: "", isSection: true });
21307
+ lines.push({ id: `td-${p.callId}-out`, kind: "tool-detail", label: "output", value: "", isSection: true, cumTokens });
21302
21308
  const outLines = p.output.trim().split("\n").slice(0, 40);
21303
21309
  let outIdx = 0;
21304
21310
  for (const ol of outLines) {
21305
21311
  for (const wrapped of wrapText2(ol === "" ? " " : ol, contentWidth - 5)) {
21306
- lines.push({ id: `td-${p.callId}-out-${outIdx++}`, kind: "tool-detail", label: "", value: wrapped, isSection: false });
21312
+ lines.push({ id: `td-${p.callId}-out-${outIdx++}`, kind: "tool-detail", label: "", value: wrapped, isSection: false, cumTokens });
21307
21313
  }
21308
21314
  }
21309
21315
  }
@@ -21311,12 +21317,12 @@ function buildLines(session, contentWidth, expandedIds) {
21311
21317
  } else if (part.type === "text" && part.text.trim()) {
21312
21318
  let txtIdx = 0;
21313
21319
  for (const row of wrapText2(part.text.trim(), contentWidth - 3)) {
21314
- lines.push({ id: `tx-${interaction.id}-${txtIdx++}`, kind: "text", text: row });
21320
+ lines.push({ id: `tx-${interaction.id}-${txtIdx++}`, kind: "text", text: row, cumTokens });
21315
21321
  }
21316
21322
  } else if (part.type === "reasoning" && part.text.trim()) {
21317
21323
  let rIdx = 0;
21318
21324
  for (const row of wrapText2(part.text.trim(), contentWidth - 5)) {
21319
- lines.push({ id: `r-${interaction.id}-${rIdx++}`, kind: "reasoning", text: row });
21325
+ lines.push({ id: `r-${interaction.id}-${rIdx++}`, kind: "reasoning", text: row, cumTokens });
21320
21326
  }
21321
21327
  }
21322
21328
  }
@@ -21448,6 +21454,9 @@ function MessagesPanelInner({ session, maxHeight, isActive }) {
21448
21454
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Box_default, { height: maxHeight, paddingX: 1, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { color: colors.textDim, children: "No messages" }) });
21449
21455
  }
21450
21456
  const visibleLines = allLines.slice(clampedOffset, clampedOffset + viewHeight);
21457
+ const cursorLine = allLines[clampedCursor];
21458
+ const cursorCumTokens = cursorLine?.cumTokens ?? 0;
21459
+ const totalTokens = allLines[allLines.length - 1]?.cumTokens ?? 0;
21451
21460
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Box_default, { flexDirection: "column", height: maxHeight, paddingX: 1, children: [
21452
21461
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Box_default, { flexDirection: "row", height: 1, children: [
21453
21462
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Text, { color: colors.textDim, children: [
@@ -21457,6 +21466,15 @@ function MessagesPanelInner({ session, maxHeight, isActive }) {
21457
21466
  "/",
21458
21467
  allLines.length
21459
21468
  ] }),
21469
+ totalTokens > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
21470
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { color: colors.textDim, children: " \xB7 " }),
21471
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { color: colors.info, children: formatTokens4(cursorCumTokens) }),
21472
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Text, { color: colors.textDim, children: [
21473
+ "/",
21474
+ formatTokens4(totalTokens),
21475
+ " tok"
21476
+ ] })
21477
+ ] }),
21460
21478
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Box_default, { flexGrow: 1 }),
21461
21479
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Text, { color: colors.textDim, children: [
21462
21480
  session.interactions.filter((i) => i.role === "assistant").length,
@@ -21480,17 +21498,17 @@ function MessagesPanelInner({ session, maxHeight, isActive }) {
21480
21498
  "]"
21481
21499
  ] }),
21482
21500
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Box_default, { flexGrow: 1 }),
21483
- line.tokensIn > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Text, { color: colors.textDim, children: [
21484
- "\u2193",
21485
- formatTokens4(line.tokensIn),
21486
- " "
21501
+ line.tokensIn > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
21502
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { color: colors.textDim, children: "\u2193" }),
21503
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { color: colors.warning, children: formatTokens4(line.tokensIn) }),
21504
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { children: " " })
21487
21505
  ] }),
21488
- line.tokensOut > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Text, { color: colors.textDim, children: [
21489
- "\u2191",
21490
- formatTokens4(line.tokensOut),
21491
- " "
21506
+ line.tokensOut > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
21507
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { color: colors.textDim, children: "\u2191" }),
21508
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { color: colors.success, children: formatTokens4(line.tokensOut) }),
21509
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Text, { children: " " })
21492
21510
  ] }),
21493
- line.duration && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Text, { color: colors.textDim, children: [
21511
+ line.duration && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Text, { color: colors.cyan, children: [
21494
21512
  line.duration,
21495
21513
  " "
21496
21514
  ] }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-top",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Monitor OpenCode AI coding sessions - Token usage, costs, and agent analytics",
5
5
  "type": "module",
6
6
  "bin": {
package/screenshot.svg ADDED
@@ -0,0 +1,81 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="900" height="500">
2
+ <rect x="0" y="0" width="900" height="500" fill="#1a1a2e"/>
3
+ <text x="8" y="20" style="fill:#e94560;font-size:13px;font-family:monospace;font-weight:bold">oc-top</text>
4
+ <text x="66.80000000000001" y="20" style="fill:#e94560;font-size:13px;font-family:monospace">[1]</text>
5
+ <text x="100.4" y="20" style="fill:#eaeaea;font-size:13px;font-family:monospace;font-weight:bold">Sessions</text>
6
+ <text x="176" y="20" style="fill:#888;font-size:13px;font-family:monospace">[2] Tools</text>
7
+ <text x="260" y="20" style="fill:#888;font-size:13px;font-family:monospace">[3] Overview</text>
8
+ <text x="680" y="20" style="fill:#888;font-size:13px;font-family:monospace">01:07:23 AM</text>
9
+ <rect x="6" y="32" width="321.2" height="468" fill="#0f1f3d"/>
10
+ <rect x="333.6" y="32" width="562.4" height="468" fill="#0d1830"/>
11
+ <text x="16.4" y="36" style="fill:#e94560;font-size:13px;font-family:monospace;font-weight:bold">Sessions</text>
12
+ <text x="285.2" y="36" style="fill:#888;font-size:13px;font-family:monospace">4</text>
13
+ <rect x="6" y="40" width="321.2" height="16" fill="#e9456033"/>
14
+ <text x="16.4" y="52" style="fill:#e94560;font-size:13px;font-family:monospace;font-weight:bold">● fix auth bug</text>
15
+ <text x="159.20000000000002" y="52" style="fill:#888;font-size:13px;font-family:monospace">03/15 01:02</text>
16
+ <text x="234.8" y="52" style="fill:#888;font-size:13px;font-family:monospace">48.1k</text>
17
+ <text x="276.8" y="52" style="fill:#4ade80;font-size:13px;font-family:monospace">$0.0000</text>
18
+ <text x="16.4" y="68" style="fill:#eaeaea;font-size:13px;font-family:monospace">● add dark mode</text>
19
+ <text x="159.20000000000002" y="68" style="fill:#888;font-size:13px;font-family:monospace">03/14 23:45</text>
20
+ <text x="234.8" y="68" style="fill:#888;font-size:13px;font-family:monospace">31.2k</text>
21
+ <text x="276.8" y="68" style="fill:#4ade80;font-size:13px;font-family:monospace">$0.0000</text>
22
+ <text x="16.4" y="84" style="fill:#eaeaea;font-size:13px;font-family:monospace">▶ refactor API</text>
23
+ <text x="159.20000000000002" y="84" style="fill:#888;font-size:13px;font-family:monospace">03/14 21:30</text>
24
+ <text x="234.8" y="84" style="fill:#888;font-size:13px;font-family:monospace">94.7k</text>
25
+ <text x="276.8" y="84" style="fill:#4ade80;font-size:13px;font-family:monospace">$0.0124</text>
26
+ <text x="16.4" y="100" style="fill:#eaeaea;font-size:13px;font-family:monospace"> ├─ [build] sub</text>
27
+ <text x="159.20000000000002" y="100" style="fill:#888;font-size:13px;font-family:monospace">03/14 21:31</text>
28
+ <text x="234.8" y="100" style="fill:#888;font-size:13px;font-family:monospace">22.1k</text>
29
+ <text x="276.8" y="100" style="fill:#4ade80;font-size:13px;font-family:monospace">$0.0031</text>
30
+ <text x="16.4" y="116" style="fill:#eaeaea;font-size:13px;font-family:monospace">● setup CI pipeline</text>
31
+ <text x="159.20000000000002" y="116" style="fill:#888;font-size:13px;font-family:monospace">03/13 18:12</text>
32
+ <text x="234.8" y="116" style="fill:#888;font-size:13px;font-family:monospace">15.6k</text>
33
+ <text x="276.8" y="116" style="fill:#4ade80;font-size:13px;font-family:monospace">$0.0000</text>
34
+ <text x="344" y="36" style="fill:#888;font-size:13px;font-family:monospace">[Stats]</text>
35
+ <text x="411.20000000000005" y="36" style="fill:#e94560;font-size:13px;font-family:monospace;font-weight:bold">[Messages]</text>
36
+ <text x="806" y="36" style="fill:#888;font-size:13px;font-family:monospace">Tab:switch</text>
37
+ <text x="344" y="52" style="fill:#888;font-size:13px;font-family:monospace">1–28/187</text>
38
+ <text x="428" y="52" style="fill:#888;font-size:13px;font-family:monospace">·</text>
39
+ <text x="444.8" y="52" style="fill:#60a5fa;font-size:13px;font-family:monospace">14.2k</text>
40
+ <text x="486.8" y="52" style="fill:#888;font-size:13px;font-family:monospace">/48.1k tok</text>
41
+ <text x="814.4000000000001" y="52" style="fill:#888;font-size:13px;font-family:monospace">6 turns</text>
42
+ <text x="848" y="52" style="fill:#888;font-size:13px;font-family:monospace"> ↓</text>
43
+ <rect x="333.6" y="56" width="562.4" height="16" fill="#e9456022"/>
44
+ <text x="335.6" y="68" style="fill:#e94560;font-size:13px;font-family:monospace">›</text>
45
+ <text x="344" y="68" style="fill:#a855f7;font-size:13px;font-family:monospace;font-weight:bold">◆ </text>
46
+ <text x="360.8" y="68" style="fill:#60a5fa;font-size:13px;font-family:monospace">claude-sonnet-4-5-2025</text>
47
+ <text x="545.6" y="68" style="fill:#eaeaea;font-size:13px;font-family:monospace"> </text>
48
+ <text x="579.2" y="68" style="fill:#888;font-size:13px;font-family:monospace">↓</text>
49
+ <text x="587.6" y="68" style="fill:#fbbf24;font-size:13px;font-family:monospace">12.8k</text>
50
+ <text x="629.6" y="68" style="fill:#888;font-size:13px;font-family:monospace"> ↑</text>
51
+ <text x="646.4" y="68" style="fill:#4ade80;font-size:13px;font-family:monospace">48</text>
52
+ <text x="663.2" y="68" style="fill:#22d3ee;font-size:13px;font-family:monospace"> 4.9s</text>
53
+ <text x="705.2" y="68" style="fill:#888;font-size:13px;font-family:monospace"> 01:05:52 AM</text>
54
+ <text x="344" y="84" style="fill:#4ade80;font-size:13px;font-family:monospace"> ✓ </text>
55
+ <text x="377.6" y="84" style="fill:#eaeaea;font-size:13px;font-family:monospace;font-weight:bold">bash</text>
56
+ <text x="411.20000000000005" y="84" style="fill:#888;font-size:13px;font-family:monospace"> npm install --save-dev</text>
57
+ <text x="604.4" y="84" style="fill:#888;font-size:13px;font-family:monospace"> ▶ 1.2s</text>
58
+ <text x="344" y="100" style="fill:#4ade80;font-size:13px;font-family:monospace"> ✓ </text>
59
+ <text x="377.6" y="100" style="fill:#eaeaea;font-size:13px;font-family:monospace;font-weight:bold">read</text>
60
+ <text x="411.20000000000005" y="100" style="fill:#888;font-size:13px;font-family:monospace"> src/auth/index.ts</text>
61
+ <text x="562.4" y="100" style="fill:#888;font-size:13px;font-family:monospace"> ▶ 0.3s</text>
62
+ <text x="344" y="116" style="fill:#f87171;font-size:13px;font-family:monospace"> ✗ </text>
63
+ <text x="377.6" y="116" style="fill:#eaeaea;font-size:13px;font-family:monospace;font-weight:bold">bash</text>
64
+ <text x="411.20000000000005" y="116" style="fill:#888;font-size:13px;font-family:monospace"> npx tsc --noEmit</text>
65
+ <text x="554" y="116" style="fill:#888;font-size:13px;font-family:monospace"> ▶ exit:1 2.1s</text>
66
+ <text x="344" y="132" style="fill:#a855f7;font-size:13px;font-family:monospace"> ── output</text>
67
+ <text x="344" y="148" style="fill:#888;font-size:13px;font-family:monospace"> </text>
68
+ <text x="402.8" y="148" style="fill:#f87171;font-size:13px;font-family:monospace">error TS2345: Argument of type 'string'…</text>
69
+ <text x="344" y="164" style="fill:#eaeaea;font-size:13px;font-family:monospace"> I see the TypeScript error. Let me fix the type mismatch in</text>
70
+ <text x="344" y="180" style="fill:#eaeaea;font-size:13px;font-family:monospace"> the auth middleware...</text>
71
+ <text x="344" y="196" style="fill:#a855f7;font-size:13px;font-family:monospace;font-weight:bold"> ◆ </text>
72
+ <text x="369.2" y="196" style="fill:#60a5fa;font-size:13px;font-family:monospace">claude-sonnet-4-5-2025</text>
73
+ <text x="554" y="196" style="fill:#eaeaea;font-size:13px;font-family:monospace"> </text>
74
+ <text x="587.6" y="196" style="fill:#888;font-size:13px;font-family:monospace">↓</text>
75
+ <text x="596" y="196" style="fill:#fbbf24;font-size:13px;font-family:monospace">9.1k</text>
76
+ <text x="629.6" y="196" style="fill:#888;font-size:13px;font-family:monospace"> ↑</text>
77
+ <text x="646.4" y="196" style="fill:#4ade80;font-size:13px;font-family:monospace">312</text>
78
+ <text x="671.6" y="196" style="fill:#22d3ee;font-size:13px;font-family:monospace"> 8.3s</text>
79
+ <text x="713.6" y="196" style="fill:#888;font-size:13px;font-family:monospace"> 01:06:04 AM</text>
80
+ <text x="8" y="488" style="fill:#888;font-size:13px;font-family:monospace">Tab:stats j/k:scroll d/u:½page g/G:top/bot Enter:expand [:prev ]:next q:quit</text>
81
+ </svg>