@xingular/harness 1.3.1-unstable.2 → 1.3.1-unstable.4

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +3680 -1947
  4. package/dist/sdk/cli.js +2 -0
  5. package/dist/sdk/commands/registry.d.ts +15 -0
  6. package/dist/sdk/commands/registry.js +2 -0
  7. package/dist/sdk/computer/screenshot.d.ts +32 -0
  8. package/dist/sdk/computer/screenshot.js +35 -0
  9. package/dist/sdk/context/attribution.d.ts +18 -0
  10. package/dist/sdk/context/attribution.js +295 -0
  11. package/dist/sdk/context/types.d.ts +94 -0
  12. package/dist/sdk/context/types.js +10 -0
  13. package/dist/sdk/index.d.ts +2 -1
  14. package/dist/sdk/index.js +2 -1
  15. package/dist/sdk/plugins/builtin.js +1 -0
  16. package/dist/sdk/prompt/compile.d.ts +42 -2
  17. package/dist/sdk/prompt/compile.js +87 -15
  18. package/dist/sdk/remote/addresses.d.ts +19 -0
  19. package/dist/sdk/remote/addresses.js +52 -0
  20. package/dist/sdk/remote/pairing.d.ts +59 -0
  21. package/dist/sdk/remote/pairing.js +160 -0
  22. package/dist/sdk/remote/seal.d.ts +28 -0
  23. package/dist/sdk/remote/seal.js +84 -0
  24. package/dist/sdk/remote/server.d.ts +28 -0
  25. package/dist/sdk/remote/server.js +158 -0
  26. package/dist/sdk/remote/service.d.ts +125 -0
  27. package/dist/sdk/remote/service.js +330 -0
  28. package/dist/sdk/remote/webClient.d.ts +4 -0
  29. package/dist/sdk/remote/webClient.js +444 -0
  30. package/dist/sdk/server/methods.js +3 -0
  31. package/dist/sdk/sessions/contracts.d.ts +4 -0
  32. package/dist/sdk/sessions/contracts.js +2 -0
  33. package/dist/sdk/sessions/planMode.d.ts +26 -1
  34. package/dist/sdk/sessions/planMode.js +48 -1
  35. package/dist/sdk/sessions/runner.d.ts +12 -0
  36. package/dist/sdk/sessions/runner.js +34 -2
  37. package/dist/sdk/sessions/sessions.js +1 -0
  38. package/dist/sdk/sessions/subagentService.d.ts +11 -1
  39. package/dist/sdk/sessions/subagentService.js +120 -38
  40. package/dist/sdk/sessions/subagents.d.ts +31 -0
  41. package/dist/sdk/sessions/subagents.js +45 -0
  42. package/dist/sdk/sessions/workSlice.d.ts +5 -9
  43. package/dist/sdk/sessions/workSlice.js +8 -149
  44. package/dist/sdk/terminal/client.d.ts +3 -1
  45. package/dist/sdk/terminal/client.js +15 -6
  46. package/dist/sdk/terminal/control.d.ts +1 -1
  47. package/dist/sdk/terminal/control.js +2 -2
  48. package/dist/sdk/terminal/controller.d.ts +22 -0
  49. package/dist/sdk/terminal/controller.js +104 -13
  50. package/dist/sdk/terminal/piExtension.js +106 -4
  51. package/dist/sdk/terminal/stallWatch.d.ts +13 -0
  52. package/dist/sdk/terminal/stallWatch.js +16 -0
  53. package/dist/sdk/terminal/standalone.d.ts +2 -0
  54. package/dist/sdk/terminal/standalone.js +9 -2
  55. package/dist/sdk/terminal/terminals.d.ts +14 -0
  56. package/dist/sdk/terminal/terminals.js +98 -2
  57. package/dist/sdk/terminal/tui/commandPages.d.ts +4 -1
  58. package/dist/sdk/terminal/tui/commandPages.js +12 -0
  59. package/dist/sdk/terminal/tui/commands.d.ts +3 -2
  60. package/dist/sdk/terminal/tui/commands.js +19 -5
  61. package/dist/sdk/terminal/tui/dialogs.d.ts +3 -2
  62. package/dist/sdk/terminal/tui/dialogs.js +4 -3
  63. package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
  64. package/dist/sdk/terminal/tui/keybindings.js +1 -1
  65. package/dist/sdk/terminal/tui/markdown.d.ts +16 -0
  66. package/dist/sdk/terminal/tui/markdown.js +55 -0
  67. package/dist/sdk/terminal/tui/modeChoices.d.ts +3 -2
  68. package/dist/sdk/terminal/tui/modeChoices.js +4 -4
  69. package/dist/sdk/terminal/tui/mouseReport.d.ts +30 -0
  70. package/dist/sdk/terminal/tui/mouseReport.js +80 -0
  71. package/dist/sdk/terminal/tui/pages/annotatePage.js +3 -2
  72. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +1 -1
  73. package/dist/sdk/terminal/tui/pages/contextPage.d.ts +62 -0
  74. package/dist/sdk/terminal/tui/pages/contextPage.js +484 -0
  75. package/dist/sdk/terminal/tui/pages/remoteControlPage.d.ts +32 -0
  76. package/dist/sdk/terminal/tui/pages/remoteControlPage.js +170 -0
  77. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +5 -4
  78. package/dist/sdk/terminal/tui/qr.d.ts +4 -0
  79. package/dist/sdk/terminal/tui/qr.js +35 -0
  80. package/dist/sdk/terminal/tui/runtime.js +8 -2
  81. package/dist/sdk/terminal/tui/search.js +3 -2
  82. package/dist/sdk/terminal/tui/sideQuestion.d.ts +1 -0
  83. package/dist/sdk/terminal/tui/sideQuestion.js +8 -3
  84. package/dist/sdk/terminal/tui/skillInvocation.d.ts +21 -2
  85. package/dist/sdk/terminal/tui/skillInvocation.js +31 -11
  86. package/dist/sdk/terminal/tui/status.d.ts +6 -0
  87. package/dist/sdk/terminal/tui/status.js +15 -1
  88. package/dist/sdk/terminal/tui/theme.d.ts +9 -0
  89. package/dist/sdk/terminal/tui/theme.js +9 -0
  90. package/dist/sdk/tools/defaultSkills.d.ts +19 -0
  91. package/dist/sdk/tools/defaultSkills.js +27 -1
  92. package/dist/sdk/tools/dispatch.js +8 -9
  93. package/dist/sdk/tools/spawnSubagent.d.ts +1 -1
  94. package/dist/sdk/tools/spawnSubagent.js +4 -2
  95. package/dist/terminal-opentui.js +1607 -492
  96. package/dist/terminal-pi.js +256 -144
  97. package/docs/README.md +1 -0
  98. package/docs/architecture/02-sessions-and-runs.md +55 -22
  99. package/docs/architecture/04-prompt.md +23 -4
  100. package/docs/architecture/07-tools-and-gate.md +4 -2
  101. package/docs/architecture/10-server-protocol.md +24 -0
  102. package/docs/architecture/11-terminal.md +52 -10
  103. package/docs/architecture/12-storage.md +5 -2
  104. package/docs/architecture/13-world-tools.md +17 -3
  105. package/docs/architecture/14-surfaces-and-parity.md +7 -3
  106. package/docs/architecture/15-remote-control.md +118 -0
  107. package/docs/architecture/README.md +3 -0
  108. package/docs/architecture/proposals/PROP-0049-context-attribution.md +409 -0
  109. package/docs/architecture/proposals/README.md +2 -1
  110. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +4 -0
  111. package/docs/decisions/ADR-0048-required-skills-from-a-message.md +42 -0
  112. package/docs/decisions/ADR-0050-approved-window-screenshots.md +19 -0
  113. package/docs/decisions/ADR-0051-terminal-decision-wait.md +10 -0
  114. package/docs/decisions/ADR-0052-per-profile-subagent-budgets.md +25 -0
  115. package/docs/decisions/ADR-0053-no-repeated-work-guard.md +45 -0
  116. package/docs/decisions/ADR-0054-terminal-reply-heartbeat-and-stall.md +52 -0
  117. package/docs/decisions/ADR-0055-plan-output-repair.md +51 -0
  118. package/docs/decisions/ADR-0056-remote-control.md +53 -0
  119. package/docs/decisions/ADR-0057-structured-plan-outcomes.md +39 -0
  120. package/docs/decisions/ADR-0058-ask-user-only-plan-clarification.md +36 -0
  121. package/docs/parity.md +14 -10
  122. package/docs/releases/1.3.1-unstable.3.md +54 -0
  123. package/docs/releases/1.3.1-unstable.4.md +56 -0
  124. package/docs/terminal-tui.md +43 -18
  125. package/package.json +3 -1
