@vibesdotdev/terminal-client 0.0.0-stage → 1.0.0-rc.2

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 (195) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +102 -2
  3. package/dist/ansi/index.d.ts +11 -0
  4. package/dist/ansi/index.d.ts.map +1 -0
  5. package/dist/ansi/index.js +11 -0
  6. package/dist/ansi/index.js.map +1 -0
  7. package/dist/ansi/keymap.d.ts +77 -0
  8. package/dist/ansi/keymap.d.ts.map +1 -0
  9. package/dist/ansi/keymap.js +257 -0
  10. package/dist/ansi/keymap.js.map +1 -0
  11. package/dist/api-client-plugin.d.ts +4 -0
  12. package/dist/api-client-plugin.d.ts.map +1 -0
  13. package/dist/api-client-plugin.js +16 -0
  14. package/dist/api-client-plugin.js.map +1 -0
  15. package/dist/client/http-session.d.ts +44 -0
  16. package/dist/client/http-session.d.ts.map +1 -0
  17. package/dist/client/http-session.js +457 -0
  18. package/dist/client/http-session.js.map +1 -0
  19. package/dist/client/index.d.ts +8 -0
  20. package/dist/client/index.d.ts.map +1 -0
  21. package/dist/client/index.js +8 -0
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/client.d.ts +56 -0
  24. package/dist/client.d.ts.map +1 -0
  25. package/dist/client.js +169 -0
  26. package/dist/client.js.map +1 -0
  27. package/dist/index.d.ts +14 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/pty-bridge/index.d.ts +15 -0
  32. package/dist/pty-bridge/index.d.ts.map +1 -0
  33. package/dist/pty-bridge/index.js +15 -0
  34. package/dist/pty-bridge/index.js.map +1 -0
  35. package/dist/pty-bridge/protocol.d.ts +6 -0
  36. package/dist/pty-bridge/protocol.d.ts.map +1 -0
  37. package/dist/pty-bridge/protocol.js +6 -0
  38. package/dist/pty-bridge/protocol.js.map +1 -0
  39. package/dist/pty-bridge/server.d.ts +7 -0
  40. package/dist/pty-bridge/server.d.ts.map +1 -0
  41. package/dist/pty-bridge/server.js +9 -0
  42. package/dist/pty-bridge/server.js.map +1 -0
  43. package/dist/remote-session.d.ts +51 -0
  44. package/dist/remote-session.d.ts.map +1 -0
  45. package/dist/remote-session.js +326 -0
  46. package/dist/remote-session.js.map +1 -0
  47. package/dist/schemas/http.d.ts +61 -0
  48. package/dist/schemas/http.d.ts.map +1 -0
  49. package/dist/schemas/http.js +45 -0
  50. package/dist/schemas/http.js.map +1 -0
  51. package/dist/schemas/remote.d.ts +25 -0
  52. package/dist/schemas/remote.d.ts.map +1 -0
  53. package/dist/schemas/remote.js +23 -0
  54. package/dist/schemas/remote.js.map +1 -0
  55. package/dist/schemas/session.d.ts +26 -0
  56. package/dist/schemas/session.d.ts.map +1 -0
  57. package/dist/schemas/session.js +17 -0
  58. package/dist/schemas/session.js.map +1 -0
  59. package/dist/schemas/store.d.ts +29 -0
  60. package/dist/schemas/store.d.ts.map +1 -0
  61. package/dist/schemas/store.js +22 -0
  62. package/dist/schemas/store.js.map +1 -0
  63. package/dist/schemas/terminal.d.ts +296 -0
  64. package/dist/schemas/terminal.d.ts.map +1 -0
  65. package/dist/schemas/terminal.js +243 -0
  66. package/dist/schemas/terminal.js.map +1 -0
  67. package/dist/server/index.d.ts +9 -0
  68. package/dist/server/index.d.ts.map +1 -0
  69. package/dist/server/index.js +9 -0
  70. package/dist/server/index.js.map +1 -0
  71. package/dist/server/session-store.d.ts +176 -0
  72. package/dist/server/session-store.d.ts.map +1 -0
  73. package/dist/server/session-store.js +462 -0
  74. package/dist/server/session-store.js.map +1 -0
  75. package/dist/server/sse-stream.d.ts +32 -0
  76. package/dist/server/sse-stream.d.ts.map +1 -0
  77. package/dist/server/sse-stream.js +87 -0
  78. package/dist/server/sse-stream.js.map +1 -0
  79. package/dist/svelte/HostPicker.svelte +256 -0
  80. package/dist/svelte/HostPicker.svelte.d.ts +30 -0
  81. package/dist/svelte/HostPicker.svelte.d.ts.map +1 -0
  82. package/dist/svelte/README.md +52 -0
  83. package/dist/svelte/ReconnectBanner.svelte +88 -0
  84. package/dist/svelte/ReconnectBanner.svelte.d.ts +16 -0
  85. package/dist/svelte/ReconnectBanner.svelte.d.ts.map +1 -0
  86. package/dist/svelte/SessionTabs.svelte +192 -0
  87. package/dist/svelte/SessionTabs.svelte.d.ts +17 -0
  88. package/dist/svelte/SessionTabs.svelte.d.ts.map +1 -0
  89. package/dist/svelte/Terminal.svelte +567 -0
  90. package/dist/svelte/Terminal.svelte.d.ts +25 -0
  91. package/dist/svelte/Terminal.svelte.d.ts.map +1 -0
  92. package/dist/svelte/TerminalMobileToolbar.svelte +56 -0
  93. package/dist/svelte/TerminalMobileToolbar.svelte.d.ts +14 -0
  94. package/dist/svelte/TerminalMobileToolbar.svelte.d.ts.map +1 -0
  95. package/dist/svelte/TerminalWorkspace.svelte +562 -0
  96. package/dist/svelte/TerminalWorkspace.svelte.d.ts +25 -0
  97. package/dist/svelte/TerminalWorkspace.svelte.d.ts.map +1 -0
  98. package/dist/svelte/ambient.d.ts +9 -0
  99. package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts +4 -0
  100. package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts.map +1 -0
  101. package/dist/svelte/docs/terminal-svelte.docs.descriptor.js +74 -0
  102. package/dist/svelte/index.d.ts +27 -0
  103. package/dist/svelte/index.d.ts.map +1 -0
  104. package/dist/svelte/index.js +26 -0
  105. package/dist/svelte/styles.css +8 -0
  106. package/dist/svelte/terminal-session-authority.d.ts +17 -0
  107. package/dist/svelte/terminal-session-authority.d.ts.map +1 -0
  108. package/dist/svelte/terminal-session-authority.js +25 -0
  109. package/dist/svelte/terminal-svelte.plugin.d.ts +19 -0
  110. package/dist/svelte/terminal-svelte.plugin.d.ts.map +1 -0
  111. package/dist/svelte/terminal-svelte.plugin.js +42 -0
  112. package/dist/svelte/terminal-theme.d.ts +11 -0
  113. package/dist/svelte/terminal-theme.d.ts.map +1 -0
  114. package/dist/svelte/terminal-theme.js +116 -0
  115. package/dist/svelte/tool-widgets/TerminalToolCard.svelte +65 -0
  116. package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts +5 -0
  117. package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts.map +1 -0
  118. package/dist/svelte/tool-widgets/terminal-tool-card.d.ts +25 -0
  119. package/dist/svelte/tool-widgets/terminal-tool-card.d.ts.map +1 -0
  120. package/dist/svelte/tool-widgets/terminal-tool-card.js +116 -0
  121. package/dist/svelte/types.d.ts +160 -0
  122. package/dist/svelte/types.d.ts.map +1 -0
  123. package/dist/svelte/types.js +51 -0
  124. package/dist/svelte/workspace.d.ts +3 -0
  125. package/dist/svelte/workspace.d.ts.map +1 -0
  126. package/dist/svelte/workspace.js +2 -0
  127. package/dist/terminal-client.plugin.d.ts +3 -0
  128. package/dist/terminal-client.plugin.d.ts.map +1 -0
  129. package/dist/terminal-client.plugin.js +3 -0
  130. package/dist/terminal-client.plugin.js.map +1 -0
  131. package/dist/terminal-runtime.d.ts +30 -0
  132. package/dist/terminal-runtime.d.ts.map +1 -0
  133. package/dist/terminal-runtime.js +673 -0
  134. package/dist/terminal-runtime.js.map +1 -0
  135. package/dist/terminal.consumer.plugin.d.ts +14 -0
  136. package/dist/terminal.consumer.plugin.d.ts.map +1 -0
  137. package/dist/terminal.consumer.plugin.js +31 -0
  138. package/dist/terminal.consumer.plugin.js.map +1 -0
  139. package/dist/tools/terminal.impl.consumer.d.ts +11 -0
  140. package/dist/tools/terminal.impl.consumer.d.ts.map +1 -0
  141. package/dist/tools/terminal.impl.consumer.js +20 -0
  142. package/dist/tools/terminal.impl.consumer.js.map +1 -0
  143. package/dist/tools/terminal.schemas.d.ts +3 -0
  144. package/dist/tools/terminal.schemas.d.ts.map +1 -0
  145. package/dist/tools/terminal.schemas.js +3 -0
  146. package/dist/tools/terminal.schemas.js.map +1 -0
  147. package/dist/tools/terminal.unified.descriptor.d.ts +218 -0
  148. package/dist/tools/terminal.unified.descriptor.d.ts.map +1 -0
  149. package/dist/tools/terminal.unified.descriptor.js +36 -0
  150. package/dist/tools/terminal.unified.descriptor.js.map +1 -0
  151. package/dist/tui/index.d.ts +9 -0
  152. package/dist/tui/index.d.ts.map +1 -0
  153. package/dist/tui/index.js +8 -0
  154. package/dist/tui/index.js.map +1 -0
  155. package/dist/tui/terminal-open.descriptor.d.ts +9 -0
  156. package/dist/tui/terminal-open.descriptor.d.ts.map +1 -0
  157. package/dist/tui/terminal-open.descriptor.js +18 -0
  158. package/dist/tui/terminal-open.descriptor.js.map +1 -0
  159. package/dist/tui/terminal-open.impl.consumer.d.ts +9 -0
  160. package/dist/tui/terminal-open.impl.consumer.d.ts.map +1 -0
  161. package/dist/tui/terminal-open.impl.consumer.js +13 -0
  162. package/dist/tui/terminal-open.impl.consumer.js.map +1 -0
  163. package/dist/tui/terminal-registry.descriptor.d.ts +11 -0
  164. package/dist/tui/terminal-registry.descriptor.d.ts.map +1 -0
  165. package/dist/tui/terminal-registry.descriptor.js +17 -0
  166. package/dist/tui/terminal-registry.descriptor.js.map +1 -0
  167. package/dist/tui/terminal-registry.impl.consumer.d.ts +71 -0
  168. package/dist/tui/terminal-registry.impl.consumer.d.ts.map +1 -0
  169. package/dist/tui/terminal-registry.impl.consumer.js +190 -0
  170. package/dist/tui/terminal-registry.impl.consumer.js.map +1 -0
  171. package/dist/tui/terminal-tui.plugin.d.ts +10 -0
  172. package/dist/tui/terminal-tui.plugin.d.ts.map +1 -0
  173. package/dist/tui/terminal-tui.plugin.js +32 -0
  174. package/dist/tui/terminal-tui.plugin.js.map +1 -0
  175. package/dist/tui/terminal.descriptor.d.ts +10 -0
  176. package/dist/tui/terminal.descriptor.d.ts.map +1 -0
  177. package/dist/tui/terminal.descriptor.js +25 -0
  178. package/dist/tui/terminal.descriptor.js.map +1 -0
  179. package/dist/tui/terminal.impl.consumer.d.ts +14 -0
  180. package/dist/tui/terminal.impl.consumer.d.ts.map +1 -0
  181. package/dist/tui/terminal.impl.consumer.js +189 -0
  182. package/dist/tui/terminal.impl.consumer.js.map +1 -0
  183. package/dist/tui/terminal.state-key.d.ts +30 -0
  184. package/dist/tui/terminal.state-key.d.ts.map +1 -0
  185. package/dist/tui/terminal.state-key.js +9 -0
  186. package/dist/tui/terminal.state-key.js.map +1 -0
  187. package/dist/tui/terminal.state.d.ts +152 -0
  188. package/dist/tui/terminal.state.d.ts.map +1 -0
  189. package/dist/tui/terminal.state.js +149 -0
  190. package/dist/tui/terminal.state.js.map +1 -0
  191. package/dist/workspace.d.ts +155 -0
  192. package/dist/workspace.d.ts.map +1 -0
  193. package/dist/workspace.js +292 -0
  194. package/dist/workspace.js.map +1 -0
  195. package/package.json +228 -4
