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 +16 -0
- package/AGENTS.md +243 -0
- package/README.md +5 -0
- package/bun.lock +108 -0
- package/package.json +34 -0
- package/src/ansi.ts +22 -0
- package/src/commands/agents.ts +178 -0
- package/src/commands/debug.ts +74 -0
- package/src/commands/details.ts +17 -0
- package/src/commands/diff.ts +155 -0
- package/src/commands/exit.ts +17 -0
- package/src/commands/init.ts +35 -0
- package/src/commands/kill.ts +33 -0
- package/src/commands/log.ts +24 -0
- package/src/commands/models.ts +218 -0
- package/src/commands/new.ts +42 -0
- package/src/commands/page.ts +78 -0
- package/src/commands/quit.ts +17 -0
- package/src/commands/run.ts +34 -0
- package/src/commands/sessions.ts +257 -0
- package/src/commands/undo.ts +65 -0
- package/src/config.ts +56 -0
- package/src/index.ts +990 -0
- package/src/render.ts +320 -0
- package/src/types.ts +11 -0
- package/test/render.test.ts +390 -0
- package/test/test.ts +115 -0
- package/tsconfig.json +29 -0
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
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
|
+
}
|