@opencode-cockpit/shell 0.1.4 → 0.1.5
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/dist/connect.js +31 -0
- package/dist/server.js +152 -0
- package/dist/tools/find.js +66 -0
- package/dist/tools/format.js +63 -0
- package/dist/tools/index.js +433 -0
- package/{src/tools/keys.ts → dist/tools/keys.js} +11 -14
- package/dist/tui/badge.js +29 -0
- package/dist/tui/console.js +628 -0
- package/dist/tui/dock.js +214 -0
- package/dist/tui/index.js +264 -0
- package/dist/tui/keys.js +43 -0
- package/dist/tui/sidebar.js +121 -0
- package/dist/tui/store.js +156 -0
- package/dist/tui/view.js +154 -0
- package/package.json +22 -12
- package/types/connect.d.ts +9 -0
- package/types/server.d.ts +12 -0
- package/types/tools/find.d.ts +32 -0
- package/types/tools/format.d.ts +8 -0
- package/types/tools/index.d.ts +17 -0
- package/types/tools/keys.d.ts +3 -0
- package/types/tui/badge.d.ts +9 -0
- package/types/tui/console.d.ts +16 -0
- package/types/tui/dock.d.ts +12 -0
- package/types/tui/index.d.ts +19 -0
- package/types/tui/keys.d.ts +8 -0
- package/types/tui/sidebar.d.ts +14 -0
- package/types/tui/store.d.ts +46 -0
- package/types/tui/view.d.ts +67 -0
- package/src/connect.ts +0 -24
- package/src/server.ts +0 -156
- package/src/tools/find.ts +0 -80
- package/src/tools/format.ts +0 -78
- package/src/tools/index.ts +0 -467
- 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 -286
- 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/dist/tui/dock.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
2
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
3
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
4
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
5
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
6
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
8
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
9
|
+
/** @jsxImportSource @opentui/solid */
|
|
10
|
+
|
|
11
|
+
import { useTerminalDimensions } from "@opentui/solid";
|
|
12
|
+
import { createMemo, For, Show } from "solid-js";
|
|
13
|
+
import { Badge } from "./badge.js";
|
|
14
|
+
import { useScreen } from "./store.js";
|
|
15
|
+
import { displayCommand, kindColor, kindOf, statusDetail, tailLines, truncate } from "./view.js";
|
|
16
|
+
/** Split pane under the chat: status tabs for shells and the selected shell's live screen. */
|
|
17
|
+
export function Dock(props) {
|
|
18
|
+
const theme = () => props.api.theme.current;
|
|
19
|
+
const dims = useTerminalDimensions();
|
|
20
|
+
const {
|
|
21
|
+
screen
|
|
22
|
+
} = useScreen(props.store, () => props.store.selected()?.id);
|
|
23
|
+
const bodyRows = () => Math.max(2, props.height - 3);
|
|
24
|
+
// Tabs share one row: keep them to what fits, the rest lives behind the "N more" chip.
|
|
25
|
+
const tabLimit = () => Math.max(1, Math.floor((dims().width - 26) / 30));
|
|
26
|
+
const tabs = createMemo(() => (props.store.showAll() ? props.store.shells() : props.store.visible()).slice(0, tabLimit()));
|
|
27
|
+
const overflow = createMemo(() => props.store.shells().length - tabs().length);
|
|
28
|
+
const body = createMemo(() => tailLines(screen()?.text, bodyRows(), Math.max(10, dims().width - 4)));
|
|
29
|
+
return (() => {
|
|
30
|
+
var _el$ = _$createElement("box"),
|
|
31
|
+
_el$2 = _$createElement("box"),
|
|
32
|
+
_el$3 = _$createElement("text"),
|
|
33
|
+
_el$4 = _$createElement("b"),
|
|
34
|
+
_el$7 = _$createElement("box"),
|
|
35
|
+
_el$8 = _$createElement("text");
|
|
36
|
+
_$insertNode(_el$, _el$2);
|
|
37
|
+
_$setProp(_el$, "flexShrink", 0);
|
|
38
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
39
|
+
_$setProp(_el$, "border", ["top"]);
|
|
40
|
+
_$insertNode(_el$2, _el$3);
|
|
41
|
+
_$insertNode(_el$2, _el$7);
|
|
42
|
+
_$insertNode(_el$2, _el$8);
|
|
43
|
+
_$setProp(_el$2, "flexDirection", "row");
|
|
44
|
+
_$setProp(_el$2, "gap", 1);
|
|
45
|
+
_$setProp(_el$2, "paddingLeft", 1);
|
|
46
|
+
_$setProp(_el$2, "paddingRight", 1);
|
|
47
|
+
_$setProp(_el$2, "flexShrink", 0);
|
|
48
|
+
_$setProp(_el$2, "height", 1);
|
|
49
|
+
_$setProp(_el$2, "overflow", "hidden");
|
|
50
|
+
_$insertNode(_el$3, _el$4);
|
|
51
|
+
_$setProp(_el$3, "flexShrink", 0);
|
|
52
|
+
_$insertNode(_el$4, _$createTextNode(`Shells`));
|
|
53
|
+
_$insert(_el$2, _$createComponent(Show, {
|
|
54
|
+
get when() {
|
|
55
|
+
return props.store.shells().length > 0;
|
|
56
|
+
},
|
|
57
|
+
get fallback() {
|
|
58
|
+
return (() => {
|
|
59
|
+
var _el$9 = _$createElement("text");
|
|
60
|
+
_$insertNode(_el$9, _$createTextNode(`none yet · the agent starts them, or /shell-new`));
|
|
61
|
+
_$setProp(_el$9, "wrapMode", "none");
|
|
62
|
+
_$effect(_$p => _$setProp(_el$9, "fg", theme().textMuted, _$p));
|
|
63
|
+
return _el$9;
|
|
64
|
+
})();
|
|
65
|
+
},
|
|
66
|
+
get children() {
|
|
67
|
+
return [_$createComponent(For, {
|
|
68
|
+
get each() {
|
|
69
|
+
return tabs();
|
|
70
|
+
},
|
|
71
|
+
children: shell => {
|
|
72
|
+
const active = () => props.store.selected()?.id === shell.id;
|
|
73
|
+
return (() => {
|
|
74
|
+
var _el$1 = _$createElement("box"),
|
|
75
|
+
_el$10 = _$createElement("text"),
|
|
76
|
+
_el$11 = _$createTextNode(` `);
|
|
77
|
+
_$insertNode(_el$1, _el$10);
|
|
78
|
+
_$setProp(_el$1, "flexDirection", "row");
|
|
79
|
+
_$setProp(_el$1, "flexShrink", 0);
|
|
80
|
+
_$setProp(_el$1, "paddingRight", 1);
|
|
81
|
+
_$setProp(_el$1, "onMouseUp", () => active() ? props.onOpenConsole(shell.id) : props.store.select(shell.id));
|
|
82
|
+
_$insert(_el$1, _$createComponent(Badge, {
|
|
83
|
+
get api() {
|
|
84
|
+
return props.api;
|
|
85
|
+
},
|
|
86
|
+
shell: shell,
|
|
87
|
+
get frame() {
|
|
88
|
+
return props.store.frame();
|
|
89
|
+
}
|
|
90
|
+
}), _el$10);
|
|
91
|
+
_$insertNode(_el$10, _el$11);
|
|
92
|
+
_$setProp(_el$10, "wrapMode", "none");
|
|
93
|
+
_$insert(_el$10, (() => {
|
|
94
|
+
var _c$2 = _$memo(() => !!active());
|
|
95
|
+
return () => _c$2() ? (() => {
|
|
96
|
+
var _el$12 = _$createElement("b");
|
|
97
|
+
_$insert(_el$12, () => truncate(shell.title, 22));
|
|
98
|
+
return _el$12;
|
|
99
|
+
})() : truncate(shell.title, 22);
|
|
100
|
+
})(), null);
|
|
101
|
+
_$effect(_p$ => {
|
|
102
|
+
var _v$6 = active() ? theme().backgroundElement : theme().backgroundPanel,
|
|
103
|
+
_v$7 = active() ? theme().text : theme().textMuted;
|
|
104
|
+
_v$6 !== _p$.e && (_p$.e = _$setProp(_el$1, "backgroundColor", _v$6, _p$.e));
|
|
105
|
+
_v$7 !== _p$.t && (_p$.t = _$setProp(_el$10, "fg", _v$7, _p$.t));
|
|
106
|
+
return _p$;
|
|
107
|
+
}, {
|
|
108
|
+
e: undefined,
|
|
109
|
+
t: undefined
|
|
110
|
+
});
|
|
111
|
+
return _el$1;
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
}), _$createComponent(Show, {
|
|
115
|
+
get when() {
|
|
116
|
+
return overflow() > 0 || props.store.showAll();
|
|
117
|
+
},
|
|
118
|
+
get children() {
|
|
119
|
+
var _el$6 = _$createElement("text");
|
|
120
|
+
_$setProp(_el$6, "wrapMode", "none");
|
|
121
|
+
_$setProp(_el$6, "flexShrink", 0);
|
|
122
|
+
_$setProp(_el$6, "onMouseUp", () => props.store.toggleAll());
|
|
123
|
+
_$insert(_el$6, (() => {
|
|
124
|
+
var _c$ = _$memo(() => !!props.store.showAll());
|
|
125
|
+
return () => _c$() ? "▾ fewer" : `▸ ${overflow()} more`;
|
|
126
|
+
})());
|
|
127
|
+
_$effect(_$p => _$setProp(_el$6, "fg", theme().textMuted, _$p));
|
|
128
|
+
return _el$6;
|
|
129
|
+
}
|
|
130
|
+
})];
|
|
131
|
+
}
|
|
132
|
+
}), _el$7);
|
|
133
|
+
_$setProp(_el$7, "flexGrow", 1);
|
|
134
|
+
_$setProp(_el$8, "wrapMode", "none");
|
|
135
|
+
_$setProp(_el$8, "flexShrink", 0);
|
|
136
|
+
_$insert(_el$8, () => props.hint());
|
|
137
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
138
|
+
get when() {
|
|
139
|
+
return props.store.selected();
|
|
140
|
+
},
|
|
141
|
+
children: shell => [(() => {
|
|
142
|
+
var _el$13 = _$createElement("box"),
|
|
143
|
+
_el$14 = _$createElement("text");
|
|
144
|
+
_$insertNode(_el$13, _el$14);
|
|
145
|
+
_$setProp(_el$13, "flexGrow", 1);
|
|
146
|
+
_$setProp(_el$13, "paddingLeft", 2);
|
|
147
|
+
_$setProp(_el$13, "paddingRight", 1);
|
|
148
|
+
_$setProp(_el$13, "minHeight", 0);
|
|
149
|
+
_$setProp(_el$13, "overflow", "hidden");
|
|
150
|
+
_$setProp(_el$13, "onMouseUp", () => props.onOpenConsole(shell().id));
|
|
151
|
+
_$setProp(_el$14, "wrapMode", "none");
|
|
152
|
+
_$insert(_el$14, () => body() || " ");
|
|
153
|
+
_$effect(_$p => _$setProp(_el$14, "fg", theme().text, _$p));
|
|
154
|
+
return _el$13;
|
|
155
|
+
})(), (() => {
|
|
156
|
+
var _el$15 = _$createElement("box"),
|
|
157
|
+
_el$16 = _$createElement("text");
|
|
158
|
+
_$insertNode(_el$15, _el$16);
|
|
159
|
+
_$setProp(_el$15, "flexDirection", "row");
|
|
160
|
+
_$setProp(_el$15, "gap", 1);
|
|
161
|
+
_$setProp(_el$15, "paddingLeft", 2);
|
|
162
|
+
_$setProp(_el$15, "flexShrink", 0);
|
|
163
|
+
_$setProp(_el$15, "height", 1);
|
|
164
|
+
_$setProp(_el$15, "overflow", "hidden");
|
|
165
|
+
_$setProp(_el$16, "wrapMode", "none");
|
|
166
|
+
_$setProp(_el$16, "flexShrink", 0);
|
|
167
|
+
_$insert(_el$16, () => statusDetail(shell(), props.store.now()));
|
|
168
|
+
_$insert(_el$15, _$createComponent(Show, {
|
|
169
|
+
get when() {
|
|
170
|
+
return _$memo(() => kindOf(shell()) === "fail")() && shell().summary;
|
|
171
|
+
},
|
|
172
|
+
get fallback() {
|
|
173
|
+
return (() => {
|
|
174
|
+
var _el$18 = _$createElement("text");
|
|
175
|
+
_$setProp(_el$18, "wrapMode", "none");
|
|
176
|
+
_$insert(_el$18, () => truncate(`$ ${displayCommand(shell())}`, Math.max(20, dims().width - 40)));
|
|
177
|
+
_$effect(_$p => _$setProp(_el$18, "fg", theme().textMuted, _$p));
|
|
178
|
+
return _el$18;
|
|
179
|
+
})();
|
|
180
|
+
},
|
|
181
|
+
get children() {
|
|
182
|
+
var _el$17 = _$createElement("text");
|
|
183
|
+
_$setProp(_el$17, "wrapMode", "none");
|
|
184
|
+
_$insert(_el$17, () => truncate(`${shell().summary}`, Math.max(20, dims().width - 40)));
|
|
185
|
+
_$effect(_$p => _$setProp(_el$17, "fg", theme().error, _$p));
|
|
186
|
+
return _el$17;
|
|
187
|
+
}
|
|
188
|
+
}), null);
|
|
189
|
+
_$effect(_$p => _$setProp(_el$16, "fg", kindColor(theme(), kindOf(shell())), _$p));
|
|
190
|
+
return _el$15;
|
|
191
|
+
})()]
|
|
192
|
+
}), null);
|
|
193
|
+
_$effect(_p$ => {
|
|
194
|
+
var _v$ = props.height,
|
|
195
|
+
_v$2 = theme().border,
|
|
196
|
+
_v$3 = theme().backgroundPanel,
|
|
197
|
+
_v$4 = theme().text,
|
|
198
|
+
_v$5 = theme().textMuted;
|
|
199
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "height", _v$, _p$.e));
|
|
200
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "borderColor", _v$2, _p$.t));
|
|
201
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
|
|
202
|
+
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$3, "fg", _v$4, _p$.o));
|
|
203
|
+
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$8, "fg", _v$5, _p$.i));
|
|
204
|
+
return _p$;
|
|
205
|
+
}, {
|
|
206
|
+
e: undefined,
|
|
207
|
+
t: undefined,
|
|
208
|
+
a: undefined,
|
|
209
|
+
o: undefined,
|
|
210
|
+
i: undefined
|
|
211
|
+
});
|
|
212
|
+
return _el$;
|
|
213
|
+
})();
|
|
214
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
2
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
3
|
+
/** @jsxImportSource @opentui/solid */
|
|
4
|
+
import { createBindingLookup } from "@opencode-ai/plugin/tui";
|
|
5
|
+
import { claimFeature, duplicateFeatureMessage } from "@opencode-cockpit/client";
|
|
6
|
+
import { createSignal } from "solid-js";
|
|
7
|
+
import { createClient } from "../connect.js";
|
|
8
|
+
import { Console } from "./console.js";
|
|
9
|
+
import { Dock } from "./dock.js";
|
|
10
|
+
import { SidebarShells } from "./sidebar.js";
|
|
11
|
+
import { createShellStore } from "./store.js";
|
|
12
|
+
import { BADGE_LABEL, displayCommand, kindOf, order } from "./view.js";
|
|
13
|
+
const DEFAULT_KEYS = {
|
|
14
|
+
"cockpit.shells.dock": "<leader>o",
|
|
15
|
+
"cockpit.shells.console": "<leader>i"
|
|
16
|
+
};
|
|
17
|
+
const SHELL_PACKAGE = "@opencode-cockpit/shell";
|
|
18
|
+
|
|
19
|
+
/** Shell's TUI half as a factory, so bundles such as `opencode-cockpit` can include it. */
|
|
20
|
+
export function createShellTui({
|
|
21
|
+
source = SHELL_PACKAGE
|
|
22
|
+
} = {}) {
|
|
23
|
+
return async (api, rawOptions, meta) => {
|
|
24
|
+
// The renderer is shared by every TUI plugin in this OpenCode window.
|
|
25
|
+
const claim = claimFeature(api.renderer, "shell", source);
|
|
26
|
+
if (!claim.active) {
|
|
27
|
+
api.ui.toast({
|
|
28
|
+
variant: "warning",
|
|
29
|
+
title: "opencode-cockpit",
|
|
30
|
+
message: duplicateFeatureMessage("Shell", claim.owner, source),
|
|
31
|
+
duration: 10_000
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
api.lifecycle.onDispose(() => claim.release());
|
|
36
|
+
await shellTui(api, rawOptions, meta);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const shellTui = async (api, rawOptions) => {
|
|
40
|
+
const options = rawOptions ?? {};
|
|
41
|
+
const client = createClient("opencode-cockpit/tui");
|
|
42
|
+
const store = createShellStore(api, client, {
|
|
43
|
+
historyMinutes: options.historyMinutes
|
|
44
|
+
});
|
|
45
|
+
const keys = createBindingLookup({
|
|
46
|
+
...DEFAULT_KEYS,
|
|
47
|
+
...options.keybinds
|
|
48
|
+
});
|
|
49
|
+
const [dockOpen, setDockOpen] = createSignal(api.kv.get("cockpit.dock.open", options.dockOpen ?? false));
|
|
50
|
+
const toggleDock = () => {
|
|
51
|
+
const next = !dockOpen();
|
|
52
|
+
setDockOpen(next);
|
|
53
|
+
api.kv.set("cockpit.dock.open", next);
|
|
54
|
+
};
|
|
55
|
+
const shortcut = command => {
|
|
56
|
+
const bindings = api.keymap.getCommandBindings({
|
|
57
|
+
visibility: "registered",
|
|
58
|
+
commands: [command]
|
|
59
|
+
});
|
|
60
|
+
return api.keys.formatBindings(bindings.get(command)) ?? "";
|
|
61
|
+
};
|
|
62
|
+
const openConsole = (id, typing = false) => {
|
|
63
|
+
if (id) store.select(id);
|
|
64
|
+
api.ui.dialog.replace(() => _$createComponent(Console, {
|
|
65
|
+
api: api,
|
|
66
|
+
store: store,
|
|
67
|
+
typing: typing,
|
|
68
|
+
onClose: () => api.ui.dialog.clear(),
|
|
69
|
+
onNewShell: () => newShell(api, store, openConsole)
|
|
70
|
+
}), () => {});
|
|
71
|
+
api.ui.dialog.setSize("xlarge");
|
|
72
|
+
};
|
|
73
|
+
api.keymap.registerLayer({
|
|
74
|
+
commands: [{
|
|
75
|
+
name: "cockpit.shells.dock",
|
|
76
|
+
title: "Toggle shells panel",
|
|
77
|
+
category: "Shells",
|
|
78
|
+
namespace: "palette",
|
|
79
|
+
slashName: "shells",
|
|
80
|
+
run: () => toggleDock()
|
|
81
|
+
}, {
|
|
82
|
+
name: "cockpit.shells.console",
|
|
83
|
+
title: "Open shell console",
|
|
84
|
+
category: "Shells",
|
|
85
|
+
namespace: "palette",
|
|
86
|
+
slashName: "shell",
|
|
87
|
+
run: () => openConsole()
|
|
88
|
+
}, {
|
|
89
|
+
name: "cockpit.shells.new",
|
|
90
|
+
title: "New background shell",
|
|
91
|
+
category: "Shells",
|
|
92
|
+
namespace: "palette",
|
|
93
|
+
slashName: "shell-new",
|
|
94
|
+
run: () => newShell(api, store, openConsole)
|
|
95
|
+
}, {
|
|
96
|
+
name: "cockpit.shells.clear",
|
|
97
|
+
title: "Clear finished shells",
|
|
98
|
+
category: "Shells",
|
|
99
|
+
namespace: "palette",
|
|
100
|
+
slashName: "shells-clear",
|
|
101
|
+
run: () => {
|
|
102
|
+
void store.clearFinished().then(n => api.ui.toast({
|
|
103
|
+
variant: "success",
|
|
104
|
+
title: "Shells",
|
|
105
|
+
message: `Cleared ${n} finished shell${n === 1 ? "" : "s"}`
|
|
106
|
+
})).catch(err => api.ui.toast({
|
|
107
|
+
variant: "error",
|
|
108
|
+
title: "Shells",
|
|
109
|
+
message: String(err)
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
}, {
|
|
113
|
+
name: "cockpit.shells.restartDaemon",
|
|
114
|
+
title: "Restart shell daemon",
|
|
115
|
+
category: "Shells",
|
|
116
|
+
namespace: "palette",
|
|
117
|
+
slashName: "shells-restart-daemon",
|
|
118
|
+
run: () => restartDaemon(api, store)
|
|
119
|
+
}, {
|
|
120
|
+
name: "cockpit.shells.pick",
|
|
121
|
+
title: "Switch shell",
|
|
122
|
+
category: "Shells",
|
|
123
|
+
namespace: "palette",
|
|
124
|
+
run: () => pickShell(api, store, openConsole)
|
|
125
|
+
}],
|
|
126
|
+
bindings: keys.gather("cockpit", Object.keys(DEFAULT_KEYS))
|
|
127
|
+
});
|
|
128
|
+
const height = () => Math.max(6, Math.min(options.dockHeight ?? 14, Math.floor(api.renderer.height * 0.45)));
|
|
129
|
+
api.slots.register({
|
|
130
|
+
order: 150,
|
|
131
|
+
slots: {
|
|
132
|
+
app_bottom() {
|
|
133
|
+
return _$memo(() => _$memo(() => !!dockOpen())() ? _$createComponent(Dock, {
|
|
134
|
+
api: api,
|
|
135
|
+
store: store,
|
|
136
|
+
get height() {
|
|
137
|
+
return height();
|
|
138
|
+
},
|
|
139
|
+
hint: () => `${shortcut("cockpit.shells.console")} console · ${shortcut("cockpit.shells.dock")} hide`,
|
|
140
|
+
onOpenConsole: id => openConsole(id)
|
|
141
|
+
}) : null);
|
|
142
|
+
},
|
|
143
|
+
sidebar_content() {
|
|
144
|
+
return _$createComponent(SidebarShells, {
|
|
145
|
+
api: api,
|
|
146
|
+
store: store,
|
|
147
|
+
get rows() {
|
|
148
|
+
return options.sidebarRows;
|
|
149
|
+
},
|
|
150
|
+
onOpen: id => openConsole(id),
|
|
151
|
+
consoleShortcut: () => shortcut("cockpit.shells.console")
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// A daemon from older plugin code is kept only while it runs shells; say so once.
|
|
158
|
+
const offOutdated = client.onOutdated(info => {
|
|
159
|
+
if (!info) return;
|
|
160
|
+
api.ui.toast({
|
|
161
|
+
variant: "warning",
|
|
162
|
+
title: "Shells",
|
|
163
|
+
message: "The shell daemon is running older code because shells are still running. Run /shells-restart-daemon when convenient.",
|
|
164
|
+
duration: 8000
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
api.lifecycle.onDispose(() => {
|
|
168
|
+
offOutdated();
|
|
169
|
+
store.dispose();
|
|
170
|
+
client.close();
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
function newShell(api, store, open) {
|
|
174
|
+
const DialogPrompt = api.ui.DialogPrompt;
|
|
175
|
+
api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
|
|
176
|
+
title: "New background shell",
|
|
177
|
+
placeholder: "npm run dev",
|
|
178
|
+
onConfirm: value => {
|
|
179
|
+
const command = value.trim();
|
|
180
|
+
if (!command) return api.ui.dialog.clear();
|
|
181
|
+
const shell = process.env.SHELL && /(bash|zsh|fish|sh)$/.test(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
|
|
182
|
+
store.client.call("shell.start", {
|
|
183
|
+
command: shell,
|
|
184
|
+
args: ["-c", command],
|
|
185
|
+
cwd: store.project(),
|
|
186
|
+
title: command.slice(0, 60),
|
|
187
|
+
owner: {
|
|
188
|
+
project: store.project()
|
|
189
|
+
},
|
|
190
|
+
reuse: true
|
|
191
|
+
}).then(info => {
|
|
192
|
+
void store.refresh();
|
|
193
|
+
open(info.id);
|
|
194
|
+
}).catch(err => {
|
|
195
|
+
api.ui.dialog.clear();
|
|
196
|
+
api.ui.toast({
|
|
197
|
+
variant: "error",
|
|
198
|
+
title: "Shell",
|
|
199
|
+
message: err instanceof Error ? err.message : String(err)
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
onCancel: () => api.ui.dialog.clear()
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
function restartDaemon(api, store) {
|
|
207
|
+
const running = store.shells().filter(s => s.status === "running").length;
|
|
208
|
+
const restart = force => {
|
|
209
|
+
api.ui.dialog.clear();
|
|
210
|
+
store.client.restartDaemon({
|
|
211
|
+
force
|
|
212
|
+
}).then(ok => {
|
|
213
|
+
void store.refresh();
|
|
214
|
+
api.ui.toast({
|
|
215
|
+
variant: ok ? "success" : "warning",
|
|
216
|
+
title: "Shells",
|
|
217
|
+
message: ok ? "Shell daemon restarted" : "Shells are running; restart was not forced"
|
|
218
|
+
});
|
|
219
|
+
}).catch(err => api.ui.toast({
|
|
220
|
+
variant: "error",
|
|
221
|
+
title: "Shells",
|
|
222
|
+
message: String(err)
|
|
223
|
+
}));
|
|
224
|
+
};
|
|
225
|
+
if (running === 0) return restart(false);
|
|
226
|
+
const DialogConfirm = api.ui.DialogConfirm;
|
|
227
|
+
api.ui.dialog.replace(() => _$createComponent(DialogConfirm, {
|
|
228
|
+
title: "Restart shell daemon?",
|
|
229
|
+
message: `${running} running shell${running === 1 ? "" : "s"} will be stopped.`,
|
|
230
|
+
onConfirm: () => restart(true),
|
|
231
|
+
onCancel: () => api.ui.dialog.clear()
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
function pickShell(api, store, open) {
|
|
235
|
+
const DialogSelect = api.ui.DialogSelect;
|
|
236
|
+
const shells = order(store.shells());
|
|
237
|
+
if (shells.length === 0) {
|
|
238
|
+
api.ui.toast({
|
|
239
|
+
variant: "info",
|
|
240
|
+
title: "Shells",
|
|
241
|
+
message: "No shells in this project yet"
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
api.ui.dialog.replace(() => _$createComponent(DialogSelect, {
|
|
246
|
+
title: "Shells",
|
|
247
|
+
get current() {
|
|
248
|
+
return store.selected()?.id;
|
|
249
|
+
},
|
|
250
|
+
get options() {
|
|
251
|
+
return shells.map(s => ({
|
|
252
|
+
title: s.title,
|
|
253
|
+
value: s.id,
|
|
254
|
+
description: `${BADGE_LABEL[kindOf(s)]} · ${displayCommand(s).slice(0, 60)}`
|
|
255
|
+
}));
|
|
256
|
+
},
|
|
257
|
+
onSelect: option => open(option.value)
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
const plugin = {
|
|
261
|
+
id: "opencode-cockpit.shell",
|
|
262
|
+
tui: createShellTui()
|
|
263
|
+
};
|
|
264
|
+
export default plugin;
|
package/dist/tui/keys.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const NAMED = {
|
|
2
|
+
return: "\r",
|
|
3
|
+
enter: "\r",
|
|
4
|
+
linefeed: "\n",
|
|
5
|
+
tab: "\t",
|
|
6
|
+
backspace: "\x7f",
|
|
7
|
+
escape: "\x1b",
|
|
8
|
+
space: " ",
|
|
9
|
+
up: "\x1b[A",
|
|
10
|
+
down: "\x1b[B",
|
|
11
|
+
right: "\x1b[C",
|
|
12
|
+
left: "\x1b[D",
|
|
13
|
+
home: "\x1b[H",
|
|
14
|
+
end: "\x1b[F",
|
|
15
|
+
pageup: "\x1b[5~",
|
|
16
|
+
pagedown: "\x1b[6~",
|
|
17
|
+
delete: "\x1b[3~",
|
|
18
|
+
insert: "\x1b[2~"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Re-encodes a parsed key as legacy terminal bytes. The TUI may receive keys via the kitty
|
|
23
|
+
* protocol, whose raw form programs in the PTY would not understand.
|
|
24
|
+
*/
|
|
25
|
+
export function keyToBytes(e) {
|
|
26
|
+
if (e.eventType === "release") return undefined;
|
|
27
|
+
const name = e.name?.toLowerCase() ?? "";
|
|
28
|
+
if (e.name === "tab" && e.shift) return "\x1b[Z";
|
|
29
|
+
const named = NAMED[name];
|
|
30
|
+
if (named !== undefined) return e.meta || e.option ? `\x1b${named}` : named;
|
|
31
|
+
if (e.ctrl && name.length === 1) {
|
|
32
|
+
const code = name.toUpperCase().charCodeAt(0);
|
|
33
|
+
if (code >= 64 && code <= 95) return String.fromCharCode(code - 64);
|
|
34
|
+
}
|
|
35
|
+
const text = e.sequence && !e.sequence.startsWith("\x1b") ? e.sequence : name.length === 1 ? e.shift ? name.toUpperCase() : name : undefined;
|
|
36
|
+
if (!text) return undefined;
|
|
37
|
+
return e.meta || e.option ? `\x1b${text}` : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** ctrl+] releases typing mode (the telnet escape), since esc and ctrl+c belong to the program. */
|
|
41
|
+
export function isReleaseKey(e) {
|
|
42
|
+
return e.ctrl && (e.name === "]" || e.sequence === "\x1d");
|
|
43
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
2
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
3
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
4
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
5
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
6
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
8
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
9
|
+
/** @jsxImportSource @opentui/solid */
|
|
10
|
+
|
|
11
|
+
import { createMemo, For, Show } from "solid-js";
|
|
12
|
+
import { Badge } from "./badge.js";
|
|
13
|
+
import { kindOf, shortDetail, truncate } from "./view.js";
|
|
14
|
+
export function SidebarShells(props) {
|
|
15
|
+
const theme = () => props.api.theme.current;
|
|
16
|
+
const limit = () => Math.max(1, props.store.showAll() ? props.expandedRows ?? 12 : props.rows ?? 5);
|
|
17
|
+
const counts = createMemo(() => {
|
|
18
|
+
const list = props.store.shells();
|
|
19
|
+
const of = kind => list.filter(s => kindOf(s) === kind).length;
|
|
20
|
+
// The sidebar column is ~26 characters wide; keep the summary short enough to survive it.
|
|
21
|
+
return [of("run") ? `${of("run")} run` : "", of("fail") ? `${of("fail")} fail` : "", of("done") + of("stop") ? `${of("done") + of("stop")} done` : ""].filter(Boolean).join(" · ");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Running shells and recent failures first; everything else only when expanded.
|
|
25
|
+
const candidates = createMemo(() => props.store.showAll() ? props.store.shells() : props.store.visible());
|
|
26
|
+
const shown = createMemo(() => candidates().slice(0, limit()));
|
|
27
|
+
const overflow = createMemo(() => props.store.shells().length - shown().length);
|
|
28
|
+
return _$createComponent(Show, {
|
|
29
|
+
get when() {
|
|
30
|
+
return props.store.shells().length > 0;
|
|
31
|
+
},
|
|
32
|
+
get children() {
|
|
33
|
+
var _el$ = _$createElement("box"),
|
|
34
|
+
_el$2 = _$createElement("text"),
|
|
35
|
+
_el$3 = _$createElement("b"),
|
|
36
|
+
_el$5 = _$createElement("span"),
|
|
37
|
+
_el$6 = _$createTextNode(` `);
|
|
38
|
+
_$insertNode(_el$, _el$2);
|
|
39
|
+
_$insertNode(_el$2, _el$3);
|
|
40
|
+
_$insertNode(_el$2, _el$5);
|
|
41
|
+
_$setProp(_el$2, "wrapMode", "none");
|
|
42
|
+
_$insertNode(_el$3, _$createTextNode(`Shells`));
|
|
43
|
+
_$insertNode(_el$5, _el$6);
|
|
44
|
+
_$insert(_el$5, counts, null);
|
|
45
|
+
_$insert(_el$, _$createComponent(For, {
|
|
46
|
+
get each() {
|
|
47
|
+
return shown();
|
|
48
|
+
},
|
|
49
|
+
children: shell => // Mouse-up, not mouse-down: the host dialog closes on the mouse-up that follows, so
|
|
50
|
+
// opening the console on press would need the button held down.
|
|
51
|
+
(() => {
|
|
52
|
+
var _el$8 = _$createElement("box"),
|
|
53
|
+
_el$9 = _$createElement("text"),
|
|
54
|
+
_el$0 = _$createTextNode(` `),
|
|
55
|
+
_el$1 = _$createTextNode(` `),
|
|
56
|
+
_el$10 = _$createElement("span");
|
|
57
|
+
_$insertNode(_el$8, _el$9);
|
|
58
|
+
_$setProp(_el$8, "flexDirection", "row");
|
|
59
|
+
_$setProp(_el$8, "onMouseUp", () => props.onOpen(shell.id));
|
|
60
|
+
_$insert(_el$8, _$createComponent(Badge, {
|
|
61
|
+
get api() {
|
|
62
|
+
return props.api;
|
|
63
|
+
},
|
|
64
|
+
shell: shell,
|
|
65
|
+
get frame() {
|
|
66
|
+
return props.store.frame();
|
|
67
|
+
}
|
|
68
|
+
}), _el$9);
|
|
69
|
+
_$insertNode(_el$9, _el$0);
|
|
70
|
+
_$insertNode(_el$9, _el$1);
|
|
71
|
+
_$insertNode(_el$9, _el$10);
|
|
72
|
+
_$setProp(_el$9, "wrapMode", "none");
|
|
73
|
+
_$insert(_el$9, () => truncate(shell.title, 20), _el$1);
|
|
74
|
+
_$insert(_el$10, () => shortDetail(shell, props.store.now()));
|
|
75
|
+
_$effect(_p$ => {
|
|
76
|
+
var _v$3 = theme().text,
|
|
77
|
+
_v$4 = {
|
|
78
|
+
fg: theme().textMuted
|
|
79
|
+
};
|
|
80
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$9, "fg", _v$3, _p$.e));
|
|
81
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$10, "style", _v$4, _p$.t));
|
|
82
|
+
return _p$;
|
|
83
|
+
}, {
|
|
84
|
+
e: undefined,
|
|
85
|
+
t: undefined
|
|
86
|
+
});
|
|
87
|
+
return _el$8;
|
|
88
|
+
})()
|
|
89
|
+
}), null);
|
|
90
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
91
|
+
get when() {
|
|
92
|
+
return overflow() > 0 || props.store.showAll();
|
|
93
|
+
},
|
|
94
|
+
get children() {
|
|
95
|
+
var _el$7 = _$createElement("text");
|
|
96
|
+
_$setProp(_el$7, "wrapMode", "none");
|
|
97
|
+
_$setProp(_el$7, "onMouseUp", () => props.store.toggleAll());
|
|
98
|
+
_$insert(_el$7, (() => {
|
|
99
|
+
var _c$ = _$memo(() => !!props.store.showAll());
|
|
100
|
+
return () => _c$() ? _$memo(() => overflow() > 0)() ? `▾ ${overflow()} more in ${props.consoleShortcut()} console` : "▾ show fewer" : `▸ ${overflow()} more`;
|
|
101
|
+
})());
|
|
102
|
+
_$effect(_$p => _$setProp(_el$7, "fg", theme().textMuted, _$p));
|
|
103
|
+
return _el$7;
|
|
104
|
+
}
|
|
105
|
+
}), null);
|
|
106
|
+
_$effect(_p$ => {
|
|
107
|
+
var _v$ = theme().text,
|
|
108
|
+
_v$2 = {
|
|
109
|
+
fg: theme().textMuted
|
|
110
|
+
};
|
|
111
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
|
112
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$5, "style", _v$2, _p$.t));
|
|
113
|
+
return _p$;
|
|
114
|
+
}, {
|
|
115
|
+
e: undefined,
|
|
116
|
+
t: undefined
|
|
117
|
+
});
|
|
118
|
+
return _el$;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|