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 +18 -15
- package/dist/cli.cjs +150 -3
- package/dist/panel/assets/index-BCdCSEsV.css +1 -0
- package/dist/panel/assets/index-CsFBWeyA.js +17 -0
- package/dist/panel/index.html +2 -2
- package/package.json +3 -1
- package/dist/panel/assets/index-BEUC2x_H.css +0 -1
- package/dist/panel/assets/index-Buu6e9cG.js +0 -10
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
|
-
|
|
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**
|
|
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
|
|
54
|
+
In the control panel, choose **+ Add** and follow the four steps:
|
|
51
55
|
|
|
52
|
-
1. Name the bot and
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
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}
|