conductor-remote 1.40.0 → 1.41.0

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/index.html CHANGED
@@ -13,8 +13,8 @@
13
13
  <title>Conductor Remote</title>
14
14
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
15
15
  <script src="/self-heal.js"></script>
16
- <script type="module" crossorigin src="/assets/index-BhYD1Ar2.js"></script>
17
- <link rel="stylesheet" crossorigin href="/assets/index-UopM0hHW.css">
16
+ <script type="module" crossorigin src="/assets/index-BSDnFvjd.js"></script>
17
+ <link rel="stylesheet" crossorigin href="/assets/index-95IdnA_r.css">
18
18
  <link rel="manifest" href="/manifest.webmanifest"></head>
19
19
  <body>
20
20
  <div id="root"></div>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const c=e=>i(e,o),t={module:{uri:o},exports:l,require:c};s[o]=Promise.all(n.map(e=>t[e]||c(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"1dca8269fa4358508f3ca90f0c216cdb"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-UopM0hHW.css",revision:null},{url:"assets/index-BhYD1Ar2.js",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"9a6849700ab5e2d2b416d9108090e5af"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-BSDnFvjd.js",revision:null},{url:"assets/index-95IdnA_r.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
@@ -4,6 +4,29 @@ import path from 'node:path';
4
4
  import { describeRepoIcon, resolveRepoIcon } from "./icons.js";
5
5
  import { parseMessage } from "./transcript.js";
6
6
  const worktreeCache = new Map();
7
+ /**
8
+ * Neutralize LIKE's own wildcards in a user's search word. `queryTokens` already
9
+ * strips everything but letters, digits and `_` — and `_` is LIKE's single-character
10
+ * wildcard, so "add_set" would quietly also match "addaset" without this.
11
+ */
12
+ function escapeLike(s) {
13
+ return s.replace(/[\\%_]/g, m => `\\${m}`);
14
+ }
15
+ /** Shared row → `SearchWorkspace` shape for the two search reads below. */
16
+ function toSearchWorkspace(r) {
17
+ return {
18
+ id: r.id,
19
+ workspace_name: r.workspace_name,
20
+ pr_title: r.pr_title,
21
+ branch: r.branch,
22
+ directory_name: r.directory_name,
23
+ state: r.state,
24
+ updated_at: r.updated_at,
25
+ repo_name: r.repo_name,
26
+ icon: describeRepoIcon({ icon: r.repo_icon, repoRoot: r.repo_root, remoteUrl: r.remote_url }),
27
+ archived: r.state === 'archived'
28
+ };
29
+ }
7
30
  /**
8
31
  * Conductor's sidebar title for a workspace: manual name → PR title → humanized
9
32
  * branch → worktree codename → id. Deliberately a second copy of the web's
@@ -131,6 +154,55 @@ export class Reads {
131
154
  icon: describeRepoIcon({ icon: r.icon, repoRoot: r.root_path, remoteUrl: r.remote_url })
132
155
  }));
133
156
  }
157
+ /**
158
+ * Map chat ids to the workspace that owns them, archived included.
159
+ *
160
+ * `listWorkspaces` filters to `state IN ('ready','setting_up')`, which is right for
161
+ * the sidebar and wrong for search: the whole point of searching the transcript is
162
+ * to reach work you finished and put away. One query for the whole hit set, because
163
+ * a search fans out over up to 300 chunks and a per-chat lookup would be 300 of them.
164
+ */
165
+ searchTargets(sessionIds) {
166
+ const out = new Map();
167
+ if (!sessionIds.length)
168
+ return out;
169
+ const holes = sessionIds.map(() => '?').join(',');
170
+ const rows = this.db.query(`SELECT s.id AS session_id, s.title AS session_title,
171
+ w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
172
+ r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
173
+ FROM sessions s
174
+ JOIN workspaces w ON w.id = s.workspace_id
175
+ LEFT JOIN repos r ON r.id = w.repository_id
176
+ WHERE s.id IN (${holes})`, sessionIds);
177
+ for (const r of rows)
178
+ out.set(r.session_id, { sessionTitle: r.session_title, workspace: toSearchWorkspace(r) });
179
+ return out;
180
+ }
181
+ /**
182
+ * Workspaces whose own identity matches every token — name, PR title, branch,
183
+ * worktree codename or repo. Archived included, same reason as `searchTargets`.
184
+ *
185
+ * This is the half of search the transcript index cannot do. A workspace named for
186
+ * the thing you are looking for may never have said those words in its chat, and
187
+ * one whose chat is empty has no chunks at all.
188
+ */
189
+ findWorkspacesByName(tokens, limit = 20) {
190
+ if (!tokens.length)
191
+ return [];
192
+ const fields = ['w.workspace_name', 'w.pr_title', 'w.branch', 'w.directory_name', 'r.name'];
193
+ // AND across tokens, OR across fields: "auk lamp" should find the lamp workspace in
194
+ // the auk repo, where no single column holds both words.
195
+ const where = tokens.map(() => `(${fields.map(f => `${f} LIKE ? ESCAPE '\\'`).join(' OR ')})`).join(' AND ');
196
+ const params = tokens.flatMap(t => fields.map(() => `%${escapeLike(t)}%`));
197
+ const rows = this.db.query(`SELECT w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
198
+ r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
199
+ FROM workspaces w
200
+ LEFT JOIN repos r ON r.id = w.repository_id
201
+ WHERE ${where}
202
+ ORDER BY (w.state = 'archived'), w.updated_at DESC
203
+ LIMIT ?`, [...params, limit]);
204
+ return rows.map(toSearchWorkspace);
205
+ }
134
206
  /** Resolve a repo's icon by its name (the sidebar avatar) — null if the repo or icon is unknown. */
135
207
  resolveRepoIcon(repoName) {
136
208
  const rows = this.db.query('SELECT root_path FROM repos WHERE name = ? LIMIT 1', [
@@ -0,0 +1,333 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { parseMessage } from "./transcript.js";
5
+ /**
6
+ * Full-text search over the chat history, in a sidecar DB the relay owns.
7
+ *
8
+ * Two facts decide the whole shape of this file.
9
+ *
10
+ * **Prose is 1.2% of the transcript.** Measured over this Mac's 1.7M
11
+ * `session_messages` rows (3,106 MB of `content`): tool_result output is 799 MB,
12
+ * `type:"system"` frames 522 MB, tool_use arguments 162 MB, thinking 77 MB — and
13
+ * what a person would ever search for, the assistant's own words plus the prompts
14
+ * they typed, is **38 MB**. So this indexes prose only. A grep of the raw column
15
+ * would be a 3 GB scan to search 38 MB, and it would rank a file dump the agent
16
+ * happened to `cat` above the sentence that explained the decision.
17
+ *
18
+ * **`node:sqlite` ships FTS5.** Porter stemming, `bm25()`, `snippet()`, `NEAR()`
19
+ * all work on the bundled SQLite (3.51.2), so the index costs no runtime
20
+ * dependency — which the tarball rule requires (see CLAUDE.md ▸ Traps). Measured
21
+ * on the full history: 7.6s to build, 111,079 chunks, queries in 1–7ms.
22
+ *
23
+ * The index is **never** written into `conductor.db`. That handle is read-only and
24
+ * stays that way; this opens its own file under the relay's state dir, and it is
25
+ * disposable — delete it and the next start rebuilds it.
26
+ */
27
+ /** Bump to force a rebuild: a tokenizer or extraction change makes every stored chunk wrong. */
28
+ const SCHEMA_VERSION = 1;
29
+ /**
30
+ * Source rows advanced per tick. The cursor moves by *scanned* rowid rather than
31
+ * matched rowid, so a caught-up index re-scans nothing — get that wrong and every
32
+ * idle poll re-reads the 3 GB tail looking for rows it already rejected.
33
+ */
34
+ const WINDOW_ROWS = 4000;
35
+ /** Yield to the event loop between batches: the backfill is ~19ms of blocking work per window. */
36
+ const BACKFILL_PAUSE_MS = 5;
37
+ /** Once caught up, look for new messages at about the rate a chat produces them. */
38
+ const IDLE_POLL_MS = 15_000;
39
+ /** A pathological single message can't be allowed to dominate the index. */
40
+ const MAX_CHUNK_CHARS = 64_000;
41
+ /** How many chunks a query ranks before they are folded into workspaces. */
42
+ const CHUNK_LIMIT = 300;
43
+ /**
44
+ * Snippet highlight markers. Control characters rather than brackets: they survive
45
+ * JSON, they need no escaping on the way to the phone, and no transcript contains
46
+ * them, so `web/src/lib/format.ts` can split on them without a parser. The client
47
+ * must not render them literally — see `splitSnippet`.
48
+ */
49
+ export const HIT_OPEN = '\u0001';
50
+ export const HIT_CLOSE = '\u0002';
51
+ /**
52
+ * Turn a phone query into an FTS5 MATCH expression.
53
+ *
54
+ * Every token is quoted, because FTS5 reads `-`, `*`, `:`, `(`, `AND`, `NEAR` and
55
+ * friends as syntax: an unquoted apostrophe or hyphen is a *parse error*, not a
56
+ * poor result, so a raw query would fail rather than under-match. Tokens are OR'd
57
+ * because someone reaching for a workspace on a phone is recalling it, not
58
+ * filtering it — BM25 is what puts the message matching four of four words above
59
+ * the one matching one, and requiring all four would return nothing whenever a
60
+ * single word is misremembered.
61
+ *
62
+ * The last token gets a prefix `*` so search-as-you-type matches mid-word, but only
63
+ * from three characters: `"a"*` matches a large fraction of the index and would
64
+ * spend the whole query budget on a keystroke that means nothing yet.
65
+ */
66
+ export function matchQuery(raw) {
67
+ const tokens = raw.toLowerCase().match(/[\p{L}\p{N}_]+/gu);
68
+ if (!tokens?.length)
69
+ return null;
70
+ const terms = tokens.map(t => `"${t}"`);
71
+ const last = tokens[tokens.length - 1];
72
+ if (!/\s$/.test(raw) && last.length >= 3)
73
+ terms[terms.length - 1] = `"${last}"*`;
74
+ return terms.join(' OR ');
75
+ }
76
+ /** The tokens `matchQuery` will search for — what a caller matches names against. */
77
+ export function queryTokens(raw) {
78
+ return raw.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? [];
79
+ }
80
+ export class SearchIndex {
81
+ source;
82
+ file;
83
+ db = null;
84
+ openError = null;
85
+ cursor = 0;
86
+ caughtUp = false;
87
+ timer = null;
88
+ sourceMax = 0;
89
+ constructor(source, file) {
90
+ this.source = source;
91
+ this.file = file;
92
+ }
93
+ /** Open (or rebuild) the sidecar and start indexing in the background. */
94
+ start() {
95
+ try {
96
+ this.open();
97
+ }
98
+ catch (err) {
99
+ // A search index is a convenience; failing to open one must never stop the relay
100
+ // serving state, transcripts or sends. Report it on /api/search instead.
101
+ this.openError = err instanceof Error ? err.message : String(err);
102
+ console.warn(`⚠ search index unavailable (${this.openError}) — /api/search will report it`);
103
+ return;
104
+ }
105
+ this.schedule(0);
106
+ }
107
+ stop() {
108
+ if (this.timer)
109
+ clearTimeout(this.timer);
110
+ this.timer = null;
111
+ }
112
+ open() {
113
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
114
+ const db = new DatabaseSync(this.file);
115
+ db.exec('PRAGMA journal_mode = WAL');
116
+ db.exec('PRAGMA synchronous = NORMAL');
117
+ // A dev relay on another port shares this file with the LaunchAgent's. WAL lets them
118
+ // both read; the writer that loses waits rather than throwing away its batch, and a
119
+ // tick that still fails is retried by the scheduler with nothing lost (the cursor
120
+ // only advances on commit).
121
+ db.exec('PRAGMA busy_timeout = 5000');
122
+ db.exec('CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)');
123
+ const version = Number(this.readMeta(db, 'version') ?? 0);
124
+ if (version !== SCHEMA_VERSION) {
125
+ db.exec('DROP TABLE IF EXISTS chunks');
126
+ db.exec(`
127
+ CREATE VIRTUAL TABLE chunks USING fts5(
128
+ body,
129
+ session_id UNINDEXED,
130
+ src_rowid UNINDEXED,
131
+ role UNINDEXED,
132
+ at UNINDEXED,
133
+ tokenize='porter unicode61'
134
+ )
135
+ `);
136
+ db.prepare('INSERT OR REPLACE INTO meta(k, v) VALUES (?, ?)').run('version', String(SCHEMA_VERSION));
137
+ db.prepare('INSERT OR REPLACE INTO meta(k, v) VALUES (?, ?)').run('cursor', '0');
138
+ if (version)
139
+ console.log(`search index schema ${version} → ${SCHEMA_VERSION}, rebuilding`);
140
+ }
141
+ this.db = db;
142
+ this.cursor = Number(this.readMeta(db, 'cursor') ?? 0);
143
+ }
144
+ readMeta(db, key) {
145
+ const row = db.prepare('SELECT v FROM meta WHERE k = ?').get(key);
146
+ return row?.v ?? null;
147
+ }
148
+ schedule(ms) {
149
+ if (this.timer)
150
+ clearTimeout(this.timer);
151
+ this.timer = setTimeout(() => {
152
+ let more = false;
153
+ try {
154
+ more = this.tick();
155
+ }
156
+ catch (err) {
157
+ console.warn(`⚠ search index tick failed: ${err instanceof Error ? err.message : err}`);
158
+ }
159
+ this.schedule(more ? BACKFILL_PAUSE_MS : IDLE_POLL_MS);
160
+ }, ms);
161
+ this.timer.unref?.();
162
+ }
163
+ /**
164
+ * Index one window of source rows. Returns true while there is more to do.
165
+ *
166
+ * The window is picked by rowid *before* the prose filter runs, so the cursor
167
+ * advances past rows that hold nothing worth indexing. Filtering first and
168
+ * advancing to the last match instead would leave a caught-up index re-scanning
169
+ * every tool_result between the last prose row and the end of the table, every
170
+ * poll, forever.
171
+ */
172
+ tick() {
173
+ const db = this.db;
174
+ if (!db)
175
+ return false;
176
+ const window = this.source.query('SELECT rowid FROM session_messages WHERE rowid > ? ORDER BY rowid LIMIT ?', [this.cursor, WINDOW_ROWS]);
177
+ if (!window.length) {
178
+ this.caughtUp = true;
179
+ return false;
180
+ }
181
+ const end = window[window.length - 1].rowid;
182
+ // Only rows that can hold prose: a plain-text prompt, or a frame carrying a text
183
+ // block. Everything else is tool plumbing and 98.8% of the bytes.
184
+ const rows = this.source.query(`SELECT rowid, id, session_id, role, content, full_message, created_at, sent_at, queue_order
185
+ FROM session_messages
186
+ WHERE rowid > ? AND rowid <= ? AND session_id IS NOT NULL
187
+ AND (role = 'user' OR content LIKE '%"type":"text"%')
188
+ ORDER BY rowid`, [this.cursor, end]);
189
+ const insert = db.prepare('INSERT INTO chunks(body, session_id, src_rowid, role, at) VALUES (?, ?, ?, ?, ?)');
190
+ db.exec('BEGIN');
191
+ try {
192
+ for (const row of rows) {
193
+ // Reuse the transcript parser rather than a second JSON walk: it already knows
194
+ // that text inside a `type:"user"` frame is injected context and not the user's
195
+ // words, and indexing something the chat view would never show is how a search
196
+ // result becomes impossible to find once you open it.
197
+ for (const entry of parseMessage(row, null)) {
198
+ if (entry.role !== 'user' && entry.role !== 'assistant')
199
+ continue;
200
+ const body = entry.text.trim();
201
+ if (!body)
202
+ continue;
203
+ insert.run(body.slice(0, MAX_CHUNK_CHARS), row.session_id, row.rowid, entry.role, entry.ts);
204
+ }
205
+ }
206
+ db.prepare('INSERT OR REPLACE INTO meta(k, v) VALUES (?, ?)').run('cursor', String(end));
207
+ db.exec('COMMIT');
208
+ }
209
+ catch (err) {
210
+ db.exec('ROLLBACK');
211
+ throw err;
212
+ }
213
+ this.cursor = end;
214
+ return true;
215
+ }
216
+ status() {
217
+ if (this.openError)
218
+ return { chunks: 0, ready: false, progress: 0, error: this.openError };
219
+ const db = this.db;
220
+ if (!db)
221
+ return { chunks: 0, ready: false, progress: 0 };
222
+ const chunks = Number(db.prepare('SELECT COUNT(*) c FROM chunks').get().c);
223
+ if (this.caughtUp)
224
+ return { chunks, ready: true, progress: 1 };
225
+ // Only re-read the source's high-water mark while backfilling; it costs a query
226
+ // and the answer only matters for the progress bar.
227
+ if (!this.sourceMax) {
228
+ const max = this.source.query('SELECT MAX(rowid) m FROM session_messages')[0]?.m;
229
+ this.sourceMax = max ?? 0;
230
+ }
231
+ const progress = this.sourceMax ? Math.min(1, this.cursor / this.sourceMax) : 0;
232
+ return { chunks, ready: false, progress };
233
+ }
234
+ /** Top matching chunks, best first. Empty when the query has no searchable tokens. */
235
+ search(raw, limit = CHUNK_LIMIT) {
236
+ const db = this.db;
237
+ if (!db)
238
+ return [];
239
+ const match = matchQuery(raw);
240
+ if (!match)
241
+ return [];
242
+ let rows;
243
+ try {
244
+ rows = db
245
+ .prepare(`SELECT session_id, src_rowid, role, at, -bm25(chunks) AS score,
246
+ snippet(chunks, 0, ?, ?, '…', 24) AS snippet
247
+ FROM chunks WHERE chunks MATCH ? ORDER BY bm25(chunks) LIMIT ?`)
248
+ .all(HIT_OPEN, HIT_CLOSE, match, limit);
249
+ }
250
+ catch (err) {
251
+ // A MATCH that still fails to parse is a bug in matchQuery, not user error —
252
+ // report it rather than showing an empty result that looks like "no matches".
253
+ throw new Error(`search failed for ${JSON.stringify(match)}: ${err instanceof Error ? err.message : err}`);
254
+ }
255
+ return rows.map(r => ({
256
+ sessionId: r.session_id,
257
+ srcRowid: Number(r.src_rowid),
258
+ role: r.role === 'user' ? 'user' : 'assistant',
259
+ at: r.at,
260
+ score: Number(r.score),
261
+ snippet: r.snippet
262
+ }));
263
+ }
264
+ }
265
+ const SNIPPETS_PER_RESULT = 3;
266
+ /**
267
+ * Fold chunk hits up into workspaces.
268
+ *
269
+ * Chunk-level results are the wrong unit here: one long conversation produces a
270
+ * dozen and buries every other workspace. What to do with those dozen is the whole
271
+ * ranking decision, and it was measured rather than guessed — searching this Mac's
272
+ * history for "removing adding lamp manual", where the right answer is a chat that
273
+ * says "Add by name is gone. Removed the form":
274
+ *
275
+ * summing every hit → right answer ranks 9th
276
+ * best single hit → 5th
277
+ * sum of the top 3 → 5th, and steadier across other queries
278
+ *
279
+ * Summing everything ranks by *volume*: a 32-message conversation about lamps beat
280
+ * the four messages that actually removed the feature. So only the best
281
+ * `SNIPPETS_PER_RESULT` hits score, which caps what repetition can buy and makes
282
+ * the number mean something the user can check — the score is exactly the strength
283
+ * of the snippets shown under the row. `hits` still counts them all.
284
+ *
285
+ * Hits whose session no longer resolves to a workspace are dropped; a result nobody
286
+ * can open is worse than one fewer result.
287
+ */
288
+ export function foldHits(hits, resolve) {
289
+ const byWorkspace = new Map();
290
+ for (const hit of hits) {
291
+ const workspace = resolve(hit.sessionId);
292
+ if (!workspace)
293
+ continue;
294
+ let entry = byWorkspace.get(workspace.id);
295
+ if (!entry) {
296
+ entry = {
297
+ workspace,
298
+ sessionId: null,
299
+ hits: 0,
300
+ score: 0,
301
+ at: null,
302
+ snippets: [],
303
+ byName: false,
304
+ bestBySession: new Map()
305
+ };
306
+ byWorkspace.set(workspace.id, entry);
307
+ }
308
+ entry.hits++;
309
+ if (!entry.at || hit.at > entry.at)
310
+ entry.at = hit.at;
311
+ entry.bestBySession.set(hit.sessionId, Math.max(entry.bestBySession.get(hit.sessionId) ?? 0, hit.score));
312
+ // `hits` arrives in BM25 order, so the first few of a workspace are its best few:
313
+ // scoring and snippeting the same slice needs no second sort.
314
+ if (entry.snippets.length < SNIPPETS_PER_RESULT) {
315
+ entry.score += hit.score;
316
+ entry.snippets.push({ sessionId: hit.sessionId, role: hit.role, at: hit.at, text: hit.snippet });
317
+ }
318
+ }
319
+ const results = [];
320
+ for (const entry of byWorkspace.values()) {
321
+ const { bestBySession, ...rest } = entry;
322
+ let sessionId = null;
323
+ let best = -Infinity;
324
+ for (const [id, score] of bestBySession) {
325
+ if (score <= best)
326
+ continue;
327
+ best = score;
328
+ sessionId = id;
329
+ }
330
+ results.push({ ...rest, sessionId });
331
+ }
332
+ return results.sort((a, b) => b.score - a.score);
333
+ }
@@ -16,6 +16,7 @@ import { chatRoute, notifyAll, notifyDevice, pushConfig, startNotifier, subscrib
16
16
  import { ParkedPromptQueue } from "./parked.js";
17
17
  import { attachPrStatus } from "./pr.js";
18
18
  import { Reads } from "./reads.js";
19
+ import { foldHits, queryTokens, SearchIndex } from "./search.js";
19
20
  import { readSettings, writeSettings } from "./settings.js";
20
21
  import { driftWarningLines, tailscaleBin } from "./tailscale.js";
21
22
  import { autoJoinHotspotMode, currentSsid, looksLikeHotspot, preferredNetworks } from "./wifi.js";
@@ -27,6 +28,11 @@ const cfg = loadConfig();
27
28
  const db = new ConductorDb(cfg.dbPath);
28
29
  const reads = new Reads(db, cfg.workspacesRoot);
29
30
  const actuator = pickActuator(cfg.writeStrategy);
31
+ // Full-text index over the chat prose, in the relay's own sidecar DB — never in
32
+ // Conductor's (see src/search.ts). It backfills in the background and is disposable:
33
+ // deleting the file rebuilds it on the next start.
34
+ const search = new SearchIndex(db, path.join(stateDir(), 'search.db'));
35
+ search.start();
30
36
  // A windowless Conductor that ignores reopen *and* a Dock click can only be fixed
31
37
  // by restarting it — and quitting takes any agent mid-turn down with it. So the
32
38
  // write path may only do that while nothing is working, which is a DB fact, not
@@ -418,6 +424,49 @@ const server = http.createServer(async (req, res) => {
418
424
  update
419
425
  });
420
426
  }
427
+ // GET /api/search?q= — find a workspace by its name or by what was said in its chats.
428
+ //
429
+ // Two sources, merged. `findWorkspacesByName` matches the workspace's own identity
430
+ // and wins ties, because someone who types a name wants that workspace and not the
431
+ // twelve chats that mention it. The transcript index answers the harder question —
432
+ // "which workspace did I do this in" — and is the only one that can, since the
433
+ // words you remember are usually the agent's, not the branch's.
434
+ //
435
+ // Both reach archived workspaces. That is the point: 1,846 of the 1,886 here are
436
+ // archived, so a search limited to the live sidebar would miss almost everything.
437
+ if (req.method === 'GET' && pathname === '/api/search') {
438
+ const q = url.searchParams.get('q') ?? '';
439
+ // 12, not 50: an OR query over common words ("add", "remove") has a long weak tail,
440
+ // and past the first screenful nobody scrolls — they retype instead.
441
+ const limit = Math.min(50, Math.max(1, Number(url.searchParams.get('limit') ?? 12) || 12));
442
+ const index = search.status();
443
+ const tokens = queryTokens(q);
444
+ if (!tokens.length)
445
+ return json(req, res, 200, { query: q, results: [], index });
446
+ const hits = search.search(q);
447
+ const targets = reads.searchTargets([...new Set(hits.map(h => h.sessionId))]);
448
+ const fromChats = foldHits(hits, sid => targets.get(sid)?.workspace ?? null);
449
+ const remaining = new Map(fromChats.map(r => [r.workspace.id, r]));
450
+ const merged = [];
451
+ for (const workspace of reads.findWorkspacesByName(tokens, limit)) {
452
+ const evidence = remaining.get(workspace.id);
453
+ remaining.delete(workspace.id);
454
+ // Keep the chat evidence when there is any: the snippet is what tells you this
455
+ // is the right "fix-lamp-thing" out of three with similar names.
456
+ merged.push(evidence
457
+ ? { ...evidence, byName: true }
458
+ : { workspace, sessionId: null, hits: 0, score: 0, at: null, snippets: [], byName: true });
459
+ }
460
+ merged.push(...remaining.values());
461
+ return json(req, res, 200, {
462
+ query: q,
463
+ index,
464
+ results: merged.slice(0, limit).map(r => ({
465
+ ...r,
466
+ sessionTitle: r.sessionId ? (targets.get(r.sessionId)?.sessionTitle ?? null) : null
467
+ }))
468
+ });
469
+ }
421
470
  // GET /api/repos — repos a new workspace can be created in
422
471
  if (req.method === 'GET' && pathname === '/api/repos') {
423
472
  return json(req, res, 200, { repos: reads.listRepos() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
@@ -1 +0,0 @@
1
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "Segoe UI", Roboto, sans-serif;--font-mono:ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#0a0b0e;--color-surface:#14161b;--color-surface-2:#1b1e26;--color-border:#262a33;--color-border-soft:#1e222a;--color-text:#e7e9ee;--color-muted:#8b909d;--color-faint:#5c616d;--color-accent:#8b7dff;--color-accent-soft:#2a2650;--color-working:#f5a623;--color-idle:#3ecf8e;--color-done:#5b9dff;--color-pr-merged:#ac47ff;--color-pr-draft:#a4a3a2;--color-pr-conflicts:#fd9a00;--color-pr-mergeable:#49de80;--color-add:#3ecf8e;--color-del:#ff6b6b}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{-webkit-tap-highlight-color:transparent}html,body,#root{height:var(--app-height,100dvh)}html,body{overflow:hidden}@media(display-mode:standalone){html,body,#root{height:var(--app-height,100vh)}}html[data-standalone],html[data-standalone] body,html[data-standalone] #root{height:var(--app-height,100vh)}html{background:var(--color-bg)}body{background:var(--color-bg);color:var(--color-text);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;overscroll-behavior:none;margin:0}button{font:inherit;color:inherit;cursor:pointer}::-webkit-scrollbar{width:0;height:0}}@layer components{.card{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3.5);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}.card:active{background-color:var(--color-surface-2);scale:.985}.pill{padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.pill-active{background-color:var(--color-surface-2);color:var(--color-text)}.ctl{padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:13px;font-weight:var(--font-weight-medium);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.ctl-on{background-color:var(--color-surface-2);color:var(--color-text)}.ctl-staged{background-color:var(--color-accent-soft);color:var(--color-accent)}.dot{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5);background-color:var(--color-faint);border-radius:3.40282e38px;flex-shrink:0}.dot-spinner{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);animation:var(--animate-spin);border-style:var(--tw-border-style);--spin-color:var(--color-working);border-width:2px;border-color:var(--spin-color);border-radius:3.40282e38px;flex-shrink:0}@supports (color:color-mix(in lab,red,red)){.dot-spinner{border-color:color-mix(in srgb,var(--spin-color) 30%,transparent)}}.dot-spinner{border-top-color:var(--spin-color);border-right-color:var(--spin-color)}.dot-idle{background:var(--color-idle)}.dot-done{background:var(--color-done)}.typing-dot{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5);background-color:var(--color-muted);border-radius:3.40282e38px;animation:1.2s ease-in-out infinite typing}.typing-dot:nth-child(2){animation-delay:.15s}.typing-dot:nth-child(3){animation-delay:.3s}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p{margin:.5em 0}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:.9em 0 .4em;font-weight:600;line-height:1.3}.md h1{font-size:1.25em}.md h2{font-size:1.15em}.md h3{font-size:1.05em}.md ul,.md ol{margin:.5em 0;padding-left:1.4em}.md ul{list-style:outside}.md ol{list-style:decimal}.md li,.md li>ul,.md li>ol{margin:.2em 0}.md code{background:#1b1e26cc;border-radius:.3rem}@supports (color:color-mix(in lab,red,red)){.md code{background:color-mix(in srgb,var(--color-surface-2) 80%,transparent)}}.md code{font-family:var(--font-mono);padding:.1em .35em;font-size:.85em}.md pre{border:1px solid var(--color-border-soft);background:var(--color-bg);border-radius:.6rem;margin:.5em 0;padding:.6em .75em;overflow-x:auto}.md pre code{background:0 0;padding:0;font-size:12px;line-height:1.5}.md a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline}.md blockquote{border-left:2px solid var(--color-border);color:var(--color-muted);margin:.5em 0;padding-left:.75em}.md hr{border:0;border-top:1px solid var(--color-border-soft);margin:.75em 0}.md table{border-collapse:collapse;max-width:100%;margin:.5em 0;font-size:.9em;display:block;overflow-x:auto}.md th,.md td{border:1px solid var(--color-border);text-align:left;padding:.3em .6em}.md th{background:var(--color-surface-2);font-weight:600}.md strong{font-weight:600}.md input[type=checkbox]{margin-right:.4em}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{inset-inline:0}.inset-y-0{inset-block:0}.top-0{top:0}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-full{top:100%}.-right-0\.5{right:calc(var(--spacing) * -.5)}.right-1\.5{right:calc(var(--spacing) * 1.5)}.right-2{right:calc(var(--spacing) * 2)}.-bottom-0\.5{bottom:calc(var(--spacing) * -.5)}.bottom-0{bottom:0}.bottom-full{bottom:100%}.left-0{left:0}.left-0\.5{left:calc(var(--spacing) * .5)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.mx-auto{margin-inline:auto}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mr-2{margin-right:calc(var(--spacing) * -2)}.mr-1{margin-right:var(--spacing)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-4{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.size-20{width:calc(var(--spacing) * 20);height:calc(var(--spacing) * 20)}.size-60{width:calc(var(--spacing) * 60);height:calc(var(--spacing) * 60)}.size-full{width:100%;height:100%}.h-6{height:calc(var(--spacing) * 6)}.h-12{height:calc(var(--spacing) * 12)}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-\[70\%\]{max-height:70%}.max-h-\[85dvh\]{max-height:85dvh}.min-h-0{min-height:0}.min-h-14{min-height:calc(var(--spacing) * 14)}.w-1{width:var(--spacing)}.w-5{width:calc(var(--spacing) * 5)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-44{width:calc(var(--spacing) * 44)}.w-56{width:calc(var(--spacing) * 56)}.w-64{width:calc(var(--spacing) * 64)}.w-\[85\%\]{width:85%}.w-full{width:100%}.max-w-36{max-width:calc(var(--spacing) * 36)}.max-w-40{max-width:calc(var(--spacing) * 40)}.max-w-64{max-width:calc(var(--spacing) * 64)}.max-w-80{max-width:calc(var(--spacing) * 80)}.max-w-\[55vw\]{max-width:55vw}.max-w-\[70vw\]{max-width:70vw}.max-w-\[85\%\]{max-width:85%}.max-w-\[92\%\]{max-width:92%}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-3{--tw-translate-y:calc(var(--spacing) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.-rotate-90{rotate:-90deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.scrollbar-thin{scrollbar-width:thin}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-start{justify-content:flex-start}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-accent\/50{border-color:#8b7dff80}@supports (color:color-mix(in lab,red,red)){.border-accent\/50{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.border-border{border-color:var(--color-border)}.border-border-soft{border-color:var(--color-border-soft)}.border-del\/30{border-color:#ff6b6b4d}@supports (color:color-mix(in lab,red,red)){.border-del\/30{border-color:color-mix(in oklab,var(--color-del) 30%,transparent)}}.border-del\/40{border-color:#ff6b6b66}@supports (color:color-mix(in lab,red,red)){.border-del\/40{border-color:color-mix(in oklab,var(--color-del) 40%,transparent)}}.border-faint{border-color:var(--color-faint)}.border-white\/80{border-color:#fffc}@supports (color:color-mix(in lab,red,red)){.border-white\/80{border-color:color-mix(in oklab,var(--color-white) 80%,transparent)}}.border-t-accent{border-top-color:var(--color-accent)}.border-t-text{border-top-color:var(--color-text)}.border-l-accent{border-left-color:var(--color-accent)}.bg-accent{background-color:var(--color-accent)}.bg-accent-soft{background-color:var(--color-accent-soft)}.bg-accent\/10{background-color:#8b7dff1a}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--color-accent) 10%,transparent)}}.bg-add{background-color:var(--color-add)}.bg-add\/10{background-color:#3ecf8e1a}@supports (color:color-mix(in lab,red,red)){.bg-add\/10{background-color:color-mix(in oklab,var(--color-add) 10%,transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-bg\/80{background-color:#0a0b0ecc}@supports (color:color-mix(in lab,red,red)){.bg-bg\/80{background-color:color-mix(in oklab,var(--color-bg) 80%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-black\/75{background-color:#000000bf}@supports (color:color-mix(in lab,red,red)){.bg-black\/75{background-color:color-mix(in oklab,var(--color-black) 75%,transparent)}}.bg-del{background-color:var(--color-del)}.bg-del\/5{background-color:#ff6b6b0d}@supports (color:color-mix(in lab,red,red)){.bg-del\/5{background-color:color-mix(in oklab,var(--color-del) 5%,transparent)}}.bg-del\/10{background-color:#ff6b6b1a}@supports (color:color-mix(in lab,red,red)){.bg-del\/10{background-color:color-mix(in oklab,var(--color-del) 10%,transparent)}}.bg-del\/15{background-color:#ff6b6b26}@supports (color:color-mix(in lab,red,red)){.bg-del\/15{background-color:color-mix(in oklab,var(--color-del) 15%,transparent)}}.bg-pr-merged\/10{background-color:#ac47ff1a}@supports (color:color-mix(in lab,red,red)){.bg-pr-merged\/10{background-color:color-mix(in oklab,var(--color-pr-merged) 10%,transparent)}}.bg-pr-merged\/15{background-color:#ac47ff26}@supports (color:color-mix(in lab,red,red)){.bg-pr-merged\/15{background-color:color-mix(in oklab,var(--color-pr-merged) 15%,transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-2{background-color:var(--color-surface-2)}.bg-surface-2\/60{background-color:#1b1e2699}@supports (color:color-mix(in lab,red,red)){.bg-surface-2\/60{background-color:color-mix(in oklab,var(--color-surface-2) 60%,transparent)}}.bg-surface\/40{background-color:#14161b66}@supports (color:color-mix(in lab,red,red)){.bg-surface\/40{background-color:color-mix(in oklab,var(--color-surface) 40%,transparent)}}.bg-surface\/60{background-color:#14161b99}@supports (color:color-mix(in lab,red,red)){.bg-surface\/60{background-color:color-mix(in oklab,var(--color-surface) 60%,transparent)}}.bg-surface\/95{background-color:#14161bf2}@supports (color:color-mix(in lab,red,red)){.bg-surface\/95{background-color:color-mix(in oklab,var(--color-surface) 95%,transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-working{background-color:var(--color-working)}.bg-working\/10{background-color:#f5a6231a}@supports (color:color-mix(in lab,red,red)){.bg-working\/10{background-color:color-mix(in oklab,var(--color-working) 10%,transparent)}}.bg-working\/15{background-color:#f5a62326}@supports (color:color-mix(in lab,red,red)){.bg-working\/15{background-color:color-mix(in oklab,var(--color-working) 15%,transparent)}}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-px{padding-block:1px}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-2\.5{padding-top:calc(var(--spacing) * 2.5)}.pr-1{padding-right:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-2\.5{padding-bottom:calc(var(--spacing) * 2.5)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--color-accent)}.text-add{color:var(--color-add)}.text-bg{color:var(--color-bg)}.text-black{color:var(--color-black)}.text-del{color:var(--color-del)}.text-del\/80{color:#ff6b6bcc}@supports (color:color-mix(in lab,red,red)){.text-del\/80{color:color-mix(in oklab,var(--color-del) 80%,transparent)}}.text-faint{color:var(--color-faint)}.text-muted{color:var(--color-muted)}.text-pr-merged{color:var(--color-pr-merged)}.text-text{color:var(--color-text)}.text-white{color:var(--color-white)}.text-working{color:var(--color-working)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.\[color-scheme\:dark\]{color-scheme:dark}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_100vmax_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 0 0 100vmax var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color:#0006}@supports (color:color-mix(in lab,red,red)){.shadow-black\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-black\/60{--tw-shadow-color:#0009}@supports (color:color-mix(in lab,red,red)){.shadow-black\/60{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 60%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-surface{--tw-ring-color:var(--color-surface)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\/steps\:invisible:is(:where(.group\/steps):is([open],:popover-open,:open) *){visibility:hidden}.group-open\/steps\:rotate-90:is(:where(.group\/steps):is([open],:popover-open,:open) *),.group-open\/think\:rotate-90:is(:where(.group\/think):is([open],:popover-open,:open) *){rotate:90deg}.placeholder\:font-sans::placeholder{font-family:var(--font-sans)}.placeholder\:text-faint::placeholder{color:var(--color-faint)}@media(hover:hover){.hover\:underline:hover{text-decoration-line:underline}}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:-outline-offset-2:focus-visible{outline-offset:-2px}.focus-visible\:outline-accent:focus-visible{outline-color:var(--color-accent)}.active\:scale-90:active{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.985\]:active{scale:.985}.active\:bg-black\/70:active{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.active\:bg-black\/70:active{background-color:color-mix(in oklab,var(--color-black) 70%,transparent)}}.active\:bg-surface:active{background-color:var(--color-surface)}.active\:bg-surface-2:active{background-color:var(--color-surface-2)}.active\:text-text:active{color:var(--color-text)}.active\:opacity-80:active{opacity:.8}.disabled\:bg-surface-2:disabled{background-color:var(--color-surface-2)}.disabled\:text-faint:disabled{color:var(--color-faint)}.disabled\:text-faint\/40:disabled{color:#5c616d66}@supports (color:color-mix(in lab,red,red)){.disabled\:text-faint\/40:disabled{color:color-mix(in oklab,var(--color-faint) 40%,transparent)}}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.has-\[textarea\:focus\]\:border-accent\/60:has(:is(textarea:focus)){border-color:#8b7dff99}@supports (color:color-mix(in lab,red,red)){.has-\[textarea\:focus\]\:border-accent\/60:has(:is(textarea:focus)){border-color:color-mix(in oklab,var(--color-accent) 60%,transparent)}}@media(prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}@media(min-width:48rem){.md\:static{position:static}.md\:inset-0{inset:0}.md\:inset-6{inset:calc(var(--spacing) * 6)}.md\:z-auto{z-index:auto}.md\:m-auto{margin:auto}.md\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.md\:block{display:block}.md\:hidden{display:none}.md\:h-fit{height:fit-content}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:max-w-none{max-width:none}.md\:shrink-0{flex-shrink:0}.md\:translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:rounded-3xl{border-radius:var(--radius-3xl)}.md\:border{border-style:var(--tw-border-style);border-width:1px}.md\:border-border-soft{border-color:var(--color-border-soft)}.md\:transition-none{transition-property:none}}@media(min-width:64rem){.lg\:static{position:static}.lg\:z-auto{z-index:auto}.lg\:w-80{width:calc(var(--spacing) * 80)}.lg\:w-\[380px\]{width:380px}.lg\:shrink-0{flex-shrink:0}.lg\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.lg\:border-border-soft{border-color:var(--color-border-soft)}}@media(min-width:80rem){.xl\:w-\[460px\]{width:460px}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}@keyframes typing{0%,60%,to{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-3px)}}@keyframes fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.fade-in{animation:.18s ease-out fade-in}@media(prefers-reduced-motion:reduce){.fade-in{opacity:1;animation:none}.typing-dot{opacity:.6;animation:none}}.pt-safe{padding-top:max(env(safe-area-inset-top),.5rem)}.pb-safe{padding-bottom:max(env(safe-area-inset-bottom),.5rem)}html[data-keyboard] .pb-safe{padding-bottom:.5rem}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}