@@ -0,0 +1,562 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import { CapabilityError, CaughtFailureSchema } from '@vibesdotdev/kernel';
4
+ import { getRuntimeSvelteContext } from '@vibesdotdev/surface-sveltekit/svelte';
5
+ import { Button } from '@vibesdotdev/kit/components/ui/button';
6
+ import * as Select from '@vibesdotdev/kit/components/ui/select';
7
+ import * as AlertDialog from '@vibesdotdev/kit/components/ui/alert-dialog';
8
+ import {
9
+ CircleHelp,
10
+ Eraser,
11
+ Info,
12
+ OctagonX,
13
+ PanelsTopLeft,
14
+ Play,
15
+ RotateCw,
16
+ TriangleAlert
17
+ } from '@lucide/svelte';
18
+ import type {
19
+ ExecutionCapabilities,
20
+ ExecutionSession,
21
+ ProcessHandle
22
+ } from '@vibesdotdev/kernel/execution';
23
+ import Terminal from './Terminal.svelte';
24
+ import {
25
+ openTerminalWorkspaceTargetSession,
26
+ terminalWorkspaceCapabilityBanner,
27
+ terminalWorkspacePlaceDefaultCapabilities,
28
+ terminalWorkspaceQuickActions,
29
+ terminalWorkspaceConnectionNotice,
30
+ terminalWorkspaceDefaultCommand,
31
+ terminalWorkspaceError,
32
+ terminalWorkspaceExitedEvent,
33
+ terminalWorkspaceLaunchSignature,
34
+ terminalWorkspacePlaceLabel,
35
+ terminalWorkspaceStatusLabel,
36
+ type TerminalWorkspaceLifecycleEvent,
37
+ type TerminalWorkspaceOpenedReceipt,
38
+ type TerminalWorkspaceQuickActionId,
39
+ type TerminalWorkspaceRuntimePlaceOption,
40
+ type TerminalWorkspaceStatus
41
+ } from '../workspace.js';
42
+ import type { TerminalTheme } from './types.js';
43
+
44
+ interface Props {
45
+ /** Trusted host transport; session lifecycle and rendering remain owner-managed. */
46
+ sessionOpener?: typeof openTerminalWorkspaceTargetSession;
47
+ executionId?: string;
48
+ runtimePlaceId?: string;
49
+ command?: string;
50
+ args?: string[];
51
+ cwd?: string;
52
+ title?: string;
53
+ class?: string;
54
+ compact?: boolean;
55
+ theme?: TerminalTheme;
56
+ /** Host-supplied, executable runtime places plus honest unavailable inventory. */
57
+ loadRuntimePlaces?: () => Promise<readonly TerminalWorkspaceRuntimePlaceOption[]>;
58
+ /** Opens a separate Terminal window; the current session never migrates silently. */
59
+ onOpenRuntimePlace?: (place: TerminalWorkspaceRuntimePlaceOption) => void;
60
+ /** Receives real execution outcomes; `opened` is emitted only after `session()` resolves. */
61
+ onLifecycle?: (event: TerminalWorkspaceLifecycleEvent) => void;
62
+ }
63
+
64
+ let {
65
+ sessionOpener = openTerminalWorkspaceTargetSession,
66
+ executionId = 'browser-shell',
67
+ runtimePlaceId = 'browser:this-profile',
68
+ command,
69
+ args = [],
70
+ cwd = '/',
71
+ title = 'Terminal',
72
+ class: className = '',
73
+ compact = false,
74
+ theme,
75
+ loadRuntimePlaces,
76
+ onOpenRuntimePlace,
77
+ onLifecycle
78
+ }: Props = $props();
79
+
80
+ const runtimeStore = getRuntimeSvelteContext();
81
+
82
+ let status = $state<TerminalWorkspaceStatus>('idle');
83
+ let notice = $state('Opening terminal...');
84
+ let session = $state<ExecutionSession | null>(null);
85
+ let lastDimensions = $state({ cols: 80, rows: 24 });
86
+ let inputReady = $state(false);
87
+ let replaceSessionOpen = $state(false);
88
+ function requestNewSession(): void {
89
+ if (status === 'live') replaceSessionOpen = true;
90
+ else void openSession();
91
+ }
92
+ let activeCwd = $state('/');
93
+ let activeLaunchSignature = $state('');
94
+ let openedReceipt = $state<TerminalWorkspaceOpenedReceipt | null>(null);
95
+ let terminalReady = $state(false);
96
+ let openGeneration = 0;
97
+ let placeLoadGeneration = 0;
98
+ let runtimePlaces = $state<TerminalWorkspaceRuntimePlaceOption[]>([]);
99
+ let runtimePlacesLoading = $state(false);
100
+ let runtimePlacesError = $state<string | null>(null);
101
+ /** Live capability mask from the opened place; overrides the pre-open expectation. */
102
+ let openedCapabilities = $state<ExecutionCapabilities | null>(null);
103
+ /** Accumulated exec-mode output, appended live as stdout/stderr bytes arrive. */
104
+ let execResultText = $state<string | null>(null);
105
+ /** The live exec-mode process; killed on re-open/destroy. */
106
+ let execProcess = $state<ProcessHandle | null>(null);
107
+
108
+ const statusLabel = $derived(terminalWorkspaceStatusLabel(status));
109
+ const placeLabel = $derived(terminalWorkspacePlaceLabel(runtimePlaceId));
110
+ const currentRuntimePlace = $derived(
111
+ runtimePlaces.find((place) => place.id === runtimePlaceId) ?? {
112
+ id: runtimePlaceId,
113
+ label: placeLabel,
114
+ status: 'available' as const,
115
+ executionId
116
+ }
117
+ );
118
+ const encoder = new TextEncoder();
119
+ const quickActionIcons = { help: CircleHelp, clear: Eraser, interrupt: OctagonX, end: Play };
120
+ // Honest capability gate: the live mask if a place has opened, else the place's
121
+ // advertised mask, else the pre-resolution expectation derived from the id.
122
+ const caps = $derived(
123
+ openedCapabilities ??
124
+ currentRuntimePlace.capabilities ??
125
+ terminalWorkspacePlaceDefaultCapabilities(runtimePlaceId)
126
+ );
127
+ const isExecMode = $derived(caps.pty === false);
128
+ const capabilityBanner = $derived(terminalWorkspaceCapabilityBanner(caps, runtimePlaceId));
129
+
130
+ function fallbackRuntimePlace(): TerminalWorkspaceRuntimePlaceOption {
131
+ return {
132
+ id: runtimePlaceId,
133
+ label: terminalWorkspacePlaceLabel(runtimePlaceId),
134
+ status: 'available',
135
+ executionId
136
+ };
137
+ }
138
+
139
+ onDestroy(() => {
140
+ openGeneration += 1;
141
+ placeLoadGeneration += 1;
142
+ session?.kill();
143
+ session = null;
144
+ execProcess?.kill();
145
+ execProcess = null;
146
+ openedReceipt = null;
147
+ inputReady = false;
148
+ terminalReady = false;
149
+ });
150
+
151
+ $effect(() => {
152
+ const loader = loadRuntimePlaces;
153
+ void runtimePlaceId;
154
+ void executionId;
155
+ const generation = ++placeLoadGeneration;
156
+ if (!loader) {
157
+ runtimePlaces = [fallbackRuntimePlace()];
158
+ runtimePlacesLoading = false;
159
+ runtimePlacesError = null;
160
+ return;
161
+ }
162
+ runtimePlacesLoading = true;
163
+ runtimePlacesError = null;
164
+ void loader()
165
+ .then(
166
+ (places) => ({ kind: 'loaded' as const, places }),
167
+ (cause) => ({
168
+ kind: 'failure' as const,
169
+ failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
170
+ })
171
+ )
172
+ .then((outcome) => {
173
+ if (generation !== placeLoadGeneration) return;
174
+ if (outcome.kind === 'failure') {
175
+ runtimePlaces = [fallbackRuntimePlace()];
176
+ runtimePlacesError = outcome.failure.message;
177
+ return;
178
+ }
179
+ const unique = new Map(outcome.places.map((place) => [place.id, place]));
180
+ if (!unique.has(runtimePlaceId)) unique.set(runtimePlaceId, fallbackRuntimePlace());
181
+ runtimePlaces = [...unique.values()];
182
+ })
183
+ .finally(() => {
184
+ if (generation === placeLoadGeneration) runtimePlacesLoading = false;
185
+ });
186
+ });
187
+
188
+ function openRuntimePlace(place: TerminalWorkspaceRuntimePlaceOption): void {
189
+ if (place.status !== 'available') return;
190
+ onOpenRuntimePlace?.(place);
191
+ }
192
+
193
+ function handleRuntimePlaceSelection(value: string | undefined): void {
194
+ if (!value || value === runtimePlaceId) return;
195
+ const place = runtimePlaces.find((candidate) => candidate.id === value);
196
+ if (place) openRuntimePlace(place);
197
+ }
198
+
199
+ function handleReady(dimensions: { cols: number; rows: number }): void {
200
+ lastDimensions = dimensions;
201
+ terminalReady = true;
202
+ if (!session && status === 'idle') {
203
+ void openSession(dimensions);
204
+ }
205
+ }
206
+
207
+ async function openSession(dimensions = lastDimensions): Promise<void> {
208
+ const generation = ++openGeneration;
209
+ const sessionExecutionId = executionId;
210
+ const sessionCommand = command ?? terminalWorkspaceDefaultCommand(runtimePlaceId);
211
+ const sessionArgs = [...args];
212
+ const sessionCwd = cwd;
213
+ activeLaunchSignature = terminalWorkspaceLaunchSignature({
214
+ runtimePlaceId,
215
+ executionId: sessionExecutionId,
216
+ command: sessionCommand,
217
+ args: sessionArgs,
218
+ cwd: sessionCwd
219
+ });
220
+ let lifecycleEvent: TerminalWorkspaceOpenedReceipt | null = null;
221
+ session?.kill();
222
+ session = null;
223
+ execProcess?.kill();
224
+ execProcess = null;
225
+ openedReceipt = null;
226
+ openedCapabilities = null;
227
+ execResultText = null;
228
+ inputReady = false;
229
+ status = 'opening';
230
+ notice = isExecMode ? 'Preparing cloud exec…' : 'Opening terminal...';
231
+ const opening = runtimeStore.load().then((runtime) => sessionOpener({
232
+ runtime,
233
+ executionId: sessionExecutionId,
234
+ command: sessionCommand,
235
+ args: sessionArgs,
236
+ runtimePlaceId,
237
+ cwd: sessionCwd,
238
+ cols: dimensions.cols,
239
+ rows: dimensions.rows
240
+ }));
241
+ const outcome = await opening.then(
242
+ (opened) => ({ kind: 'opened' as const, opened }),
243
+ (cause) => ({
244
+ kind: 'failure' as const,
245
+ failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
246
+ })
247
+ );
248
+ if (outcome.kind === 'failure') {
249
+ if (generation !== openGeneration) return;
250
+ if (isExecMode) {
251
+ status = 'idle';
252
+ notice = 'Cloud exec — connect a cloud runtime to run commands.';
253
+ } else {
254
+ status = 'error';
255
+ notice = terminalWorkspaceError(outcome.failure, runtimePlaceId);
256
+ }
257
+ return;
258
+ }
259
+ const { opened } = outcome;
260
+ if (generation !== openGeneration) {
261
+ opened.session?.kill();
262
+ return;
263
+ }
264
+ openedCapabilities = opened.capabilities;
265
+ openedReceipt = opened.receipt;
266
+ activeCwd = sessionCwd;
267
+ if (opened.mode === 'exec' || !opened.session) {
268
+ // Honest non-PTY path: NO interactive PTY, NO faked keystroke stream.
269
+ // But output STREAMS live — drain the ProcessHandle into the panel as
270
+ // bytes arrive, then settle when the process exits.
271
+ session = null;
272
+ execProcess = opened.execProcess ?? null;
273
+ execResultText = '';
274
+ status = 'live';
275
+ notice = 'Cloud exec — streaming output…';
276
+ if (opened.execProcess) {
277
+ void streamExecProcess(opened.execProcess, generation, opened.receipt);
278
+ }
279
+ } else {
280
+ session = opened.session;
281
+ status = 'live';
282
+ notice = terminalWorkspaceConnectionNotice(sessionCwd, runtimePlaceId);
283
+ }
284
+ lifecycleEvent = opened.receipt;
285
+ if (lifecycleEvent) onLifecycle?.(lifecycleEvent);
286
+ }
287
+
288
+ function handleDisconnect(reason: string): void {
289
+ status = 'error';
290
+ inputReady = false;
291
+ notice = reason || 'Connection lost. Start a new session to reconnect.';
292
+ }
293
+
294
+ function handleExit(result: { exitCode: number | null }): void {
295
+ const receipt = openedReceipt;
296
+ status = 'exited';
297
+ notice = 'Session ended.';
298
+ inputReady = false;
299
+ if (receipt) onLifecycle?.(terminalWorkspaceExitedEvent(receipt, result));
300
+ }
301
+
302
+ /**
303
+ * Drain a live exec-mode ProcessHandle into the panel. Reads stdout+stderr
304
+ * concurrently, appending decoded text as bytes arrive (chunk 1 shows before
305
+ * exit), then settles status + emits the exited lifecycle event on exit. A
306
+ * stale generation (re-open/destroy) aborts the drain and kills the process.
307
+ */
308
+ async function streamExecProcess(
309
+ handle: ProcessHandle,
310
+ generation: number,
311
+ receipt: TerminalWorkspaceOpenedReceipt
312
+ ): Promise<void> {
313
+ const pump = async (stream: ReadableStream<Uint8Array> | null): Promise<void> => {
314
+ if (!stream) return;
315
+ const reader = stream.getReader();
316
+ const decoder = new TextDecoder();
317
+ try {
318
+ for (;;) {
319
+ const { done, value } = await reader.read();
320
+ if (done) break;
321
+ if (generation !== openGeneration) return;
322
+ execResultText = (execResultText ?? '') + decoder.decode(value, { stream: true });
323
+ }
324
+ const tail = decoder.decode();
325
+ if (tail && generation === openGeneration) execResultText = (execResultText ?? '') + tail;
326
+ } finally {
327
+ reader.releaseLock();
328
+ }
329
+ };
330
+ const outcome = await Promise.all([
331
+ pump(handle.stdout),
332
+ pump(handle.stderr),
333
+ handle.exited
334
+ ])
335
+ .then(
336
+ ([, , exit]) => ({ kind: 'exit' as const, exit }),
337
+ (cause) => ({
338
+ kind: 'failure' as const,
339
+ failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
340
+ })
341
+ );
342
+ if (generation !== openGeneration) return;
343
+ if (outcome.kind === 'failure') {
344
+ status = 'error';
345
+ notice = terminalWorkspaceError(outcome.failure, runtimePlaceId);
346
+ return;
347
+ }
348
+ if ((execResultText ?? '').length === 0) execResultText = '(command produced no output)';
349
+ status = 'exited';
350
+ const { exit } = outcome;
351
+ notice = exit.code === 0 || exit.code === null ? 'Command finished.' : `Command exited (${exit.code}).`;
352
+ onLifecycle?.(terminalWorkspaceExitedEvent(receipt, { exitCode: exit.code }));
353
+ }
354
+
355
+ function handleInputReady(): void {
356
+ inputReady = true;
357
+ }
358
+
359
+ $effect(() => {
360
+ const requestedLaunchSignature = terminalWorkspaceLaunchSignature({
361
+ runtimePlaceId,
362
+ executionId,
363
+ command: command ?? terminalWorkspaceDefaultCommand(runtimePlaceId),
364
+ args,
365
+ cwd
366
+ });
367
+ if (requestedLaunchSignature === activeLaunchSignature) return;
368
+ // Exec-mode places have no interactive terminal to become ready, so open as
369
+ // soon as the place resolves; PTY places still wait for onReady dimensions.
370
+ if (isExecMode) {
371
+ void openSession();
372
+ return;
373
+ }
374
+ if (!terminalReady) return;
375
+ void openSession();
376
+ });
377
+
378
+ function runQuickAction(action: {
379
+ id: TerminalWorkspaceQuickActionId;
380
+ command?: string;
381
+ signal?: string;
382
+ }): void {
383
+ if (!session || status !== 'live' || !inputReady) return;
384
+ if (action.signal) {
385
+ // PTY input delivers SIGINT to the foreground process group. Signaling
386
+ // only the shell PID can leave its current command running.
387
+ if (action.signal === 'SIGINT' && caps.pty) session.write(new Uint8Array([3]));
388
+ else session.signal(action.signal);
389
+ notice = 'Interrupt requested.';
390
+ return;
391
+ }
392
+ if (action.command) {
393
+ session.write(encoder.encode(`${action.command}\r`));
394
+ if (action.id === 'help') notice = 'Showing available commands.';
395
+ if (action.id === 'clear') notice = 'Cleared terminal output.';
396
+ if (action.id === 'end') notice = 'Ending session...';
397
+ }
398
+ }
399
+ </script>
400
+
401
+ <section
402
+ class={`grid h-full min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] ${compact ? 'gap-1' : 'gap-3'} overflow-hidden ${className}`}
403
+ aria-label={title}
404
+ data-terminal-workspace
405
+ data-terminal-status={status}
406
+ data-terminal-active-cwd={activeCwd}
407
+ data-terminal-runtime-place={runtimePlaceId}
408
+ >
409
+ <header class={`flex flex-wrap items-center justify-between ${compact ? 'gap-1 border-b px-3 py-1' : 'gap-3'}`}>
410
+ <div class="min-w-0">
411
+ <div class="flex items-center gap-2">
412
+ <h2 class="truncate text-lg font-semibold">{title}</h2>
413
+ <span
414
+ class="rounded-full border border-border bg-background/70 px-2 py-0.5 text-xs font-medium"
415
+ title={runtimePlaceId}
416
+ data-runtime-place-id={runtimePlaceId}
417
+ >
418
+ {placeLabel}
419
+ </span>
420
+ <span class="rounded-full border border-border bg-muted px-2 py-0.5 text-xs font-medium">
421
+ {statusLabel}
422
+ </span>
423
+ </div>
424
+ <p class={compact && status === 'live' ? 'sr-only' : 'mt-1 truncate text-sm text-muted-foreground'}>{notice}</p>
425
+ </div>
426
+ <div class="terminal-workspace-actions flex flex-wrap items-center justify-end gap-2">
427
+ {#if onOpenRuntimePlace}
428
+ <Select.Root type="single" value={runtimePlaceId} onValueChange={handleRuntimePlaceSelection}>
429
+ <Select.Trigger
430
+ class="h-8 max-w-40 min-w-0 rounded-full"
431
+ aria-label="Open another terminal on a runtime place"
432
+ >
433
+ {runtimePlacesLoading ? 'Finding machines…' : currentRuntimePlace.label}
434
+ </Select.Trigger>
435
+ <Select.Content>
436
+ {#each runtimePlaces as place (place.id)}
437
+ <Select.Item
438
+ value={place.id}
439
+ label={place.label}
440
+ disabled={place.status !== 'available'}
441
+ >
442
+ <span class="flex min-w-0 flex-col">
443
+ <span class="truncate">{place.label}</span>
444
+ {#if place.detail}
445
+ <span class="truncate text-xs text-muted-foreground">{place.detail}</span>
446
+ {/if}
447
+ </span>
448
+ </Select.Item>
449
+ {/each}
450
+ </Select.Content>
451
+ </Select.Root>
452
+ <Button
453
+ variant="outline"
454
+ size="sm"
455
+ class="gap-2 rounded-full"
456
+ aria-label={`Open another terminal on ${currentRuntimePlace.label}`}
457
+ disabled={currentRuntimePlace.status !== 'available'}
458
+ onclick={() => openRuntimePlace(currentRuntimePlace)}
459
+ >
460
+ <PanelsTopLeft class="size-3.5" />
461
+ New window
462
+ </Button>
463
+ {/if}
464
+ {#each terminalWorkspaceQuickActions as action (action.id)}
465
+ {@const Icon = quickActionIcons[action.id]}
466
+ <Button
467
+ variant="outline"
468
+ size="sm"
469
+ class="gap-2 rounded-full"
470
+ disabled={status !== 'live' || !inputReady || isExecMode}
471
+ aria-label={action.label}
472
+ title={action.description}
473
+ onclick={() => runQuickAction(action)}
474
+ >
475
+ <Icon class="size-3.5" />
476
+ {#if !compact}{action.label}{/if}
477
+ </Button>
478
+ {/each}
479
+ <Button
480
+ variant="outline"
481
+ size="sm"
482
+ class="gap-2 rounded-full"
483
+ disabled={status === 'opening'}
484
+ onclick={requestNewSession}
485
+ >
486
+ <RotateCw class="size-3.5" />
487
+ New session
488
+ </Button>
489
+ </div>
490
+ </header>
491
+ {#if runtimePlacesError}
492
+ <p class="-mt-2 text-xs text-destructive" role="status">{runtimePlacesError}</p>
493
+ {/if}
494
+
495
+ {#if capabilityBanner}
496
+ <div
497
+ class="flex items-start gap-2 rounded-lg border border-border bg-muted px-3 py-2 text-sm text-foreground"
498
+ role="status"
499
+ data-capability-banner
500
+ data-terminal-exec-mode={isExecMode ? 'true' : 'false'}
501
+ >
502
+ {#if isExecMode}
503
+ <TriangleAlert class="mt-0.5 size-4 shrink-0" />
504
+ {:else}
505
+ <Info class="mt-0.5 size-4 shrink-0" />
506
+ {/if}
507
+ <span>{capabilityBanner}</span>
508
+ </div>
509
+ {/if}
510
+
511
+ {#if isExecMode}
512
+ <div
513
+ class="grid min-h-64 place-items-center rounded-xl border border-dashed border-border bg-muted/30 p-6 text-center"
514
+ data-terminal-exec-panel
515
+ >
516
+ <div class="max-w-md">
517
+ <h3 class="text-base font-semibold">Exec session — live output, no keystrokes</h3>
518
+ <p class="mt-2 text-sm leading-6 text-muted-foreground">{notice}</p>
519
+ {#if execResultText}
520
+ <pre
521
+ class="mt-4 max-h-64 overflow-auto rounded-lg bg-background/70 p-3 text-left text-xs">{execResultText}</pre>
522
+ {/if}
523
+ </div>
524
+ </div>
525
+ {:else if status === 'error'}
526
+ <div
527
+ class="grid min-h-64 place-items-center rounded-xl border border-dashed border-border bg-muted/30 p-6 text-center"
528
+ >
529
+ <div>
530
+ <h3 class="text-base font-semibold">Terminal is not available</h3>
531
+ <p class="mt-2 max-w-sm text-sm leading-6 text-muted-foreground">{notice}</p>
532
+ <Button class="mt-4 rounded-full" variant="outline" onclick={() => void openSession()}>
533
+ Try again
534
+ </Button>
535
+ </div>
536
+ </div>
537
+ {:else}
538
+ <Terminal
539
+ {session}
540
+ {theme}
541
+ onReady={handleReady}
542
+ onInputReady={handleInputReady}
543
+ onExit={handleExit}
544
+ onDisconnect={handleDisconnect}
545
+ onNewSession={requestNewSession}
546
+ class="min-h-0"
547
+ />
548
+ {/if}
549
+ </section>
550
+
551
+ <AlertDialog.Root bind:open={replaceSessionOpen}>
552
+ <AlertDialog.Content>
553
+ <AlertDialog.Header>
554
+ <AlertDialog.Title>Replace this terminal session?</AlertDialog.Title>
555
+ <AlertDialog.Description>This ends the current shell and its running commands. Use another terminal tab if you want to keep them running.</AlertDialog.Description>
556
+ </AlertDialog.Header>
557
+ <AlertDialog.Footer>
558
+ <AlertDialog.Cancel>Keep session</AlertDialog.Cancel>
559
+ <AlertDialog.Action onclick={() => void openSession()}>Replace session</AlertDialog.Action>
560
+ </AlertDialog.Footer>
561
+ </AlertDialog.Content>
562
+ </AlertDialog.Root>
@@ -0,0 +1,25 @@
1
+ import { openTerminalWorkspaceTargetSession, type TerminalWorkspaceLifecycleEvent, type TerminalWorkspaceRuntimePlaceOption } from '../workspace.ts';
2
+ import type { TerminalTheme } from './types.ts';
3
+ interface Props {
4
+ /** Trusted host transport; session lifecycle and rendering remain owner-managed. */
5
+ sessionOpener?: typeof openTerminalWorkspaceTargetSession;
6
+ executionId?: string;
7
+ runtimePlaceId?: string;
8
+ command?: string;
9
+ args?: string[];
10
+ cwd?: string;
11
+ title?: string;
12
+ class?: string;
13
+ compact?: boolean;
14
+ theme?: TerminalTheme;
15
+ /** Host-supplied, executable runtime places plus honest unavailable inventory. */
16
+ loadRuntimePlaces?: () => Promise<readonly TerminalWorkspaceRuntimePlaceOption[]>;
17
+ /** Opens a separate Terminal window; the current session never migrates silently. */
18
+ onOpenRuntimePlace?: (place: TerminalWorkspaceRuntimePlaceOption) => void;
19
+ /** Receives real execution outcomes; `opened` is emitted only after `session()` resolves. */
20
+ onLifecycle?: (event: TerminalWorkspaceLifecycleEvent) => void;
21
+ }
22
+ declare const TerminalWorkspace: import("svelte").Component<Props, {}, "">;
23
+ type TerminalWorkspace = ReturnType<typeof TerminalWorkspace>;
24
+ export default TerminalWorkspace;
25
+ //# sourceMappingURL=TerminalWorkspace.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TerminalWorkspace.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/TerminalWorkspace.svelte.ts"],"names":[],"mappings":"AAyBA,OAAO,EACL,kCAAkC,EAWlC,KAAK,+BAA+B,EAGpC,KAAK,mCAAmC,EAExC,MAAM,iBAAiB,CAAC;AAC1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG/C,UAAU,KAAK;IACd,oFAAoF;IACpF,aAAa,CAAC,EAAE,OAAO,kCAAkC,CAAC;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,mCAAmC,EAAE,CAAC,CAAC;IAClF,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mCAAmC,KAAK,IAAI,CAAC;IAC1E,6FAA6F;IAC7F,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,IAAI,CAAC;CAC/D;AA0cF,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Ambient module declarations for terminal-svelte.
3
+ *
4
+ * The component dynamically imports xterm's stylesheet (`@xterm/xterm/css/
5
+ * xterm.css`) inside `onMount` so the browser bundler injects the styles. CSS
6
+ * modules carry no TypeScript types, so declare them as side-effecting imports.
7
+ */
8
+ declare module '*.css';
9
+ declare module '@xterm/xterm/css/xterm.css';
@@ -0,0 +1,4 @@
1
+ import type { DocsTopicDescriptor } from '@vibesdotdev/docs-contracts';
2
+ declare const descriptor: DocsTopicDescriptor;
3
+ export default descriptor;
4
+ //# sourceMappingURL=terminal-svelte.docs.descriptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-svelte.docs.descriptor.d.ts","sourceRoot":"","sources":["../../../src/svelte/docs/terminal-svelte.docs.descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAEvE,QAAA,MAAM,UAAU,EAAE,mBAwEjB,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -0,0 +1,74 @@
1
+ const descriptor = {
2
+ kind: 'docs/topic',
3
+ id: 'terminal-svelte',
4
+ parent: 'runtime',
5
+ order: 44,
6
+ title: 'Terminal Svelte',
7
+ summary: 'Svelte terminal surfaces over runtime-resolved interactive execution sessions.',
8
+ tags: ['terminal', 'svelte', 'execution', 'runtime', 'vessel'],
9
+ surfaces: ['web', 'in-app'],
10
+ hardware: ['consumer', 'cloud'],
11
+ enabled: true,
12
+ body: {
13
+ type: 'markdown',
14
+ sourceType: 'raw',
15
+ source: `---
16
+ title: Terminal Svelte
17
+ summary: Svelte terminal surfaces over runtime-resolved interactive execution sessions
18
+ tags: [terminal, svelte, execution, runtime, vessel]
19
+ parent: runtime
20
+ order: 44
21
+ surfaces: [web, in-app]
22
+ hardware: [consumer, cloud]
23
+ ---
24
+
25
+ # Terminal Svelte
26
+
27
+ \`@vibesdotdev/terminal-client/svelte\` presents interactive terminal sessions without
28
+ owning execution. \`TerminalWorkspace\` resolves \`runtime/execution\`, awaits a
29
+ real \`ExecutionSession\`, and then mounts the xterm renderer over that session's
30
+ byte stream.
31
+
32
+ ## Entrypoints
33
+
34
+ - \`@vibesdotdev/terminal-client/svelte/Terminal.svelte\` - xterm renderer for an
35
+ injected \`ExecutionSession\`.
36
+ - \`@vibesdotdev/terminal-client/svelte/TerminalWorkspace.svelte\` - runtime-backed
37
+ terminal workspace and lifecycle boundary.
38
+ - \`@vibesdotdev/terminal-client/workspace\` - rune-free lifecycle helpers and
39
+ receipt types.
40
+ - \`@vibesdotdev/terminal-client/svelte/plugin\` - docs descriptor registration and
41
+ the designed \`terminal\` Kit ToolCard (\`tools/ui\`, web).
42
+ - \`@vibesdotdev/vessel/apps/terminal.vessel.plugin\` - Vessel-owned
43
+ \`terminal.workspace\` registration and browser loader for this owner component.
44
+
45
+ ## Terminal tool card
46
+
47
+ The Svelte plugin registers a designed Kit \`ToolCard\` for \`tools/tool:terminal\`.
48
+ The card shows cwd, command/session, and a bounded output excerpt through Kit
49
+ \`CodeViewer\`. First-party terminal calls do not fall through to JSON
50
+ \`DefaultToolCard\`.
51
+
52
+ ## Lifecycle boundary
53
+
54
+ \`phase: 'opened'\` is emitted only after \`runtime/execution.session()\`
55
+ resolves. Its receipt carries the resolved session, environment, and execution
56
+ ids. A request to open Terminal is intent; this execution-backed receipt is the
57
+ success signal.
58
+
59
+ ## Vessel capability
60
+
61
+ The owner-declared Vessel surface requires \`runtime/execution\`. That metadata
62
+ lets a host explain why Terminal is unavailable before it attempts to compose
63
+ the surface, while the owner plugin keeps its Svelte module private behind a
64
+ browser-scoped loader.
65
+
66
+ ## Boundary
67
+
68
+ This package does not allocate PTYs, choose execution environments, manage
69
+ filesystem adapters, or persist Vessel return points. Those capabilities remain
70
+ with their runtime and feature owners.
71
+ `
72
+ }
73
+ };
74
+ export default descriptor;