t3-bridge 0.1.2 → 0.2.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/README.md CHANGED
@@ -39,26 +39,29 @@ Slack, and bookmark each link. On a single Mac without Tailscale, open the `127.
39
39
  | `npx t3-bridge link` | Print the control panel link again |
40
40
  | `npx t3-bridge status` | Show whether T3 Code and the bots are connected |
41
41
 
42
- To update, open the control panel's **Updates** card and choose **Update now**, or run
42
+ The panel lists your bots in a sidebar. Each bot's **Conversation** tab shows every request from
43
+ chat, what the agent is doing about it, its subagents, and anything that went wrong.
44
+
45
+ To update, open **Settings** in the panel and choose **Update**, or run
43
46
  `npx t3-bridge@latest install`. Settings live in `~/Library/Application Support/T3 Bridge` and
44
47
  logs in `~/Library/Logs/T3 Bridge.log`.
45
48
 
46
- **Replace link** in **Open from other devices** issues a new link; every old copy stops working.
49
+ **Replace link** under **Settings → Open from other devices** issues a new link; every old copy
50
+ stops working. The QR code next to it opens the panel on your phone.
47
51
 
48
52
  ## Add a Slack bot
49
53
 
50
- In the control panel, choose **Add Slack bot** and follow the steps:
54
+ In the control panel, choose **+ Add** and follow the four steps:
51
55
 
52
- 1. Name the bot and copy the generated manifest.
53
- 2. On api.slack.com/apps, choose **Create New App → From a manifest**, paste it, and install the
54
- app to your workspace. Copy the **Bot User OAuth Token** (`xoxb-…`).
55
- 3. Under **Basic Information → App-Level Tokens**, create a token with the `connections:write`
56
- scope (`xapp-…`).
57
- 4. Paste both tokens and choose **Test tokens**.
58
- 5. Choose a thread, or create a new orchestrator thread. A new thread is pinned, runs with full
56
+ 1. Name the bot and choose **Create Slack app**. Slack opens with the app filled in; choose
57
+ **Create**, then **Install to Workspace**.
58
+ 2. Paste the **Bot User OAuth Token** (`xoxb-…`) and an app-level token with the
59
+ `connections:write` scope (`xapp-…`, under **Basic Information → App-Level Tokens**). They
60
+ are checked as you paste.
61
+ 3. Choose a thread, or create a new orchestrator thread. A new thread is pinned, runs with full
59
62
  access, and starts with the orchestrator instructions.
60
- 6. Leave **Who may use it** empty to claim the bot by sending it a direct message, or list Slack
61
- member IDs.
63
+ 4. Send the bot a direct message in Slack. The first person to do so becomes its owner; add
64
+ others under the bot's **Settings → Who may use it**.
62
65
 
63
66
  Message the bot directly, or invite it to a channel with `/invite @bot`. Both go to the same thread. In
64
67
  a channel, mention the bot (`@bot fix the login page`) or reply under one of its messages; other
@@ -66,9 +69,9 @@ channel chatter is ignored.
66
69
 
67
70
  ## Updating an existing bot's Slack app
68
71
 
69
- When a new version needs more Slack permissions, the bot's card shows **Update Slack app**. It
70
- gives you a new manifest to paste under **App Manifest** on api.slack.com, then asks you to
71
- reinstall the app. The tokens stay the same.
72
+ When a new version needs more Slack permissions, the bot's **Slack app** tab says so. It gives
73
+ you a new manifest to paste under **App Manifest** on api.slack.com, then asks you to reinstall
74
+ the app. The tokens stay the same.
72
75
 
73
76
  ## In Slack
74
77
 
package/dist/cli.cjs CHANGED
@@ -74375,6 +74375,87 @@ const DEFAULT_INSTRUCTIONS = `You are the orchestrator for this project. Message
74375
74375
  - You may answer quick questions directly only when no code needs to be read or changed.
74376
74376
  - Keep replies concise and chat-friendly. Use short paragraphs and lists; avoid wide tables.
