@vibesdotdev/terminal-client 1.0.0-rc.2 → 1.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,12 @@
38
38
  initialCommand?: string;
39
39
  /** Href of the add-machine flow (shown when hosts array is empty). */
40
40
  addMachineHref?: string;
41
+ /**
42
+ * Allow opening with a blank command. `onOpen` then receives `''`, which
43
+ * the caller treats as "the host's default shell". Defaults to false
44
+ * (a blank field resolves to `defaultCommand`).
45
+ */
46
+ allowDefaultShell?: boolean;
41
47
  /** Called when the user confirms their selection. */
42
48
  onOpen: (hostId: string, command: string) => void;
43
49
  /** Called when the user presses Escape or dismisses. */
@@ -50,6 +56,7 @@
50
56
  defaultCommand = 'sh',
51
57
  initialCommand = '',
52
58
  addMachineHref = '/fleet',
59
+ allowDefaultShell = false,
53
60
  onOpen,
54
61
  onCancel
55
62
  }: Props = $props();
@@ -72,8 +79,8 @@
72
79
  let commandEl = $state<HTMLInputElement | null>(null);
73
80
 
74
81
  const selectedHost = $derived(hosts.find((h) => h.id === selectedHostId) ?? null);
75
- const resolvedCommand = $derived(command.trim() || defaultCommand);
76
- const canOpen = $derived(selectedHost !== null && resolvedCommand.length > 0);
82
+ const resolvedCommand = $derived(command.trim() || (allowDefaultShell ? '' : defaultCommand));
83
+ const canOpen = $derived(selectedHost !== null && (allowDefaultShell || resolvedCommand.length > 0));
77
84
 
