@broberg/seti-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/frame-accumulator.ts","../src/types.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n SetiInputResult,\n SetiKey,\n SetiRoster,\n SetiStreamHandle,\n SetiStreamHandlers,\n} from \"./types\";\n\nexport interface SetiClientOptions {\n /**\n * Base URL of the SETI surface. In a browser this is the host app's proxy\n * mount (same-origin, e.g. \"/api/seti\" via @broberg/seti-server). Server-side\n * it can be the cloud directly (\"https://buddycloud.cc/api/seti/v1\") together\n * with `token`.\n */\n baseUrl: string;\n /** Bearer token — only for server-side/direct use. NEVER ship to a browser. */\n token?: string;\n /** Override fetch (tests / custom runtimes). */\n fetch?: typeof fetch;\n}\n\n/**\n * Typed client for the SETI API (roster + SSE stream + input). One code path\n * for browser and server: fetch-based SSE with automatic reconnect (1s → 5s\n * backoff), so bearer headers work everywhere and no EventSource is needed.\n */\nexport class SetiClient {\n private readonly base: string;\n private readonly headers: Record<string, string>;\n private readonly doFetch: typeof fetch;\n\n constructor(opts: SetiClientOptions) {\n this.base = opts.baseUrl.replace(/\\/$/, \"\");\n this.headers = opts.token ? { Authorization: `Bearer ${opts.token}` } : {};\n this.doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n async listSessions(): Promise<SetiRoster> {\n const res = await this.doFetch(`${this.base}/sessions`, { headers: this.headers });\n if (!res.ok) return { edges: [], error: `http_${res.status}` };\n return (await res.json()) as SetiRoster;\n }\n\n async sendText(edge: string, session: string, text: string): Promise<SetiInputResult> {\n return this.input({ edge, session, text });\n }\n\n async sendKey(edge: string, session: string, key: SetiKey): Promise<SetiInputResult> {\n return this.input({ edge, session, key });\n }\n\n private async input(body: {\n edge: string;\n session: string;\n text?: string;\n key?: SetiKey;\n }): Promise<SetiInputResult> {\n try {\n const res = await this.doFetch(`${this.base}/input`, {\n method: \"POST\",\n headers: { ...this.headers, \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(8000),\n });\n const json = (await res.json().catch(() => ({}))) as Partial<SetiInputResult>;\n return { ok: !!json.ok, edgeConnected: !!json.edgeConnected, error: json.error };\n } catch (err) {\n return { ok: false, edgeConnected: false, error: err instanceof Error ? err.message : \"send_failed\" };\n }\n }\n\n /**\n * Open the live frame stream for one edge session. Reconnects automatically\n * until `close()` is called.\n */\n openStream(edge: string, session: string, handlers: SetiStreamHandlers): SetiStreamHandle {\n let closed = false;\n let controller: AbortController | null = null;\n\n const run = async (): Promise<void> => {\n let attempt = 0;\n while (!closed) {\n handlers.onStateChange?.(attempt === 0 ? \"connecting\" : \"reconnecting\");\n controller = new AbortController();\n try {\n const res = await this.doFetch(\n `${this.base}/stream?edge=${encodeURIComponent(edge)}&session=${encodeURIComponent(session)}`,\n { headers: { ...this.headers, accept: \"text/event-stream\" }, signal: controller.signal },\n );\n if (!res.ok || !res.body) throw new Error(`http_${res.status}`);\n handlers.onStateChange?.(\"open\");\n attempt = 0;\n await this.consume(res.body, handlers);\n } catch {\n /* fall through to reconnect */\n }\n if (closed) break;\n attempt++;\n await new Promise((r) => setTimeout(r, Math.min(1000 * attempt, 5000)));\n }\n handlers.onStateChange?.(\"closed\");\n };\n void run();\n\n return {\n close: () => {\n closed = true;\n controller?.abort();\n },\n };\n }\n\n /** Minimal SSE parser: `event:` + `data:` lines, events split on blank lines. */\n private async consume(body: ReadableStream<Uint8Array>, handlers: SetiStreamHandlers): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buf = \"\";\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n let sep: number;\n while ((sep = buf.indexOf(\"\\n\\n\")) !== -1) {\n const chunk = buf.slice(0, sep);\n buf = buf.slice(sep + 2);\n let event = \"message\";\n const data: string[] = [];\n for (const line of chunk.split(\"\\n\")) {\n if (line.startsWith(\"event:\")) event = line.slice(6).trim();\n else if (line.startsWith(\"data:\")) data.push(line.slice(5).trimStart());\n }\n if (data.length === 0) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(data.join(\"\\n\"));\n } catch {\n continue;\n }\n if (event === \"frame\") {\n const c = (parsed as { content?: unknown }).content;\n if (typeof c === \"string\") handlers.onFrame?.(c);\n } else if (event === \"hello\") {\n handlers.onHello?.(parsed as { edge: string; session: string; edgeConnected: boolean });\n } else if (event === \"ping\") {\n handlers.onPing?.(parsed as { edgeConnected: boolean });\n }\n }\n }\n }\n}\n","/**\n * FrameAccumulator — the F071 scrollback engine as a tested pure class.\n *\n * cc runs on the terminal alt-screen, so tmux has no scrollback: every frame is\n * a full snapshot of the visible window. The accumulator splits each frame into\n * a volatile footer (cc's input box + statusline + spinner, rendered live) and\n * a body, then overlap-merges successive bodies so the dialogue that scrolls\n * off the top is retained.\n */\nexport interface FrameView {\n /** Accumulated dialogue lines (grows from the first fed frame). */\n history: string[];\n /** The volatile tail of the latest frame (input box / statusline / spinner). */\n footer: string[];\n}\n\nconst RULE = /^[─━-]{10,}\\s*$/;\nconst SPIN = /^[✻✶✳·•*]\\s/;\n\nexport function splitFooter(lines: string[]): { body: string[]; footer: string[] } {\n let inp = -1;\n for (let i = lines.length - 1; i >= 0 && i >= lines.length - 8; i--) {\n const t = lines[i].replace(/\\s+$/, \"\");\n if (t.charCodeAt(0) === 0x276f || t[0] === \">\") {\n inp = i;\n break;\n }\n }\n let start: number;\n if (inp === -1) start = Math.max(0, lines.length - 3);\n else start = inp > 0 && RULE.test(lines[inp - 1].trim()) ? inp - 1 : inp;\n while (start > 0 && SPIN.test(lines[start - 1])) start--;\n return { body: lines.slice(0, start), footer: lines.slice(start) };\n}\n\nexport function mergeOverlap(hist: string[], body: string[]): string[] {\n const max = Math.min(hist.length, body.length);\n for (let k = max; k > 0; k--) {\n let ok = true;\n for (let i = 0; i < k; i++) {\n if (hist[hist.length - k + i] !== body[i]) {\n ok = false;\n break;\n }\n }\n if (ok) return hist.concat(body.slice(k));\n }\n return hist.concat(body);\n}\n\nexport class FrameAccumulator {\n private history: string[] = [];\n private footer: string[] = [];\n\n constructor(private readonly maxHistory = 5000) {}\n\n /** Feed a full frame snapshot; returns the updated view. */\n feed(content: string): FrameView {\n const lines = content.replace(/\\s+$/, \"\").split(\"\\n\");\n const { body, footer } = splitFooter(lines);\n this.history = mergeOverlap(this.history, body);\n if (this.history.length > this.maxHistory) {\n this.history = this.history.slice(-this.maxHistory);\n }\n this.footer = footer;\n return this.view;\n }\n\n get view(): FrameView {\n return { history: this.history, footer: this.footer };\n }\n\n /** Full rendered text (history + live footer). */\n get text(): string {\n return this.history.concat(this.footer).join(\"\\n\");\n }\n\n reset(): void {\n this.history = [];\n this.footer = [];\n }\n}\n","/** A cc session registered on an edge (intercom channel snapshot). */\nexport interface SetiRemoteSession {\n ccSessionId: string | null;\n sessionName: string | null;\n cwd: string;\n}\n\n/** One edge host in the fleet roster. */\nexport interface SetiEdge {\n edgeId: string;\n connected: boolean;\n lastSeenMs: number;\n connectedAtMs: number | null;\n sessions: SetiRemoteSession[];\n /**\n * The tmux session names live on the edge — the STREAMABLE units. Stream and\n * input target these by name (channel sessionNames can differ, e.g. container\n * tmux \"cc\" vs channel \"fly-arn-1-cc\"). Empty = nothing streamable (M1 iTerm).\n */\n tmuxSessions: string[];\n}\n\nexport interface SetiRoster {\n edges: SetiEdge[];\n error?: string;\n}\n\n/** tmux key names accepted by input's `key` field (navigates cc's menus). */\nexport const SETI_KEYS = [\n \"Escape\",\n \"Up\",\n \"Down\",\n \"Left\",\n \"Right\",\n \"Enter\",\n \"BSpace\",\n \"Tab\",\n] as const;\nexport type SetiKey = (typeof SETI_KEYS)[number];\n\nexport interface SetiInputResult {\n ok: boolean;\n edgeConnected: boolean;\n error?: string;\n}\n\nexport type SetiStreamState = \"connecting\" | \"open\" | \"reconnecting\" | \"closed\";\n\nexport interface SetiStreamHandlers {\n /** First event after (re)connect: { edge, session, edgeConnected }. */\n onHello?: (info: { edge: string; session: string; edgeConnected: boolean }) => void;\n /** A full capture-pane snapshot of the session's visible window. */\n onFrame?: (content: string) => void;\n /** Idle keep-alive carrying the latest edge connectivity. */\n onPing?: (info: { edgeConnected: boolean }) => void;\n onStateChange?: (state: SetiStreamState) => void;\n}\n\nexport interface SetiStreamHandle {\n close: () => void;\n}\n"]}
@@ -0,0 +1,36 @@
1
+ export { S as SETI_KEYS, a as SetiClient, b as SetiClientOptions, c as SetiEdge, d as SetiInputResult, e as SetiKey, f as SetiRemoteSession, g as SetiRoster, h as SetiStreamHandle, i as SetiStreamHandlers, j as SetiStreamState } from './client-BIxdDJYz.cjs';
2
+
3
+ /**
4
+ * FrameAccumulator — the F071 scrollback engine as a tested pure class.
5
+ *
6
+ * cc runs on the terminal alt-screen, so tmux has no scrollback: every frame is
7
+ * a full snapshot of the visible window. The accumulator splits each frame into
8
+ * a volatile footer (cc's input box + statusline + spinner, rendered live) and
9
+ * a body, then overlap-merges successive bodies so the dialogue that scrolls
10
+ * off the top is retained.
11
+ */
12
+ interface FrameView {
13
+ /** Accumulated dialogue lines (grows from the first fed frame). */
14
+ history: string[];
15
+ /** The volatile tail of the latest frame (input box / statusline / spinner). */
16
+ footer: string[];
17
+ }
18
+ declare function splitFooter(lines: string[]): {
19
+ body: string[];
20
+ footer: string[];
21
+ };
22
+ declare function mergeOverlap(hist: string[], body: string[]): string[];
23
+ declare class FrameAccumulator {
24
+ private readonly maxHistory;
25
+ private history;
26
+ private footer;
27
+ constructor(maxHistory?: number);
28
+ /** Feed a full frame snapshot; returns the updated view. */
29
+ feed(content: string): FrameView;
30
+ get view(): FrameView;
31
+ /** Full rendered text (history + live footer). */
32
+ get text(): string;
33
+ reset(): void;
34
+ }
35
+
36
+ export { FrameAccumulator, type FrameView, mergeOverlap, splitFooter };
@@ -0,0 +1,36 @@
1
+ export { S as SETI_KEYS, a as SetiClient, b as SetiClientOptions, c as SetiEdge, d as SetiInputResult, e as SetiKey, f as SetiRemoteSession, g as SetiRoster, h as SetiStreamHandle, i as SetiStreamHandlers, j as SetiStreamState } from './client-BIxdDJYz.js';
2
+
3
+ /**
4
+ * FrameAccumulator — the F071 scrollback engine as a tested pure class.
5
+ *
6
+ * cc runs on the terminal alt-screen, so tmux has no scrollback: every frame is
7
+ * a full snapshot of the visible window. The accumulator splits each frame into
8
+ * a volatile footer (cc's input box + statusline + spinner, rendered live) and
9
+ * a body, then overlap-merges successive bodies so the dialogue that scrolls
10
+ * off the top is retained.
11
+ */
12
+ interface FrameView {
13
+ /** Accumulated dialogue lines (grows from the first fed frame). */
14
+ history: string[];
15
+ /** The volatile tail of the latest frame (input box / statusline / spinner). */
16
+ footer: string[];
17
+ }
18
+ declare function splitFooter(lines: string[]): {
19
+ body: string[];
20
+ footer: string[];
21
+ };
22
+ declare function mergeOverlap(hist: string[], body: string[]): string[];
23
+ declare class FrameAccumulator {
24
+ private readonly maxHistory;
25
+ private history;
26
+ private footer;
27
+ constructor(maxHistory?: number);
28
+ /** Feed a full frame snapshot; returns the updated view. */
29
+ feed(content: string): FrameView;
30
+ get view(): FrameView;
31
+ /** Full rendered text (history + live footer). */
32
+ get text(): string;
33
+ reset(): void;
34
+ }
35
+
36
+ export { FrameAccumulator, type FrameView, mergeOverlap, splitFooter };
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ export { FrameAccumulator, SetiClient, mergeOverlap, splitFooter } from './chunk-VWTGZF3D.js';
2
+
3
+ // src/types.ts
4
+ var SETI_KEYS = [
5
+ "Escape",
6
+ "Up",
7
+ "Down",
8
+ "Left",
9
+ "Right",
10
+ "Enter",
11
+ "BSpace",
12
+ "Tab"
13
+ ];
14
+
15
+ export { SETI_KEYS };
16
+ //# sourceMappingURL=index.js.map
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";;;AA4BO,IAAM,SAAA,GAAY;AAAA,EACvB,QAAA;AAAA,EACA,IAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF","file":"index.js","sourcesContent":["/** A cc session registered on an edge (intercom channel snapshot). */\nexport interface SetiRemoteSession {\n ccSessionId: string | null;\n sessionName: string | null;\n cwd: string;\n}\n\n/** One edge host in the fleet roster. */\nexport interface SetiEdge {\n edgeId: string;\n connected: boolean;\n lastSeenMs: number;\n connectedAtMs: number | null;\n sessions: SetiRemoteSession[];\n /**\n * The tmux session names live on the edge — the STREAMABLE units. Stream and\n * input target these by name (channel sessionNames can differ, e.g. container\n * tmux \"cc\" vs channel \"fly-arn-1-cc\"). Empty = nothing streamable (M1 iTerm).\n */\n tmuxSessions: string[];\n}\n\nexport interface SetiRoster {\n edges: SetiEdge[];\n error?: string;\n}\n\n/** tmux key names accepted by input's `key` field (navigates cc's menus). */\nexport const SETI_KEYS = [\n \"Escape\",\n \"Up\",\n \"Down\",\n \"Left\",\n \"Right\",\n \"Enter\",\n \"BSpace\",\n \"Tab\",\n] as const;\nexport type SetiKey = (typeof SETI_KEYS)[number];\n\nexport interface SetiInputResult {\n ok: boolean;\n edgeConnected: boolean;\n error?: string;\n}\n\nexport type SetiStreamState = \"connecting\" | \"open\" | \"reconnecting\" | \"closed\";\n\nexport interface SetiStreamHandlers {\n /** First event after (re)connect: { edge, session, edgeConnected }. */\n onHello?: (info: { edge: string; session: string; edgeConnected: boolean }) => void;\n /** A full capture-pane snapshot of the session's visible window. */\n onFrame?: (content: string) => void;\n /** Idle keep-alive carrying the latest edge connectivity. */\n onPing?: (info: { edgeConnected: boolean }) => void;\n onStateChange?: (state: SetiStreamState) => void;\n}\n\nexport interface SetiStreamHandle {\n close: () => void;\n}\n"]}
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ var hooks = require('preact/hooks');
4
+ var jsxRuntime = require('preact/jsx-runtime');
5
+
6
+ // src/preact.tsx
7
+
8
+ // src/client.ts
9
+ var SetiClient = class {
10
+ base;
11
+ headers;
12
+ doFetch;
13
+ constructor(opts) {
14
+ this.base = opts.baseUrl.replace(/\/$/, "");
15
+ this.headers = opts.token ? { Authorization: `Bearer ${opts.token}` } : {};
16
+ this.doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
17
+ }
18
+ async listSessions() {
19
+ const res = await this.doFetch(`${this.base}/sessions`, { headers: this.headers });
20
+ if (!res.ok) return { edges: [], error: `http_${res.status}` };
21
+ return await res.json();
22
+ }
23
+ async sendText(edge, session, text) {
24
+ return this.input({ edge, session, text });
25
+ }
26
+ async sendKey(edge, session, key) {
27
+ return this.input({ edge, session, key });
28
+ }
29
+ async input(body) {
30
+ try {
31
+ const res = await this.doFetch(`${this.base}/input`, {
32
+ method: "POST",
33
+ headers: { ...this.headers, "content-type": "application/json" },
34
+ body: JSON.stringify(body),
35
+ signal: AbortSignal.timeout(8e3)
36
+ });
37
+ const json = await res.json().catch(() => ({}));
38
+ return { ok: !!json.ok, edgeConnected: !!json.edgeConnected, error: json.error };
39
+ } catch (err) {
40
+ return { ok: false, edgeConnected: false, error: err instanceof Error ? err.message : "send_failed" };
41
+ }
42
+ }
43
+ /**
44
+ * Open the live frame stream for one edge session. Reconnects automatically
45
+ * until `close()` is called.
46
+ */
47
+ openStream(edge, session, handlers) {
48
+ let closed = false;
49
+ let controller = null;
50
+ const run = async () => {
51
+ let attempt = 0;
52
+ while (!closed) {
53
+ handlers.onStateChange?.(attempt === 0 ? "connecting" : "reconnecting");
54
+ controller = new AbortController();
55
+ try {
56
+ const res = await this.doFetch(
57
+ `${this.base}/stream?edge=${encodeURIComponent(edge)}&session=${encodeURIComponent(session)}`,
58
+ { headers: { ...this.headers, accept: "text/event-stream" }, signal: controller.signal }
59
+ );
60
+ if (!res.ok || !res.body) throw new Error(`http_${res.status}`);
61
+ handlers.onStateChange?.("open");
62
+ attempt = 0;
63
+ await this.consume(res.body, handlers);
64
+ } catch {
65
+ }
66
+ if (closed) break;
67
+ attempt++;
68
+ await new Promise((r) => setTimeout(r, Math.min(1e3 * attempt, 5e3)));
69
+ }
70
+ handlers.onStateChange?.("closed");
71
+ };
72
+ void run();
73
+ return {
74
+ close: () => {
75
+ closed = true;
76
+ controller?.abort();
77
+ }
78
+ };
79
+ }
80
+ /** Minimal SSE parser: `event:` + `data:` lines, events split on blank lines. */
81
+ async consume(body, handlers) {
82
+ const reader = body.getReader();
83
+ const decoder = new TextDecoder();
84
+ let buf = "";
85
+ for (; ; ) {
86
+ const { done, value } = await reader.read();
87
+ if (done) break;
88
+ buf += decoder.decode(value, { stream: true });
89
+ let sep;
90
+ while ((sep = buf.indexOf("\n\n")) !== -1) {
91
+ const chunk = buf.slice(0, sep);
92
+ buf = buf.slice(sep + 2);
93
+ let event = "message";
94
+ const data = [];
95
+ for (const line of chunk.split("\n")) {
96
+ if (line.startsWith("event:")) event = line.slice(6).trim();
97
+ else if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
98
+ }
99
+ if (data.length === 0) continue;
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(data.join("\n"));
103
+ } catch {
104
+ continue;
105
+ }
106
+ if (event === "frame") {
107
+ const c = parsed.content;
108
+ if (typeof c === "string") handlers.onFrame?.(c);
109
+ } else if (event === "hello") {
110
+ handlers.onHello?.(parsed);
111
+ } else if (event === "ping") {
112
+ handlers.onPing?.(parsed);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ };
118
+
119
+ // src/frame-accumulator.ts
120
+ var RULE = /^[─━-]{10,}\s*$/;
121
+ var SPIN = /^[✻✶✳·•*]\s/;
122
+ function splitFooter(lines) {
123
+ let inp = -1;
124
+ for (let i = lines.length - 1; i >= 0 && i >= lines.length - 8; i--) {
125
+ const t = lines[i].replace(/\s+$/, "");
126
+ if (t.charCodeAt(0) === 10095 || t[0] === ">") {
127
+ inp = i;
128
+ break;
129
+ }
130
+ }
131
+ let start;
132
+ if (inp === -1) start = Math.max(0, lines.length - 3);
133
+ else start = inp > 0 && RULE.test(lines[inp - 1].trim()) ? inp - 1 : inp;
134
+ while (start > 0 && SPIN.test(lines[start - 1])) start--;
135
+ return { body: lines.slice(0, start), footer: lines.slice(start) };
136
+ }
137
+ function mergeOverlap(hist, body) {
138
+ const max = Math.min(hist.length, body.length);
139
+ for (let k = max; k > 0; k--) {
140
+ let ok = true;
141
+ for (let i = 0; i < k; i++) {
142
+ if (hist[hist.length - k + i] !== body[i]) {
143
+ ok = false;
144
+ break;
145
+ }
146
+ }
147
+ if (ok) return hist.concat(body.slice(k));
148
+ }
149
+ return hist.concat(body);
150
+ }
151
+ var FrameAccumulator = class {
152
+ constructor(maxHistory = 5e3) {
153
+ this.maxHistory = maxHistory;
154
+ }
155
+ maxHistory;
156
+ history = [];
157
+ footer = [];
158
+ /** Feed a full frame snapshot; returns the updated view. */
159
+ feed(content) {
160
+ const lines = content.replace(/\s+$/, "").split("\n");
161
+ const { body, footer } = splitFooter(lines);
162
+ this.history = mergeOverlap(this.history, body);
163
+ if (this.history.length > this.maxHistory) {
164
+ this.history = this.history.slice(-this.maxHistory);
165
+ }
166
+ this.footer = footer;
167
+ return this.view;
168
+ }
169
+ get view() {
170
+ return { history: this.history, footer: this.footer };
171
+ }
172
+ /** Full rendered text (history + live footer). */
173
+ get text() {
174
+ return this.history.concat(this.footer).join("\n");
175
+ }
176
+ reset() {
177
+ this.history = [];
178
+ this.footer = [];
179
+ }
180
+ };
181
+ var NAV_KEYS = [
182
+ { key: "Escape", label: "Esc", title: "Escape" },
183
+ { key: "Up", label: "\u2191", title: "Pil op" },
184
+ { key: "Down", label: "\u2193", title: "Pil ned" },
185
+ { key: "Left", label: "\u2190", title: "Pil venstre" },
186
+ { key: "Right", label: "\u2192", title: "Pil h\xF8jre" },
187
+ { key: "Enter", label: "\u23CE", title: "Enter" }
188
+ ];
189
+ var STYLE_ID = "broberg-seti-chat-style";
190
+ var CSS = `
191
+ .seti-chat{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--seti-bg,#0b0e14);color:var(--seti-fg,#d7dce5);border:1px solid var(--seti-edge,#1e2430);border-radius:var(--seti-radius,12px);overflow:hidden}
192
+ .seti-chat__header{display:flex;align-items:center;gap:.55rem;padding:.6rem .9rem;background:var(--seti-panel,#11151f);border-bottom:1px solid var(--seti-edge,#1e2430);font-size:.82rem}
193
+ .seti-chat__dot{width:9px;height:9px;border-radius:50%;background:var(--seti-dim,#8a93a6);transition:background .2s;flex:none}
194
+ .seti-chat__dot.is-on{background:var(--seti-accent,#34d399);animation:seti-pulse 2s infinite}
195
+ .seti-chat__dot.is-bad{background:var(--seti-bad,#f87171)}
196
+ @keyframes seti-pulse{0%{box-shadow:0 0 0 0 rgba(52,211,153,.5)}70%{box-shadow:0 0 0 7px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}}
197
+ .seti-chat__meta{color:var(--seti-dim,#8a93a6);font-family:var(--seti-mono,ui-monospace,Menlo,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
198
+ .seti-chat__screen{flex:1;margin:0;padding:.9rem 1rem;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:12.5px;line-height:1.45;-webkit-overflow-scrolling:touch}
199
+ .seti-chat__screen.is-empty{color:var(--seti-dim,#8a93a6)}
200
+ .seti-chat__navkeys{display:flex;gap:.4rem;padding:.45rem .65rem .2rem;background:var(--seti-panel,#11151f);flex-wrap:wrap;border-top:1px solid var(--seti-edge,#1e2430)}
201
+ .seti-chat__navkeys button{padding:.45rem .7rem;background:var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);font-size:13px;min-width:2.6rem;min-height:2.2rem;font-weight:600;border:0;border-radius:8px;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}
202
+ .seti-chat__navkeys button:hover{filter:brightness(1.25)}
203
+ .seti-chat__navkeys button:active{transform:translateY(1px) scale(.98)}
204
+ .seti-chat__navkeys button:disabled{opacity:.5;cursor:not-allowed}
205
+ .seti-chat__form{display:flex;gap:.5rem;padding:.6rem;background:var(--seti-panel,#11151f);border-top:1px solid var(--seti-edge,#1e2430)}
206
+ .seti-chat__input{flex:1;min-width:0;background:var(--seti-bg,#0b0e14);border:1px solid var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);border-radius:10px;padding:.65rem .85rem;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:16px;outline:none}
207
+ .seti-chat__input:focus{border-color:var(--seti-accent,#34d399);box-shadow:0 0 0 3px rgba(52,211,153,.15)}
208
+ .seti-chat__send{background:var(--seti-accent,#34d399);color:#07120d;border:0;border-radius:10px;padding:.65rem 1.05rem;font-weight:650;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}
209
+ .seti-chat__send:hover{filter:brightness(1.08)}
210
+ .seti-chat__send:active{transform:translateY(1px) scale(.99)}
211
+ .seti-chat__send:disabled{opacity:.5;cursor:not-allowed}
212
+ .seti-chat__send.is-sending{background:var(--seti-warn,#fbbf24);color:#2a1d00}
213
+ `;
214
+ function ensureStyle() {
215
+ if (typeof document === "undefined") return;
216
+ if (document.getElementById(STYLE_ID)) return;
217
+ const el = document.createElement("style");
218
+ el.id = STYLE_ID;
219
+ el.textContent = CSS;
220
+ document.head.appendChild(el);
221
+ }
222
+ function SetiChat(props) {
223
+ const client = hooks.useMemo(
224
+ () => props.client ?? new SetiClient({ baseUrl: props.baseUrl ?? "/api/seti" }),
225
+ [props.client, props.baseUrl]
226
+ );
227
+ const [text, setText] = hooks.useState("");
228
+ const [sending, setSending] = hooks.useState(false);
229
+ const [screenText, setScreenText] = hooks.useState("");
230
+ const [edgeOn, setEdgeOn] = hooks.useState(null);
231
+ const [streamState, setStreamState] = hooks.useState("connecting");
232
+ const [notice, setNotice] = hooks.useState(null);
233
+ const screenRef = hooks.useRef(null);
234
+ hooks.useEffect(ensureStyle, []);
235
+ hooks.useEffect(() => {
236
+ const acc = new FrameAccumulator();
237
+ setScreenText("");
238
+ setNotice(null);
239
+ const atBottom = () => {
240
+ const s = screenRef.current;
241
+ return !s || s.scrollHeight - s.scrollTop - s.clientHeight < 40;
242
+ };
243
+ const handle = client.openStream(props.edge, props.session, {
244
+ onHello: (h) => setEdgeOn(h.edgeConnected),
245
+ onPing: (p) => setEdgeOn(p.edgeConnected),
246
+ onStateChange: setStreamState,
247
+ onFrame: (content) => {
248
+ const stick = atBottom();
249
+ acc.feed(content);
250
+ setScreenText(acc.text);
251
+ if (stick) {
252
+ requestAnimationFrame(() => {
253
+ const s = screenRef.current;
254
+ if (s) s.scrollTop = s.scrollHeight;
255
+ });
256
+ }
257
+ }
258
+ });
259
+ return () => handle.close();
260
+ }, [client, props.edge, props.session]);
261
+ const meta = streamState === "open" ? edgeOn === false ? `${props.edge} \xB7 edge offline` : `${props.edge} \xB7 ${props.session}` : streamState === "closed" ? "lukket" : "forbinder\u2026";
262
+ const dotClass = "seti-chat__dot" + (streamState === "open" && edgeOn ? " is-on" : edgeOn === false ? " is-bad" : "");
263
+ const submit = async (ev) => {
264
+ ev.preventDefault();
265
+ if (!text.trim() || sending) return;
266
+ setSending(true);
267
+ setNotice(null);
268
+ const res = await client.sendText(props.edge, props.session, text);
269
+ if (res.ok) {
270
+ setText("");
271
+ } else {
272
+ setNotice("Ikke leveret \u2014 din tekst er bevaret");
273
+ }
274
+ setSending(false);
275
+ };
276
+ const pressKey = async (key) => {
277
+ await client.sendKey(props.edge, props.session, key);
278
+ };
279
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "seti-chat" + (props.class ? ` ${props.class}` : ""), "data-testid": "seti-chat-root", children: [
280
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "seti-chat__header", "data-testid": "seti-chat-header", children: [
281
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: dotClass, "data-testid": "seti-chat-status-dot" }),
282
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: "seti-chat__meta", "data-testid": "seti-chat-meta", children: notice ?? meta })
283
+ ] }),
284
+ /* @__PURE__ */ jsxRuntime.jsx(
285
+ "pre",
286
+ {
287
+ ref: screenRef,
288
+ class: "seti-chat__screen" + (screenText ? "" : " is-empty"),
289
+ "data-testid": "seti-chat-screen",
290
+ children: screenText || "Venter p\xE5 den f\xF8rste frame fra edgen\u2026"
291
+ }
292
+ ),
293
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "seti-chat__navkeys", "data-testid": "seti-chat-navkeys", children: NAV_KEYS.map((k) => /* @__PURE__ */ jsxRuntime.jsx(
294
+ "button",
295
+ {
296
+ type: "button",
297
+ title: k.title,
298
+ "data-testid": `seti-chat-key-${k.key.toLowerCase()}`,
299
+ onClick: () => void pressKey(k.key),
300
+ children: k.label
301
+ },
302
+ k.key
303
+ )) }),
304
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { class: "seti-chat__form", "data-testid": "seti-chat-form", onSubmit: submit, children: [
305
+ /* @__PURE__ */ jsxRuntime.jsx(
306
+ "input",
307
+ {
308
+ class: "seti-chat__input",
309
+ "data-testid": "seti-chat-input",
310
+ value: text,
311
+ placeholder: props.placeholder ?? "Skriv til sessionen\u2026",
312
+ autocomplete: "off",
313
+ onInput: (e) => setText(e.target.value)
314
+ }
315
+ ),
316
+ /* @__PURE__ */ jsxRuntime.jsx(
317
+ "button",
318
+ {
319
+ type: "submit",
320
+ class: "seti-chat__send" + (sending ? " is-sending" : ""),
321
+ "data-testid": "seti-chat-send",
322
+ disabled: sending,
323
+ children: sending ? "\u2026" : "Send"
324
+ }
325
+ )
326
+ ] })
327
+ ] });
328
+ }
329
+
330
+ exports.SetiChat = SetiChat;
331
+ exports.SetiClient = SetiClient;
332
+ //# sourceMappingURL=preact.cjs.map
333
+ //# sourceMappingURL=preact.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/frame-accumulator.ts","../src/preact.tsx"],"names":["useMemo","useState","useRef","useEffect","jsxs","jsx"],"mappings":"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","file":"preact.cjs","sourcesContent":["import type {\n SetiInputResult,\n SetiKey,\n SetiRoster,\n SetiStreamHandle,\n SetiStreamHandlers,\n} from \"./types\";\n\nexport interface SetiClientOptions {\n /**\n * Base URL of the SETI surface. In a browser this is the host app's proxy\n * mount (same-origin, e.g. \"/api/seti\" via @broberg/seti-server). Server-side\n * it can be the cloud directly (\"https://buddycloud.cc/api/seti/v1\") together\n * with `token`.\n */\n baseUrl: string;\n /** Bearer token — only for server-side/direct use. NEVER ship to a browser. */\n token?: string;\n /** Override fetch (tests / custom runtimes). */\n fetch?: typeof fetch;\n}\n\n/**\n * Typed client for the SETI API (roster + SSE stream + input). One code path\n * for browser and server: fetch-based SSE with automatic reconnect (1s → 5s\n * backoff), so bearer headers work everywhere and no EventSource is needed.\n */\nexport class SetiClient {\n private readonly base: string;\n private readonly headers: Record<string, string>;\n private readonly doFetch: typeof fetch;\n\n constructor(opts: SetiClientOptions) {\n this.base = opts.baseUrl.replace(/\\/$/, \"\");\n this.headers = opts.token ? { Authorization: `Bearer ${opts.token}` } : {};\n this.doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n async listSessions(): Promise<SetiRoster> {\n const res = await this.doFetch(`${this.base}/sessions`, { headers: this.headers });\n if (!res.ok) return { edges: [], error: `http_${res.status}` };\n return (await res.json()) as SetiRoster;\n }\n\n async sendText(edge: string, session: string, text: string): Promise<SetiInputResult> {\n return this.input({ edge, session, text });\n }\n\n async sendKey(edge: string, session: string, key: SetiKey): Promise<SetiInputResult> {\n return this.input({ edge, session, key });\n }\n\n private async input(body: {\n edge: string;\n session: string;\n text?: string;\n key?: SetiKey;\n }): Promise<SetiInputResult> {\n try {\n const res = await this.doFetch(`${this.base}/input`, {\n method: \"POST\",\n headers: { ...this.headers, \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(8000),\n });\n const json = (await res.json().catch(() => ({}))) as Partial<SetiInputResult>;\n return { ok: !!json.ok, edgeConnected: !!json.edgeConnected, error: json.error };\n } catch (err) {\n return { ok: false, edgeConnected: false, error: err instanceof Error ? err.message : \"send_failed\" };\n }\n }\n\n /**\n * Open the live frame stream for one edge session. Reconnects automatically\n * until `close()` is called.\n */\n openStream(edge: string, session: string, handlers: SetiStreamHandlers): SetiStreamHandle {\n let closed = false;\n let controller: AbortController | null = null;\n\n const run = async (): Promise<void> => {\n let attempt = 0;\n while (!closed) {\n handlers.onStateChange?.(attempt === 0 ? \"connecting\" : \"reconnecting\");\n controller = new AbortController();\n try {\n const res = await this.doFetch(\n `${this.base}/stream?edge=${encodeURIComponent(edge)}&session=${encodeURIComponent(session)}`,\n { headers: { ...this.headers, accept: \"text/event-stream\" }, signal: controller.signal },\n );\n if (!res.ok || !res.body) throw new Error(`http_${res.status}`);\n handlers.onStateChange?.(\"open\");\n attempt = 0;\n await this.consume(res.body, handlers);\n } catch {\n /* fall through to reconnect */\n }\n if (closed) break;\n attempt++;\n await new Promise((r) => setTimeout(r, Math.min(1000 * attempt, 5000)));\n }\n handlers.onStateChange?.(\"closed\");\n };\n void run();\n\n return {\n close: () => {\n closed = true;\n controller?.abort();\n },\n };\n }\n\n /** Minimal SSE parser: `event:` + `data:` lines, events split on blank lines. */\n private async consume(body: ReadableStream<Uint8Array>, handlers: SetiStreamHandlers): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buf = \"\";\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n let sep: number;\n while ((sep = buf.indexOf(\"\\n\\n\")) !== -1) {\n const chunk = buf.slice(0, sep);\n buf = buf.slice(sep + 2);\n let event = \"message\";\n const data: string[] = [];\n for (const line of chunk.split(\"\\n\")) {\n if (line.startsWith(\"event:\")) event = line.slice(6).trim();\n else if (line.startsWith(\"data:\")) data.push(line.slice(5).trimStart());\n }\n if (data.length === 0) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(data.join(\"\\n\"));\n } catch {\n continue;\n }\n if (event === \"frame\") {\n const c = (parsed as { content?: unknown }).content;\n if (typeof c === \"string\") handlers.onFrame?.(c);\n } else if (event === \"hello\") {\n handlers.onHello?.(parsed as { edge: string; session: string; edgeConnected: boolean });\n } else if (event === \"ping\") {\n handlers.onPing?.(parsed as { edgeConnected: boolean });\n }\n }\n }\n }\n}\n","/**\n * FrameAccumulator — the F071 scrollback engine as a tested pure class.\n *\n * cc runs on the terminal alt-screen, so tmux has no scrollback: every frame is\n * a full snapshot of the visible window. The accumulator splits each frame into\n * a volatile footer (cc's input box + statusline + spinner, rendered live) and\n * a body, then overlap-merges successive bodies so the dialogue that scrolls\n * off the top is retained.\n */\nexport interface FrameView {\n /** Accumulated dialogue lines (grows from the first fed frame). */\n history: string[];\n /** The volatile tail of the latest frame (input box / statusline / spinner). */\n footer: string[];\n}\n\nconst RULE = /^[─━-]{10,}\\s*$/;\nconst SPIN = /^[✻✶✳·•*]\\s/;\n\nexport function splitFooter(lines: string[]): { body: string[]; footer: string[] } {\n let inp = -1;\n for (let i = lines.length - 1; i >= 0 && i >= lines.length - 8; i--) {\n const t = lines[i].replace(/\\s+$/, \"\");\n if (t.charCodeAt(0) === 0x276f || t[0] === \">\") {\n inp = i;\n break;\n }\n }\n let start: number;\n if (inp === -1) start = Math.max(0, lines.length - 3);\n else start = inp > 0 && RULE.test(lines[inp - 1].trim()) ? inp - 1 : inp;\n while (start > 0 && SPIN.test(lines[start - 1])) start--;\n return { body: lines.slice(0, start), footer: lines.slice(start) };\n}\n\nexport function mergeOverlap(hist: string[], body: string[]): string[] {\n const max = Math.min(hist.length, body.length);\n for (let k = max; k > 0; k--) {\n let ok = true;\n for (let i = 0; i < k; i++) {\n if (hist[hist.length - k + i] !== body[i]) {\n ok = false;\n break;\n }\n }\n if (ok) return hist.concat(body.slice(k));\n }\n return hist.concat(body);\n}\n\nexport class FrameAccumulator {\n private history: string[] = [];\n private footer: string[] = [];\n\n constructor(private readonly maxHistory = 5000) {}\n\n /** Feed a full frame snapshot; returns the updated view. */\n feed(content: string): FrameView {\n const lines = content.replace(/\\s+$/, \"\").split(\"\\n\");\n const { body, footer } = splitFooter(lines);\n this.history = mergeOverlap(this.history, body);\n if (this.history.length > this.maxHistory) {\n this.history = this.history.slice(-this.maxHistory);\n }\n this.footer = footer;\n return this.view;\n }\n\n get view(): FrameView {\n return { history: this.history, footer: this.footer };\n }\n\n /** Full rendered text (history + live footer). */\n get text(): string {\n return this.history.concat(this.footer).join(\"\\n\");\n }\n\n reset(): void {\n this.history = [];\n this.footer = [];\n }\n}\n","import { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport { SetiClient } from \"./client\";\nimport { FrameAccumulator } from \"./frame-accumulator\";\nimport type { SetiKey, SetiStreamState } from \"./types\";\n\n/**\n * <SetiChat> — the complete mobile-first SET/SETI live chat surface:\n * status header, accumulated screen (FrameAccumulator), nav-keys bar\n * (Esc/↑/↓/←/→/⏎) and a text input with delivery feedback (text is preserved\n * when delivery fails).\n *\n * Self-contained styles, themeable via CSS vars (set them on a parent):\n * --seti-bg, --seti-panel, --seti-edge, --seti-fg, --seti-dim,\n * --seti-accent, --seti-warn, --seti-bad, --seti-mono, --seti-radius\n *\n * Every interactive element carries data-testid=\"seti-chat-*\".\n */\nexport interface SetiChatProps {\n /** The host app's proxy mount, e.g. \"/api/seti\". Ignored if `client` is given. */\n baseUrl?: string;\n client?: SetiClient;\n edge: string;\n session: string;\n /** Extra class on the root (sizing/layout belongs to the host). */\n class?: string;\n /** Placeholder for the text input. Default: \"Skriv til sessionen…\" */\n placeholder?: string;\n}\n\nconst NAV_KEYS: Array<{ key: SetiKey; label: string; title: string }> = [\n { key: \"Escape\", label: \"Esc\", title: \"Escape\" },\n { key: \"Up\", label: \"↑\", title: \"Pil op\" },\n { key: \"Down\", label: \"↓\", title: \"Pil ned\" },\n { key: \"Left\", label: \"←\", title: \"Pil venstre\" },\n { key: \"Right\", label: \"→\", title: \"Pil højre\" },\n { key: \"Enter\", label: \"⏎\", title: \"Enter\" },\n];\n\nconst STYLE_ID = \"broberg-seti-chat-style\";\nconst CSS = `\n.seti-chat{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--seti-bg,#0b0e14);color:var(--seti-fg,#d7dce5);border:1px solid var(--seti-edge,#1e2430);border-radius:var(--seti-radius,12px);overflow:hidden}\n.seti-chat__header{display:flex;align-items:center;gap:.55rem;padding:.6rem .9rem;background:var(--seti-panel,#11151f);border-bottom:1px solid var(--seti-edge,#1e2430);font-size:.82rem}\n.seti-chat__dot{width:9px;height:9px;border-radius:50%;background:var(--seti-dim,#8a93a6);transition:background .2s;flex:none}\n.seti-chat__dot.is-on{background:var(--seti-accent,#34d399);animation:seti-pulse 2s infinite}\n.seti-chat__dot.is-bad{background:var(--seti-bad,#f87171)}\n@keyframes seti-pulse{0%{box-shadow:0 0 0 0 rgba(52,211,153,.5)}70%{box-shadow:0 0 0 7px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}}\n.seti-chat__meta{color:var(--seti-dim,#8a93a6);font-family:var(--seti-mono,ui-monospace,Menlo,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.seti-chat__screen{flex:1;margin:0;padding:.9rem 1rem;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:12.5px;line-height:1.45;-webkit-overflow-scrolling:touch}\n.seti-chat__screen.is-empty{color:var(--seti-dim,#8a93a6)}\n.seti-chat__navkeys{display:flex;gap:.4rem;padding:.45rem .65rem .2rem;background:var(--seti-panel,#11151f);flex-wrap:wrap;border-top:1px solid var(--seti-edge,#1e2430)}\n.seti-chat__navkeys button{padding:.45rem .7rem;background:var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);font-size:13px;min-width:2.6rem;min-height:2.2rem;font-weight:600;border:0;border-radius:8px;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}\n.seti-chat__navkeys button:hover{filter:brightness(1.25)}\n.seti-chat__navkeys button:active{transform:translateY(1px) scale(.98)}\n.seti-chat__navkeys button:disabled{opacity:.5;cursor:not-allowed}\n.seti-chat__form{display:flex;gap:.5rem;padding:.6rem;background:var(--seti-panel,#11151f);border-top:1px solid var(--seti-edge,#1e2430)}\n.seti-chat__input{flex:1;min-width:0;background:var(--seti-bg,#0b0e14);border:1px solid var(--seti-edge,#1e2430);color:var(--seti-fg,#d7dce5);border-radius:10px;padding:.65rem .85rem;font-family:var(--seti-mono,ui-monospace,Menlo,monospace);font-size:16px;outline:none}\n.seti-chat__input:focus{border-color:var(--seti-accent,#34d399);box-shadow:0 0 0 3px rgba(52,211,153,.15)}\n.seti-chat__send{background:var(--seti-accent,#34d399);color:#07120d;border:0;border-radius:10px;padding:.65rem 1.05rem;font-weight:650;cursor:pointer;transition:transform .06s,background .15s,opacity .15s}\n.seti-chat__send:hover{filter:brightness(1.08)}\n.seti-chat__send:active{transform:translateY(1px) scale(.99)}\n.seti-chat__send:disabled{opacity:.5;cursor:not-allowed}\n.seti-chat__send.is-sending{background:var(--seti-warn,#fbbf24);color:#2a1d00}\n`;\n\nfunction ensureStyle(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = CSS;\n document.head.appendChild(el);\n}\n\nexport function SetiChat(props: SetiChatProps) {\n const client = useMemo(\n () => props.client ?? new SetiClient({ baseUrl: props.baseUrl ?? \"/api/seti\" }),\n [props.client, props.baseUrl],\n );\n const [text, setText] = useState(\"\");\n const [sending, setSending] = useState(false);\n const [screenText, setScreenText] = useState(\"\");\n const [edgeOn, setEdgeOn] = useState<boolean | null>(null);\n const [streamState, setStreamState] = useState<SetiStreamState>(\"connecting\");\n const [notice, setNotice] = useState<string | null>(null);\n const screenRef = useRef<HTMLPreElement>(null);\n\n useEffect(ensureStyle, []);\n\n useEffect(() => {\n const acc = new FrameAccumulator();\n setScreenText(\"\");\n setNotice(null);\n const atBottom = (): boolean => {\n const s = screenRef.current;\n return !s || s.scrollHeight - s.scrollTop - s.clientHeight < 40;\n };\n const handle = client.openStream(props.edge, props.session, {\n onHello: (h) => setEdgeOn(h.edgeConnected),\n onPing: (p) => setEdgeOn(p.edgeConnected),\n onStateChange: setStreamState,\n onFrame: (content) => {\n const stick = atBottom();\n acc.feed(content);\n setScreenText(acc.text);\n if (stick) {\n requestAnimationFrame(() => {\n const s = screenRef.current;\n if (s) s.scrollTop = s.scrollHeight;\n });\n }\n },\n });\n return () => handle.close();\n }, [client, props.edge, props.session]);\n\n const meta =\n streamState === \"open\"\n ? edgeOn === false\n ? `${props.edge} · edge offline`\n : `${props.edge} · ${props.session}`\n : streamState === \"closed\"\n ? \"lukket\"\n : \"forbinder…\";\n const dotClass =\n \"seti-chat__dot\" + (streamState === \"open\" && edgeOn ? \" is-on\" : edgeOn === false ? \" is-bad\" : \"\");\n\n const submit = async (ev: Event) => {\n ev.preventDefault();\n if (!text.trim() || sending) return;\n setSending(true);\n setNotice(null);\n const res = await client.sendText(props.edge, props.session, text);\n if (res.ok) {\n setText(\"\"); // only clear when actually delivered — text survives failures\n } else {\n setNotice(\"Ikke leveret — din tekst er bevaret\");\n }\n setSending(false);\n };\n\n const pressKey = async (key: SetiKey) => {\n await client.sendKey(props.edge, props.session, key);\n };\n\n return (\n <div class={\"seti-chat\" + (props.class ? ` ${props.class}` : \"\")} data-testid=\"seti-chat-root\">\n <div class=\"seti-chat__header\" data-testid=\"seti-chat-header\">\n <span class={dotClass} data-testid=\"seti-chat-status-dot\" />\n <span class=\"seti-chat__meta\" data-testid=\"seti-chat-meta\">\n {notice ?? meta}\n </span>\n </div>\n <pre\n ref={screenRef}\n class={\"seti-chat__screen\" + (screenText ? \"\" : \" is-empty\")}\n data-testid=\"seti-chat-screen\"\n >\n {screenText || \"Venter på den første frame fra edgen…\"}\n </pre>\n <div class=\"seti-chat__navkeys\" data-testid=\"seti-chat-navkeys\">\n {NAV_KEYS.map((k) => (\n <button\n key={k.key}\n type=\"button\"\n title={k.title}\n data-testid={`seti-chat-key-${k.key.toLowerCase()}`}\n onClick={() => void pressKey(k.key)}\n >\n {k.label}\n </button>\n ))}\n </div>\n <form class=\"seti-chat__form\" data-testid=\"seti-chat-form\" onSubmit={submit}>\n <input\n class=\"seti-chat__input\"\n data-testid=\"seti-chat-input\"\n value={text}\n placeholder={props.placeholder ?? \"Skriv til sessionen…\"}\n autocomplete=\"off\"\n onInput={(e) => setText((e.target as HTMLInputElement).value)}\n />\n <button\n type=\"submit\"\n class={\"seti-chat__send\" + (sending ? \" is-sending\" : \"\")}\n data-testid=\"seti-chat-send\"\n disabled={sending}\n >\n {sending ? \"…\" : \"Send\"}\n </button>\n </form>\n </div>\n );\n}\n\nexport { SetiClient } from \"./client\";\nexport type { SetiKey } from \"./types\";\n"]}
@@ -0,0 +1,30 @@
1
+ import * as preact from 'preact';
2
+ import { a as SetiClient } from './client-BIxdDJYz.cjs';
3
+ export { e as SetiKey } from './client-BIxdDJYz.cjs';
4
+
5
+ /**
6
+ * <SetiChat> — the complete mobile-first SET/SETI live chat surface:
7
+ * status header, accumulated screen (FrameAccumulator), nav-keys bar
8
+ * (Esc/↑/↓/←/→/⏎) and a text input with delivery feedback (text is preserved
9
+ * when delivery fails).
10
+ *
11
+ * Self-contained styles, themeable via CSS vars (set them on a parent):
12
+ * --seti-bg, --seti-panel, --seti-edge, --seti-fg, --seti-dim,
13
+ * --seti-accent, --seti-warn, --seti-bad, --seti-mono, --seti-radius
14
+ *
15
+ * Every interactive element carries data-testid="seti-chat-*".
16
+ */
17
+ interface SetiChatProps {
18
+ /** The host app's proxy mount, e.g. "/api/seti". Ignored if `client` is given. */
19
+ baseUrl?: string;
20
+ client?: SetiClient;
21
+ edge: string;
22
+ session: string;
23
+ /** Extra class on the root (sizing/layout belongs to the host). */
24
+ class?: string;
25
+ /** Placeholder for the text input. Default: "Skriv til sessionen…" */
26
+ placeholder?: string;
27
+ }
28
+ declare function SetiChat(props: SetiChatProps): preact.JSX.Element;
29
+
30
+ export { SetiChat, type SetiChatProps, SetiClient };
@@ -0,0 +1,30 @@
1
+ import * as preact from 'preact';
2
+ import { a as SetiClient } from './client-BIxdDJYz.js';
3
+ export { e as SetiKey } from './client-BIxdDJYz.js';
4
+
5
+ /**
6
+ * <SetiChat> — the complete mobile-first SET/SETI live chat surface:
7
+ * status header, accumulated screen (FrameAccumulator), nav-keys bar
8
+ * (Esc/↑/↓/←/→/⏎) and a text input with delivery feedback (text is preserved
9
+ * when delivery fails).
10
+ *
11
+ * Self-contained styles, themeable via CSS vars (set them on a parent):
12
+ * --seti-bg, --seti-panel, --seti-edge, --seti-fg, --seti-dim,
13
+ * --seti-accent, --seti-warn, --seti-bad, --seti-mono, --seti-radius
14
+ *
15
+ * Every interactive element carries data-testid="seti-chat-*".
16
+ */
17
+ interface SetiChatProps {
18
+ /** The host app's proxy mount, e.g. "/api/seti". Ignored if `client` is given. */
19
+ baseUrl?: string;
20
+ client?: SetiClient;
21
+ edge: string;
22
+ session: string;
23
+ /** Extra class on the root (sizing/layout belongs to the host). */
24
+ class?: string;
25
+ /** Placeholder for the text input. Default: "Skriv til sessionen…" */
26
+ placeholder?: string;
27
+ }
28
+ declare function SetiChat(props: SetiChatProps): preact.JSX.Element;
29
+
30
+ export { SetiChat, type SetiChatProps, SetiClient };