@opencode-cockpit/shell 0.1.4 → 0.2.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.
Files changed (77) hide show
  1. package/README.md +100 -10
  2. package/dist/agent/plugin.js +180 -0
  3. package/dist/agent/tools/index.js +23 -0
  4. package/{src/tools/keys.ts → dist/agent/tools/keys.js} +11 -14
  5. package/dist/agent/tools/list.js +68 -0
  6. package/dist/agent/tools/read.js +59 -0
  7. package/dist/agent/tools/restart.js +44 -0
  8. package/dist/agent/tools/send.js +71 -0
  9. package/dist/agent/tools/shared.js +94 -0
  10. package/dist/agent/tools/start.js +140 -0
  11. package/dist/agent/tools/stop.js +42 -0
  12. package/dist/agent/tools/wait.js +79 -0
  13. package/dist/agent/tools/watch-args.js +12 -0
  14. package/dist/agent/tools/watch.js +74 -0
  15. package/dist/connect.js +31 -0
  16. package/dist/core/config.js +97 -0
  17. package/dist/core/find.js +69 -0
  18. package/dist/core/format.js +63 -0
  19. package/dist/core/kind.js +31 -0
  20. package/dist/server.js +2 -0
  21. package/dist/tui/components/badge.js +29 -0
  22. package/dist/tui/components/console.js +759 -0
  23. package/dist/tui/components/dock.js +270 -0
  24. package/dist/tui/components/sidebar.js +136 -0
  25. package/dist/tui/dialogs.js +163 -0
  26. package/dist/tui/index.js +205 -0
  27. package/dist/tui/lib/details.js +23 -0
  28. package/dist/tui/lib/keys.js +43 -0
  29. package/dist/tui/lib/search.js +39 -0
  30. package/dist/tui/lib/update.js +58 -0
  31. package/dist/tui/lib/view.js +191 -0
  32. package/dist/tui/state/store.js +158 -0
  33. package/package.json +21 -12
  34. package/types/agent/plugin.d.ts +12 -0
  35. package/types/agent/tools/index.d.ts +5 -0
  36. package/types/agent/tools/keys.d.ts +3 -0
  37. package/types/agent/tools/list.d.ts +3 -0
  38. package/types/agent/tools/read.d.ts +3 -0
  39. package/types/agent/tools/restart.d.ts +3 -0
  40. package/types/agent/tools/send.d.ts +3 -0
  41. package/types/agent/tools/shared.d.ts +40 -0
  42. package/types/agent/tools/start.d.ts +3 -0
  43. package/types/agent/tools/stop.d.ts +3 -0
  44. package/types/agent/tools/wait.d.ts +3 -0
  45. package/types/agent/tools/watch-args.d.ts +10 -0
  46. package/types/agent/tools/watch.d.ts +3 -0
  47. package/types/connect.d.ts +9 -0
  48. package/types/core/config.d.ts +59 -0
  49. package/types/core/find.d.ts +37 -0
  50. package/types/core/format.d.ts +8 -0
  51. package/types/core/kind.d.ts +9 -0
  52. package/types/server.d.ts +2 -0
  53. package/types/tui/components/badge.d.ts +9 -0
  54. package/types/tui/components/console.d.ts +22 -0
  55. package/types/tui/components/dock.d.ts +14 -0
  56. package/types/tui/components/sidebar.d.ts +14 -0
  57. package/types/tui/dialogs.d.ts +10 -0
  58. package/types/tui/index.d.ts +13 -0
  59. package/types/tui/lib/details.d.ts +3 -0
  60. package/types/tui/lib/keys.d.ts +8 -0
  61. package/types/tui/lib/search.d.ts +7 -0
  62. package/types/tui/lib/update.d.ts +25 -0
  63. package/types/tui/lib/view.d.ts +80 -0
  64. package/types/tui/state/store.d.ts +55 -0
  65. package/src/connect.ts +0 -24
  66. package/src/server.ts +0 -156
  67. package/src/tools/find.ts +0 -80
  68. package/src/tools/format.ts +0 -78
  69. package/src/tools/index.ts +0 -467
  70. package/src/tui/badge.tsx +0 -17
  71. package/src/tui/console.tsx +0 -438
  72. package/src/tui/dock.tsx +0 -130
  73. package/src/tui/index.tsx +0 -286
  74. package/src/tui/keys.ts +0 -52
  75. package/src/tui/sidebar.tsx +0 -44
  76. package/src/tui/store.ts +0 -185
  77. package/src/tui/view.ts +0 -161
