@vibesdotdev/terminal-client 0.0.0-stage → 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.
- package/LICENSE +18 -0
- package/README.md +102 -2
- package/dist/ansi/index.d.ts +11 -0
- package/dist/ansi/index.d.ts.map +1 -0
- package/dist/ansi/index.js +11 -0
- package/dist/ansi/index.js.map +1 -0
- package/dist/ansi/keymap.d.ts +77 -0
- package/dist/ansi/keymap.d.ts.map +1 -0
- package/dist/ansi/keymap.js +257 -0
- package/dist/ansi/keymap.js.map +1 -0
- package/dist/api-client-plugin.d.ts +4 -0
- package/dist/api-client-plugin.d.ts.map +1 -0
- package/dist/api-client-plugin.js +16 -0
- package/dist/api-client-plugin.js.map +1 -0
- package/dist/client/http-session.d.ts +44 -0
- package/dist/client/http-session.d.ts.map +1 -0
- package/dist/client/http-session.js +457 -0
- package/dist/client/http-session.js.map +1 -0
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +169 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/pty-bridge/index.d.ts +15 -0
- package/dist/pty-bridge/index.d.ts.map +1 -0
- package/dist/pty-bridge/index.js +15 -0
- package/dist/pty-bridge/index.js.map +1 -0
- package/dist/pty-bridge/protocol.d.ts +6 -0
- package/dist/pty-bridge/protocol.d.ts.map +1 -0
- package/dist/pty-bridge/protocol.js +6 -0
- package/dist/pty-bridge/protocol.js.map +1 -0
- package/dist/pty-bridge/server.d.ts +7 -0
- package/dist/pty-bridge/server.d.ts.map +1 -0
- package/dist/pty-bridge/server.js +9 -0
- package/dist/pty-bridge/server.js.map +1 -0
- package/dist/remote-session.d.ts +51 -0
- package/dist/remote-session.d.ts.map +1 -0
- package/dist/remote-session.js +326 -0
- package/dist/remote-session.js.map +1 -0
- package/dist/schemas/http.d.ts +61 -0
- package/dist/schemas/http.d.ts.map +1 -0
- package/dist/schemas/http.js +45 -0
- package/dist/schemas/http.js.map +1 -0
- package/dist/schemas/remote.d.ts +25 -0
- package/dist/schemas/remote.d.ts.map +1 -0
- package/dist/schemas/remote.js +23 -0
- package/dist/schemas/remote.js.map +1 -0
- package/dist/schemas/session.d.ts +26 -0
- package/dist/schemas/session.d.ts.map +1 -0
- package/dist/schemas/session.js +17 -0
- package/dist/schemas/session.js.map +1 -0
- package/dist/schemas/store.d.ts +29 -0
- package/dist/schemas/store.d.ts.map +1 -0
- package/dist/schemas/store.js +22 -0
- package/dist/schemas/store.js.map +1 -0
- package/dist/schemas/terminal.d.ts +296 -0
- package/dist/schemas/terminal.d.ts.map +1 -0
- package/dist/schemas/terminal.js +243 -0
- package/dist/schemas/terminal.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/session-store.d.ts +176 -0
- package/dist/server/session-store.d.ts.map +1 -0
- package/dist/server/session-store.js +462 -0
- package/dist/server/session-store.js.map +1 -0
- package/dist/server/sse-stream.d.ts +32 -0
- package/dist/server/sse-stream.d.ts.map +1 -0
- package/dist/server/sse-stream.js +87 -0
- package/dist/server/sse-stream.js.map +1 -0
- package/dist/svelte/HostPicker.svelte +263 -0
- package/dist/svelte/HostPicker.svelte.d.ts +36 -0
- package/dist/svelte/HostPicker.svelte.d.ts.map +1 -0
- package/dist/svelte/README.md +75 -0
- package/dist/svelte/ReconnectBanner.svelte +88 -0
- package/dist/svelte/ReconnectBanner.svelte.d.ts +16 -0
- package/dist/svelte/ReconnectBanner.svelte.d.ts.map +1 -0
- package/dist/svelte/SessionTabs.svelte +192 -0
- package/dist/svelte/SessionTabs.svelte.d.ts +17 -0
- package/dist/svelte/SessionTabs.svelte.d.ts.map +1 -0
- package/dist/svelte/Terminal.svelte +567 -0
- package/dist/svelte/Terminal.svelte.d.ts +25 -0
- package/dist/svelte/Terminal.svelte.d.ts.map +1 -0
- package/dist/svelte/TerminalHostWorkspace.svelte +264 -0
- package/dist/svelte/TerminalHostWorkspace.svelte.d.ts +59 -0
- package/dist/svelte/TerminalHostWorkspace.svelte.d.ts.map +1 -0
- package/dist/svelte/TerminalMobileToolbar.svelte +56 -0
- package/dist/svelte/TerminalMobileToolbar.svelte.d.ts +14 -0
- package/dist/svelte/TerminalMobileToolbar.svelte.d.ts.map +1 -0
- package/dist/svelte/TerminalWorkspace.svelte +562 -0
- package/dist/svelte/TerminalWorkspace.svelte.d.ts +25 -0
- package/dist/svelte/TerminalWorkspace.svelte.d.ts.map +1 -0
- package/dist/svelte/ambient.d.ts +9 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts +4 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts.map +1 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.js +74 -0
- package/dist/svelte/index.d.ts +29 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +28 -0
- package/dist/svelte/styles.css +8 -0
- package/dist/svelte/terminal-host-workspace.d.ts +152 -0
- package/dist/svelte/terminal-host-workspace.d.ts.map +1 -0
- package/dist/svelte/terminal-host-workspace.js +335 -0
- package/dist/svelte/terminal-session-authority.d.ts +17 -0
- package/dist/svelte/terminal-session-authority.d.ts.map +1 -0
- package/dist/svelte/terminal-session-authority.js +25 -0
- package/dist/svelte/terminal-svelte.plugin.d.ts +21 -0
- package/dist/svelte/terminal-svelte.plugin.d.ts.map +1 -0
- package/dist/svelte/terminal-svelte.plugin.js +53 -0
- package/dist/svelte/terminal-theme.d.ts +11 -0
- package/dist/svelte/terminal-theme.d.ts.map +1 -0
- package/dist/svelte/terminal-theme.js +116 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte +65 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.d.ts +25 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.js +116 -0
- package/dist/svelte/types.d.ts +160 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +51 -0
- package/dist/svelte/workspace.d.ts +3 -0
- package/dist/svelte/workspace.d.ts.map +1 -0
- package/dist/svelte/workspace.js +2 -0
- package/dist/terminal-client.plugin.d.ts +3 -0
- package/dist/terminal-client.plugin.d.ts.map +1 -0
- package/dist/terminal-client.plugin.js +3 -0
- package/dist/terminal-client.plugin.js.map +1 -0
- package/dist/terminal-runtime.d.ts +30 -0
- package/dist/terminal-runtime.d.ts.map +1 -0
- package/dist/terminal-runtime.js +673 -0
- package/dist/terminal-runtime.js.map +1 -0
- package/dist/terminal.consumer.plugin.d.ts +14 -0
- package/dist/terminal.consumer.plugin.d.ts.map +1 -0
- package/dist/terminal.consumer.plugin.js +31 -0
- package/dist/terminal.consumer.plugin.js.map +1 -0
- package/dist/tools/terminal.impl.consumer.d.ts +11 -0
- package/dist/tools/terminal.impl.consumer.d.ts.map +1 -0
- package/dist/tools/terminal.impl.consumer.js +20 -0
- package/dist/tools/terminal.impl.consumer.js.map +1 -0
- package/dist/tools/terminal.schemas.d.ts +3 -0
- package/dist/tools/terminal.schemas.d.ts.map +1 -0
- package/dist/tools/terminal.schemas.js +3 -0
- package/dist/tools/terminal.schemas.js.map +1 -0
- package/dist/tools/terminal.unified.descriptor.d.ts +218 -0
- package/dist/tools/terminal.unified.descriptor.d.ts.map +1 -0
- package/dist/tools/terminal.unified.descriptor.js +36 -0
- package/dist/tools/terminal.unified.descriptor.js.map +1 -0
- package/dist/tui/index.d.ts +9 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/terminal-open.descriptor.d.ts +9 -0
- package/dist/tui/terminal-open.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal-open.descriptor.js +18 -0
- package/dist/tui/terminal-open.descriptor.js.map +1 -0
- package/dist/tui/terminal-open.impl.consumer.d.ts +9 -0
- package/dist/tui/terminal-open.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal-open.impl.consumer.js +13 -0
- package/dist/tui/terminal-open.impl.consumer.js.map +1 -0
- package/dist/tui/terminal-registry.descriptor.d.ts +11 -0
- package/dist/tui/terminal-registry.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal-registry.descriptor.js +17 -0
- package/dist/tui/terminal-registry.descriptor.js.map +1 -0
- package/dist/tui/terminal-registry.impl.consumer.d.ts +71 -0
- package/dist/tui/terminal-registry.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal-registry.impl.consumer.js +190 -0
- package/dist/tui/terminal-registry.impl.consumer.js.map +1 -0
- package/dist/tui/terminal-tui.plugin.d.ts +10 -0
- package/dist/tui/terminal-tui.plugin.d.ts.map +1 -0
- package/dist/tui/terminal-tui.plugin.js +32 -0
- package/dist/tui/terminal-tui.plugin.js.map +1 -0
- package/dist/tui/terminal.descriptor.d.ts +10 -0
- package/dist/tui/terminal.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal.descriptor.js +25 -0
- package/dist/tui/terminal.descriptor.js.map +1 -0
- package/dist/tui/terminal.impl.consumer.d.ts +14 -0
- package/dist/tui/terminal.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal.impl.consumer.js +189 -0
- package/dist/tui/terminal.impl.consumer.js.map +1 -0
- package/dist/tui/terminal.state-key.d.ts +30 -0
- package/dist/tui/terminal.state-key.d.ts.map +1 -0
- package/dist/tui/terminal.state-key.js +9 -0
- package/dist/tui/terminal.state-key.js.map +1 -0
- package/dist/tui/terminal.state.d.ts +152 -0
- package/dist/tui/terminal.state.d.ts.map +1 -0
- package/dist/tui/terminal.state.js +149 -0
- package/dist/tui/terminal.state.js.map +1 -0
- package/dist/workspace.d.ts +155 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +292 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +228 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibesdotdev/terminal-client — SSE stream builder for terminal output.
|
|
3
|
+
*
|
|
4
|
+
* Builds the `ReadableStream<Uint8Array>` body for `GET /api/session/{id}/output`.
|
|
5
|
+
* Used by both hq-web (remote PTY) and consumer-hq (local PTY): the wire format is
|
|
6
|
+
* identical regardless of what backs the session.
|
|
7
|
+
*
|
|
8
|
+
* Wire format:
|
|
9
|
+
* event: data\ndata: <base64>\n\n — a chunk of PTY output
|
|
10
|
+
* event: exit\ndata: {"exitCode":N|null}\n\n — session exited (final event)
|
|
11
|
+
*
|
|
12
|
+
* SERVER-ONLY.
|
|
13
|
+
*/
|
|
14
|
+
import { base64ToBytes, bytesToBase64 } from '../pty-bridge/protocol.ts';
|
|
15
|
+
import type { StoredSession } from './session-store.ts';
|
|
16
|
+
export declare const toBase64: typeof bytesToBase64;
|
|
17
|
+
export declare const fromBase64: typeof base64ToBytes;
|
|
18
|
+
/**
|
|
19
|
+
* Build an SSE `ReadableStream` for a session's PTY output.
|
|
20
|
+
*
|
|
21
|
+
* Replays any backlog that arrived before this stream attached (so a
|
|
22
|
+
* slightly-late EventSource connection sees the shell prompt), then streams
|
|
23
|
+
* live output. A final `exit` event closes the stream.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildSseStream(stored: StoredSession): ReadableStream<Uint8Array>;
|
|
26
|
+
/** Standard SSE response headers. */
|
|
27
|
+
export declare const SSE_HEADERS: {
|
|
28
|
+
'content-type': string;
|
|
29
|
+
'cache-control': string;
|
|
30
|
+
connection: string;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=sse-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-stream.d.ts","sourceRoot":"","sources":["../../src/server/sse-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EACN,aAAa,EACb,aAAa,EACb,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,eAAO,MAAM,QAAQ,sBAAgB,CAAC;AACtC,eAAO,MAAM,UAAU,sBAAgB,CAAC;AAExC;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAAC,UAAU,CAAC,CAwDhF;AAED,qCAAqC;AACrC,eAAO,MAAM,WAAW;;;;CAIU,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibesdotdev/terminal-client — SSE stream builder for terminal output.
|
|
3
|
+
*
|
|
4
|
+
* Builds the `ReadableStream<Uint8Array>` body for `GET /api/session/{id}/output`.
|
|
5
|
+
* Used by both hq-web (remote PTY) and consumer-hq (local PTY): the wire format is
|
|
6
|
+
* identical regardless of what backs the session.
|
|
7
|
+
*
|
|
8
|
+
* Wire format:
|
|
9
|
+
* event: data\ndata: <base64>\n\n — a chunk of PTY output
|
|
10
|
+
* event: exit\ndata: {"exitCode":N|null}\n\n — session exited (final event)
|
|
11
|
+
*
|
|
12
|
+
* SERVER-ONLY.
|
|
13
|
+
*/
|
|
14
|
+
import { CapabilityError, CaughtFailureSchema } from '@vibesdotdev/kernel';
|
|
15
|
+
import { base64ToBytes, bytesToBase64 } from "../pty-bridge/protocol.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Base64 helpers (shared with the client adapter in ../client/http-session.ts)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export const toBase64 = bytesToBase64;
|
|
20
|
+
export const fromBase64 = base64ToBytes;
|
|
21
|
+
/**
|
|
22
|
+
* Build an SSE `ReadableStream` for a session's PTY output.
|
|
23
|
+
*
|
|
24
|
+
* Replays any backlog that arrived before this stream attached (so a
|
|
25
|
+
* slightly-late EventSource connection sees the shell prompt), then streams
|
|
26
|
+
* live output. A final `exit` event closes the stream.
|
|
27
|
+
*/
|
|
28
|
+
export function buildSseStream(stored) {
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
let closed = false;
|
|
31
|
+
let unsubscribe = () => { };
|
|
32
|
+
return new ReadableStream({
|
|
33
|
+
start(controller) {
|
|
34
|
+
const send = (eventName, data) => {
|
|
35
|
+
if (closed)
|
|
36
|
+
return;
|
|
37
|
+
controller.enqueue(encoder.encode(`event: ${eventName}\ndata: ${data}\n\n`));
|
|
38
|
+
};
|
|
39
|
+
// Flush headers immediately, including for a quiet PTY with no backlog.
|
|
40
|
+
controller.enqueue(encoder.encode(': connected\n\n'));
|
|
41
|
+
// Replay backlog captured before this stream attached.
|
|
42
|
+
for (const chunk of stored.backlog)
|
|
43
|
+
send('data', toBase64(chunk));
|
|
44
|
+
unsubscribe = stored.session.onOutput((bytes) => {
|
|
45
|
+
send('data', toBase64(bytes));
|
|
46
|
+
});
|
|
47
|
+
const finish = (exitCode) => {
|
|
48
|
+
if (closed)
|
|
49
|
+
return;
|
|
50
|
+
send('exit', JSON.stringify({ exitCode }));
|
|
51
|
+
closed = true;
|
|
52
|
+
unsubscribe();
|
|
53
|
+
controller.close();
|
|
54
|
+
};
|
|
55
|
+
const fail = (failure) => {
|
|
56
|
+
if (closed)
|
|
57
|
+
return;
|
|
58
|
+
send('failure', JSON.stringify(failure.toEnvelope().error));
|
|
59
|
+
closed = true;
|
|
60
|
+
unsubscribe();
|
|
61
|
+
controller.close();
|
|
62
|
+
};
|
|
63
|
+
void stored.session.exited()
|
|
64
|
+
.then(({ exitCode }) => ({ kind: 'exit', exitCode }), (cause) => ({
|
|
65
|
+
kind: 'failure',
|
|
66
|
+
failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
|
|
67
|
+
}))
|
|
68
|
+
.then((outcome) => {
|
|
69
|
+
if (outcome.kind === 'exit')
|
|
70
|
+
finish(outcome.exitCode);
|
|
71
|
+
else
|
|
72
|
+
fail(outcome.failure);
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
cancel() {
|
|
76
|
+
closed = true;
|
|
77
|
+
unsubscribe();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Standard SSE response headers. */
|
|
82
|
+
export const SSE_HEADERS = {
|
|
83
|
+
'content-type': 'text/event-stream',
|
|
84
|
+
'cache-control': 'no-cache, no-transform',
|
|
85
|
+
connection: 'keep-alive'
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=sse-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-stream.js","sourceRoot":"","sources":["../../src/server/sse-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* <HostPicker> — host + shell picker for the terminal surface.
|
|
4
|
+
*
|
|
5
|
+
* Presents the list of reachable hosts (consumer: "This machine" always first;
|
|
6
|
+
* cloud: fleet hosts). The user picks a host and an optional command, then
|
|
7
|
+
* clicks "Open". Deep-linked ?host= preselects a host by id.
|
|
8
|
+
*
|
|
9
|
+
* When no hosts are available the empty state explains why and offers a
|
|
10
|
+
* link to add a machine — it is never a dead end.
|
|
11
|
+
*
|
|
12
|
+
* Keyboard-first: focus opens the host list; Enter/Space selects; Tab moves
|
|
13
|
+
* between host, command, and Open; Escape closes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Button } from '@vibesdotdev/kit/components/ui/button';
|
|
17
|
+
import { Input } from '@vibesdotdev/kit/components/ui/input';
|
|
18
|
+
import { cn } from '@vibesdotdev/kit/utils';
|
|
19
|
+
import type { HostOption } from './types.js';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
/**
|
|
23
|
+
* Ordered list of available hosts. The first entry with `local: true` is
|
|
24
|
+
* "This machine" in consumer-hq; cloud hosts follow in alphabetical order.
|
|
25
|
+
*/
|
|
26
|
+
hosts: HostOption[];
|
|
27
|
+
/** Pre-selected host id (from ?host= deep-link). */
|
|
28
|
+
preselectedHostId?: string;
|
|
29
|
+
/** Shown in the command field as placeholder when unset. */
|
|
30
|
+
defaultCommand?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Pre-fill the command input with this value VERBATIM (from a ?command=
|
|
33
|
+
* deep-link). The field stays fully editable and is NEVER auto-run — the
|
|
34
|
+
* user must still click "Open terminal". Empty/undefined leaves the field
|
|
35
|
+
* blank (placeholder = defaultCommand). Non-breaking: omitting it preserves
|
|
36
|
+
* the prior empty-field behavior.
|
|
37
|
+
*/
|
|
38
|
+
initialCommand?: string;
|
|
39
|
+
/** Href of the add-machine flow (shown when hosts array is empty). */
|
|
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;
|
|
47
|
+
/** Called when the user confirms their selection. */
|
|
48
|
+
onOpen: (hostId: string, command: string) => void;
|
|
49
|
+
/** Called when the user presses Escape or dismisses. */
|
|
50
|
+
onCancel?: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
hosts,
|
|
55
|
+
preselectedHostId,
|
|
56
|
+
defaultCommand = 'sh',
|
|
57
|
+
initialCommand = '',
|
|
58
|
+
addMachineHref = '/fleet',
|
|
59
|
+
allowDefaultShell = false,
|
|
60
|
+
onOpen,
|
|
61
|
+
onCancel
|
|
62
|
+
}: Props = $props();
|
|
63
|
+
|
|
64
|
+
// Use a derived to compute the default selection from props, then allow the
|
|
65
|
+
// user to override it with a local $state. This avoids the Svelte 5 warning
|
|
66
|
+
// about $state referencing props directly in the initializer.
|
|
67
|
+
const _defaultSelectedId = $derived(
|
|
68
|
+
preselectedHostId && hosts.some((h) => h.id === preselectedHostId)
|
|
69
|
+
? preselectedHostId
|
|
70
|
+
: (hosts[0]?.id ?? '')
|
|
71
|
+
);
|
|
72
|
+
let _manualSelection = $state<string | null>(null);
|
|
73
|
+
const selectedHostId = $derived(_manualSelection ?? _defaultSelectedId);
|
|
74
|
+
// Pre-fill verbatim from initialCommand; the field stays editable and is never
|
|
75
|
+
// auto-run (the user must click Open). The closure marks this as an
|
|
76
|
+
// intentional one-time capture for Svelte's rune analyzer.
|
|
77
|
+
const initialCommandValue = (() => initialCommand)();
|
|
78
|
+
let command = $state(initialCommandValue);
|
|
79
|
+
let commandEl = $state<HTMLInputElement | null>(null);
|
|
80
|
+
|
|
81
|
+
const selectedHost = $derived(hosts.find((h) => h.id === selectedHostId) ?? null);
|
|
82
|
+
const resolvedCommand = $derived(command.trim() || (allowDefaultShell ? '' : defaultCommand));
|
|
83
|
+
const canOpen = $derived(selectedHost !== null && (allowDefaultShell || resolvedCommand.length > 0));
|
|
84
|
+
|
|
85
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
86
|
+
if (e.key === 'Escape') {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
onCancel?.();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleOpen() {
|
|
93
|
+
if (!canOpen) return;
|
|
94
|
+
onOpen(selectedHostId, resolvedCommand);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function selectHost(id: string) {
|
|
98
|
+
_manualSelection = id;
|
|
99
|
+
// Move focus to command field after host selection so the user can
|
|
100
|
+
// proceed without touching the mouse.
|
|
101
|
+
commandEl?.focus();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hostLabel(host: HostOption): string {
|
|
105
|
+
if (host.local) return `This machine — ${host.name}`;
|
|
106
|
+
return host.name;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hostOptionId(hostId: string): string {
|
|
110
|
+
return `host-opt-${encodeURIComponent(hostId)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hostStatusLabel(host: HostOption): string {
|
|
114
|
+
if (host.status === 'online') return 'Online';
|
|
115
|
+
if (host.status === 'degraded') return 'Limited connection';
|
|
116
|
+
if (host.status === 'offline') return 'Offline';
|
|
117
|
+
return 'Status unknown';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hostClass(host: HostOption): string {
|
|
121
|
+
return cn(
|
|
122
|
+
'flex cursor-pointer items-center gap-2.5 rounded-md border border-transparent px-3 py-2.5 outline-none transition-colors hover:border-border hover:bg-accent/60 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50',
|
|
123
|
+
host.id === selectedHostId && 'border-primary bg-primary/10 text-foreground',
|
|
124
|
+
host.status === 'offline' && 'opacity-60'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function statusDotClass(status: HostOption['status']): string {
|
|
129
|
+
return cn(
|
|
130
|
+
'size-2 shrink-0 rounded-full bg-muted-foreground',
|
|
131
|
+
status === 'online' && 'bg-success',
|
|
132
|
+
status === 'degraded' && 'bg-warning',
|
|
133
|
+
status === 'offline' && 'bg-muted-foreground/70'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
139
|
+
<div
|
|
140
|
+
class="w-full max-w-lg rounded-md border border-border bg-card p-5 text-sm text-card-foreground outline-none"
|
|
141
|
+
role="dialog"
|
|
142
|
+
aria-modal="true"
|
|
143
|
+
aria-label="Open a terminal session"
|
|
144
|
+
tabindex="-1"
|
|
145
|
+
onkeydown={handleKeydown}
|
|
146
|
+
>
|
|
147
|
+
<div class="mb-4 flex items-center justify-between gap-3">
|
|
148
|
+
<h2 class="m-0 text-base font-semibold text-card-foreground">Open a terminal</h2>
|
|
149
|
+
{#if onCancel}
|
|
150
|
+
<Button
|
|
151
|
+
variant="ghost"
|
|
152
|
+
size="icon-sm"
|
|
153
|
+
class="size-6 text-muted-foreground hover:text-foreground"
|
|
154
|
+
aria-label="Cancel"
|
|
155
|
+
onclick={onCancel}
|
|
156
|
+
>×</Button>
|
|
157
|
+
{/if}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{#if hosts.length === 0}
|
|
161
|
+
<!-- Empty state: never a dead end — offer the add-machine flow -->
|
|
162
|
+
<div class="flex flex-col items-center gap-2 py-6 text-center">
|
|
163
|
+
<div class="text-muted-foreground" aria-hidden="true">
|
|
164
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
165
|
+
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
|
166
|
+
<path d="M8 21h8M12 17v4"/>
|
|
167
|
+
|
|
168
|
+
</svg>
|
|
169
|
+
</div>
|
|
170
|
+
<p class="m-0 text-sm font-medium text-foreground">No machines connected</p>
|
|
171
|
+
<p class="m-0 max-w-sm text-sm leading-normal text-muted-foreground">
|
|
172
|
+
Add a machine to open a remote terminal. The setup takes under a minute.
|
|
173
|
+
</p>
|
|
174
|
+
<Button variant="outline" size="sm" href={addMachineHref} class="mt-2">
|
|
175
|
+
Add a machine →
|
|
176
|
+
</Button>
|
|
177
|
+
</div>
|
|
178
|
+
{:else}
|
|
179
|
+
<div class="mb-4">
|
|
180
|
+
<span class="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase text-muted-foreground" id="host-list-label">Machine</span>
|
|
181
|
+
<ul
|
|
182
|
+
class="flex max-h-44 list-none flex-col gap-1 overflow-y-auto p-0"
|
|
183
|
+
role="listbox"
|
|
184
|
+
tabindex="0"
|
|
185
|
+
aria-labelledby="host-list-label"
|
|
186
|
+
aria-activedescendant={selectedHost ? hostOptionId(selectedHost.id) : undefined}
|
|
187
|
+
>
|
|
188
|
+
{#each hosts as host (host.id)}
|
|
189
|
+
<li
|
|
190
|
+
id={hostOptionId(host.id)}
|
|
191
|
+
class={hostClass(host)}
|
|
192
|
+
role="option"
|
|
193
|
+
aria-selected={host.id === selectedHostId}
|
|
194
|
+
tabindex={host.id === selectedHostId ? 0 : -1}
|
|
195
|
+
onclick={() => selectHost(host.id)}
|
|
196
|
+
onkeydown={(e) => {
|
|
197
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
selectHost(host.id);
|
|
200
|
+
}
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<span
|
|
204
|
+
class={statusDotClass(host.status)}
|
|
205
|
+
aria-label={hostStatusLabel(host)}
|
|
206
|
+
title={hostStatusLabel(host)}
|
|
207
|
+
></span>
|
|
208
|
+
<span class="min-w-0 flex-1 truncate font-medium">{hostLabel(host)}</span>
|
|
209
|
+
{#if host.id === selectedHostId}
|
|
210
|
+
<span class="shrink-0 text-primary" aria-hidden="true">✓</span>
|
|
211
|
+
{/if}
|
|
212
|
+
</li>
|
|
213
|
+
{/each}
|
|
214
|
+
</ul>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="mb-4">
|
|
218
|
+
<label class="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase text-muted-foreground" for="picker-command">
|
|
219
|
+
Command
|
|
220
|
+
<span class="font-normal normal-case text-muted-foreground/80">optional — defaults to your shell</span>
|
|
221
|
+
</label>
|
|
222
|
+
<Input
|
|
223
|
+
id="picker-command"
|
|
224
|
+
bind:ref={commandEl}
|
|
225
|
+
type="text"
|
|
226
|
+
bind:value={command}
|
|
227
|
+
placeholder={defaultCommand}
|
|
228
|
+
class="font-mono"
|
|
229
|
+
autocomplete="off"
|
|
230
|
+
autocorrect="off"
|
|
231
|
+
autocapitalize="off"
|
|
232
|
+
spellcheck={false}
|
|
233
|
+
onkeydown={(e) => {
|
|
234
|
+
if (e.key === 'Enter') {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
handleOpen();
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{#if selectedHost && selectedHost.status === 'offline'}
|
|
243
|
+
<p class="mb-4 rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning" role="alert">
|
|
244
|
+
This machine appears offline. You can still try to connect — it may recover.
|
|
245
|
+
</p>
|
|
246
|
+
{/if}
|
|
247
|
+
|
|
248
|
+
<div class="flex justify-end gap-2">
|
|
249
|
+
{#if onCancel}
|
|
250
|
+
<Button variant="outline" size="sm" onclick={onCancel}>
|
|
251
|
+
Cancel
|
|
252
|
+
</Button>
|
|
253
|
+
{/if}
|
|
254
|
+
<Button
|
|
255
|
+
size="sm"
|
|
256
|
+
disabled={!canOpen}
|
|
257
|
+
onclick={handleOpen}
|
|
258
|
+
>
|
|
259
|
+
Open terminal
|
|
260
|
+
</Button>
|
|
261
|
+
</div>
|
|
262
|
+
{/if}
|
|
263
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HostOption } from './types.ts';
|
|
2
|
+
interface Props {
|
|
3
|
+
/**
|
|
4
|
+
* Ordered list of available hosts. The first entry with `local: true` is
|
|
5
|
+
* "This machine" in consumer-hq; cloud hosts follow in alphabetical order.
|
|
6
|
+
*/
|
|
7
|
+
hosts: HostOption[];
|
|
8
|
+
/** Pre-selected host id (from ?host= deep-link). */
|
|
9
|
+
preselectedHostId?: string;
|
|
10
|
+
/** Shown in the command field as placeholder when unset. */
|
|
11
|
+
defaultCommand?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Pre-fill the command input with this value VERBATIM (from a ?command=
|
|
14
|
+
* deep-link). The field stays fully editable and is NEVER auto-run — the
|
|
15
|
+
* user must still click "Open terminal". Empty/undefined leaves the field
|
|
16
|
+
* blank (placeholder = defaultCommand). Non-breaking: omitting it preserves
|
|
17
|
+
* the prior empty-field behavior.
|
|
18
|
+
*/
|
|
19
|
+
initialCommand?: string;
|
|
20
|
+
/** Href of the add-machine flow (shown when hosts array is empty). */
|
|
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;
|
|
28
|
+
/** Called when the user confirms their selection. */
|
|
29
|
+
onOpen: (hostId: string, command: string) => void;
|
|
30
|
+
/** Called when the user presses Escape or dismisses. */
|
|
31
|
+
onCancel?: () => void;
|
|
32
|
+
}
|
|
33
|
+
declare const HostPicker: import("svelte").Component<Props, {}, "">;
|
|
34
|
+
type HostPicker = ReturnType<typeof HostPicker>;
|
|
35
|
+
export default HostPicker;
|
|
36
|
+
//# sourceMappingURL=HostPicker.svelte.d.ts.map
|
|
@@ -0,0 +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;;;;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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @vibesdotdev/terminal-client/svelte
|
|
2
|
+
|
|
3
|
+
Svelte 5 terminal surfaces over the canonical `runtime/execution` contract.
|
|
4
|
+
|
|
5
|
+
`Terminal` renders an injected `ExecutionSession`. `TerminalWorkspace` resolves
|
|
6
|
+
an execution provider, creates the real session, and reports its lifecycle.
|
|
7
|
+
|
|
8
|
+
```svelte
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import TerminalWorkspace from '@vibesdotdev/terminal-client/svelte/TerminalWorkspace.svelte';
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<TerminalWorkspace
|
|
14
|
+
cwd="/projects/vessel"
|
|
15
|
+
onLifecycle={(event) => {
|
|
16
|
+
if (event.phase === 'opened') persistReceipt(event);
|
|
17
|
+
}}
|
|
18
|
+
/>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`opened` is the success boundary. It is emitted only after `session()` resolves
|
|
22
|
+
and includes the real session id, environment id, execution id, requested `cwd`,
|
|
23
|
+
and timestamp. A Files `onOpenTerminal` callback is only the request that begins
|
|
24
|
+
this flow.
|
|
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
|
+
|
|
49
|
+
## Runtime docs
|
|
50
|
+
|
|
51
|
+
Register `@vibesdotdev/terminal-client/svelte/plugin` to contribute the canonical
|
|
52
|
+
`terminal-svelte` `docs/topic` and the designed Kit ToolCard for `tools/tool:terminal`
|
|
53
|
+
(cwd, command/session, output excerpt via CodeViewer). The topic documents public
|
|
54
|
+
entrypoints, the execution-backed lifecycle boundary, and the ownership split.
|
|
55
|
+
|
|
56
|
+
## Vessel surface
|
|
57
|
+
|
|
58
|
+
Hosts that compose Vessel may register
|
|
59
|
+
`@vibesdotdev/vessel/apps/terminal.vessel.plugin`. It contributes stable surface id
|
|
60
|
+
`terminal.workspace` and a browser-only lazy loader. The descriptor contains no
|
|
61
|
+
component import path; Vessel keeps that product-registration detail while loading
|
|
62
|
+
Terminal's owner component. It consumes the strict runtime-validatable state schema
|
|
63
|
+
from `@vibesdotdev/terminal-client/workspace` (`executionId`, `cwd`, optional
|
|
64
|
+
correlation/source ids) and requires `runtime/execution`, selected exactly by the
|
|
65
|
+
normalized `executionId`. Package presence or a different execution provider does
|
|
66
|
+
not make a saved Terminal anchor available.
|
|
67
|
+
|
|
68
|
+
A retained Terminal treats `executionId`, command, arguments, and `cwd` as one
|
|
69
|
+
live launch signature. Changing any member cancels the older open generation and
|
|
70
|
+
starts a session through the newly selected exact provider; same-folder provider
|
|
71
|
+
changes never keep the old session by accident.
|
|
72
|
+
|
|
73
|
+
The xterm attachment has its own generation lease. A transient `session=null` or
|
|
74
|
+
provider swap detaches output immediately, and deferred exit/disconnect callbacks
|
|
75
|
+
from the old session cannot overwrite the new session's visible state.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* <ReconnectBanner> — visible reconnect / error banner.
|
|
4
|
+
*
|
|
5
|
+
* Renders in three visual states:
|
|
6
|
+
* - 'reconnecting': amber, shows attempt number + "Reconnecting…"
|
|
7
|
+
* - 'reconnected': green, confirms the session was restored with backlog replay
|
|
8
|
+
* - 'failed': red, explains the drop + offers "Start new session"
|
|
9
|
+
*
|
|
10
|
+
* The green 'reconnected' banner auto-dismisses after a short delay
|
|
11
|
+
* (the page manages this externally by setting status back to null).
|
|
12
|
+
*/
|
|
13
|
+
import { Button } from '@vibesdotdev/kit/components/ui/button';
|
|
14
|
+
import { cn } from '@vibesdotdev/kit/utils';
|
|
15
|
+
|
|
16
|
+
type BannerStatus = 'reconnecting' | 'reconnected' | 'failed' | null;
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
status: BannerStatus;
|
|
20
|
+
/** Current attempt number (1-based), shown during 'reconnecting'. */
|
|
21
|
+
attempt?: number;
|
|
22
|
+
/** Delay in ms before the next reconnect attempt, shown during 'reconnecting'. */
|
|
23
|
+
delayMs?: number;
|
|
24
|
+
/** Human-readable failure reason shown in the 'failed' state. */
|
|
25
|
+
reason?: string;
|
|
26
|
+
/** Called when the user clicks "Start new session" in the failed state. */
|
|
27
|
+
onNewSession?: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { status, attempt = 0, delayMs = 0, reason = '', onNewSession }: Props = $props();
|
|
31
|
+
|
|
32
|
+
const bannerClass = $derived(
|
|
33
|
+
cn(
|
|
34
|
+
'flex shrink-0 items-center gap-2 border-t px-3.5 py-2 font-mono text-xs',
|
|
35
|
+
status === 'reconnecting' &&
|
|
36
|
+
'border-border bg-warning/10 text-warning',
|
|
37
|
+
status === 'reconnected' &&
|
|
38
|
+
'border-border bg-success/10 text-success',
|
|
39
|
+
status === 'failed' &&
|
|
40
|
+
'border-border bg-destructive/15 text-destructive'
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function formatDelay(ms: number): string {
|
|
45
|
+
if (ms < 1000) return `${ms} ms`;
|
|
46
|
+
return `${(ms / 1000).toFixed(1)} s`;
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
{#if status !== null}
|
|
51
|
+
<div
|
|
52
|
+
class={bannerClass}
|
|
53
|
+
role="status"
|
|
54
|
+
aria-live="polite"
|
|
55
|
+
aria-atomic="true"
|
|
56
|
+
>
|
|
57
|
+
{#if status === 'reconnecting'}
|
|
58
|
+
<span class="shrink-0 text-sm" aria-hidden="true">↻</span>
|
|
59
|
+
<span class="flex-1 leading-normal">
|
|
60
|
+
Connection lost — reconnecting
|
|
61
|
+
{#if attempt > 0}(attempt {attempt}){/if}
|
|
62
|
+
{#if delayMs > 0}
|
|
63
|
+
<span class="ml-1 opacity-70">next attempt in {formatDelay(delayMs)}</span>
|
|
64
|
+
{/if}
|
|
65
|
+
</span>
|
|
66
|
+
{:else if status === 'reconnected'}
|
|
67
|
+
<span class="shrink-0 text-sm" aria-hidden="true">✓</span>
|
|
68
|
+
<span class="flex-1 leading-normal">
|
|
69
|
+
Reattached — your session kept running
|
|
70
|
+
</span>
|
|
71
|
+
{:else if status === 'failed'}
|
|
72
|
+
<span class="shrink-0 text-sm" aria-hidden="true">⚠</span>
|
|
73
|
+
<span class="flex-1 leading-normal">
|
|
74
|
+
{reason || 'Connection lost and could not be restored.'}
|
|
75
|
+
</span>
|
|
76
|
+
{#if onNewSession}
|
|
77
|
+
<Button
|
|
78
|
+
variant="outline"
|
|
79
|
+
size="sm"
|
|
80
|
+
class="h-7 shrink-0 border-current bg-transparent px-2 font-mono text-xs text-inherit hover:bg-background/20 hover:text-inherit"
|
|
81
|
+
onclick={onNewSession}
|
|
82
|
+
>
|
|
83
|
+
Start new session
|
|
84
|
+
</Button>
|
|
85
|
+
{/if}
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
{/if}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type BannerStatus = 'reconnecting' | 'reconnected' | 'failed' | null;
|
|
2
|
+
interface Props {
|
|
3
|
+
status: BannerStatus;
|
|
4
|
+
/** Current attempt number (1-based), shown during 'reconnecting'. */
|
|
5
|
+
attempt?: number;
|
|
6
|
+
/** Delay in ms before the next reconnect attempt, shown during 'reconnecting'. */
|
|
7
|
+
delayMs?: number;
|
|
8
|
+
/** Human-readable failure reason shown in the 'failed' state. */
|
|
9
|
+
reason?: string;
|
|
10
|
+
/** Called when the user clicks "Start new session" in the failed state. */
|
|
11
|
+
onNewSession?: () => void;
|
|
12
|
+
}
|
|
13
|
+
declare const ReconnectBanner: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type ReconnectBanner = ReturnType<typeof ReconnectBanner>;
|
|
15
|
+
export default ReconnectBanner;
|
|
16
|
+
//# sourceMappingURL=ReconnectBanner.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReconnectBanner.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/ReconnectBanner.svelte.ts"],"names":[],"mappings":"AAkBC,KAAK,YAAY,GAAG,cAAc,GAAG,aAAa,GAAG,QAAQ,GAAG,IAAI,CAAC;AAErE,UAAU,KAAK;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AA4DF,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|