@broberg/seti-client 0.1.0 → 0.1.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.
@@ -111,7 +111,15 @@ var SetiClient = class {
111
111
 
112
112
  // src/frame-accumulator.ts
113
113
  var RULE = /^[─━-]{10,}\s*$/;
114
- var SPIN = /^[✻✶✳·•*]\s/;
114
+ var SPIN = /^[✻✶✳✢✽·•*]\s/;
115
+ var BOX = /^[\s─━│┌┐└┘├┤┬┴┼╭╮╯╰╠╣╦╩╬═╳]+$/;
116
+ function substantial(line) {
117
+ const t = line.trim();
118
+ if (t.length < 4) return false;
119
+ if (SPIN.test(line)) return false;
120
+ if (BOX.test(t)) return false;
121
+ return true;
122
+ }
115
123
  function splitFooter(lines) {
116
124
  let inp = -1;
117
125
  for (let i = lines.length - 1; i >= 0 && i >= lines.length - 8; i--) {
@@ -127,17 +135,45 @@ function splitFooter(lines) {
127
135
  while (start > 0 && SPIN.test(lines[start - 1])) start--;
128
136
  return { body: lines.slice(0, start), footer: lines.slice(start) };
129
137
  }
138
+ function norm(line) {
139
+ const t = line.replace(/\s+$/, "");
140
+ if (/^[✻✶✳✢✽·•]/.test(t)) return `\u2022${t.slice(1).replace(/\d+/g, "#")}`;
141
+ return t;
142
+ }
130
143
  function mergeOverlap(hist, body) {
131
144
  const max = Math.min(hist.length, body.length);
132
145
  for (let k = max; k > 0; k--) {
133
146
  let ok = true;
134
147
  for (let i = 0; i < k; i++) {
135
- if (hist[hist.length - k + i] !== body[i]) {
148
+ if (norm(hist[hist.length - k + i]) !== norm(body[i])) {
136
149
  ok = false;
137
150
  break;
138
151
  }
139
152
  }
140
- if (ok) return hist.concat(body.slice(k));
153
+ if (ok) return hist.slice(0, hist.length - k).concat(body);
154
+ }
155
+ if (body.length > 0) {
156
+ const windowStart = Math.max(0, hist.length - body.length * 2);
157
+ for (let s = hist.length - body.length; s >= windowStart; s--) {
158
+ let ok = true;
159
+ for (let i = 0; i < body.length; i++) {
160
+ if (norm(hist[s + i]) !== norm(body[i])) {
161
+ ok = false;
162
+ break;
163
+ }
164
+ }
165
+ if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
166
+ }
167
+ }
168
+ const anchor = body.findIndex(substantial);
169
+ if (anchor !== -1) {
170
+ const key = norm(body[anchor]);
171
+ const floor = Math.max(0, hist.length - 400);
172
+ for (let p = hist.length - 1; p >= floor; p--) {
173
+ if (norm(hist[p]) !== key) continue;
174
+ const start = p - anchor;
175
+ if (start >= 0) return hist.slice(0, start).concat(body);
176
+ }
141
177
  }
142
178
  return hist.concat(body);
143
179
  }
@@ -173,5 +209,5 @@ var FrameAccumulator = class {
173
209
  };
174
210
 
175
211
  export { FrameAccumulator, SetiClient, mergeOverlap, splitFooter };
176
- //# sourceMappingURL=chunk-VWTGZF3D.js.map
177
- //# sourceMappingURL=chunk-VWTGZF3D.js.map
212
+ //# sourceMappingURL=chunk-OJUDFIKW.js.map
213
+ //# sourceMappingURL=chunk-OJUDFIKW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/frame-accumulator.ts"],"names":[],"mappings":"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","file":"chunk-OJUDFIKW.js","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// Pure box-drawing / whitespace — never a reliable merge anchor.\nconst BOX = /^[\\s─━│┌┐└┘├┤┬┴┼╭╮╯╰╠╣╦╩╬═╳]+$/;\n\n/**\n * A line stable enough to align two frames on. Excludes blanks, short\n * fragments, horizontal rules / box-drawing, and the volatile spinner line —\n * i.e. the things that get redrawn between snapshots.\n */\nfunction substantial(line: string): boolean {\n const t = line.trim();\n if (t.length < 4) return false;\n if (SPIN.test(line)) return false;\n if (BOX.test(t)) return false;\n return true;\n}\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\n/**\n * Comparison key for overlap matching. cc updates lines IN PLACE while a turn\n * runs (spinner glyph rotates, \"Worked for Xs\" counts up, token counters tick)\n * — exact equality then finds no overlap and the whole frame gets re-appended\n * as a duplicate block (F078). Normalizing the volatile parts (one spinner\n * glyph, digit runs masked) makes those lines compare equal, and the merge\n * then REFRESHES them with the new frame's text.\n */\nfunction norm(line: string): string {\n const t = line.replace(/\\s+$/, \"\");\n // Only status lines (spinner-prefixed) get digit masking — masking digits in\n // ordinary output would make e.g. \"line 1\" equal \"line 2\" and eat real lines.\n if (/^[✻✶✳✢✽·•]/.test(t)) return `•${t.slice(1).replace(/\\d+/g, \"#\")}`;\n return t;\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 (norm(hist[hist.length - k + i]) !== norm(body[i])) {\n ok = false;\n break;\n }\n }\n // Take the NEW frame's lines for the overlapping segment so in-place\n // updates (timers, spinners) refresh instead of going stale.\n if (ok) return hist.slice(0, hist.length - k).concat(body);\n }\n // No suffix/prefix overlap. If the body is already CONTAINED in the recent\n // history (pure redraw or an SSE reconnect replaying the latest frame),\n // refresh that segment in place instead of appending a duplicate block.\n if (body.length > 0) {\n const windowStart = Math.max(0, hist.length - body.length * 2);\n for (let s = hist.length - body.length; s >= windowStart; s--) {\n let ok = true;\n for (let i = 0; i < body.length; i++) {\n if (norm(hist[s + i]) !== norm(body[i])) {\n ok = false;\n break;\n }\n }\n if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));\n }\n }\n // Still no seam. The body's first SUBSTANTIAL line is the top of cc's visible\n // window — a stable dialogue line, because the volatile parts (spinner gerund\n // that rotates \"Skedaddling…\"→\"Noodling…\", the \"⎿ Tip:\" line, \"esc to\n // interrupt\") sit at the BOTTOM. When those trailing lines differ word-for-word\n // between frames, neither the suffix-overlap nor the strict containment above\n // can fire and the whole window re-appends (F078 redux: \"⏺ Bash(…)\" seen 18×\n // on Christian's screen). Anchor on that stable line; if it recurs in recent\n // history, replace from there so the volatile tail refreshes in place.\n const anchor = body.findIndex(substantial);\n if (anchor !== -1) {\n const key = norm(body[anchor]);\n const floor = Math.max(0, hist.length - 400);\n for (let p = hist.length - 1; p >= floor; p--) {\n if (norm(hist[p]) !== key) continue;\n const start = p - anchor;\n if (start >= 0) return hist.slice(0, start).concat(body);\n }\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"]}
package/dist/index.cjs CHANGED
@@ -113,7 +113,15 @@ var SetiClient = class {
113
113
 
114
114
  // src/frame-accumulator.ts
115
115
  var RULE = /^[─━-]{10,}\s*$/;
116
- var SPIN = /^[✻✶✳·•*]\s/;
116
+ var SPIN = /^[✻✶✳✢✽·•*]\s/;
117
+ var BOX = /^[\s─━│┌┐└┘├┤┬┴┼╭╮╯╰╠╣╦╩╬═╳]+$/;
118
+ function substantial(line) {
119
+ const t = line.trim();
120
+ if (t.length < 4) return false;
121
+ if (SPIN.test(line)) return false;
122
+ if (BOX.test(t)) return false;
123
+ return true;
124
+ }
117
125
  function splitFooter(lines) {
118
126
  let inp = -1;
119
127
  for (let i = lines.length - 1; i >= 0 && i >= lines.length - 8; i--) {
@@ -129,17 +137,45 @@ function splitFooter(lines) {
129
137
  while (start > 0 && SPIN.test(lines[start - 1])) start--;
130
138
  return { body: lines.slice(0, start), footer: lines.slice(start) };
131
139
  }
140
+ function norm(line) {
141
+ const t = line.replace(/\s+$/, "");
142
+ if (/^[✻✶✳✢✽·•]/.test(t)) return `\u2022${t.slice(1).replace(/\d+/g, "#")}`;
143
+ return t;
144
+ }
132
145
  function mergeOverlap(hist, body) {
133
146
  const max = Math.min(hist.length, body.length);
134
147
  for (let k = max; k > 0; k--) {
135
148
  let ok = true;
136
149
  for (let i = 0; i < k; i++) {
137
- if (hist[hist.length - k + i] !== body[i]) {
150
+ if (norm(hist[hist.length - k + i]) !== norm(body[i])) {
138
151
  ok = false;
139
152
  break;
140
153
  }
141
154
  }
142
- if (ok) return hist.concat(body.slice(k));
155
+ if (ok) return hist.slice(0, hist.length - k).concat(body);
156
+ }
157
+ if (body.length > 0) {
158
+ const windowStart = Math.max(0, hist.length - body.length * 2);
159
+ for (let s = hist.length - body.length; s >= windowStart; s--) {
160
+ let ok = true;
161
+ for (let i = 0; i < body.length; i++) {
162
+ if (norm(hist[s + i]) !== norm(body[i])) {
163
+ ok = false;
164
+ break;
165
+ }
166
+ }
167
+ if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
168
+ }
169
+ }
170
+ const anchor = body.findIndex(substantial);
171
+ if (anchor !== -1) {
172
+ const key = norm(body[anchor]);
173
+ const floor = Math.max(0, hist.length - 400);
174
+ for (let p = hist.length - 1; p >= floor; p--) {
175
+ if (norm(hist[p]) !== key) continue;
176
+ const start = p - anchor;
177
+ if (start >= 0) return hist.slice(0, start).concat(body);
178
+ }
143
179
  }
144
180
  return hist.concat(body);
145
181
  }
@@ -1 +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"]}
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// Pure box-drawing / whitespace — never a reliable merge anchor.\nconst BOX = /^[\\s─━│┌┐└┘├┤┬┴┼╭╮╯╰╠╣╦╩╬═╳]+$/;\n\n/**\n * A line stable enough to align two frames on. Excludes blanks, short\n * fragments, horizontal rules / box-drawing, and the volatile spinner line —\n * i.e. the things that get redrawn between snapshots.\n */\nfunction substantial(line: string): boolean {\n const t = line.trim();\n if (t.length < 4) return false;\n if (SPIN.test(line)) return false;\n if (BOX.test(t)) return false;\n return true;\n}\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\n/**\n * Comparison key for overlap matching. cc updates lines IN PLACE while a turn\n * runs (spinner glyph rotates, \"Worked for Xs\" counts up, token counters tick)\n * — exact equality then finds no overlap and the whole frame gets re-appended\n * as a duplicate block (F078). Normalizing the volatile parts (one spinner\n * glyph, digit runs masked) makes those lines compare equal, and the merge\n * then REFRESHES them with the new frame's text.\n */\nfunction norm(line: string): string {\n const t = line.replace(/\\s+$/, \"\");\n // Only status lines (spinner-prefixed) get digit masking — masking digits in\n // ordinary output would make e.g. \"line 1\" equal \"line 2\" and eat real lines.\n if (/^[✻✶✳✢✽·•]/.test(t)) return `•${t.slice(1).replace(/\\d+/g, \"#\")}`;\n return t;\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 (norm(hist[hist.length - k + i]) !== norm(body[i])) {\n ok = false;\n break;\n }\n }\n // Take the NEW frame's lines for the overlapping segment so in-place\n // updates (timers, spinners) refresh instead of going stale.\n if (ok) return hist.slice(0, hist.length - k).concat(body);\n }\n // No suffix/prefix overlap. If the body is already CONTAINED in the recent\n // history (pure redraw or an SSE reconnect replaying the latest frame),\n // refresh that segment in place instead of appending a duplicate block.\n if (body.length > 0) {\n const windowStart = Math.max(0, hist.length - body.length * 2);\n for (let s = hist.length - body.length; s >= windowStart; s--) {\n let ok = true;\n for (let i = 0; i < body.length; i++) {\n if (norm(hist[s + i]) !== norm(body[i])) {\n ok = false;\n break;\n }\n }\n if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));\n }\n }\n // Still no seam. The body's first SUBSTANTIAL line is the top of cc's visible\n // window — a stable dialogue line, because the volatile parts (spinner gerund\n // that rotates \"Skedaddling…\"→\"Noodling…\", the \"⎿ Tip:\" line, \"esc to\n // interrupt\") sit at the BOTTOM. When those trailing lines differ word-for-word\n // between frames, neither the suffix-overlap nor the strict containment above\n // can fire and the whole window re-appends (F078 redux: \"⏺ Bash(…)\" seen 18×\n // on Christian's screen). Anchor on that stable line; if it recurs in recent\n // history, replace from there so the volatile tail refreshes in place.\n const anchor = body.findIndex(substantial);\n if (anchor !== -1) {\n const key = norm(body[anchor]);\n const floor = Math.max(0, hist.length - 400);\n for (let p = hist.length - 1; p >= floor; p--) {\n if (norm(hist[p]) !== key) continue;\n const start = p - anchor;\n if (start >= 0) return hist.slice(0, start).concat(body);\n }\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"]}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { FrameAccumulator, SetiClient, mergeOverlap, splitFooter } from './chunk-VWTGZF3D.js';
1
+ export { FrameAccumulator, SetiClient, mergeOverlap, splitFooter } from './chunk-OJUDFIKW.js';
2
2
 
3
3
  // src/types.ts
4
4
  var SETI_KEYS = [
package/dist/preact.cjs CHANGED
@@ -118,7 +118,15 @@ var SetiClient = class {
118
118
 
119
119
  // src/frame-accumulator.ts
120
120
  var RULE = /^[─━-]{10,}\s*$/;
121
- var SPIN = /^[✻✶✳·•*]\s/;
121
+ var SPIN = /^[✻✶✳✢✽·•*]\s/;
122
+ var BOX = /^[\s─━│┌┐└┘├┤┬┴┼╭╮╯╰╠╣╦╩╬═╳]+$/;
123
+ function substantial(line) {
124
+ const t = line.trim();
125
+ if (t.length < 4) return false;
126
+ if (SPIN.test(line)) return false;
127
+ if (BOX.test(t)) return false;
128
+ return true;
129
+ }
122
130
  function splitFooter(lines) {
123
131
  let inp = -1;
124
132
  for (let i = lines.length - 1; i >= 0 && i >= lines.length - 8; i--) {
@@ -134,17 +142,45 @@ function splitFooter(lines) {
134
142
  while (start > 0 && SPIN.test(lines[start - 1])) start--;
135
143
  return { body: lines.slice(0, start), footer: lines.slice(start) };
136
144
  }
145
+ function norm(line) {
146
+ const t = line.replace(/\s+$/, "");
147
+ if (/^[✻✶✳✢✽·•]/.test(t)) return `\u2022${t.slice(1).replace(/\d+/g, "#")}`;
148
+ return t;
149
+ }
137
150
  function mergeOverlap(hist, body) {
138
151
  const max = Math.min(hist.length, body.length);
139
152
  for (let k = max; k > 0; k--) {
140
153
  let ok = true;
141
154
  for (let i = 0; i < k; i++) {
142
- if (hist[hist.length - k + i] !== body[i]) {
155
+ if (norm(hist[hist.length - k + i]) !== norm(body[i])) {
143
156
  ok = false;
144
157
  break;
145
158
  }
146
159
  }
147
- if (ok) return hist.concat(body.slice(k));
160
+ if (ok) return hist.slice(0, hist.length - k).concat(body);
161
+ }
162
+ if (body.length > 0) {
163
+ const windowStart = Math.max(0, hist.length - body.length * 2);
164
+ for (let s = hist.length - body.length; s >= windowStart; s--) {
165
+ let ok = true;
166
+ for (let i = 0; i < body.length; i++) {
167
+ if (norm(hist[s + i]) !== norm(body[i])) {
168
+ ok = false;
169
+ break;
170
+ }
171
+ }
172
+ if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
173
+ }
174
+ }
175
+ const anchor = body.findIndex(substantial);
176
+ if (anchor !== -1) {
177
+ const key = norm(body[anchor]);
178
+ const floor = Math.max(0, hist.length - 400);
179
+ for (let p = hist.length - 1; p >= floor; p--) {
180
+ if (norm(hist[p]) !== key) continue;
181
+ const start = p - anchor;
182
+ if (start >= 0) return hist.slice(0, start).concat(body);
183
+ }
148
184
  }
149
185
  return hist.concat(body);
150
186
  }
@@ -240,12 +276,14 @@ function SetiChat(props) {
240
276
  const s = screenRef.current;
241
277
  return !s || s.scrollHeight - s.scrollTop - s.clientHeight < 40;
242
278
  };
279
+ let first = true;
243
280
  const handle = client.openStream(props.edge, props.session, {
244
281
  onHello: (h) => setEdgeOn(h.edgeConnected),
245
282
  onPing: (p) => setEdgeOn(p.edgeConnected),
246
283
  onStateChange: setStreamState,
247
284
  onFrame: (content) => {
248
- const stick = atBottom();
285
+ const stick = first || atBottom();
286
+ first = false;
249
287
  acc.feed(content);
250
288
  setScreenText(acc.text);
251
289
  if (stick) {
@@ -301,16 +339,23 @@ function SetiChat(props) {
301
339
  },
302
340
  k.key
303
341
  )) }),
304
- /* @__PURE__ */ jsxRuntime.jsxs("form", { class: "seti-chat__form", "data-testid": "seti-chat-form", onSubmit: submit, children: [
342
+ !props.hideInput && /* @__PURE__ */ jsxRuntime.jsxs("form", { class: "seti-chat__form", "data-testid": "seti-chat-form", onSubmit: submit, children: [
305
343
  /* @__PURE__ */ jsxRuntime.jsx(
306
- "input",
344
+ "textarea",
307
345
  {
308
346
  class: "seti-chat__input",
309
347
  "data-testid": "seti-chat-input",
348
+ rows: 2,
310
349
  value: text,
311
350
  placeholder: props.placeholder ?? "Skriv til sessionen\u2026",
312
351
  autocomplete: "off",
313
- onInput: (e) => setText(e.target.value)
352
+ onInput: (e) => setText(e.target.value),
353
+ onKeyDown: (e) => {
354
+ if (e.key === "Enter" && !e.shiftKey) {
355
+ e.preventDefault();
356
+ void submit(e);
357
+ }
358
+ }
314
359
  }
315
360
  ),
316
361
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1 +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"]}
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// Pure box-drawing / whitespace — never a reliable merge anchor.\nconst BOX = /^[\\s─━│┌┐└┘├┤┬┴┼╭╮╯╰╠╣╦╩╬═╳]+$/;\n\n/**\n * A line stable enough to align two frames on. Excludes blanks, short\n * fragments, horizontal rules / box-drawing, and the volatile spinner line —\n * i.e. the things that get redrawn between snapshots.\n */\nfunction substantial(line: string): boolean {\n const t = line.trim();\n if (t.length < 4) return false;\n if (SPIN.test(line)) return false;\n if (BOX.test(t)) return false;\n return true;\n}\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\n/**\n * Comparison key for overlap matching. cc updates lines IN PLACE while a turn\n * runs (spinner glyph rotates, \"Worked for Xs\" counts up, token counters tick)\n * — exact equality then finds no overlap and the whole frame gets re-appended\n * as a duplicate block (F078). Normalizing the volatile parts (one spinner\n * glyph, digit runs masked) makes those lines compare equal, and the merge\n * then REFRESHES them with the new frame's text.\n */\nfunction norm(line: string): string {\n const t = line.replace(/\\s+$/, \"\");\n // Only status lines (spinner-prefixed) get digit masking — masking digits in\n // ordinary output would make e.g. \"line 1\" equal \"line 2\" and eat real lines.\n if (/^[✻✶✳✢✽·•]/.test(t)) return `•${t.slice(1).replace(/\\d+/g, \"#\")}`;\n return t;\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 (norm(hist[hist.length - k + i]) !== norm(body[i])) {\n ok = false;\n break;\n }\n }\n // Take the NEW frame's lines for the overlapping segment so in-place\n // updates (timers, spinners) refresh instead of going stale.\n if (ok) return hist.slice(0, hist.length - k).concat(body);\n }\n // No suffix/prefix overlap. If the body is already CONTAINED in the recent\n // history (pure redraw or an SSE reconnect replaying the latest frame),\n // refresh that segment in place instead of appending a duplicate block.\n if (body.length > 0) {\n const windowStart = Math.max(0, hist.length - body.length * 2);\n for (let s = hist.length - body.length; s >= windowStart; s--) {\n let ok = true;\n for (let i = 0; i < body.length; i++) {\n if (norm(hist[s + i]) !== norm(body[i])) {\n ok = false;\n break;\n }\n }\n if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));\n }\n }\n // Still no seam. The body's first SUBSTANTIAL line is the top of cc's visible\n // window — a stable dialogue line, because the volatile parts (spinner gerund\n // that rotates \"Skedaddling…\"→\"Noodling…\", the \"⎿ Tip:\" line, \"esc to\n // interrupt\") sit at the BOTTOM. When those trailing lines differ word-for-word\n // between frames, neither the suffix-overlap nor the strict containment above\n // can fire and the whole window re-appends (F078 redux: \"⏺ Bash(…)\" seen 18×\n // on Christian's screen). Anchor on that stable line; if it recurs in recent\n // history, replace from there so the volatile tail refreshes in place.\n const anchor = body.findIndex(substantial);\n if (anchor !== -1) {\n const key = norm(body[anchor]);\n const floor = Math.max(0, hist.length - 400);\n for (let p = hist.length - 1; p >= floor; p--) {\n if (norm(hist[p]) !== key) continue;\n const start = p - anchor;\n if (start >= 0) return hist.slice(0, start).concat(body);\n }\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 * Hide the built-in compose form (text input + Send) while keeping the\n * nav-keys bar. For hosts that supply their own single compose field and\n * only want the screen + nav-keys (e.g. cardmem Chat v2). Cleaner than\n * CSS-hiding `.seti-chat__form` from the outside.\n */\n hideInput?: boolean;\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 let first = true;\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 // First frame lands you at the LATEST line (cc's now), not the top of a\n // tall scrollback — then the existing stick-to-bottom takes over.\n const stick = first || atBottom();\n first = false;\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 {!props.hideInput && (\n <form class=\"seti-chat__form\" data-testid=\"seti-chat-form\" onSubmit={submit}>\n <textarea\n class=\"seti-chat__input\"\n data-testid=\"seti-chat-input\"\n rows={2}\n value={text}\n placeholder={props.placeholder ?? \"Skriv til sessionen…\"}\n autocomplete=\"off\"\n onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}\n onKeyDown={(e) => {\n // Enter sends, Shift+Enter inserts a newline.\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void submit(e);\n }\n }}\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 )}\n </div>\n );\n}\n\nexport { SetiClient } from \"./client\";\nexport type { SetiKey } from \"./types\";\n"]}
package/dist/preact.d.cts CHANGED
@@ -24,6 +24,13 @@ interface SetiChatProps {
24
24
  class?: string;
25
25
  /** Placeholder for the text input. Default: "Skriv til sessionen…" */
26
26
  placeholder?: string;
27
+ /**
28
+ * Hide the built-in compose form (text input + Send) while keeping the
29
+ * nav-keys bar. For hosts that supply their own single compose field and
30
+ * only want the screen + nav-keys (e.g. cardmem Chat v2). Cleaner than
31
+ * CSS-hiding `.seti-chat__form` from the outside.
32
+ */
33
+ hideInput?: boolean;
27
34
  }
28
35
  declare function SetiChat(props: SetiChatProps): preact.JSX.Element;
29
36
 
package/dist/preact.d.ts CHANGED
@@ -24,6 +24,13 @@ interface SetiChatProps {
24
24
  class?: string;
25
25
  /** Placeholder for the text input. Default: "Skriv til sessionen…" */
26
26
  placeholder?: string;
27
+ /**
28
+ * Hide the built-in compose form (text input + Send) while keeping the
29
+ * nav-keys bar. For hosts that supply their own single compose field and
30
+ * only want the screen + nav-keys (e.g. cardmem Chat v2). Cleaner than
31
+ * CSS-hiding `.seti-chat__form` from the outside.
32
+ */
33
+ hideInput?: boolean;
27
34
  }
28
35
  declare function SetiChat(props: SetiChatProps): preact.JSX.Element;
29
36
 
package/dist/preact.js CHANGED
@@ -1,5 +1,5 @@
1
- import { SetiClient, FrameAccumulator } from './chunk-VWTGZF3D.js';
2
- export { SetiClient } from './chunk-VWTGZF3D.js';
1
+ import { SetiClient, FrameAccumulator } from './chunk-OJUDFIKW.js';
2
+ export { SetiClient } from './chunk-OJUDFIKW.js';
3
3
  import { useMemo, useState, useRef, useEffect } from 'preact/hooks';
4
4
  import { jsxs, jsx } from 'preact/jsx-runtime';
5
5
 
@@ -65,12 +65,14 @@ function SetiChat(props) {
65
65
  const s = screenRef.current;
66
66
  return !s || s.scrollHeight - s.scrollTop - s.clientHeight < 40;
67
67
  };
68
+ let first = true;
68
69
  const handle = client.openStream(props.edge, props.session, {
69
70
  onHello: (h) => setEdgeOn(h.edgeConnected),
70
71
  onPing: (p) => setEdgeOn(p.edgeConnected),
71
72
  onStateChange: setStreamState,
72
73
  onFrame: (content) => {
73
- const stick = atBottom();
74
+ const stick = first || atBottom();
75
+ first = false;
74
76
  acc.feed(content);
75
77
  setScreenText(acc.text);
76
78
  if (stick) {
@@ -126,16 +128,23 @@ function SetiChat(props) {
126
128
  },
127
129
  k.key
128
130
  )) }),
129
- /* @__PURE__ */ jsxs("form", { class: "seti-chat__form", "data-testid": "seti-chat-form", onSubmit: submit, children: [
131
+ !props.hideInput && /* @__PURE__ */ jsxs("form", { class: "seti-chat__form", "data-testid": "seti-chat-form", onSubmit: submit, children: [
130
132
  /* @__PURE__ */ jsx(
131
- "input",
133
+ "textarea",
132
134
  {
133
135
  class: "seti-chat__input",
134
136
  "data-testid": "seti-chat-input",
137
+ rows: 2,
135
138
  value: text,
136
139
  placeholder: props.placeholder ?? "Skriv til sessionen\u2026",
137
140
  autocomplete: "off",
138
- onInput: (e) => setText(e.target.value)
141
+ onInput: (e) => setText(e.target.value),
142
+ onKeyDown: (e) => {
143
+ if (e.key === "Enter" && !e.shiftKey) {
144
+ e.preventDefault();
145
+ void submit(e);
146
+ }
147
+ }
139
148
  }
140
149
  ),
141
150
  /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/preact.tsx"],"names":[],"mappings":"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","file":"preact.js","sourcesContent":["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"]}
1
+ {"version":3,"sources":["../src/preact.tsx"],"names":[],"mappings":"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","file":"preact.js","sourcesContent":["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 * Hide the built-in compose form (text input + Send) while keeping the\n * nav-keys bar. For hosts that supply their own single compose field and\n * only want the screen + nav-keys (e.g. cardmem Chat v2). Cleaner than\n * CSS-hiding `.seti-chat__form` from the outside.\n */\n hideInput?: boolean;\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 let first = true;\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 // First frame lands you at the LATEST line (cc's now), not the top of a\n // tall scrollback — then the existing stick-to-bottom takes over.\n const stick = first || atBottom();\n first = false;\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 {!props.hideInput && (\n <form class=\"seti-chat__form\" data-testid=\"seti-chat-form\" onSubmit={submit}>\n <textarea\n class=\"seti-chat__input\"\n data-testid=\"seti-chat-input\"\n rows={2}\n value={text}\n placeholder={props.placeholder ?? \"Skriv til sessionen…\"}\n autocomplete=\"off\"\n onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}\n onKeyDown={(e) => {\n // Enter sends, Shift+Enter inserts a newline.\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void submit(e);\n }\n }}\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 )}\n </div>\n );\n}\n\nexport { SetiClient } from \"./client\";\nexport type { SetiKey } from \"./types\";\n"]}
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@broberg/seti-client",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Typed client + frame-merge engine + Preact <SetiChat> component for buddycloud.cc SET/SETI live streaming chat (consumed through a host-app proxy from @broberg/seti-server).",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "sideEffects": false,
8
- "files": ["dist", "README.md"],
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
9
12
  "main": "./dist/index.cjs",
10
13
  "module": "./dist/index.js",
11
14
  "types": "./dist/index.d.ts",
@@ -30,7 +33,9 @@
30
33
  "preact": "^10.0.0"
31
34
  },
32
35
  "peerDependenciesMeta": {
33
- "preact": { "optional": true }
36
+ "preact": {
37
+ "optional": true
38
+ }
34
39
  },
35
40
  "devDependencies": {
36
41
  "preact": "^10.24.3",
@@ -55,5 +60,7 @@
55
60
  "url": "https://github.com/broberg-ai/components",
56
61
  "directory": "packages/seti-client"
57
62
  },
58
- "publishConfig": { "access": "public" }
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
59
66
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/client.ts","../src/frame-accumulator.ts"],"names":[],"mappings":";AA2BO,IAAM,aAAN,MAAiB;AAAA,EACL,IAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EAEjB,YAAY,IAAA,EAAyB;AACnC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC1C,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA,CAAK,KAAA,GAAQ,EAAE,aAAA,EAAe,UAAU,IAAA,CAAK,KAAK,CAAA,CAAA,EAAG,GAAI,EAAC;AACzE,IAAA,IAAA,CAAK,UAAU,IAAA,CAAK,KAAA,IAAS,UAAA,CAAW,KAAA,CAAM,KAAK,UAAU,CAAA;AAAA,EAC/D;AAAA,EAEA,MAAM,YAAA,GAAoC;AACxC,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA,CAAQ,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,SAAA,CAAA,EAAa,EAAE,OAAA,EAAS,IAAA,CAAK,OAAA,EAAS,CAAA;AACjF,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,OAAO,EAAE,KAAA,EAAO,EAAC,EAAG,KAAA,EAAO,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAA,EAAG;AAC7D,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AAAA,EAEA,MAAM,QAAA,CAAS,IAAA,EAAc,OAAA,EAAiB,IAAA,EAAwC;AACpF,IAAA,OAAO,KAAK,KAAA,CAAM,EAAE,IAAA,EAAM,OAAA,EAAS,MAAM,CAAA;AAAA,EAC3C;AAAA,EAEA,MAAM,OAAA,CAAQ,IAAA,EAAc,OAAA,EAAiB,GAAA,EAAwC;AACnF,IAAA,OAAO,KAAK,KAAA,CAAM,EAAE,IAAA,EAAM,OAAA,EAAS,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAc,MAAM,IAAA,EAKS;AAC3B,IAAA,IAAI;AACF,MAAA,MAAM,MAAM,MAAM,IAAA,CAAK,QAAQ,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,MAAA,CAAA,EAAU;AAAA,QACnD,MAAA,EAAQ,MAAA;AAAA,QACR,SAAS,EAAE,GAAG,IAAA,CAAK,OAAA,EAAS,gBAAgB,kBAAA,EAAmB;AAAA,QAC/D,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAAA,QACzB,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,GAAI;AAAA,OACjC,CAAA;AACD,MAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,GAAO,KAAA,CAAM,OAAO,EAAC,CAAE,CAAA;AAC/C,MAAA,OAAO,EAAE,EAAA,EAAI,CAAC,CAAC,IAAA,CAAK,EAAA,EAAI,aAAA,EAAe,CAAC,CAAC,IAAA,CAAK,aAAA,EAAe,KAAA,EAAO,KAAK,KAAA,EAAM;AAAA,IACjF,SAAS,GAAA,EAAK;AACZ,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,aAAA,EAAe,KAAA,EAAO,OAAO,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,aAAA,EAAc;AAAA,IACtG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAA,CAAW,IAAA,EAAc,OAAA,EAAiB,QAAA,EAAgD;AACxF,IAAA,IAAI,MAAA,GAAS,KAAA;AACb,IAAA,IAAI,UAAA,GAAqC,IAAA;AAEzC,IAAA,MAAM,MAAM,YAA2B;AACrC,MAAA,IAAI,OAAA,GAAU,CAAA;AACd,MAAA,OAAO,CAAC,MAAA,EAAQ;AACd,QAAA,QAAA,CAAS,aAAA,GAAgB,OAAA,KAAY,CAAA,GAAI,YAAA,GAAe,cAAc,CAAA;AACtE,QAAA,UAAA,GAAa,IAAI,eAAA,EAAgB;AACjC,QAAA,IAAI;AACF,UAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA;AAAA,YACrB,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,aAAA,EAAgB,kBAAA,CAAmB,IAAI,CAAC,CAAA,SAAA,EAAY,kBAAA,CAAmB,OAAO,CAAC,CAAA,CAAA;AAAA,YAC3F,EAAE,OAAA,EAAS,EAAE,GAAG,IAAA,CAAK,OAAA,EAAS,MAAA,EAAQ,mBAAA,EAAoB,EAAG,MAAA,EAAQ,UAAA,CAAW,MAAA;AAAO,WACzF;AACA,UAAA,IAAI,CAAC,GAAA,CAAI,EAAA,IAAM,CAAC,GAAA,CAAI,IAAA,EAAM,MAAM,IAAI,KAAA,CAAM,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC9D,UAAA,QAAA,CAAS,gBAAgB,MAAM,CAAA;AAC/B,UAAA,OAAA,GAAU,CAAA;AACV,UAAA,MAAM,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,IAAA,EAAM,QAAQ,CAAA;AAAA,QACvC,CAAA,CAAA,MAAQ;AAAA,QAER;AACA,QAAA,IAAI,MAAA,EAAQ;AACZ,QAAA,OAAA,EAAA;AACA,QAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,GAAA,GAAO,OAAA,EAAS,GAAI,CAAC,CAAC,CAAA;AAAA,MACxE;AACA,MAAA,QAAA,CAAS,gBAAgB,QAAQ,CAAA;AAAA,IACnC,CAAA;AACA,IAAA,KAAK,GAAA,EAAI;AAET,IAAA,OAAO;AAAA,MACL,OAAO,MAAM;AACX,QAAA,MAAA,GAAS,IAAA;AACT,QAAA,UAAA,EAAY,KAAA,EAAM;AAAA,MACpB;AAAA,KACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,OAAA,CAAQ,IAAA,EAAkC,QAAA,EAA6C;AACnG,IAAA,MAAM,MAAA,GAAS,KAAK,SAAA,EAAU;AAC9B,IAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,IAAA,IAAI,GAAA,GAAM,EAAA;AACV,IAAA,WAAS;AACP,MAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,MAAA,IAAI,IAAA,EAAM;AACV,MAAA,GAAA,IAAO,QAAQ,MAAA,CAAO,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAC7C,MAAA,IAAI,GAAA;AACJ,MAAA,OAAA,CAAQ,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,MAAM,OAAO,EAAA,EAAI;AACzC,QAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC9B,QAAA,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA;AACvB,QAAA,IAAI,KAAA,GAAQ,SAAA;AACZ,QAAA,MAAM,OAAiB,EAAC;AACxB,QAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,EAAG;AACpC,UAAA,IAAI,IAAA,CAAK,WAAW,QAAQ,CAAA,UAAW,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA,EAAK;AAAA,eAAA,IACjD,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAE,SAAA,EAAW,CAAA;AAAA,QACxE;AACA,QAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACvB,QAAA,IAAI,MAAA;AACJ,QAAA,IAAI;AACF,UAAA,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,QACrC,CAAA,CAAA,MAAQ;AACN,UAAA;AAAA,QACF;AACA,QAAA,IAAI,UAAU,OAAA,EAAS;AACrB,UAAA,MAAM,IAAK,MAAA,CAAiC,OAAA;AAC5C,UAAA,IAAI,OAAO,CAAA,KAAM,QAAA,EAAU,QAAA,CAAS,UAAU,CAAC,CAAA;AAAA,QACjD,CAAA,MAAA,IAAW,UAAU,OAAA,EAAS;AAC5B,UAAA,QAAA,CAAS,UAAU,MAAmE,CAAA;AAAA,QACxF,CAAA,MAAA,IAAW,UAAU,MAAA,EAAQ;AAC3B,UAAA,QAAA,CAAS,SAAS,MAAoC,CAAA;AAAA,QACxD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACtIA,IAAM,IAAA,GAAO,iBAAA;AACb,IAAM,IAAA,GAAO,aAAA;AAEN,SAAS,YAAY,KAAA,EAAuD;AACjF,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,CAAA,IAAK,KAAK,CAAA,IAAK,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,CAAA,EAAA,EAAK;AACnE,IAAA,MAAM,IAAI,KAAA,CAAM,CAAC,CAAA,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACrC,IAAA,IAAI,CAAA,CAAE,WAAW,CAAC,CAAA,KAAM,SAAU,CAAA,CAAE,CAAC,MAAM,GAAA,EAAK;AAC9C,MAAA,GAAA,GAAM,CAAA;AACN,MAAA;AAAA,IACF;AAAA,EACF;AACA,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,GAAA,KAAQ,IAAI,KAAA,GAAQ,IAAA,CAAK,IAAI,CAAA,EAAG,KAAA,CAAM,SAAS,CAAC,CAAA;AAAA,OAC/C,KAAA,GAAQ,GAAA,GAAM,CAAA,IAAK,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA,CAAE,IAAA,EAAM,CAAA,GAAI,MAAM,CAAA,GAAI,GAAA;AACrE,EAAA,OAAO,KAAA,GAAQ,KAAK,IAAA,CAAK,IAAA,CAAK,MAAM,KAAA,GAAQ,CAAC,CAAC,CAAA,EAAG,KAAA,EAAA;AACjD,EAAA,OAAO,EAAE,IAAA,EAAM,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,KAAA,CAAM,KAAK,CAAA,EAAE;AACnE;AAEO,SAAS,YAAA,CAAa,MAAgB,IAAA,EAA0B;AACrE,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,MAAA,EAAQ,KAAK,MAAM,CAAA;AAC7C,EAAA,KAAA,IAAS,CAAA,GAAI,GAAA,EAAK,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK;AAC5B,IAAA,IAAI,EAAA,GAAK,IAAA;AACT,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK;AAC1B,MAAA,IAAI,IAAA,CAAK,KAAK,MAAA,GAAS,CAAA,GAAI,CAAC,CAAA,KAAM,IAAA,CAAK,CAAC,CAAA,EAAG;AACzC,QAAA,EAAA,GAAK,KAAA;AACL,QAAA;AAAA,MACF;AAAA,IACF;AACA,IAAA,IAAI,IAAI,OAAO,IAAA,CAAK,OAAO,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,EAC1C;AACA,EAAA,OAAO,IAAA,CAAK,OAAO,IAAI,CAAA;AACzB;AAEO,IAAM,mBAAN,MAAuB;AAAA,EAI5B,WAAA,CAA6B,aAAa,GAAA,EAAM;AAAnB,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AAAA,EAAoB;AAAA,EAApB,UAAA;AAAA,EAHrB,UAAoB,EAAC;AAAA,EACrB,SAAmB,EAAC;AAAA;AAAA,EAK5B,KAAK,OAAA,EAA4B;AAC/B,IAAA,MAAM,QAAQ,OAAA,CAAQ,OAAA,CAAQ,QAAQ,EAAE,CAAA,CAAE,MAAM,IAAI,CAAA;AACpD,IAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAO,GAAI,YAAY,KAAK,CAAA;AAC1C,IAAA,IAAA,CAAK,OAAA,GAAU,YAAA,CAAa,IAAA,CAAK,OAAA,EAAS,IAAI,CAAA;AAC9C,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,GAAS,IAAA,CAAK,UAAA,EAAY;AACzC,MAAA,IAAA,CAAK,UAAU,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,CAAC,KAAK,UAAU,CAAA;AAAA,IACpD;AACA,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,OAAO,IAAA,CAAK,IAAA;AAAA,EACd;AAAA,EAEA,IAAI,IAAA,GAAkB;AACpB,IAAA,OAAO,EAAE,OAAA,EAAS,IAAA,CAAK,OAAA,EAAS,MAAA,EAAQ,KAAK,MAAA,EAAO;AAAA,EACtD;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,MAAM,CAAA,CAAE,KAAK,IAAI,CAAA;AAAA,EACnD;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,UAAU,EAAC;AAChB,IAAA,IAAA,CAAK,SAAS,EAAC;AAAA,EACjB;AACF","file":"chunk-VWTGZF3D.js","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"]}