74377
74377
  - Images you show (by file path or attachment) and pages you render with html_render reach the user in Slack as files, so do not tell them to open T3 to see them.`;
74378
+ // src/engine/core/activity.ts
74379
+ const SNIPPET = 280;
74380
+ const snippet = (text) => text.length > SNIPPET ? `${text.slice(0, SNIPPET).trimEnd()}…` : text;
74381
+ function newActivity(input) {
74382
+ const at = (/* @__PURE__ */ new Date()).toISOString();
74383
+ return {
74384
+ id: input.id,
74385
+ at,
74386
+ user: input.user,
74387
+ text: snippet(input.text),
74388
+ files: input.files,
74389
+ status: input.error === void 0 ? "running" : "not_sent",
74390
+ subagents: [],
74391
+ waiting: null,
74392
+ reply: null,
74393
+ error: input.error ?? null,
74394
+ updatedAt: at
74395
+ };
74396
+ }
74397
+ /** Folds a post the agent produced for this request into its entry. */
74398
+ function applyPost(entry, post) {
74399
+ const touched = {
74400
+ ...entry,
74401
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
74402
+ };
74403
+ switch (post.kind) {
74404
+ case "assistant": return post.markdown.trim() === "" ? entry : {
74405
+ ...touched,
74406
+ reply: snippet(post.markdown.trim())
74407
+ };
74408
+ case "subagents": return {
74409
+ ...touched,
74410
+ subagents: post.tasks.map((task) => ({
74411
+ id: task.id,
74412
+ title: task.title,
74413
+ status: task.status
74414
+ }))
74415
+ };
74416
+ case "subagent": {
74417
+ const task = {
74418
+ id: post.id,
74419
+ title: post.title,
74420
+ status: post.status
74421
+ };
74422
+ const others = entry.subagents.filter((existing) => existing.id !== post.id);
74423
+ return {
74424
+ ...touched,
74425
+ subagents: [...others, task]
74426
+ };
74427
+ }
74428
+ case "approval": return {
74429
+ ...touched,
74430
+ waiting: post.resolved === null ? "approval" : null
74431
+ };
74432
+ case "question": return {
74433
+ ...touched,
74434
+ waiting: post.resolved ? null : "question"
74435
+ };
74436
+ case "error": return {
74437
+ ...touched,
74438
+ error: snippet(post.message)
74439
+ };
74440
+ default: return entry;
74441
+ }
74442
+ }
74443
+ function applyRunStatus(entry, status) {
74444
+ if (entry.status === status) return entry;
74445
+ return {
74446
+ ...entry,
74447
+ status,
74448
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
74449
+ };
74450
+ }
74451
+ /** Whether the agent is still on it: its turn is running or subagents it started still are. */
74452
+ function isWorking(entry) {
74453
+ return entry.status === "running" || entry.subagents.some((task) => [
74454
+ "pending",
74455
+ "running",
74456
+ "waiting"
74457
+ ].includes(task.status));
74458
+ }
74378
74459
  /** Writes through a temp file so a crash mid-write never leaves half a JSON file behind. */
74379
74460
  async function writeJsonAtomic(file, value, mode = 384) {
74380
74461
  await node_fs_promises.mkdir(node_path.dirname(file), { recursive: true });
@@ -74751,9 +74832,10 @@ var BotRuntime = class {
74751
74832
  }
74752
74833
  applyRun(run) {
74753
74834
  this.runs.set(run.id, run);
74835
+ const status = runStatus(run.status);
74836
+ if (status !== null) this.updateActivity(run.id, (entry) => applyRunStatus(entry, status));
74754
74837
  const sent = this.state.sentMessages[run.userMessageId];
74755
74838
  if (sent === void 0) return;
74756
- const status = runStatus(run.status);
74757
74839
  if (status === null || status === sent.status) return;
74758
74840
  this.state.sentMessages[run.userMessageId] = {
74759
74841
  ...sent,
@@ -74770,8 +74852,36 @@ var BotRuntime = class {
74770
74852
  post: keyed.post,
74771
74853
  runId
74772
74854
  });
74855
+ this.updateActivity(runId, (entry) => applyPost(entry, keyed.post));
74773
74856
  this.scheduleDelivery(0);
74774
74857
  }
74858
+ get activity() {
74859
+ return this.state?.activity ?? [];
74860
+ }
74861
+ recordActivity(entry) {
74862
+ const activity = [...this.activity, entry];
74863
+ this.state.activity = activity.slice(-200);
74864
+ this.scheduleSave();
74865
+ }
74866
+ /** Updates the request a run belongs to, following summaries back to the run that delegated. */
74867
+ updateActivity(runId, change) {
74868
+ let current = runId;
74869
+ for (let depth = 0; current !== null && depth < 16; depth += 1) {
74870
+ const run = this.runs.get(current);
74871
+ if (run === void 0) return;
74872
+ const index = this.activity.findIndex((entry) => entry.id === run.userMessageId);
74873
+ if (index !== -1) {
74874
+ const before = this.activity[index];
74875
+ const after = change(before);
74876
+ if (after !== before) {
74877
+ this.state.activity = this.activity.with(index, after);
74878
+ this.scheduleSave();
74879
+ }
74880
+ return;
74881
+ }
74882
+ current = this.wakeParents.get(run.userMessageId) ?? null;
74883
+ }
74884
+ }
74775
74885
  /**
74776
74886
  * The chat thread a run's output belongs in: the thread of the chat message that started
74777
74887
  * it, the thread under a message typed in T3, or, for the summary T3 asks for after
@@ -74923,6 +75033,7 @@ var BotRuntime = class {
74923
75033
  return;
74924
75034
  }
74925
75035
  if (await this.answerFromReply(message, text)) return;
75036
+ let recorded = null;
74926
75037
  try {
74927
75038
  const attachments = await this.uploadFiles(message);
74928
75039
  const messageId = MessageId.make(node_crypto.randomUUID());
@@ -74940,6 +75051,13 @@ var BotRuntime = class {
74940
75051
  ...title === null ? [] : [`${message.replyTo === null ? "new thread" : "thread"} "${title}"`],
74941
75052
  ...this.bot.reminder ? ["delegate via subagents"] : []
74942
75053
  ].join(" · ")}]\n`;
