@openparachute/agent 0.2.2 → 0.2.3-rc.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.parachute/module.json +3 -3
  2. package/package.json +4 -1
  3. package/src/agent-defs.ts +9 -0
  4. package/src/auth.ts +182 -14
  5. package/src/backends/programmatic.ts +35 -2
  6. package/src/backends/registry.ts +159 -40
  7. package/src/backends/types.ts +44 -0
  8. package/src/daemon.ts +317 -12
  9. package/src/def-vault-triggers.ts +317 -0
  10. package/src/preflight.ts +139 -0
  11. package/src/spawn-agent.ts +16 -0
  12. package/src/step-up.ts +316 -0
  13. package/src/terminal-ui.ts +73 -0
  14. package/src/transports/http-ui.ts +10 -8
  15. package/src/transports/vault.ts +48 -27
  16. package/src/ui-kit.ts +6 -3
  17. package/src/ui-ticket.ts +121 -0
  18. package/web/ui/dist/assets/index-Dhr5Kl_d.css +1 -0
  19. package/web/ui/dist/assets/index-Di5MmFZR.js +60 -0
  20. package/web/ui/dist/index.html +2 -2
  21. package/src/_parked/interactive-spawn.test.ts +0 -324
  22. package/src/_parked/interactive-spawn.ts +0 -701
  23. package/src/agent-defs.test.ts +0 -1504
  24. package/src/agent-mcp-config.test.ts +0 -115
  25. package/src/agents.test.ts +0 -360
  26. package/src/auth.test.ts +0 -46
  27. package/src/backends/attached-queue.test.ts +0 -376
  28. package/src/backends/programmatic.test.ts +0 -1715
  29. package/src/backends/registry.test.ts +0 -1494
  30. package/src/backends/stream-json.test.ts +0 -570
  31. package/src/channel-backend-wiring.test.ts +0 -237
  32. package/src/credentials.test.ts +0 -274
  33. package/src/cron.test.ts +0 -342
  34. package/src/daemon-agent-def-api.test.ts +0 -166
  35. package/src/daemon-agent-defs-api.test.ts +0 -953
  36. package/src/daemon-agent-env-api.test.ts +0 -338
  37. package/src/daemon-attached-queue-store.test.ts +0 -65
  38. package/src/daemon-config-api.test.ts +0 -962
  39. package/src/daemon-jobs-api.test.ts +0 -271
  40. package/src/daemon-vault-chat.test.ts +0 -250
  41. package/src/daemon.test.ts +0 -746
  42. package/src/def-vaults.test.ts +0 -136
  43. package/src/delivery-state.test.ts +0 -110
  44. package/src/effective-env.test.ts +0 -114
  45. package/src/grants.test.ts +0 -638
  46. package/src/hub-jwt.test.ts +0 -161
  47. package/src/jobs.test.ts +0 -245
  48. package/src/mcp-http.test.ts +0 -265
  49. package/src/mint-token.test.ts +0 -152
  50. package/src/module-manifest.test.ts +0 -158
  51. package/src/programmatic-wiring.test.ts +0 -838
  52. package/src/registry.test.ts +0 -227
  53. package/src/resolve-port.test.ts +0 -64
  54. package/src/routing.test.ts +0 -184
  55. package/src/runner.test.ts +0 -506
  56. package/src/sandbox/config.test.ts +0 -150
  57. package/src/sandbox/egress.test.ts +0 -113
  58. package/src/sandbox/live-seatbelt.test.ts +0 -277
  59. package/src/sandbox/mounts.test.ts +0 -154
  60. package/src/sandbox/sandbox.test.ts +0 -168
  61. package/src/services-manifest.test.ts +0 -106
  62. package/src/spa-serve.test.ts +0 -116
  63. package/src/spawn-agent-cli.test.ts +0 -172
  64. package/src/spawn-agent.test.ts +0 -1218
  65. package/src/spawn-deps.test.ts +0 -54
  66. package/src/terminal-assets.test.ts +0 -50
  67. package/src/terminal.test.ts +0 -530
  68. package/src/transports/http-ui.test.ts +0 -455
  69. package/src/transports/telegram.test.ts +0 -174
  70. package/src/transports/vault.test.ts +0 -2011
  71. package/src/ui-kit.test.ts +0 -178
  72. package/web/ui/dist/assets/index-C-iWdFFV.css +0 -1
  73. package/web/ui/dist/assets/index-VFETBk0a.js +0 -60
  74. package/web/ui/tsconfig.json +0 -21