@@ -0,0 +1,330 @@
1
+ // Remote control of one terminal session (15-remote-control.md). A paired phone becomes a second
2
+ // person at the terminal: it sends prompts, watches, interrupts, and answers decisions, all through
3
+ // the same controller and Terminals methods the terminal uses (INV-RC-3). It reaches nothing else:
4
+ // no commands, no shell, no links, no plugins or accounts (INV-RC-6).
5
+ import { createRequire } from 'node:module';
6
+ import { readFileSync } from 'node:fs';
7
+ import { DEFAULT_PRODUCT_NAME } from "../core/env.js";
8
+ import { HarnessError } from "../core/errors.js";
9
+ import { hostPart, pickRemoteAddress, remoteAddresses } from "./addresses.js";
10
+ import { Pairing } from "./pairing.js";
11
+ import { listenRemote } from "./server.js";
12
+ import { PAGE_CSS, PAGE_JS, pageHtml, pageManifest } from "./webClient.js";
13
+ const MAX_TEXT = 1_000_000;
14
+ const MAX_HISTORY = 400;
15
+ const text = (value, size = MAX_TEXT) => (typeof value === 'string' ? value.slice(0, size) : '');
16
+ export function projectEvent(event) {
17
+ const payload = (event.payload ?? {});
18
+ const runId = event.runId;
19
+ switch (event.type) {
20
+ case 'message.delta':
21
+ return { kind: 'delta', runId, messageId: text(payload.messageId, 200), text: text(payload.text) };
22
+ case 'message.completed':
23
+ return { kind: 'message', runId, messageId: text(payload.messageId, 200), role: payload.role === 'user' ? 'user' : 'assistant', text: text(payload.text) };
24
+ case 'message.steered':
25
+ return { kind: 'message', runId, messageId: event.id, role: 'user', text: text(payload.text) };
26
+ case 'tool.started':
27
+ return { kind: 'tool', runId, id: text(payload.toolCallId, 200), name: text(payload.name, 200), phase: 'start', summary: text(payload.summary, 400) };
28
+ case 'tool.completed':
29
+ return { kind: 'tool', runId, id: text(payload.toolCallId, 200), name: text(payload.name, 200), phase: 'end', ok: payload.ok !== false };
30
+ case 'started':
31
+ return { kind: 'run', runId, status: 'running' };
32
+ case 'completed':
33
+ case 'failed':
34
+ return { kind: 'run', runId, status: event.type };
35
+ case 'status.changed':
36
+ return { kind: 'run', runId, status: text(payload.status, 40) || 'running' };
37
+ case 'todo.updated': {
38
+ const items = Array.isArray(payload.items) ? payload.items.slice(0, 50) : [];
39
+ return { kind: 'todo', runId, items: items.map((item) => ({ text: text(item?.text, 500), status: text(item?.status, 20) })) };
40
+ }
41
+ default:
42
+ return null;
43
+ }
44
+ }
45
+ /** A pending decision as the phone shows it. The approval's full action text is kept whole. */
46
+ export function projectRequest(request) {
47
+ const options = Array.isArray(request.options) ? request.options.slice(0, 12).map((option) => {
48
+ const value = option;
49
+ return { label: text(value?.label, 200), description: text(value?.description, 600), recommended: value?.recommended === true };
50
+ }) : [];
51
+ return {
52
+ requestId: text(request.requestId, 200),
53
+ type: request.type === 'question' ? 'question' : 'approval',
54
+ runId: text(request.runId, 200),
55
+ target: text(request.target, 400),
56
+ action: text(request.action, 64_000),
57
+ rememberable: request.rememberable === true,
58
+ summary: text(request.summary, 2_000),
59
+ question: text(request.question, 4_000),
60
+ options,
61
+ multiSelect: request.multiSelect === true,
62
+ batchId: text(request.batchId, 200),
63
+ batchIndex: typeof request.batchIndex === 'number' ? request.batchIndex : 0
64
+ };
65
+ }
66
+ /** What the phone may type. Commands and shell escapes belong to the terminal (INV-RC-6). */
67
+ export function checkPrompt(value) {
68
+ const prompt = typeof value === 'string' ? value : '';
69
+ if (!prompt.trim())
70
+ throw new HarnessError('invalid_request', 'Message is empty.');
71
+ if (prompt.length > MAX_TEXT)
72
+ throw new HarnessError('too_large', 'This message is too large.');
73
+ const first = prompt.trimStart()[0];
74
+ if (first === '/' || first === '!')
75
+ throw new HarnessError('invalid_request', 'Slash commands and ! shell commands run only at the terminal.');
76
+ return prompt;
77
+ }
78
+ let naclSource = null;
79
+ function naclScript() {
80
+ naclSource ??= readFileSync(createRequire(import.meta.url).resolve('tweetnacl/nacl-fast.min.js'), 'utf8');
81
+ return naclSource;
82
+ }
83
+ export class RemoteControl {
84
+ session;
85
+ options;
86
+ listening = null;
87
+ address = null;
88
+ pairing;
89
+ connections = new Set();
90
+ revision = 0;
91
+ waiters = new Set();
92
+ unlisten = null;
93
+ pendingLoop = null;
94
+ pendingRequests = [];
95
+ starting = null;
96
+ ended = false;
97
+ constructor(session, options = {}) {
98
+ this.session = session;
99
+ this.options = options;
100
+ this.pairing = new Pairing(() => this.changed());
101
+ }
102
+ changed() {
103
+ this.revision += 1;
104
+ for (const wake of this.waiters)
105
+ wake();
106
+ }
107
+ status() {
108
+ const kinds = [...new Set((this.options.addresses ?? remoteAddresses)().map((address) => address.kind))];
109
+ const connected = new Set([...this.connections].map((connection) => connection.device.id));
110
+ return {
111
+ running: !!this.listening,
112
+ revision: this.revision,
113
+ url: this.listening && this.address ? `${this.base()}/#${this.pairing.fragment}` : null,
114
+ kind: this.listening ? this.address?.kind ?? null : null,
115
+ kinds,
116
+ devices: this.pairing.list().map((device) => ({ id: device.id, label: device.label, connected: connected.has(device.id) })),
117
+ request: this.pairing.request,
118
+ warning: this.pairing.warning
119
+ };
120
+ }
121
+ /** Returns at once for a changed revision, otherwise after a change or 20 seconds. */
122
+ async waitStatus(revision) {
123
+ if (revision === this.revision && !this.ended) {
124
+ await new Promise((resolve) => {
125
+ const wake = () => { clearTimeout(timer); this.waiters.delete(wake); resolve(); };
126
+ const timer = setTimeout(wake, 20_000);
127
+ timer.unref?.();
128
+ this.waiters.add(wake);
129
+ });
130
+ }
131
+ return this.status();
132
+ }
133
+ base() {
134
+ const address = this.address;
135
+ return `http://${hostPart(address)}:${this.listening.port}`;
136
+ }
137
+ /** Starts listening, or switches address kind. Starting again with the same kind changes nothing. */
138
+ async start(choice = 'auto') {
139
+ if (this.ended)
140
+ throw new HarnessError('terminal_not_ready', 'This terminal has ended.');
141
+ if (this.starting)
142
+ await this.starting.catch(() => undefined);
143
+ if (this.listening && (choice === 'auto' || choice === this.address?.kind))
144
+ return this.status();
145
+ this.starting = this.listen(choice);
146
+ try {
147
+ return await this.starting;
148
+ }
149
+ finally {
150
+ this.starting = null;
151
+ }
152
+ }
153
+ async listen(choice) {
154
+ const address = pickRemoteAddress(choice, (this.options.addresses ?? remoteAddresses)());
155
+ await this.close(false);
156
+ const host = this.options.bindHost ?? address.address;
157
+ this.listening = await listenRemote(host, this.options.port ?? 0, {
158
+ asset: (path) => this.asset(path),
159
+ hello: async (body) => ({ f: await this.pairing.hello(body.p, body.f) }),
160
+ call: (body) => this.call(body),
161
+ stream: (deviceId, frame, stream) => this.openStream(deviceId, frame, stream)
162
+ });
163
+ this.address = this.options.bindHost ? { ...address, address: this.options.bindHost, family: host.includes(':') ? 'IPv6' : 'IPv4' } : address;
164
+ this.unlisten = this.session.listen((event) => {
165
+ const projected = projectEvent(event);
166
+ if (projected)
167
+ this.broadcast({ type: 'event', event: projected });
168
+ });
169
+ this.changed();
170
+ return this.status();
171
+ }
172
+ /** A new pairing link; every paired phone is disconnected. */
173
+ rotate() {
174
+ for (const connection of [...this.connections])
175
+ this.drop(connection, { type: 'unpaired' });
176
+ this.pairing.rotate();
177
+ return this.status();
178
+ }
179
+ confirm(requestId, allow) {
180
+ this.pairing.confirm(requestId, allow);
181
+ return this.status();
182
+ }
183
+ /** Stops remote control; phones are told the session is no longer reachable. */
184
+ async stop() {
185
+ await this.close(true);
186
+ this.changed();
187
+ return this.status();
188
+ }
189
+ /** The terminal session ended (INV-RC-5). */
190
+ async dispose() {
191
+ if (this.ended)
192
+ return;
193
+ await this.close(true);
194
+ this.ended = true;
195
+ this.changed();
196
+ }
197
+ async close(unpair) {
198
+ this.unlisten?.();
199
+ this.unlisten = null;
200
+ this.pendingLoop?.abort();
201
+ this.pendingLoop = null;
202
+ for (const connection of [...this.connections])
203
+ this.drop(connection, { type: 'ended' });
204
+ const listening = this.listening;
205
+ this.listening = null;
206
+ if (unpair)
207
+ this.pairing = new Pairing(() => this.changed());
208
+ await listening?.close();
209
+ }
210
+ get productName() {
211
+ return this.options.productName ?? DEFAULT_PRODUCT_NAME;
212
+ }
213
+ asset(path) {
214
+ if (path === '/')
215
+ return { type: 'text/html; charset=utf-8', body: pageHtml(this.productName) };
216
+ if (path === '/app.js')
217
+ return { type: 'text/javascript; charset=utf-8', body: PAGE_JS };
218
+ if (path === '/app.css')
219
+ return { type: 'text/css; charset=utf-8', body: PAGE_CSS };
220
+ if (path === '/nacl.js')
221
+ return { type: 'text/javascript; charset=utf-8', body: naclScript() };
222
+ if (path === '/manifest.webmanifest')
223
+ return { type: 'application/manifest+json', body: pageManifest(this.productName) };
224
+ return null;
225
+ }
226
+ send(connection, body) {
227
+ connection.stream.send(this.pairing.sealFor(connection.device, body));
228
+ }
229
+ broadcast(body) {
230
+ for (const connection of this.connections)
231
+ this.send(connection, body);
232
+ }
233
+ drop(connection, last) {
234
+ try {
235
+ this.send(connection, last);
236
+ }
237
+ catch { }
238
+ this.connections.delete(connection);
239
+ connection.stream.end();
240
+ }
241
+ openStream(deviceId, frame, stream) {
242
+ const { device } = this.pairing.openFrom(deviceId, frame);
243
+ const connection = { device, stream };
244
+ this.connections.add(connection);
245
+ this.send(connection, { type: 'pending', requests: this.pendingRequests.map(projectRequest) });
246
+ this.watchPending();
247
+ this.changed();
248
+ return () => {
249
+ if (this.connections.delete(connection))
250
+ this.changed();
251
+ if (!this.connections.size) {
252
+ this.pendingLoop?.abort();
253
+ this.pendingLoop = null;
254
+ }
255
+ };
256
+ }
257
+ /** Follows the controller's decisions while a phone is connected, as the terminal itself does. */
258
+ watchPending() {
259
+ if (this.pendingLoop)
260
+ return;
261
+ const loop = new AbortController();
262
+ this.pendingLoop = loop;
263
+ void (async () => {
264
+ let revision;
265
+ while (!loop.signal.aborted) {
266
+ try {
267
+ const snapshot = await this.session.control('terminal.pending.wait', { revision });
268
+ if (loop.signal.aborted)
269
+ return;
270
+ const changed = snapshot.revision !== revision;
271
+ revision = snapshot.revision;
272
+ this.pendingRequests = snapshot.requests;
273
+ if (changed)
274
+ this.broadcast({ type: 'pending', requests: snapshot.requests.map(projectRequest) });
275
+ }
276
+ catch {
277
+ if (loop.signal.aborted)
278
+ return;
279
+ await new Promise((resolve) => setTimeout(resolve, 1000).unref?.());
280
+ revision = undefined;
281
+ }
282
+ }
283
+ })();
284
+ }
285
+ async call(body) {
286
+ const { device, body: request } = this.pairing.openFrom(body.d, body.f);
287
+ const id = request?.id ?? null;
288
+ const params = (request?.params && typeof request.params === 'object' ? request.params : {});
289
+ try {
290
+ const result = await this.dispatch(device, String(request?.method ?? ''), params);
291
+ return { f: this.pairing.sealFor(device, { id, result }) };
292
+ }
293
+ catch (error) {
294
+ const shape = error instanceof HarnessError ? { code: error.code, message: error.message } : { code: 'harness_error', message: error instanceof Error ? error.message : String(error) };
295
+ return { f: this.pairing.sealFor(device, { id, error: shape }) };
296
+ }
297
+ }
298
+ async dispatch(device, method, params) {
299
+ switch (method) {
300
+ case 'snapshot': {
301
+ const events = this.session.history().map(projectEvent).filter((event) => !!event && event.kind !== 'delta');
302
+ return {
303
+ title: this.session.title(),
304
+ device: device.label,
305
+ events: events.slice(-MAX_HISTORY),
306
+ pending: this.pendingRequests.map(projectRequest),
307
+ state: this.session.state()
308
+ };
309
+ }
310
+ case 'send':
311
+ return this.session.send(checkPrompt(params.text));
312
+ case 'interrupt':
313
+ return this.session.control('terminal.interrupt', {});
314
+ case 'approve':
315
+ return this.session.control('terminal.approve', { requestId: String(params.requestId ?? ''), decision: params.decision, alwaysAllow: params.alwaysAllow === true });
316
+ case 'answer':
317
+ return this.session.control('terminal.answer', { requestId: String(params.requestId ?? ''), answer: params.answer });
318
+ case 'answers':
319
+ return this.session.control('terminal.answers', { batchId: String(params.batchId ?? ''), answers: params.answers });
320
+ case 'unpair':
321
+ this.pairing.forget(device.id);
322
+ for (const connection of [...this.connections])
323
+ if (connection.device === device)
324
+ this.drop(connection, { type: 'unpaired' });
325
+ return {};
326
+ default:
327
+ throw new HarnessError('unknown_method', 'The phone asked for something remote control does not offer.');
328
+ }
329
+ }
330
+ }
@@ -0,0 +1,4 @@
1
+ export declare function pageHtml(productName: string): string;
2
+ export declare function pageManifest(productName: string): string;
3
+ export declare const PAGE_CSS = "\n:root {\n --bg: #f6f6f4; --panel: #ffffff; --text: #18181b; --muted: #6b6b73; --line: #e2e2de;\n --accent: #2b59c3; --accent-text: #ffffff; --user: #e8eefb; --danger: #b42318; --ok: #1f7a3f;\n --mono: ui-monospace, SFMono-Regular, Menlo, monospace;\n color-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n :root { --bg: #111214; --panel: #1b1c1f; --text: #ececee; --muted: #9a9aa3; --line: #2c2d31; --accent: #7aa2ff; --accent-text: #0d0f14; --user: #1f2a44; --danger: #ff7b72; --ok: #5bd08a; }\n}\n* { box-sizing: border-box; }\nhtml, body { margin: 0; height: 100%; }\nbody {\n background: var(--bg); color: var(--text);\n font: 16px/1.45 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n display: flex; flex-direction: column; max-width: 860px; margin: 0 auto;\n padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);\n}\n.bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--line); }\n.title { display: flex; flex-direction: column; min-width: 0; }\n.title strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.muted { color: var(--muted); font-size: 13px; }\n.chip { font-size: 13px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); white-space: nowrap; }\n.chip.working { color: var(--accent); border-color: var(--accent); }\n.chip.bad { color: var(--danger); border-color: var(--danger); }\n#log { flex: 1; overflow-y: auto; padding: 12px 16px; -webkit-overflow-scrolling: touch; }\n.msg { margin: 10px 0; white-space: pre-wrap; overflow-wrap: anywhere; }\n.msg.user { margin-left: 15%; background: var(--user); padding: 8px 12px; border-radius: 14px 14px 4px 14px; }\n.msg.assistant { padding: 2px 0; }\n.tool { margin: 4px 0; font: 13px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }\n.tool.failed { color: var(--danger); }\n#todo { border-top: 1px solid var(--line); padding: 8px 16px; font-size: 14px; max-height: 30vh; overflow-y: auto; }\n#todo li { list-style: none; margin: 2px 0; }\n#todo ul { margin: 0; padding: 0; }\n#cards { padding: 0 16px; }\n.card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px; margin: 10px 0; }\n.card h2 { font-size: 15px; margin: 0 0 6px; }\n.card pre { font: 13px/1.45 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; max-height: 40vh; overflow-y: auto; background: var(--bg); padding: 8px; border-radius: 8px; margin: 6px 0; }\n.card .row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }\n.card label.option { display: flex; gap: 8px; align-items: flex-start; padding: 8px; border: 1px solid var(--line); border-radius: 8px; margin: 6px 0; }\n.card label.option small { display: block; color: var(--muted); }\n.card input[type=text] { width: 100%; font: inherit; padding: 8px; border-radius: 8px; border: 1px solid var(--line); background: var(--bg); color: var(--text); margin-top: 6px; }\n#notice { margin: 0 16px 8px; padding: 8px 12px; border-radius: 8px; background: var(--panel); border: 1px solid var(--line); font-size: 14px; }\n#notice.bad { border-color: var(--danger); color: var(--danger); }\n.composer { display: flex; gap: 8px; align-items: flex-end; padding: 8px 16px calc(8px + env(safe-area-inset-bottom)); border-top: 1px solid var(--line); background: var(--bg); }\ntextarea { flex: 1; resize: none; font: inherit; padding: 9px 12px; border-radius: 18px; border: 1px solid var(--line); background: var(--panel); color: var(--text); max-height: 40vh; }\nbutton { font: inherit; font-weight: 600; padding: 9px 16px; border-radius: 18px; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-text); }\nbutton.secondary { background: transparent; color: var(--text); border-color: var(--line); }\nbutton.danger { background: transparent; color: var(--danger); border-color: var(--danger); }\nbutton:disabled { opacity: .5; }\n#cover { position: fixed; inset: 0; background: var(--bg); display: flex; align-items: center; justify-content: center; padding: 24px; }\n#cover .panel { max-width: 420px; text-align: center; }\n#cover h1 { font-size: 20px; }\n#cover p { color: var(--muted); }\n[hidden] { display: none !important; }\n";
4
+ export declare const PAGE_JS: string;