75054
+ recorded = messageId;
75055
+ this.recordActivity(newActivity({
75056
+ id: messageId,
75057
+ user: message.userName,
75058
+ text,
75059
+ files: message.files.length
75060
+ }));
74943
75061
  await this.options.t3.dispatch({
74944
75062
  type: "message.dispatch",
74945
75063
  commandId: CommandId.make(node_crypto.randomUUID()),
@@ -74960,6 +75078,15 @@ var BotRuntime = class {
74960
75078
  };
74961
75079
  } catch (error) {
74962
75080
  log.error(`[${this.bot.name}] sending a message to T3 failed`, error);
75081
+ const failed = newActivity({
75082
+ id: recorded ?? node_crypto.randomUUID(),
75083
+ user: message.userName,
75084
+ text,
75085
+ files: message.files.length,
75086
+ error: `Could not reach T3: ${errorText(error)}`
75087
+ });
75088
+ if (recorded === null) this.recordActivity(failed);
75089
+ else this.state.activity = this.activity.map((entry) => entry.id === recorded ? failed : entry);
74963
75090
  await adapter.reply(message, `⚠️ Could not reach T3: ${errorText(error)}`);
74964
75091
  }
74965
75092
  }
@@ -76495,7 +76622,7 @@ function describeCause(cause) {
76495
76622
  return ((error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error)) ?? "Unknown error").split("\n")[0].slice(0, 500);
76496
76623
  }
76497
76624
  // src/engine/version.ts
76498
- const BRIDGE_VERSION = "0.1.2";
76625
+ const BRIDGE_VERSION = "0.2.0";
76499
76626
  // src/engine/Engine.ts
76500
76627
  const botSecret = (botId, name) => `bot.${botId}.${name}`;
76501
76628
  var Engine = class {
@@ -76546,11 +76673,17 @@ var Engine = class {
76546
76673
  error: "Not running"
76547
76674
  },
76548
76675
  lastError: runtime?.lastError ?? null,
76549
- manifestOutdated: (bot.manifestVersion ?? 1) < 3
76676
+ manifestOutdated: (bot.manifestVersion ?? 1) < 3,
76677
+ activity: summarize(runtime?.activity ?? [])
76550
76678
  };
76551
76679
  })
76552
76680
  };
76553
76681
  }