@@ -0,0 +1,270 @@
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 { displayCommand, kindColor, kindOf, statusDetail, tailLines, tailRuns, truncate, watchColor, watchLabel } from "../lib/view.js";
14
+ import { useScreen } from "../state/store.js";
15
+ import { Badge } from "./badge.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 bodyCols = () => Math.max(10, dims().width - 4);
29
+ const body = createMemo(() => tailLines(screen()?.text, bodyRows(), bodyCols()));
30
+ const bodyRuns = createMemo(() => props.colors === false ? [] : tailRuns(screen()?.styled, bodyRows(), bodyCols()));
31
+ return (() => {
32
+ var _el$ = _$createElement("box"),
33
+ _el$2 = _$createElement("box"),
34
+ _el$3 = _$createElement("text"),
35
+ _el$4 = _$createElement("b"),
36
+ _el$7 = _$createElement("box"),
37
+ _el$8 = _$createElement("text");
38
+ _$insertNode(_el$, _el$2);
39
+ _$setProp(_el$, "flexShrink", 0);
40
+ _$setProp(_el$, "flexDirection", "column");
41
+ _$setProp(_el$, "border", ["top"]);
42
+ _$insertNode(_el$2, _el$3);
43
+ _$insertNode(_el$2, _el$7);
44
+ _$insertNode(_el$2, _el$8);
45
+ _$setProp(_el$2, "flexDirection", "row");
46
+ _$setProp(_el$2, "gap", 1);
47
+ _$setProp(_el$2, "paddingLeft", 1);
48
+ _$setProp(_el$2, "paddingRight", 1);
49
+ _$setProp(_el$2, "flexShrink", 0);
50
+ _$setProp(_el$2, "height", 1);
51
+ _$setProp(_el$2, "overflow", "hidden");
52
+ _$insertNode(_el$3, _el$4);
53
+ _$setProp(_el$3, "flexShrink", 0);
54
+ _$insertNode(_el$4, _$createTextNode(`Shells`));
55
+ _$insert(_el$2, _$createComponent(Show, {
56
+ get when() {
57
+ return props.store.shells().length > 0;
58
+ },
59
+ get fallback() {
60
+ return (() => {
61
+ var _el$9 = _$createElement("text");
62
+ _$insertNode(_el$9, _$createTextNode(`none yet · the agent starts them, or /shell-new`));
63
+ _$setProp(_el$9, "wrapMode", "none");
64
+ _$effect(_$p => _$setProp(_el$9, "fg", theme().textMuted, _$p));
65
+ return _el$9;
66
+ })();
67
+ },
68
+ get children() {
69
+ return [_$createComponent(For, {
70
+ get each() {
71
+ return tabs();
72
+ },
73
+ children: shell => {
74
+ const active = () => props.store.selected()?.id === shell.id;
75
+ return (() => {
76
+ var _el$1 = _$createElement("box"),
77
+ _el$10 = _$createElement("text"),
78
+ _el$11 = _$createTextNode(` `);
79
+ _$insertNode(_el$1, _el$10);
80
+ _$setProp(_el$1, "flexDirection", "row");
81
+ _$setProp(_el$1, "flexShrink", 0);
82
+ _$setProp(_el$1, "paddingRight", 1);
83
+ _$setProp(_el$1, "onMouseUp", () => active() ? props.onOpenConsole(shell.id) : props.store.select(shell.id));
84
+ _$insert(_el$1, _$createComponent(Badge, {
85
+ get api() {
86
+ return props.api;
87
+ },
88
+ shell: shell,
89
+ get frame() {
90
+ return props.store.frame();
91
+ }
92
+ }), _el$10);
93
+ _$insertNode(_el$10, _el$11);
94
+ _$setProp(_el$10, "wrapMode", "none");
95
+ _$insert(_el$10, (() => {
96
+ var _c$2 = _$memo(() => !!active());
97
+ return () => _c$2() ? (() => {
98
+ var _el$12 = _$createElement("b");
99
+ _$insert(_el$12, () => truncate(shell.title, 22));
100
+ return _el$12;
101
+ })() : truncate(shell.title, 22);
102
+ })(), null);
103
+ _$effect(_p$ => {
104
+ var _v$6 = active() ? theme().backgroundElement : theme().backgroundPanel,
105
+ _v$7 = active() ? theme().text : theme().textMuted;
106
+ _v$6 !== _p$.e && (_p$.e = _$setProp(_el$1, "backgroundColor", _v$6, _p$.e));
107
+ _v$7 !== _p$.t && (_p$.t = _$setProp(_el$10, "fg", _v$7, _p$.t));
108
+ return _p$;
109
+ }, {
110
+ e: undefined,
111
+ t: undefined
112
+ });
113
+ return _el$1;
114
+ })();
115
+ }
116
+ }), _$createComponent(Show, {
117
+ get when() {
118
+ return overflow() > 0 || props.store.showAll();
119
+ },
120
+ get children() {
121
+ var _el$6 = _$createElement("text");
122
+ _$setProp(_el$6, "wrapMode", "none");
123
+ _$setProp(_el$6, "flexShrink", 0);
124
+ _$setProp(_el$6, "onMouseUp", () => props.store.toggleAll());
125
+ _$insert(_el$6, (() => {
126
+ var _c$ = _$memo(() => !!props.store.showAll());
127
+ return () => _c$() ? "▾ fewer" : `▸ ${overflow()} more`;
128
+ })());
129
+ _$effect(_$p => _$setProp(_el$6, "fg", theme().textMuted, _$p));
130
+ return _el$6;
131
+ }
132
+ })];
133
+ }
134
+ }), _el$7);
135
+ _$setProp(_el$7, "flexGrow", 1);
136
+ _$setProp(_el$8, "wrapMode", "none");
137
+ _$setProp(_el$8, "flexShrink", 0);
138
+ _$insert(_el$8, () => props.hint());
139
+ _$insert(_el$, _$createComponent(Show, {
140
+ get when() {
141
+ return props.store.selected();
142
+ },
143
+ children: shell => [(() => {
144
+ var _el$13 = _$createElement("box");
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
+ _$insert(_el$13, _$createComponent(Show, {
152
+ get when() {
153
+ return bodyRuns().length > 0;
154
+ },
155
+ get fallback() {
156
+ return (() => {
157
+ var _el$19 = _$createElement("text");
158
+ _$setProp(_el$19, "wrapMode", "none");
159
+ _$insert(_el$19, () => body() || " ");
160
+ _$effect(_$p => _$setProp(_el$19, "fg", theme().text, _$p));
161
+ return _el$19;
162
+ })();
163
+ },
164
+ get children() {
165
+ var _el$14 = _$createElement("box");
166
+ _$setProp(_el$14, "flexDirection", "column");
167
+ _$insert(_el$14, _$createComponent(For, {
168
+ get each() {
169
+ return bodyRuns();
170
+ },
171
+ children: row => (() => {
172
+ var _el$20 = _$createElement("text"),
173
+ _el$21 = _$createTextNode(` `);
174
+ _$insertNode(_el$20, _el$21);
175
+ _$setProp(_el$20, "wrapMode", "none");
176
+ _$insert(_el$20, _$createComponent(For, {
177
+ each: row,
178
+ children: run => (() => {
179
+ var _el$22 = _$createElement("span");
180
+ _$insert(_el$22, () => run.text);
181
+ _$effect(_$p => _$setProp(_el$22, "style", {
182
+ fg: run.fg,
183
+ bg: run.bg,
184
+ bold: run.bold,
185
+ italic: run.italic
186
+ }, _$p));
187
+ return _el$22;
188
+ })()
189
+ }), _el$21);
190
+ _$effect(_$p => _$setProp(_el$20, "fg", theme().text, _$p));
191
+ return _el$20;
192
+ })()
193
+ }));
194
+ return _el$14;
195
+ }
196
+ }));
197
+ return _el$13;
198
+ })(), (() => {
199
+ var _el$15 = _$createElement("box"),
200
+ _el$16 = _$createElement("text");
201
+ _$insertNode(_el$15, _el$16);
202
+ _$setProp(_el$15, "flexDirection", "row");
203
+ _$setProp(_el$15, "gap", 1);
204
+ _$setProp(_el$15, "paddingLeft", 2);
205
+ _$setProp(_el$15, "flexShrink", 0);
206
+ _$setProp(_el$15, "height", 1);
207
+ _$setProp(_el$15, "overflow", "hidden");
208
+ _$setProp(_el$16, "wrapMode", "none");
209
+ _$setProp(_el$16, "flexShrink", 0);
210
+ _$insert(_el$16, () => statusDetail(shell(), props.store.now()));
211
+ _$insert(_el$15, _$createComponent(Show, {
212
+ get when() {
213
+ return watchLabel(shell());
214
+ },
215
+ get children() {
216
+ var _el$17 = _$createElement("text");
217
+ _$setProp(_el$17, "wrapMode", "none");
218
+ _$setProp(_el$17, "flexShrink", 0);
219
+ _$insert(_el$17, () => watchLabel(shell()));
220
+ _$effect(_$p => _$setProp(_el$17, "fg", watchColor(theme(), shell()), _$p));
221
+ return _el$17;
222
+ }
223
+ }), null);
224
+ _$insert(_el$15, _$createComponent(Show, {
225
+ get when() {
226
+ return _$memo(() => kindOf(shell()) === "fail")() && shell().summary;
227
+ },
228
+ get fallback() {
229
+ return (() => {
230
+ var _el$23 = _$createElement("text");
231
+ _$setProp(_el$23, "wrapMode", "none");
232
+ _$insert(_el$23, () => truncate(`$ ${displayCommand(shell())}`, Math.max(20, dims().width - 40)));
233
+ _$effect(_$p => _$setProp(_el$23, "fg", theme().textMuted, _$p));
234
+ return _el$23;
235
+ })();
236
+ },
237
+ get children() {
238
+ var _el$18 = _$createElement("text");
239
+ _$setProp(_el$18, "wrapMode", "none");
240
+ _$insert(_el$18, () => truncate(`${shell().summary}`, Math.max(20, dims().width - 40)));
241
+ _$effect(_$p => _$setProp(_el$18, "fg", theme().error, _$p));
242
+ return _el$18;
243
+ }
244
+ }), null);
245
+ _$effect(_$p => _$setProp(_el$16, "fg", kindColor(theme(), kindOf(shell())), _$p));
246
+ return _el$15;
247
+ })()]
248
+ }), null);
249
+ _$effect(_p$ => {
250
+ var _v$ = props.height,
251
+ _v$2 = theme().border,
252
+ _v$3 = theme().backgroundPanel,
253
+ _v$4 = theme().text,
254
+ _v$5 = theme().textMuted;
255
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$, "height", _v$, _p$.e));
256
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "borderColor", _v$2, _p$.t));
257
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
258
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$3, "fg", _v$4, _p$.o));
259
+ _v$5 !== _p$.i && (_p$.i = _$setProp(_el$8, "fg", _v$5, _p$.i));
260
+ return _p$;
261
+ }, {
262
+ e: undefined,
263
+ t: undefined,
264
+ a: undefined,
265
+ o: undefined,
266
+ i: undefined
267
+ });
268
+ return _el$;
269
+ })();
270
+ }
@@ -0,0 +1,136 @@
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 { kindOf, shortDetail, truncate, watchColor, watchLabel } from "../lib/view.js";
13
+ import { Badge } from "./badge.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
+ _$insert(_el$9, _$createComponent(Show, {
76
+ get when() {
77
+ return watchLabel(shell);
78
+ },
79
+ get children() {
80
+ var _el$11 = _$createElement("span"),
81
+ _el$12 = _$createTextNode(` `);
82
+ _$insertNode(_el$11, _el$12);
83
+ _$insert(_el$11, () => watchLabel(shell), null);
84
+ _$effect(_$p => _$setProp(_el$11, "style", {
85
+ fg: watchColor(theme(), shell)
86
+ }, _$p));
87
+ return _el$11;
88
+ }
89
+ }), null);
90
+ _$effect(_p$ => {
91
+ var _v$3 = theme().text,
92
+ _v$4 = {
93
+ fg: theme().textMuted
94
+ };
95
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$9, "fg", _v$3, _p$.e));
96
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$10, "style", _v$4, _p$.t));
97
+ return _p$;
98
+ }, {
99
+ e: undefined,
100
+ t: undefined
101
+ });
102
+ return _el$8;
103
+ })()
104
+ }), null);
105
+ _$insert(_el$, _$createComponent(Show, {
106
+ get when() {
107
+ return overflow() > 0 || props.store.showAll();
108
+ },
109
+ get children() {
110
+ var _el$7 = _$createElement("text");
111
+ _$setProp(_el$7, "wrapMode", "none");
112
+ _$setProp(_el$7, "onMouseUp", () => props.store.toggleAll());
113
+ _$insert(_el$7, (() => {
114
+ var _c$ = _$memo(() => !!props.store.showAll());
115
+ return () => _c$() ? _$memo(() => overflow() > 0)() ? `▾ ${overflow()} more in ${props.consoleShortcut()} console` : "▾ show fewer" : `▸ ${overflow()} more`;
116
+ })());
117
+ _$effect(_$p => _$setProp(_el$7, "fg", theme().textMuted, _$p));
118
+ return _el$7;
119
+ }
120
+ }), null);
121
+ _$effect(_p$ => {
122
+ var _v$ = theme().text,
123
+ _v$2 = {
124
+ fg: theme().textMuted
125
+ };
126
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
127
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$5, "style", _v$2, _p$.t));
128
+ return _p$;
129
+ }, {
130
+ e: undefined,
131
+ t: undefined
132
+ });
133
+ return _el$;
134
+ }
135
+ });
136
+ }
@@ -0,0 +1,163 @@
1
+ import { createComponent as _$createComponent } from "@opentui/solid";
2
+ /** @jsxImportSource @opentui/solid */
3
+
4
+ import { rmSync } from "node:fs";
5
+ import pkg from "../../package.json" with { type: "json" };
6
+ import { cacheDirFor, fetchLatestVersion, isNewer, shouldCheck } from "./lib/update.js";
7
+ import { BADGE_LABEL, displayCommand, kindOf, order } from "./lib/view.js";
8
+ const PACKAGE_NAME = pkg.name;
9
+
10
+ /** One quiet registry check a day; a newer release is announced once per version. */
11
+ export async function announceUpdate(api, meta, current) {
12
+ if (meta.source !== "npm") return;
13
+ const now = Date.now();
14
+ if (!shouldCheck(api.kv.get("cockpit.update.checkedAt", undefined), now)) return;
15
+ api.kv.set("cockpit.update.checkedAt", now);
16
+ const latest = await fetchLatestVersion(PACKAGE_NAME);
17
+ if (!latest || !isNewer(latest, current)) return;
18
+ if (api.kv.get("cockpit.update.announced", "") === latest) return;
19
+ api.kv.set("cockpit.update.announced", latest);
20
+ api.ui.toast({
21
+ variant: "info",
22
+ title: "opencode-cockpit",
23
+ message: `${latest} is available (you have ${current}). Run /cockpit-update.`,
24
+ duration: 10_000
25
+ });
26
+ }
27
+
28
+ /** Clears this plugin's cache entry so the next OpenCode start installs the newer release. */
29
+ export function offerUpdate(api, meta, current) {
30
+ const dir = cacheDirFor(meta.target, meta.source);
31
+ if (!dir) {
32
+ api.ui.toast({
33
+ variant: "info",
34
+ title: "opencode-cockpit",
35
+ message: meta.source === "file" ? `Running from ${meta.target} — update it with git, not npm.` : "This install is not managed by OpenCode's plugin installer."
36
+ });
37
+ return;
38
+ }
39
+ void fetchLatestVersion(PACKAGE_NAME).then(latest => {
40
+ if (latest && !isNewer(latest, current)) {
41
+ api.ui.toast({
42
+ variant: "success",
43
+ title: "opencode-cockpit",
44
+ message: `${current} is the latest release.`
45
+ });
46
+ return;
47
+ }
48
+ const DialogConfirm = api.ui.DialogConfirm;
49
+ api.ui.dialog.replace(() => _$createComponent(DialogConfirm, {
50
+ title: latest ? `Update to ${latest}?` : "Reinstall this plugin?",
51
+ message: `Removes the cached copy in ${dir}. OpenCode installs the new version the next time it starts, so restart it afterwards.`,
52
+ onConfirm: () => {
53
+ api.ui.dialog.clear();
54
+ try {
55
+ rmSync(dir, {
56
+ recursive: true,
57
+ force: true
58
+ });
59
+ api.ui.toast({
60
+ variant: "success",
61
+ title: "opencode-cockpit",
62
+ message: `Cached ${current} removed — restart OpenCode to install ${latest ?? "the latest release"}.`,
63
+ duration: 10_000
64
+ });
65
+ } catch (err) {
66
+ api.ui.toast({
67
+ variant: "error",
68
+ title: "opencode-cockpit",
69
+ message: String(err)
70
+ });
71
+ }
72
+ },
73
+ onCancel: () => api.ui.dialog.clear()
74
+ }));
75
+ });
76
+ }
77
+ export function newShell(api, store, open) {
78
+ const DialogPrompt = api.ui.DialogPrompt;
79
+ api.ui.dialog.replace(() => _$createComponent(DialogPrompt, {
80
+ title: "New background shell",
81
+ placeholder: "npm run dev",
82
+ onConfirm: value => {
83
+ const command = value.trim();
84
+ if (!command) return api.ui.dialog.clear();
85
+ const shell = process.env.SHELL && /(bash|zsh|fish|sh)$/.test(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
86
+ store.client.call("shell.start", {
87
+ command: shell,
88
+ args: ["-c", command],
89
+ cwd: store.project(),
90
+ title: command.slice(0, 60),
91
+ owner: {
92
+ project: store.project()
93
+ },
94
+ reuse: true
95
+ }).then(info => {
96
+ void store.refresh();
97
+ open(info.id);
98
+ }).catch(err => {
99
+ api.ui.dialog.clear();
100
+ api.ui.toast({
101
+ variant: "error",
102
+ title: "Shell",
103
+ message: err instanceof Error ? err.message : String(err)
104
+ });
105
+ });
106
+ },
107
+ onCancel: () => api.ui.dialog.clear()
108
+ }));
109
+ }
110
+ export function restartDaemon(api, store) {
111
+ const running = store.shells().filter(s => s.status === "running").length;
112
+ const restart = force => {
113
+ api.ui.dialog.clear();
114
+ store.client.restartDaemon({
115
+ force
116
+ }).then(ok => {
117
+ void store.refresh();
118
+ api.ui.toast({
119
+ variant: ok ? "success" : "warning",
120
+ title: "Shells",
121
+ message: ok ? "Shell daemon restarted" : "Shells are running; restart was not forced"
122
+ });
123
+ }).catch(err => api.ui.toast({
124
+ variant: "error",
125
+ title: "Shells",
126
+ message: String(err)
127
+ }));
128
+ };
129
+ if (running === 0) return restart(false);
130
+ const DialogConfirm = api.ui.DialogConfirm;
131
+ api.ui.dialog.replace(() => _$createComponent(DialogConfirm, {
132
+ title: "Restart shell daemon?",
133
+ message: `${running} running shell${running === 1 ? "" : "s"} will be stopped.`,
134
+ onConfirm: () => restart(true),
135
+ onCancel: () => api.ui.dialog.clear()
136
+ }));
137
+ }
138
+ export function pickShell(api, store, open) {
139
+ const DialogSelect = api.ui.DialogSelect;
140
+ const shells = order(store.shells());
141
+ if (shells.length === 0) {
142
+ api.ui.toast({
143
+ variant: "info",
144
+ title: "Shells",
145
+ message: "No shells in this project yet"
146
+ });
147
+ return;
148
+ }
149
+ api.ui.dialog.replace(() => _$createComponent(DialogSelect, {
150
+ title: "Shells",
151
+ get current() {
152
+ return store.selected()?.id;
153
+ },
154
+ get options() {
155
+ return shells.map(s => ({
156
+ title: s.title,
157
+ value: s.id,
158
+ description: `${BADGE_LABEL[kindOf(s)]} · ${displayCommand(s).slice(0, 60)}`
159
+ }));
160
+ },
161
+ onSelect: option => open(option.value)
162
+ }));
163
+ }