@broberg/seti-client 0.1.1 → 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.
- package/dist/{chunk-3SJJDEX7.js → chunk-OJUDFIKW.js} +21 -3
- package/dist/chunk-OJUDFIKW.js.map +1 -0
- package/dist/index.cjs +19 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/preact.cjs +32 -5
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +7 -0
- package/dist/preact.d.ts +7 -0
- package/dist/preact.js +15 -6
- package/dist/preact.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3SJJDEX7.js.map +0 -1
|
@@ -111,7 +111,15 @@ var SetiClient = class {
|
|
|
111
111
|
|
|
112
112
|
// src/frame-accumulator.ts
|
|
113
113
|
var RULE = /^[─━-]{10,}\s*$/;
|
|
114
|
-
var SPIN = /^[
|
|
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--) {
|
|
@@ -157,6 +165,16 @@ function mergeOverlap(hist, body) {
|
|
|
157
165
|
if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
|
|
158
166
|
}
|
|
159
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
|
+
}
|
|
177
|
+
}
|
|
160
178
|
return hist.concat(body);
|
|
161
179
|
}
|
|
162
180
|
var FrameAccumulator = class {
|
|
@@ -191,5 +209,5 @@ var FrameAccumulator = class {
|
|
|
191
209
|
};
|
|
192
210
|
|
|
193
211
|
export { FrameAccumulator, SetiClient, mergeOverlap, splitFooter };
|
|
194
|
-
//# sourceMappingURL=chunk-
|
|
195
|
-
//# sourceMappingURL=chunk-
|
|
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 = /^[
|
|
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--) {
|
|
@@ -159,6 +167,16 @@ function mergeOverlap(hist, body) {
|
|
|
159
167
|
if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
|
|
160
168
|
}
|
|
161
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
|
+
}
|
|
179
|
+
}
|
|
162
180
|
return hist.concat(body);
|
|
163
181
|
}
|
|
164
182
|
var FrameAccumulator = class {
|
package/dist/index.cjs.map
CHANGED
|
@@ -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\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"]}
|
|
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
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 = /^[
|
|
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--) {
|
|
@@ -164,6 +172,16 @@ function mergeOverlap(hist, body) {
|
|
|
164
172
|
if (ok) return hist.slice(0, s).concat(body, hist.slice(s + body.length));
|
|
165
173
|
}
|
|
166
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
|
+
}
|
|
184
|
+
}
|
|
167
185
|
return hist.concat(body);
|
|
168
186
|
}
|
|
169
187
|
var FrameAccumulator = class {
|
|
@@ -258,12 +276,14 @@ function SetiChat(props) {
|
|
|
258
276
|
const s = screenRef.current;
|
|
259
277
|
return !s || s.scrollHeight - s.scrollTop - s.clientHeight < 40;
|
|
260
278
|
};
|
|
279
|
+
let first = true;
|
|
261
280
|
const handle = client.openStream(props.edge, props.session, {
|
|
262
281
|
onHello: (h) => setEdgeOn(h.edgeConnected),
|
|
263
282
|
onPing: (p) => setEdgeOn(p.edgeConnected),
|
|
264
283
|
onStateChange: setStreamState,
|
|
265
284
|
onFrame: (content) => {
|
|
266
|
-
const stick = atBottom();
|
|
285
|
+
const stick = first || atBottom();
|
|
286
|
+
first = false;
|
|
267
287
|
acc.feed(content);
|
|
268
288
|
setScreenText(acc.text);
|
|
269
289
|
if (stick) {
|
|
@@ -319,16 +339,23 @@ function SetiChat(props) {
|
|
|
319
339
|
},
|
|
320
340
|
k.key
|
|
321
341
|
)) }),
|
|
322
|
-
/* @__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: [
|
|
323
343
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
324
|
-
"
|
|
344
|
+
"textarea",
|
|
325
345
|
{
|
|
326
346
|
class: "seti-chat__input",
|
|
327
347
|
"data-testid": "seti-chat-input",
|
|
348
|
+
rows: 2,
|
|
328
349
|
value: text,
|
|
329
350
|
placeholder: props.placeholder ?? "Skriv til sessionen\u2026",
|
|
330
351
|
autocomplete: "off",
|
|
331
|
-
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
|
+
}
|
|
332
359
|
}
|
|
333
360
|
),
|
|
334
361
|
/* @__PURE__ */ jsxRuntime.jsx(
|
package/dist/preact.cjs.map
CHANGED
|
@@ -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\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"]}
|
|
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-
|
|
2
|
-
export { SetiClient } from './chunk-
|
|
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
|
-
"
|
|
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(
|
package/dist/preact.js.map
CHANGED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broberg/seti-client",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|