76682
+ /** A bot's latest requests, newest first. */
76683
+ activity(botId) {
76684
+ if (!this.config.get().bots.some((bot) => bot.id === botId)) throw new NotFoundError("No such bot");
76685
+ return [...this.bots.get(botId)?.activity ?? []].reverse();
76686
+ }
76554
76687
  async listThreads() {
76555
76688
  const shell = await this.t3.getShell();
76556
76689
  const projects = new Map(shell.projects.map((project) => [project.id, project.title]));
@@ -76800,6 +76933,18 @@ var Engine = class {
76800
76933
  }
76801
76934
  };
76802
76935
  var NotFoundError = class extends Error {};
76936
+ /** What the bot list shows: the request in progress, and how today went. */
76937
+ function summarize(activity) {
76938
+ const midnight = /* @__PURE__ */ new Date();
76939
+ midnight.setHours(0, 0, 0, 0);
76940
+ const today = activity.filter((entry) => Date.parse(entry.at) >= midnight.getTime());
76941
+ return {
76942
+ working: activity.findLast(isWorking) ?? null,
76943
+ today: today.length,
76944
+ problemsToday: today.filter((entry) => entry.error !== null || entry.status === "failed").length,
76945
+ lastAt: activity.at(-1)?.updatedAt ?? null
76946
+ };
76947
+ }
76803
76948
  /** Bots set up with the agent manifest talk in Slack's AI-app layout, one thread per request. */
