opencode-cockpit 0.1.1 → 0.1.3
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 +41 -78
- package/package.json +7 -32
- package/src/compose.ts +43 -0
- package/src/features.ts +34 -0
- package/src/server.ts +13 -95
- package/src/tui.ts +13 -0
- package/src/connect.ts +0 -24
- package/src/tools/format.ts +0 -78
- package/src/tools/index.ts +0 -382
- package/src/tools/keys.ts +0 -33
- package/src/tui/badge.tsx +0 -17
- package/src/tui/console.tsx +0 -438
- package/src/tui/dock.tsx +0 -130
- package/src/tui/index.tsx +0 -264
- package/src/tui/keys.ts +0 -52
- package/src/tui/sidebar.tsx +0 -44
- package/src/tui/store.ts +0 -185
- package/src/tui/view.ts +0 -161
package/README.md
CHANGED
|
@@ -4,120 +4,83 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/opencode-cockpit)
|
|
5
5
|
[](https://github.com/Codestz/opencode-cockpit/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Superpowers for [OpenCode](https://opencode.ai).
|
|
7
|
+
Superpowers for [OpenCode](https://opencode.ai). Take all of them, or only the ones you want.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
opens, runs a slow test suite and gets a message when it finishes, drives REPLs and prompts.
|
|
14
|
-
- **Logs that don't waste tokens.** Colours are stripped, progress-bar redraws collapse to their
|
|
15
|
-
final frame, repeated lines fold, and reads continue from a cursor.
|
|
16
|
-
- **You see everything.** A docked panel under the chat, a sidebar section, and a keyboard-first
|
|
17
|
-
console with a real typing mode.
|
|
18
|
-
- **Shells outlive OpenCode.** They run in a small daemon, `cockpitd`, shared by every OpenCode
|
|
19
|
-
window and restarted automatically when the plugin updates.
|
|
9
|
+
| Feature | What it gives you | Package |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Shell** | Background terminals the agent starts, waits on and drives, with a docked panel and console for you | [`@opencode-cockpit/shell`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell) |
|
|
12
|
+
| **Agents** *(coming)* | A live, keyboard-first view of every subagent, without leaving your chat | `@opencode-cockpit/agents` |
|
|
20
13
|
|
|
21
14
|
## Install
|
|
22
15
|
|
|
16
|
+
**Everything**
|
|
17
|
+
|
|
23
18
|
```sh
|
|
24
19
|
opencode plugin opencode-cockpit --global
|
|
25
20
|
```
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
OpenCode. Requires OpenCode 1.18 or newer on macOS or Linux.
|
|
22
|
+
**Only what you want**
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|---|---|
|
|
34
|
-
| `shell_start` | Run a command in a background PTY. Optional `waitFor` blocks until a port opens, a pattern prints, output goes idle, or the process exits. Rerunning the same command in the same session reuses the shell. |
|
|
35
|
-
| `shell_wait` | Block on a condition instead of sleeping. |
|
|
36
|
-
| `shell_read` | Clean log from a cursor, with `grep`; or `view: "screen"` for full-screen programs. |
|
|
37
|
-
| `shell_send` | Type text or keys (`ctrl+c`, `up`, `enter`) and get the reply. |
|
|
38
|
-
| `shell_list` · `shell_stop` · `shell_restart` | Manage shells. |
|
|
39
|
-
|
|
40
|
-
The agent is messaged when a shell it started exits on its own.
|
|
41
|
-
|
|
42
|
-
Things to ask:
|
|
43
|
-
|
|
44
|
-
- *"Start the dev server in a background shell and wait until it's ready."*
|
|
45
|
-
- *"Run the test suite in the background, keep working on the parser, tell me if it fails."*
|
|
46
|
-
- *"Open a node REPL in a shell and check what `new URL('..', import.meta.url)` returns."*
|
|
24
|
+
```sh
|
|
25
|
+
opencode plugin @opencode-cockpit/shell --global
|
|
26
|
+
```
|
|
47
27
|
|
|
48
|
-
|
|
28
|
+
**Everything except some features**: switch them off on the plugin entry, in both `opencode.json`
|
|
29
|
+
and `tui.json`:
|
|
49
30
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| `ctrl+x i` · `/shell` | Open the shell console |
|
|
54
|
-
| `/shell-new` | Start a shell yourself |
|
|
55
|
-
| `/shells-clear` | Remove finished shells |
|
|
56
|
-
| `/shells-restart-daemon` | Restart `cockpitd` (asks first when shells are running) |
|
|
31
|
+
```json
|
|
32
|
+
{ "plugin": [["opencode-cockpit", { "features": { "shell": false } }]] }
|
|
33
|
+
```
|
|
57
34
|
|
|
58
|
-
|
|
59
|
-
and recent failures stay visible; everything else folds into `▸ N more`.
|
|
35
|
+
Restart OpenCode after installing. Requires OpenCode 1.18 or newer on macOS or Linux.
|
|
60
36
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
37
|
+
> Install a feature **either** through `opencode-cockpit` **or** on its own, not both. If both are
|
|
38
|
+
> configured, the first one loaded is used and OpenCode shows a warning telling you which entry to
|
|
39
|
+
> remove.
|
|
64
40
|
|
|
65
|
-
##
|
|
41
|
+
## Configuring features
|
|
66
42
|
|
|
67
|
-
|
|
43
|
+
Each feature's README lists its options. With `opencode-cockpit`, nest them under the feature's
|
|
44
|
+
name:
|
|
68
45
|
|
|
69
46
|
```json
|
|
70
47
|
{
|
|
71
48
|
"plugin": [
|
|
72
|
-
["opencode-cockpit", {
|
|
73
|
-
"dockHeight": 16,
|
|
74
|
-
"dockOpen": true,
|
|
75
|
-
"historyMinutes": 60,
|
|
76
|
-
"keybinds": { "cockpit.shells.dock": "<leader>j", "cockpit.shells.console": "<leader>k" }
|
|
77
|
-
}]
|
|
49
|
+
["opencode-cockpit", { "shell": { "dockHeight": 16, "historyMinutes": 60 } }]
|
|
78
50
|
]
|
|
79
51
|
}
|
|
80
52
|
```
|
|
81
53
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
| `COCKPIT_HOME` | `~/.cache/opencode-cockpit` | Socket, logs, process registry |
|
|
85
|
-
| `COCKPIT_IDLE_TIMEOUT_MS` | `600000` | Daemon exits after this long with no clients and no running shells |
|
|
86
|
-
| `COCKPIT_LOG_LEVEL` | `info` | `debug` for verbose daemon logs |
|
|
87
|
-
|
|
88
|
-
## Troubleshooting
|
|
89
|
-
|
|
90
|
-
| Problem | Look at |
|
|
91
|
-
|---|---|
|
|
92
|
-
| Tools fail with "did not start" | `~/.cache/opencode-cockpit/cockpitd.log` |
|
|
93
|
-
| Plugin not loading | newest file in `~/.local/share/opencode/log/` |
|
|
94
|
-
| Panel says the daemon runs older code | `/shells-restart-daemon` once your shells are done |
|
|
54
|
+
With a standalone package, put them directly on its entry:
|
|
55
|
+
`["@opencode-cockpit/shell", { "dockHeight": 16 }]`.
|
|
95
56
|
|
|
96
57
|
## How it works
|
|
97
58
|
|
|
98
59
|
```
|
|
99
|
-
OpenCode TUI thread ──
|
|
100
|
-
|
|
101
|
-
OpenCode server worker ─
|
|
60
|
+
OpenCode TUI thread ── feature plugins (tui) ──┐
|
|
61
|
+
├── unix socket, JSON-RPC ── cockpitd ── processes
|
|
62
|
+
OpenCode server worker ─ feature plugins (server) ┘
|
|
102
63
|
```
|
|
103
64
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
buffer (replay for late viewers). See [CONTRIBUTING.md](https://github.com/Codestz/opencode-cockpit/blob/main/CONTRIBUTING.md) for the full picture.
|
|
65
|
+
Each feature is a complete OpenCode plugin. Features that need long-lived processes share one
|
|
66
|
+
small daemon, `cockpitd`, which starts on demand, is shared by every OpenCode window, upgrades
|
|
67
|
+
itself when a newer plugin connects and exits when idle. See [CONTRIBUTING.md](https://github.com/Codestz/opencode-cockpit/blob/main/CONTRIBUTING.md).
|
|
108
68
|
|
|
109
69
|
| Package | Role |
|
|
110
70
|
|---|---|
|
|
111
|
-
| [`opencode-cockpit`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/opencode) |
|
|
112
|
-
| [`@opencode-cockpit/
|
|
113
|
-
| [`@opencode-cockpit/
|
|
71
|
+
| [`opencode-cockpit`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/opencode) | All features in one plugin, each can be switched off |
|
|
72
|
+
| [`@opencode-cockpit/shell`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell) | Shell feature |
|
|
73
|
+
| [`@opencode-cockpit/daemon`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/daemon) | `cockpitd`, the shared process host |
|
|
74
|
+
| [`@opencode-cockpit/client`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/client) | Typed, auto-spawning client and shared plugin helpers |
|
|
114
75
|
| [`@opencode-cockpit/protocol`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/protocol) | Wire contracts |
|
|
115
76
|
|
|
116
77
|
## Roadmap
|
|
117
78
|
|
|
118
|
-
- **Watchers
|
|
119
|
-
- **Agents
|
|
120
|
-
-
|
|
79
|
+
- **Watchers** (Shell): `tsc`, `eslint` and `vitest` shells that report only state changes.
|
|
80
|
+
- **Agents**: live subagent tree with a peek overlay.
|
|
81
|
+
- **Doctor**: one command that checks your setup (duplicate features, daemon health, config) and
|
|
82
|
+
tells you how to fix it.
|
|
83
|
+
- Coloured output in the Shell panel and console.
|
|
121
84
|
|
|
122
85
|
## License
|
|
123
86
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-cockpit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "OpenCode superpowers
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "OpenCode superpowers, all in one: installs every opencode-cockpit feature (Shell today), each can be switched off",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Codestz",
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
"shell",
|
|
22
22
|
"background",
|
|
23
23
|
"tui",
|
|
24
|
-
"terminal"
|
|
24
|
+
"terminal",
|
|
25
|
+
"agents"
|
|
25
26
|
],
|
|
26
27
|
"exports": {
|
|
27
28
|
"./server": "./src/server.ts",
|
|
28
|
-
"./tui": "./src/tui
|
|
29
|
+
"./tui": "./src/tui.ts"
|
|
29
30
|
},
|
|
30
31
|
"engines": {
|
|
31
32
|
"opencode": ">=1.18.0",
|
|
@@ -40,36 +41,10 @@
|
|
|
40
41
|
"access": "public"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@opencode-cockpit/
|
|
44
|
-
"@opencode-cockpit/daemon": "0.1.1",
|
|
45
|
-
"@opencode-cockpit/protocol": "0.1.1",
|
|
44
|
+
"@opencode-cockpit/shell": "0.1.3",
|
|
46
45
|
"@opencode-ai/plugin": "1.18.31"
|
|
47
46
|
},
|
|
48
|
-
"peerDependencies": {
|
|
49
|
-
"@opentui/core": ">=0.4.5",
|
|
50
|
-
"@opentui/keymap": ">=0.4.5",
|
|
51
|
-
"@opentui/solid": ">=0.4.5",
|
|
52
|
-
"solid-js": ">=1.9"
|
|
53
|
-
},
|
|
54
47
|
"devDependencies": {
|
|
55
|
-
"@
|
|
56
|
-
"@opentui/keymap": "0.4.5",
|
|
57
|
-
"@opentui/solid": "0.4.5",
|
|
58
|
-
"solid-js": "1.9.15",
|
|
59
|
-
"zod": "4.1.8"
|
|
60
|
-
},
|
|
61
|
-
"peerDependenciesMeta": {
|
|
62
|
-
"@opentui/core": {
|
|
63
|
-
"optional": true
|
|
64
|
-
},
|
|
65
|
-
"@opentui/keymap": {
|
|
66
|
-
"optional": true
|
|
67
|
-
},
|
|
68
|
-
"@opentui/solid": {
|
|
69
|
-
"optional": true
|
|
70
|
-
},
|
|
71
|
-
"solid-js": {
|
|
72
|
-
"optional": true
|
|
73
|
-
}
|
|
48
|
+
"@opencode-cockpit/client": "0.1.3"
|
|
74
49
|
}
|
|
75
50
|
}
|
package/src/compose.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Hooks } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merges the server hooks of several features into one plugin. Tool maps are unioned (a name
|
|
5
|
+
* clash is a bug, so it throws); function hooks run in feature order, each awaited, which is how
|
|
6
|
+
* OpenCode itself runs hooks from separate plugins.
|
|
7
|
+
*/
|
|
8
|
+
export function composeHooks(parts: Hooks[]): Hooks {
|
|
9
|
+
const tools: NonNullable<Hooks["tool"]> = {}
|
|
10
|
+
const functions = new Map<string, ((...args: unknown[]) => unknown)[]>()
|
|
11
|
+
const singles: Record<string, unknown> = {}
|
|
12
|
+
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
for (const [key, value] of Object.entries(part)) {
|
|
15
|
+
if (value === undefined) continue
|
|
16
|
+
if (key === "tool") {
|
|
17
|
+
for (const [id, def] of Object.entries(value as NonNullable<Hooks["tool"]>)) {
|
|
18
|
+
if (id in tools) throw new Error(`tool "${id}" is registered by more than one cockpit feature`)
|
|
19
|
+
tools[id] = def
|
|
20
|
+
}
|
|
21
|
+
} else if (typeof value === "function") {
|
|
22
|
+
const list = functions.get(key) ?? []
|
|
23
|
+
list.push(value as (...args: unknown[]) => unknown)
|
|
24
|
+
functions.set(key, list)
|
|
25
|
+
} else {
|
|
26
|
+
if (key in singles) throw new Error(`hook "${key}" is provided by more than one cockpit feature`)
|
|
27
|
+
singles[key] = value
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hooks: Record<string, unknown> = { ...singles }
|
|
33
|
+
for (const [key, list] of functions) {
|
|
34
|
+
hooks[key] =
|
|
35
|
+
list.length === 1
|
|
36
|
+
? list[0]
|
|
37
|
+
: async (...args: unknown[]) => {
|
|
38
|
+
for (const fn of list) await fn(...args)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (Object.keys(tools).length > 0) hooks.tool = tools
|
|
42
|
+
return hooks as Hooks
|
|
43
|
+
}
|
package/src/features.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Every feature the bundle can load. Adding one: list it here and wire it in server.ts / tui.ts. */
|
|
2
|
+
export const FEATURES = ["shell"] as const
|
|
3
|
+
export type Feature = (typeof FEATURES)[number]
|
|
4
|
+
|
|
5
|
+
export interface CockpitOptions {
|
|
6
|
+
/** Switch features off, e.g. `{ "shell": false }`. Everything is on by default. */
|
|
7
|
+
features?: Partial<Record<Feature, boolean>>
|
|
8
|
+
/** Options passed to a single feature, keyed by feature name. */
|
|
9
|
+
shell?: Record<string, unknown>
|
|
10
|
+
[key: string]: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const BUNDLE = "opencode-cockpit"
|
|
14
|
+
|
|
15
|
+
export function isEnabled(options: CockpitOptions | undefined, feature: Feature): boolean {
|
|
16
|
+
return options?.features?.[feature] !== false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for one feature. Shell also accepts options at the top level, which is where 0.1.x
|
|
21
|
+
* (when `opencode-cockpit` was Shell) read them from.
|
|
22
|
+
*/
|
|
23
|
+
export function featureOptions(
|
|
24
|
+
options: CockpitOptions | undefined,
|
|
25
|
+
feature: Feature,
|
|
26
|
+
): Record<string, unknown> {
|
|
27
|
+
const own = (options?.[feature] as Record<string, unknown> | undefined) ?? {}
|
|
28
|
+
if (feature !== "shell" || !options) return own
|
|
29
|
+
const legacy: Record<string, unknown> = {}
|
|
30
|
+
for (const [key, value] of Object.entries(options)) {
|
|
31
|
+
if (key !== "features" && !(FEATURES as readonly string[]).includes(key)) legacy[key] = value
|
|
32
|
+
}
|
|
33
|
+
return { ...legacy, ...own }
|
|
34
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,98 +1,16 @@
|
|
|
1
|
-
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const cockpit = createClient("opencode-cockpit/server")
|
|
14
|
-
const instance = crypto.randomUUID()
|
|
15
|
-
const quiet = new Set<string>()
|
|
16
|
-
|
|
17
|
-
const userShell =
|
|
18
|
-
process.env.SHELL && /(bash|zsh|fish|sh)$/.test(process.env.SHELL) ? process.env.SHELL : "/bin/bash"
|
|
19
|
-
const env = () => {
|
|
20
|
-
const out: Record<string, string> = {}
|
|
21
|
-
for (const [k, v] of Object.entries(process.env))
|
|
22
|
-
if (v !== undefined && !k.startsWith("OPENCODE_")) out[k] = v
|
|
23
|
-
return out
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Wake the agent when a shell it owns ends on its own.
|
|
27
|
-
cockpit.on("shell.exited", (info) => {
|
|
28
|
-
if (info.owner.instance !== instance || !info.owner.session) return
|
|
29
|
-
if (quiet.delete(info.id)) return
|
|
30
|
-
void notifyExit(info).catch(() => {})
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
async function notifyExit(info: ShellInfo): Promise<void> {
|
|
34
|
-
const session = info.owner.session as string
|
|
35
|
-
const page = await cockpit.call("shell.read", { id: info.id, tail: 15 })
|
|
36
|
-
const failed =
|
|
37
|
-
info.status === "failed" ||
|
|
38
|
-
(info.status === "exited" && info.exitCode !== 0) ||
|
|
39
|
-
info.status === "killed"
|
|
40
|
-
const text = [
|
|
41
|
-
`<shell_exited id="${info.id}" title="${info.title}">`,
|
|
42
|
-
describeStatus(info),
|
|
43
|
-
page.lines.length > 0 ? `last output:\n${formatLines(page.lines)}` : "(no output)",
|
|
44
|
-
"</shell_exited>",
|
|
45
|
-
failed
|
|
46
|
-
? `Investigate with shell_read id=${info.id} grep="error|fail" if the failure matters to the task.`
|
|
47
|
-
: `Full output: shell_read id=${info.id}.`,
|
|
48
|
-
].join("\n")
|
|
49
|
-
await opencode.session.promptAsync({
|
|
50
|
-
path: { id: session },
|
|
51
|
-
body: { parts: [{ type: "text", text, synthetic: true } as never] },
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
tool: createTools({
|
|
57
|
-
client: cockpit,
|
|
58
|
-
instance,
|
|
59
|
-
quiet,
|
|
60
|
-
env,
|
|
61
|
-
shellCommand: (command) => ({ command: userShell, args: ["-c", command] }),
|
|
62
|
-
}),
|
|
63
|
-
|
|
64
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
65
|
-
output.system.push(GUIDANCE)
|
|
66
|
-
const running = await cockpit
|
|
67
|
-
.call("shell.list", { owner: { project: directory }, includeExited: false })
|
|
68
|
-
.catch(() => [] as ShellInfo[])
|
|
69
|
-
if (running.length > 0) {
|
|
70
|
-
output.system.push(
|
|
71
|
-
`Background shells currently running in this project:\n${running
|
|
72
|
-
.slice(0, 15)
|
|
73
|
-
.map((s) => `- ${s.id} ${s.title} (${describeStatus(s)})`)
|
|
74
|
-
.join("\n")}`,
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
event: async ({ event }) => {
|
|
80
|
-
if (event.type !== "session.deleted") return
|
|
81
|
-
const sessionID = event.properties.info.id
|
|
82
|
-
const owned = await cockpit
|
|
83
|
-
.call("shell.list", { owner: { session: sessionID } })
|
|
84
|
-
.catch(() => [] as ShellInfo[])
|
|
85
|
-
for (const shell of owned) {
|
|
86
|
-
quiet.add(shell.id)
|
|
87
|
-
await cockpit.call("shell.remove", { id: shell.id }).catch(() => {})
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
dispose: async () => {
|
|
92
|
-
cockpit.close()
|
|
93
|
-
},
|
|
94
|
-
}
|
|
1
|
+
import type { Hooks, Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
+
import { createShellServer } from "@opencode-cockpit/shell/server"
|
|
3
|
+
import { composeHooks } from "./compose.ts"
|
|
4
|
+
import { BUNDLE, type CockpitOptions, featureOptions, isEnabled } from "./features.ts"
|
|
5
|
+
|
|
6
|
+
const shell = createShellServer({ source: BUNDLE })
|
|
7
|
+
|
|
8
|
+
const server: Plugin = async (input, rawOptions) => {
|
|
9
|
+
const options = rawOptions as CockpitOptions | undefined
|
|
10
|
+
const parts: Hooks[] = []
|
|
11
|
+
if (isEnabled(options, "shell")) parts.push(await shell(input, featureOptions(options, "shell")))
|
|
12
|
+
return composeHooks(parts)
|
|
95
13
|
}
|
|
96
14
|
|
|
97
|
-
const plugin: PluginModule & { id: string } = { id:
|
|
15
|
+
const plugin: PluginModule & { id: string } = { id: BUNDLE, server }
|
|
98
16
|
export default plugin
|
package/src/tui.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import { createShellTui } from "@opencode-cockpit/shell/tui"
|
|
3
|
+
import { BUNDLE, type CockpitOptions, featureOptions, isEnabled } from "./features.ts"
|
|
4
|
+
|
|
5
|
+
const shell = createShellTui({ source: BUNDLE })
|
|
6
|
+
|
|
7
|
+
const tui: TuiPlugin = async (api, rawOptions, meta) => {
|
|
8
|
+
const options = rawOptions as CockpitOptions | undefined
|
|
9
|
+
if (isEnabled(options, "shell")) await shell(api, featureOptions(options, "shell"), meta)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const plugin: TuiPluginModule & { id: string } = { id: BUNDLE, tui }
|
|
13
|
+
export default plugin
|
package/src/connect.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url"
|
|
2
|
-
import { CockpitClient } from "@opencode-cockpit/client"
|
|
3
|
-
import daemonPkg from "@opencode-cockpit/daemon/package.json" with { type: "json" }
|
|
4
|
-
import { daemonBuildId } from "@opencode-cockpit/protocol"
|
|
5
|
-
import pkg from "../package.json" with { type: "json" }
|
|
6
|
-
|
|
7
|
-
/** Resolves the daemon entry shipped with this package. */
|
|
8
|
-
export function daemonEntry(): string {
|
|
9
|
-
return fileURLToPath(import.meta.resolve("@opencode-cockpit/daemon/main"))
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Inside OpenCode `process.execPath` is the OpenCode binary; the client starts the daemon with
|
|
14
|
-
* BUN_BE_BUN=1 so it runs on OpenCode's embedded Bun (ADR 0001). The expected build lets the
|
|
15
|
-
* client replace a daemon left running from older plugin code.
|
|
16
|
-
*/
|
|
17
|
-
export function createClient(name: string): CockpitClient {
|
|
18
|
-
const entry = daemonEntry()
|
|
19
|
-
return new CockpitClient({
|
|
20
|
-
client: { name, version: pkg.version, pid: process.pid },
|
|
21
|
-
spawn: { entry, execPath: process.execPath },
|
|
22
|
-
expectedBuild: daemonBuildId(entry, daemonPkg.version),
|
|
23
|
-
})
|
|
24
|
-
}
|
package/src/tools/format.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { LogLine, ReadResult, ShellInfo, WaitResult } from "@opencode-cockpit/protocol/shell"
|
|
2
|
-
|
|
3
|
-
const MAX_LINE = 2000
|
|
4
|
-
|
|
5
|
-
/** Compact log lines for a model: numbered, long lines cut, consecutive repeats collapsed. */
|
|
6
|
-
export function formatLines(lines: LogLine[]): string {
|
|
7
|
-
const out: string[] = []
|
|
8
|
-
let i = 0
|
|
9
|
-
while (i < lines.length) {
|
|
10
|
-
const line = lines[i] as LogLine
|
|
11
|
-
let j = i + 1
|
|
12
|
-
while (j < lines.length && (lines[j] as LogLine).text === line.text) j++
|
|
13
|
-
const repeats = j - i
|
|
14
|
-
const text =
|
|
15
|
-
line.text.length > MAX_LINE
|
|
16
|
-
? `${line.text.slice(0, MAX_LINE)}… [${line.text.length - MAX_LINE} chars cut]`
|
|
17
|
-
: line.text
|
|
18
|
-
out.push(
|
|
19
|
-
repeats > 1
|
|
20
|
-
? `${line.n}| ${text} (×${repeats}, lines ${line.n}-${line.n + repeats - 1})`
|
|
21
|
-
: `${line.n}| ${text}`,
|
|
22
|
-
)
|
|
23
|
-
i = j
|
|
24
|
-
}
|
|
25
|
-
return out.join("\n")
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function describeStatus(info: ShellInfo): string {
|
|
29
|
-
switch (info.status) {
|
|
30
|
-
case "running":
|
|
31
|
-
return `running (pid ${info.pid}, up ${duration(Date.now() - info.startedAt)})`
|
|
32
|
-
case "exited":
|
|
33
|
-
return `exited with code ${info.exitCode ?? "?"} after ${duration((info.endedAt ?? Date.now()) - info.startedAt)}`
|
|
34
|
-
case "killed":
|
|
35
|
-
return `killed${info.signal ? ` by ${info.signal}` : ""} after ${duration((info.endedAt ?? Date.now()) - info.startedAt)}`
|
|
36
|
-
case "failed":
|
|
37
|
-
return `failed to start: ${info.error ?? "unknown error"}`
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function header(info: ShellInfo): string {
|
|
42
|
-
const run = info.run > 1 ? ` run=${info.run}` : ""
|
|
43
|
-
return `<shell id="${info.id}" title="${info.title.replaceAll('"', "'")}" status="${info.status}"${run}>`
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function formatRead(info: ShellInfo, page: ReadResult, empty = "(no output yet)"): string {
|
|
47
|
-
const parts = [header(info), `status: ${describeStatus(info)}`]
|
|
48
|
-
if (page.truncated)
|
|
49
|
-
parts.push(`(older lines were dropped from the buffer; oldest kept is ${page.firstLine})`)
|
|
50
|
-
parts.push(page.lines.length > 0 ? formatLines(page.lines) : empty)
|
|
51
|
-
if (page.hasMore) parts.push(`(more lines available: call shell_read with after=${page.nextCursor})`)
|
|
52
|
-
parts.push("</shell>", `cursor: ${page.nextCursor}`)
|
|
53
|
-
return parts.join("\n")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function formatWait(result: WaitResult, timeoutSeconds: number): string {
|
|
57
|
-
switch (result.reason) {
|
|
58
|
-
case "pattern":
|
|
59
|
-
return `condition met: pattern matched on line ${result.match?.n}: ${result.match?.text}`
|
|
60
|
-
case "port":
|
|
61
|
-
return "condition met: port is accepting connections"
|
|
62
|
-
case "idle":
|
|
63
|
-
return "condition met: no output for the idle window (the program may be waiting for input)"
|
|
64
|
-
case "exit":
|
|
65
|
-
return `process ended: ${describeStatus(result.info)}`
|
|
66
|
-
case "timeout":
|
|
67
|
-
return `timed out after ${timeoutSeconds}s without the condition being met; the shell is still ${result.info.status}`
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function duration(ms: number): string {
|
|
72
|
-
const s = Math.max(0, Math.round(ms / 1000))
|
|
73
|
-
if (s < 60) return `${s}s`
|
|
74
|
-
const m = Math.floor(s / 60)
|
|
75
|
-
if (m < 60) return `${m}m${s % 60 ? `${s % 60}s` : ""}`
|
|
76
|
-
const h = Math.floor(m / 60)
|
|
77
|
-
return `${h}h${m % 60 ? `${m % 60}m` : ""}`
|
|
78
|
-
}
|