@@ -1,178 +0,0 @@
1
- /**
2
- * Unit tests for the shared UI kit (src/ui-kit.ts) — the foundation every
3
- * channel page adopts. Guards the shell contract (active-tab marking, controls
4
- * slot, nav set) + the token/CSS invariants the pages depend on.
5
- */
6
- import { describe, test, expect } from "bun:test";
7
- import { THEME_CSS, SHELL_JS, appShell, NAV_VIEWS, BRAND } from "./ui-kit.ts";
8
-
9
- describe("appShell", () => {
10
- test("renders the brand + all nav tabs, marking only the active one", () => {
11
- const h = appShell({ active: "agents" });
12
- expect(h).toContain("app-header");
13
- expect(h).toContain('class="brand-mark"');
14
- for (const v of NAV_VIEWS) expect(h).toContain(`data-view="${v.view}"`);
15
- // active tab gets class="active"; others don't.
16
- expect(h).toContain('data-view="agents" href="#" class="active"');
17
- expect(h).toContain('data-view="chat" href="#"');
18
- expect(h).not.toContain('data-view="chat" href="#" class="active"');
19
- });
20
-
21
- test("marks Home active when it is the current view", () => {
22
- const h = appShell({ active: "home" });
23
- expect(h).toContain('data-view="home" href="#" class="active"');
24
- expect(h).toContain('data-view="chat" href="#"');
25
- expect(h).not.toContain('data-view="chat" href="#" class="active"');
26
- });
27
-
28
- test("status defaults, and a custom status + tag suffix render", () => {
29
- expect(appShell({ active: "chat" })).toContain('id="status"');
30
- const h = appShell({ active: "chat", status: "● ready", tag: "chat" });
31
- expect(h).toContain("● ready");
32
- expect(h).toContain("· chat");
33
- });
34
-
35
- test("controls slot is injected before the status when provided, omitted otherwise", () => {
36
- const withCtl = appShell({ active: "terminal", controls: "<button id='reconnect'>Reconnect</button>" });
37
- expect(withCtl).toContain("app-controls");
38
- expect(withCtl).toContain("id='reconnect'");
39
- expect(appShell({ active: "terminal" })).not.toContain("app-controls");
40
- });
41
-
42
- test("nav covers exactly home/chat/agents/schedules/terminal/config, Home first", () => {
43
- expect(NAV_VIEWS.map((v) => v.view)).toEqual([
44
- "home",
45
- "chat",
46
- "agents",
47
- "schedules",
48
- "terminal",
49
- "config",
50
- ]);
51
- });
52
- });
53
-
54
- describe("THEME_CSS", () => {
55
- test("declares the brand tokens incl. the warm-light bg, accent, warn, and dark term pane", () => {
56
- expect(THEME_CSS).toContain(":root");
57
- expect(THEME_CSS).toContain(`--bg: ${BRAND.bg}`); // #faf8f4, the warm light brand
58
- expect(THEME_CSS).toContain(`--accent: ${BRAND.accent}`);
59
- expect(THEME_CSS).toContain(`--warn: ${BRAND.warn}`);
60
- expect(THEME_CSS).toContain(`--term-bg: ${BRAND.termBg}`); // #000 — the only intended black
61
- });
62
- test("ships the shared component layer (shell + buttons + banners + pills)", () => {
63
- for (const sel of [".app-nav", ".app-nav a.active", ".btn-primary", ".banner-warn", ".pill.warn"]) {
64
- expect(THEME_CSS).toContain(sel);
65
- }
66
- });
67
- test("carries NO leftover dark-console tokens (the pages unified on the brand)", () => {
68
- expect(THEME_CSS).not.toContain("#0f1115");
69
- expect(THEME_CSS).not.toContain("--panel");
70
- });
71
- });
72
-
73
- describe("SHELL_JS", () => {
74
- test("provides MOUNT derivation, nav wiring, token fetch, and helpers", () => {
75
- for (const sym of ["var MOUNT", "function wireShell", "function escapeHtml", "function setStatus", "function fetchToken", "function authedFetch", "function setTerminalNavVisible"]) {
76
- expect(SHELL_JS).toContain(sym);
77
- }
78
- // It hits the hub agent-token endpoint with the operator cookie.
79
- expect(SHELL_JS).toContain("/admin/agent-token");
80
- expect(SHELL_JS).toContain('credentials: "include"');
81
- });
82
-
83
- // Terminal-nav cleanup (Parachute Agent Phase 1): wireShell hides the standalone
84
- // Terminal nav entry by default (programmatic backend has no terminal); pages
85
- // reveal it via setTerminalNavVisible when an interactive agent exists. The
86
- // Terminal page itself (active === "terminal") shows it.
87
- test("wireShell gates the Terminal nav link via setTerminalNavVisible", () => {
88
- expect(SHELL_JS).toContain('a[data-view="terminal"]');
89
- // wireShell defaults the terminal entry to its own-page-only visibility.
90
- expect(SHELL_JS).toContain('setTerminalNavVisible(active === "terminal")');
91
- });
92
- test("is safe to interpolate — no naked backtick that could break a host literal", () => {
93
- expect(SHELL_JS.includes("`")).toBe(false);
94
- });
95
- test("exports a renderMarkdown helper (reused by the chat transcript)", () => {
96
- expect(SHELL_JS).toContain("function renderMarkdown");
97
- });
98
- });
99
-
100
- // renderMarkdown lives inside SHELL_JS (vanilla JS, no DOM). Evaluate SHELL_JS in
101
- // a fresh function scope and hand back its renderMarkdown so we can exercise it
102
- // directly — the same code the chat page runs in the browser.
103
- function loadRenderMarkdown(): (text: string) => string {
104
- // SHELL_JS defines `var MOUNT = window.location...` at the top; stub a minimal
105
- // window so that line doesn't throw when evaluated outside a browser.
106
- const factory = new Function(
107
- "window",
108
- SHELL_JS + "\nreturn renderMarkdown;",
109
- ) as (w: unknown) => (text: string) => string;
110
- return factory({ location: { pathname: "/ui" } });
111
- }
112
-
113
- describe("renderMarkdown (SHELL_JS, XSS-safe Markdown subset)", () => {
114
- const renderMarkdown = loadRenderMarkdown();
115
-
116
- test("escapes raw HTML first — a <script> tag never survives as markup", () => {
117
- const out = renderMarkdown("<script>alert(1)</script>");
118
- expect(out).not.toContain("<script>");
119
- expect(out).toContain("&lt;script&gt;");
120
- });
121
-
122
- test("renders bold and italic", () => {
123
- expect(renderMarkdown("**bold**")).toContain("<strong>bold</strong>");
124
- expect(renderMarkdown("an *italic* word")).toContain("<em>italic</em>");
125
- });
126
-
127
- test("renders inline code and fenced code blocks", () => {
128
- const bt = String.fromCharCode(96);
129
- expect(renderMarkdown(bt + "inline" + bt)).toContain("<code>inline</code>");
130
- const fenced = renderMarkdown(bt + bt + bt + "\nconst x = 1;\n" + bt + bt + bt);
131
- expect(fenced).toContain("<pre><code>");
132
- expect(fenced).toContain("const x = 1;");
133
- });
134
-
135
- test("does not apply inline rules inside code spans", () => {
136
- const bt = String.fromCharCode(96);
137
- const out = renderMarkdown(bt + "**not bold**" + bt);
138
- expect(out).toContain("<code>**not bold**</code>");
139
- expect(out).not.toContain("<strong>");
140
- });
141
-
142
- test("renders http/https links as anchors with the url preserved", () => {
143
- const out = renderMarkdown("[site](https://example.com/x)");
144
- expect(out).toContain('href="https://example.com/x"');
145
- expect(out).toContain(">site</a>");
146
- expect(out).toContain('rel="noopener noreferrer"');
147
- });
148
-
149
- test("rejects javascript: URLs — renders inert escaped text, no anchor", () => {
150
- const out = renderMarkdown("[click](javascript:alert(1))");
151
- // No anchor and no href is produced — the would-be URL never reaches markup.
152
- expect(out).not.toContain("<a ");
153
- expect(out).not.toContain("href=");
154
- // The markdown is left as inert escaped text (safe — not an executable link).
155
- expect(out).toContain("[click]");
156
- });
157
-
158
- test("rejects data: URLs too — only http/https survive as anchors", () => {
159
- const out = renderMarkdown("[x](data:text/html,<script>alert(1)</script>)");
160
- expect(out).not.toContain("<a ");
161
- expect(out).not.toContain("href=");
162
- // any escaped markup inside is inert text, never executable.
163
- expect(out).not.toContain("<script>");
164
- });
165
-
166
- test("escapes other canonical XSS vectors (img onerror, svg onload)", () => {
167
- const out1 = renderMarkdown('<img src=x onerror=alert(1)>');
168
- expect(out1).not.toContain("<img");
169
- expect(out1).toContain("&lt;img");
170
- const out2 = renderMarkdown('<svg onload=alert(1)>');
171
- expect(out2).not.toContain("<svg");
172
- expect(out2).toContain("&lt;svg");
173
- });
174
-
175
- test("converts newlines to <br>", () => {
176
- expect(renderMarkdown("line1\nline2")).toContain("line1<br>line2");
177
- });
178
- });
@@ -1 +0,0 @@
1
- :root{--bg: #faf8f4;--bg-soft: #f3f0ea;--fg: #2c2a26;--fg-muted: #6b6860;--fg-dim: #9a9690;--accent: #4a7c59;--accent-soft: rgba(74, 124, 89, .08);--accent-hover: #3d6849;--border: #e4e0d8;--border-light: #ece9e2;--card-bg: #ffffff;--error: #a3392b;--error-soft: rgba(163, 57, 43, .08);--warn: #b08023;--warn-soft: rgba(176, 128, 35, .08);--success: #3d6849;--success-soft: rgba(61, 104, 73, .08);--font-serif: Georgia, "Times New Roman", serif;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", monospace;font-family:var(--font-sans)}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.45rem .9rem;cursor:pointer;transition:background .15s ease}button:hover{background:var(--accent-hover)}button:disabled{opacity:.5;cursor:not-allowed}button.secondary{background:#fff;color:var(--fg);border:1px solid var(--border)}button.secondary:hover{background:var(--bg-soft)}code{font-family:var(--font-mono);font-size:.85em;background:var(--bg-soft);padding:.1em .3em;border-radius:3px}.page{max-width:960px;margin:0 auto;padding:1.5rem 1.5rem 6rem}.nav{display:flex;flex-wrap:wrap;gap:.6rem 1rem;align-items:center;padding-bottom:1rem;border-bottom:1px solid var(--border);margin-bottom:2rem}.nav .brand{font-weight:600;font-family:var(--font-serif);font-size:1.15rem;margin-right:auto;display:inline-flex;align-items:center;gap:.45rem;color:var(--accent);text-decoration:none}.nav .brand:hover{color:var(--accent-hover);text-decoration:none}.nav .brand-wordmark{color:var(--fg);letter-spacing:-.005em}.nav .brand .sub{color:var(--fg-dim);font-size:.78rem;font-weight:400;margin-left:.4rem;font-family:var(--font-sans)}.nav a{color:var(--fg-muted);font-size:.95rem}.nav a:hover{text-decoration:none;color:var(--fg)}.nav a.nav-link-active{color:var(--accent);font-weight:500;text-decoration:underline;text-underline-offset:.3em;text-decoration-thickness:2px}h1{margin:0 0 .5rem;font-family:var(--font-serif);font-size:1.85rem;font-weight:400;letter-spacing:-.01em;line-height:1.2;color:var(--fg)}h2{margin:0 0 1rem;font-size:1.4rem;font-weight:500}h3{margin:0 0 .5rem;font-size:1.05rem;font-weight:600}.muted{color:var(--fg-muted);font-size:.92rem}.dim{color:var(--fg-dim);font-size:.85rem}.lede{color:var(--fg-muted);font-size:.95rem;margin:0 0 1.5rem;max-width:60ch}.error-banner{background:var(--error-soft);border:1px solid var(--error);color:var(--error);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.info-banner{background:var(--accent-soft);border:1px solid var(--accent);color:var(--fg);padding:.65rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.88rem}.empty{border:1px dashed var(--border);border-radius:10px;padding:2rem 1.5rem;text-align:center;color:var(--fg-muted);background:var(--card-bg)}.loading{color:var(--fg-muted);padding:1rem 0;font-size:.92rem}.card{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:1.1rem 1.25rem;box-shadow:0 1px 2px #2c2a260a,0 8px 24px #2c2a260d;margin-bottom:1.5rem}.section-head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:.75rem}.section-head .count{color:var(--fg-dim);font-size:.85rem}table{width:100%;border-collapse:collapse;font-size:.88rem}th,td{text-align:left;padding:.6rem .7rem;border-bottom:1px solid var(--border);vertical-align:middle}th{color:var(--fg-muted);font-weight:500;font-size:.78rem;text-transform:uppercase;letter-spacing:.03em}tr.agent-row{cursor:pointer}tr.agent-row:hover{background:var(--bg-soft)}tr.agent-row.selected{background:var(--accent-soft)}.cell-name{font-weight:600;color:var(--fg)}.cell-dim{color:var(--fg-dim)}.pill{display:inline-block;padding:.1rem .5rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.02em;border:1px solid var(--border);color:var(--fg-muted);background:var(--bg-soft)}.pill.backend-programmatic{color:var(--accent);background:var(--accent-soft);border-color:transparent}.pill.backend-channel{color:var(--warn);background:var(--warn-soft);border-color:transparent}.pill.backend-interactive{color:var(--fg-muted);background:var(--bg-soft)}.pill.status-enabled,.pill.status-idle{color:var(--success);background:var(--success-soft);border-color:transparent}.pill.status-working{color:var(--accent);background:var(--accent-soft);border-color:transparent}.pill.status-pending,.pill.status-queued{color:var(--warn);background:var(--warn-soft);border-color:transparent}.pill.status-error{color:var(--error);background:var(--error-soft);border-color:transparent}.detail{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:1.25rem 1.4rem;margin-bottom:1.5rem;box-shadow:0 1px 2px #2c2a260a,0 8px 24px #2c2a260d}.detail-head{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-bottom:.9rem}.detail-head h2{margin:0;font-size:1.25rem}.detail-grid{display:grid;grid-template-columns:max-content 1fr;gap:.45rem 1.2rem;font-size:.9rem;margin-bottom:1rem}.detail-grid dt{color:var(--fg-muted);font-weight:500}.detail-grid dd{margin:0;color:var(--fg);word-break:break-word}.detail-prompt{background:var(--bg-soft);border:1px solid var(--border-light);border-radius:8px;padding:.75rem .9rem;font-family:var(--font-mono);font-size:.82rem;white-space:pre-wrap;color:var(--fg);margin:.3rem 0 1rem}.detail-note{font-size:.82rem;color:var(--fg-dim);margin:.5rem 0 0}.detail-close{margin-left:auto;background:#fff;color:var(--fg-muted);border:1px solid var(--border);padding:.3rem .7rem;font-size:.82rem}.detail-close:hover{background:var(--bg-soft);color:var(--fg)}.tag-list{display:flex;flex-wrap:wrap;gap:.35rem}.tag{font-family:var(--font-mono);font-size:.76rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:4px;padding:.05rem .35rem;color:var(--fg-muted)}.section-head-actions{display:inline-flex;align-items:center;gap:.9rem}.button-link{display:inline-block;background:var(--accent);color:#fff;border-radius:6px;padding:.4rem .85rem;font-size:.85rem;font-weight:500;transition:background .15s ease}.button-link:hover{background:var(--accent-hover);color:#fff;text-decoration:none}.field{display:block;border:0;margin:0 0 1.25rem;padding:0}.field>label,.field>legend{display:block;font-weight:500;font-size:.9rem;color:var(--fg);margin-bottom:.35rem;padding:0}.field input[type=text],.field select,.field textarea{width:100%;font:inherit;font-size:.9rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:.5rem .6rem}.field textarea{font-family:var(--font-mono);font-size:.82rem;resize:vertical}.field input[type=text]:focus,.field select:focus,.field textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.field-hint{color:var(--fg-dim);font-size:.8rem;margin:.3rem 0 0}.field-error{color:var(--error);font-size:.8rem;margin:.3rem 0 0}.radio-row{display:flex;align-items:flex-start;gap:.6rem;border:1px solid var(--border);border-radius:8px;padding:.65rem .8rem;margin-bottom:.5rem;cursor:pointer;transition:border-color .15s ease,background .15s ease}.radio-row:hover{background:var(--bg-soft)}.radio-row.selected{border-color:var(--accent);background:var(--accent-soft)}.radio-row input[type=radio]{margin-top:.2rem;accent-color:var(--accent)}.radio-body{display:flex;flex-direction:column;gap:.15rem}.radio-label{font-weight:500;font-size:.9rem;color:var(--fg)}.radio-help{font-size:.8rem;color:var(--fg-muted)}.advanced{margin:0 0 1.25rem}.advanced>summary{cursor:pointer;font-size:.88rem;font-weight:500;color:var(--fg-muted);margin-bottom:.75rem}.advanced>summary:hover{color:var(--fg)}.form-actions{display:flex;align-items:center;gap:1rem}.cancel-link{color:var(--fg-muted);font-size:.9rem}.success-banner{background:var(--success-soft);border:1px solid var(--success);color:var(--success);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.95rem}.snippet-row{display:flex;align-items:stretch;gap:.6rem;margin:.5rem 0}.snippet{flex:1;font-family:var(--font-mono);font-size:.8rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:6px;padding:.6rem .75rem;color:var(--fg);white-space:pre-wrap;word-break:break-all}.detail-actions{display:flex;gap:.6rem;margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border-light)}button.button-danger{background:#fff;color:var(--error);border:1px solid var(--error)}button.button-danger:hover{background:var(--error-soft)}button.button-danger:disabled{opacity:.5;cursor:not-allowed}.confirm-box{margin-top:1.25rem;padding:1rem;border:1px solid var(--error);border-radius:8px;background:var(--error-soft)}.confirm-prompt{margin:0 0 .75rem;font-size:.9rem;color:var(--fg)}.confirm-inline{display:inline-flex;align-items:center;gap:.6rem}.inline-form{margin:.75rem 0 1rem}.schedules,.detail-section{margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border-light)}.schedules .section-head h3,.detail-section .section-head h3{margin:0;font-size:1.05rem}.schedule-presets{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.45rem}.schedule-presets button{font-size:.78rem;padding:.2rem .55rem}.schedule-row-actions{display:inline-flex;align-items:center;gap:.6rem}.schedule-status{font-size:.82rem;color:var(--fg-muted);margin:.5rem 0 0}.chat-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:.75rem 1rem;margin-bottom:1rem}.chat-head h1{margin:0}.chat-picker{display:inline-flex;align-items:center;gap:.5rem}.chat-picker-label{font-size:.82rem;color:var(--fg-muted)}.chat-picker select{font:inherit;font-size:.88rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:.35rem .5rem}.chat-status{font-size:.8rem;color:var(--fg-muted);margin-left:auto}.chat-status.status-live{color:var(--success)}.chat-status.status-err{color:var(--error)}.transcript{display:flex;flex-direction:column;gap:.5rem;height:60vh;min-height:18rem;overflow-y:auto;padding:1rem;background:var(--card-bg);border:1px solid var(--border);border-radius:12px 12px 0 0}.transcript .msg{max-width:78%;padding:.5rem .75rem;border-radius:12px;font-size:.9rem;line-height:1.4;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:anywhere}.transcript .msg.you{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:4px}.transcript .msg.them{align-self:flex-start;background:var(--bg-soft);color:var(--fg);border:1px solid var(--border);border-bottom-left-radius:4px}.transcript .msg.sys{align-self:center;background:transparent;color:var(--fg-muted);font-size:.8rem;font-style:italic;max-width:90%;text-align:center}.transcript .msg.live{border-style:dashed;animation:chatLivePulse 1.4s ease-in-out infinite}@keyframes chatLivePulse{0%,to{opacity:1}50%{opacity:.6}}.transcript .msg.live.errored{border-color:var(--error);color:var(--error);animation:none}.transcript .live-tools{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.4rem}.transcript .tool-chip{font-family:var(--font-mono);font-size:.72rem;padding:.05rem .45rem;border-radius:10px;background:var(--card-bg);color:var(--fg-muted);border:1px solid var(--border)}.transcript .live-working{margin-top:.4rem;font-size:.75rem;color:var(--fg-muted);font-style:italic}.composer{display:flex;gap:.6rem;padding:.75rem 1rem;background:var(--card-bg);border:1px solid var(--border);border-top:0;border-radius:0 0 12px 12px;margin-bottom:1.5rem}.composer .chat-input{flex:1;font:inherit;font-size:.9rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:.55rem .7rem;resize:none;max-height:7.5rem}.composer .chat-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.composer button{flex:0 0 auto;align-self:flex-end}