76804
76949
  function isAgentLayout(bot) {
76805
76950
  return (bot.manifestVersion ?? 1) >= 3;
@@ -76934,6 +77079,8 @@ var ControlServer = class {
76934
77079
  token: await engine.rotateControlToken(),
76935
77080
  connectLinks: this.connectLinks()
76936
77081
  };
77082
+ const activityMatch = /^\/api\/bots\/([\w-]+)\/activity$/.exec(path);
77083
+ if (method === "GET" && activityMatch !== null) return { entries: engine.activity(activityMatch[1]) };
76937
77084
  const freshMatch = /^\/api\/bots\/([\w-]+)\/new-thread$/.exec(path);
76938
77085
  if (method === "POST" && freshMatch !== null) return engine.startFreshThread(freshMatch[1]);
76939
77086
  const botMatch = /^\/api\/bots\/([\w-]+)$/.exec(path);
@@ -0,0 +1 @@
1
+ :root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:#fff;--sidebar:#fafafa;--surface:#fff;--raised:#f4f4f5;--hover:#f4f4f5;--selected:#ececee;--text:#27272a;--muted:#71717b;--border:#e4e4e7;--input:#d4d4d8;--accent:#1b4ed8;--accent-text:#fff;--info:#1b4ed8;--ok:#16a34a;--warn:#bb4d00;--warn-surface:#fcf4e8;--bad:#c10007;--bad-surface:#fcebec;--code:#f4f4f5;--radius:10px;--font:-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--mono:ui-monospace, "SF Mono", Menlo, monospace;font-family:var(--font);font-size:13px}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial;--bg:#0a0a0a;--sidebar:#000;--surface:#111;--raised:#141414;--hover:#131313;--selected:#1a1b1b;--text:#f5f5f5;--muted:#818181;--border:#1f1f1f;--input:#262626;--accent:#346bf1;--info:#51a2ff;--ok:#22c55e;--warn:#ffb900;--warn-surface:#312108;--bad:#ff6467;--bad-surface:#301214;--code:#111}}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;margin:0}a{color:var(--info)}button{font:inherit;color:inherit}code,.mono{font-family:var(--mono);font-size:12px}code{background:var(--code);border:1px solid var(--border);border-radius:5px;padding:0 4px}h1{margin:0;font-size:16px;font-weight:650}h2{margin:0;font-size:13.5px;font-weight:600}p{margin:6px 0;line-height:1.5}.muted{color:var(--muted)}.small{font-size:12px}.ok-text{color:var(--ok)}.error-text{color:var(--bad)}.row{align-items:center;gap:8px;display:flex}.row.wrap{flex-wrap:wrap}.spread{justify-content:space-between}.stack{gap:12px;display:grid}.stack.tight{gap:8px}.shell{grid-template-columns:248px 1fr;height:100%;display:grid}.sidebar{background:var(--sidebar);border-right:1px solid var(--border);flex-direction:column;min-height:0;display:flex}.sidebar-head{justify-content:space-between;align-items:center;padding:14px 12px 8px 14px;display:flex}.brand{align-items:center;gap:8px;font-size:13.5px;font-weight:650;display:flex}.brand svg{color:var(--accent)}.sidebar-list{flex:1;padding:4px 6px 10px;overflow-y:auto}.group{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);align-items:center;gap:6px;padding:12px 8px 4px;font-size:10.5px;display:flex}.item{all:unset;box-sizing:border-box;cursor:pointer;border-radius:8px;grid-template-columns:1fr auto;gap:2px 8px;width:100%;padding:8px;display:grid}.item:hover{background:var(--hover)}.item.selected{background:var(--selected)}.item-name{align-items:center;gap:7px;min-width:0;font-weight:550;display:flex}.item-name span:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.item-side{color:var(--muted);font-size:11.5px}.item-sub{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;grid-column:1/-1;padding-left:14px;font-size:11.5px;overflow:hidden}.item-sub.working{color:var(--info)}.item-sub.problem{color:var(--bad)}.badge{background:var(--info);color:var(--bg);border-radius:99px;padding:0 6px;font-size:10.5px;font-weight:700;line-height:16px}.sidebar-foot{border-top:1px solid var(--border);gap:2px;padding:8px 6px 10px;display:grid}.foot-line{color:var(--muted);align-items:center;gap:7px;padding:2px 8px;font-size:11.5px;display:flex}.empty-list{color:var(--muted);padding:16px 8px;font-size:12px;line-height:1.5}.main{min-width:0;overflow-y:auto}.page{max-width:860px;padding:20px 24px 40px}.page-head{align-items:flex-start;gap:12px;margin-bottom:4px;display:flex}.page-head .grow{flex:1;min-width:0}.subtitle{color:var(--muted);flex-wrap:wrap;gap:2px 6px;margin-top:3px;font-size:12px;display:flex}.btn.back{display:none}@media (max-width:720px){.shell{grid-template-columns:1fr}.shell.detail .sidebar,.shell:not(.detail) .main{display:none}.page{padding:14px 14px 32px}.btn.back{display:inline-flex}}.btn{cursor:pointer;border:1px solid var(--border);background:var(--surface);white-space:nowrap;border-radius:8px;align-items:center;gap:6px;padding:6px 11px;font-size:12.5px;font-weight:550;display:inline-flex}.btn:hover:not(:disabled){background:var(--hover)}.btn:disabled{opacity:.5;cursor:default}.btn.primary{background:var(--accent);color:var(--accent-text);border-color:#0000}.btn.primary:hover:not(:disabled){background:var(--accent);filter:brightness(1.08)}.btn.ghost{color:var(--muted);background:0 0;border-color:#0000}.btn.ghost:hover:not(:disabled){color:var(--text)}.btn.danger{color:var(--bad)}.btn.sm{padding:4px 9px;font-size:12px}.btn.icon{padding:4px 8px}.input{border:1px solid var(--input);background:var(--bg);width:100%;color:var(--text);font:inherit;border-radius:8px;padding:7px 9px}.input:focus{outline:2px solid color-mix(in srgb, var(--accent) 45%, transparent);outline-offset:-1px}textarea.input{resize:vertical;line-height:1.45}.field{gap:5px;display:grid}.field>span{font-size:12px;font-weight:550}.field .hint{color:var(--muted);font-size:11.5px;font-weight:400}.check{align-items:flex-start;gap:8px;font-size:12.5px;line-height:1.45;display:flex}.check input{margin-top:2px}.segmented{border:1px solid var(--border);border-radius:8px;gap:2px;padding:2px;display:inline-flex}.segmented button{cursor:pointer;color:var(--muted);background:0 0;border:0;border-radius:6px;padding:4px 10px;font-size:12px}.segmented button.active{background:var(--selected);color:var(--text);font-weight:550}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px}.dot{background:var(--muted);border-radius:50%;flex:none;width:7px;height:7px}.dot.ok{background:var(--ok)}.dot.pending{background:var(--warn)}.dot.bad{background:var(--bad)}.dot.working{background:var(--info);box-shadow:0 0 0 3px color-mix(in srgb, var(--info) 22%, transparent)}.platform{color:#fff;border-radius:5px;flex:none;place-items:center;width:18px;height:18px;font-size:10px;font-weight:800;display:inline-grid}.platform.lg{border-radius:8px;width:32px;height:32px;font-size:14px}.platform.slack{background:#611f69}.banner{border:1px solid var(--border);background:var(--raised);border-radius:8px;padding:10px 12px;font-size:12.5px;line-height:1.5}.banner.warn{background:var(--warn-surface);border-color:color-mix(in srgb, var(--warn) 35%, transparent)}.banner.bad{background:var(--bad-surface);border-color:color-mix(in srgb, var(--bad) 35%, transparent);color:var(--bad)}.tabs{border-bottom:1px solid var(--border);gap:18px;margin:14px 0 16px;display:flex}.tabs button{cursor:pointer;color:var(--muted);background:0 0;border:0;border-bottom:2px solid #0000;margin-bottom:-1px;padding:0 0 9px;font-size:12.5px}.tabs button.active{color:var(--text);border-bottom-color:var(--accent);font-weight:600}.facts{grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px;display:grid}.fact{border:1px solid var(--border);border-radius:8px;padding:8px 10px}.fact .k{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-size:10.5px}.fact .v{margin-top:2px;font-weight:600}.menu{position:relative}.menu-list{z-index:5;background:var(--surface);border:1px solid var(--border);border-radius:9px;min-width:200px;padding:4px;position:absolute;top:calc(100% + 4px);right:0;box-shadow:0 8px 24px #00000040}.menu-list button{text-align:left;cursor:pointer;background:0 0;border:0;border-radius:6px;width:100%;padding:7px 9px;font-size:12.5px;display:block}.menu-list button:hover{background:var(--hover)}.menu-list .danger{color:var(--bad)}.feed{gap:14px;display:grid}.req{gap:6px;display:grid}.msg{grid-template-columns:26px 1fr;gap:10px;display:grid}.avatar{background:var(--raised);border:1px solid var(--border);border-radius:50%;place-items:center;width:26px;height:26px;font-size:11px;font-weight:700;display:grid}.avatar.bot{background:var(--accent);color:var(--accent-text);border-color:#0000}.bubble{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);min-width:0;padding:9px 11px}.bubble.reply{background:var(--raised)}.who{color:var(--muted);margin-bottom:3px;font-size:11.5px}.text{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.5}.chips{flex-wrap:wrap;align-items:center;gap:6px;margin-top:8px;display:flex}.chip{background:var(--raised);border:1px solid var(--border);color:var(--muted);border-radius:5px;padding:2px 7px;font-size:11px}.chip.completed{color:var(--ok)}.chip.failed{color:var(--bad)}.chip.running,.chip.waiting,.chip.pending{color:var(--info)}.state{color:var(--muted);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.state.working{color:var(--info)}.state.waiting{color:var(--warn)}.state.bad{color:var(--bad)}.state.ok{color:var(--ok)}.empty{text-align:center;color:var(--muted);padding:36px 16px;line-height:1.6}.stepper{gap:6px;margin:14px 0 16px;display:flex}.stepper div{color:var(--muted);border-top:2px solid var(--border);flex:1;padding-top:8px;font-size:11.5px}.stepper .done{border-color:var(--ok);color:var(--text)}.stepper .current{border-color:var(--accent);color:var(--text);font-weight:600}.steps-list{gap:6px;margin:0;padding-left:18px;line-height:1.5;display:grid}.wizard-foot{justify-content:space-between;margin-top:16px;display:flex}.thread-list{border:1px solid var(--border);border-radius:8px;max-height:260px;overflow-y:auto}.thread-row{text-align:left;border:0;border-bottom:1px solid var(--border);cursor:pointer;background:0 0;gap:1px;width:100%;padding:7px 10px;display:grid}.thread-row:last-child{border-bottom:0}.thread-row:hover{background:var(--hover)}.thread-row.selected{background:color-mix(in srgb, var(--accent) 14%, transparent)}.thread-title{font-weight:550}details summary{cursor:pointer}.link-row{align-items:center;gap:14px;margin-top:10px;display:flex}.qr{background:#fff;border-radius:8px;flex:none;width:96px;height:96px;padding:6px}.log{background:var(--code);border:1px solid var(--border);font-family:var(--mono);white-space:pre-wrap;border-radius:8px;max-height:360px;margin:10px 0 0;padding:10px;font-size:11px;line-height:1.55;overflow:auto}.connect{max-width:460px;margin:15vh auto;padding:0 16px}