@opencode-cockpit/shell 0.2.1 → 0.3.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 +20 -1
- package/dist/agent/plugin.js +2 -1
- package/dist/agent/tools/start.js +5 -0
- package/dist/connect.js +3 -2
- package/dist/core/config.js +5 -1
- package/dist/tui/components/badge.js +29 -9
- package/dist/tui/components/console.js +9 -0
- package/dist/tui/components/dock.js +113 -106
- package/dist/tui/components/sidebar.js +1 -0
- package/dist/tui/dialogs.js +48 -1
- package/dist/tui/index.js +19 -2
- package/dist/tui/lib/view.js +12 -3
- package/dist/tui/state/store.js +28 -4
- package/package.json +4 -4
- package/types/connect.d.ts +1 -1
- package/types/core/config.d.ts +10 -0
- package/types/tui/components/badge.d.ts +7 -1
- package/types/tui/dialogs.d.ts +5 -0
- package/types/tui/lib/view.d.ts +9 -1
- package/types/tui/state/store.d.ts +12 -1
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @opencode-cockpit/shell
|
|
2
2
|
|
|
3
|
+
**Bay 01 of [Cockpit](https://github.com/Codestz/opencode-cockpit).**
|
|
4
|
+
[Docs →](https://codestz.github.io/opencode-cockpit/shell/overview/) ·
|
|
5
|
+
[Agent tools](https://codestz.github.io/opencode-cockpit/shell/tools/) ·
|
|
6
|
+
[Watching health](https://codestz.github.io/opencode-cockpit/shell/watching/) ·
|
|
7
|
+
[Panel and keys](https://codestz.github.io/opencode-cockpit/shell/interface/) ·
|
|
8
|
+
[Configuration](https://codestz.github.io/opencode-cockpit/configuration/)
|
|
9
|
+
|
|
3
10
|
[](https://www.npmjs.com/package/@opencode-cockpit/shell)
|
|
4
11
|
|
|
5
12
|
Background terminals for [OpenCode](https://opencode.ai) that the agent starts, waits on and
|
|
@@ -15,6 +22,10 @@ drives, and that you watch and control without leaving the chat. Part of
|
|
|
15
22
|
- **Shells outlive OpenCode.** They run in a small daemon, `cockpitd`, shared by every OpenCode
|
|
16
23
|
window and upgraded automatically when the plugin updates.
|
|
17
24
|
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
*Recorded from a real session, re-recorded on every release.*
|
|
28
|
+
|
|
18
29
|
## Install
|
|
19
30
|
|
|
20
31
|
```sh
|
|
@@ -96,9 +107,12 @@ Things to ask:
|
|
|
96
107
|
| Key / command | Does |
|
|
97
108
|
|---|---|
|
|
98
109
|
| `ctrl+x o` · `/shells` | Toggle the shells panel under the chat |
|
|
110
|
+
| `s` (in the console) | This conversation only, or the whole project |
|
|
99
111
|
| `ctrl+x i` · `/shell` | Open the shell console |
|
|
100
112
|
| `/shell-new` | Start a shell yourself |
|
|
101
113
|
| `/shells-clear` | Remove finished shells |
|
|
114
|
+
| `/shells-stop` | Stop the shells in view — this conversation, or the project |
|
|
115
|
+
| `/shells-stop-all` | Stop every shell in the project, asking first about the ones you cannot see |
|
|
102
116
|
| `/cockpit-update` | Update the plugin when a newer release exists |
|
|
103
117
|
| `/shells-restart-daemon` | Restart `cockpitd` (asks first when shells are running) |
|
|
104
118
|
|
|
@@ -112,6 +126,10 @@ the program, `ctrl+]` to stop typing), `c` ctrl+c, `r` restart, `x` stop. Finish
|
|
|
112
126
|
again, `d` remove. Always: `tab` screen or log, `/` search the log (`backspace` clears the filter),
|
|
113
127
|
`?` details, `[` `]` switch shells, `D` clear finished, `esc` close.
|
|
114
128
|
|
|
129
|
+
**One conversation at a time.** The panel lists the shells of the session you are in, because a
|
|
130
|
+
project's shells mixed together is how it stops making sense. `s` in the console widens it to the
|
|
131
|
+
whole project and back, and `shell_list` takes `session: "this" | "others" | "any"`.
|
|
132
|
+
|
|
115
133
|
**Searching a long log.** In the console, `/` filters the scrollback to matching lines — keeping
|
|
116
134
|
their original line numbers and highlighting the match — so a 40k-line dev server is one query away
|
|
117
135
|
from the five lines you want. Filtering happens in the daemon, not the terminal.
|
|
@@ -152,11 +170,12 @@ working. (`XDG_CONFIG_HOME` is honoured for the global path.)
|
|
|
152
170
|
| `kinds` | Extra shell categories, or overrides, as name → regex matched against the command. Drives the badges in the sidebar, panel and `shell_list`, so you can group your own stack (`e2e`, `infra`, `worker`) instead of the built-ins. |
|
|
153
171
|
| `watch.presets` | Your own watch rules, or replacements for built-ins, keyed by name. A rule is `{ done?, fail?, ok?, idleSeconds?, ignoreCase? }` — patterns are regular expressions matched against each output line. The agent can then ask for `watch: "e2e"`. |
|
|
154
172
|
| `watch.auto` | Attach a matching preset to every new shell without being asked. **Off by default**: watching is useful, but silently attaching rules to commands you didn't opt in surprises people. Turn it on once you trust your presets. |
|
|
173
|
+
| `lifecycle` | When shells end by themselves. `onExit` (`stopMine` by default) stops this window's shells when it closes; `keep` leaves them for the next window. `orphanAfterMinutes` (60) stops a shell that no window has claimed for that long — the answer to shells running for a week. |
|
|
155
174
|
| `defaults` | Applied to every shell the agent starts unless the call says otherwise: `watch` (`true` for a matching preset, or a preset name), `logFile`, `idleTimeoutSeconds`, `timeoutSeconds`, `notifyOnExit`. |
|
|
156
175
|
| `notify` | What may interrupt the agent — `exit`, `watch`, and `tailLines` (output lines included in an exit message). |
|
|
157
176
|
| `guidance` | The system-prompt paragraph that teaches the agent when to use shells. `false` saves ~120 tokens per request, at the cost of a model that uses shells less well. |
|
|
158
177
|
| `listRunningShells` | How many running shells are listed in the system prompt each turn (~20 tokens each). `0` disables it; the agent can still call `shell_list`. |
|
|
159
|
-
| `ui` | Interface only: `dockHeight`, `dockOpen
|
|
178
|
+
| `ui` | Interface only: `dockHeight`, `dockOpen` (set it and the panel always starts that way; leave it out and it starts as you last left it), `sidebarRows`, `historyMinutes`, `colors`, `defaultView` (`"screen"` or `"log"`), `keybinds`, `updateCheck`. |
|
|
160
179
|
|
|
161
180
|
The same settings can go on the plugin entry instead, which is handy for one-offs and for machines
|
|
162
181
|
where you'd rather keep everything in `tui.json`/`opencode.json`:
|
package/dist/agent/plugin.js
CHANGED
|
@@ -44,8 +44,9 @@ async function shellHooks({
|
|
|
44
44
|
directory
|
|
45
45
|
}, options) {
|
|
46
46
|
const config = loadConfig(directory, options);
|
|
47
|
-
|
|
47
|
+
// Identifies this OpenCode window to the daemon, so shells can end with it.
|
|
48
48
|
const instance = crypto.randomUUID();
|
|
49
|
+
const cockpit = createClient("opencode-cockpit/server", instance);
|
|
49
50
|
const quiet = new Set();
|
|
50
51
|
const userShell = process.env.SHELL && /(bash|zsh|fish|sh)$/.test(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
|
|
51
52
|
const env = () => {
|
|
@@ -66,6 +66,9 @@ export function shellStart(kit) {
|
|
|
66
66
|
const logFile = args.logFile ?? defaults.logFile ?? false;
|
|
67
67
|
const notifyOnExit = args.notifyOnExit ?? defaults.notifyOnExit ?? true;
|
|
68
68
|
const watch = args.watch ?? defaults.watch ?? (config.watch?.auto ? "auto" : undefined);
|
|
69
|
+
// Nothing should outlive the window that asked for it by more than the user allows.
|
|
70
|
+
const lifecycle = config.lifecycle ?? {};
|
|
71
|
+
const orphanMinutes = lifecycle.orphanAfterMinutes ?? 60;
|
|
69
72
|
const shell = deps.shellCommand(args.command);
|
|
70
73
|
const info = await client.call("shell.start", {
|
|
71
74
|
command: shell.command,
|
|
@@ -84,6 +87,8 @@ export function shellStart(kit) {
|
|
|
84
87
|
timeoutMs: seconds(args.timeoutSeconds ?? defaults.timeoutSeconds),
|
|
85
88
|
idleTimeoutMs: seconds(args.idleTimeoutSeconds ?? defaults.idleTimeoutSeconds),
|
|
86
89
|
logFile: logFile === true,
|
|
90
|
+
stopOnExit: (lifecycle.onExit ?? "stopMine") === "stopMine",
|
|
91
|
+
orphanAfterMs: orphanMinutes > 0 ? orphanMinutes * 60_000 : undefined,
|
|
87
92
|
reuse: true
|
|
88
93
|
});
|
|
89
94
|
if (notifyOnExit === false) deps.quiet.add(info.id);else deps.quiet.delete(info.id);
|
package/dist/connect.js
CHANGED
|
@@ -14,13 +14,14 @@ export function daemonEntry() {
|
|
|
14
14
|
* BUN_BE_BUN=1 so it runs on OpenCode's embedded Bun (ADR 0001). The expected build lets the
|
|
15
15
|
* client replace a daemon left running from older plugin code.
|
|
16
16
|
*/
|
|
17
|
-
export function createClient(name) {
|
|
17
|
+
export function createClient(name, instance) {
|
|
18
18
|
const entry = daemonEntry();
|
|
19
19
|
return new CockpitClient({
|
|
20
20
|
client: {
|
|
21
21
|
name,
|
|
22
22
|
version: pkg.version,
|
|
23
|
-
pid: process.pid
|
|
23
|
+
pid: process.pid,
|
|
24
|
+
instance
|
|
24
25
|
},
|
|
25
26
|
spawn: {
|
|
26
27
|
entry,
|
package/dist/core/config.js
CHANGED
|
@@ -53,6 +53,10 @@ export function mergeConfig(base, over) {
|
|
|
53
53
|
...base.defaults,
|
|
54
54
|
...over.defaults
|
|
55
55
|
},
|
|
56
|
+
lifecycle: {
|
|
57
|
+
...base.lifecycle,
|
|
58
|
+
...over.lifecycle
|
|
59
|
+
},
|
|
56
60
|
notify: {
|
|
57
61
|
...base.notify,
|
|
58
62
|
...over.notify
|
|
@@ -76,7 +80,7 @@ function asConfig(input) {
|
|
|
76
80
|
if (!input || typeof input !== "object") return {};
|
|
77
81
|
const raw = input;
|
|
78
82
|
const config = {};
|
|
79
|
-
for (const key of ["watch", "kinds", "defaults", "notify", "ui"]) {
|
|
83
|
+
for (const key of ["watch", "kinds", "defaults", "lifecycle", "notify", "ui"]) {
|
|
80
84
|
const value = raw[key];
|
|
81
85
|
if (value && typeof value === "object") Object.assign(config, {
|
|
82
86
|
[key]: value
|
|
@@ -5,25 +5,45 @@ import { setProp as _$setProp } from "@opentui/solid";
|
|
|
5
5
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
6
6
|
/** @jsxImportSource @opentui/solid */
|
|
7
7
|
|
|
8
|
-
import { badgeText, kindColor, kindOf } from "../lib/view.js";
|
|
8
|
+
import { BADGE_RULE, badgeText, kindColor, kindOf } from "../lib/view.js";
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Status mark: a coloured rule and its label.
|
|
12
|
+
*
|
|
13
|
+
* Not a filled pill. A block of colour has to be as wide as the word inside it, and a list of
|
|
14
|
+
* them reads as a wall of colour rather than as a list of shells; the rule carries the same
|
|
15
|
+
* meaning in one column.
|
|
16
|
+
*/
|
|
11
17
|
export function Badge(props) {
|
|
12
18
|
const theme = () => props.api.theme.current;
|
|
13
19
|
const kind = () => kindOf(props.shell);
|
|
20
|
+
const colour = () => kindColor(theme(), kind());
|
|
14
21
|
return (() => {
|
|
15
22
|
var _el$ = _$createElement("text"),
|
|
16
23
|
_el$2 = _$createElement("span"),
|
|
17
|
-
_el$3 = _$createElement("
|
|
24
|
+
_el$3 = _$createElement("span"),
|
|
25
|
+
_el$4 = _$createElement("b");
|
|
18
26
|
_$insertNode(_el$, _el$2);
|
|
27
|
+
_$insertNode(_el$, _el$3);
|
|
19
28
|
_$setProp(_el$, "flexShrink", 0);
|
|
20
29
|
_$setProp(_el$, "wrapMode", "none");
|
|
21
|
-
_$
|
|
22
|
-
_$
|
|
23
|
-
_$
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
_$insert(_el$2, BADGE_RULE);
|
|
31
|
+
_$insertNode(_el$3, _el$4);
|
|
32
|
+
_$insert(_el$4, () => badgeText(kind(), props.frame).slice(BADGE_RULE.length));
|
|
33
|
+
_$effect(_p$ => {
|
|
34
|
+
var _v$ = {
|
|
35
|
+
fg: colour()
|
|
36
|
+
},
|
|
37
|
+
_v$2 = {
|
|
38
|
+
fg: colour()
|
|
39
|
+
};
|
|
40
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "style", _v$, _p$.e));
|
|
41
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "style", _v$2, _p$.t));
|
|
42
|
+
return _p$;
|
|
43
|
+
}, {
|
|
44
|
+
e: undefined,
|
|
45
|
+
t: undefined
|
|
46
|
+
});
|
|
27
47
|
return _el$;
|
|
28
48
|
})();
|
|
29
49
|
}
|
|
@@ -271,6 +271,10 @@ export function Console(props) {
|
|
|
271
271
|
name: "cockpit.console.top",
|
|
272
272
|
title: "Scroll to top",
|
|
273
273
|
run: () => scroll?.scrollTo(0)
|
|
274
|
+
}, {
|
|
275
|
+
name: "cockpit.console.scope",
|
|
276
|
+
title: "This session / whole project",
|
|
277
|
+
run: () => props.store.toggleScope()
|
|
274
278
|
}, {
|
|
275
279
|
name: "cockpit.console.close",
|
|
276
280
|
title: "Close console",
|
|
@@ -328,6 +332,10 @@ export function Console(props) {
|
|
|
328
332
|
key: "n",
|
|
329
333
|
cmd: "cockpit.console.new",
|
|
330
334
|
desc: "New"
|
|
335
|
+
}, {
|
|
336
|
+
key: "s",
|
|
337
|
+
cmd: "cockpit.console.scope",
|
|
338
|
+
desc: "Scope"
|
|
331
339
|
}, {
|
|
332
340
|
key: "j,down",
|
|
333
341
|
cmd: "cockpit.console.down",
|
|
@@ -380,6 +388,7 @@ export function Console(props) {
|
|
|
380
388
|
if (view() !== "details") keys.push(label("/ search log", "/"));
|
|
381
389
|
keys.push(label("? details", "?"));
|
|
382
390
|
if (props.store.shells().length > 1) keys.push(label("[ ] switch", "[ ]"));
|
|
391
|
+
keys.push(props.store.scope() === "session" ? label("s whole project", "s project") : label("s this session", "s session"));
|
|
383
392
|
if (finished() > 0) keys.push(label("D clear done", "D"));
|
|
384
393
|
keys.push(label("n new", "n"), "esc");
|
|
385
394
|
return keys.join(" · ");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { effect as _$effect } from "@opentui/solid";
|
|
2
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2
3
|
import { insert as _$insert } from "@opentui/solid";
|
|
3
4
|
import { memo as _$memo } from "@opentui/solid";
|
|
4
|
-
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
5
5
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
6
6
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
7
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
@@ -33,15 +33,17 @@ export function Dock(props) {
|
|
|
33
33
|
_el$2 = _$createElement("box"),
|
|
34
34
|
_el$3 = _$createElement("text"),
|
|
35
35
|
_el$4 = _$createElement("b"),
|
|
36
|
-
_el$
|
|
37
|
-
_el$8 = _$createElement("
|
|
36
|
+
_el$6 = _$createElement("text"),
|
|
37
|
+
_el$8 = _$createElement("box"),
|
|
38
|
+
_el$9 = _$createElement("text");
|
|
38
39
|
_$insertNode(_el$, _el$2);
|
|
39
40
|
_$setProp(_el$, "flexShrink", 0);
|
|
40
41
|
_$setProp(_el$, "flexDirection", "column");
|
|
41
42
|
_$setProp(_el$, "border", ["top"]);
|
|
42
43
|
_$insertNode(_el$2, _el$3);
|
|
43
|
-
_$insertNode(_el$2, _el$
|
|
44
|
+
_$insertNode(_el$2, _el$6);
|
|
44
45
|
_$insertNode(_el$2, _el$8);
|
|
46
|
+
_$insertNode(_el$2, _el$9);
|
|
45
47
|
_$setProp(_el$2, "flexDirection", "row");
|
|
46
48
|
_$setProp(_el$2, "gap", 1);
|
|
47
49
|
_$setProp(_el$2, "paddingLeft", 1);
|
|
@@ -52,17 +54,19 @@ export function Dock(props) {
|
|
|
52
54
|
_$insertNode(_el$3, _el$4);
|
|
53
55
|
_$setProp(_el$3, "flexShrink", 0);
|
|
54
56
|
_$insertNode(_el$4, _$createTextNode(`Shells`));
|
|
57
|
+
_$setProp(_el$6, "flexShrink", 0);
|
|
58
|
+
_$insert(_el$6, () => props.store.scope() === "session" ? "this session" : "project");
|
|
55
59
|
_$insert(_el$2, _$createComponent(Show, {
|
|
56
60
|
get when() {
|
|
57
61
|
return props.store.shells().length > 0;
|
|
58
62
|
},
|
|
59
63
|
get fallback() {
|
|
60
64
|
return (() => {
|
|
61
|
-
var _el$
|
|
62
|
-
_$insertNode(_el$
|
|
63
|
-
_$setProp(_el$
|
|
64
|
-
_$effect(_$p => _$setProp(_el$
|
|
65
|
-
return _el$
|
|
65
|
+
var _el$0 = _$createElement("text");
|
|
66
|
+
_$insertNode(_el$0, _$createTextNode(`none yet · the agent starts them, or /shell-new`));
|
|
67
|
+
_$setProp(_el$0, "wrapMode", "none");
|
|
68
|
+
_$effect(_$p => _$setProp(_el$0, "fg", theme().textMuted, _$p));
|
|
69
|
+
return _el$0;
|
|
66
70
|
})();
|
|
67
71
|
},
|
|
68
72
|
get children() {
|
|
@@ -73,15 +77,15 @@ export function Dock(props) {
|
|
|
73
77
|
children: shell => {
|
|
74
78
|
const active = () => props.store.selected()?.id === shell.id;
|
|
75
79
|
return (() => {
|
|
76
|
-
var _el$
|
|
77
|
-
_el$
|
|
78
|
-
_el$
|
|
79
|
-
_$insertNode(_el$
|
|
80
|
-
_$setProp(_el$
|
|
81
|
-
_$setProp(_el$
|
|
82
|
-
_$setProp(_el$
|
|
83
|
-
_$setProp(_el$
|
|
84
|
-
_$insert(_el$
|
|
80
|
+
var _el$10 = _$createElement("box"),
|
|
81
|
+
_el$11 = _$createElement("text"),
|
|
82
|
+
_el$12 = _$createTextNode(` `);
|
|
83
|
+
_$insertNode(_el$10, _el$11);
|
|
84
|
+
_$setProp(_el$10, "flexDirection", "row");
|
|
85
|
+
_$setProp(_el$10, "flexShrink", 0);
|
|
86
|
+
_$setProp(_el$10, "paddingRight", 1);
|
|
87
|
+
_$setProp(_el$10, "onMouseUp", () => active() ? props.onOpenConsole(shell.id) : props.store.select(shell.id));
|
|
88
|
+
_$insert(_el$10, _$createComponent(Badge, {
|
|
85
89
|
get api() {
|
|
86
90
|
return props.api;
|
|
87
91
|
},
|
|
@@ -89,28 +93,28 @@ export function Dock(props) {
|
|
|
89
93
|
get frame() {
|
|
90
94
|
return props.store.frame();
|
|
91
95
|
}
|
|
92
|
-
}), _el$
|
|
93
|
-
_$insertNode(_el$
|
|
94
|
-
_$setProp(_el$
|
|
95
|
-
_$insert(_el$
|
|
96
|
+
}), _el$11);
|
|
97
|
+
_$insertNode(_el$11, _el$12);
|
|
98
|
+
_$setProp(_el$11, "wrapMode", "none");
|
|
99
|
+
_$insert(_el$11, (() => {
|
|
96
100
|
var _c$2 = _$memo(() => !!active());
|
|
97
101
|
return () => _c$2() ? (() => {
|
|
98
|
-
var _el$
|
|
99
|
-
_$insert(_el$
|
|
100
|
-
return _el$
|
|
102
|
+
var _el$13 = _$createElement("b");
|
|
103
|
+
_$insert(_el$13, () => truncate(shell.title, 22));
|
|
104
|
+
return _el$13;
|
|
101
105
|
})() : truncate(shell.title, 22);
|
|
102
106
|
})(), null);
|
|
103
107
|
_$effect(_p$ => {
|
|
104
|
-
var _v$
|
|
105
|
-
_v$
|
|
106
|
-
_v$
|
|
107
|
-
_v$
|
|
108
|
+
var _v$7 = active() ? theme().backgroundElement : theme().backgroundPanel,
|
|
109
|
+
_v$8 = active() ? theme().text : theme().textMuted;
|
|
110
|
+
_v$7 !== _p$.e && (_p$.e = _$setProp(_el$10, "backgroundColor", _v$7, _p$.e));
|
|
111
|
+
_v$8 !== _p$.t && (_p$.t = _$setProp(_el$11, "fg", _v$8, _p$.t));
|
|
108
112
|
return _p$;
|
|
109
113
|
}, {
|
|
110
114
|
e: undefined,
|
|
111
115
|
t: undefined
|
|
112
116
|
});
|
|
113
|
-
return _el$
|
|
117
|
+
return _el$10;
|
|
114
118
|
})();
|
|
115
119
|
}
|
|
116
120
|
}), _$createComponent(Show, {
|
|
@@ -118,132 +122,132 @@ export function Dock(props) {
|
|
|
118
122
|
return overflow() > 0 || props.store.showAll();
|
|
119
123
|
},
|
|
120
124
|
get children() {
|
|
121
|
-
var _el$
|
|
122
|
-
_$setProp(_el$
|
|
123
|
-
_$setProp(_el$
|
|
124
|
-
_$setProp(_el$
|
|
125
|
-
_$insert(_el$
|
|
125
|
+
var _el$7 = _$createElement("text");
|
|
126
|
+
_$setProp(_el$7, "wrapMode", "none");
|
|
127
|
+
_$setProp(_el$7, "flexShrink", 0);
|
|
128
|
+
_$setProp(_el$7, "onMouseUp", () => props.store.toggleAll());
|
|
129
|
+
_$insert(_el$7, (() => {
|
|
126
130
|
var _c$ = _$memo(() => !!props.store.showAll());
|
|
127
131
|
return () => _c$() ? "▾ fewer" : `▸ ${overflow()} more`;
|
|
128
132
|
})());
|
|
129
|
-
_$effect(_$p => _$setProp(_el$
|
|
130
|
-
return _el$
|
|
133
|
+
_$effect(_$p => _$setProp(_el$7, "fg", theme().textMuted, _$p));
|
|
134
|
+
return _el$7;
|
|
131
135
|
}
|
|
132
136
|
})];
|
|
133
137
|
}
|
|
134
|
-
}), _el$
|
|
135
|
-
_$setProp(_el$
|
|
136
|
-
_$setProp(_el$
|
|
137
|
-
_$setProp(_el$
|
|
138
|
-
_$insert(_el$
|
|
138
|
+
}), _el$8);
|
|
139
|
+
_$setProp(_el$8, "flexGrow", 1);
|
|
140
|
+
_$setProp(_el$9, "wrapMode", "none");
|
|
141
|
+
_$setProp(_el$9, "flexShrink", 0);
|
|
142
|
+
_$insert(_el$9, () => props.hint());
|
|
139
143
|
_$insert(_el$, _$createComponent(Show, {
|
|
140
144
|
get when() {
|
|
141
145
|
return props.store.selected();
|
|
142
146
|
},
|
|
143
147
|
children: shell => [(() => {
|
|
144
|
-
var _el$
|
|
145
|
-
_$setProp(_el$
|
|
146
|
-
_$setProp(_el$
|
|
147
|
-
_$setProp(_el$
|
|
148
|
-
_$setProp(_el$
|
|
149
|
-
_$setProp(_el$
|
|
150
|
-
_$setProp(_el$
|
|
151
|
-
_$insert(_el$
|
|
148
|
+
var _el$14 = _$createElement("box");
|
|
149
|
+
_$setProp(_el$14, "flexGrow", 1);
|
|
150
|
+
_$setProp(_el$14, "paddingLeft", 2);
|
|
151
|
+
_$setProp(_el$14, "paddingRight", 1);
|
|
152
|
+
_$setProp(_el$14, "minHeight", 0);
|
|
153
|
+
_$setProp(_el$14, "overflow", "hidden");
|
|
154
|
+
_$setProp(_el$14, "onMouseUp", () => props.onOpenConsole(shell().id));
|
|
155
|
+
_$insert(_el$14, _$createComponent(Show, {
|
|
152
156
|
get when() {
|
|
153
157
|
return bodyRuns().length > 0;
|
|
154
158
|
},
|
|
155
159
|
get fallback() {
|
|
156
160
|
return (() => {
|
|
157
|
-
var _el$
|
|
158
|
-
_$setProp(_el$
|
|
159
|
-
_$insert(_el$
|
|
160
|
-
_$effect(_$p => _$setProp(_el$
|
|
161
|
-
return _el$
|
|
161
|
+
var _el$20 = _$createElement("text");
|
|
162
|
+
_$setProp(_el$20, "wrapMode", "none");
|
|
163
|
+
_$insert(_el$20, () => body() || " ");
|
|
164
|
+
_$effect(_$p => _$setProp(_el$20, "fg", theme().text, _$p));
|
|
165
|
+
return _el$20;
|
|
162
166
|
})();
|
|
163
167
|
},
|
|
164
168
|
get children() {
|
|
165
|
-
var _el$
|
|
166
|
-
_$setProp(_el$
|
|
167
|
-
_$insert(_el$
|
|
169
|
+
var _el$15 = _$createElement("box");
|
|
170
|
+
_$setProp(_el$15, "flexDirection", "column");
|
|
171
|
+
_$insert(_el$15, _$createComponent(For, {
|
|
168
172
|
get each() {
|
|
169
173
|
return bodyRuns();
|
|
170
174
|
},
|
|
171
175
|
children: row => (() => {
|
|
172
|
-
var _el$
|
|
173
|
-
_el$
|
|
174
|
-
_$insertNode(_el$
|
|
175
|
-
_$setProp(_el$
|
|
176
|
-
_$insert(_el$
|
|
176
|
+
var _el$21 = _$createElement("text"),
|
|
177
|
+
_el$22 = _$createTextNode(` `);
|
|
178
|
+
_$insertNode(_el$21, _el$22);
|
|
179
|
+
_$setProp(_el$21, "wrapMode", "none");
|
|
180
|
+
_$insert(_el$21, _$createComponent(For, {
|
|
177
181
|
each: row,
|
|
178
182
|
children: run => (() => {
|
|
179
|
-
var _el$
|
|
180
|
-
_$insert(_el$
|
|
181
|
-
_$effect(_$p => _$setProp(_el$
|
|
183
|
+
var _el$23 = _$createElement("span");
|
|
184
|
+
_$insert(_el$23, () => run.text);
|
|
185
|
+
_$effect(_$p => _$setProp(_el$23, "style", {
|
|
182
186
|
fg: run.fg,
|
|
183
187
|
bg: run.bg,
|
|
184
188
|
bold: run.bold,
|
|
185
189
|
italic: run.italic
|
|
186
190
|
}, _$p));
|
|
187
|
-
return _el$
|
|
191
|
+
return _el$23;
|
|
188
192
|
})()
|
|
189
|
-
}), _el$
|
|
190
|
-
_$effect(_$p => _$setProp(_el$
|
|
191
|
-
return _el$
|
|
193
|
+
}), _el$22);
|
|
194
|
+
_$effect(_$p => _$setProp(_el$21, "fg", theme().text, _$p));
|
|
195
|
+
return _el$21;
|
|
192
196
|
})()
|
|
193
197
|
}));
|
|
194
|
-
return _el$
|
|
198
|
+
return _el$15;
|
|
195
199
|
}
|
|
196
200
|
}));
|
|
197
|
-
return _el$
|
|
201
|
+
return _el$14;
|
|
198
202
|
})(), (() => {
|
|
199
|
-
var _el$
|
|
200
|
-
_el$
|
|
201
|
-
_$insertNode(_el$
|
|
202
|
-
_$setProp(_el$
|
|
203
|
-
_$setProp(_el$
|
|
204
|
-
_$setProp(_el$
|
|
205
|
-
_$setProp(_el$15, "flexShrink", 0);
|
|
206
|
-
_$setProp(_el$15, "height", 1);
|
|
207
|
-
_$setProp(_el$15, "overflow", "hidden");
|
|
208
|
-
_$setProp(_el$16, "wrapMode", "none");
|
|
203
|
+
var _el$16 = _$createElement("box"),
|
|
204
|
+
_el$17 = _$createElement("text");
|
|
205
|
+
_$insertNode(_el$16, _el$17);
|
|
206
|
+
_$setProp(_el$16, "flexDirection", "row");
|
|
207
|
+
_$setProp(_el$16, "gap", 1);
|
|
208
|
+
_$setProp(_el$16, "paddingLeft", 2);
|
|
209
209
|
_$setProp(_el$16, "flexShrink", 0);
|
|
210
|
-
_$
|
|
211
|
-
_$
|
|
210
|
+
_$setProp(_el$16, "height", 1);
|
|
211
|
+
_$setProp(_el$16, "overflow", "hidden");
|
|
212
|
+
_$setProp(_el$17, "wrapMode", "none");
|
|
213
|
+
_$setProp(_el$17, "flexShrink", 0);
|
|
214
|
+
_$insert(_el$17, () => statusDetail(shell(), props.store.now()));
|
|
215
|
+
_$insert(_el$16, _$createComponent(Show, {
|
|
212
216
|
get when() {
|
|
213
217
|
return watchLabel(shell());
|
|
214
218
|
},
|
|
215
219
|
get children() {
|
|
216
|
-
var _el$
|
|
217
|
-
_$setProp(_el$
|
|
218
|
-
_$setProp(_el$
|
|
219
|
-
_$insert(_el$
|
|
220
|
-
_$effect(_$p => _$setProp(_el$
|
|
221
|
-
return _el$
|
|
220
|
+
var _el$18 = _$createElement("text");
|
|
221
|
+
_$setProp(_el$18, "wrapMode", "none");
|
|
222
|
+
_$setProp(_el$18, "flexShrink", 0);
|
|
223
|
+
_$insert(_el$18, () => watchLabel(shell()));
|
|
224
|
+
_$effect(_$p => _$setProp(_el$18, "fg", watchColor(theme(), shell()), _$p));
|
|
225
|
+
return _el$18;
|
|
222
226
|
}
|
|
223
227
|
}), null);
|
|
224
|
-
_$insert(_el$
|
|
228
|
+
_$insert(_el$16, _$createComponent(Show, {
|
|
225
229
|
get when() {
|
|
226
230
|
return _$memo(() => kindOf(shell()) === "fail")() && shell().summary;
|
|
227
231
|
},
|
|
228
232
|
get fallback() {
|
|
229
233
|
return (() => {
|
|
230
|
-
var _el$
|
|
231
|
-
_$setProp(_el$
|
|
232
|
-
_$insert(_el$
|
|
233
|
-
_$effect(_$p => _$setProp(_el$
|
|
234
|
-
return _el$
|
|
234
|
+
var _el$24 = _$createElement("text");
|
|
235
|
+
_$setProp(_el$24, "wrapMode", "none");
|
|
236
|
+
_$insert(_el$24, () => truncate(`$ ${displayCommand(shell())}`, Math.max(20, dims().width - 40)));
|
|
237
|
+
_$effect(_$p => _$setProp(_el$24, "fg", theme().textMuted, _$p));
|
|
238
|
+
return _el$24;
|
|
235
239
|
})();
|
|
236
240
|
},
|
|
237
241
|
get children() {
|
|
238
|
-
var _el$
|
|
239
|
-
_$setProp(_el$
|
|
240
|
-
_$insert(_el$
|
|
241
|
-
_$effect(_$p => _$setProp(_el$
|
|
242
|
-
return _el$
|
|
242
|
+
var _el$19 = _$createElement("text");
|
|
243
|
+
_$setProp(_el$19, "wrapMode", "none");
|
|
244
|
+
_$insert(_el$19, () => truncate(`${shell().summary}`, Math.max(20, dims().width - 40)));
|
|
245
|
+
_$effect(_$p => _$setProp(_el$19, "fg", theme().error, _$p));
|
|
246
|
+
return _el$19;
|
|
243
247
|
}
|
|
244
248
|
}), null);
|
|
245
|
-
_$effect(_$p => _$setProp(_el$
|
|
246
|
-
return _el$
|
|
249
|
+
_$effect(_$p => _$setProp(_el$17, "fg", kindColor(theme(), kindOf(shell())), _$p));
|
|
250
|
+
return _el$16;
|
|
247
251
|
})()]
|
|
248
252
|
}), null);
|
|
249
253
|
_$effect(_p$ => {
|
|
@@ -251,19 +255,22 @@ export function Dock(props) {
|
|
|
251
255
|
_v$2 = theme().border,
|
|
252
256
|
_v$3 = theme().backgroundPanel,
|
|
253
257
|
_v$4 = theme().text,
|
|
254
|
-
_v$5 = theme().textMuted
|
|
258
|
+
_v$5 = theme().textMuted,
|
|
259
|
+
_v$6 = theme().textMuted;
|
|
255
260
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "height", _v$, _p$.e));
|
|
256
261
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "borderColor", _v$2, _p$.t));
|
|
257
262
|
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
|
|
258
263
|
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$3, "fg", _v$4, _p$.o));
|
|
259
|
-
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$
|
|
264
|
+
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$6, "fg", _v$5, _p$.i));
|
|
265
|
+
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$9, "fg", _v$6, _p$.n));
|
|
260
266
|
return _p$;
|
|
261
267
|
}, {
|
|
262
268
|
e: undefined,
|
|
263
269
|
t: undefined,
|
|
264
270
|
a: undefined,
|
|
265
271
|
o: undefined,
|
|
266
|
-
i: undefined
|
|
272
|
+
i: undefined,
|
|
273
|
+
n: undefined
|
|
267
274
|
});
|
|
268
275
|
return _el$;
|
|
269
276
|
})();
|
|
@@ -39,6 +39,7 @@ export function SidebarShells(props) {
|
|
|
39
39
|
_$insertNode(_el$2, _el$3);
|
|
40
40
|
_$insertNode(_el$2, _el$5);
|
|
41
41
|
_$setProp(_el$2, "wrapMode", "none");
|
|
42
|
+
_$setProp(_el$2, "marginBottom", 1);
|
|
42
43
|
_$insertNode(_el$3, _$createTextNode(`Shells`));
|
|
43
44
|
_$insertNode(_el$5, _el$6);
|
|
44
45
|
_$insert(_el$5, counts, null);
|
package/dist/tui/dialogs.js
CHANGED
|
@@ -89,7 +89,8 @@ export function newShell(api, store, open) {
|
|
|
89
89
|
cwd: store.project(),
|
|
90
90
|
title: command.slice(0, 60),
|
|
91
91
|
owner: {
|
|
92
|
-
project: store.project()
|
|
92
|
+
project: store.project(),
|
|
93
|
+
session: store.session()
|
|
93
94
|
},
|
|
94
95
|
reuse: true
|
|
95
96
|
}).then(info => {
|
|
@@ -135,6 +136,52 @@ export function restartDaemon(api, store) {
|
|
|
135
136
|
onCancel: () => api.ui.dialog.clear()
|
|
136
137
|
}));
|
|
137
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stopping shells in bulk. Two commands rather than one, because "everything I can see" and
|
|
142
|
+
* "everything, including what I cannot" are different intentions — and the second one asks first.
|
|
143
|
+
*/
|
|
144
|
+
export function stopShells(api, store, reach) {
|
|
145
|
+
const list = (reach === "view" ? store.shells() : store.all()).filter(s => s.status === "running");
|
|
146
|
+
if (list.length === 0) {
|
|
147
|
+
api.ui.toast({
|
|
148
|
+
title: "Shells",
|
|
149
|
+
message: "Nothing is running.",
|
|
150
|
+
duration: 3000
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const stop = () => {
|
|
155
|
+
api.ui.dialog.clear();
|
|
156
|
+
void Promise.all(list.map(shell => store.client.call("shell.stop", {
|
|
157
|
+
id: shell.id,
|
|
158
|
+
graceMs: 2000
|
|
159
|
+
}).catch(() => {}))).then(() => {
|
|
160
|
+
void store.refresh();
|
|
161
|
+
api.ui.toast({
|
|
162
|
+
title: "Shells",
|
|
163
|
+
message: `Stopped ${list.length} shell${list.length === 1 ? "" : "s"}.`,
|
|
164
|
+
duration: 4000
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Shells from other conversations are the ones you are not looking at; say so before killing them.
|
|
170
|
+
const elsewhere = reach === "project" ? list.length - store.shells().filter(s => s.status === "running").length : 0;
|
|
171
|
+
if (elsewhere <= 0) {
|
|
172
|
+
stop();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const DialogConfirm = api.ui.DialogConfirm;
|
|
176
|
+
api.ui.dialog.replace(() => _$createComponent(DialogConfirm, {
|
|
177
|
+
title: "Stop every shell in this project?",
|
|
178
|
+
get message() {
|
|
179
|
+
return `${list.length} running · ${elsewhere} from other conversations.`;
|
|
180
|
+
},
|
|
181
|
+
onConfirm: stop,
|
|
182
|
+
onCancel: () => api.ui.dialog.clear()
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
138
185
|
export function pickShell(api, store, open) {
|
|
139
186
|
const DialogSelect = api.ui.DialogSelect;
|
|
140
187
|
const shells = order(store.shells());
|
package/dist/tui/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { loadConfig } from "../core/config.js";
|
|
|
11
11
|
import { Console } from "./components/console.js";
|
|
12
12
|
import { Dock } from "./components/dock.js";
|
|
13
13
|
import { SidebarShells } from "./components/sidebar.js";
|
|
14
|
-
import { announceUpdate, newShell, offerUpdate, pickShell, restartDaemon } from "./dialogs.js";
|
|
14
|
+
import { announceUpdate, newShell, offerUpdate, pickShell, restartDaemon, stopShells } from "./dialogs.js";
|
|
15
15
|
import { createShellStore } from "./state/store.js";
|
|
16
16
|
const DEFAULT_KEYS = {
|
|
17
17
|
"cockpit.shells.dock": "<leader>o",
|
|
@@ -55,7 +55,10 @@ const shellTui = async (api, rawOptions, meta) => {
|
|
|
55
55
|
...DEFAULT_KEYS,
|
|
56
56
|
...options.keybinds
|
|
57
57
|
});
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
// An explicit `ui.dockOpen` says how the panel should start; without one, whatever you last left
|
|
60
|
+
// it as. Remembered state that overrides a written setting is a setting that appears to do nothing.
|
|
61
|
+
const [dockOpen, setDockOpen] = createSignal(options.dockOpen ?? api.kv.get("cockpit.dock.open", false));
|
|
59
62
|
const toggleDock = () => {
|
|
60
63
|
const next = !dockOpen();
|
|
61
64
|
setDockOpen(next);
|
|
@@ -107,6 +110,20 @@ const shellTui = async (api, rawOptions, meta) => {
|
|
|
107
110
|
namespace: "palette",
|
|
108
111
|
slashName: "shell-new",
|
|
109
112
|
run: () => newShell(api, store, openConsole)
|
|
113
|
+
}, {
|
|
114
|
+
name: "cockpit.shells.stop",
|
|
115
|
+
title: "Stop the shells in view",
|
|
116
|
+
category: "Shells",
|
|
117
|
+
namespace: "palette",
|
|
118
|
+
slashName: "shells-stop",
|
|
119
|
+
run: () => stopShells(api, store, "view")
|
|
120
|
+
}, {
|
|
121
|
+
name: "cockpit.shells.stopAll",
|
|
122
|
+
title: "Stop every shell in this project",
|
|
123
|
+
category: "Shells",
|
|
124
|
+
namespace: "palette",
|
|
125
|
+
slashName: "shells-stop-all",
|
|
126
|
+
run: () => stopShells(api, store, "project")
|
|
110
127
|
}, {
|
|
111
128
|
name: "cockpit.shells.clear",
|
|
112
129
|
title: "Clear finished shells",
|
package/dist/tui/lib/view.js
CHANGED
|
@@ -30,11 +30,20 @@ export function kindColor(theme, kind) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The status mark: a coloured rule and its label, in a fixed 7 columns so lists line up.
|
|
35
|
+
*
|
|
36
|
+
* It used to be a filled pill. Filled blocks have to be as wide as their text, so a column of
|
|
37
|
+
* them stacks into a wall of colour that competes with the shell names beside it; a rule carries
|
|
38
|
+
* the same colour in one column and lets the list breathe.
|
|
39
|
+
*/
|
|
34
40
|
export function badgeText(kind, frame = 0) {
|
|
35
|
-
if (kind === "run") return
|
|
36
|
-
return
|
|
41
|
+
if (kind === "run") return `▌ ${SPINNER[frame % SPINNER.length]} RUN`;
|
|
42
|
+
return `▌ ${BADGE_LABEL[kind].padEnd(5)}`;
|
|
37
43
|
}
|
|
44
|
+
|
|
45
|
+
/** The rule itself, so it can be coloured apart from the label it marks. */
|
|
46
|
+
export const BADGE_RULE = "▌";
|
|
38
47
|
const RANK = {
|
|
39
48
|
run: 0,
|
|
40
49
|
fail: 1,
|
package/dist/tui/state/store.js
CHANGED
|
@@ -2,6 +2,9 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { createEffect, createMemo, createRoot, createSignal, on, onCleanup } from "solid-js";
|
|
3
3
|
import { createStore, reconcile } from "solid-js/store";
|
|
4
4
|
import { order, partition } from "../lib/view.js";
|
|
5
|
+
|
|
6
|
+
/** A project's shells outnumber a conversation's, and mixing them is how the panel gets confusing. */
|
|
7
|
+
|
|
5
8
|
export function createShellStore(api, client, options = {}) {
|
|
6
9
|
return createRoot(dispose => {
|
|
7
10
|
const [state, setState] = createStore({
|
|
@@ -12,7 +15,12 @@ export function createShellStore(api, client, options = {}) {
|
|
|
12
15
|
const [frame, setFrame] = createSignal(0);
|
|
13
16
|
const [selectedId, setSelectedId] = createSignal();
|
|
14
17
|
const [showAll, setShowAll] = createSignal(api.kv.get("cockpit.shells.showAll", false));
|
|
18
|
+
const [scope, setScope] = createSignal(api.kv.get("cockpit.shells.scope", options.scope ?? "session"));
|
|
15
19
|
const project = () => api.state.path.directory;
|
|
20
|
+
const session = () => {
|
|
21
|
+
const route = api.route.current;
|
|
22
|
+
return route.name === "session" ? route.params.sessionID : undefined;
|
|
23
|
+
};
|
|
16
24
|
const historyMs = (options.historyMinutes ?? 30) * 60_000;
|
|
17
25
|
const refresh = async () => {
|
|
18
26
|
try {
|
|
@@ -53,7 +61,13 @@ export function createShellStore(api, client, options = {}) {
|
|
|
53
61
|
const ordered = order(state.list);
|
|
54
62
|
return ordered.find(s => s.id === selectedId()) ?? ordered[0];
|
|
55
63
|
});
|
|
56
|
-
|
|
64
|
+
/** Everything the daemon knows about this project, narrowed to what the panel is about. */
|
|
65
|
+
const inScope = createMemo(() => {
|
|
66
|
+
const current = session();
|
|
67
|
+
if (scope() === "project" || !current) return state.list;
|
|
68
|
+
return state.list.filter(shell => shell.owner.session === current);
|
|
69
|
+
});
|
|
70
|
+
const folded = createMemo(() => partition(inScope(), {
|
|
57
71
|
showAll: showAll(),
|
|
58
72
|
historyMs,
|
|
59
73
|
now: now(),
|
|
@@ -62,7 +76,15 @@ export function createShellStore(api, client, options = {}) {
|
|
|
62
76
|
return {
|
|
63
77
|
client,
|
|
64
78
|
project,
|
|
65
|
-
|
|
79
|
+
session,
|
|
80
|
+
scope,
|
|
81
|
+
toggleScope() {
|
|
82
|
+
const next = scope() === "session" ? "project" : "session";
|
|
83
|
+
setScope(next);
|
|
84
|
+
api.kv.set("cockpit.shells.scope", next);
|
|
85
|
+
},
|
|
86
|
+
shells: () => inScope(),
|
|
87
|
+
all: () => state.list,
|
|
66
88
|
visible: () => folded().visible,
|
|
67
89
|
hidden: () => folded().hidden,
|
|
68
90
|
showAll,
|
|
@@ -79,7 +101,7 @@ export function createShellStore(api, client, options = {}) {
|
|
|
79
101
|
// Cycles every shell in the project: folding only exists to keep the sidebar short, and a
|
|
80
102
|
// shell you cannot reach from the console would be a trap.
|
|
81
103
|
step(delta) {
|
|
82
|
-
const list = order(
|
|
104
|
+
const list = order(inScope());
|
|
83
105
|
if (list.length === 0) return;
|
|
84
106
|
const index = Math.max(0, list.findIndex(s => s.id === pick()?.id));
|
|
85
107
|
const next = list[(index + delta + list.length) % list.length];
|
|
@@ -135,7 +157,9 @@ export function useScreen(store, id) {
|
|
|
135
157
|
current = next;
|
|
136
158
|
setScreen(undefined);
|
|
137
159
|
if (!next) return;
|
|
138
|
-
|
|
160
|
+
// Not store.shells(): that is narrowed to the current conversation, and the selected shell may
|
|
161
|
+
// sit outside it. A missing offset here is what used to ask the daemon for bytes past the end.
|
|
162
|
+
const info = store.all().find(s => s.id === next);
|
|
139
163
|
void store.client.call("shell.attach", {
|
|
140
164
|
id: next,
|
|
141
165
|
fromOffset: info?.bytes ?? Number.MAX_SAFE_INTEGER
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencode-cockpit/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Background shells for OpenCode: the agent starts, waits on and drives PTYs; you watch them in a docked TUI panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@opencode-cockpit/client": "0.
|
|
55
|
-
"@opencode-cockpit/daemon": "0.
|
|
56
|
-
"@opencode-cockpit/protocol": "0.
|
|
54
|
+
"@opencode-cockpit/client": "0.3.0",
|
|
55
|
+
"@opencode-cockpit/daemon": "0.3.0",
|
|
56
|
+
"@opencode-cockpit/protocol": "0.3.0",
|
|
57
57
|
"@opencode-ai/plugin": "1.18.31"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
package/types/connect.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export declare function daemonEntry(): string;
|
|
|
6
6
|
* BUN_BE_BUN=1 so it runs on OpenCode's embedded Bun (ADR 0001). The expected build lets the
|
|
7
7
|
* client replace a daemon left running from older plugin code.
|
|
8
8
|
*/
|
|
9
|
-
export declare function createClient(name: string): CockpitClient;
|
|
9
|
+
export declare function createClient(name: string, instance?: string): CockpitClient;
|
package/types/core/config.d.ts
CHANGED
|
@@ -27,6 +27,16 @@ export interface CockpitConfig {
|
|
|
27
27
|
timeoutSeconds?: number;
|
|
28
28
|
notifyOnExit?: boolean;
|
|
29
29
|
};
|
|
30
|
+
/** When shells end by themselves. */
|
|
31
|
+
lifecycle?: {
|
|
32
|
+
/**
|
|
33
|
+
* What happens to this window's shells when it closes. `stopMine` stops them, `keep` leaves
|
|
34
|
+
* them running for the next window — which is how shells survive an OpenCode restart.
|
|
35
|
+
*/
|
|
36
|
+
onExit?: "stopMine" | "keep";
|
|
37
|
+
/** Stop a shell after this long with no window of its own connected. `0` never does. */
|
|
38
|
+
orphanAfterMinutes?: number;
|
|
39
|
+
};
|
|
30
40
|
/** What is allowed to interrupt the agent. */
|
|
31
41
|
notify?: {
|
|
32
42
|
exit?: boolean;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
3
3
|
import type { ShellInfo } from "@opencode-cockpit/protocol/shell";
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Status mark: a coloured rule and its label.
|
|
6
|
+
*
|
|
7
|
+
* Not a filled pill. A block of colour has to be as wide as the word inside it, and a list of
|
|
8
|
+
* them reads as a wall of colour rather than as a list of shells; the rule carries the same
|
|
9
|
+
* meaning in one column.
|
|
10
|
+
*/
|
|
5
11
|
export declare function Badge(props: {
|
|
6
12
|
api: TuiPluginApi;
|
|
7
13
|
shell: ShellInfo;
|
package/types/tui/dialogs.d.ts
CHANGED
|
@@ -7,4 +7,9 @@ export declare function announceUpdate(api: TuiPluginApi, meta: TuiPluginMeta, c
|
|
|
7
7
|
export declare function offerUpdate(api: TuiPluginApi, meta: TuiPluginMeta, current: string): void;
|
|
8
8
|
export declare function newShell(api: TuiPluginApi, store: ShellStore, open: (id?: string) => void): void;
|
|
9
9
|
export declare function restartDaemon(api: TuiPluginApi, store: ShellStore): void;
|
|
10
|
+
/**
|
|
11
|
+
* Stopping shells in bulk. Two commands rather than one, because "everything I can see" and
|
|
12
|
+
* "everything, including what I cannot" are different intentions — and the second one asks first.
|
|
13
|
+
*/
|
|
14
|
+
export declare function stopShells(api: TuiPluginApi, store: ShellStore, reach: "view" | "project"): void;
|
|
10
15
|
export declare function pickShell(api: TuiPluginApi, store: ShellStore, open: (id?: string) => void): void;
|
package/types/tui/lib/view.d.ts
CHANGED
|
@@ -7,8 +7,16 @@ export declare const BADGE_LABEL: Record<Kind, string>;
|
|
|
7
7
|
/** Braille spinner: single-width in every terminal font. */
|
|
8
8
|
export declare const SPINNER: string[];
|
|
9
9
|
export declare function kindColor(theme: TuiThemeCurrent, kind: Kind): import("@opentui/core").RGBA;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The status mark: a coloured rule and its label, in a fixed 7 columns so lists line up.
|
|
12
|
+
*
|
|
13
|
+
* It used to be a filled pill. Filled blocks have to be as wide as their text, so a column of
|
|
14
|
+
* them stacks into a wall of colour that competes with the shell names beside it; a rule carries
|
|
15
|
+
* the same colour in one column and lets the list breathe.
|
|
16
|
+
*/
|
|
11
17
|
export declare function badgeText(kind: Kind, frame?: number): string;
|
|
18
|
+
/** The rule itself, so it can be coloured apart from the label it marks. */
|
|
19
|
+
export declare const BADGE_RULE = "\u258C";
|
|
12
20
|
/** Running first (oldest first, stable tabs), then failures, stopped, done (most recent first). */
|
|
13
21
|
export declare function order(list: readonly ShellInfo[]): ShellInfo[];
|
|
14
22
|
export interface PartitionOptions {
|
|
@@ -5,13 +5,20 @@ import { type Accessor } from "solid-js";
|
|
|
5
5
|
export interface ShellStore {
|
|
6
6
|
client: CockpitClient;
|
|
7
7
|
project: () => string;
|
|
8
|
-
/**
|
|
8
|
+
/** Shells the panel is about: the current conversation, or the project. */
|
|
9
9
|
shells: Accessor<ShellInfo[]>;
|
|
10
|
+
/** Every shell in the project, whatever the panel is showing. */
|
|
11
|
+
all: Accessor<ShellInfo[]>;
|
|
10
12
|
/** Ordered and folded for display: running, recent failures, plus the selection. */
|
|
11
13
|
visible: Accessor<ShellInfo[]>;
|
|
12
14
|
hidden: Accessor<ShellInfo[]>;
|
|
13
15
|
showAll: Accessor<boolean>;
|
|
14
16
|
toggleAll(): void;
|
|
17
|
+
/** Which shells the panel is about: the conversation you are in, or the whole project. */
|
|
18
|
+
scope: Accessor<Scope>;
|
|
19
|
+
toggleScope(): void;
|
|
20
|
+
/** The session the interface is currently showing, when it is showing one. */
|
|
21
|
+
session: Accessor<string | undefined>;
|
|
15
22
|
connected: Accessor<boolean>;
|
|
16
23
|
now: Accessor<number>;
|
|
17
24
|
/** Spinner frame index; advances only while something is running. */
|
|
@@ -23,9 +30,13 @@ export interface ShellStore {
|
|
|
23
30
|
clearFinished(): Promise<number>;
|
|
24
31
|
dispose(): void;
|
|
25
32
|
}
|
|
33
|
+
/** A project's shells outnumber a conversation's, and mixing them is how the panel gets confusing. */
|
|
34
|
+
export type Scope = "session" | "project";
|
|
26
35
|
export interface StoreOptions {
|
|
27
36
|
/** Failures stay in the default view this long after they end. */
|
|
28
37
|
historyMinutes?: number;
|
|
38
|
+
/** What the panel lists by default. */
|
|
39
|
+
scope?: Scope;
|
|
29
40
|
}
|
|
30
41
|
export declare function createShellStore(api: TuiPluginApi, client: CockpitClient, options?: StoreOptions): ShellStore;
|
|
31
42
|
/**
|