opencode-hud 0.1.0

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 ADDED
@@ -0,0 +1,108 @@
1
+ # OpenCode HUD Plugin
2
+
3
+ A plugin for [OpenCode](https://opencode.ai) that displays token streaming metrics at the end of each conversation.
4
+
5
+ ## Metrics Displayed
6
+
7
+ | Metric | Description |
8
+ |--------|-------------|
9
+ | ⚡ Avg TPS | Average tokens per second (includes reasoning tokens) |
10
+ | TTFT | Time To First Token — latency from request to first response token |
11
+ | Total tokens | Cumulative token count from API (output + reasoning) |
12
+ | Elapsed time | Wall-clock time since the first token |
13
+
14
+ **Example toast:**
15
+ ```
16
+ ⚡ 42.5 t/s TTFT 312ms [639 tok / 15.0s]
17
+ ```
18
+
19
+ ## Installation
20
+
21
+ ### From npm
22
+
23
+ Add the plugin to your `.opencode/opencode.json`:
24
+
25
+ ```json
26
+ {
27
+ "plugin": ["opencode-hud@latest"]
28
+ }
29
+ ```
30
+
31
+ ### Local Development
32
+
33
+ This plugin auto-loads when you run `opencode` from this directory, because it lives in `.opencode/plugins/`.
34
+
35
+ ## Configuration
36
+
37
+ Create a config file at `~/.config/opencode/opencode-hud.json`:
38
+
39
+ ```json
40
+ {
41
+ "enableLogging": true,
42
+ "logFilePath": ".opencode/hud-debug.log"
43
+ }
44
+ ```
45
+
46
+ | Option | Type | Default | Description |
47
+ |--------|------|---------|-------------|
48
+ | `enableLogging` | boolean | `false` | Enable debug logging |
49
+ | `logFilePath` | string | `.opencode/hud-debug.log` | Path to log file |
50
+
51
+ ## Development
52
+
53
+ ```bash
54
+ # Install dependencies (Bun required)
55
+ bun install
56
+
57
+ # Run tests
58
+ bun test
59
+
60
+ # Type-check
61
+ bun run typecheck
62
+
63
+ # Build
64
+ bun run build
65
+ ```
66
+
67
+ ## Project Structure
68
+
69
+ ```
70
+ opencode-hud/
71
+ ├── .opencode/
72
+ │ ├── opencode.json # OpenCode project config
73
+ │ └── plugins/
74
+ │ └── hud.ts # Plugin entry (auto-loaded by OpenCode)
75
+ ├── src/
76
+ │ ├── index.ts # Plugin main entry — event handlers
77
+ │ ├── config.ts # Configuration management
78
+ │ ├── logger.ts # Configurable logging
79
+ │ ├── types.ts # TypeScript interfaces
80
+ │ ├── metrics.ts # Token estimation, duration formatting
81
+ │ └── display.ts # Toast formatting and emission
82
+ ├── tests/
83
+ │ ├── metrics.test.ts # Unit tests
84
+ │ └── integration.test.ts # Event flow tests
85
+ ├── dist/ # Build output
86
+ ├── package.json
87
+ └── tsconfig.json
88
+ ```
89
+
90
+ ## Architecture
91
+
92
+ The plugin listens to OpenCode events and displays metrics when a conversation ends (`session.idle`).
93
+
94
+ **Event Flow:**
95
+ 1. `message.updated` (user) → Record request start time
96
+ 2. `message.updated` (assistant) → Store message with tokens info
97
+ 3. `message.part.updated` → Track first token time, count tokens
98
+ 4. `session.idle` → Calculate metrics and show toast
99
+
100
+ **Token Counting:**
101
+ - Primary: Uses API-provided `tokens.output + tokens.reasoning`
102
+ - Fallback: Estimates from text length (`length / 3`)
103
+
104
+ **Key Design Decisions:**
105
+ - `requestStartTime` set on user message to capture full TTFT
106
+ - `streamingStartTime` set on first assistant token (lazy init)
107
+ - Token count includes reasoning tokens for accurate TPS
108
+ - Uses `performance.now()` for high-precision timing
@@ -0,0 +1,9 @@
1
+ export interface HudConfig {
2
+ enableLogging: boolean;
3
+ logFilePath?: string;
4
+ }
5
+ export declare function loadConfigFromFile(): void;
6
+ export declare function getConfig(): HudConfig;
7
+ export declare function setConfig(config: Partial<HudConfig>): void;
8
+ export declare function resetConfig(): void;
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,OAAO,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAkBD,wBAAgB,kBAAkB,IAAI,IAAI,CAWzC;AAED,wBAAgB,SAAS,IAAI,SAAS,CAErC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAE1D;AAED,wBAAgB,WAAW,IAAI,IAAI,CAElC"}
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ const CONFIG_FILE_NAME = "opencode-hud.json";
5
+ const DEFAULT_CONFIG = {
6
+ enableLogging: false,
7
+ logFilePath: ".opencode/hud-debug.log",
8
+ };
9
+ let currentConfig = { ...DEFAULT_CONFIG };
10
+ function getConfigPaths() {
11
+ return [
12
+ join(homedir(), ".config", "opencode", CONFIG_FILE_NAME),
13
+ join(process.cwd(), ".opencode", CONFIG_FILE_NAME),
14
+ ];
15
+ }
16
+ export function loadConfigFromFile() {
17
+ for (const configPath of getConfigPaths()) {
18
+ if (existsSync(configPath)) {
19
+ try {
20
+ const content = readFileSync(configPath, "utf-8");
21
+ const parsed = JSON.parse(content);
22
+ currentConfig = { ...DEFAULT_CONFIG, ...parsed };
23
+ return;
24
+ }
25
+ catch { }
26
+ }
27
+ }
28
+ }
29
+ export function getConfig() {
30
+ return currentConfig;
31
+ }
32
+ export function setConfig(config) {
33
+ currentConfig = { ...currentConfig, ...config };
34
+ }
35
+ export function resetConfig() {
36
+ currentConfig = { ...DEFAULT_CONFIG };
37
+ }
38
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAO3B,MAAM,gBAAgB,GAAG,mBAAmB,CAAA;AAE5C,MAAM,cAAc,GAAc;IAChC,aAAa,EAAE,KAAK;IACpB,WAAW,EAAE,yBAAyB;CACvC,CAAA;AAED,IAAI,aAAa,GAAc,EAAE,GAAG,cAAc,EAAE,CAAA;AAEpD,SAAS,cAAc;IACrB,OAAO;QACL,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,CAAC;QACxD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,gBAAgB,CAAC;KACnD,CAAA;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,KAAK,MAAM,UAAU,IAAI,cAAc,EAAE,EAAE,CAAC;QAC1C,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;gBACjD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAuB,CAAA;gBACxD,aAAa,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,EAAE,CAAA;gBAChD,OAAM;YACR,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,aAAa,CAAA;AACtB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAA0B;IAClD,aAAa,GAAG,EAAE,GAAG,aAAa,EAAE,GAAG,MAAM,EAAE,CAAA;AACjD,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,aAAa,GAAG,EAAE,GAAG,cAAc,EAAE,CAAA;AACvC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { OpencodeClient } from "@opencode-ai/sdk";
2
+ export declare function showHud(client: OpencodeClient, options: {
3
+ message: string;
4
+ }): Promise<void>;
5
+ //# sourceMappingURL=display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display.d.ts","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAItD,wBAAsB,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQjG"}
@@ -0,0 +1,11 @@
1
+ const TOAST_DURATION_MS = 5000;
2
+ export async function showHud(client, options) {
3
+ await client.tui.showToast({
4
+ body: {
5
+ message: options.message,
6
+ variant: "info",
7
+ duration: TOAST_DURATION_MS,
8
+ },
9
+ });
10
+ }
11
+ //# sourceMappingURL=display.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display.js","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAEA,MAAM,iBAAiB,GAAG,IAAI,CAAA;AAE9B,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAsB,EAAE,OAA4B;IAChF,MAAM,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;QACzB,IAAI,EAAE;YACJ,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,MAAM;YACf,QAAQ,EAAE,iBAAiB;SAC5B;KACF,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export { setConfig, getConfig, resetConfig } from "./config.js";
3
+ export type { HudConfig } from "./config.js";
4
+ export declare const HudPlugin: Plugin;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAQjD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC/D,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,eAAO,MAAM,SAAS,EAAE,MAiJvB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,131 @@
1
+ import { showHud } from "./display.js";
2
+ import { createFreshMetrics, formatDuration, now, estimateTokens } from "./metrics.js";
3
+ import { log } from "./logger.js";
4
+ import { loadConfigFromFile } from "./config.js";
5
+ export { setConfig, getConfig, resetConfig } from "./config.js";
6
+ export const HudPlugin = async ({ client }) => {
7
+ loadConfigFromFile();
8
+ const sessions = new Map();
9
+ const messageRoles = new Map();
10
+ const assistantMessages = new Map();
11
+ function isTextPart(part) {
12
+ return part.type === "text" && "text" in part;
13
+ }
14
+ function getOrCreate(sessionId) {
15
+ let m = sessions.get(sessionId);
16
+ if (!m) {
17
+ m = createFreshMetrics();
18
+ sessions.set(sessionId, m);
19
+ }
20
+ return m;
21
+ }
22
+ return {
23
+ event: async ({ event }) => {
24
+ switch (event.type) {
25
+ case "session.created": {
26
+ const session = event.properties.info;
27
+ log(`session.created: id=${session.id}`);
28
+ sessions.set(session.id, createFreshMetrics());
29
+ break;
30
+ }
31
+ case "message.updated": {
32
+ const msg = event.properties.info;
33
+ log(`message.updated: id=${msg.id} role=${msg.role} sessionID=${msg.sessionID}`);
34
+ messageRoles.set(msg.id, msg.role);
35
+ if (msg.role === "user") {
36
+ const metrics = getOrCreate(msg.sessionID);
37
+ metrics.requestStartTime = now();
38
+ }
39
+ else if (msg.role === "assistant") {
40
+ const assistantMsg = msg;
41
+ assistantMessages.set(msg.id, assistantMsg);
42
+ log(` assistant tokens: output=${assistantMsg.tokens?.output} reasoning=${assistantMsg.tokens?.reasoning}`);
43
+ }
44
+ break;
45
+ }
46
+ case "message.part.updated": {
47
+ const part = event.properties.part;
48
+ if (!isTextPart(part))
49
+ break;
50
+ const sessionId = part.sessionID;
51
+ const messageId = part.messageID;
52
+ log(`message.part.updated: sessionId=${sessionId} messageId=${messageId} msgRole=${messageRoles.get(messageId)}`);
53
+ if (messageRoles.get(messageId) !== "assistant") {
54
+ log(` SKIP: not assistant`);
55
+ break;
56
+ }
57
+ const metrics = getOrCreate(sessionId);
58
+ log(` metrics: totalTokens=${metrics.totalTokens} streamingStartTime=${metrics.streamingStartTime}`);
59
+ if (messageId !== metrics.currentMessageId) {
60
+ metrics.streamingStartTime = null;
61
+ metrics.totalTokens = 0;
62
+ metrics.currentMessageId = messageId;
63
+ }
64
+ const assistantMsg = assistantMessages.get(messageId);
65
+ if (assistantMsg?.tokens?.output !== undefined && assistantMsg.tokens.output > 0) {
66
+ const output = assistantMsg.tokens.output;
67
+ const reasoning = assistantMsg.tokens.reasoning ?? 0;
68
+ metrics.totalTokens = output + reasoning;
69
+ log(` using API tokens: output=${output} reasoning=${reasoning} total=${metrics.totalTokens}`);
70
+ }
71
+ else {
72
+ metrics.totalTokens = estimateTokens(part.text);
73
+ log(` using estimated tokens: ${metrics.totalTokens}`);
74
+ }
75
+ if (metrics.streamingStartTime === null) {
76
+ metrics.streamingStartTime = now();
77
+ }
78
+ break;
79
+ }
80
+ case "session.idle": {
81
+ const sessionId = event.properties.sessionID;
82
+ log(`session.idle: sessionId=${sessionId}`);
83
+ const metrics = sessions.get(sessionId);
84
+ log(` metrics: ${JSON.stringify(metrics)}`);
85
+ if (!metrics) {
86
+ log(` SKIP: no metrics`);
87
+ break;
88
+ }
89
+ if (metrics.streamingStartTime === null) {
90
+ log(` SKIP: streamingStartTime is null`);
91
+ break;
92
+ }
93
+ const assistantMsg = metrics.currentMessageId
94
+ ? assistantMessages.get(metrics.currentMessageId)
95
+ : undefined;
96
+ if (assistantMsg?.tokens?.output !== undefined) {
97
+ const output = assistantMsg.tokens.output;
98
+ const reasoning = assistantMsg.tokens.reasoning ?? 0;
99
+ metrics.totalTokens = output + reasoning;
100
+ log(` final tokens from API: output=${output} reasoning=${reasoning} total=${metrics.totalTokens}`);
101
+ }
102
+ if (metrics.totalTokens === 0) {
103
+ log(` SKIP: totalTokens is 0`);
104
+ break;
105
+ }
106
+ metrics.completionTime = now();
107
+ const ttft = metrics.requestStartTime !== null && metrics.streamingStartTime !== null
108
+ ? metrics.streamingStartTime - metrics.requestStartTime
109
+ : null;
110
+ const elapsedMs = metrics.completionTime - metrics.streamingStartTime;
111
+ const elapsedSec = elapsedMs / 1000;
112
+ const avgTps = elapsedSec > 0 ? (metrics.totalTokens / elapsedSec) : 0;
113
+ const message = `⚡ ${avgTps.toFixed(1)} t/s TTFT ${ttft !== null ? formatDuration(ttft) : "--"} [${metrics.totalTokens} tok / ${elapsedSec.toFixed(1)}s]`;
114
+ log(` SHOWING: ${message}`);
115
+ await showHud(client, { message });
116
+ break;
117
+ }
118
+ case "message.removed": {
119
+ messageRoles.delete(event.properties.messageID);
120
+ assistantMessages.delete(event.properties.messageID);
121
+ break;
122
+ }
123
+ case "session.deleted": {
124
+ sessions.delete(event.properties.info.id);
125
+ break;
126
+ }
127
+ }
128
+ },
129
+ };
130
+ };
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function log(msg: string): void;
2
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAIA,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAcrC"}
package/dist/logger.js ADDED
@@ -0,0 +1,20 @@
1
+ import { appendFileSync, mkdirSync, existsSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { getConfig } from "./config.js";
4
+ export function log(msg) {
5
+ const config = getConfig();
6
+ if (!config.enableLogging)
7
+ return;
8
+ const logFile = config.logFilePath;
9
+ if (!logFile)
10
+ return;
11
+ const timestamp = new Date().toISOString().slice(11, 23);
12
+ try {
13
+ if (!existsSync(logFile)) {
14
+ mkdirSync(dirname(logFile), { recursive: true });
15
+ }
16
+ appendFileSync(logFile, `[${timestamp}] ${msg}\n`);
17
+ }
18
+ catch { }
19
+ }
20
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvC,MAAM,UAAU,GAAG,CAAC,GAAW;IAC7B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,IAAI,CAAC,MAAM,CAAC,aAAa;QAAE,OAAM;IAEjC,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAAA;IAClC,IAAI,CAAC,OAAO;QAAE,OAAM;IAEpB,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IACxD,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAClD,CAAC;QACD,cAAc,CAAC,OAAO,EAAE,IAAI,SAAS,KAAK,GAAG,IAAI,CAAC,CAAA;IACpD,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;AACZ,CAAC"}
@@ -0,0 +1,11 @@
1
+ export declare function now(): number;
2
+ export declare function formatDuration(ms: number): string;
3
+ export declare function estimateTokens(text: string): number;
4
+ export declare function createFreshMetrics(): {
5
+ requestStartTime: number | null;
6
+ streamingStartTime: number | null;
7
+ completionTime: number | null;
8
+ totalTokens: number;
9
+ currentMessageId: string | null;
10
+ };
11
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,wBAAgB,kBAAkB;sBAEJ,MAAM,GAAG,IAAI;wBACX,MAAM,GAAG,IAAI;oBACjB,MAAM,GAAG,IAAI;;sBAEX,MAAM,GAAG,IAAI;EAE1C"}
@@ -0,0 +1,23 @@
1
+ export function now() {
2
+ return performance.now();
3
+ }
4
+ export function formatDuration(ms) {
5
+ if (ms < 1000)
6
+ return `${Math.round(ms)}ms`;
7
+ return `${(ms / 1000).toFixed(2)}s`;
8
+ }
9
+ export function estimateTokens(text) {
10
+ if (!text)
11
+ return 0;
12
+ return Math.round(text.length / 3);
13
+ }
14
+ export function createFreshMetrics() {
15
+ return {
16
+ requestStartTime: null,
17
+ streamingStartTime: null,
18
+ completionTime: null,
19
+ totalTokens: 0,
20
+ currentMessageId: null,
21
+ };
22
+ }
23
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG;IACjB,OAAO,WAAW,CAAC,GAAG,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAU;IACvC,IAAI,EAAE,GAAG,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAA;IAC3C,OAAO,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAA;AACrC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAA;IACnB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AACpC,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO;QACL,gBAAgB,EAAE,IAAqB;QACvC,kBAAkB,EAAE,IAAqB;QACzC,cAAc,EAAE,IAAqB;QACrC,WAAW,EAAE,CAAC;QACd,gBAAgB,EAAE,IAAqB;KACxC,CAAA;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ export interface SessionMetrics {
2
+ requestStartTime: number | null;
3
+ streamingStartTime: number | null;
4
+ completionTime: number | null;
5
+ totalTokens: number;
6
+ currentMessageId: string | null;
7
+ }
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "opencode-hud",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "OpenCode plugin that displays TPS, TTFT, and token metrics at conversation end.",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "test": "bun test",
21
+ "typecheck": "tsc --noEmit",
22
+ "build": "tsc",
23
+ "prepublishOnly": "bun run typecheck && bun test && bun run build"
24
+ },
25
+ "keywords": [
26
+ "opencode",
27
+ "opencode-plugin",
28
+ "tps",
29
+ "ttft",
30
+ "metrics",
31
+ "ai",
32
+ "llm"
33
+ ],
34
+ "license": "MIT",
35
+ "peerDependencies": {
36
+ "@opencode-ai/plugin": ">=0.1.0",
37
+ "@opencode-ai/sdk": ">=0.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@opencode-ai/plugin": "latest",
41
+ "@opencode-ai/sdk": "latest",
42
+ "typescript": "^5.0.0",
43
+ "bun-types": "latest",
44
+ "@types/bun": "latest"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/Alaye-Dong/opencode-hud.git"
49
+ }
50
+ }