78
85
  function handleKeydown(e: KeyboardEvent) {
79
86
  if (e.key === 'Escape') {
@@ -19,6 +19,12 @@ interface Props {
19
19
  initialCommand?: string;
20
20
  /** Href of the add-machine flow (shown when hosts array is empty). */
21
21
  addMachineHref?: string;
22
+ /**
23
+ * Allow opening with a blank command. `onOpen` then receives `''`, which
24
+ * the caller treats as "the host's default shell". Defaults to false
25
+ * (a blank field resolves to `defaultCommand`).
26
+ */
27
+ allowDefaultShell?: boolean;
22
28
  /** Called when the user confirms their selection. */
23
29
  onOpen: (hostId: string, command: string) => void;
24
30
  /** Called when the user presses Escape or dismisses. */
@@ -1 +1 @@
1
- {"version":3,"file":"HostPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/HostPicker.svelte.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG5C,UAAU,KAAK;IACd;;;OAGG;IACH,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAgLF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"HostPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/HostPicker.svelte.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG5C,UAAU,KAAK;IACd;;;OAGG;IACH,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,qDAAqD;IACrD,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAiLF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -23,6 +23,29 @@ and includes the real session id, environment id, execution id, requested `cwd`,
23
23
  and timestamp. A Files `onOpenTerminal` callback is only the request that begins
24
24
  this flow.
25
25
 
26
+ ## Host terminal workspace
27
+
28
+ `TerminalHostWorkspace` is the whole terminal experience — host picker, session
29
+ tabs, reconnect banner, terminal — for any container: a page, a dialog, or a
30
+ panel. The app supplies hosts and an opener; the workspace owns the chrome.
31
+
32
+ ```svelte
33
+ <TerminalHostWorkspace
34
+ {hosts}
35
+ layout="dialog"
36
+ initialHostId={hostId}
37
+ autoOpen
38
+ openSession={({ hostId, command, cols, rows, signal, reconnect }) =>
39
+ openHttpSession(runtime, { command, cols, rows, signal, extra: { host: hostId }, reconnect })}
40
+ />
41
+ ```
42
+
43
+ Props: `hosts`, `openSession`, `initialHostId`, `initialCommand` (pre-filled,
44
+ never auto-run), `autoOpen`, `readonly` (observe mode), `layout`
45
+ (`page` | `dialog` | `panel`), `defaultCommand`, `addMachineHref`, `hostsError`,
46
+ `hostsRefreshing`, `onRetryHosts`, `empty` and `actions` snippets, `cols`/`rows`
47
+ grid lock, `theme`, `describeError`, `label`, `class`.
48
+
26
49
  ## Runtime docs
27
50
 
28
51
  Register `@vibesdotdev/terminal-client/svelte/plugin` to contribute the canonical
@@ -0,0 +1,264 @@
1
+ <script lang="ts">
2
+ /**
3
+ * <TerminalHostWorkspace> — the embeddable terminal experience.
4
+ *
5
+ * Host picker, session tabs, reconnect banner and the terminal itself, as
6
+ * one component that fills whatever container it is given: a full page, a
7
+ * dialog, or a side panel. The host app supplies the reachable hosts and a
8
+ * session opener (dependency-injected transport); the workspace owns every
9
+ * piece of terminal chrome so each surface looks and behaves the same.
10
+ *
11
+ * State lives in the rune-free `TerminalHostWorkspaceController`
12
+ * (`./terminal-host-workspace.ts`); this file renders its snapshot.
13
+ *
14
+ * Styling is kit theme tokens only (`bg-background`, `border-border`, …), so
15
+ * the workspace matches the surrounding app shell and theme.
16
+ */
17
+ import { onDestroy, untrack, type Snippet } from 'svelte';
18
+ import { Button } from '@vibesdotdev/kit/components/ui/button';
19
+ import { Alert, AlertDescription } from '@vibesdotdev/kit/components/ui/alert';
20
+ import { cn } from '@vibesdotdev/kit/utils';
21
+ import Terminal from './Terminal.svelte';
22
+ import HostPicker from './HostPicker.svelte';
23
+ import SessionTabs from './SessionTabs.svelte';
24
+ import ReconnectBanner from './ReconnectBanner.svelte';
25
+ import {
26
+ TerminalHostWorkspaceController,
27
+ terminalHostTabBanner,
28
+ type TerminalHostErrorDescriber,
29
+ type TerminalHostSessionOpener,
30
+ type TerminalHostWorkspaceLayout
31
+ } from './terminal-host-workspace.js';
32
+ import type { HostOption, TabSession, TerminalTheme } from './types.js';
33
+
34
+ interface Props {
35
+ /** Reachable hosts, in display order. */
36
+ hosts: HostOption[];
37
+ /** Opens a session on a host. The workspace never owns a transport. */
38
+ openSession: TerminalHostSessionOpener;
39
+ /** Deep-linked host id; an unknown id shows a "not reachable" notice and the picker. */
40
+ initialHostId?: string;
41
+ /** Deep-linked command: pre-filled in the picker, never run without an explicit click. */
42
+ initialCommand?: string;
43
+ /** Open on the initial host straight away (no picker) when no command is pre-filled. */
44
+ autoOpen?: boolean;
45
+ /** Observe mode for every session: output renders, keystrokes never reach the host. */
46
+ readonly?: boolean;
47
+ /** Density only — the workspace always fills its container. */
48
+ layout?: TerminalHostWorkspaceLayout;
49
+ /** Placeholder in the picker's command field. Blank opens the host's default shell. */
50
+ defaultCommand?: string;
51
+ /** Where "Add a machine" goes in the empty picker. */
52
+ addMachineHref?: string;
53
+ /** Host discovery failure copy; shown above the workspace. */
54
+ hostsError?: string;
55
+ hostsRefreshing?: boolean;
56
+ /** Retry host discovery. Shows a retry button next to `hostsError`. */
57
+ onRetryHosts?: () => void;
58
+ /** Replaces the picker when `hosts` is empty (e.g. a sign-in prompt). */
59
+ empty?: Snippet;
60
+ /** Extra toolbar actions (rendered at the end of the tab strip). */
61
+ actions?: Snippet;
62
+ /** Lock the grid (both required). */
63
+ cols?: number;
64
+ rows?: number;
65
+ theme?: TerminalTheme;
66
+ describeError?: TerminalHostErrorDescriber;
67
+ /** Accessible name of the workspace region. */
68
+ label?: string;
69
+ class?: string;
70
+ }
71
+
72
+ let {
73
+ hosts,
74
+ openSession,
75
+ initialHostId,
76
+ initialCommand,
77
+ autoOpen = false,
78
+ readonly = false,
79
+ layout = 'page',
80
+ defaultCommand = 'your shell',
81
+ addMachineHref,
82
+ hostsError = '',
83
+ hostsRefreshing = false,
84
+ onRetryHosts,
85
+ empty,
86
+ actions,
87
+ cols,
88
+ rows,
89
+ theme,
90
+ describeError,
91
+ label = 'Terminal',
92
+ class: className = ''
93
+ }: Props = $props();
94
+
95
+ const controller = untrack(
96
+ () =>
97
+ new TerminalHostWorkspaceController({
98
+ openSession: (request) => openSession(request),
99
+ readonly,
100
+ describeError: describeError ? (failure, host) => describeError(failure, host) : undefined
101
+ })
102
+ );
103
+ let view = $state(controller.snapshot);
104
+ const unsubscribe = controller.subscribe((next) => {
105
+ view = next;
106
+ });
107
+ untrack(() => controller.start({ hosts, initialHostId, initialCommand, autoOpen }));
108
+ $effect(() => controller.setHosts(hosts));
109
+ onDestroy(() => {
110
+ unsubscribe();
111
+ controller.dispose();
112
+ });
113
+
114
+ const lockedGrid = $derived(
115
+ cols !== undefined && rows !== undefined && cols > 0 && rows > 0 ? { cols, rows } : null
116
+ );
117
+ const activeTab = $derived(view.tabs.find((tab) => tab.key === view.activeKey));
118
+ const banner = $derived(terminalHostTabBanner(activeTab));
119
+ const stripTabs = $derived<TabSession[]>(
120
+ view.tabs.map((tab) => ({
121
+ id: tab.key,
122
+ command: tab.command ?? 'shell',
123
+ hostLabel: tab.hostLabel,
124
+ startedAt: 0,
125
+ detached: tab.status === 'reconnecting' || tab.status === 'failed',
126
+ exited: tab.status === 'exited'
127
+ }))
128
+ );
129
+ const lastGrid = new Map<string, { cols: number; rows: number }>();
130
+ const padding = $derived(layout === 'page' ? 'p-2 sm:p-3' : layout === 'dialog' ? 'p-2' : 'p-1');
131
+
132
+ function ready(key: string, dims: { cols: number; rows: number }): void {
133
+ const grid = lockedGrid ?? dims;
134
+ lastGrid.set(key, grid);
135
+ controller.terminalReady(key, grid);
136
+ }
137
+
138
+ function startOver(): void {
139
+ if (activeTab?.status === 'failed' && activeTab.session === null) {
140
+ controller.retry(activeTab.key, lastGrid.get(activeTab.key) ?? lockedGrid ?? { cols: 80, rows: 24 });
141
+ return;
142
+ }
143
+ controller.showPicker();
144
+ }
145
+ </script>
146
+
147
+ <section
148
+ class={cn('flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden bg-background text-foreground', className)}
149
+ aria-label={label}
150
+ data-terminal-host-workspace
151
+ data-layout={layout}
152
+ data-picker-open={view.pickerOpen ? 'true' : 'false'}
153
+ data-active-status={activeTab?.status ?? 'none'}
154
+ data-banner-status={banner.status ?? 'none'}
155
+ >
156
+ {#if view.tabs.length > 0 || actions}
157
+ <header class="flex shrink-0 items-stretch border-b border-border">
158
+ {#if view.tabs.length > 0}
159
+ <div class="-mb-px min-w-0 flex-1">
160
+ <SessionTabs
161
+ tabs={stripTabs}
162
+ activeId={view.activeKey}
163
+ onSelect={(key) => controller.select(key)}
164
+ onClose={(key) => controller.close(key)}
165
+ onNewTab={() => controller.showPicker()}
166
+ />
167
+ </div>
168
+ {:else}
169
+ <div class="flex-1"></div>
170
+ {/if}
171
+ {#if actions}
172
+ <div class="flex shrink-0 items-center gap-2 px-2 py-1">{@render actions()}</div>
173
+ {/if}
174
+ </header>
175
+ {/if}
176
+
177
+ {#if hostsError}
178
+ <Alert class="shrink-0 rounded-none border-x-0 border-t-0" data-host-discovery-error>
179
+ <AlertDescription class="flex flex-wrap items-center justify-between gap-2">
180
+ <span>{hostsError}</span>
181
+ {#if onRetryHosts}
182
+ <Button variant="outline" size="sm" onclick={onRetryHosts} disabled={hostsRefreshing}>
183
+ {hostsRefreshing ? 'Refreshing…' : 'Retry'}
184
+ </Button>
185
+ {/if}
186
+ </AlertDescription>
187
+ </Alert>
188
+ {/if}
189
+
190
+ <div class="relative min-h-0 min-w-0 flex-1">
191
+ {#each view.tabs as tab (tab.key)}
192
+ {@const active = tab.key === view.activeKey}
193
+ <div
194
+ class={cn('absolute inset-0 flex min-h-0 flex-col', padding, !active && 'invisible')}
195
+ aria-hidden={!active}
196
+ data-terminal-tab={tab.key}
197
+ data-terminal-tab-status={tab.status}
198
+ data-terminal-tab-host={tab.hostId}
199
+ >
200
+ {#if tab.readonly}
201
+ <p class="mb-2 shrink-0 rounded-md border border-warning/40 bg-warning/10 px-3 py-1.5 text-xs text-warning" role="status">
202
+ Observe mode — input is disabled.
203
+ </p>
204
+ {/if}
205
+ <div class="min-h-0 flex-1">
206
+ <Terminal
207
+ session={tab.session}
208
+ {theme}
209
+ cols={lockedGrid?.cols}
210
+ rows={lockedGrid?.rows}
211
+ connectionState={tab.session ? 'live' : 'connecting'}
212
+ readonly={tab.readonly || tab.status !== 'live' || !tab.inputReady}
213
+ onReady={(dims) => ready(tab.key, dims)}
214
+ onInputReady={() => controller.inputReady(tab.key)}
215
+ onExit={(result) => controller.exited(tab.key, result.exitCode)}
216
+ onDisconnect={(reason) => controller.disconnected(tab.key, reason)}
217
+ />
218
+ </div>
219
+ {#if active}
220
+ <ReconnectBanner
221
+ status={banner.status}
222
+ attempt={banner.attempt}
223
+ delayMs={banner.delayMs}
224
+ reason={banner.reason}
225
+ onNewSession={startOver}
226
+ />
227
+ {/if}
228
+ </div>
229
+ {/each}
230
+
231
+ {#if view.pickerOpen}
232
+ <div
233
+ class={cn(
234
+ 'absolute inset-0 z-10 flex items-center justify-center overflow-auto',
235
+ layout === 'panel' ? 'p-2' : 'p-4',
236
+ view.tabs.length > 0 && 'bg-background/80 backdrop-blur-sm'
237
+ )}
238
+ data-terminal-picker
239
+ >
240
+ <div class="flex w-full max-w-lg flex-col gap-3">
241
+ {#if view.requestedHostUnavailable}
242
+ <Alert variant="destructive" data-requested-host-unavailable>
243
+ <AlertDescription>The requested machine is not reachable. Pick another machine below.</AlertDescription>
244
+ </Alert>
245
+ {/if}
246
+ {#if hosts.length === 0 && empty}
247
+ {@render empty()}
248
+ {:else}
249
+ <HostPicker
250
+ {hosts}
251
+ preselectedHostId={view.pickerHostId}
252
+ initialCommand={view.pickerCommand}
253
+ {defaultCommand}
254
+ {addMachineHref}
255
+ allowDefaultShell
256
+ onOpen={(hostId, command) => controller.open(hostId, command)}
257
+ onCancel={view.tabs.length > 0 ? () => controller.hidePicker() : undefined}
258
+ />
259
+ {/if}
260
+ </div>
261
+ </div>
262
+ {/if}
263
+ </div>
264
+ </section>
@@ -0,0 +1,59 @@
1
+ /**
2
+ * <TerminalHostWorkspace> — the embeddable terminal experience.
3
+ *
4
+ * Host picker, session tabs, reconnect banner and the terminal itself, as
5
+ * one component that fills whatever container it is given: a full page, a
6
+ * dialog, or a side panel. The host app supplies the reachable hosts and a
7
+ * session opener (dependency-injected transport); the workspace owns every
8
+ * piece of terminal chrome so each surface looks and behaves the same.
9
+ *
10
+ * State lives in the rune-free `TerminalHostWorkspaceController`
11
+ * (`./terminal-host-workspace.ts`); this file renders its snapshot.
12
+ *
13
+ * Styling is kit theme tokens only (`bg-background`, `border-border`, …), so
14
+ * the workspace matches the surrounding app shell and theme.
15
+ */
16
+ import { type Snippet } from 'svelte';
17
+ import { type TerminalHostErrorDescriber, type TerminalHostSessionOpener, type TerminalHostWorkspaceLayout } from './terminal-host-workspace.ts';
18
+ import type { HostOption, TerminalTheme } from './types.ts';
19
+ interface Props {
20
+ /** Reachable hosts, in display order. */
21
+ hosts: HostOption[];
22
+ /** Opens a session on a host. The workspace never owns a transport. */
23
+ openSession: TerminalHostSessionOpener;
24
+ /** Deep-linked host id; an unknown id shows a "not reachable" notice and the picker. */
25
+ initialHostId?: string;
26
+ /** Deep-linked command: pre-filled in the picker, never run without an explicit click. */
27
+ initialCommand?: string;
28
+ /** Open on the initial host straight away (no picker) when no command is pre-filled. */
29
+ autoOpen?: boolean;
30
+ /** Observe mode for every session: output renders, keystrokes never reach the host. */
31
+ readonly?: boolean;
32
+ /** Density only — the workspace always fills its container. */
33
+ layout?: TerminalHostWorkspaceLayout;
34
+ /** Placeholder in the picker's command field. Blank opens the host's default shell. */
35
+ defaultCommand?: string;
36
+ /** Where "Add a machine" goes in the empty picker. */
37
+ addMachineHref?: string;
38
+ /** Host discovery failure copy; shown above the workspace. */
39
+ hostsError?: string;
40
+ hostsRefreshing?: boolean;
41
+ /** Retry host discovery. Shows a retry button next to `hostsError`. */
42
+ onRetryHosts?: () => void;
43
+ /** Replaces the picker when `hosts` is empty (e.g. a sign-in prompt). */
44
+ empty?: Snippet;
45
+ /** Extra toolbar actions (rendered at the end of the tab strip). */
46
+ actions?: Snippet;
47
+ /** Lock the grid (both required). */
48
+ cols?: number;
49
+ rows?: number;
50
+ theme?: TerminalTheme;
51
+ describeError?: TerminalHostErrorDescriber;
52
+ /** Accessible name of the workspace region. */
53
+ label?: string;
54
+ class?: string;
55
+ }
56
+ declare const TerminalHostWorkspace: import("svelte").Component<Props, {}, "">;
57
+ type TerminalHostWorkspace = ReturnType<typeof TerminalHostWorkspace>;
58
+ export default TerminalHostWorkspace;
59
+ //# sourceMappingURL=TerminalHostWorkspace.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TerminalHostWorkspace.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/TerminalHostWorkspace.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;OAcI;AACJ,OAAO,EAAsB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAQ1D,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,MAAM,8BAA8B,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAc,aAAa,EAAE,MAAM,YAAY,CAAC;AAGvE,UAAU,KAAK;IACd,yCAAyC;IACzC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,uEAAuE;IACvE,WAAW,EAAE,yBAAyB,CAAC;IACvC,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,uFAAuF;IACvF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,yEAAyE;IACzE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,aAAa,CAAC,EAAE,0BAA0B,CAAC;IAC3C,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAkKF,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
@@ -16,11 +16,13 @@
16
16
  */
17
17
  export { default as Terminal } from './Terminal.svelte';
18
18
  export { default as TerminalWorkspace } from './TerminalWorkspace.svelte';
19
+ export { default as TerminalHostWorkspace } from './TerminalHostWorkspace.svelte';
19
20
  export { default as TerminalMobileToolbar } from './TerminalMobileToolbar.svelte';
20
21
  export { default as HostPicker } from './HostPicker.svelte';
21
22
  export { default as SessionTabs } from './SessionTabs.svelte';
22
23
  export { default as ReconnectBanner } from './ReconnectBanner.svelte';
23
24
  export { default as TerminalToolCard } from './tool-widgets/TerminalToolCard.svelte';
25
+ export { TerminalHostWorkspaceController, describeTerminalHostFailure, terminalHostLabel, terminalHostTabBanner, type TerminalHostBanner, type TerminalHostBannerStatus, type TerminalHostErrorDescriber, type TerminalHostOpenedSession, type TerminalHostSessionOpener, type TerminalHostSessionRequest, type TerminalHostTab, type TerminalHostTabStatus, type TerminalHostWorkspaceLayout, type TerminalHostWorkspaceOptions, type TerminalHostWorkspaceSnapshot, type TerminalHostWorkspaceStart } from './terminal-host-workspace.ts';
24
26
  export { registerTerminalSvelteToolWidget, terminalSveltePlugin, terminalSvelteWidgetDescriptor, default } from './terminal-svelte.plugin.ts';
25
27
  export { openTerminalWorkspaceSession, openTerminalWorkspaceTargetSession, terminalWorkspaceCapabilityBanner, terminalWorkspaceError, terminalWorkspaceExitedEvent, terminalWorkspacePlaceDefaultCapabilities, terminalWorkspacePlaceLabel, terminalWorkspaceStatusLabel, terminalWorkspaceVesselStateSchema, type OpenTerminalWorkspaceSessionInput, type OpenTerminalWorkspaceSessionResult, type OpenTerminalWorkspaceTargetSessionInput, type OpenTerminalWorkspaceTargetSessionResult, type TerminalWorkspaceExitedEvent, type TerminalWorkspaceLifecycleEvent, type TerminalWorkspaceOpenedReceipt, type TerminalWorkspaceRuntimePlaceOption, type TerminalWorkspaceSessionMode, type TerminalWorkspaceStatus } from '../workspace.ts';
26
28
  export { DEFAULT_FONT, DEFAULT_TERMINAL_THEME, VIBES_TERMINAL_THEME, type ExecutionSession, type TerminalProps, type TerminalTheme, type HostOption, type TabSession } from './types.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,EACN,gCAAgC,EAChC,oBAAoB,EACpB,8BAA8B,EAC9B,OAAO,EACP,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACN,4BAA4B,EAC5B,kCAAkC,EAClC,iCAAiC,EACjC,sBAAsB,EACtB,4BAA4B,EAC5B,yCAAyC,EACzC,2BAA2B,EAC3B,4BAA4B,EAC5B,kCAAkC,EAClC,KAAK,iCAAiC,EACtC,KAAK,kCAAkC,EACvC,KAAK,uCAAuC,EAC5C,KAAK,wCAAwC,EAC7C,KAAK,4BAA4B,EACjC,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,EACnC,KAAK,mCAAmC,EACxC,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,EACN,+BAA+B,EAC/B,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACN,gCAAgC,EAChC,oBAAoB,EACpB,8BAA8B,EAC9B,OAAO,EACP,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACN,4BAA4B,EAC5B,kCAAkC,EAClC,iCAAiC,EACjC,sBAAsB,EACtB,4BAA4B,EAC5B,yCAAyC,EACzC,2BAA2B,EAC3B,4BAA4B,EAC5B,kCAAkC,EAClC,KAAK,iCAAiC,EACtC,KAAK,kCAAkC,EACvC,KAAK,uCAAuC,EAC5C,KAAK,wCAAwC,EAC7C,KAAK,4BAA4B,EACjC,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,EACnC,KAAK,mCAAmC,EACxC,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,MAAM,YAAY,CAAC"}
@@ -16,11 +16,13 @@
16
16
  */
17
17
  export { default as Terminal } from './Terminal.svelte';
18
18
  export { default as TerminalWorkspace } from './TerminalWorkspace.svelte';
19
+ export { default as TerminalHostWorkspace } from './TerminalHostWorkspace.svelte';
19
20
  export { default as TerminalMobileToolbar } from './TerminalMobileToolbar.svelte';
20
21
  export { default as HostPicker } from './HostPicker.svelte';
21
22
  export { default as SessionTabs } from './SessionTabs.svelte';
22
23
  export { default as ReconnectBanner } from './ReconnectBanner.svelte';
23
24
  export { default as TerminalToolCard } from './tool-widgets/TerminalToolCard.svelte';
25
+ export { TerminalHostWorkspaceController, describeTerminalHostFailure, terminalHostLabel, terminalHostTabBanner } from "./terminal-host-workspace.js";
24
26
  export { registerTerminalSvelteToolWidget, terminalSveltePlugin, terminalSvelteWidgetDescriptor, default } from "./terminal-svelte.plugin.js";
25
27
  export { openTerminalWorkspaceSession, openTerminalWorkspaceTargetSession, terminalWorkspaceCapabilityBanner, terminalWorkspaceError, terminalWorkspaceExitedEvent, terminalWorkspacePlaceDefaultCapabilities, terminalWorkspacePlaceLabel, terminalWorkspaceStatusLabel, terminalWorkspaceVesselStateSchema } from "../workspace.js";
26
28
  export { DEFAULT_FONT, DEFAULT_TERMINAL_THEME, VIBES_TERMINAL_THEME } from "./types.js";
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Rune-free state owner for `<TerminalHostWorkspace>`.
3
+ *
4
+ * The workspace is the embeddable terminal experience: pick a host, open
5
+ * sessions in tabs, see reconnect state, close sessions. This controller owns
6
+ * that state machine so it is the same in a full page, a dialog, or a panel,
7
+ * and so it is testable without a DOM. The component renders a snapshot and
8
+ * forwards user and `<Terminal>` events here.
9
+ *
10
+ * Transport is injected: the host app supplies a `TerminalHostSessionOpener`
11
+ * (typically a closure over `openHttpSession`). The controller never imports
12
+ * an HTTP client and never invents a session — a tab is `live` only after the
13
+ * opener resolves with a real `ExecutionSession`.
14
+ */
15
+ import { CapabilityError } from '@vibesdotdev/kernel';
16
+ import type { ExecutionSession } from '@vibesdotdev/kernel/execution';
17
+ import type { HostOption } from './types.ts';
18
+ /** What the workspace asks the host app's opener for. */
19
+ export interface TerminalHostSessionRequest {
20
+ /** The selected `HostOption.id`. */
21
+ hostId: string;
22
+ /** Command to run; `undefined` means the host's default shell. */
23
+ command?: string;
24
+ /** Fitted (or locked) grid the session must start at. */
25
+ cols: number;
26
+ rows: number;
27
+ /** Aborted when the user closes the tab or the workspace unmounts mid-open. */
28
+ signal: AbortSignal;
29
+ /**
30
+ * Transport reconnect hooks. Forward these to the transport (for
31
+ * `openHttpSession`, as `reconnect.onReconnecting` / `reconnect.onReconnected`)
32
+ * so the workspace can show the reconnect banner.
33
+ */
34
+ reconnect: {
35
+ onReconnecting: (attempt: number, delayMs: number) => void;
36
+ onReconnected: (attempt: number) => void;
37
+ };
38
+ }
39
+ /** A session the opener created. */
40
+ export interface TerminalHostOpenedSession {
41
+ /** Stable session id (shown nowhere; used for tab identity and app hooks). */
42
+ id: string;
43
+ session: ExecutionSession;
44
+ }
45
+ /** Dependency-injected session opener; the workspace owns no transport. */
46
+ export type TerminalHostSessionOpener = (request: TerminalHostSessionRequest) => Promise<TerminalHostOpenedSession>;
47
+ /** Turns an open failure into user-facing copy. */
48
+ export type TerminalHostErrorDescriber = (failure: CapabilityError, hostLabel: string) => string;
49
+ /** Where the workspace is mounted. Only density changes; it always fills its container. */
50
+ export type TerminalHostWorkspaceLayout = 'page' | 'dialog' | 'panel';
51
+ export type TerminalHostTabStatus = 'opening' | 'live' | 'reconnecting' | 'failed' | 'exited';
52
+ export type TerminalHostBannerStatus = 'reconnecting' | 'reconnected' | 'failed' | null;
53
+ export interface TerminalHostTab {
54
+ /** Workspace-local identity (stable before the session id exists). */
55
+ readonly key: string;
56
+ /** Session id once opened. */
57
+ readonly sessionId: string | null;
58
+ readonly hostId: string;
59
+ readonly hostLabel: string;
60
+ /** Requested command; `undefined` = host default shell. */
61
+ readonly command: string | undefined;
62
+ /** Observe mode: keystrokes never reach the session. */
63
+ readonly readonly: boolean;
64
+ readonly status: TerminalHostTabStatus;
65
+ readonly session: ExecutionSession | null;
66
+ readonly inputReady: boolean;
67
+ readonly exitCode: number | null;
68
+ /** User-facing failure copy for `failed`. */
69
+ readonly error: string;
70
+ /** The typed failure behind `failed`, for hosts that log or report it. */
71
+ readonly failure: CapabilityError | null;
72
+ readonly reconnectAttempt: number;
73
+ readonly reconnectDelayMs: number;
74
+ /** True for a short moment after a successful reconnect. */
75
+ readonly reattached: boolean;
76
+ }
77
+ export interface TerminalHostBanner {
78
+ status: TerminalHostBannerStatus;
79
+ attempt: number;
80
+ delayMs: number;
81
+ reason: string;
82
+ }
83
+ export interface TerminalHostWorkspaceSnapshot {
84
+ readonly tabs: readonly TerminalHostTab[];
85
+ readonly activeKey: string;
86
+ readonly pickerOpen: boolean;
87
+ /** The deep-linked host is not in the reachable host list. */
88
+ readonly requestedHostUnavailable: boolean;
89
+ /** Host the picker preselects. */
90
+ readonly pickerHostId: string;
91
+ /** Command the picker pre-fills (never auto-run). */
92
+ readonly pickerCommand: string;
93
+ }
94
+ export interface TerminalHostWorkspaceStart {
95
+ hosts: readonly HostOption[];
96
+ /** Deep-linked host id. Absent → the first host. */
97
+ initialHostId?: string;
98
+ /** Deep-linked command: pre-filled in the picker, never run without a click. */
99
+ initialCommand?: string;
100
+ /** Open a session on the initial host immediately (no picker) when no command is pre-filled. */
101
+ autoOpen?: boolean;
102
+ }
103
+ export interface TerminalHostWorkspaceOptions {
104
+ openSession: TerminalHostSessionOpener;
105
+ /** Observe mode for every session this workspace opens. */
106
+ readonly?: boolean;
107
+ describeError?: TerminalHostErrorDescriber;
108
+ /** How long the "Reattached" banner stays (ms). */
109
+ reattachedNoticeMs?: number;
110
+ }
111
+ /** Product copy for an open failure. Never names transports, kinds, or descriptors. */
112
+ export declare function describeTerminalHostFailure(failure: CapabilityError, hostLabel: string): string;
113
+ /** The banner the active tab shows. Exited and failed tabs offer a new session. */
114
+ export declare function terminalHostTabBanner(tab: TerminalHostTab | undefined): TerminalHostBanner;
115
+ /** Tab label host suffix and picker label for a host. */
116
+ export declare function terminalHostLabel(host: HostOption | undefined, hostId: string): string;
117
+ type Listener = (snapshot: TerminalHostWorkspaceSnapshot) => void;
118
+ export declare class TerminalHostWorkspaceController {
119
+ #private;
120
+ constructor(options: TerminalHostWorkspaceOptions);
121
+ get snapshot(): TerminalHostWorkspaceSnapshot;
122
+ subscribe(listener: Listener): () => void;
123
+ /** The latest reachable hosts (labels for new tabs). */
124
+ setHosts(hosts: readonly HostOption[]): void;
125
+ /** Resolve the deep link once: open on the initial host, or show the picker. */
126
+ start(input: TerminalHostWorkspaceStart): void;
127
+ /** Create a tab on `hostId`. The session opens once its terminal reports a grid. */
128
+ open(hostId: string, command?: string): string;
129
+ /** `<Terminal onReady>` for a tab: start the session at the fitted grid. */
130
+ terminalReady(key: string, dims: {
131
+ cols: number;
132
+ rows: number;
133
+ }): void;
134
+ /** Retry a failed tab in place (same host + command, new session). */
135
+ retry(key: string, dims: {
136
+ cols: number;
137
+ rows: number;
138
+ }): void;
139
+ inputReady(key: string): void;
140
+ exited(key: string, exitCode: number | null): void;
141
+ disconnected(key: string, reason: string): void;
142
+ select(key: string): void;
143
+ /** Close a tab: abort an in-flight open, end a live session. */
144
+ close(key: string): void;
145
+ showPicker(): void;
146
+ /** Dismiss the picker (only when a tab can be shown behind it). */
147
+ hidePicker(): void;
148
+ /** Unmount: abort opens and end every session this workspace started. */
149
+ dispose(): void;
150
+ }
151
+ export {};
152
+ //# sourceMappingURL=terminal-host-workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-host-workspace.d.ts","sourceRoot":"","sources":["../../src/svelte/terminal-host-workspace.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Rune-free state owner for `<TerminalHostWorkspace>`.
3
+ *
4
+ * The workspace is the embeddable terminal experience: pick a host, open
5
+ * sessions in tabs, see reconnect state, close sessions. This controller owns
6
+ * that state machine so it is the same in a full page, a dialog, or a panel,
7
+ * and so it is testable without a DOM. The component renders a snapshot and
8
+ * forwards user and `<Terminal>` events here.
9
+ *
10
+ * Transport is injected: the host app supplies a `TerminalHostSessionOpener`
11
+ * (typically a closure over `openHttpSession`). The controller never imports
12
+ * an HTTP client and never invents a session — a tab is `live` only after the
13
+ * opener resolves with a real `ExecutionSession`.
14
+ */
15
+ import * as z from 'zod/v4';
16
+ import { CapabilityError, CaughtFailureSchema } from '@vibesdotdev/kernel';
17
+ const HttpStatusSchema = z.number().int();
18
+ /** Product copy for an open failure. Never names transports, kinds, or descriptors. */
19
+ export function describeTerminalHostFailure(failure, hostLabel) {
20
+ const parsedStatus = HttpStatusSchema.safeParse(failure.details?.status);
21
+ const status = parsedStatus.success ? parsedStatus.data : undefined;
22
+ const text = `${failure.message} ${failure.reason}`.toLowerCase();
23
+ const machine = hostLabel || 'this machine';
24
+ if (status === 401)
25
+ return 'Sign in to open a terminal session.';
26
+ if (status === 403 || failure.code === 'PERMISSION_DENIED') {
27
+ return "Access denied — you don't have permission to open a terminal on this machine.";
28
+ }
29
+ if (status === 429 || /max[_\s-]?sessions|too many sessions/.test(text)) {
30
+ return `Too many sessions are open on ${machine} — close one to start another.`;
31
+ }
32
+ if (status === 503 || status === 502 || /unreachable|unavailable|offline/.test(text)) {
33
+ return `${machine} is not reachable right now. Make sure it is online, then try again.`;
34
+ }
35
+ return `Unable to open a terminal on ${machine}. Check that it is online, then try again.`;
36
+ }
37
+ /** The banner the active tab shows. Exited and failed tabs offer a new session. */
38
+ export function terminalHostTabBanner(tab) {
39
+ const none = { status: null, attempt: 0, delayMs: 0, reason: '' };
40
+ if (!tab)
41
+ return none;
42
+ switch (tab.status) {
43
+ case 'reconnecting':
44
+ return { status: 'reconnecting', attempt: tab.reconnectAttempt, delayMs: tab.reconnectDelayMs, reason: '' };
45
+ case 'failed':
46
+ return { ...none, status: 'failed', reason: tab.error || 'Connection lost and could not be restored.' };
47
+ case 'exited':
48
+ return {
49
+ ...none,
50
+ status: 'failed',
51
+ reason: tab.exitCode === null ? 'Session ended.' : `Session ended — exit code ${tab.exitCode}.`
52
+ };
53
+ case 'live':
54
+ return tab.reattached ? { ...none, status: 'reconnected' } : none;
55
+ default:
56
+ return none;
57
+ }
58
+ }
59
+ /** Tab label host suffix and picker label for a host. */
60
+ export function terminalHostLabel(host, hostId) {
61
+ if (!host)
62
+ return hostId;
63
+ return host.local ? `This machine — ${host.name}` : host.name;
64
+ }
65
+ export class TerminalHostWorkspaceController {
66
+ #options;
67
+ #hosts = [];
68
+ #tabs = [];
69
+ #runtime = new Map();
70
+ #activeKey = '';
71
+ #pickerOpen = true;
72
+ #requestedHostUnavailable = false;
73
+ #pickerHostId = '';
74
+ #pickerCommand = '';
75
+ #nextKey = 0;
76
+ #disposed = false;
77
+ #listeners = new Set();
78
+ #snapshot;
79
+ constructor(options) {
80
+ this.#options = options;
81
+ this.#snapshot = this.#build();
82
+ }
83
+ get snapshot() {
84
+ return this.#snapshot;
85
+ }
86
+ subscribe(listener) {
87
+ this.#listeners.add(listener);
88
+ return () => this.#listeners.delete(listener);
89
+ }
90
+ /** The latest reachable hosts (labels for new tabs). */
91
+ setHosts(hosts) {
92
+ this.#hosts = hosts;
93
+ }
94
+ /** Resolve the deep link once: open on the initial host, or show the picker. */
95
+ start(input) {
96
+ this.#hosts = input.hosts;
97
+ const requested = input.initialHostId?.trim() ?? '';
98
+ const known = requested ? input.hosts.some((host) => host.id === requested) : false;
99
+ this.#requestedHostUnavailable = Boolean(requested) && !known;
100
+ this.#pickerHostId = known ? requested : (input.hosts[0]?.id ?? '');
101
+ this.#pickerCommand = input.initialCommand ?? '';
102
+ const canAutoOpen = input.autoOpen === true &&
103
+ !this.#requestedHostUnavailable &&
104
+ !this.#pickerCommand &&
105
+ this.#pickerHostId !== '';
106
+ if (canAutoOpen)
107
+ this.open(this.#pickerHostId);
108
+ else
109
+ this.#emit();
110
+ }
111
+ /** Create a tab on `hostId`. The session opens once its terminal reports a grid. */
112
+ open(hostId, command) {
113
+ const key = `tab-${++this.#nextKey}`;
114
+ const host = this.#hosts.find((candidate) => candidate.id === hostId);
115
+ this.#tabs = [
116
+ ...this.#tabs,
117
+ {
118
+ key,
119
+ sessionId: null,
120
+ hostId,
121
+ hostLabel: terminalHostLabel(host, hostId),
122
+ command: command?.trim() ? command.trim() : undefined,
123
+ readonly: this.#options.readonly === true,
124
+ status: 'opening',
125
+ session: null,
126
+ inputReady: false,
127
+ exitCode: null,
128
+ error: '',
129
+ failure: null,
130
+ reconnectAttempt: 0,
131
+ reconnectDelayMs: 0,
132
+ reattached: false
133
+ }
134
+ ];
135
+ this.#runtime.set(key, { controller: null, requested: false, reattachTimer: null });
136
+ this.#activeKey = key;
137
+ this.#pickerOpen = false;
138
+ this.#requestedHostUnavailable = false;
139
+ this.#emit();
140
+ return key;
141
+ }
142
+ /** `<Terminal onReady>` for a tab: start the session at the fitted grid. */
143
+ terminalReady(key, dims) {
144
+ const tab = this.#tab(key);
145
+ const runtime = this.#runtime.get(key);
146
+ if (!tab || !runtime || runtime.requested || tab.status !== 'opening')
147
+ return;
148
+ runtime.requested = true;
149
+ void this.#openSession(key, tab, runtime, dims);
150
+ }
151
+ /** Retry a failed tab in place (same host + command, new session). */
152
+ retry(key, dims) {
153
+ const tab = this.#tab(key);
154
+ const runtime = this.#runtime.get(key);
155
+ if (!tab || !runtime || tab.status !== 'failed')
156
+ return;
157
+ tab.session?.kill();
158
+ this.#patch(key, { status: 'opening', session: null, sessionId: null, error: '', inputReady: false });
159
+ runtime.requested = false;
160
+ this.terminalReady(key, dims);
161
+ }
162
+ inputReady(key) {
163
+ const tab = this.#tab(key);
164
+ if (tab && !tab.inputReady && tab.session)
165
+ this.#patch(key, { inputReady: true });
166
+ }
167
+ exited(key, exitCode) {
168
+ const tab = this.#tab(key);
169
+ if (!tab || tab.status === 'exited')
170
+ return;
171
+ this.#patch(key, { status: 'exited', exitCode, inputReady: false, reattached: false });
172
+ }
173
+ disconnected(key, reason) {
174
+ const tab = this.#tab(key);
175
+ if (!tab || tab.status === 'exited')
176
+ return;
177
+ this.#patch(key, {
178
+ status: 'failed',
179
+ inputReady: false,
180
+ reattached: false,
181
+ error: reason ? `Connection lost — ${reason}` : 'Connection lost and could not be restored.'
182
+ });
183
+ }
184
+ select(key) {
185
+ if (!this.#tab(key))
186
+ return;
187
+ this.#activeKey = key;
188
+ this.#pickerOpen = false;
189
+ this.#emit();
190
+ }
191
+ /** Close a tab: abort an in-flight open, end a live session. */
192
+ close(key) {
193
+ const tab = this.#tab(key);
194
+ if (!tab)
195
+ return;
196
+ this.#release(key, tab);
197
+ const index = this.#tabs.findIndex((candidate) => candidate.key === key);
198
+ this.#tabs = this.#tabs.filter((candidate) => candidate.key !== key);
199
+ if (this.#activeKey === key) {
200
+ this.#activeKey = this.#tabs[Math.min(index, this.#tabs.length - 1)]?.key ?? '';
201
+ }
202
+ if (this.#tabs.length === 0)
203
+ this.#pickerOpen = true;
204
+ this.#emit();
205
+ }
206
+ showPicker() {
207
+ this.#pickerOpen = true;
208
+ this.#emit();
209
+ }
210
+ /** Dismiss the picker (only when a tab can be shown behind it). */
211
+ hidePicker() {
212
+ if (this.#tabs.length === 0)
213
+ return;
214
+ this.#pickerOpen = false;
215
+ this.#emit();
216
+ }
217
+ /** Unmount: abort opens and end every session this workspace started. */
218
+ dispose() {
219
+ if (this.#disposed)
220
+ return;
221
+ this.#disposed = true;
222
+ for (const tab of this.#tabs)
223
+ this.#release(tab.key, tab);
224
+ this.#listeners.clear();
225
+ }
226
+ async #openSession(key, tab, runtime, dims) {
227
+ const controller = new AbortController();
228
+ runtime.controller = controller;
229
+ const live = () => !this.#disposed && !controller.signal.aborted && this.#tab(key) !== undefined;
230
+ const outcome = await this.#options
231
+ .openSession({
232
+ hostId: tab.hostId,
233
+ command: tab.command,
234
+ cols: dims.cols,
235
+ rows: dims.rows,
236
+ signal: controller.signal,
237
+ reconnect: {
238
+ onReconnecting: (attempt, delayMs) => {
239
+ if (!live())
240
+ return;
241
+ this.#clearReattached(runtime);
242
+ this.#patch(key, {
243
+ status: 'reconnecting',
244
+ reconnectAttempt: attempt,
245
+ reconnectDelayMs: delayMs,
246
+ reattached: false
247
+ });
248
+ },
249
+ onReconnected: () => {
250
+ if (!live())
251
+ return;
252
+ this.#patch(key, { status: 'live', reconnectAttempt: 0, reconnectDelayMs: 0, reattached: true });
253
+ this.#clearReattached(runtime);
254
+ runtime.reattachTimer = setTimeout(() => {
255
+ runtime.reattachTimer = null;
256
+ if (this.#tab(key))
257
+ this.#patch(key, { reattached: false });
258
+ }, this.#options.reattachedNoticeMs ?? 3000);
259
+ }
260
+ }
261
+ })
262
+ .then((opened) => ({ kind: 'opened', opened }), (cause) => ({ kind: 'failure', failure: CapabilityError.from(CaughtFailureSchema.parse(cause)) }));
263
+ if (runtime.controller === controller)
264
+ runtime.controller = null;
265
+ if (outcome.kind === 'opened' && !live()) {
266
+ outcome.opened.session.kill();
267
+ return;
268
+ }
269
+ if (!live())
270
+ return;
271
+ if (outcome.kind === 'failure') {
272
+ const describe = this.#options.describeError ?? describeTerminalHostFailure;
273
+ this.#patch(key, { status: 'failed', failure: outcome.failure, error: describe(outcome.failure, tab.hostLabel) });
274
+ return;
275
+ }
276
+ this.#patch(key, { status: 'live', session: outcome.opened.session, sessionId: outcome.opened.id });
277
+ void outcome.opened.session.exited().then((result) => this.exited(key, result.exitCode), (cause) => this.#lost(key, CapabilityError.from(CaughtFailureSchema.parse(cause))));
278
+ }
279
+ /** The session's exit wait failed: the transport is gone and could not reconnect. */
280
+ #lost(key, failure) {
281
+ const tab = this.#tab(key);
282
+ if (!tab || tab.status === 'exited')
283
+ return;
284
+ this.#patch(key, {
285
+ status: 'failed',
286
+ inputReady: false,
287
+ reattached: false,
288
+ failure,
289
+ error: 'Connection lost and could not be restored.'
290
+ });
291
+ }
292
+ #release(key, tab) {
293
+ const runtime = this.#runtime.get(key);
294
+ if (runtime) {
295
+ runtime.controller?.abort();
296
+ runtime.controller = null;
297
+ this.#clearReattached(runtime);
298
+ }
299
+ this.#runtime.delete(key);
300
+ if (tab.session && tab.status !== 'exited')
301
+ tab.session.kill();
302
+ }
303
+ #clearReattached(runtime) {
304
+ if (runtime.reattachTimer !== null) {
305
+ clearTimeout(runtime.reattachTimer);
306
+ runtime.reattachTimer = null;
307
+ }
308
+ }
309
+ #tab(key) {
310
+ return this.#tabs.find((tab) => tab.key === key);
311
+ }
312
+ #patch(key, patch) {
313
+ if (this.#disposed)
314
+ return;
315
+ this.#tabs = this.#tabs.map((tab) => (tab.key === key ? { ...tab, ...patch } : tab));
316
+ this.#emit();
317
+ }
318
+ #build() {
319
+ return {
320
+ tabs: this.#tabs,
321
+ activeKey: this.#activeKey,
322
+ pickerOpen: this.#pickerOpen,
323
+ requestedHostUnavailable: this.#requestedHostUnavailable,
324
+ pickerHostId: this.#pickerHostId,
325
+ pickerCommand: this.#pickerCommand
326
+ };
327
+ }
328
+ #emit() {
329
+ if (this.#disposed)
330
+ return;
331
+ this.#snapshot = this.#build();
332
+ for (const listener of this.#listeners)
333
+ listener(this.#snapshot);
334
+ }
335
+ }
@@ -1,3 +1,5 @@
1
+ /** Settles after every widget registration this module queued so far (none when Kit is absent). */
2
+ export declare function toolWidgetsRegistered(): Promise<void>;
1
3
  /**
2
4
  * Designed Kit card for `tools/tool:terminal`. Queued as `tools/ui` when the
3
5
  * kind is not yet registered so docs-only hosts can still load this plugin.
@@ -1 +1 @@
1
- {"version":3,"file":"terminal-svelte.plugin.d.ts","sourceRoot":"","sources":["../../src/svelte/terminal-svelte.plugin.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;CAU1C,CAAC;AAEF,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD;AAID,eAAO,MAAM,oBAAoB,6CAiB/B,CAAC;AAEH,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"terminal-svelte.plugin.d.ts","sourceRoot":"","sources":["../../src/svelte/terminal-svelte.plugin.ts"],"names":[],"mappings":"AAcA,mGAAmG;AACnG,wBAAgB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAErD;AAED;;;GAGG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;CAU1C,CAAC;AAEF,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD;AAID,eAAO,MAAM,oBAAoB,6CAiB/B,CAAC;AAEH,eAAe,oBAAoB,CAAC"}
@@ -1,8 +1,19 @@
1
1
  import { createRuntimeImplementation, createRuntimePlugin, loader, settle } from '@vibesdotdev/kernel';
2
2
  import terminalSvelteDocsDescriptor from "./docs/terminal-svelte.docs.descriptor.js";
3
3
  const loadTerminalToolCard = () => import('./tool-widgets/TerminalToolCard.svelte');
4
- const kitToolWidgets = await settle(() => import('@vibesdotdev/kit/components/tool-widgets'));
5
- const registerKitToolWidget = kitToolWidgets.ok ? kitToolWidgets.value.registerToolWidget : undefined;
4
+ // Kit's widget registry loads lazily: a top-level await here would suspend every importer and,
5
+ // inside a module cycle, let WebKit run a dependent before this module's bindings exist.
6
+ const kitToolWidgets = settle(() => import('@vibesdotdev/kit/components/tool-widgets'));
7
+ function registerKitToolWidget(...args) {
8
+ void kitToolWidgets.then((kit) => {
9
+ if (kit.ok)
10
+ kit.value.registerToolWidget(...args);
11
+ });
12
+ }
13
+ /** Settles after every widget registration this module queued so far (none when Kit is absent). */
14
+ export function toolWidgetsRegistered() {
15
+ return kitToolWidgets.then(() => undefined);
16
+ }
6
17
  /**
7
18
  * Designed Kit card for `tools/tool:terminal`. Queued as `tools/ui` when the
8
19
  * kind is not yet registered so docs-only hosts can still load this plugin.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@vibesdotdev/terminal-client",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.3",
4
4
  "dependencies": {
5
5
  "@opentui/react": "^0.5.17",
6
6
  "@vibesdotdev/docs-contracts": "^1.0.0-rc.4",
7
- "@vibesdotdev/logging": "^1.0.0-rc.3",
8
- "@vibesdotdev/kernel-rpc": "^1.0.0-rc.2",
9
- "@vibesdotdev/host-bun": "^1.0.0-rc.2",
7
+ "@vibesdotdev/logging": "^1.0.0-rc.5",
8
+ "@vibesdotdev/kernel-rpc": "^1.0.0-rc.4",
9
+ "@vibesdotdev/host-bun": "^1.0.0-rc.3",
10
10
  "@vibesdotdev/tui": "^1.0.0-rc.2",
11
11
  "@xterm/addon-fit": "^0.11.0",
12
12
  "@xterm/xterm": "^6.0.0",
@@ -15,11 +15,11 @@
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@lucide/svelte": "^1.59.0",
18
- "@vibesdotdev/kernel": "^1.0.0-rc.5",
19
- "@vibesdotdev/kit": "^1.0.0-rc.2",
20
- "@vibesdotdev/execution-local": "^1.0.0-rc.3",
18
+ "@vibesdotdev/kernel": "^1.0.0-rc.0",
19
+ "@vibesdotdev/kit": "^1.0.0-rc.0",
20
+ "@vibesdotdev/execution-local": "^1.0.0-rc.0",
21
21
  "svelte": "^5.57.2",
22
- "@vibesdotdev/surface-sveltekit": "^1.0.0-rc.2"
22
+ "@vibesdotdev/surface-sveltekit": "^1.0.0-rc.0"
23
23
  },
24
24
  "optionalDependencies": {
25
25
  "@xterm/addon-webgl": "^0.19.0"
@@ -221,10 +221,10 @@
221
221
  "optional": true
222
222
  }
223
223
  },
224
- "gitHead": "51230ebdad54bc405014633f435b499fa2a06b89",
224
+ "gitHead": "f1ff526c235b10c970497738beb16b0651c428c8",
225
225
  "_vibes_source": {
226
226
  "repo": "ssh://git@git.vibes.dev/vibesdotdev/framework.git",
227
- "sha": "51230ebdad54bc405014633f435b499fa2a06b89",
228
- "workspaceDigest": "sha256:ad943c70c92aa7226c82519c5853f5ea66f36aaa9c0515a093d9bba8c189272a"
227
+ "sha": "f1ff526c235b10c970497738beb16b0651c428c8",
228
+ "workspaceDigest": "sha256:05ad771f6531189784ed3a20b0c2018f7e69e557018ae5a578a8f03b194190cc"
229
229
  }
230
230
  }