opencode-miniterm 1.0.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/.prettierrc ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "useTabs": true,
3
+ "printWidth": 100,
4
+ "trailingComma": "all",
5
+ "plugins": ["@trivago/prettier-plugin-sort-imports"],
6
+ "importOrder": ["^[../]", "^[./]"],
7
+ "importOrderSortSpecifiers": true,
8
+ "overrides": [
9
+ {
10
+ "files": ["*.json", "*.jsonc"],
11
+ "options": {
12
+ "trailingComma": "none"
13
+ }
14
+ }
15
+ ]
16
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,243 @@
1
+ # OpenCode MiniTerm - Agent Guidelines
2
+
3
+ ## Build & Development Commands
4
+
5
+ ### Run the application
6
+ ```bash
7
+ bun run src/index.ts
8
+ # Or just:
9
+ bun src/index.ts
10
+ ```
11
+
12
+ ### Build (when bundler is added)
13
+ ```bash
14
+ bun build src/index.ts --outdir dist
15
+ ```
16
+
17
+ ### Testing
18
+ No test framework is currently configured. Add one of these to package.json:
19
+ - **Bun Test**: `bun test` (recommended - built-in, fast)
20
+ - **Jest**: `npm test` or `bun run test`
21
+ - **Vitest**: `vitest`
22
+
23
+ To run a single test (once configured):
24
+ - Bun Test: `bun test --test-name-pattern "testName"`
25
+ - Jest: `npm test -- testName`
26
+ - Vitest: `vitest run testName`
27
+
28
+ ### Linting & Formatting (recommended additions)
29
+ Install and configure these tools:
30
+ ```bash
31
+ bun add -d eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier
32
+ ```
33
+
34
+ Commands to add to package.json:
35
+ ```json
36
+ {
37
+ "lint": "eslint src --ext .ts",
38
+ "lint:fix": "eslint src --ext .ts --fix",
39
+ "format": "prettier --write \"src/**/*.ts\"",
40
+ "format:check": "prettier --check \"src/**/*.ts\"",
41
+ "typecheck": "tsc --noEmit"
42
+ }
43
+ ```
44
+
45
+ ## Code Style Guidelines
46
+
47
+ ### TypeScript Configuration
48
+ - Use strict mode: `"strict": true` in tsconfig.json
49
+ - Target ES2022+ for modern Node/Bun features
50
+ - Use `moduleResolution: "bundler"` for Bun compatibility
51
+
52
+ ### Imports
53
+ - Use ES6 imports (ESM): `import { something } from 'module'`
54
+ - Group imports in this order:
55
+ 1. Node/Bun built-ins
56
+ 2. External packages
57
+ 3. Internal modules
58
+ - Use absolute imports where possible (configure path aliases in tsconfig.json)
59
+ - Avoid default exports; prefer named exports for better tree-shaking
60
+
61
+ ### Formatting
62
+ - Use 2 spaces for indentation
63
+ - Use single quotes for strings
64
+ - Use semicolons at end of statements
65
+ - Maximum line length: 100 characters
66
+ - Trailing commas in multi-line arrays/objects
67
+ - Spaces around operators: `a = b + c` not `a=b+c`
68
+
69
+ ### Types & Type Safety
70
+ - Always provide explicit return types for functions
71
+ - Use `interface` for object shapes, `type` for unions/primitives
72
+ - Avoid `any`; use `unknown` when type is truly unknown
73
+ - Use type guards for runtime type checking
74
+ - Leverage Bun's built-in type definitions (from `bun-types`)
75
+
76
+ ### Naming Conventions
77
+ - **Files**: kebab-case: `my-service.ts`
78
+ - **Variables/Functions**: camelCase: `myFunction`
79
+ - **Classes**: PascalCase: `MyService`
80
+ - **Constants**: UPPER_SNAKE_CASE for global constants: `MAX_RETRIES`
81
+ - **Private members**: Leading underscore: `_privateMethod`
82
+ - **Types/Interfaces**: PascalCase, often with suffixes: `UserService`, `ConfigOptions`
83
+
84
+ ### Error Handling
85
+ - Use try/catch for async operations
86
+ - Create custom error classes for domain-specific errors:
87
+ ```ts
88
+ class TerminalError extends Error {
89
+ constructor(message: string, public code: string) {
90
+ super(message);
91
+ this.name = 'TerminalError';
92
+ }
93
+ }
94
+ ```
95
+ - Always include error context in error messages
96
+ - Log errors appropriately (avoid logging secrets)
97
+ - Never swallow errors silently
98
+
99
+ ### Async/Promise Handling
100
+ - Use async/await over .then()/.catch()
101
+ - Handle promise rejections: `process.on('unhandledRejection')`
102
+ - Use Bun's optimized APIs where available (e.g., `Bun.file()`)
103
+ - Implement timeouts for network requests
104
+
105
+ ### Code Organization
106
+ - Structure by feature/domain, not by file type
107
+ - Keep files focused: one responsibility per file
108
+ - Export at file end; avoid export分散
109
+ - Use barrel files (`index.ts`) for cleaner imports
110
+
111
+ ### Comments
112
+ - Use JSDoc for public APIs: `/** @description ... */`
113
+ - Comment WHY, not WHAT
114
+ - Keep comments current with code changes
115
+ - Avoid inline comments for obvious logic
116
+
117
+ ### Performance (Bun-Specific)
118
+ - Leverage Bun's fast I/O: `Bun.write()`, `Bun.file()`
119
+ - Use `TextEncoder`/`TextDecoder` for encoding
120
+ - Prefer native over polyfills
121
+ - Benchmark before optimizing
122
+
123
+ ## Project Context
124
+
125
+ This is an alternative terminal UI for OpenCode. Focus on:
126
+ - Fast, responsive terminal rendering
127
+ - Clean CLI UX with good error messages
128
+ - Efficient resource usage (memory/CPU)
129
+ - Compatibility with OpenCode's API
130
+
131
+ ## File Organization
132
+
133
+ - Create all temporary files in `./tmp` directory
134
+ - Avoid using `/tmp` or system temp directories
135
+ - The `tmp` folder is gitignored and safe for transient files
136
+ - Clean up temporary files after use to keep the directory organized
137
+
138
+ ## OpenCode Server Integration
139
+
140
+ ### Starting the Server
141
+ - Use `opencode serve` to start a headless HTTP server (not `opencode server`)
142
+ - Default URL: `http://127.0.0.1:4096` (port may vary, can be 0/random)
143
+ - Server requires 2-3 seconds to initialize before accepting requests
144
+ - Spawn with `stdio: ['ignore', 'pipe', 'pipe']` to avoid interfering with parent I/O
145
+ - Always handle SIGINT to properly shut down the server process
146
+
147
+ ### Authentication
148
+ - Server may require HTTP Basic Auth if `OPENCODE_SERVER_PASSWORD` is set
149
+ - Username: `OPENCODE_SERVER_USERNAME` env var (default: 'opencode')
150
+ - Password: `OPENCODE_SERVER_PASSWORD` env var (required if server has password set)
151
+ - Include `Authorization: Basic <base64(username:password)>` header when password is set
152
+ - Include `Content-Type: application/json` header for all POST requests
153
+ - Check env vars at startup: `echo $OPENCODE_SERVER_PASSWORD` to verify it's set
154
+
155
+ ### Creating Sessions
156
+ ```ts
157
+ POST /session
158
+ Headers: { "Content-Type": "application/json", "Authorization": "Basic <creds>" }
159
+ Body: {}
160
+ Response: { id: string, title?: string, ... }
161
+ ```
162
+
163
+ ### Getting Available Models
164
+ ```ts
165
+ GET /config/providers
166
+ Headers: { "Authorization": "Basic <creds>" }
167
+ Response: { providers: Provider[], default: { [key: string]: string } }
168
+ ```
169
+ Note: `/models` endpoint returns HTML documentation, not JSON. Use `/config/providers` for programmatic access.
170
+
171
+ ### Sending Messages
172
+ ```ts
173
+ POST /session/:id/message
174
+ Headers: { "Content-Type": "application/json", "Authorization": "Basic <creds>" }
175
+ Body: {
176
+ model: {
177
+ modelID: 'big-pickle',
178
+ providerID: 'opencode'
179
+ },
180
+ parts: [{ type: 'text', text: 'your message here' }]
181
+ }
182
+ Response: { info: Message, parts: Part[] }
183
+ ```
184
+
185
+ ### Getting Session Messages
186
+ ```ts
187
+ GET /session/:id/message
188
+ Headers: { "Authorization": "Basic <creds>" }
189
+ Response: { info: Message, parts: Part[] }[]
190
+ ```
191
+
192
+ ### Undoing Messages (Revert)
193
+ ```ts
194
+ POST /session/:id/revert
195
+ Headers: { "Content-Type": "application/json", "Authorization": "Basic <creds>" }
196
+ Body: { messageID: string, partID?: string }
197
+ Response: { id: string, revert: { messageID, snapshot, diff } }
198
+ ```
199
+ Typically used to undo the last assistant message by fetching messages first, then reverting the last one.
200
+
201
+ **IMPORTANT**: The `model` field is required when sending messages. Without it, the request will hang indefinitely. Get available models from `GET /config/providers` or `GET /models`. Common models:
202
+ - `big-pickle` (opencode provider) - default, high quality
203
+ - `glm-5-free` (opencode provider) - free GLM model
204
+ - `gpt-5-nano` (opencode provider) - fast GPT model
205
+
206
+ ### Response Format
207
+ - Response has `{ info, parts }` structure
208
+ - Parts can be: `step-start`, `reasoning`, `text`, `step-finish`, `tool_use`, `tool_result`
209
+ - `step-start` - Indicates beginning of a thinking/processing step
210
+ - `reasoning` - AI's internal reasoning/thinking process (shows "💭 Thinking...")
211
+ - `text` - The actual AI response to display to the user
212
+ - `step-finish` - Indicates completion of a thinking/processing step
213
+ - `tool_use` - Indicates an AI tool is being used
214
+ - `tool_result` - Contains result of tool execution (can be filtered out)
215
+ - Display reasoning and text parts to the user for transparency
216
+
217
+ ### Server-Sent Events (SSE)
218
+ - Connect to event stream at `/event` for real-time updates
219
+ - Events include: `message.part.updated`, `session.status`, `session.updated`, `message.updated`, `session.diff`, `session.idle`
220
+ - Event structure: `{ type: string, properties: {...} }`
221
+ - Parts can be streamed via `message.part.updated` events with delta updates
222
+ - Delta is at `event.properties.delta`, NOT in `event.properties.part.delta`
223
+ - Use `seenParts` set to track processed parts and avoid duplicate display
224
+ - Delta updates allow streaming reasoning and text for better UX
225
+
226
+ ### Error Handling
227
+ - Server returns 401 Unauthorized when authentication is missing/invalid
228
+ - Handle connection errors (server may not be ready yet)
229
+ - Always parse error text from response for debugging
230
+ - Bun's fetch doesn't timeout by default - use AbortController for timeouts:
231
+ ```ts
232
+ const controller = new AbortController();
233
+ const timeout = setTimeout(() => controller.abort(), 180000);
234
+ const response = await fetch(url, { signal: controller.signal });
235
+ clearTimeout(timeout);
236
+ ```
237
+
238
+ ## Safety Notes
239
+
240
+ - Never commit API keys, tokens, or secrets
241
+ - Validate all user inputs
242
+ - Sanitize terminal output to prevent injection
243
+ - Use environment variables for configuration
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # OpenCode Miniterm
2
+
3
+ A small front-end terminal UI for [OpenCode](https://github.com/anomalyco/opencode).
4
+
5
+ This project is not affiliated with OpenCode.
package/bun.lock ADDED
@@ -0,0 +1,108 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "opencode-miniterm",
7
+ "dependencies": {
8
+ "@opencode-ai/sdk": "^1.2.10",
9
+ "allmark": "^1.0.0",
10
+ },
11
+ "devDependencies": {
12
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
13
+ "@types/bun": "latest",
14
+ "@typescript/native-preview": "7.0.0-dev.20260122.2",
15
+ "prettier": "^3.8.1",
16
+ "typescript": "^5.9.3",
17
+ },
18
+ "peerDependencies": {
19
+ "typescript": "^5",
20
+ },
21
+ },
22
+ },
23
+ "packages": {
24
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
25
+
26
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
27
+
28
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
29
+
30
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
31
+
32
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
33
+
34
+ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
35
+
36
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
37
+
38
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
39
+
40
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
41
+
42
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
43
+
44
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
45
+
46
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
47
+
48
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
49
+
50
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.10", "", {}, "sha512-SyXcVqry2hitPVvQtvXOhqsWyFhSycG/+LTLYXrcq8AFmd9FR7dyBSDB3f5Ol6IPkYOegk8P2Eg2kKPNSNiKGw=="],
51
+
52
+ "@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@6.0.2", "", { "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "javascript-natural-sort": "^0.7.1", "lodash-es": "^4.17.21", "minimatch": "^9.0.0", "parse-imports-exports": "^0.2.4" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-ember-template-tag": ">= 2.0.0", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-ember-template-tag", "prettier-plugin-svelte", "svelte"] }, "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA=="],
53
+
54
+ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
55
+
56
+ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
57
+
58
+ "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260122.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260122.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260122.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260122.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260122.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260122.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260122.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260122.2" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-+bVirfVpvhDG/oJWzA1c9HX6fC5Wu2hPv3LdBcpf0UtOu0KoWBN+B4L0wYY9gvxvvoxGmupdRCqaM8hRNlXzaw=="],
59
+
60
+ "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260122.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rpy7WzXpETCIc+QBfE8J1RpNxvb/5pnKh98DbC4gXpxHKrkjkg0LX86CJSoPaCF6xGcfRsegi49q+bXrtH4LWQ=="],
61
+
62
+ "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260122.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-s1Zn16TqesG2x4wx5cKciyfmffZRhJO2fBSN4fQ6bBWbrZHT8KnjJUpoRd8t+JAXsHfxDy4S/S0DcmX5eRfAHQ=="],
63
+
64
+ "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260122.2", "", { "os": "linux", "cpu": "arm" }, "sha512-ybrJVEuB9TmLWwdNl1ryUHeAm6KV3QcOKZUdfKhp6TjmuesOd1/2rdnGHmEcZlQDQt5+RtsCaxHWadvqDF1h3Q=="],
65
+
66
+ "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260122.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dMIOprNNvILRLKLlevs9fEE6Q6Bhikjv9OiQb3Sma3lcV/4lvh8DJ5hVKj2/aiGrK1000EC54UzwB6rLxnuw0w=="],
67
+
68
+ "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260122.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bbgb8a3dETxTTFUbSsCF7sSQ2oCsnyg7/HcFXi12TxdXscu1qod4/MxznBNOvwG/eYcrsLmI14k6BVRVSiGFoQ=="],
69
+
70
+ "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260122.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-JxPwmsaV6wuijnOo4qg5aIMrzQarTNVHqUaVP5pijkon8Y6dCvJ+3lYNEe4E6baiiw1u3npUQY4l/cvx2RL1eQ=="],
71
+
72
+ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260122.2", "", { "os": "win32", "cpu": "x64" }, "sha512-TJH+sSn7vxcGvaSm+fhcpW//dfSGBZGHrxoy0edAbE7UpFf8ub/3cILL5V1pCsF7w3aLNNgVKRpUkowdUgYcPQ=="],
73
+
74
+ "allmark": ["allmark@1.0.0", "", { "bin": { "allmark": "dist/bin/index.mjs" } }, "sha512-Stsjcu2cLPBwPlbfba/iMaDGVhJcm6pYU8hNtyTIlYHhckBvCOtiHhyBujl6PltcFnpvG0VQpwyBXWjwY1e6ZA=="],
75
+
76
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
77
+
78
+ "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
79
+
80
+ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
81
+
82
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
83
+
84
+ "javascript-natural-sort": ["javascript-natural-sort@0.7.1", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="],
85
+
86
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
87
+
88
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
89
+
90
+ "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
91
+
92
+ "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
93
+
94
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
95
+
96
+ "parse-imports-exports": ["parse-imports-exports@0.2.4", "", { "dependencies": { "parse-statements": "1.0.11" } }, "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ=="],
97
+
98
+ "parse-statements": ["parse-statements@1.0.11", "", {}, "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA=="],
99
+
100
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
101
+
102
+ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
103
+
104
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
105
+
106
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
107
+ }
108
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "opencode-miniterm",
3
+ "version": "1.0.0",
4
+ "description": "A small front-end terminal UI for OpenCode",
5
+ "main": "src/index.ts",
6
+ "bin": {
7
+ "ocmt": "src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "dev": "bun src/index.ts",
11
+ "test": "bun test",
12
+ "check": "tsgo --noEmit"
13
+ },
14
+ "keywords": [
15
+ "opencode",
16
+ "terminal"
17
+ ],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "devDependencies": {
21
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
22
+ "@types/bun": "latest",
23
+ "@typescript/native-preview": "7.0.0-dev.20260122.2",
24
+ "prettier": "^3.8.1",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "peerDependencies": {
28
+ "typescript": "^5"
29
+ },
30
+ "dependencies": {
31
+ "@opencode-ai/sdk": "^1.2.10",
32
+ "allmark": "^1.0.0"
33
+ }
34
+ }
package/src/ansi.ts ADDED
@@ -0,0 +1,22 @@
1
+ export const CLEAR_SCREEN = "\x1b[2J";
2
+ export const CLEAR_FROM_CURSOR = "\x1b[0J";
3
+ export const CLEAR_LINE = "\x1b[K";
4
+ export const CLEAR_SCREEN_UP = "\x1b[2A";
5
+ export const CURSOR_HOME = "\x1b[0G";
6
+ export const CURSOR_HIDE = "\x1b[?25l";
7
+ export const CURSOR_SHOW = "\x1b[?25h";
8
+ export const CURSOR_UP = (lines: number) => `\x1b[${lines}A`;
9
+ export const RESET = "\x1b[0m";
10
+ export const BRIGHT_WHITE = "\x1b[97m";
11
+ export const BRIGHT_BLACK = "\x1b[90m";
12
+ export const RED = "\x1b[31m";
13
+ export const GREEN = "\x1b[32m";
14
+ export const BLUE = "\x1b[34m";
15
+ export const CYAN = "\x1b[36m";
16
+ export const BOLD_MAGENTA = "\x1b[1;35m";
17
+ export const STRIKETHROUGH = "\x1b[9m";
18
+ export const ANSI_CODE_PATTERN = /^\x1b\[[0-9;]*m/;
19
+
20
+ export function stripAnsiCodes(str: string): string {
21
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
22
+ }
@@ -0,0 +1,178 @@
1
+ import type { Agent, OpencodeClient } from "@opencode-ai/sdk";
2
+ import readline, { type Key } from "node:readline";
3
+ import { config, saveConfig } from "../config";
4
+ import { getActiveDisplay, writePrompt } from "../render";
5
+ import type { Command } from "../types";
6
+
7
+ interface AgentInfo {
8
+ id: string;
9
+ name: string;
10
+ }
11
+
12
+ let agentList: AgentInfo[] = [];
13
+ let selectedAgentIndex = 0;
14
+ let agentListLineCount = 0;
15
+ let agentSearchString = "";
16
+ let agentFilteredIndices: number[] = [];
17
+
18
+ let command: Command = {
19
+ name: "/agents",
20
+ description: "List and select available agents",
21
+ run,
22
+ handleKey,
23
+ running: false,
24
+ };
25
+
26
+ export default command;
27
+
28
+ async function run(client: OpencodeClient): Promise<void> {
29
+ const result = await client.app.agents();
30
+
31
+ if (result.error) {
32
+ throw new Error(
33
+ `Failed to fetch agents (${result.response.status}): ${JSON.stringify(result.error)}`,
34
+ );
35
+ }
36
+
37
+ agentList = (result.data || []).map((agent: Agent) => ({
38
+ id: agent.name,
39
+ name: agent.name,
40
+ }));
41
+
42
+ agentSearchString = "";
43
+ updateAgentFilter();
44
+
45
+ command.running = true;
46
+
47
+ renderAgentList();
48
+ }
49
+
50
+ async function handleKey(client: OpencodeClient, key: Key, str?: string) {
51
+ switch (key.name) {
52
+ case "up": {
53
+ if (selectedAgentIndex === 0) {
54
+ selectedAgentIndex = agentFilteredIndices.length - 1;
55
+ } else {
56
+ selectedAgentIndex--;
57
+ }
58
+ renderAgentList();
59
+ return;
60
+ }
61
+ case "down": {
62
+ if (selectedAgentIndex === agentFilteredIndices.length - 1) {
63
+ selectedAgentIndex = 0;
64
+ } else {
65
+ selectedAgentIndex++;
66
+ }
67
+ renderAgentList();
68
+ return;
69
+ }
70
+ case "escape": {
71
+ clearAgentList();
72
+ process.stdout.write("\x1b[?25h");
73
+ command.running = false;
74
+ agentList = [];
75
+ selectedAgentIndex = 0;
76
+ agentListLineCount = 0;
77
+ agentSearchString = "";
78
+ agentFilteredIndices = [];
79
+ readline.cursorTo(process.stdout, 0);
80
+ readline.clearScreenDown(process.stdout);
81
+ writePrompt();
82
+ return;
83
+ }
84
+ case "return": {
85
+ agentListLineCount++;
86
+ clearAgentList();
87
+ process.stdout.write("\x1b[?25h");
88
+ const selectedIndex = agentFilteredIndices[selectedAgentIndex];
89
+ const selected = selectedIndex !== undefined ? agentList[selectedIndex] : undefined;
90
+ command.running = false;
91
+ agentList = [];
92
+ selectedAgentIndex = 0;
93
+ agentListLineCount = 0;
94
+ agentSearchString = "";
95
+ agentFilteredIndices = [];
96
+ readline.cursorTo(process.stdout, 0);
97
+ readline.clearScreenDown(process.stdout);
98
+ if (selected) {
99
+ config.agentID = selected.id;
100
+ saveConfig();
101
+ const activeDisplay = await getActiveDisplay(client);
102
+ console.log(activeDisplay);
103
+ console.log();
104
+ }
105
+ writePrompt();
106
+ return;
107
+ }
108
+ case "backspace": {
109
+ agentSearchString = agentSearchString.slice(0, -1);
110
+ updateAgentFilter();
111
+ selectedAgentIndex = 0;
112
+ renderAgentList();
113
+ return;
114
+ }
115
+ }
116
+
117
+ if (str && str.length === 1) {
118
+ agentSearchString += str;
119
+ updateAgentFilter();
120
+ selectedAgentIndex = 0;
121
+ renderAgentList();
122
+ return;
123
+ }
124
+ }
125
+
126
+ function clearAgentList() {
127
+ process.stdout.write("\x1b[?25l");
128
+ if (agentListLineCount > 0) {
129
+ process.stdout.write(`\x1b[${agentListLineCount}A`);
130
+ }
131
+ readline.cursorTo(process.stdout, 0);
132
+ readline.clearScreenDown(process.stdout);
133
+ }
134
+
135
+ function renderAgentList(): void {
136
+ clearAgentList();
137
+
138
+ agentListLineCount = 0;
139
+ console.log(" \x1b[36;1mAvailable Agents\x1b[0m");
140
+ agentListLineCount++;
141
+
142
+ if (agentSearchString) {
143
+ console.log(` \x1b[90mFilter: \x1b[0m\x1b[33m${agentSearchString}\x1b[0m`);
144
+ agentListLineCount++;
145
+ }
146
+
147
+ for (let i = 0; i < agentFilteredIndices.length; i++) {
148
+ const globalIndex = agentFilteredIndices[i]!;
149
+ const agent = agentList[globalIndex];
150
+ if (!agent) continue;
151
+ const isSelected = i === selectedAgentIndex;
152
+ const isActive = agent.id === config.agentID;
153
+ const prefix = isSelected ? " >" : " -";
154
+ const name = isSelected ? `\x1b[33;1m${agent.name}\x1b[0m` : agent.name;
155
+ const status = isActive ? " (active)" : "";
156
+
157
+ console.log(`${prefix} ${name}${status}`);
158
+ agentListLineCount++;
159
+ }
160
+ }
161
+
162
+ function updateAgentFilter(): void {
163
+ if (!agentSearchString) {
164
+ agentFilteredIndices = agentList.map((_, i) => i);
165
+ } else {
166
+ const search = agentSearchString.toLowerCase();
167
+ agentFilteredIndices = agentList
168
+ .map((agent, i) => ({ agent, index: i }))
169
+ .filter(({ agent }) => agent.name.toLowerCase().includes(search))
170
+ .map(({ index }) => index);
171
+ }
172
+ if (agentFilteredIndices.length > 0) {
173
+ selectedAgentIndex = agentFilteredIndices.indexOf(
174
+ agentList.findIndex((a) => a.id === config.agentID),
175
+ );
176
+ if (selectedAgentIndex === -1) selectedAgentIndex = 0;
177
+ }
178
+ }