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/assets/index-95IdnA_r.css +1 -0
- package/dist/assets/{index-BhYD1Ar2.js → index-BSDnFvjd.js} +28 -28
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/reads.js +72 -0
- package/dist-node/src/search.js +333 -0
- package/dist-node/src/server.js +49 -0
- package/package.json +1 -1
- package/dist/assets/index-UopM0hHW.css +0 -1
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-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
|
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\//]}))});
|
package/dist-node/src/reads.js
CHANGED
|
@@ -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
|
+
}
|
package/dist-node/src/server.js
CHANGED
|
@@ -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.
|
|
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)}}
|