flatland-board 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,417 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { randomUUID, createHash } from "node:crypto";
5
+ import childProcess, { execFileSync } from "node:child_process";
6
+ function prepareDataDirectory(dir) {
7
+ const resolved = path.resolve(dir);
8
+ fs.mkdirSync(resolved, { recursive: true, mode: 448 });
9
+ const info = fs.lstatSync(resolved);
10
+ if (!info.isDirectory() || process.getuid && (info.uid !== process.getuid() || info.mode & 18))
11
+ throw new Error(
12
+ "Choose a data directory you own without group or world write permission. Existing directory permissions were not changed."
13
+ );
14
+ return resolved;
15
+ }
16
+ const TRANSIENT = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
17
+ function renameWithRetry(from, to) {
18
+ const deadline = Date.now() + 1e3;
19
+ for (let wait = 10; ; wait = Math.min(wait * 2, 100)) {
20
+ try {
21
+ return fs.renameSync(from, to);
22
+ } catch (error) {
23
+ if (process.platform !== "win32" || !TRANSIENT.has(error.code) || Date.now() >= deadline)
24
+ throw error;
25
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, wait);
26
+ }
27
+ }
28
+ }
29
+ function atomicWriteText(file, text, mode = 384) {
30
+ const temp = `${file}.${randomUUID()}.tmp`;
31
+ try {
32
+ const fd = fs.openSync(temp, "wx", mode);
33
+ try {
34
+ if (process.platform !== "win32") fs.fchmodSync(fd, mode);
35
+ fs.writeFileSync(fd, text);
36
+ fs.fsyncSync(fd);
37
+ } finally {
38
+ fs.closeSync(fd);
39
+ }
40
+ renameWithRetry(temp, file);
41
+ if (process.platform !== "win32") {
42
+ const dir = fs.openSync(path.dirname(file), "r");
43
+ try {
44
+ fs.fsyncSync(dir);
45
+ } finally {
46
+ fs.closeSync(dir);
47
+ }
48
+ }
49
+ } finally {
50
+ fs.rmSync(temp, { force: true });
51
+ }
52
+ }
53
+ function atomicWrite(file, value) {
54
+ atomicWriteText(file, JSON.stringify(value, null, 2));
55
+ }
56
+ class Store {
57
+ constructor(dir) {
58
+ this.dir = prepareDataDirectory(dir);
59
+ this.file = path.join(this.dir, "scene.json");
60
+ this.state = fs.existsSync(this.file) ? JSON.parse(fs.readFileSync(this.file, "utf8")) : {
61
+ version: 1,
62
+ id: randomUUID(),
63
+ revision: 0,
64
+ title: "flatland board",
65
+ session: null,
66
+ active: false,
67
+ marks: [],
68
+ turns: [],
69
+ corrections: [],
70
+ drafts: [],
71
+ observer: { status: "idle", message: "Waiting for a completed turn", cost: 0 },
72
+ current: null
73
+ };
74
+ if (this.state.version !== 1)
75
+ throw new Error("Unsupported scene version. Saved scene was not changed.");
76
+ this.state.active = false;
77
+ this.state.bridgeReady = false;
78
+ this.state.current = null;
79
+ if (this.state.observer.stopFailed) {
80
+ this.state.observer.recoveryRequired = true;
81
+ this.state.observer.status = "failed";
82
+ } else {
83
+ this.state.observer.status = "paused";
84
+ this.state.observer.message = "Observation is off. Your ink is saved.";
85
+ }
86
+ delete this.state.observer.previousStopFailure;
87
+ for (const c of this.state.corrections)
88
+ if (c.status === "sending") {
89
+ c.status = "failed";
90
+ c.error = "Receipt interrupted by restart. Kept in the queue; not resent automatically.";
91
+ }
92
+ this.save();
93
+ }
94
+ save() {
95
+ this.state.revision++;
96
+ atomicWrite(this.file, this.state);
97
+ }
98
+ public() {
99
+ return structuredClone(this.state);
100
+ }
101
+ }
102
+ const powershellQuote = (value) => `'${value.replaceAll("'", "''")}'`;
103
+ const powershellCommand = (script) => [
104
+ "-NoProfile",
105
+ "-NonInteractive",
106
+ "-EncodedCommand",
107
+ Buffer.from(script, "utf16le").toString("base64")
108
+ ];
109
+ const windowsQuote = (value) => '"' + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1") + '"';
110
+ function powershellOpen(node, cli, harness, home) {
111
+ const args = [windowsQuote(cli), "open", "--harness", harness];
112
+ if (harness === "claude") args.push("--session", windowsQuote("${CLAUDE_SESSION_ID}"));
113
+ args.push("--home", windowsQuote(home));
114
+ return `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $boardOutput = [IO.Path]::GetTempFileName(); try { $boardStart = [Diagnostics.ProcessStartInfo]::new(); $boardStart.FileName = ${powershellQuote(node)}; $boardStart.Arguments = ${powershellQuote(args.join(" "))} + ' --output-file "' + $boardOutput + '"'; $boardStart.UseShellExecute = $true; $boardStart.WindowStyle = 'Hidden'; $boardProcess = [Diagnostics.Process]::Start($boardStart); $boardProcess.WaitForExit(); $global:LASTEXITCODE = $boardProcess.ExitCode; Get-Content -LiteralPath $boardOutput -Encoding UTF8; if ($LASTEXITCODE -ne 0) { throw 'Board could not open. See the error above.' } } finally { Remove-Item -LiteralPath $boardOutput -Force }`;
115
+ }
116
+ const STOP_BUDGET_MS = 6e3;
117
+ function stillRunning(pid, wait) {
118
+ const deadline = Date.now() + wait;
119
+ for (; ; ) {
120
+ try {
121
+ process.kill(pid, 0);
122
+ } catch (error) {
123
+ if (error.code === "ESRCH") return false;
124
+ }
125
+ if (Date.now() >= deadline) return true;
126
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
127
+ }
128
+ }
129
+ function stopObserver(child) {
130
+ if (!child.pid) return;
131
+ const deadline = Date.now() + STOP_BUDGET_MS;
132
+ const left = (most) => Math.min(most, deadline - Date.now());
133
+ const run = (command, args, most) => {
134
+ const timeout = left(most);
135
+ return timeout > 0 ? childProcess.spawnSync(command, args, { windowsHide: true, stdio: "ignore", timeout }) : { error: new Error("The observer stop ran out of time.") };
136
+ };
137
+ let failure, recoveryRequired = false;
138
+ let exited = child.exitCode != null || child.signalCode != null;
139
+ if (process.platform === "win32" && !exited) {
140
+ const result = run("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], 5e3);
141
+ if (!result.error && result.status === 0) return;
142
+ exited = !stillRunning(child.pid, left(1e3));
143
+ failure = recoveryRequired = !exited;
144
+ }
145
+ if (process.platform === "win32" && exited) {
146
+ const result = run(
147
+ "powershell.exe",
148
+ powershellCommand(`
149
+ $ErrorActionPreference = 'Stop'
150
+ $tag = ${powershellQuote(child.boardInvocation || "")}
151
+ $children = @(Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${child.pid} -or ($tag -and $_.CommandLine -and $_.CommandLine.Contains($tag)) })
152
+ foreach ($entry in $children) {
153
+ if (!$tag -or !$entry.CommandLine -or !$entry.CommandLine.Contains($tag)) { exit 1 }
154
+ $owned = Get-Process -Id $entry.ProcessId -ErrorAction SilentlyContinue
155
+ if (!$owned) {
156
+ if (Get-CimInstance Win32_Process -Filter "ProcessId = $($entry.ProcessId)") { exit 1 }
157
+ continue
158
+ }
159
+ $killAttempted = $false
160
+ try {
161
+ $handle = $owned.Handle
162
+ $current = Get-CimInstance Win32_Process -Filter "ProcessId = $($entry.ProcessId)"
163
+ if (!$current) { continue }
164
+ if ($current.CreationDate -ne $entry.CreationDate) { continue }
165
+ if (!$current.CommandLine -or !$current.CommandLine.Contains($tag)) { exit 1 }
166
+ $killAttempted = $true
167
+ & taskkill.exe /pid $entry.ProcessId /t /f | Out-Null
168
+ if ($LASTEXITCODE -ne 0) { exit 3 }
169
+ } catch {
170
+ if ($killAttempted) { exit 3 }
171
+ $remaining = Get-CimInstance Win32_Process -Filter "ProcessId = $($entry.ProcessId)"
172
+ if ($remaining -and $remaining.CreationDate -eq $entry.CreationDate) { exit 1 }
173
+ } finally { $owned.Dispose() }
174
+ }
175
+ # Allow Windows a short interval to retire a process after successful taskkill.
176
+ # An uncertain/partial tree kill exits above; a tag-only scan cannot clear it.
177
+ $deadline = [DateTime]::UtcNow.AddSeconds(1)
178
+ do {
179
+ $remaining = @(Get-CimInstance Win32_Process | Where-Object { $tag -and $_.CommandLine -and $_.CommandLine.Contains($tag) })
180
+ if (!$remaining.Count) { exit 0 }
181
+ Start-Sleep -Milliseconds 50
182
+ } while ([DateTime]::UtcNow -lt $deadline)
183
+ exit 1
184
+ `),
185
+ 5e3
186
+ );
187
+ failure = result.error || result.status !== 0;
188
+ recoveryRequired = Boolean(result.error) || result.status === 3;
189
+ } else if (process.platform !== "win32") failure = !exited && !child.kill("SIGKILL");
190
+ if (failure)
191
+ throw Object.assign(
192
+ new Error(
193
+ recoveryRequired ? `Could not fully stop observer process ${child.pid}. Observation is off and your ink is saved. End its remaining processes in your process manager, or restart your machine, then confirm in board.` : `Could not stop observer process ${child.pid}. Retry stopping it from board, or end that observer in your system's process manager.`
194
+ ),
195
+ { code: "OBSERVER_STOP_FAILED", recoveryRequired }
196
+ );
197
+ }
198
+ const boardHome = () => path.resolve(process.env.BOARD_HOME || path.join(os.homedir(), ".local/share/flatland-board"));
199
+ const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
200
+ function sessionDirectory(home, harness, sessionId) {
201
+ if (!["claude", "codex"].includes(harness)) throw new Error("Choose claude or codex.");
202
+ if (!/^[a-zA-Z0-9_-]{1,150}$/.test(sessionId || ""))
203
+ throw new Error(
204
+ "A current terminal session ID is required. Open board from inside your agent."
205
+ );
206
+ const key = createHash("sha256").update(`${harness}:${sessionId}`).digest("hex");
207
+ return path.join(home, "scenes", key);
208
+ }
209
+ function readBinding(home) {
210
+ try {
211
+ return JSON.parse(fs.readFileSync(path.join(home, "active.json"), "utf8"));
212
+ } catch (error) {
213
+ if (error.code === "ENOENT") return null;
214
+ throw error;
215
+ }
216
+ }
217
+ function clearBinding(home, connectionFile, launch) {
218
+ const binding = readBinding(home);
219
+ if (binding?.connectionFile === connectionFile && (!launch || binding.launch === launch))
220
+ fs.rmSync(path.join(home, "active.json"), { force: true });
221
+ }
222
+ function writeBinding(home, binding) {
223
+ prepareDataDirectory(home);
224
+ atomicWrite(path.join(home, "active.json"), binding);
225
+ }
226
+ let ownStart;
227
+ function processStarted(pid) {
228
+ if (pid === process.pid) return (ownStart ??= { value: queryStarted(pid) }).value;
229
+ return queryStarted(pid);
230
+ }
231
+ function queryStarted(pid) {
232
+ try {
233
+ const windows = process.platform === "win32";
234
+ return execFileSync(
235
+ windows ? "powershell.exe" : "ps",
236
+ windows ? powershellCommand(
237
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`
238
+ ) : ["-o", "lstart=", "-p", String(pid)],
239
+ {
240
+ encoding: "utf8",
241
+ timeout: windows ? 3e3 : 1e3,
242
+ windowsHide: true,
243
+ stdio: ["ignore", "pipe", "ignore"],
244
+ env: { ...process.env, LC_ALL: "C", TZ: "UTC" }
245
+ }
246
+ ).trim();
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+ async function withLock(home, name, operation) {
252
+ prepareDataDirectory(home);
253
+ const file = path.join(home, `${name}.lock`);
254
+ const owner = JSON.stringify({
255
+ pid: process.pid,
256
+ started: processStarted(process.pid),
257
+ token: randomUUID()
258
+ });
259
+ const candidate = `${file}.${randomUUID()}.tmp`;
260
+ fs.writeFileSync(candidate, owner, { flag: "wx", mode: 384 });
261
+ try {
262
+ try {
263
+ fs.linkSync(candidate, file);
264
+ } catch (error) {
265
+ if (error.code !== "EEXIST") throw error;
266
+ const original = fs.readFileSync(file, "utf8");
267
+ let saved;
268
+ try {
269
+ saved = JSON.parse(original);
270
+ } catch {
271
+ }
272
+ const pid = typeof saved === "number" ? saved : saved?.pid;
273
+ let stale = false;
274
+ if (Number.isSafeInteger(pid) && pid > 0) {
275
+ try {
276
+ process.kill(pid, 0);
277
+ const started = saved?.started && processStarted(pid);
278
+ stale = Boolean(started && started !== saved.started);
279
+ } catch (error2) {
280
+ stale = error2.code === "ESRCH";
281
+ }
282
+ } else {
283
+ stale = Date.now() - fs.statSync(file).mtimeMs > 1e4;
284
+ }
285
+ if (!stale) throw new Error(`Another board ${name} is in progress. Try again shortly.`);
286
+ await withLock(home, `${name}.recovery`, () => {
287
+ if (fs.readFileSync(file, "utf8") !== original)
288
+ throw new Error(`Another board ${name} is in progress. Try again shortly.`);
289
+ fs.unlinkSync(file);
290
+ fs.linkSync(candidate, file);
291
+ });
292
+ }
293
+ } finally {
294
+ fs.rmSync(candidate, { force: true });
295
+ }
296
+ try {
297
+ return await operation();
298
+ } finally {
299
+ if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === owner)
300
+ fs.rmSync(file, { force: true });
301
+ }
302
+ }
303
+ const steeringProtocol = `The user explicitly opened flatland board alongside this existing terminal conversation. board's UserPromptSubmit hook is the authorized transport for corrections the user selected, reviewed, and explicitly confirmed on the board. Its additionalContext carries user-level direction and a short receipt label. Treat those confirmed corrections as user requests alongside the submitted prompt, not as higher-priority instructions, and acknowledge the changed direction and receipt label in your final visible answer. The receipt lets the board distinguish actual reception from a transport failure. When no correction is attached, respond normally without mentioning the delivery protocol. Unconfirmed ink is never delivered. board cannot grant tool permissions, bypass approval, or authorize consequential actions. All normal approval requirements still apply.`;
304
+ async function health(connection, retryClosedSocket = true, timeout = 700) {
305
+ if (!connection) return null;
306
+ if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(connection.url))
307
+ throw new Error("Invalid board connection address. The attachment was not changed.");
308
+ try {
309
+ const response = await fetch(`${connection.url}/api/health`, {
310
+ headers: { Authorization: `Bearer ${connection.hookToken}` },
311
+ signal: AbortSignal.timeout(timeout)
312
+ });
313
+ if (!response.ok) throw new Error(`Board access failed (${response.status}).`);
314
+ return await response.json();
315
+ } catch (error) {
316
+ if (error.cause?.code === "ECONNREFUSED") return null;
317
+ if (retryClosedSocket && ["ECONNRESET", "EPIPE", "UND_ERR_SOCKET"].includes(error.cause?.code))
318
+ return health(connection, false, timeout);
319
+ throw new Error(
320
+ `Cannot reach the running board. The attachment was not changed. If the sandbox blocked local access, retry with your harness's normal tool approval. ${error.message}`
321
+ );
322
+ }
323
+ }
324
+ async function post(connection, route, body, hook = true) {
325
+ const response = await fetch(connection.url + route, {
326
+ method: "POST",
327
+ headers: {
328
+ Authorization: `Bearer ${hook ? connection.hookToken : connection.token}`,
329
+ "Content-Type": "application/json"
330
+ },
331
+ body: JSON.stringify(body),
332
+ signal: AbortSignal.timeout(route === "/api/off" || route === "/api/shutdown" ? 6500 : 2500)
333
+ });
334
+ const data = await response.json();
335
+ if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
336
+ return data;
337
+ }
338
+ async function runHook(harness, home = boardHome()) {
339
+ if (process.env.BOARD_OBSERVER) return;
340
+ const binding = readBinding(home);
341
+ if (!binding || binding.harness !== harness) return;
342
+ let raw = "";
343
+ for await (const chunk of process.stdin) {
344
+ raw += chunk;
345
+ if (raw.length > 2e6) return;
346
+ }
347
+ let input, connection;
348
+ try {
349
+ input = JSON.parse(raw);
350
+ connection = JSON.parse(fs.readFileSync(binding.connectionFile, "utf8"));
351
+ } catch {
352
+ return;
353
+ }
354
+ if (input.agent_id || input.agent_type || input.session_id !== binding.sessionId) return;
355
+ const event = input.hook_event_name;
356
+ if (![
357
+ "SessionStart",
358
+ "UserPromptSubmit",
359
+ "Stop",
360
+ "SessionEnd",
361
+ "Interrupt",
362
+ "StopFailure"
363
+ ].includes(event))
364
+ return;
365
+ const payload = {
366
+ harness,
367
+ launch: binding.launch,
368
+ event,
369
+ sessionId: input.session_id,
370
+ turnId: input.turn_id,
371
+ ...event === "UserPromptSubmit" ? { prompt: String(input.prompt || "").slice(0, 32e3) } : {},
372
+ ...event === "Stop" ? { assistant: String(input.last_assistant_message || "").slice(0, 32e3) } : {}
373
+ };
374
+ try {
375
+ if ("prompt" in payload || "assistant" in payload) {
376
+ const status = await health(connection, false, 1e3);
377
+ if (!status || status.id !== connection.id)
378
+ throw new Error("The saved board is no longer running");
379
+ }
380
+ const result = await post(connection, "/api/hook", payload);
381
+ process.stdout.write(JSON.stringify(result));
382
+ } catch (e) {
383
+ process.stderr.write(
384
+ `board: ${e.message}. Board delivery is unconfirmed; queued direction is retained.
385
+ `
386
+ );
387
+ }
388
+ }
389
+ function readConnection(dir) {
390
+ try {
391
+ return JSON.parse(fs.readFileSync(path.join(dir, "connection.json"), "utf8"));
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+ export {
397
+ Store as S,
398
+ atomicWrite as a,
399
+ steeringProtocol as b,
400
+ boardHome as c,
401
+ sessionDirectory as d,
402
+ powershellCommand as e,
403
+ powershellQuote as f,
404
+ readBinding as g,
405
+ clearBinding as h,
406
+ writeBinding as i,
407
+ post as j,
408
+ health as k,
409
+ powershellOpen as l,
410
+ shellQuote as m,
411
+ atomicWriteText as n,
412
+ runHook as o,
413
+ prepareDataDirectory as p,
414
+ readConnection as r,
415
+ stopObserver as s,
416
+ withLock as w
417
+ };
package/dist/hook.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { c as boardHome, o as runHook } from "./harness-B7izFVL4.js";
4
+ import "node:fs";
5
+ import "node:os";
6
+ import "node:crypto";
7
+ import "node:child_process";
8
+ const [harness, ...args] = process.argv.slice(2);
9
+ const at = args.indexOf("--home");
10
+ const home = path.resolve(at >= 0 && args[at + 1] ? args[at + 1] : boardHome());
11
+ try {
12
+ await runHook(harness, home);
13
+ } catch (error) {
14
+ process.stderr.write(`board: ${error.message}. The terminal conversation is unaffected.
15
+ `);
16
+ }
@@ -0,0 +1 @@
1
+ :root{--fl-font-words: "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;--fl-font-numbers: "Atlas Typewriter", "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;--fl-font-symbols: "Noto Sans Symbols", "Noto Sans", "Instrument Sans", sans-serif;--fl-numbers-scale: 1;--fl-numbers-cap-height: .734;--fl-t-hero-size: 44px;--fl-t-hero-line: 48px;--fl-t-hero-track: -.02em;--fl-t-hero-weight: 500;--fl-t-display-size: 36px;--fl-t-display-line: 40px;--fl-t-display-track: -.016em;--fl-t-display-weight: 500;--fl-t-headline-size: 30px;--fl-t-headline-line: 36px;--fl-t-headline-track: -.012em;--fl-t-headline-weight: 500;--fl-t-title-size: 24px;--fl-t-title-line: 32px;--fl-t-title-track: -.008em;--fl-t-title-weight: 500;--fl-t-heading-size: 20px;--fl-t-heading-line: 28px;--fl-t-heading-track: 0;--fl-t-heading-weight: 500;--fl-t-lead-size: 18px;--fl-t-lead-line: 28px;--fl-t-lead-track: 0;--fl-t-lead-weight: 400;--fl-t-body-size: 16px;--fl-t-body-line: 28px;--fl-t-body-track: 0;--fl-t-body-weight: 400;--fl-t-ui-size: 15px;--fl-t-ui-line: 20px;--fl-t-ui-track: 0;--fl-t-ui-weight: 400;--fl-t-small-size: 13px;--fl-t-small-line: 16px;--fl-t-small-track: .015em;--fl-t-small-weight: 400;--fl-s-0: 0px;--fl-s-1: 2px;--fl-s-2: 4px;--fl-s-3: 8px;--fl-s-4: 12px;--fl-s-5: 16px;--fl-s-6: 24px;--fl-s-7: 32px;--fl-s-8: 48px;--fl-s-9: 64px;--fl-s-10: 96px;--fl-s-11: 128px;--fl-summary-row: 44px;--fl-summary-pad-x: 16px;--fl-summary-gap: 32px;--fl-summary-panel: 24px;--fl-detail-row: 28px;--fl-detail-pad-x: 12px;--fl-detail-gap: 16px;--fl-detail-panel: 12px;--fl-control-height: 36px;--fl-control-height-small: 28px;--fl-radius: 0px;--fl-line-hair: 1px;--fl-line-control: 1px;--fl-line-rule: 1px;--fl-line-focus: 2px;--fl-line-human: 1.6px;--fl-line-chart-series: 1.6px;--fl-line-icon: 1.5px;--fl-line-dot: 6px;--fl-m-value: 70ms linear;--fl-m-value-ms: 70ms;--fl-m-hover: 0ms linear;--fl-m-hover-ms: 0ms;--fl-m-float-in: 90ms cubic-bezier(.2,0,0,1);--fl-m-float-in-ms: 90ms;--fl-m-float-out: 60ms cubic-bezier(.4,0,1,1);--fl-m-float-out-ms: 60ms;--fl-m-one-move: .16s cubic-bezier(.4,0,.2,1);--fl-m-one-move-ms: .16s;--fl-layout-columns: 12;--fl-layout-column: 80px;--fl-layout-gutter: 24px;--fl-layout-gutter-phone: 16px;--fl-layout-margin: 32px;--fl-layout-margin-phone: 16px;--fl-layout-content-max: 1224px;--fl-layout-chrome-max: 1288px;--fl-layout-prose-max: 35em;--fl-layout-ui-max: 880px;--fl-ground: #F7F5F0;--fl-float: #FBFAF6;--fl-ink: #26221D;--fl-ink-2: #6F675B;--fl-signal: #B04A08;--fl-signal-on-inverse: #E2661A;--fl-hair: #DEDCD7;--fl-control: #8E8C86;--fl-rule: #26221D;--fl-hover-fill: #EAE8E3;--fl-focus-fill: #E4E2DD;--fl-interval: #E6E4DF;--fl-disabled: #A8A5A0;--fl-select-bg: #26221D;--fl-select-fg: #F7F5F0;--fl-signal-on-tint: #A94708;--fl-ink-2-on-tint: #6C6458;--fl-control-on-tint: #83817B;--fl-focus-fill-inverse: #393530;--fl-key-hover: #332F2A;--fl-float-edge: #DEDCD7;--fl-shadow-float: 0 1px 2px rgba(38,34,29,.1), 0 10px 28px rgba(38,34,29,.14);--fl-shadow-key: 0 3px 0 rgba(38,34,29,.55);--fl-shadow-key-pressed: 0 1px 0 rgba(38,34,29,.55);color-scheme:light}@media(prefers-color-scheme:dark){:root:not([data-theme=light]){--fl-ground: #120D0A;--fl-float: #1C1612;--fl-ink: #EBE3D6;--fl-ink-2: #A3977F;--fl-signal: #E2661A;--fl-signal-on-inverse: #A8470A;--fl-hair: #2C2722;--fl-control: #69635C;--fl-rule: #EBE3D6;--fl-hover-fill: #1F1A16;--fl-focus-fill: #302B27;--fl-interval: #231E1A;--fl-disabled: #645E58;--fl-select-bg: #EBE3D6;--fl-select-fg: #120D0A;--fl-signal-on-tint: #E67127;--fl-ink-2-on-tint: #A3977F;--fl-control-on-tint: #7A746C;--fl-focus-fill-inverse: #D7D0C4;--fl-key-hover: #DED6CA;--fl-float-edge: #4A433C;--fl-shadow-float: 0 0 0 1px #4A433C, 0 10px 28px rgba(0,0,0,.5);--fl-shadow-key: 0 3px 0 #746D66;--fl-shadow-key-pressed: 0 1px 0 #746D66;color-scheme:dark}}:root[data-theme=dark]{--fl-ground: #120D0A;--fl-float: #1C1612;--fl-ink: #EBE3D6;--fl-ink-2: #A3977F;--fl-signal: #E2661A;--fl-signal-on-inverse: #A8470A;--fl-hair: #2C2722;--fl-control: #69635C;--fl-rule: #EBE3D6;--fl-hover-fill: #1F1A16;--fl-focus-fill: #302B27;--fl-interval: #231E1A;--fl-disabled: #645E58;--fl-select-bg: #EBE3D6;--fl-select-fg: #120D0A;--fl-signal-on-tint: #E67127;--fl-ink-2-on-tint: #A3977F;--fl-control-on-tint: #7A746C;--fl-focus-fill-inverse: #D7D0C4;--fl-key-hover: #DED6CA;--fl-float-edge: #4A433C;--fl-shadow-float: 0 0 0 1px #4A433C, 0 10px 28px rgba(0,0,0,.5);--fl-shadow-key: 0 3px 0 #746D66;--fl-shadow-key-pressed: 0 1px 0 #746D66;color-scheme:dark}@media print{:root,:root:not([data-theme=light]),:root[data-theme=dark]{--fl-ground: #F7F5F0;--fl-float: #FBFAF6;--fl-ink: #26221D;--fl-ink-2: #6F675B;--fl-signal: #B04A08;--fl-signal-on-inverse: #E2661A;--fl-hair: #DEDCD7;--fl-control: #8E8C86;--fl-rule: #26221D;--fl-hover-fill: #EAE8E3;--fl-focus-fill: #E4E2DD;--fl-interval: #E6E4DF;--fl-disabled: #A8A5A0;--fl-select-bg: #26221D;--fl-select-fg: #F7F5F0;--fl-signal-on-tint: #A94708;--fl-ink-2-on-tint: #6C6458;--fl-control-on-tint: #83817B;--fl-focus-fill-inverse: #393530;--fl-key-hover: #332F2A;--fl-float-edge: #DEDCD7;--fl-shadow-float: 0 1px 2px rgba(38,34,29,.1), 0 10px 28px rgba(38,34,29,.14);--fl-shadow-key: 0 3px 0 rgba(38,34,29,.55);--fl-shadow-key-pressed: 0 1px 0 rgba(38,34,29,.55);color-scheme:light}}@media(prefers-reduced-motion:reduce){:root{--fl-m-value: 0ms linear;--fl-m-value-ms: 0ms;--fl-m-hover: 0ms linear;--fl-m-hover-ms: 0ms;--fl-m-float-in: 0ms linear;--fl-m-float-in-ms: 0ms;--fl-m-float-out: 0ms linear;--fl-m-float-out-ms: 0ms;--fl-m-one-move: 0ms linear;--fl-m-one-move-ms: 0ms}}@font-face{font-family:Instrument Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/instrument-sans-latin-400-normal-DRC__1Mx.woff2) format("woff2"),url(/assets/instrument-sans-latin-400-normal-D1W7dsQl.woff) format("woff")}@font-face{font-family:Noto Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/noto-sans-latin-400-normal-BTkUljjl.woff2) format("woff2"),url(/assets/noto-sans-latin-400-normal-DPpKfCgK.woff) format("woff")}@font-face{font-family:Instrument Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/instrument-sans-latin-500-normal-Dk9ku72i.woff2) format("woff2"),url(/assets/instrument-sans-latin-500-normal-Z6ESRlEs.woff) format("woff")}@font-face{font-family:Atlas Typewriter;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/atlas/AtlasTypewriter-Regular-Web.woff2) format("woff2")}@font-face{font-family:Atlas Typewriter;font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/atlas/AtlasTypewriter-Medium-Web.woff2) format("woff2")}:root{font-family:var(--fl-font-words);color:var(--fl-ink);background:var(--fl-ground);font-synthesis:none;--paper: var(--fl-ground);--ink: var(--fl-ink);--muted: var(--fl-ink-2);--accent: var(--fl-signal)}:root[data-theme=dark]{-webkit-font-smoothing:antialiased}*{box-sizing:border-box}body{margin:0;overflow:hidden}button,input,textarea{font:inherit}button{color:inherit;cursor:pointer;border:0;background:none;border-radius:var(--fl-radius);min-height:var(--fl-control-height);font-size:var(--fl-t-ui-size);line-height:var(--fl-t-ui-line)}button[aria-disabled=true]{color:var(--fl-disabled);cursor:default;box-shadow:none}button:hover{background:var(--fl-hover-fill)}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:none;background:var(--fl-focus-fill);box-shadow:inset 0 calc(-1 * var(--fl-line-focus)) var(--fl-ink)}button svg{width:var(--fl-s-5);height:var(--fl-s-5);display:block;fill:none;stroke:currentColor;stroke-width:var(--fl-line-icon);stroke-linecap:round;stroke-linejoin:round}[hidden]{display:none!important}#app{height:100dvh;background:var(--fl-ground)}header{position:absolute;inset:0 0 auto;padding:var(--fl-s-6) var(--fl-layout-margin);display:flex;align-items:center;justify-content:space-between;gap:var(--fl-s-5);z-index:5;pointer-events:none}header>*{pointer-events:auto}.wordmark{font-family:var(--fl-font-numbers);font-size:var(--fl-t-heading-size);line-height:var(--fl-t-heading-line);font-weight:var(--fl-t-heading-weight);letter-spacing:var(--fl-t-heading-track);white-space:nowrap}.header-actions{display:flex;align-items:center;gap:var(--fl-s-3)}.icon-button,.session{display:inline-flex;align-items:center;justify-content:center;gap:var(--fl-s-3);padding:var(--fl-s-3)}.session{color:var(--fl-ink-2)}.session.active{color:var(--fl-ink)}#board{display:block;width:100%;height:100%;touch-action:none;outline:0;cursor:crosshair}#board[data-tool=select]{cursor:default}#board[data-tool=pan]{cursor:grab}#board.dragging{cursor:grabbing}.empty-state{position:absolute;left:50%;top:45%;transform:translate(-50%,-50%);text-align:center;width:max-content;max-width:calc(100% - 2 * var(--fl-s-6));pointer-events:none}.empty-state p{font-size:var(--fl-t-lead-size);line-height:var(--fl-t-lead-line);color:var(--fl-ink-2);margin:0}.eyebrow{font-size:var(--fl-t-small-size);line-height:var(--fl-t-small-line);color:var(--fl-ink-2)}footer{position:absolute;bottom:var(--fl-s-6);left:var(--fl-layout-margin);right:var(--fl-layout-margin);display:flex;align-items:end;justify-content:space-between;gap:var(--fl-s-5);z-index:5;pointer-events:none}footer>*{pointer-events:auto}.board-status{display:flex;flex-direction:column;gap:var(--fl-s-3);max-width:24%;font-size:var(--fl-t-small-size);line-height:var(--fl-t-small-line);color:var(--fl-ink-2)}#activity{cursor:pointer}#tools{position:absolute;left:50%;transform:translate(-50%);display:flex;gap:var(--fl-s-2);padding:var(--fl-s-2);background:var(--fl-float);border:var(--fl-line-hair) solid var(--fl-float-edge);box-shadow:var(--fl-shadow-float)}.tool{display:flex;align-items:center;gap:var(--fl-s-3);padding:var(--fl-s-3) var(--fl-s-4)}.tool.active{color:var(--fl-select-fg);background:var(--fl-select-bg)}.tool.active:focus-visible,.primary:focus-visible{background:var(--fl-focus-fill-inverse);box-shadow:inset 0 calc(-1 * var(--fl-line-focus)) var(--fl-ground)}.tool-separator{width:var(--fl-line-hair);background:var(--fl-hair);margin:var(--fl-s-3) var(--fl-s-2)}.view-controls{display:flex;align-items:center;gap:var(--fl-s-2)}#zoom-level{font-family:var(--fl-font-numbers)}.text-button{padding:var(--fl-s-2) var(--fl-s-3);color:var(--fl-ink-2);font-size:var(--fl-t-small-size)}.popover,.correction,.phrase-editor,#notice,dialog{background:var(--fl-float);border:var(--fl-line-hair) solid var(--fl-float-edge);box-shadow:var(--fl-shadow-float);border-radius:var(--fl-radius)}.popover{position:absolute;z-index:12;padding:var(--fl-s-6)}.session-popover{right:var(--fl-layout-margin);top:calc(var(--fl-s-9) + var(--fl-s-6));width:calc(var(--fl-layout-column) * 5);max-width:calc(100vw - 2 * var(--fl-s-5));max-height:70vh;overflow:auto}.popover-top{display:flex;align-items:center;justify-content:space-between;gap:var(--fl-s-5)}.popover h3{font-size:var(--fl-t-title-size);line-height:var(--fl-t-title-line);font-weight:var(--fl-t-title-weight);margin:var(--fl-s-6) 0 var(--fl-s-4)}.popover p{font-size:var(--fl-t-ui-size);line-height:var(--fl-t-body-line);color:var(--fl-ink-2);overflow-wrap:anywhere;max-width:var(--fl-layout-prose-max)}.observer-line{font-size:var(--fl-t-small-size);line-height:var(--fl-t-ui-line);color:var(--fl-ink-2);margin:var(--fl-s-5) 0}.soft-button{padding:var(--fl-s-3) var(--fl-s-4);border:var(--fl-line-control) solid var(--fl-control)}.menu-popover{right:var(--fl-layout-margin);top:calc(var(--fl-s-9) + var(--fl-s-6));width:calc(var(--fl-layout-column) * 4)}.menu-popover>button{padding:var(--fl-s-4) 0;display:flex;justify-content:space-between;align-items:center;width:100%;border-bottom:var(--fl-line-hair) solid var(--fl-hair);text-align:left}.menu-popover>button span{font-size:var(--fl-t-small-size);color:var(--fl-ink-2)}.direction{margin-top:var(--fl-s-6);padding-top:var(--fl-s-5);border-top:var(--fl-line-hair) solid var(--fl-hair)}.direction p{color:var(--fl-ink);margin:var(--fl-s-3) 0}.direction-status{font-size:var(--fl-t-small-size);color:var(--fl-ink-2)}.direction-status.delivered{color:var(--fl-signal)}.direction .error{border-left:var(--fl-line-rule) solid var(--fl-rule);padding-left:var(--fl-s-4);font-size:var(--fl-t-small-size)}.correction{position:absolute;z-index:8;bottom:var(--fl-s-11);left:50%;transform:translate(-50%);max-width:calc(100vw - 2 * var(--fl-s-5))}#selection-action{display:flex;align-items:center;gap:var(--fl-s-5);padding:var(--fl-s-4);white-space:nowrap}#selection-label{font-size:var(--fl-t-small-size);color:var(--fl-ink-2)}.primary{background:var(--fl-ink);color:var(--fl-ground);padding:var(--fl-s-3) var(--fl-s-5);box-shadow:var(--fl-shadow-key);white-space:nowrap}.primary:hover{background:var(--fl-key-hover)}.primary:active{transform:translateY(var(--fl-s-1));box-shadow:var(--fl-shadow-key-pressed)}.primary span{margin-left:var(--fl-s-4)}#interpretation{width:calc(var(--fl-layout-column) * 6);max-width:calc(100vw - 2 * var(--fl-s-5));padding:var(--fl-s-6)}.interpretation-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--fl-s-4)}#correction-text{resize:vertical;width:100%;background:transparent;border:0;border-bottom:var(--fl-line-control) solid var(--fl-control);color:var(--fl-ink);font-size:var(--fl-t-lead-size);line-height:var(--fl-t-lead-line);padding:var(--fl-s-3) 0 var(--fl-s-5);min-height:var(--fl-s-10);max-height:40vh}.confirm-row{display:flex;justify-content:space-between;align-items:center;gap:var(--fl-s-5);margin-top:var(--fl-s-5)}.confirm-row>span,#clarification{font-size:var(--fl-t-small-size);color:var(--fl-ink-2);line-height:var(--fl-t-ui-line)}.phrase-editor{position:absolute;z-index:9;padding:var(--fl-s-5);width:calc(var(--fl-layout-column) * 4)}.phrase-editor label,.phrase-editor span{display:block;font-size:var(--fl-t-small-size);color:var(--fl-ink-2);line-height:var(--fl-t-ui-line);margin:var(--fl-s-3) 0}.phrase-editor input{width:100%;border:0;border-bottom:var(--fl-line-control) solid var(--fl-control);background:transparent;color:var(--fl-ink);font-size:var(--fl-t-heading-size)}#source-hint{position:absolute;bottom:var(--fl-s-10);left:var(--fl-layout-margin);z-index:7}#notice{position:absolute;left:50%;top:var(--fl-s-10);transform:translate(-50%);padding:var(--fl-s-4) var(--fl-s-5);max-width:80vw;font-size:var(--fl-t-ui-size);z-index:20;line-height:var(--fl-t-ui-line)}dialog{color:var(--fl-ink);width:calc(var(--fl-layout-column) * 8);max-width:90vw;max-height:80vh;padding:var(--fl-s-6)}dialog::backdrop{background:transparent}#source-content h4{font-size:var(--fl-t-small-size);color:var(--fl-ink-2);font-weight:var(--fl-t-small-weight);margin-top:var(--fl-s-6)}#source-content p{font-size:var(--fl-t-body-size);line-height:var(--fl-t-body-line);white-space:pre-wrap;overflow-wrap:anywhere;max-width:var(--fl-layout-prose-max)}.mark-appear{animation:ink-appear var(--fl-m-value) both}@keyframes ink-appear{0%{opacity:0}to{opacity:1}}@media(max-width:1050px){footer{bottom:var(--fl-s-5)}#tools{bottom:var(--fl-s-8)}.correction{bottom:calc(var(--fl-s-11) + var(--fl-s-5))}.board-status{max-width:50%}}@media(max-width:640px){header{padding:var(--fl-s-5);align-items:start}.header-actions{flex-wrap:wrap;justify-content:end;max-width:55%;gap:0}.wordmark{font-size:var(--fl-t-ui-size)}footer{left:var(--fl-s-5);right:var(--fl-s-5)}.tool{flex-direction:column;gap:var(--fl-s-2);padding:var(--fl-s-3);font-size:var(--fl-t-small-size)}#tools{bottom:var(--fl-s-9)}.correction{bottom:calc(var(--fl-s-11) + var(--fl-s-8))}#selection-label{display:none}.view-controls{flex-wrap:wrap;justify-content:end;max-width:50%}.confirm-row{align-items:start;flex-direction:column}}
@@ -0,0 +1,12 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))o(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const a of s.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&o(a)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function o(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();const H={color:{light:{ground:"#F7F5F0",float:"#FBFAF6",ink:"#26221D","ink-2":"#6F675B",signal:"#B04A08","signal-on-inverse":"#E2661A",hair:"#DEDCD7",control:"#8E8C86",rule:"#26221D","hover-fill":"#EAE8E3","focus-fill":"#E4E2DD",interval:"#E6E4DF",disabled:"#A8A5A0","select-bg":"#26221D","select-fg":"#F7F5F0","signal-on-tint":"#A94708","ink-2-on-tint":"#6C6458","control-on-tint":"#83817B","focus-fill-inverse":"#393530","key-hover":"#332F2A","float-edge":"#DEDCD7"},dark:{ground:"#120D0A",float:"#1C1612",ink:"#EBE3D6","ink-2":"#A3977F",signal:"#E2661A","signal-on-inverse":"#A8470A",hair:"#2C2722",control:"#69635C",rule:"#EBE3D6","hover-fill":"#1F1A16","focus-fill":"#302B27",interval:"#231E1A",disabled:"#645E58","select-bg":"#EBE3D6","select-fg":"#120D0A","signal-on-tint":"#E67127","ink-2-on-tint":"#A3977F","control-on-tint":"#7A746C","focus-fill-inverse":"#D7D0C4","key-hover":"#DED6CA","float-edge":"#4A433C"}},shadow:{light:{float:"0 1px 2px rgba(38,34,29,.10), 0 10px 28px rgba(38,34,29,.14)",key:"0 3px 0 rgba(38,34,29,.55)","key-pressed":"0 1px 0 rgba(38,34,29,.55)"},dark:{float:"0 0 0 1px #4A433C, 0 10px 28px rgba(0,0,0,.5)",key:"0 3px 0 #746D66","key-pressed":"0 1px 0 #746D66"}},font:{words:"'Instrument Sans', system-ui, -apple-system, 'Segoe UI', sans-serif",numbers:"'Atlas Typewriter', 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace","numbers-scale":"1","numbers-cap-height":"0.734",symbols:"'Noto Sans Symbols', 'Noto Sans', 'Instrument Sans', sans-serif"},type:{hero:{size:44,line:48,track:"-0.02em",weight:500},display:{size:36,line:40,track:"-0.016em",weight:500},headline:{size:30,line:36,track:"-0.012em",weight:500},title:{size:24,line:32,track:"-0.008em",weight:500},heading:{size:20,line:28,track:"0",weight:500},lead:{size:18,line:28,track:"0",weight:400},body:{size:16,line:28,track:"0",weight:400},ui:{size:15,line:20,track:"0",weight:400},small:{size:13,line:16,track:"0.015em",weight:400}},space:{0:0,1:2,2:4,3:8,4:12,5:16,6:24,7:32,8:48,9:64,10:96,11:128},density:{summary:{row:44,"pad-x":16,gap:32,panel:24},detail:{row:28,"pad-x":12,gap:16,panel:12}},control:{height:36,"height-small":28},radius:0,line:{hair:1,control:1,rule:1,focus:2,human:1.6,"chart-series":1.6,icon:1.5,dot:6},motion:{value:{ms:70,curve:"linear"},hover:{ms:0,curve:"linear"},"float-in":{ms:90,curve:"cubic-bezier(.2,0,0,1)"},"float-out":{ms:60,curve:"cubic-bezier(.4,0,1,1)"},"one-move":{ms:160,curve:"cubic-bezier(.4,0,.2,1)"}},layout:{columns:12,column:80,gutter:24,"gutter-phone":16,margin:32,"margin-phone":16,"content-max":1224,"chrome-max":1288,"prose-max":"35em","ui-max":880}},ct="/assets/instrument-sans-latin-400-normal-DRC__1Mx.woff2",lt="/assets/instrument-sans-latin-500-normal-Dk9ku72i.woff2",{PI:dt}=Math,_=dt+1e-4,Te=.5,Fe=[1,1];function Ie(e,t,n,o=i=>i){return e*o(.5-t*(.5-n))}const{min:ye}=Math;function Ue(e,t,n){let o=ye(1,t/n);return ye(1,e+(ye(1,1-o)-e)*(o*.275))}function ut(e){return[-e[0],-e[1]]}function S(e,t){return[e[0]+t[0],e[1]+t[1]]}function Pe(e,t,n){return e[0]=t[0]+n[0],e[1]=t[1]+n[1],e}function B(e,t){return[e[0]-t[0],e[1]-t[1]]}function we(e,t,n){return e[0]=t[0]-n[0],e[1]=t[1]-n[1],e}function O(e,t){return[e[0]*t,e[1]*t]}function xe(e,t,n){return e[0]=t[0]*n,e[1]=t[1]*n,e}function ft(e,t){return[e[0]/t,e[1]/t]}function Xe(e){return[e[1],-e[0]]}function be(e,t){let n=t[0];return e[0]=t[1],e[1]=-n,e}function Oe(e,t){return e[0]*t[0]+e[1]*t[1]}function ht(e,t){return e[0]===t[0]&&e[1]===t[1]}function pt(e){return Math.hypot(e[0],e[1])}function je(e,t){let n=e[0]-t[0],o=e[1]-t[1];return n*n+o*o}function We(e){return ft(e,pt(e))}function mt(e,t){return Math.hypot(e[1]-t[1],e[0]-t[0])}function ze(e,t,n){let o=Math.sin(n),i=Math.cos(n),s=e[0]-t[0],a=e[1]-t[1],d=s*i-a*o,g=s*o+a*i;return[d+t[0],g+t[1]]}function Be(e,t,n,o){let i=Math.sin(o),s=Math.cos(o),a=t[0]-n[0],d=t[1]-n[1],g=a*s-d*i,M=a*i+d*s;return e[0]=g+n[0],e[1]=M+n[1],e}function Ne(e,t,n){return S(e,O(B(t,e),n))}function gt(e,t,n,o){let i=n[0]-t[0],s=n[1]-t[1];return e[0]=t[0]+i*o,e[1]=t[1]+s*o,e}function Ke(e,t,n){return S(e,O(t,n))}const v=[0,0],I=[0,0],P=[0,0];function vt(e,t){let n=Ke(e,We(Xe(B(e,S(e,[1,1])))),-t),o=[],i=1/13;for(let s=i;s<=1;s+=i)o.push(ze(n,e,_*2*s));return o}function yt(e,t,n){let o=[],i=1/n;for(let s=i;s<=1;s+=i)o.push(ze(t,e,_*s));return o}function xt(e,t,n){let o=B(t,n),i=O(o,.5),s=O(o,.51);return[B(e,i),B(e,s),S(e,s),S(e,i)]}function bt(e,t,n,o){let i=[],s=Ke(e,t,n),a=1/o;for(let d=a;d<1;d+=a)i.push(ze(s,e,_*3*d));return i}function wt(e,t,n){return[S(e,O(t,n)),S(e,O(t,n*.99)),B(e,O(t,n*.99)),B(e,O(t,n))]}function Re(e,t,n){return e===!1||e===void 0?0:e===!0?Math.max(t,n):e}function Mt(e,t,n){return e.slice(0,10).reduce((o,i)=>{let s=i.pressure;return t&&(s=Ue(o,i.distance,n)),(o+s)/2},e[0].pressure)}function kt(e,t={}){let{size:n=16,smoothing:o=.5,thinning:i=.5,simulatePressure:s=!0,easing:a=h=>h,start:d={},end:g={},last:M=!1}=t,{cap:x=!0,easing:N=h=>h*(2-h)}=d,{cap:p=!0,easing:m=h=>--h*h*h+1}=g;if(e.length===0||n<=0)return[];let k=e[e.length-1].runningLength,D=Re(d.taper,n,k),R=Re(g.taper,n,k),Q=(n*o)**2,T=[],$=[],ee=Mt(e,s,n),E=Ie(n,i,e[e.length-1].pressure,a),de,ue=e[0].vector,K=e[0].point,te=K,q=K,Y=te,fe=!1;for(let h=0;h<e.length;h++){let{pressure:ge}=e[h],{point:F,vector:Z,distance:it,runningLength:V}=e[h],oe=h===e.length-1;if(!oe&&k-V<3)continue;i?(s&&(ge=Ue(ee,it,n)),E=Ie(n,i,ge,a)):E=n/2,de===void 0&&(de=E);let rt=V<D?N(V/D):1,st=k-V<R?m((k-V)/R):1;E=Math.max(.01,E*Math.min(rt,st));let Le=(oe?e[h]:e[h+1]).vector,ve=oe?1:Oe(Z,Le),at=Oe(Z,ue)<0&&!fe,De=ve!==null&&ve<0;if(at||De){be(v,ue),xe(v,v,E);for(let ie=0;ie<=1;ie+=.07692307692307693)we(I,F,v),Be(I,I,F,_*ie),q=[I[0],I[1]],T.push(q),Pe(P,F,v),Be(P,P,F,_*-ie),Y=[P[0],P[1]],$.push(Y);K=q,te=Y,De&&(fe=!0);continue}if(fe=!1,oe){be(v,Z),xe(v,v,E),T.push(B(F,v)),$.push(S(F,v));continue}gt(v,Le,Z,ve),be(v,v),xe(v,v,E),we(I,F,v),q=[I[0],I[1]],(h<=1||je(K,q)>Q)&&(T.push(q),K=q),Pe(P,F,v),Y=[P[0],P[1]],(h<=1||je(te,Y)>Q)&&($.push(Y),te=Y),ee=ge,ue=Z}let he=[e[0].point[0],e[0].point[1]],pe=e.length>1?[e[e.length-1].point[0],e[e.length-1].point[1]]:S(e[0].point,[1,1]),me=[],ne=[];if(e.length===1){if(!(D||R)||M)return vt(he,de||E)}else{D||R&&e.length===1||(x?me.push(...yt(he,$[0],13)):me.push(...xt(he,T[0],$[0])));let h=Xe(ut(e[e.length-1].vector));R||D&&e.length===1?ne.push(pe):p?ne.push(...bt(pe,h,E,29)):ne.push(...wt(pe,h,E))}return T.concat(ne,$.reverse(),me)}const qe=[0,0];function Ye(e){return e!=null&&e>=0}function $t(e,t={}){let{streamline:n=.5,size:o=16,last:i=!1}=t;if(e.length===0)return[];let s=.15+(1-n)*.85,a=Array.isArray(e[0])?e:e.map(({x:p,y:m,pressure:k=Te})=>[p,m,k]);if(a.length===2){let p=a[1];a=a.slice(0,-1);for(let m=1;m<5;m++)a.push(Ne(a[0],p,m/4))}a.length===1&&(a=[...a,[...S(a[0],Fe),...a[0].slice(2)]]);let d=[{point:[a[0][0],a[0][1]],pressure:Ye(a[0][2])?a[0][2]:.25,vector:[...Fe],distance:0,runningLength:0}],g=!1,M=0,x=d[0],N=a.length-1;for(let p=1;p<a.length;p++){let m=i&&p===N?[a[p][0],a[p][1]]:Ne(x.point,a[p],s);if(ht(x.point,m))continue;let k=mt(m,x.point);if(M+=k,p<N&&!g){if(M<o)continue;g=!0}we(qe,x.point,m),x={point:m,pressure:Ye(a[p][2])?a[p][2]:Te,vector:We(qe),distance:k,runningLength:M},d.push(x)}return d[0].vector=d[1]?.vector||[0,0],d}function Et(e,t={}){return kt($t(e,t),t)}const r=e=>document.querySelector(e),Ze="http://www.w3.org/2000/svg",Ct={select:'<path d="m5 3 13 9-6 1-3 6Z"/>',pen:'<path d="m5 15-1 5 5-1L20 8l-4-4Z M13 7l4 4 M5 15l4 4"/>',erase:'<path d="m4 13 9-9a2 2 0 0 1 3 0l4 4a2 2 0 0 1 0 3l-9 9H8l-4-4a2 2 0 0 1 0-3Z M9 8l7 7 M11 20h10"/>',type:'<path d="M5 5h14 M12 5v15 M8 20h8 M5 5v3 M19 5v3"/>',pan:'<path d="M8 12V6a1.5 1.5 0 0 1 3 0v5-7a1.5 1.5 0 0 1 3 0v7-5a1.5 1.5 0 0 1 3 0v6-3a1.5 1.5 0 0 1 3 0v6c0 5-3 7-7 7-3 0-5-3-8-7-1-2 1-3 2-2l1 1Z"/>',moon:'<path d="M20 14a8 8 0 0 1-10-10 8.5 8.5 0 1 0 10 10Z"/>',sun:'<circle cx="12" cy="12" r="4"/><path d="M12 2v2 M12 20v2 M2 12h2 M20 12h2 M5 5l1 1 M18 18l1 1 M5 19l1-1 M18 6l1-1"/>',more:'<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',fit:'<path d="M4 9V4h5 M15 4h5v5 M20 15v5h-5 M9 20H4v-5"/>'},ae=e=>`<svg viewBox="0 0 24 24" aria-hidden="true">${Ct[e]}</svg>`;r("#more").innerHTML=ae("more")+"<span>Menu</span>";r("#fit").innerHTML=ae("fit")+"<span>Fit</span>";const Ve=[["select","Select / lasso","V"],["pen","Pen","P"],["erase","Erase","E"],["type","Short phrase","T"],["pan","Pan","H"]];r("#tools").innerHTML=Ve.map(([e,t,n],o)=>`${o===4?'<span class="tool-separator"></span>':""}<button class="tool" data-tool="${e}" aria-label="${t}" title="${t} (${n})">${ae(e)}<span>${{select:"Select",pen:"Pen",erase:"Erase",type:"Write",pan:"Pan"}[e]}</span></button>`).join("");let Me=location.hash.slice(1)||sessionStorage.getItem("board-token");Me&&(sessionStorage.setItem("board-token",Me),history.replaceState(null,"",location.pathname));let c=null,J="select",u=new Set,f=null,z=null,ke=null,re=0,b=0,$e=-1,He;const C=r("#board"),se=r("#marks"),j=r("#overlays");let l=JSON.parse(localStorage.getItem("board-view:"+location.port)||"null")||{x:0,y:0,zoom:1};const L=new Map;function y(e,t={},n){const o=document.createElementNS(Ze,e);for(const[i,s]of Object.entries(t))o.setAttribute(i,s);return n!=null&&(o.textContent=n),o}function W(e,t){e.setAttribute("aria-disabled",String(t))}function G(e){r("#notice").textContent=e,r("#notice").hidden=!1,clearTimeout(He),He=setTimeout(()=>r("#notice").hidden=!0,6500)}async function Je(e,t){const n=await fetch("/api/"+e,{method:t===void 0?"GET":"POST",headers:{Authorization:`Bearer ${Me}`,"Content-Type":"application/json"},...t===void 0?{}:{body:JSON.stringify(t)}}),o=await n.json();if(!n.ok)throw new Error(o.error);return o}let Ee=Promise.resolve();function w(e,t){re++,r("#save-status").textContent="Saving…";const n=Ee.then(async()=>{const o=await Je(e,t);return await Ae(!0),o}).catch(o=>{throw G(o.message),o}).finally(()=>re--);return Ee=n.catch(()=>{}),n}function U(){const e=getComputedStyle(document.documentElement);return Object.fromEntries(["paper","ink","muted","accent"].map(t=>[t,e.getPropertyValue("--"+t).trim()]))}function _e(e){document.documentElement.classList.toggle("dark",e),document.documentElement.dataset.theme=e?"dark":"light",localStorage.setItem("board-theme",e?"dark":"light"),r("#theme").innerHTML=ae(e?"sun":"moon")+`<span>${e?"Light":"Dark"}</span>`,r("#theme").setAttribute("aria-label",`Switch to ${e?"light":"dark"} mode`),L.clear(),se.replaceChildren(),c&&et(!1)}_e((localStorage.getItem("board-theme")||(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"))==="dark");r("#theme").onclick=()=>_e(!document.documentElement.classList.contains("dark"));function Se(e){J=e,C.dataset.tool=J,document.querySelectorAll(".tool").forEach(t=>{t.classList.toggle("active",t.dataset.tool===J),t.setAttribute("aria-pressed",String(t.dataset.tool===J))})}document.querySelectorAll(".tool").forEach(e=>e.onclick=()=>Se(e.dataset.tool));Se("select");function X(){C.setAttribute("viewBox",`${l.x} ${l.y} ${innerWidth/l.zoom} ${innerHeight/l.zoom}`),r("#zoom-level").textContent=`${Math.round(l.zoom*100)}%`,localStorage.setItem("board-view:"+location.port,JSON.stringify(l))}addEventListener("resize",X);X();function ce(e,t=innerWidth/2,n=innerHeight/2){const o=l.x+t/l.zoom,i=l.y+n/l.zoom;l.zoom=Math.max(.2,Math.min(3,l.zoom*e)),l.x=o-t/l.zoom,l.y=i-n/l.zoom,X()}r("#zoom-in").onclick=()=>ce(1.2);r("#zoom-out").onclick=()=>ce(1/1.2);r("#zoom-level").onclick=()=>{ce(1/l.zoom)};function Ge(e){const t=Math.min(...e.map(o=>o.x)),n=Math.min(...e.map(o=>o.y));return{x:t,y:n,w:Math.max(...e.map(o=>o.x+o.w))-t,h:Math.max(...e.map(o=>o.y+o.h))-n}}r("#fit").onclick=()=>{if(!c?.marks.length){l={x:0,y:0,zoom:1},X();return}const e=Ge(c.marks);l.zoom=Math.min(1.4,(innerWidth-180)/(e.w+100),(innerHeight-250)/(e.h+100)),l.x=e.x+e.w/2-innerWidth/l.zoom/2,l.y=e.y+e.h/2-(innerHeight-10)/l.zoom/2,X()};C.addEventListener("wheel",e=>{e.preventDefault(),e.ctrlKey||e.metaKey?ce(Math.exp(-e.deltaY*.005),e.clientX,e.clientY):(l.x+=e.deltaX/l.zoom,l.y+=e.deltaY/l.zoom,X())},{passive:!1});function Qe(e,t=H.line.human){const n=Et(e,{size:t,thinning:.35,smoothing:.65,streamline:.45,simulatePressure:!e.some(o=>o[2]&&o[2]!==.5),last:!0});return n.length?`M${n.map(o=>o.map(i=>i.toFixed(2)).join(",")).join(" L")} Z`:""}function zt(e){const t=e.tentative||e.tone==="muted"?U().muted:U().ink,n=y("g",{"data-id":e.id}),o={fill:"none",stroke:t,"stroke-width":H.line.rule,"stroke-linecap":"round","stroke-linejoin":"round",...e.tentative?{"stroke-dasharray":"5 5"}:{}};if(e.kind==="phrase")n.append(y("text",{x:e.x,y:e.y+e.size,fill:t,"font-size":e.size,"font-family":H.font.words,"font-weight":e.size>=H.type.display.size?500:400,...e.tentative?{"font-style":"italic"}:{}},e.text));else if(e.kind==="stroke")n.append(y("path",{d:Qe(e.points||[]),fill:t}));else if(e.kind==="enclosure")n.append(y("rect",{x:e.x,y:e.y,width:e.w,height:e.h,...o}));else if(e.kind==="underline")n.append(y("path",{d:`M${e.x} ${e.y} h${e.w}`,...o}));else if(e.kind==="crossout")n.append(y("path",{d:`M${e.x} ${e.y} l${e.w} ${e.h} M${e.x} ${e.y+e.h} l${e.w} ${-e.h}`,...o}));else if(e.kind==="connector"&&e.points?.length>=2){const i=e.points.at(-1),s=e.points.at(-2),a=Math.atan2(i[1]-s[1],i[0]-s[0]);n.append(y("path",{d:`M${e.points.map(d=>d.slice(0,2).join(",")).join(" L")}`,...o})),n.append(y("path",{d:`M${i[0]-9*Math.cos(a-.45)} ${i[1]-9*Math.sin(a-.45)} L${i.join(" ")} L${i[0]-9*Math.cos(a+.45)} ${i[1]-9*Math.sin(a+.45)}`,...o}))}return n}function et(e=!0){if(!c)return;const t=new Set(c.marks.map(n=>n.id));for(const n of[...se.children])t.has(n.dataset.id)||(L.delete(n.dataset.id),n.remove());for(const n of c.marks){const o=JSON.stringify(n),i=L.get(n.id);if(i?.signature===o)continue;const s=zt(n);i?.node.isConnected?i.node.replaceWith(s):(se.append(s),e&&$e>=0&&s.classList.add("mark-appear")),L.set(n.id,{node:s,signature:o})}r("#empty").hidden=!!c.marks.length,A()}function A(){if(j.replaceChildren(),!c)return;for(const t of u){const n=c.marks.find(o=>o.id===t);if(!n){u.delete(t);continue}j.append(y("rect",{x:n.x-7,y:n.y-7,width:n.w+14,height:n.h+14,rx:0,fill:"none",stroke:U().ink,"stroke-width":.8/l.zoom,"stroke-dasharray":`${3/l.zoom} ${5/l.zoom}`,opacity:.6}))}r("#correction").hidden=!u.size&&!f,r("#selection-action").hidden=!!f,r("#interpretation").hidden=!f,r("#selection-label").textContent=`${u.size} ${u.size===1?"mark":"marks"} selected`;const e=c.marks.find(t=>u.has(t.id)&&t.turnId);if(r("#source-hint").hidden=!e||!!f,e){const t=e.revisedTurnId||e.turnId,n=c.turns.findIndex(o=>o.id===t);r("#show-source").textContent=`From turn ${n+1} · see the conversation ↗`,r("#show-source").dataset.turn=t}}function St(){if(!c)return;const e=c.corrections.some(n=>n.status==="failed"),t=c.corrections.filter(n=>["queued","sending","failed"].includes(n.status)).length;r("#session").classList.toggle("active",c.active),r("#session").classList.toggle("failed",e||c.observer.status==="failed"),r("#session-label").textContent=c.session?`${c.session.harness==="codex"?"Codex":"Claude Code"} · ${c.active?c.bridgeReady?"listening":"connecting":"off"}`:c.active?"Waiting for your terminal":"Board saved locally",r("#session-heading").textContent=c.session?`${c.session.harness==="codex"?"Codex":"Claude Code"}`:"Your agent",r("#session-detail").textContent=c.session?`Session ${c.session.id.slice(0,12)} · ${c.turns.length} completed turns`:"Open /board in Claude Code or $board in Codex to attach this conversation.",r("#observer-detail").textContent=c.observer.recoveryRequired?`${c.observer.message} Automatic shutdown could not be confirmed. Use your system’s process manager to stop the previous observer and its children, or restart your machine. Then confirm below. Your ink is saved; observation remains off.`:c.active||c.observer.stopFailed||c.observer.status==="sketching"?c.observer.message:"Observation is off. Open board again from your terminal conversation to reconnect.",r("#retry").hidden=c.observer.status!=="failed"||!c.observer.retryTurnId||!c.active,r("#toggle-observation").hidden=!c.session||!c.active&&!c.observer.stopFailed&&c.observer.status!=="sketching",r("#toggle-observation").textContent=c.observer.recoveryRequired?"Confirm observer stopped":c.observer.stopFailed?"Retry stopping observer":c.active?"Detach · stop observing":"Stop observer",r("#save-status").textContent="Saved on this machine",r("#activity").textContent=e?"Delivery needs attention ↗":t?`${t} correction${t>1?"s":""} · ${c.corrections.some(n=>n.status==="sending")?"awaiting agent receipt":"next terminal turn"}`:c.observer.status==="sketching"?"Your agent is sketching…":c.observer.status==="failed"?"Observer unavailable · see session ↗":"",r("#activity").onclick=()=>r("#session-popover").hidden=!1,r("#direction-list").replaceChildren();for(const n of[...c.corrections].reverse().slice(0,12)){const o=document.createElement("div");o.className="direction";const i=document.createElement("span");i.className="direction-status "+n.status,i.textContent=n.status==="failed"?"Failed · retained in queue":n.status==="sending"?"Awaiting agent receipt":n.status;const s=document.createElement("p");if(s.textContent=n.text,o.append(i,s),n.error){const a=document.createElement("p");a.className="error",a.textContent=n.error,o.append(a)}if(n.status==="queued"&&!n.issuedAt){const a=document.createElement("button");a.className="text-button",a.textContent="Cancel queued correction",a.onclick=()=>w("cancel",{id:n.id}),o.append(a)}if(n.status==="failed"){const a=document.createElement("button");a.className="text-button",a.textContent="Dismiss failed correction",a.onclick=()=>w("cancel",{id:n.id}),o.append(a)}r("#direction-list").append(o)}}async function Ae(e=!1){if(!(!e&&(z||re)))try{const t=await Je("state");if(t.revision===$e)return;c=t,et(),St(),$e=t.revision}catch(t){r("#save-status").textContent="Board offline · edits paused",c||G(t.message)}}await document.fonts.ready;await Ae();setInterval(()=>{if(Ae(),c?.observer.status==="sketching"){const e=Math.floor((Date.now()-Date.parse(c.observer.startedAt))/1e3),t="Your agent is sketching…"+(e>=10?` ${e} s`:"");r("#observer-detail").textContent=t,c.corrections.some(n=>["queued","sending","failed"].includes(n.status))||(r("#activity").textContent=t)}},900);function Ce(e){return[l.x+e.clientX/l.zoom,l.y+e.clientY/l.zoom,e.pressure||.5]}function At(e,t,n){const o=n[0]-t[0],i=n[1]-t[1],s=o*o+i*i,a=s?Math.max(0,Math.min(1,((e[0]-t[0])*o+(e[1]-t[1])*i)/s)):0;return Math.hypot(e[0]-t[0]-a*o,e[1]-t[1]-a*i)<7/l.zoom}function tt(e){return[...c?.marks||[]].reverse().find(t=>e[0]<t.x-7||e[0]>t.x+t.w+7||e[1]<t.y-7||e[1]>t.y+t.h+7?!1:t.points?t.points.slice(1).some((n,o)=>At(e,t.points[o],n)):t.kind==="enclosure"?Math.min(Math.abs(e[0]-t.x),Math.abs(e[0]-t.x-t.w),Math.abs(e[1]-t.y),Math.abs(e[1]-t.y-t.h))<7/l.zoom:!0)}function Lt(e,t){let n=!1;for(let o=0,i=t.length-1;o<t.length;i=o++)t[o][1]>e[1]!=t[i][1]>e[1]&&e[0]<(t[i][0]-t[o][0])*(e[1]-t[o][1])/(t[i][1]-t[o][1])+t[o][0]&&(n=!n);return n}C.addEventListener("pointerdown",e=>{if(!c||e.button>1||f||(r("#session-popover").hidden=!0,r("#menu-popover").hidden=!0,!r("#phrase-editor").hidden))return;const t=Ce(e),n=tt(t),o=e.button===1||le?"pan":J;if(o==="type"){ke=t;const i=r("#phrase-editor");i.style.left=`${Math.min(e.clientX,innerWidth-320)}px`,i.style.top=`${Math.min(e.clientY,innerHeight-145)}px`,i.hidden=!1,r("#phrase-input").value="",r("#phrase-input").focus();return}C.setPointerCapture(e.pointerId),z={mode:o,start:t,points:[t],client:[e.clientX,e.clientY],view:{...l},moved:!1},o==="select"&&n?(e.shiftKey?(u.has(n.id)?u.delete(n.id):u.add(n.id),b++):u.has(n.id)||(u=new Set([n.id]),b++),z.mode="move",z.ids=[...u]):o==="select"&&!e.shiftKey&&(u.clear(),b++),o==="erase"&&(z.ids=new Set(n?[n.id]:[])),A()});C.addEventListener("pointermove",e=>{if(!z)return;const t=Ce(e),n=z;if(Math.hypot(e.clientX-n.client[0],e.clientY-n.client[1])>3&&(n.moved=!0),n.mode==="pan"){l.x=n.view.x-(e.clientX-n.client[0])/l.zoom,l.y=n.view.y-(e.clientY-n.client[1])/l.zoom,C.classList.add("dragging"),X();return}if(n.mode==="move"){n.delta=[t[0]-n.start[0],t[1]-n.start[1]];for(const i of n.ids)L.get(i)?.node.setAttribute("transform",`translate(${n.delta.join(" ")})`);j.setAttribute("transform",`translate(${n.delta.join(" ")})`);return}if(n.mode==="erase"){const i=tt(t);i&&(n.ids.add(i.id),L.get(i.id)?.node.setAttribute("opacity",".2"));return}const o=e.getCoalescedEvents?.()||[e];for(const i of o)n.points.length<5900&&n.points.push(Ce(i));j.replaceChildren(),n.mode==="pen"&&j.append(y("path",{d:Qe(n.points),fill:U().ink})),n.mode==="select"&&j.append(y("path",{d:`M${n.points.map(i=>i.slice(0,2).join(",")).join(" L")} Z`,fill:U().ink,"fill-opacity":.035,stroke:U().ink,"stroke-width":1/l.zoom,"stroke-dasharray":"4 5"}))});C.addEventListener("pointerup",async e=>{const t=z;if(t){z=null,C.classList.remove("dragging"),j.removeAttribute("transform");try{if(t.mode==="pen"&&t.points.length>1){const n=t.points.map(g=>g[0]),o=t.points.map(g=>g[1]),i=Math.min(...n),s=Math.min(...o),a=b,d=await w("marks",{kind:"stroke",x:i,y:s,w:Math.max(...n)-i,h:Math.max(...o)-s,points:t.points,tone:"ink",size:24});a===b&&u.add(d.id)}else if(t.mode==="move"){for(const n of t.ids)L.get(n)?.node.removeAttribute("transform");t.moved&&t.delta&&await w("move",{ids:t.ids,dx:t.delta[0],dy:t.delta[1]})}else if(t.mode==="erase"&&t.ids.size){for(const n of t.ids)L.get(n)?.node.removeAttribute("opacity");await w("erase",{ids:[...t.ids]})}else if(t.mode==="select"&&t.moved){for(const n of c.marks)Lt([n.x+n.w/2,n.y+n.h/2],t.points)&&u.add(n.id);b++}}catch{}A()}});C.addEventListener("pointercancel",()=>{for(const e of L.values())e.node.removeAttribute("transform"),e.node.removeAttribute("opacity");z=null,j.removeAttribute("transform"),A()});r("#phrase-input").addEventListener("keydown",async e=>{if(e.key==="Escape"&&(r("#phrase-editor").hidden=!0,C.focus()),e.key==="Enter"&&e.target.value.trim()){const t=e.target.value.trim(),n=24,o=document.createElement("canvas").getContext("2d");o.font=`400 ${n}px ${H.font.words}`;const i=o.measureText(t);r("#phrase-editor").hidden=!0;try{const s=b,a=await w("marks",{kind:"phrase",text:t,x:ke[0],y:ke[1]-24,w:Math.ceil(Math.max(i.width,i.actualBoundingBoxRight))+4,h:n*1.4,size:n,tone:"ink"});s===b&&(u=new Set([a.id]),b++),A()}catch{}}});let le=!1;addEventListener("keydown",e=>{if(["INPUT","TEXTAREA"].includes(document.activeElement?.tagName)||r("#source-dialog").open)return;e.code==="Space"&&(e.preventDefault(),le=!0);const t=e.key.toLowerCase();t==="escape"&&(f?nt():(u.clear(),b++,A()));const n=Ve.find(o=>o[2].toLowerCase()===t);n&&!e.ctrlKey&&!e.metaKey&&Se(n[0]),(t==="delete"||t==="backspace")&&u.size&&!f&&(e.preventDefault(),w("erase",{ids:[...u]}).catch(()=>{}),u.clear(),b++)});addEventListener("keyup",e=>{e.code==="Space"&&(le=!1)});addEventListener("blur",()=>le=!1);r("#clear-selection").onclick=()=>{u.clear(),b++,A()};async function Dt(){for(;re;)await Ee,await new Promise(e=>setTimeout(e))}r("#send").onclick=async()=>{if(r("#send").getAttribute("aria-disabled")!=="true"){W(r("#send"),!0);try{await Dt(),f=await w("interpret",{ids:[...u]}),r("#correction-text").value=f.text,r("#clarification").textContent=f.question||"",r("#clarification").hidden=!f.question,W(r("#confirm"),f.needsClarification||!f.text.trim()),A(),r("#correction-text").focus()}catch{}finally{W(r("#send"),!1)}}};r("#correction-text").oninput=()=>{f&&(f.clarified=!0),W(r("#confirm"),!r("#correction-text").value.trim())};async function nt(){if(f){const e=f.id;f=null,await w("cancel",{id:e}).catch(()=>{})}A()}r("#cancel").onclick=nt;r("#confirm").onclick=async()=>{if(!(!f||r("#confirm").getAttribute("aria-disabled")==="true")){W(r("#confirm"),!0);try{await w("confirm",{id:f.id,text:r("#correction-text").value,clarified:!!f.clarified}),f=null,u.clear(),b++,A(),G("Direction queued for your next terminal turn.")}catch{}finally{W(r("#confirm"),!1)}}};r("#session").onclick=()=>{r("#session-popover").hidden=!r("#session-popover").hidden,r("#session").setAttribute("aria-expanded",String(!r("#session-popover").hidden)),r("#menu-popover").hidden=!0};r(".close-popover").onclick=()=>r("#session-popover").hidden=!0;r("#more").onclick=()=>{r("#menu-popover").hidden=!r("#menu-popover").hidden,r("#session-popover").hidden=!0};r("#toggle-observation").onclick=()=>{c.observer.recoveryRequired?window.confirm("Have you checked that the previous observer and its child processes have stopped? Only confirm after ending them in your process manager or restarting your machine.")&&w("observer-stopped",{confirmed:!0}).catch(()=>{}):w("off",{}).catch(()=>{})};r("#retry").onclick=()=>w("retry-observer",{}).catch(()=>{});r("#show-source").onclick=()=>{const e=c.turns.find(n=>n.id===r("#show-source").dataset.turn);if(!e)return;const t=r("#source-content");t.replaceChildren();for(const[n,o]of[[e.source==="attachment"?"Board opened in this conversation":"You",e.user],["Your agent · final visible reply",e.assistant]]){const i=document.createElement("h4");i.textContent=n;const s=document.createElement("p");s.textContent=o,t.append(i,s)}r("#source-dialog").showModal()};r("#close-source").onclick=()=>r("#source-dialog").close();function ot(e,t){const n=URL.createObjectURL(e),o=document.createElement("a");o.href=n,o.download=t,o.click(),setTimeout(()=>URL.revokeObjectURL(n),1e4)}r("#export-recap").onclick=()=>{const e=c.corrections.filter(n=>n.status!=="cancelled"),t=`flatland board — confirmed direction
2
+ ${new Date().toISOString()}
3
+ Session: ${c.session?.harness||"unattached"} ${c.session?.id||""}
4
+
5
+ ${e.length?e.map(n=>`• ${n.text}
6
+ ${n.status==="failed"?"Delivery failed; retained in queue, no automatic resend":n.status}
7
+ `).join(`
8
+ `):"No direction has been confirmed. Ink on the board remains exploration."}
9
+
10
+ A confirmed correction does not authorize consequential actions.
11
+ `;ot(new Blob([t],{type:"text/plain"}),"board-direction.txt"),r("#menu-popover").hidden=!0};r("#export-image").onclick=async()=>{if(!c.marks.length){G("There is no ink to export yet.");return}let e;try{await document.fonts.ready;const t=Ge(c.marks),n=H.space[9],o=t.w+n*2,i=t.h+n*2,s=y("svg",{xmlns:Ze,width:o,height:i,viewBox:`${t.x-n} ${t.y-n} ${o} ${i}`});s.append(y("rect",{x:t.x-n,y:t.y-n,width:o,height:i,fill:U().paper}));const a=await Promise.all([[ct,400],[lt,500]].map(async([m,k])=>{const D=await fetch(m);if(!D.ok)throw new Error("The local export font could not be loaded.");const R=await new Promise((Q,T)=>{const $=new FileReader;$.onload=()=>Q($.result),$.onerror=T,D.blob().then(ee=>$.readAsDataURL(ee),T)});return`@font-face{font-family:${H.font.words.split(",")[0]};font-weight:${k};src:url(${R})}`}));s.append(y("style",{},a.join(`
12
+ `)));const d=se.cloneNode(!0);d.querySelectorAll(".mark-appear").forEach(m=>m.removeAttribute("class")),s.append(d),e=URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(s)],{type:"image/svg+xml;charset=utf-8"}));const g=new Image;g.src=e,await g.decode();const M=Math.min(2,8e3/Math.max(o,i)),x=document.createElement("canvas");x.width=o*M,x.height=i*M;const N=x.getContext("2d");N.scale(M,M),N.drawImage(g,0,0,o,i);const p=await new Promise(m=>x.toBlob(m,"image/png"));if(p)ot(p,"flatland-board.png");else throw new Error("Image export failed")}catch(t){G("The image could not be exported from this board. Saved ink is unchanged. Open Menu and export again. Detail: "+t.message)}finally{e&&URL.revokeObjectURL(e),r("#menu-popover").hidden=!0}};