@broberg/seti-client 0.1.0 → 0.1.1

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.
@@ -127,17 +127,35 @@ function splitFooter(lines) {
127
127
  while (start > 0 && SPIN.test(lines[start - 1])) start--;
128
128
  return { body: lines.slice(0, start), footer: lines.slice(start) };
129
129
  }
130
+ function norm(line) {
131
+ const t = line.replace(/\s+$/, "");
132
+ if (/^[✻✶✳✢✽·•]/.test(t)) return `\u2022${t.slice(1).replace(/\d+/g, "#")}`;
133
+ return t;
134
+ }
130
135
  function mergeOverlap(hist, body) {
131
136
  const max = Math.min(hist.length, body.length);
132
137
  for (let k = max; k > 0; k--) {
133
138
  let ok = true;
134
139
  for (let i = 0; i < k; i++) {
135
- if (hist[hist.length - k + i] !== body[i]) {
140
+ if (norm(hist[hist.length - k + i]) !== norm(body[i])) {
136
141
  ok = false;
137
142
  break;
138
143
  }
139
144
  }
140
- if (ok) return hist.concat(body.slice(k));
145
+ if (ok) return hist.slice(0, hist.length - k).concat(body);
146
+ }
147
+ if (body.length > 0) {
148
+ const windowStart = Math.max(0, hist.length - body.length * 2);
149
+ for (let s = hist.length - body.length; s >= windowStart; s--) {
150
+ let ok = true;
151
+ for (let i = 0; i < body.length; i++) {
152
+ if (norm(hist[s + i]) !== norm(body[i])) {
153
+ ok = false;
154
+ break;
155
+ }
156
+ }
157
+ if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
158
+ }
141
159
  }
142
160
  return hist.concat(body);
143
161
  }
@@ -173,5 +191,5 @@ var FrameAccumulator = class {
173
191
  };
174
192
 
175
193
  export { FrameAccumulator, SetiClient, mergeOverlap, splitFooter };
176
- //# sourceMappingURL=chunk-VWTGZF3D.js.map
177
- //# sourceMappingURL=chunk-VWTGZF3D.js.map
194
+ //# sourceMappingURL=chunk-3SJJDEX7.js.map
195
+ //# sourceMappingURL=chunk-3SJJDEX7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/frame-accumulator.ts"],"names":[],"mappings":"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","file":"chunk-3SJJDEX7.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\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 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
@@ -129,17 +129,35 @@ function splitFooter(lines) {
129
129
  while (start > 0 && SPIN.test(lines[start - 1])) start--;
130
130
  return { body: lines.slice(0, start), footer: lines.slice(start) };
131
131
  }
132
+ function norm(line) {
133
+ const t = line.replace(/\s+$/, "");
134
+ if (/^[✻✶✳✢✽·•]/.test(t)) return `\u2022${t.slice(1).replace(/\d+/g, "#")}`;
135
+ return t;
136
+ }
132
137
  function mergeOverlap(hist, body) {
133
138
  const max = Math.min(hist.length, body.length);
134
139
  for (let k = max; k > 0; k--) {
135
140
  let ok = true;
136
141
  for (let i = 0; i < k; i++) {
137
- if (hist[hist.length - k + i] !== body[i]) {
142
+ if (norm(hist[hist.length - k + i]) !== norm(body[i])) {
138
143
  ok = false;
139
144
  break;
140
145
  }
141
146
  }
142
- if (ok) return hist.concat(body.slice(k));
147
+ if (ok) return hist.slice(0, hist.length - k).concat(body);
148
+ }
149
+ if (body.length > 0) {
150
+ const windowStart = Math.max(0, hist.length - body.length * 2);
151
+ for (let s = hist.length - body.length; s >= windowStart; s--) {
152
+ let ok = true;
153
+ for (let i = 0; i < body.length; i++) {
154
+ if (norm(hist[s + i]) !== norm(body[i])) {
155
+ ok = false;
156
+ break;
157
+ }
158
+ }
159
+ if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
160
+ }
143
161
  }
144
162
  return hist.concat(body);
145
163
  }
@@ -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\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 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-3SJJDEX7.js';
2
2
 
3
3
  // src/types.ts
4
4
  var SETI_KEYS = [
package/dist/preact.cjs CHANGED
@@ -134,17 +134,35 @@ function splitFooter(lines) {
134
134
  while (start > 0 && SPIN.test(lines[start - 1])) start--;
135
135
  return { body: lines.slice(0, start), footer: lines.slice(start) };
136
136
  }
137
+ function norm(line) {
138
+ const t = line.replace(/\s+$/, "");
139
+ if (/^[✻✶✳✢✽·•]/.test(t)) return `\u2022${t.slice(1).replace(/\d+/g, "#")}`;
140
+ return t;
141
+ }
137
142
  function mergeOverlap(hist, body) {
138
143
  const max = Math.min(hist.length, body.length);
139
144
  for (let k = max; k > 0; k--) {
140
145
  let ok = true;
141
146
  for (let i = 0; i < k; i++) {
142
- if (hist[hist.length - k + i] !== body[i]) {
147
+ if (norm(hist[hist.length - k + i]) !== norm(body[i])) {
143
148
  ok = false;
144
149
  break;
145
150
  }
146
151
  }
147
- if (ok) return hist.concat(body.slice(k));
152
+ if (ok) return hist.slice(0, hist.length - k).concat(body);
153
+ }
154
+ if (body.length > 0) {
155
+ const windowStart = Math.max(0, hist.length - body.length * 2);
156
+ for (let s = hist.length - body.length; s >= windowStart; s--) {
157
+ let ok = true;
158
+ for (let i = 0; i < body.length; i++) {
159
+ if (norm(hist[s + i]) !== norm(body[i])) {
160
+ ok = false;
161
+ break;
162
+ }
163
+ }
164
+ if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
165
+ }
148
166
  }
149
167
  return hist.concat(body);
150
168
  }
@@ -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\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 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"]}
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-3SJJDEX7.js';
2
+ export { SetiClient } from './chunk-3SJJDEX7.js';
3
3
  import { useMemo, useState, useRef, useEffect } from 'preact/hooks';
4
4
  import { jsxs, jsx } from 'preact/jsx-runtime';
5
5
 
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.1",
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":"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","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"]}