@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
package/src/tui/index.tsx DELETED
@@ -1,286 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import {
3
- createBindingLookup,
4
- type TuiPlugin,
5
- type TuiPluginApi,
6
- type TuiPluginModule,
7
- } from "@opencode-ai/plugin/tui"
8
- import { claimFeature, duplicateFeatureMessage } from "@opencode-cockpit/client"
9
- import { createSignal } from "solid-js"
10
- import { createClient } from "../connect.ts"
11
- import { Console } from "./console.tsx"
12
- import { Dock } from "./dock.tsx"
13
- import { SidebarShells } from "./sidebar.tsx"
14
- import { createShellStore, type ShellStore } from "./store.ts"
15
- import { BADGE_LABEL, displayCommand, kindOf, order } from "./view.ts"
16
-
17
- const DEFAULT_KEYS = {
18
- "cockpit.shells.dock": "<leader>o",
19
- "cockpit.shells.console": "<leader>i",
20
- }
21
-
22
- export interface ShellTuiOptions {
23
- dockHeight?: number
24
- /** Failures stay visible this long after they end (default 30). */
25
- historyMinutes?: number
26
- dockOpen?: boolean
27
- keybinds?: Record<string, string>
28
- }
29
-
30
- const SHELL_PACKAGE = "@opencode-cockpit/shell"
31
-
32
- /** Shell's TUI half as a factory, so bundles such as `opencode-cockpit` can include it. */
33
- export function createShellTui({ source = SHELL_PACKAGE }: { source?: string } = {}): TuiPlugin {
34
- return async (api, rawOptions, meta) => {
35
- // The renderer is shared by every TUI plugin in this OpenCode window.
36
- const claim = claimFeature(api.renderer, "shell", source)
37
- if (!claim.active) {
38
- api.ui.toast({
39
- variant: "warning",
40
- title: "opencode-cockpit",
41
- message: duplicateFeatureMessage("Shell", claim.owner, source),
42
- duration: 10_000,
43
- })
44
- return
45
- }
46
- api.lifecycle.onDispose(() => claim.release())
47
- await shellTui(api, rawOptions, meta)
48
- }
49
- }
50
-
51
- const shellTui: TuiPlugin = async (api, rawOptions) => {
52
- const options = (rawOptions ?? {}) as ShellTuiOptions
53
- const client = createClient("opencode-cockpit/tui")
54
- const store = createShellStore(api, client, { historyMinutes: options.historyMinutes })
55
- const keys = createBindingLookup({ ...DEFAULT_KEYS, ...options.keybinds })
56
-
57
- const [dockOpen, setDockOpen] = createSignal<boolean>(
58
- api.kv.get("cockpit.dock.open", options.dockOpen ?? false),
59
- )
60
- const toggleDock = () => {
61
- const next = !dockOpen()
62
- setDockOpen(next)
63
- api.kv.set("cockpit.dock.open", next)
64
- }
65
- const shortcut = (command: string) => {
66
- const bindings = api.keymap.getCommandBindings({ visibility: "registered", commands: [command] })
67
- return api.keys.formatBindings(bindings.get(command)) ?? ""
68
- }
69
-
70
- const openConsole = (id?: string, typing = false) => {
71
- if (id) store.select(id)
72
- api.ui.dialog.replace(
73
- () => (
74
- <Console
75
- api={api}
76
- store={store}
77
- typing={typing}
78
- onClose={() => api.ui.dialog.clear()}
79
- onNewShell={() => newShell(api, store, openConsole)}
80
- />
81
- ),
82
- () => {},
83
- )
84
- api.ui.dialog.setSize("xlarge")
85
- }
86
-
87
- api.keymap.registerLayer({
88
- commands: [
89
- {
90
- name: "cockpit.shells.dock",
91
- title: "Toggle shells panel",
92
- category: "Shells",
93
- namespace: "palette",
94
- slashName: "shells",
95
- run: () => toggleDock(),
96
- },
97
- {
98
- name: "cockpit.shells.console",
99
- title: "Open shell console",
100
- category: "Shells",
101
- namespace: "palette",
102
- slashName: "shell",
103
- run: () => openConsole(),
104
- },
105
- {
106
- name: "cockpit.shells.new",
107
- title: "New background shell",
108
- category: "Shells",
109
- namespace: "palette",
110
- slashName: "shell-new",
111
- run: () => newShell(api, store, openConsole),
112
- },
113
- {
114
- name: "cockpit.shells.clear",
115
- title: "Clear finished shells",
116
- category: "Shells",
117
- namespace: "palette",
118
- slashName: "shells-clear",
119
- run: () => {
120
- void store
121
- .clearFinished()
122
- .then((n) =>
123
- api.ui.toast({
124
- variant: "success",
125
- title: "Shells",
126
- message: `Cleared ${n} finished shell${n === 1 ? "" : "s"}`,
127
- }),
128
- )
129
- .catch((err) => api.ui.toast({ variant: "error", title: "Shells", message: String(err) }))
130
- },
131
- },
132
- {
133
- name: "cockpit.shells.restartDaemon",
134
- title: "Restart shell daemon",
135
- category: "Shells",
136
- namespace: "palette",
137
- slashName: "shells-restart-daemon",
138
- run: () => restartDaemon(api, store),
139
- },
140
- {
141
- name: "cockpit.shells.pick",
142
- title: "Switch shell",
143
- category: "Shells",
144
- namespace: "palette",
145
- run: () => pickShell(api, store, openConsole),
146
- },
147
- ],
148
- bindings: keys.gather("cockpit", Object.keys(DEFAULT_KEYS)),
149
- })
150
-
151
- const height = () => Math.max(6, Math.min(options.dockHeight ?? 14, Math.floor(api.renderer.height * 0.45)))
152
-
153
- api.slots.register({
154
- order: 150,
155
- slots: {
156
- app_bottom() {
157
- return (
158
- <>
159
- {dockOpen() ? (
160
- <Dock
161
- api={api}
162
- store={store}
163
- height={height()}
164
- hint={() =>
165
- `${shortcut("cockpit.shells.console")} console · ${shortcut("cockpit.shells.dock")} hide`
166
- }
167
- onOpenConsole={(id) => openConsole(id)}
168
- />
169
- ) : null}
170
- </>
171
- )
172
- },
173
- sidebar_content() {
174
- return <SidebarShells api={api} store={store} onOpen={(id) => openConsole(id)} />
175
- },
176
- },
177
- })
178
-
179
- // A daemon from older plugin code is kept only while it runs shells; say so once.
180
- const offOutdated = client.onOutdated((info) => {
181
- if (!info) return
182
- api.ui.toast({
183
- variant: "warning",
184
- title: "Shells",
185
- message:
186
- "The shell daemon is running older code because shells are still running. Run /shells-restart-daemon when convenient.",
187
- duration: 8000,
188
- })
189
- })
190
-
191
- api.lifecycle.onDispose(() => {
192
- offOutdated()
193
- store.dispose()
194
- client.close()
195
- })
196
- }
197
-
198
- function newShell(api: TuiPluginApi, store: ShellStore, open: (id?: string) => void) {
199
- const DialogPrompt = api.ui.DialogPrompt
200
- api.ui.dialog.replace(() => (
201
- <DialogPrompt
202
- title="New background shell"
203
- placeholder="npm run dev"
204
- onConfirm={(value) => {
205
- const command = value.trim()
206
- if (!command) return api.ui.dialog.clear()
207
- const shell =
208
- process.env.SHELL && /(bash|zsh|fish|sh)$/.test(process.env.SHELL) ? process.env.SHELL : "/bin/bash"
209
- store.client
210
- .call("shell.start", {
211
- command: shell,
212
- args: ["-c", command],
213
- cwd: store.project(),
214
- title: command.slice(0, 60),
215
- owner: { project: store.project() },
216
- reuse: true,
217
- })
218
- .then((info) => {
219
- void store.refresh()
220
- open(info.id)
221
- })
222
- .catch((err) => {
223
- api.ui.dialog.clear()
224
- api.ui.toast({
225
- variant: "error",
226
- title: "Shell",
227
- message: err instanceof Error ? err.message : String(err),
228
- })
229
- })
230
- }}
231
- onCancel={() => api.ui.dialog.clear()}
232
- />
233
- ))
234
- }
235
-
236
- function restartDaemon(api: TuiPluginApi, store: ShellStore) {
237
- const running = store.shells().filter((s) => s.status === "running").length
238
- const restart = (force: boolean) => {
239
- api.ui.dialog.clear()
240
- store.client
241
- .restartDaemon({ force })
242
- .then((ok) => {
243
- void store.refresh()
244
- api.ui.toast({
245
- variant: ok ? "success" : "warning",
246
- title: "Shells",
247
- message: ok ? "Shell daemon restarted" : "Shells are running; restart was not forced",
248
- })
249
- })
250
- .catch((err) => api.ui.toast({ variant: "error", title: "Shells", message: String(err) }))
251
- }
252
- if (running === 0) return restart(false)
253
- const DialogConfirm = api.ui.DialogConfirm
254
- api.ui.dialog.replace(() => (
255
- <DialogConfirm
256
- title="Restart shell daemon?"
257
- message={`${running} running shell${running === 1 ? "" : "s"} will be stopped.`}
258
- onConfirm={() => restart(true)}
259
- onCancel={() => api.ui.dialog.clear()}
260
- />
261
- ))
262
- }
263
-
264
- function pickShell(api: TuiPluginApi, store: ShellStore, open: (id?: string) => void) {
265
- const DialogSelect = api.ui.DialogSelect
266
- const shells = order(store.shells())
267
- if (shells.length === 0) {
268
- api.ui.toast({ variant: "info", title: "Shells", message: "No shells in this project yet" })
269
- return
270
- }
271
- api.ui.dialog.replace(() => (
272
- <DialogSelect
273
- title="Shells"
274
- current={store.selected()?.id}
275
- options={shells.map((s) => ({
276
- title: s.title,
277
- value: s.id,
278
- description: `${BADGE_LABEL[kindOf(s)]} · ${displayCommand(s).slice(0, 60)}`,
279
- }))}
280
- onSelect={(option) => open(option.value as string)}
281
- />
282
- ))
283
- }
284
-
285
- const plugin: TuiPluginModule & { id: string } = { id: "opencode-cockpit.shell", tui: createShellTui() }
286
- export default plugin
package/src/tui/keys.ts DELETED
@@ -1,52 +0,0 @@
1
- import type { KeyEvent } from "@opentui/core"
2
-
3
- const NAMED: Record<string, string> = {
4
- return: "\r",
5
- enter: "\r",
6
- linefeed: "\n",
7
- tab: "\t",
8
- backspace: "\x7f",
9
- escape: "\x1b",
10
- space: " ",
11
- up: "\x1b[A",
12
- down: "\x1b[B",
13
- right: "\x1b[C",
14
- left: "\x1b[D",
15
- home: "\x1b[H",
16
- end: "\x1b[F",
17
- pageup: "\x1b[5~",
18
- pagedown: "\x1b[6~",
19
- delete: "\x1b[3~",
20
- insert: "\x1b[2~",
21
- }
22
-
23
- /**
24
- * Re-encodes a parsed key as legacy terminal bytes. The TUI may receive keys via the kitty
25
- * protocol, whose raw form programs in the PTY would not understand.
26
- */
27
- export function keyToBytes(e: KeyEvent): string | undefined {
28
- if (e.eventType === "release") return undefined
29
- const name = e.name?.toLowerCase() ?? ""
30
- if (e.name === "tab" && e.shift) return "\x1b[Z"
31
- const named = NAMED[name]
32
- if (named !== undefined) return e.meta || e.option ? `\x1b${named}` : named
33
- if (e.ctrl && name.length === 1) {
34
- const code = name.toUpperCase().charCodeAt(0)
35
- if (code >= 64 && code <= 95) return String.fromCharCode(code - 64)
36
- }
37
- const text =
38
- e.sequence && !e.sequence.startsWith("\x1b")
39
- ? e.sequence
40
- : name.length === 1
41
- ? e.shift
42
- ? name.toUpperCase()
43
- : name
44
- : undefined
45
- if (!text) return undefined
46
- return e.meta || e.option ? `\x1b${text}` : text
47
- }
48
-
49
- /** ctrl+] releases typing mode (the telnet escape), since esc and ctrl+c belong to the program. */
50
- export function isReleaseKey(e: KeyEvent): boolean {
51
- return e.ctrl && (e.name === "]" || e.sequence === "\x1d")
52
- }
@@ -1,44 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
3
- import { createMemo, For, Show } from "solid-js"
4
- import { Badge } from "./badge.tsx"
5
- import type { ShellStore } from "./store.ts"
6
- import { kindOf, shortDetail, truncate } from "./view.ts"
7
-
8
- export function SidebarShells(props: { api: TuiPluginApi; store: ShellStore; onOpen: (id: string) => void }) {
9
- const theme = () => props.api.theme.current
10
- const counts = createMemo(() => {
11
- const list = props.store.shells()
12
- const running = list.filter((s) => kindOf(s) === "run").length
13
- const failed = props.store.visible().filter((s) => kindOf(s) === "fail").length
14
- return [running ? `${running} running` : "", failed ? `${failed} failed` : ""].filter(Boolean).join(" · ")
15
- })
16
-
17
- return (
18
- <Show when={props.store.shells().length > 0}>
19
- <box>
20
- <text fg={theme().text} wrapMode="none">
21
- <b>Shells</b>
22
- <span style={{ fg: theme().textMuted }}> {counts()}</span>
23
- </text>
24
- <For each={props.store.visible()}>
25
- {(shell) => (
26
- <box flexDirection="row" onMouseDown={() => props.onOpen(shell.id)}>
27
- <Badge api={props.api} shell={shell} frame={props.store.frame()} />
28
- <text fg={theme().text} wrapMode="none">
29
- {" "}
30
- {truncate(shell.title, 20)}{" "}
31
- <span style={{ fg: theme().textMuted }}>{shortDetail(shell, props.store.now())}</span>
32
- </text>
33
- </box>
34
- )}
35
- </For>
36
- <Show when={props.store.hidden().length > 0 || props.store.showAll()}>
37
- <text fg={theme().textMuted} onMouseDown={() => props.store.toggleAll()}>
38
- {props.store.showAll() ? "▾ show fewer" : `▸ ${props.store.hidden().length} more finished`}
39
- </text>
40
- </Show>
41
- </box>
42
- </Show>
43
- )
44
- }
package/src/tui/store.ts DELETED
@@ -1,185 +0,0 @@
1
- import { existsSync } from "node:fs"
2
- import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
3
- import type { CockpitClient } from "@opencode-cockpit/client"
4
- import type { ScreenResult, ShellInfo } from "@opencode-cockpit/protocol/shell"
5
- import { type Accessor, createEffect, createMemo, createRoot, createSignal, on, onCleanup } from "solid-js"
6
- import { createStore, reconcile } from "solid-js/store"
7
- import { order, partition } from "./view.ts"
8
-
9
- export interface ShellStore {
10
- client: CockpitClient
11
- project: () => string
12
- /** Every shell in the project, unordered. */
13
- shells: Accessor<ShellInfo[]>
14
- /** Ordered and folded for display: running, recent failures, plus the selection. */
15
- visible: Accessor<ShellInfo[]>
16
- hidden: Accessor<ShellInfo[]>
17
- showAll: Accessor<boolean>
18
- toggleAll(): void
19
- connected: Accessor<boolean>
20
- now: Accessor<number>
21
- /** Spinner frame index; advances only while something is running. */
22
- frame: Accessor<number>
23
- selected: Accessor<ShellInfo | undefined>
24
- select(id: string): void
25
- step(delta: number): void
26
- refresh(): Promise<void>
27
- clearFinished(): Promise<number>
28
- dispose(): void
29
- }
30
-
31
- export interface StoreOptions {
32
- /** Failures stay in the default view this long after they end. */
33
- historyMinutes?: number
34
- }
35
-
36
- export function createShellStore(
37
- api: TuiPluginApi,
38
- client: CockpitClient,
39
- options: StoreOptions = {},
40
- ): ShellStore {
41
- return createRoot((dispose) => {
42
- const [state, setState] = createStore<{ list: ShellInfo[] }>({ list: [] })
43
- const [connected, setConnected] = createSignal(false)
44
- const [now, setNow] = createSignal(Date.now())
45
- const [frame, setFrame] = createSignal(0)
46
- const [selectedId, setSelectedId] = createSignal<string>()
47
- const [showAll, setShowAll] = createSignal<boolean>(api.kv.get("cockpit.shells.showAll", false))
48
- const project = () => api.state.path.directory
49
- const historyMs = (options.historyMinutes ?? 30) * 60_000
50
-
51
- const refresh = async () => {
52
- try {
53
- const list = await client.call("shell.list", { owner: { project: project() } })
54
- setState("list", reconcile(list, { key: "id" }))
55
- } catch {
56
- // daemon not running yet; the reconnect loop will pick it up
57
- }
58
- }
59
-
60
- const offs = [
61
- client.on("shell.started", () => void refresh()),
62
- client.on("shell.exited", () => void refresh()),
63
- client.on("shell.removed", () => void refresh()),
64
- client.onState((s) => {
65
- setConnected(s === "connected")
66
- if (s === "connected") void refresh()
67
- }),
68
- ]
69
-
70
- // Connect only to a daemon that already exists; starting one is left to explicit actions.
71
- const probe = () => {
72
- if (!client.connected && existsSync(client.paths.socket)) void client.connect().catch(() => {})
73
- }
74
- probe()
75
- const probeTimer = setInterval(probe, 3000)
76
- const tick = setInterval(() => setNow(Date.now()), 1000)
77
- const spin = setInterval(() => {
78
- if (state.list.some((s) => s.status === "running")) setFrame((f) => f + 1)
79
- }, 120)
80
- onCleanup(() => {
81
- clearInterval(probeTimer)
82
- clearInterval(tick)
83
- clearInterval(spin)
84
- for (const off of offs) off()
85
- })
86
-
87
- const pick = createMemo(() => {
88
- const ordered = order(state.list)
89
- return ordered.find((s) => s.id === selectedId()) ?? ordered[0]
90
- })
91
- const folded = createMemo(() =>
92
- partition(state.list, { showAll: showAll(), historyMs, now: now(), keep: pick()?.id }),
93
- )
94
-
95
- return {
96
- client,
97
- project,
98
- shells: () => state.list,
99
- visible: () => folded().visible,
100
- hidden: () => folded().hidden,
101
- showAll,
102
- toggleAll() {
103
- const next = !showAll()
104
- setShowAll(next)
105
- api.kv.set("cockpit.shells.showAll", next)
106
- },
107
- connected,
108
- now,
109
- frame,
110
- selected: pick,
111
- select: (id) => setSelectedId(id),
112
- step(delta) {
113
- const list = folded().visible
114
- if (list.length === 0) return
115
- const index = Math.max(
116
- 0,
117
- list.findIndex((s) => s.id === pick()?.id),
118
- )
119
- const next = list[(index + delta + list.length) % list.length]
120
- if (next) setSelectedId(next.id)
121
- },
122
- refresh,
123
- async clearFinished() {
124
- const { removed } = await client.call("shell.clear", { owner: { project: project() } })
125
- await refresh()
126
- return removed.length
127
- },
128
- dispose,
129
- }
130
- })
131
- }
132
-
133
- /**
134
- * Live terminal view of one shell: attaches for change notifications and re-renders the daemon's
135
- * emulated screen, throttled. Must be called inside a reactive owner.
136
- */
137
- export function useScreen(store: ShellStore, id: Accessor<string | undefined>) {
138
- const [screen, setScreen] = createSignal<ScreenResult>()
139
- let timer: ReturnType<typeof setTimeout> | undefined
140
- let current: string | undefined
141
-
142
- const fetch = (shellId: string) => {
143
- timer = undefined
144
- void store.client
145
- .call("shell.screen", { id: shellId })
146
- .then((s) => {
147
- if (current === shellId) setScreen(s)
148
- })
149
- .catch(() => {})
150
- }
151
- const schedule = (shellId: string) => {
152
- timer ??= setTimeout(() => fetch(shellId), 80)
153
- }
154
-
155
- const offOutput = store.client.on("shell.output", (e) => {
156
- if (e.id === current) schedule(e.id)
157
- })
158
- const offExit = store.client.on("shell.exited", (info) => {
159
- if (info.id === current) schedule(info.id)
160
- })
161
-
162
- const attach = (next: string | undefined) => {
163
- if (next === current) return
164
- if (current) void store.client.call("shell.detach", { id: current }).catch(() => {})
165
- current = next
166
- setScreen(undefined)
167
- if (!next) return
168
- const info = store.shells().find((s) => s.id === next)
169
- void store.client
170
- .call("shell.attach", { id: next, fromOffset: info?.bytes ?? Number.MAX_SAFE_INTEGER })
171
- .catch(() => {})
172
- fetch(next)
173
- }
174
-
175
- createEffect(on(id, (next) => attach(next)))
176
-
177
- onCleanup(() => {
178
- clearTimeout(timer)
179
- offOutput()
180
- offExit()
181
- if (current) void store.client.call("shell.detach", { id: current }).catch(() => {})
182
- })
183
-
184
- return { screen, refetch: () => current && fetch(current) }
185
- }