pi-roundtable-web 0.0.0-stage → 0.7.2
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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +225 -2
- package/dist/chunk-e8z9q554.css +1 -0
- package/dist/chunk-gkh6x5ag.js +9 -0
- package/dist/index.html +13 -0
- package/package.json +54 -5
- package/src/api-types.ts +128 -0
- package/src/assets.ts +47 -0
- package/src/cloudflare-access.ts +59 -0
- package/src/console-api.ts +354 -0
- package/src/console-server.ts +214 -0
- package/src/conversations.ts +383 -0
- package/src/index.ts +29 -0
- package/src/options.ts +140 -0
- package/src/verifier.ts +22 -0
- package/src/web-plugin.ts +88 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.7.2] - 2026-10-02
|
|
9
|
+
|
|
10
|
+
First release published by the lockstep workflow; no changes to the package.
|
|
11
|
+
The `v0.7.1` tag published nothing.
|
|
12
|
+
|
|
13
|
+
## [0.7.0] - 2026-10-02
|
|
14
|
+
|
|
15
|
+
Prepared for the first npm publication.
|
|
16
|
+
The earlier `0.1.0` was local-only and was never published on npm.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Move into the pi-roundtable workspace with preserved Git history and lockstep version `0.7.0`.
|
|
21
|
+
The core peer range is `>=0.7.0 <0.8.0`, and the shared `publish.yml` releases all packages from one `v*` tag.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `webConsole(options)`: a plugin that serves a web console on a route of the host's HTTP listener, `/console` by default. The Overview pane shows the agent team, the Conversations pane lists the owner, agent, group-member (`agentgroup_<channel>_<agent>`), and outside-agent conversations stored on disk and shows each transcript (and its archives), and the Notes pane views and edits the owner's memory. The `panes` option chooses the panes.
|
|
26
|
+
- Live updates over a server-sent event stream: a change in the channel queue or the agent team, or a note written through the console, reaches an open page as one coalesced `changed` event.
|
|
27
|
+
- A `verifier` option, required: `RequestVerifier`, a function from a request to `{ admitted: true }` or `{ admitted: false, reason }`. The plugin throws when the config is loaded if it is missing, so a host never serves the console unauthenticated. `admit()` and `refuse(reason)` build the answers.
|
|
28
|
+
- `cloudflareAccess({ teamDomain, audience, email, keys? })`, the built-in verifier: it admits a request whose `Cf-Access-Jwt-Assertion` token is signed by the team's keys, issued by the team, addressed to the application, and carries an allowed email.
|
|
29
|
+
- Requests that change data must carry the console's own `Origin` (the required `origin` option); every response carries `nosniff` and a same-origin referrer policy, and the page carries a Content-Security-Policy with `frame-ancestors 'none'`.
|
|
30
|
+
- Options `ownerId`, `dataDir`, `mountPath`, `listener`, `title`, `relayNotes`, and `exclude`, checked when the plugin is created.
|
|
31
|
+
- The page ships prebuilt in `dist/` with relative URLs, so it works under any `mountPath` and an operator needs no build step. `bun run build` rebuilds it from a checkout.
|
|
32
|
+
- Types for the API shapes (`ConfigView`, `OverviewView`, `ConversationsView`, `TranscriptView`, `NoteView`, and the rest).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wayne930242
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,226 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-roundtable-web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An owner-only web console for [pi-roundtable][roundtable].
|
|
4
|
+
It serves one page on a route of the host's HTTP listener, where the owner can browse conversations, read their transcripts, view and edit the assistant's memory notes, and watch the agent team, with live updates.
|
|
5
|
+
|
|
6
|
+
The console reads what the host already stores: the conversation files under the data directory, the memory addon, and the agent team.
|
|
7
|
+
It adds no database, no cookie, and no account system.
|
|
8
|
+
Every request must pass an authentication check that you configure, and the plugin refuses to start without one.
|
|
9
|
+
|
|
10
|
+
## What the console shows
|
|
11
|
+
|
|
12
|
+
The console has up to three panes.
|
|
13
|
+
The `panes` option chooses which ones a host serves.
|
|
14
|
+
|
|
15
|
+
| Pane | What it shows |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Overview | Every active agent with its model, thinking level, state (working where, waiting, or idle), context use, schedule count, and last activity; every group with its members, host, and busy count. An agent links to its Discord channel and its transcript, and a group to its Discord channel. |
|
|
18
|
+
| Conversations | Every conversation stored on disk, in four sections: owner channels (direct messages and channels where the owner talks to the assistant), agent channels, group conversations (each member's conversation inside a group), and outside-agent conversations opened over MCP. Each row shows the channel's name (when the host has a Discord connection), when it was last active, its size, and how many archives it has. An outside-agent conversation shows its first message, when it appears in the first 256 KB of the file, and when it began. |
|
|
19
|
+
| Notes | The owner's memory in three tabs, core, notes, and events, with search. The owner can add a note, edit its text, kind, and date, and delete it after a confirmation. The memory addon's own validation applies, so a refusal shows its reason and saves nothing. |
|
|
20
|
+
|
|
21
|
+
Opening a conversation shows its **transcript**: the user's and the assistant's messages, the tools the assistant called with a short preview of their arguments, and each tool's result (collapsed).
|
|
22
|
+
A conversation that was started over keeps its archives, and the page can show any of them.
|
|
23
|
+
The system prompt and the model's private reasoning are never shown.
|
|
24
|
+
A transcript reads at most 8 MB of the newest files and returns at most the last 1000 entries, and it says when it cut the beginning.
|
|
25
|
+
|
|
26
|
+
The page follows changes without a reload.
|
|
27
|
+
A turn starting or ending, a queued message, a change in the agent team, or a note written through the console reaches an open page through a server-sent event stream within about a quarter of a second, and a burst of changes arrives as one event.
|
|
28
|
+
Relative times refresh every 30 seconds.
|
|
29
|
+
Times and event dates use the host's time zone (`env.timeZone`).
|
|
30
|
+
|
|
31
|
+
The console is read-only for conversations.
|
|
32
|
+
It does not start conversations over, delete them, or change agent settings; the only thing it writes is the owner's notes.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Bun 1.3 or later, and pi-roundtable `>=0.7.0 <0.8.0` as a peer dependency.
|
|
37
|
+
- The host must run on the machine that holds its data directory: the console reads `<dataDir>/sessions` from disk.
|
|
38
|
+
- The `memory` addon for the Notes pane (it is on by default; switch the pane off with `panes` when you run without memory).
|
|
39
|
+
- A way to authenticate the owner in front of the listener, such as Cloudflare Access, or a verifier of your own (see [Authentication](#authentication)).
|
|
40
|
+
- A Discord connection is optional: with the Discord entry's `DISCORD` service the console names channels, and without one it shows channel ids.
|
|
41
|
+
|
|
42
|
+
The package ships its page prebuilt in `dist/`, so installing it needs no build step and no bundler on the host.
|
|
43
|
+
A git checkout has no `dist/` until you run `bun run build`, and the plugin stops at startup with that instruction when the page is missing, so a broken install never serves a blank page.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bun add pi-roundtable-web
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Add the plugin to `roundtable.config.ts`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import type { RoundtableConfig } from "pi-roundtable";
|
|
55
|
+
import { cloudflareAccess, webConsole } from "pi-roundtable-web";
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
// ...the settings `init` wrote...
|
|
59
|
+
plugins: [
|
|
60
|
+
webConsole({
|
|
61
|
+
verifier: cloudflareAccess({
|
|
62
|
+
teamDomain: "example.cloudflareaccess.com",
|
|
63
|
+
audience: process.env.CONSOLE_ACCESS_AUD ?? "",
|
|
64
|
+
email: "owner@example.com",
|
|
65
|
+
}),
|
|
66
|
+
origin: "https://console.example.com",
|
|
67
|
+
ownerId: "<the owner's Discord user id>",
|
|
68
|
+
dataDir: "./data",
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
} satisfies RoundtableConfig;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The plugin adds the route `/console` (a redirect to `/console/`) and `/console/…` to the `public` listener, and a line with the console's address to the dashboard message.
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
| Option | Default | What it does |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `verifier` | none, required | Decides whether a request comes from the owner. `cloudflareAccess(...)` is built in; pass your own function to use another proxy. |
|
|
81
|
+
| `origin` | none, required | The console's own origin as the browser sees it, such as `https://console.example.com`: scheme and host (and port), no path. Requests that change data must carry it as their `Origin`, and the dashboard line links to it. |
|
|
82
|
+
| `ownerId` | none | The id the owner's memory is kept under. Required when the Notes pane is served. |
|
|
83
|
+
| `dataDir` | `./data` | The host's data directory; the console reads `<dataDir>/sessions`. Set it to the `dataDir` of your configuration. |
|
|
84
|
+
| `mountPath` | `/console` | The path the console is served under: one or more segments of letters, digits, `.`, `_`, `~`, and `-`. |
|
|
85
|
+
| `listener` | `public` | The id of the listener whose address serves the console. |
|
|
86
|
+
| `panes` | all three | Which panes to serve and in what order: any of `"overview"`, `"conversations"`, `"notes"`. A pane that is not served answers 404 and is not in the page. |
|
|
87
|
+
| `title` | `Roundtable` | The page's heading and title. |
|
|
88
|
+
| `relayNotes` | the remote MCP note | Text that an outside agent's relayed message begins with, taken off before the owner's words are shown. Set it to the same value as the `relayNote` of [pi-roundtable-mcp][mcp] when you changed that one. |
|
|
89
|
+
| `exclude` | none | A function from a channel key to `true` for conversations the console must neither list nor read, such as the channels of another plugin that keeps its own sessions in the same directory. |
|
|
90
|
+
|
|
91
|
+
The options are checked when the plugin is created, so a bad setting stops the host at startup: no verifier, an `origin` with a path, a `mountPath` of `/` or with `..` in it, an unknown or repeated pane, or Notes without an `ownerId`.
|
|
92
|
+
|
|
93
|
+
The console also needs the agent server for the Overview pane and the memory addon for the Notes pane.
|
|
94
|
+
The host refuses to start with a message naming the missing service when one of them is absent for a pane you serve.
|
|
95
|
+
|
|
96
|
+
## Authentication
|
|
97
|
+
|
|
98
|
+
The console never decides who the owner is.
|
|
99
|
+
It asks the `verifier` about every request under its path, before it reads anything, and answers `403 Forbidden` with no detail when the verifier refuses, throws, or rejects.
|
|
100
|
+
|
|
101
|
+
A verifier is a function that receives the `Request` and returns `{ admitted: true }` or `{ admitted: false, reason }`.
|
|
102
|
+
The reason goes to the host's log and is never sent to the client, so keep secrets out of it.
|
|
103
|
+
`admit()` and `refuse(reason)` build the two answers.
|
|
104
|
+
|
|
105
|
+
### Cloudflare Access
|
|
106
|
+
|
|
107
|
+
`cloudflareAccess(options)` is the built-in verifier.
|
|
108
|
+
Cloudflare Access sits in front of your tunnel or proxy, signs in the owner, and adds a signed token to each request it forwards as the `Cf-Access-Jwt-Assertion` header.
|
|
109
|
+
The verifier admits a request only when the token is signed by your team's published keys, was issued by your team, is addressed to this application, and carries an email you allow (compared without regard to case).
|
|
110
|
+
It checks the token on every request and fetches the team's signing keys from `https://<team>.cloudflareaccess.com/cdn-cgi/access/certs`, caching them and following their rotation.
|
|
111
|
+
If the keys cannot be fetched and none are cached, requests are refused.
|
|
112
|
+
|
|
113
|
+
| Option | What it is |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `teamDomain` | Your Access team domain, `<team-name>.cloudflareaccess.com`, with no scheme or path. |
|
|
116
|
+
| `audience` | The Application Audience (AUD) tag of the Access application that protects the console. |
|
|
117
|
+
| `email` | The email, or a list of emails, that may use the console. |
|
|
118
|
+
|
|
119
|
+
To set it up:
|
|
120
|
+
|
|
121
|
+
1. Route a hostname to the host's listener, for example with a Cloudflare Tunnel whose public hostname (`console.example.com`) points at the listener's port or unix socket.
|
|
122
|
+
Use a hostname you do not share with anything that must stay open.
|
|
123
|
+
2. In the Cloudflare dashboard, open Zero Trust, then Access controls, then Applications, and add a **Self-hosted** application for that hostname.
|
|
124
|
+
Add the path `console` to the application's public hostname to protect only the console, or leave the path empty to protect the whole hostname.
|
|
125
|
+
3. Give the application one **Allow** policy that includes only the owner's email.
|
|
126
|
+
Do not add a Bypass or an Everyone policy.
|
|
127
|
+
4. Open the application's configuration, and under Additional settings copy the **Application Audience (AUD) Tag**.
|
|
128
|
+
Keep it in an environment variable and pass it as `audience`.
|
|
129
|
+
5. Take your team name from Zero Trust settings, the `<team-name>` of `<team-name>.cloudflareaccess.com`, and pass the full domain as `teamDomain`.
|
|
130
|
+
6. Set the plugin's `origin` to the address you opened in step 1, `https://console.example.com`.
|
|
131
|
+
|
|
132
|
+
A request to the console from a browser that has not signed in is sent by Access to its login page and never reaches the host.
|
|
133
|
+
The host still refuses any request that arrives without a valid token, so a listener that becomes reachable without Access, through a wrong route or a second tunnel, does not open the console.
|
|
134
|
+
|
|
135
|
+
### A verifier of your own
|
|
136
|
+
|
|
137
|
+
Behind another authenticating proxy, write the verifier for what that proxy sends.
|
|
138
|
+
This one trusts a header that a reverse proxy sets after it has authenticated the owner, which is safe only when the proxy removes the header from client requests and the listener cannot be reached except through the proxy, for example on a unix socket (`http.socketPath`) that only the proxy's user can open:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { admit, refuse, webConsole } from "pi-roundtable-web";
|
|
142
|
+
|
|
143
|
+
const OWNER = "owner@example.com";
|
|
144
|
+
|
|
145
|
+
export const web = webConsole({
|
|
146
|
+
verifier: (request) =>
|
|
147
|
+
request.headers.get("x-authenticated-email")?.toLowerCase() === OWNER
|
|
148
|
+
? admit()
|
|
149
|
+
: refuse("not the owner"),
|
|
150
|
+
origin: "https://console.example.com",
|
|
151
|
+
ownerId: "<the owner's Discord user id>",
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A verifier can also verify a signed token itself, with `jose` or any other library, which does not depend on the network path and is the stronger choice when the proxy can sign what it sends.
|
|
156
|
+
It may be asynchronous; a thrown error or a rejected promise counts as a refusal.
|
|
157
|
+
|
|
158
|
+
## Threat model
|
|
159
|
+
|
|
160
|
+
**What the console protects.**
|
|
161
|
+
The conversations hold everything the owner said to the assistant and what the assistant answered, including tool calls and their results, and the memory notes shape every later turn.
|
|
162
|
+
The console serves that to one owner and nobody else.
|
|
163
|
+
It has no roles, no per-user views, and no audit trail beyond the host's log, which records only that a request was refused and why.
|
|
164
|
+
|
|
165
|
+
**Authentication is the verifier, and only the verifier.**
|
|
166
|
+
The console sets no cookie and keeps no session.
|
|
167
|
+
The built-in Cloudflare Access verifier checks a signature, so a client that can reach the listener directly still cannot forge a token.
|
|
168
|
+
A custom verifier that trusts a plain header is exactly as strong as the guarantee that clients cannot set that header: use a unix socket or a firewall rule so that only the proxy reaches the listener, and have the proxy strip the header from inbound requests.
|
|
169
|
+
The console does not rate-limit failed requests; do that at the proxy.
|
|
170
|
+
|
|
171
|
+
**Cross-site requests.**
|
|
172
|
+
A request that changes data (`POST`, `PATCH`, `DELETE`) is refused unless its `Origin` header is exactly the configured `origin`, after the verifier has admitted it.
|
|
173
|
+
Reads change nothing.
|
|
174
|
+
A page on another site that makes the owner's browser send a request therefore cannot write notes, and it cannot read the answers either, because the console sends no CORS headers.
|
|
175
|
+
|
|
176
|
+
**Cross-site scripting and framing.**
|
|
177
|
+
The page is a prebuilt bundle served from the same origin.
|
|
178
|
+
Every response carries `X-Content-Type-Options: nosniff` and `Referrer-Policy: same-origin`, and the page carries a Content-Security-Policy of `default-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'`, so it loads only its own script and style, cannot be framed, and cannot be pointed at another base.
|
|
179
|
+
Transcript text, notes, and channel names are rendered as plain text; the page never interprets them as HTML or Markdown, and it opens no link taken from a transcript.
|
|
180
|
+
Links to Discord are built by the page from channel ids, not taken from stored text.
|
|
181
|
+
|
|
182
|
+
**Errors reveal nothing to the client.**
|
|
183
|
+
Every API error is a fixed message.
|
|
184
|
+
It never repeats the request URL, a header, or a token, and a refusal answers `403 Forbidden` with that single word.
|
|
185
|
+
The console logs a failure with the error that was thrown and nothing from the request: no URL, no headers.
|
|
186
|
+
The text of an error that your own code throws, such as a memory store's, is logged as it is.
|
|
187
|
+
|
|
188
|
+
**Path handling and resource limits.**
|
|
189
|
+
A conversation is named by a channel key that must match `discord:<id>`, `agentgroup:<id>.<agent>`, or `mcp:<uuid>` exactly, and an archive is chosen from the folder's own listing, so no request can name a path.
|
|
190
|
+
A request body is limited to 64 KB, a transcript to 8 MB read and 1000 entries, an entry's text to 20 000 characters, and the list of an outside agent's first message to the first 256 KB of its file.
|
|
191
|
+
At most 32 event streams stay open at once, and a stream whose client falls 16 events behind is closed, after which the page reconnects and refetches.
|
|
192
|
+
|
|
193
|
+
**What the transcript can reveal.**
|
|
194
|
+
A transcript shows the arguments of the assistant's tool calls (the first 200 characters) and the first 2000 characters of each result.
|
|
195
|
+
If your tools handle secrets, they can appear there.
|
|
196
|
+
Serve only the `notes` pane, or use `exclude`, when that is a concern.
|
|
197
|
+
|
|
198
|
+
**Out of scope.**
|
|
199
|
+
Transport security (terminate TLS at the proxy), protecting the data directory on disk, and other users of the host are not the console's job.
|
|
200
|
+
The console is for one owner on one host.
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
This package lives in `packages/web` in the pi-roundtable workspace.
|
|
205
|
+
Install at the repository root, then run the package scripts:
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
bun install --frozen-lockfile
|
|
209
|
+
bun run --cwd packages/web build # builds the page into dist/
|
|
210
|
+
bun run --cwd packages/web typecheck # the server and the page
|
|
211
|
+
bun run --cwd packages/web lint
|
|
212
|
+
bun run --cwd packages/web test
|
|
213
|
+
bun run --cwd packages/web fixture # a local host with fixture data
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`bun run fixture` starts the plugin on pi-roundtable's test harness over fixture conversations on port 4173, behind a stand-in for the authenticating proxy that signs a test Access token, and the same listener without it on port 4174, which must answer 403.
|
|
217
|
+
`/fixture/message` appends a message to the owner's conversation and `/fixture/work` toggles the agent between working and idle, so the live updates can be watched.
|
|
218
|
+
|
|
219
|
+
The page is React, bundled with Bun's bundler into relative URLs, so it works under any `mountPath`.
|
|
220
|
+
Pages are addressed by the URL fragment (`#/notes`, `#/conversations/<key>`), which keeps every page at the console's root path.
|
|
221
|
+
|
|
222
|
+
The package shares the core's version and single `v*` release tag.
|
|
223
|
+
The shared `publish.yml` checks every workspace, then publishes each separately with provenance; this package's `prepack` builds the page.
|
|
224
|
+
|
|
225
|
+
[roundtable]: https://www.npmjs.com/package/pi-roundtable
|
|
226
|
+
[mcp]: https://www.npmjs.com/package/pi-roundtable-mcp
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--buncss-light:initial;--buncss-dark: ;color-scheme:light dark;--bg:#f7f7f8;--surface:#fff;--text:#1c1d21;--muted:#62666f;--border:#d9dbe0;--accent:#2f5fd0;--accent-text:#fff;--danger:#b3261e;--busy:#1d7a46;--warn:#9a6200;--radius:8px;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;line-height:1.5}@media (prefers-color-scheme:dark){:root{--buncss-light: ;--buncss-dark:initial;--bg:#15161a;--surface:#1e2025;--text:#e8e9ed;--muted:#9a9ea8;--border:#343741;--accent:#7aa2ff;--accent-text:#101216;--danger:#ff8a80;--busy:#6fd49a;--warn:#f0b95b}}*{box-sizing:border-box}body{background:var(--bg);color:var(--text);margin:0}a{color:var(--accent)}h1,h2{margin:0;font-size:1.05rem;font-weight:600}h2{font-size:1rem}.shell{max-width:56rem;margin:0 auto;padding:1rem}.bar{display:flex;flex-wrap:wrap;justify-content:space-between;align-items: center;gap:.75rem;padding-bottom:1rem}.bar-start{display:flex;flex-wrap:wrap;align-items: center;gap:1rem}nav{display:flex;gap:.25rem}nav a{border-radius:var(--radius);color:var(--muted);text-decoration:none;padding:.25rem .7rem}nav a[aria-current=page]{background:var(--surface);border:1px solid var(--border);color:var(--text)}.live{color:var(--muted);font-size:.8rem}.live-on{color:var(--busy)}main{display:flex;flex-direction:column;gap:1.5rem;padding-bottom:3rem}.stack{display:flex;flex-direction:column;gap:.75rem}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:.75rem 1rem}.card-link{display:block;color:inherit;text-decoration:none}.card-link:hover{border-color:var(--accent)}.card-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items: flex-start;gap:.5rem}.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));gap:.5rem 1rem;margin:.5rem 0 0}.facts dt{color:var(--muted);font-size:.75rem}.facts dd{overflow-wrap:anywhere;margin:0}.meta,.hint,.empty{color:var(--muted);margin:0;font-size:.85rem}.text{white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.badge{display:inline-block;border:1px solid var(--border);color:var(--muted);border-radius:999px;padding:0 .5rem;font-size:.75rem}.badge-busy{border-color:var(--busy);color:var(--busy)}.badge-warn{border-color:var(--warn);color:var(--warn)}.failure{border:1px solid var(--danger);border-radius:var(--radius);color:var(--danger);margin:0;padding:.75rem 1rem}.skeleton{border-radius:var(--radius);background:var(--border);opacity:.5;height:4rem}button,input,select,textarea{font:inherit;color:inherit}button{border:1px solid var(--accent);border-radius:var(--radius);background:var(--accent);color:var(--accent-text);cursor:pointer;padding:.35rem .9rem}button.secondary,.tabs button{border-color:var(--border);color:var(--text);background:0 0}.tabs button[aria-selected=true],.tabs button[aria-pressed=true]{border-color:var(--accent);color:var(--accent)}button.danger{border-color:var(--danger);color:var(--danger);background:0 0}button:disabled{opacity:.6;cursor:default}input,select,textarea{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);width:100%;padding:.4rem .6rem}label{display:flex;color:var(--muted);flex-direction:column;gap:.25rem;font-size:.85rem}label.inline{flex-direction:row;align-items: center}label.inline select{width:auto}fieldset{border:0;margin:0;padding:0}legend{color:var(--muted);margin-bottom:.25rem;font-size:.85rem}.toolbar{display:flex;flex-wrap:wrap;gap:.75rem}.toolbar input{flex:1;width:auto;min-width:12rem}.tabs{display:flex;gap:.25rem}.actions{display:flex;justify-content:flex-end;gap:.5rem}dialog{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--text);width:min(32rem,100vw - 2rem);padding:1rem}dialog::backdrop{background:#00000073}.back{font-size:.9rem}.transcript{display:flex;list-style:none;flex-direction:column;gap:.6rem;margin:0;padding:0}.entry{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);padding:.6rem .9rem}.entry-user{background:color-mix(in srgb,var(--accent)10%,var(--surface))}.entry-compaction{color:var(--muted);border-style:dashed}.entry-tool{background:0 0;padding:.35rem .9rem;font-size:.85rem}.entry-tool summary{cursor:pointer;color:var(--muted)}.entry-tool pre{overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;max-height:20rem;margin:.5rem 0 0}.entry-head{display:flex;justify-content:space-between;gap:.5rem;margin-bottom:.25rem}.role{text-transform:uppercase;letter-spacing:.03em;font-size:.8rem;font-weight:600}.call{display:block;color:var(--muted);overflow-wrap:anywhere;margin-top:.35rem;font-size:.8rem}
|