@huynle/hub 0.6.1 → 0.7.1
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 +800 -0
- package/Dockerfile +30 -0
- package/README.md +870 -66
- package/package.json +45 -2
- package/packages/claude-code/package.json +12 -0
- package/packages/claude-code/src/bridge.ts +79 -0
- package/packages/claude-code/src/daemon.ts +339 -0
- package/packages/claude-code/src/hook-input.ts +44 -0
- package/packages/claude-code/src/ids.ts +27 -0
- package/packages/claude-code/src/index.ts +18 -0
- package/packages/claude-code/src/launcher.ts +265 -0
- package/packages/claude-code/src/main.ts +127 -0
- package/packages/claude-code/src/mcp.ts +109 -0
- package/packages/claude-code/src/paths.ts +34 -0
- package/packages/claude-code/src/plugin.ts +99 -0
- package/packages/claude-code/src/references.ts +51 -0
- package/packages/claude-code/src/sessions.ts +600 -0
- package/packages/claude-code/src/tools.ts +66 -0
- package/packages/claude-code/src/wire.ts +71 -0
- package/packages/claude-code/src/wrapper.ts +155 -0
- package/packages/cli/dist/hub-cli.js +10162 -0
- package/packages/codex/package.json +12 -0
- package/packages/codex/src/app-server.ts +44 -0
- package/packages/codex/src/ids.ts +28 -0
- package/packages/codex/src/index.ts +11 -0
- package/packages/codex/src/jsonrpc.ts +168 -0
- package/packages/codex/src/main.ts +122 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +249 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +100 -0
- package/packages/node/package.json +12 -0
- package/packages/node/src/config.ts +184 -0
- package/packages/node/src/connection.ts +283 -0
- package/packages/node/src/daemon.ts +281 -0
- package/packages/node/src/git.ts +107 -0
- package/packages/node/src/harnesses.ts +60 -0
- package/packages/node/src/host.ts +319 -0
- package/packages/node/src/index.ts +17 -0
- package/packages/node/src/main.ts +11 -0
- package/packages/node/src/openshell.ts +73 -0
- package/packages/node/src/run.ts +106 -0
- package/packages/preset/package.json +12 -0
- package/packages/preset/presets/aider.json +8 -0
- package/packages/preset/presets/goose.json +8 -0
- package/packages/preset/src/index.ts +2 -0
- package/packages/preset/src/main.ts +195 -0
- package/packages/preset/src/presets.ts +87 -0
- package/packages/preset/src/spoke.ts +251 -0
- package/packages/preset/src/tmux.ts +160 -0
- package/packages/protocol/package.json +12 -0
- package/packages/protocol/src/harness-events.ts +207 -0
- package/packages/protocol/src/index.ts +1215 -0
- package/packages/protocol/src/launch.ts +172 -0
- package/packages/protocol/src/node.ts +409 -0
- package/packages/protocol/src/preset.ts +144 -0
- package/packages/protocol/src/webhook.ts +62 -0
- package/packages/protocol/src/wire.ts +54 -0
- package/packages/runtime-openshell/image/Dockerfile +36 -0
- package/packages/runtime-openshell/image/README.md +78 -0
- package/packages/runtime-openshell/image/build.ts +203 -0
- package/packages/runtime-openshell/image/entry.sh +163 -0
- package/packages/runtime-openshell/image/image.ts +119 -0
- package/packages/runtime-openshell/image/start.sh +24 -0
- package/packages/runtime-openshell/package.json +12 -0
- package/packages/runtime-openshell/src/cli.ts +584 -0
- package/packages/runtime-openshell/src/index.ts +6 -0
- package/packages/runtime-openshell/src/policy.ts +155 -0
- package/packages/runtime-openshell/src/profiles.ts +181 -0
- package/packages/runtime-openshell/src/runtime.ts +475 -0
- package/packages/runtime-sdk/package.json +12 -0
- package/packages/runtime-sdk/src/index.ts +87 -0
- package/packages/sdk/package.json +12 -0
- package/packages/sdk/src/address.ts +25 -0
- package/packages/sdk/src/client.ts +502 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +106 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +67 -0
- package/packages/sdk/src/types.ts +458 -0
- package/packages/sdk/src/webhook.ts +102 -0
- package/packages/spoke-sdk/package.json +12 -0
- package/packages/spoke-sdk/src/activity.ts +3 -0
- package/packages/spoke-sdk/src/connection.ts +295 -0
- package/packages/spoke-sdk/src/delivery.ts +119 -0
- package/packages/spoke-sdk/src/enroll.ts +79 -0
- package/packages/spoke-sdk/src/harness-hello.ts +79 -0
- package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
- package/packages/spoke-sdk/src/index.ts +12 -0
- package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
- package/packages/spoke-sdk/src/prompt-source.ts +8 -0
- package/packages/spoke-sdk/src/session-ids.ts +49 -0
- package/packages/spoke-sdk/src/session-tokens.ts +78 -0
- package/scripts/hub-claude-code.service +34 -0
- package/scripts/hub-codex-spoke.service +34 -0
- package/scripts/hub-node.service +35 -0
- package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
- package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
- package/scripts/io.github.huynle.hub-node.plist +52 -0
- package/src/central/actions.ts +233 -0
- package/src/central/agent/acting-routes.ts +133 -0
- package/src/central/agent/acting-switch.ts +93 -0
- package/src/central/agent/acting.ts +599 -0
- package/src/central/agent/actor.ts +193 -0
- package/src/central/agent/app-tokens.ts +142 -0
- package/src/central/agent/audit.ts +185 -0
- package/src/central/agent/decisions.ts +530 -0
- package/src/central/agent/delivery.ts +678 -0
- package/src/central/agent/directory-routes.ts +185 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +72 -0
- package/src/central/agent/escalation.ts +219 -0
- package/src/central/agent/grant-requests.ts +221 -0
- package/src/central/agent/grant-routes.ts +70 -0
- package/src/central/agent/grants.ts +235 -0
- package/src/central/agent/launch-engine.ts +1025 -0
- package/src/central/agent/launch-routes.ts +252 -0
- package/src/central/agent/launch-spec.ts +289 -0
- package/src/central/agent/launch-store.ts +264 -0
- package/src/central/agent/launch-tokens.ts +206 -0
- package/src/central/agent/message-limits.ts +123 -0
- package/src/central/agent/message-routes.ts +385 -0
- package/src/central/agent/messages.ts +442 -0
- package/src/central/agent/names.ts +86 -0
- package/src/central/agent/placement.ts +419 -0
- package/src/central/agent/policy.ts +378 -0
- package/src/central/agent/read-routes.ts +389 -0
- package/src/central/agent/routes.ts +278 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +221 -0
- package/src/central/agent/webhook-routes.ts +78 -0
- package/src/central/agent/webhooks.ts +449 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +224 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +113 -0
- package/src/central/backend/memory-bus.ts +93 -0
- package/src/central/backend/memory-catalog.ts +90 -0
- package/src/central/backend/memory-counters.ts +102 -0
- package/src/central/backend/memory-events.ts +45 -0
- package/src/central/backend/memory-kv.ts +38 -0
- package/src/central/backend/memory-state.ts +77 -0
- package/src/central/backend/postgres-migrate.ts +100 -0
- package/src/central/backend/postgres-state.ts +197 -0
- package/src/central/backend/types.ts +244 -0
- package/src/central/backend/valkey-bus.ts +425 -0
- package/src/central/backend/valkey-catalog.ts +190 -0
- package/src/central/backend/valkey-client.ts +36 -0
- package/src/central/backend/valkey-counters.ts +38 -0
- package/src/central/backend/valkey-events.ts +168 -0
- package/src/central/backend/valkey-kv.ts +54 -0
- package/src/central/backend/valkey-locks.ts +92 -0
- package/src/central/cluster/directory.ts +141 -0
- package/src/central/cluster/feed.ts +84 -0
- package/src/central/cluster/leader.ts +232 -0
- package/src/central/cluster/live.ts +151 -0
- package/src/central/cluster/recommend.ts +59 -0
- package/src/central/cluster/rpc.ts +150 -0
- package/src/central/cluster/view.ts +232 -0
- package/src/central/config.ts +197 -3
- package/src/central/done.ts +96 -0
- package/src/central/events-log.ts +100 -0
- package/src/central/http.ts +1067 -171
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- package/src/central/nodes.ts +314 -0
- package/src/central/oidc.ts +34 -14
- package/src/central/push-target.ts +118 -18
- package/src/central/push.ts +256 -82
- package/src/central/registry.ts +902 -92
- package/src/central/say-stream.ts +128 -0
- package/src/central/session-token-push.ts +233 -0
- package/src/central/speech/openai.ts +21 -14
- package/src/central/speech/openrouter.ts +27 -10
- package/src/central/speech/provider.ts +54 -0
- package/src/central/speech/service.ts +22 -7
- package/src/central/spoke-ws.ts +522 -44
- package/src/central/store-shared.ts +518 -0
- package/src/central/store.ts +572 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +49 -1
- package/src/index.ts +165 -12
- package/src/protocol.ts +3 -650
- package/src/rpc.ts +27 -2
- package/src/spoke/client.ts +3 -272
- package/src/spoke/conversation.ts +16 -12
- package/src/spoke/deliver.ts +75 -0
- package/src/spoke/enroll.ts +2 -79
- package/src/spoke/env-inject.ts +160 -0
- package/src/spoke/hub-rpc.ts +115 -0
- package/src/spoke/hub-tool.ts +409 -0
- package/src/spoke/launch-source.ts +22 -0
- package/src/spoke/launchers.ts +68 -0
- package/src/spoke/locations.ts +3 -2
- package/src/spoke/node-roots.ts +61 -0
- package/src/spoke/prompt-refs.ts +116 -0
- package/src/spoke/session-tokens.ts +2 -0
- package/src/spoke/spoke.ts +465 -28
- package/src/spoke/taskboard.ts +243 -0
- package/src/spoke/timeline.ts +184 -6
- package/src/spoke/tracker.ts +79 -4
- package/src/status.ts +203 -10
- package/src/tui-commands.ts +13 -6
- package/src/tui-messaging.ts +93 -0
- package/src/tui.ts +15 -0
- package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-D9zMbxSg.css +1 -0
- package/web/dist/assets/index-db1z4duA.js +20 -0
- package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
- package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
- package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
- package/web/dist/index.html +9 -2
- package/web/dist/sw.js +79 -15
- package/web/dist/assets/index-DClBvfLL.css +0 -1
- package/web/dist/assets/index-Djo_yF0a.js +0 -12
package/src/status.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Live status of a voice session and the subagent sessions it spawns, built from OpenCode events.
|
|
2
|
+
import type { JobInfo } from "./protocol"
|
|
2
3
|
|
|
3
4
|
export type SessionState = "idle" | "working" | "blocked" | "error"
|
|
4
5
|
|
|
@@ -6,10 +7,26 @@ export interface Blocker {
|
|
|
6
7
|
readonly kind: "permission" | "form"
|
|
7
8
|
readonly id: string
|
|
8
9
|
readonly summary: string
|
|
10
|
+
/** permission: what OpenCode asks to do ("shell", "edit", "read", an MCP tool), when it said. */
|
|
11
|
+
readonly action?: string
|
|
12
|
+
/** permission: what it applies to (commands, paths), at most BLOCKER_MAX_RESOURCES, each shortened. */
|
|
13
|
+
readonly resources?: readonly string[]
|
|
9
14
|
/** form: asked by the agent's question tool (metadata.kind "question"). */
|
|
10
15
|
readonly question?: true
|
|
11
16
|
/** form: its fields, bounded, so a phone can render and answer it. */
|
|
12
17
|
readonly fields?: readonly FormFieldSpec[]
|
|
18
|
+
/** permission: an agent escalated it (outside its grant) and recommends this (the hub adds it, P3-T8). */
|
|
19
|
+
readonly recommendation?: BlockerRecommendation
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** "@orchestrator recommends Approve: only runs tests in the worktree". */
|
|
23
|
+
export interface BlockerRecommendation {
|
|
24
|
+
/** Who recommends it, as the hub names the agent. */
|
|
25
|
+
readonly by: string
|
|
26
|
+
readonly recommends: "approve"
|
|
27
|
+
readonly why?: string
|
|
28
|
+
/** The Decision the user answers it in. */
|
|
29
|
+
readonly decisionID?: string
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
export interface FormOptionSpec {
|
|
@@ -53,6 +70,75 @@ const FIELD_TYPES = new Set<FormFieldSpec["type"]>(["string", "number", "integer
|
|
|
53
70
|
const text = (v: unknown, max: number) => (typeof v === "string" && v ? v.slice(0, max) : undefined)
|
|
54
71
|
const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined)
|
|
55
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What a permission request asks, as OpenCode sent it (permission.asked), kept so a policy can judge it.
|
|
75
|
+
* metadata (diff previews and the like) is dropped. When anything had to be cut or dropped, `truncated` is
|
|
76
|
+
* set: a judgement on what is left does not cover the whole request.
|
|
77
|
+
*/
|
|
78
|
+
export interface PermissionRequest {
|
|
79
|
+
/** "shell", "read", "edit", "external_directory", an MCP tool ("<server>_<tool>"), … */
|
|
80
|
+
readonly action: string
|
|
81
|
+
/** What the action applies to (one per parsed shell command, paths, "*"). */
|
|
82
|
+
readonly resources: readonly string[]
|
|
83
|
+
/** The patterns an "always" reply would save as rules. */
|
|
84
|
+
readonly save?: readonly string[]
|
|
85
|
+
/** The tool call that asked. */
|
|
86
|
+
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
|
87
|
+
readonly truncated?: true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Resources (and save patterns) kept per request, and the length kept of each. */
|
|
91
|
+
export const PERMISSION_MAX_RESOURCES = 100
|
|
92
|
+
export const PERMISSION_TEXT_MAX = 4000
|
|
93
|
+
/** Longest action kept; a longer one makes the request unusable (undefined). */
|
|
94
|
+
const PERMISSION_ACTION_MAX = 200
|
|
95
|
+
const SOURCE_ID_MAX = 200
|
|
96
|
+
/** Resources shown on a blocker (catalog and status go to every console), and the length of each. */
|
|
97
|
+
export const BLOCKER_MAX_RESOURCES = 10
|
|
98
|
+
const BLOCKER_RESOURCE_MAX = 500
|
|
99
|
+
|
|
100
|
+
/** Strings of `list`, bounded; `cut` is set when anything was dropped or shortened. */
|
|
101
|
+
function boundedStrings(list: unknown, maxItems: number, maxLength: number): { items: string[]; cut: boolean } {
|
|
102
|
+
if (!Array.isArray(list)) return { items: [], cut: list !== undefined }
|
|
103
|
+
const strings = list.filter((v): v is string => typeof v === "string")
|
|
104
|
+
const items = strings.slice(0, maxItems).map((v) => v.slice(0, maxLength))
|
|
105
|
+
const cut = strings.length !== list.length || strings.length > maxItems || strings.some((v) => v.length > maxLength)
|
|
106
|
+
return { items, cut }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A permission.asked event's request, bounded; undefined without a usable action. */
|
|
110
|
+
export function permissionRequest(raw: any): PermissionRequest | undefined {
|
|
111
|
+
const action = raw?.action
|
|
112
|
+
if (typeof action !== "string" || !action || action.length > PERMISSION_ACTION_MAX) return undefined
|
|
113
|
+
const resources = boundedStrings(raw.resources, PERMISSION_MAX_RESOURCES, PERMISSION_TEXT_MAX)
|
|
114
|
+
const save = raw.save === undefined ? undefined : boundedStrings(raw.save, PERMISSION_MAX_RESOURCES, PERMISSION_TEXT_MAX)
|
|
115
|
+
const src = raw.source
|
|
116
|
+
const source =
|
|
117
|
+
src?.type === "tool" && typeof src.messageID === "string" && typeof src.id === "string"
|
|
118
|
+
? { type: "tool" as const, messageID: src.messageID.slice(0, SOURCE_ID_MAX), id: src.id.slice(0, SOURCE_ID_MAX) }
|
|
119
|
+
: undefined
|
|
120
|
+
return {
|
|
121
|
+
action,
|
|
122
|
+
resources: resources.items,
|
|
123
|
+
...(save ? { save: save.items } : {}),
|
|
124
|
+
...(source ? { source } : {}),
|
|
125
|
+
...(resources.cut || save?.cut ? { truncated: true as const } : {}),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A permission.asked event as a blocker: its summary, and what it asks (bounded for display). */
|
|
130
|
+
function permissionBlocker(data: Record<string, any>): Blocker {
|
|
131
|
+
const action = typeof data.action === "string" && data.action ? data.action.slice(0, PERMISSION_ACTION_MAX) : undefined
|
|
132
|
+
const resources = boundedStrings(data.resources, BLOCKER_MAX_RESOURCES, BLOCKER_RESOURCE_MAX).items
|
|
133
|
+
return {
|
|
134
|
+
kind: "permission",
|
|
135
|
+
id: data.id,
|
|
136
|
+
summary: [data.action, (data.resources ?? []).join(", ")].filter(Boolean).join(": "),
|
|
137
|
+
...(action ? { action } : {}),
|
|
138
|
+
...(resources.length ? { resources } : {}),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
56
142
|
function optionSpec(raw: any): FormOptionSpec | undefined {
|
|
57
143
|
const value = text(raw?.value, FORM_LABEL_MAX)
|
|
58
144
|
if (value === undefined) return undefined
|
|
@@ -112,15 +198,28 @@ export interface SessionStatus {
|
|
|
112
198
|
readonly state: SessionState
|
|
113
199
|
readonly detail?: string
|
|
114
200
|
readonly blockers: ReadonlyArray<Blocker>
|
|
201
|
+
/** OpenCode is retrying the model request (state stays "working"). */
|
|
202
|
+
readonly retry?: { readonly attempt?: number; readonly message?: string }
|
|
203
|
+
/** Shells and terminals of the session: running ones, then the last few that ended (oldest first). */
|
|
204
|
+
readonly jobs?: readonly JobInfo[]
|
|
115
205
|
}
|
|
116
206
|
|
|
207
|
+
/** Ended jobs kept per session (shown briefly with their exit code). */
|
|
208
|
+
export const ENDED_JOBS_KEPT = 5
|
|
209
|
+
/** Jobs kept per session at most (running ones beyond it are dropped oldest first). */
|
|
210
|
+
const JOBS_MAX = 50
|
|
211
|
+
|
|
117
212
|
interface Entry {
|
|
118
213
|
id: string
|
|
119
214
|
parentID?: string
|
|
120
215
|
title: string
|
|
121
216
|
running: boolean
|
|
217
|
+
/** session.status: busy or retry (OpenCode's own view of the session). */
|
|
218
|
+
busy: boolean
|
|
219
|
+
retry?: { attempt?: number; message?: string }
|
|
122
220
|
error?: string
|
|
123
221
|
blockers: Blocker[]
|
|
222
|
+
jobs: JobInfo[]
|
|
124
223
|
}
|
|
125
224
|
|
|
126
225
|
interface EventLike {
|
|
@@ -132,7 +231,7 @@ export class StatusBoard {
|
|
|
132
231
|
private readonly sessions = new Map<string, Entry>()
|
|
133
232
|
|
|
134
233
|
constructor(rootID: string, title: string) {
|
|
135
|
-
this.sessions.set(rootID, { id: rootID, title, running: false, blockers: [] })
|
|
234
|
+
this.sessions.set(rootID, { id: rootID, title, running: false, busy: false, blockers: [], jobs: [] })
|
|
136
235
|
}
|
|
137
236
|
|
|
138
237
|
/** Applies an event. Returns true when the visible status changed. */
|
|
@@ -146,7 +245,9 @@ export class StatusBoard {
|
|
|
146
245
|
parentID: data.parentID,
|
|
147
246
|
title: data.title ?? data.agent ?? data.slug ?? "subagent",
|
|
148
247
|
running: false,
|
|
248
|
+
busy: false,
|
|
149
249
|
blockers: [],
|
|
250
|
+
jobs: [],
|
|
150
251
|
})
|
|
151
252
|
return true
|
|
152
253
|
}
|
|
@@ -161,20 +262,16 @@ export class StatusBoard {
|
|
|
161
262
|
})
|
|
162
263
|
case "session.execution.succeeded":
|
|
163
264
|
case "session.execution.interrupted":
|
|
164
|
-
return this.update(data.sessionID, (entry) => (entry
|
|
265
|
+
return this.update(data.sessionID, (entry) => endRun(entry))
|
|
165
266
|
case "session.execution.failed":
|
|
166
267
|
return this.update(data.sessionID, (entry) => {
|
|
167
|
-
entry
|
|
268
|
+
endRun(entry)
|
|
168
269
|
entry.error = String(data.error?.message ?? "failed")
|
|
169
270
|
})
|
|
170
271
|
case "permission.asked":
|
|
171
272
|
return this.update(data.sessionID, (entry) => {
|
|
172
273
|
if (entry.blockers.some((b) => b.id === data.id)) return
|
|
173
|
-
entry.blockers.push(
|
|
174
|
-
kind: "permission",
|
|
175
|
-
id: data.id,
|
|
176
|
-
summary: [data.action, (data.resources ?? []).join(", ")].filter(Boolean).join(": "),
|
|
177
|
-
})
|
|
274
|
+
entry.blockers.push(permissionBlocker(data))
|
|
178
275
|
})
|
|
179
276
|
case "permission.replied":
|
|
180
277
|
return this.unblock(data.sessionID, data.requestID)
|
|
@@ -186,6 +283,30 @@ export class StatusBoard {
|
|
|
186
283
|
case "form.replied":
|
|
187
284
|
case "form.cancelled":
|
|
188
285
|
return this.unblock(data.sessionID, data.id)
|
|
286
|
+
case "session.status":
|
|
287
|
+
return this.update(data.sessionID, (entry) => setStatus(entry, data.status))
|
|
288
|
+
case "session.seed":
|
|
289
|
+
return this.update(data.sessionID, (entry) => {
|
|
290
|
+
setStatus(entry, data.status)
|
|
291
|
+
entry.jobs = entry.jobs.filter((job) => job.status !== "running")
|
|
292
|
+
for (const job of Array.isArray(data.jobs) ? data.jobs : []) if (isJob(job)) upsertJob(entry, job)
|
|
293
|
+
})
|
|
294
|
+
case "shell.created":
|
|
295
|
+
case "session.shell.started":
|
|
296
|
+
case "pty.created":
|
|
297
|
+
case "pty.updated":
|
|
298
|
+
case "shell.exited":
|
|
299
|
+
case "session.shell.ended":
|
|
300
|
+
case "pty.exited":
|
|
301
|
+
return this.update(data.sessionID, (entry) => {
|
|
302
|
+
const job = jobOf(data, entry.jobs.find((j) => j.id === data.id))
|
|
303
|
+
if (job) upsertJob(entry, job)
|
|
304
|
+
})
|
|
305
|
+
case "shell.deleted":
|
|
306
|
+
case "pty.deleted":
|
|
307
|
+
return this.update(data.sessionID, (entry) => {
|
|
308
|
+
entry.jobs = entry.jobs.filter((job) => job.id !== data.id)
|
|
309
|
+
})
|
|
189
310
|
default:
|
|
190
311
|
return false
|
|
191
312
|
}
|
|
@@ -200,6 +321,18 @@ export class StatusBoard {
|
|
|
200
321
|
return [...this.sessions.keys()]
|
|
201
322
|
}
|
|
202
323
|
|
|
324
|
+
/**
|
|
325
|
+
* True when nothing in the tree runs (no run, busy or retrying status), waits for an answer, or has a running
|
|
326
|
+
* shell job. Terminals (pty) do not count: an open terminal is not work in progress.
|
|
327
|
+
*/
|
|
328
|
+
settled(): boolean {
|
|
329
|
+
for (const entry of this.sessions.values()) {
|
|
330
|
+
if (entry.running || entry.busy || entry.blockers.length) return false
|
|
331
|
+
if (entry.jobs.some((job) => job.status === "running" && job.kind === "shell")) return false
|
|
332
|
+
}
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
|
|
203
336
|
snapshot(): { sessions: SessionStatus[] } {
|
|
204
337
|
return { sessions: [...this.sessions.values()].map(view) }
|
|
205
338
|
}
|
|
@@ -218,15 +351,75 @@ export class StatusBoard {
|
|
|
218
351
|
}
|
|
219
352
|
}
|
|
220
353
|
|
|
354
|
+
const JOB_STATUSES = new Set(["running", "exited", "timeout", "killed"])
|
|
355
|
+
|
|
356
|
+
function isJob(v: any): v is JobInfo {
|
|
357
|
+
return typeof v?.id === "string" && v.id && (v.kind === "shell" || v.kind === "pty") && JOB_STATUSES.has(v.status)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** A job event's data (compact, see compactActivity) merged over what is known of the job. */
|
|
361
|
+
function jobOf(data: Record<string, any>, known: JobInfo | undefined): JobInfo | undefined {
|
|
362
|
+
if (typeof data.id !== "string" || !JOB_STATUSES.has(data.status)) return undefined
|
|
363
|
+
const kind = data.kind === "pty" ? "pty" : "shell"
|
|
364
|
+
const ended = data.status !== "running"
|
|
365
|
+
const at = num(data.at)
|
|
366
|
+
const command = text(data.command, 200) ?? known?.command
|
|
367
|
+
const exit = num(data.exit) ?? (ended ? known?.exit : undefined)
|
|
368
|
+
const started = known?.started ?? (!ended ? at : undefined)
|
|
369
|
+
return {
|
|
370
|
+
id: data.id,
|
|
371
|
+
kind,
|
|
372
|
+
...(command ? { command } : {}),
|
|
373
|
+
status: data.status,
|
|
374
|
+
...(exit !== undefined ? { exit } : {}),
|
|
375
|
+
...(data.background === true || known?.background ? { background: true as const } : {}),
|
|
376
|
+
...(started !== undefined ? { started } : {}),
|
|
377
|
+
...(ended && at !== undefined ? { ended: at } : {}),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function upsertJob(entry: Entry, job: JobInfo): void {
|
|
382
|
+
const rest = entry.jobs.filter((j) => j.id !== job.id)
|
|
383
|
+
rest.push({ ...job })
|
|
384
|
+
const running = rest.filter((j) => j.status === "running")
|
|
385
|
+
const endedJobs = rest.filter((j) => j.status !== "running").slice(-ENDED_JOBS_KEPT)
|
|
386
|
+
const keep = new Set([...running.slice(-JOBS_MAX), ...endedJobs])
|
|
387
|
+
entry.jobs = rest.filter((j) => keep.has(j))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** A run ended: OpenCode v2 sends no idle status, so a busy or retrying status (e.g. seeded) ends with it. */
|
|
391
|
+
function endRun(entry: Entry): void {
|
|
392
|
+
entry.running = false
|
|
393
|
+
entry.busy = false
|
|
394
|
+
entry.retry = undefined
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function setStatus(entry: Entry, status: any): void {
|
|
398
|
+
const type = status?.type
|
|
399
|
+
if (type !== "idle" && type !== "busy" && type !== "retry") return
|
|
400
|
+
entry.busy = type !== "idle"
|
|
401
|
+
if (type === "retry") {
|
|
402
|
+
const attempt = num(status.attempt)
|
|
403
|
+
const message = text(status.message, 200)
|
|
404
|
+
entry.retry = { ...(attempt !== undefined ? { attempt } : {}), ...(message ? { message } : {}) }
|
|
405
|
+
} else entry.retry = undefined
|
|
406
|
+
if (type === "busy") entry.error = undefined
|
|
407
|
+
}
|
|
408
|
+
|
|
221
409
|
function view(entry: Entry): SessionStatus {
|
|
410
|
+
const working = entry.running || entry.busy || entry.jobs.some((job) => job.status === "running")
|
|
222
411
|
const state: SessionState =
|
|
223
|
-
entry.blockers.length > 0 ? "blocked" : entry.error ? "error" :
|
|
412
|
+
entry.blockers.length > 0 ? "blocked" : entry.error && !working ? "error" : working ? "working" : "idle"
|
|
413
|
+
const retry = entry.retry && state === "working" ? entry.retry : undefined
|
|
414
|
+
const detail = state === "error" ? entry.error : retry ? (retry.message ? `Retrying: ${retry.message}` : "Retrying") : undefined
|
|
224
415
|
return {
|
|
225
416
|
id: entry.id,
|
|
226
417
|
...(entry.parentID ? { parentID: entry.parentID } : {}),
|
|
227
418
|
title: entry.title,
|
|
228
419
|
state,
|
|
229
|
-
...(
|
|
420
|
+
...(detail ? { detail } : {}),
|
|
230
421
|
blockers: entry.blockers.map((blocker) => structuredClone(blocker)),
|
|
422
|
+
...(retry ? { retry: { ...retry } } : {}),
|
|
423
|
+
...(entry.jobs.length ? { jobs: entry.jobs.map((job) => ({ ...job })) } : {}),
|
|
231
424
|
}
|
|
232
425
|
}
|
package/src/tui-commands.ts
CHANGED
|
@@ -14,7 +14,13 @@ export interface HubCommand {
|
|
|
14
14
|
readonly slash: string
|
|
15
15
|
/** Shown in the command palette after "Hub: ". */
|
|
16
16
|
readonly title: string
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The command takes arguments: `/<slash> text` runs it with the raw text after the name (quotes and inner
|
|
19
|
+
* spacing kept). Without it, any text after the name sends the whole line to the model instead.
|
|
20
|
+
*/
|
|
21
|
+
readonly arguments?: boolean
|
|
22
|
+
/** `input`: the raw arguments; undefined (palette, key binding) and "" (no text) both mean none. */
|
|
23
|
+
readonly run: (input?: string) => Promise<unknown>
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
/** The shape OpenCode's keymap layer takes for one command. */
|
|
@@ -23,8 +29,8 @@ export interface KeymapCommand {
|
|
|
23
29
|
readonly title: string
|
|
24
30
|
readonly group: string
|
|
25
31
|
readonly palette: true
|
|
26
|
-
readonly slash: { readonly name: string }
|
|
27
|
-
readonly run: () => void
|
|
32
|
+
readonly slash: { readonly name: string; readonly arguments?: true }
|
|
33
|
+
readonly run: (input?: string) => void
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -44,9 +50,10 @@ export function hubCommands(features: readonly HubCommand[], onError: (cause: un
|
|
|
44
50
|
title: `${COMMAND_GROUP}: ${feature.title}`,
|
|
45
51
|
group: COMMAND_GROUP,
|
|
46
52
|
palette: true,
|
|
47
|
-
slash: { name: feature.slash },
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
slash: feature.arguments ? { name: feature.slash, arguments: true } : { name: feature.slash },
|
|
54
|
+
// Keyboard dispatch may pass something other than text; only a string is the command's input.
|
|
55
|
+
run: (input?: unknown) => {
|
|
56
|
+
void feature.run(typeof input === "string" ? input : undefined).catch(onError)
|
|
50
57
|
},
|
|
51
58
|
}
|
|
52
59
|
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// /who and /msg in the TUI: what the user typed after the slash name, and what the TUI shows back. The server side
|
|
2
|
+
// (src/spoke/hub-rpc.ts) talks to the hub as the TUI's current session; labels arrive sanitised from there.
|
|
3
|
+
import type { WhoRow } from "./spoke/hub-rpc"
|
|
4
|
+
|
|
5
|
+
/** One session in /who, as the server's hubWho RPC returns it (type only: the TUI never loads the SDK). */
|
|
6
|
+
export type { WhoRow }
|
|
7
|
+
|
|
8
|
+
export const MSG_USAGE = "Usage: /msg @name text (or @@role, @name.project, ses_…, @me)"
|
|
9
|
+
|
|
10
|
+
/** @name, @@role, @name.project, @me, or a session ID. */
|
|
11
|
+
const ADDRESS = /^(@@?[^\s@]\S*|ses_\S+)$/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `/msg` arguments: the first word is the address, everything after the whitespace that follows it is the text,
|
|
15
|
+
* word for word (spacing, quotes and new lines kept). Undefined when there are no arguments at all.
|
|
16
|
+
*/
|
|
17
|
+
export function parseMsgInput(input?: string): { to: string; text: string } | { error: string } | undefined {
|
|
18
|
+
if (input === undefined || !input.trim()) return undefined
|
|
19
|
+
const match = /^\s*(\S+)(?:\s+([\s\S]*))?$/.exec(input)
|
|
20
|
+
const to = match?.[1] ?? ""
|
|
21
|
+
const text = match?.[2] ?? ""
|
|
22
|
+
if (!ADDRESS.test(to)) return { error: `Start with the address. ${MSG_USAGE}` }
|
|
23
|
+
if (!text.trim()) return { error: `What should the message say? ${MSG_USAGE}` }
|
|
24
|
+
return { to, text }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `/who` arguments: nothing (everyone), a project, or a @@role. */
|
|
28
|
+
export function parseWhoInput(input?: string): { project?: string; role?: string } {
|
|
29
|
+
const value = input?.trim() ?? ""
|
|
30
|
+
if (!value) return {}
|
|
31
|
+
if (value.startsWith("@@") && value.length > 2) return { role: value.slice(2) }
|
|
32
|
+
return { project: value }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One line per session: name (else ID) · project · state · online/offline · roles. */
|
|
36
|
+
export function formatWho(rows: readonly WhoRow[]): string {
|
|
37
|
+
if (!rows.length) return "No named or role-holding sessions."
|
|
38
|
+
return rows
|
|
39
|
+
.map((s) =>
|
|
40
|
+
[s.name ? `@${s.name}` : s.sessionID, s.project ?? "", s.state ?? "", s.online ? "online" : "offline", s.roles.map((r) => `@@${r}`).join(" ")]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join(" · "),
|
|
43
|
+
)
|
|
44
|
+
.join("\n")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The toast after /msg: who it reached and how ("stored" when the hub only stored it). */
|
|
48
|
+
export function formatSent(sent: { id: string; to: string; delivery?: string }): string {
|
|
49
|
+
return `Sent to ${sent.to} (${sent.delivery ?? "stored"})`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What /who and /msg need from the TUI (src/tui.ts): the current session, the RPC calls and the UI. */
|
|
53
|
+
export interface MessagingDeps {
|
|
54
|
+
/** The session the TUI shows; undefined on the home screen. */
|
|
55
|
+
sessionID(): string | undefined
|
|
56
|
+
who(input: { sessionID: string; project?: string; role?: string }): Promise<{ sessions: WhoRow[] }>
|
|
57
|
+
msg(input: { sessionID: string; to: string; text: string }): Promise<{ id: string; to: string; delivery?: string }>
|
|
58
|
+
prompt(options: { title: string; description?: string; placeholder?: string; value?: string }): Promise<string | null | undefined>
|
|
59
|
+
alert(options: { title: string; message: string }): Promise<unknown>
|
|
60
|
+
toast(message: string, variant?: "info" | "success" | "warning" | "error"): unknown
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const NO_SESSION = (what: string) => `Open a session first: ${what} acts as the current session.`
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* /msg: sends as the current session. Without arguments (palette, bare /msg) it asks for "@name text"; RPC failures
|
|
67
|
+
* are thrown (the command registry shows them as a toast).
|
|
68
|
+
*/
|
|
69
|
+
export async function runMsg(deps: MessagingDeps, input?: string): Promise<void> {
|
|
70
|
+
const sessionID = deps.sessionID()
|
|
71
|
+
if (!sessionID) return void deps.toast(NO_SESSION("/msg"), "warning")
|
|
72
|
+
let parsed = parseMsgInput(input)
|
|
73
|
+
if (!parsed) {
|
|
74
|
+
const typed = await deps.prompt({ title: "Message a session", description: `Sent as this session. ${MSG_USAGE}`, placeholder: "@name text" })
|
|
75
|
+
if (typeof typed !== "string") return
|
|
76
|
+
parsed = parseMsgInput(typed)
|
|
77
|
+
if (!parsed) return
|
|
78
|
+
}
|
|
79
|
+
if ("error" in parsed) return void deps.toast(parsed.error, "error")
|
|
80
|
+
const sent = await deps.msg({ sessionID, to: parsed.to, text: parsed.text })
|
|
81
|
+
deps.toast(formatSent(sent), "success")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** /who [project | @@role]: the hub's named and role-holding sessions in a dialog, asked as the current session. */
|
|
85
|
+
export async function runWho(deps: MessagingDeps, input?: string): Promise<void> {
|
|
86
|
+
const sessionID = deps.sessionID()
|
|
87
|
+
if (!sessionID) return void deps.toast(NO_SESSION("/who"), "warning")
|
|
88
|
+
const filter = parseWhoInput(input)
|
|
89
|
+
const { sessions } = await deps.who({ sessionID, ...filter })
|
|
90
|
+
if (!sessions.length) return void deps.toast(formatWho(sessions), "info")
|
|
91
|
+
const scope = filter.role ? ` · @@${filter.role}` : filter.project ? ` · ${filter.project}` : ""
|
|
92
|
+
await deps.alert({ title: `Hub · who${scope}`, message: formatWho(sessions) })
|
|
93
|
+
}
|
package/src/tui.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { HubRpc } from "./rpc"
|
|
|
5
5
|
import { localServerFor, modeFor, needsSetup, parseServerInput, setupOptions, type SetupChoice } from "./setup"
|
|
6
6
|
import { hubCommands } from "./tui-commands"
|
|
7
7
|
import { FormRelay } from "./tui-forms"
|
|
8
|
+
import { runMsg, runWho, type MessagingDeps } from "./tui-messaging"
|
|
8
9
|
import { PresenceReporter, shownSessions } from "./tui-presence"
|
|
9
10
|
import { startVoice } from "./voice-command"
|
|
10
11
|
|
|
@@ -15,6 +16,8 @@ import { startVoice } from "./voice-command"
|
|
|
15
16
|
// /voice: no menu — connect (setting up first when needed), share this session, and open it in the browser
|
|
16
17
|
// with Voice mode starting (src/voice-command.ts). Links are shown here, never posted into the session, so
|
|
17
18
|
// they do not reach the model. Decisions live in src/setup.ts and src/voice-command.ts (tested).
|
|
19
|
+
// /who [project | @@role] and /msg @name text: other sessions on the hub, as the current session
|
|
20
|
+
// (src/tui-messaging.ts; server side src/spoke/hub-rpc.ts).
|
|
18
21
|
|
|
19
22
|
type Mode = "off" | "always" | "session"
|
|
20
23
|
interface Status {
|
|
@@ -408,6 +411,16 @@ export default Plugin.define({
|
|
|
408
411
|
toast,
|
|
409
412
|
})
|
|
410
413
|
|
|
414
|
+
// /who and /msg: the server sends as this TUI's current session, with that session's own hub token.
|
|
415
|
+
const messaging: MessagingDeps = {
|
|
416
|
+
sessionID: currentSession,
|
|
417
|
+
who: (input) => call("hubWho", input),
|
|
418
|
+
msg: (input) => call("hubMsg", input),
|
|
419
|
+
prompt: (options) => context.ui.dialog.prompt(options),
|
|
420
|
+
alert: (options) => context.ui.dialog.alert(options),
|
|
421
|
+
toast: (message, variant) => toast(message, variant),
|
|
422
|
+
}
|
|
423
|
+
|
|
411
424
|
// Presence: tell the server-side spoke which sessions this TUI shows, so the phone lists them as open.
|
|
412
425
|
const reporter = `tui_${Math.random().toString(36).slice(2, 12)}`
|
|
413
426
|
const presence = new PresenceReporter({
|
|
@@ -436,6 +449,8 @@ export default Plugin.define({
|
|
|
436
449
|
[
|
|
437
450
|
{ id: "open", slash: "hub", title: "server, connection, sharing and phones", run: open },
|
|
438
451
|
{ id: "voice", slash: "voice", title: "talk to this session (Voice mode in the browser)", run: voice },
|
|
452
|
+
{ id: "who", slash: "who", title: "who is on the hub (names and roles)", arguments: true, run: (input) => runWho(messaging, input) },
|
|
453
|
+
{ id: "msg", slash: "msg", title: "message a session as this session (/msg @name text)", arguments: true, run: (input) => runMsg(messaging, input) },
|
|
439
454
|
],
|
|
440
455
|
(cause) => toast(errorMessage(cause), "error"),
|
|
441
456
|
)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var e={ts:`ts`,typescript:`ts`,tsx:`ts`,mts:`ts`,cts:`ts`,js:`ts`,javascript:`ts`,jsx:`ts`,mjs:`ts`,cjs:`ts`,node:`ts`,json:`json`,jsonc:`json`,json5:`json`,bash:`bash`,sh:`bash`,shell:`bash`,zsh:`bash`,console:`bash`,shellsession:`bash`,fish:`bash`,python:`python`,py:`python`,python3:`python`,go:`go`,golang:`go`,rust:`rust`,rs:`rust`,yaml:`yaml`,yml:`yaml`,diff:`diff`,patch:`diff`,sql:`sql`,postgresql:`sql`,postgres:`sql`,mysql:`sql`,sqlite:`sql`,css:`css`,scss:`css`,less:`css`,html:`html`,htm:`html`,xml:`html`,svg:`html`,vue:`html`,md:`md`,markdown:`md`};function t(t){return e[String(t??``).trim().toLowerCase()]}var n=e=>new Set(e.split(` `)),r=e=>e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`||e===`_`||e===`$`,i=e=>e!==void 0&&(r(e)||e>=`0`&&e<=`9`);function a(e,t){let n=t+1;for(;i(e[n]);)n++;return e.slice(t,n)}var o=/(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)[a-zA-Z]{0,3}\b/y,s=/"(?:[^"\\\n]|\\.)*"?/y,c=/'(?:[^'\\\n]|\\.)*'?/y,l=/\/\*[\s\S]*?(?:\*\/|$)/y,u=/\/\/[^\n]*/y,d=/#[^\n]*/y,f={ts:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/`(?:[^`\\]|\\[\s\S])*`?/y,first:"`"},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`abstract as async await break case catch class const continue debugger declare default delete do else enum export extends false finally for from function get if implements import in instanceof interface keyof let new null of private protected public readonly return satisfies set static super switch this throw true try type typeof undefined var void while with yield`),types:n(`string number boolean bigint symbol object unknown never any void Promise Array Record Map Set Partial Readonly`)},json:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`prop`,re:/"(?:[^"\\\n]|\\.)*"(?=\s*:)/y,first:`"`},{kind:`string`,re:s,first:`"`},{kind:`number`,re:/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y,first:`-0123456789`}],keywords:n(`true false null`)},bash:{rules:[{kind:`comment`,re:d,at:`word`,first:`#`},{kind:`string`,re:/"(?:[^"\\]|\\[\s\S])*"?/y,first:`"`},{kind:`string`,re:/'[^']*'?/y,first:`'`},{kind:`prop`,re:/\$(?:\{[^}\n]*\}?|[\w@#?$!*-]+)/y,first:`$`},{kind:`fn`,re:/[\w./-]+/y,at:`command`},{kind:`attr`,re:/--?[\w-]+/y,at:`word`,first:`-`}],keywords:n(`if then else elif fi for do done case esac while until function in export local return select time sudo`)},python:{rules:[{kind:`comment`,re:d,first:`#`},{kind:`string`,re:/[rRbBfFuU]{0,2}(?:"""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$))/y,first:`rRbBfFuU"'`},{kind:`string`,re:/[rRbBfFuU]{0,2}"(?:[^"\\\n]|\\.)*"?/y,first:`rRbBfFuU"'`},{kind:`string`,re:/[rRbBfFuU]{0,2}'(?:[^'\\\n]|\\.)*'?/y,first:`rRbBfFuU"'`},{kind:`meta`,re:/@[\w.]+/y,first:`@`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return self True try while with yield match case`),types:n(`int str float bool list dict set tuple bytes object type`)},go:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/`[^`]*`?/y,first:"`"},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false iota`),types:n(`string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte rune float32 float64 bool error any`)},rust:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/b?r#*"[\s\S]*?(?:"#*|$)/y,first:`br`},{kind:`string`,re:/b?"(?:[^"\\]|\\[\s\S])*"?/y,first:`b"`},{kind:`string`,re:/b?'(?:[^'\\\n]|\\.){1,8}'/y,first:`b'`},{kind:`meta`,re:/#!?\[[^\]\n]*\]?/y,first:`#`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while`),types:n(`i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 bool char str String Vec Option Result Box Some None Ok Err`),macros:!0},yaml:{rules:[{kind:`comment`,re:d,at:`word`,first:`#`},{kind:`prop`,re:/[^\s#:'"][^\n:#]*?(?=\s*:(?:\s|$))/y,at:`line`},{kind:`string`,re:s,first:`"`},{kind:`string`,re:/'(?:[^'\n]|'')*'?/y,first:`'`},{kind:`meta`,re:/---|\.\.\./y,at:`line`,first:`-.`},{kind:`number`,re:/-?\d+(?:\.\d+)?\b/y,first:`-0123456789`}],keywords:n(`true false null yes no on off`)},sql:{rules:[{kind:`comment`,re:/--[^\n]*/y,first:`-`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/'(?:[^']|'')*'?/y,first:`'`},{kind:`prop`,re:/"[^"\n]*"?/y,first:`"`},{kind:`number`,re:/\d+(?:\.\d+)?\b/y,first:`0123456789`}],keywords:n(`select from where and or not insert into values update set delete create table drop alter add column index primary key foreign references join left right inner outer full on as group by order having limit offset distinct union all null is in like between case when then else end exists returning with default unique check if begin commit rollback asc desc cascade view trigger`),types:n(`int integer bigint smallint serial text varchar char boolean bool date timestamp timestamptz json jsonb uuid real numeric float double decimal`),ci:!0},css:{rules:[{kind:`comment`,re:l,first:`/`},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`keyword`,re:/@[\w-]+/y,first:`@`},{kind:`prop`,re:/--?[a-zA-Z][\w-]*(?=\s*:)|[a-zA-Z][\w-]*(?=\s*:[^:{]*[;}\n])/y},{kind:`number`,re:/#[\da-fA-F]{3,8}\b|-?\d*\.?\d+(?:%|[a-zA-Z]{1,4})?/y}],keywords:n(`important inherit initial unset none auto`)},md:{rules:[{kind:`keyword`,re:/#{1,6}\s[^\n]*/y,at:`line`,first:`#`},{kind:`meta`,re:/(?:[-*+]|\d+[.)])(?=\s)/y,at:`line`,first:`-*+0123456789`},{kind:`string`,re:/`[^`\n]*`?/y,first:"`"},{kind:`type`,re:/\*\*[^*\n]+\*\*|__[^_\n]+__/y,first:`*_`},{kind:`attr`,re:/\[[^\]\n]*\]\([^)\n]*\)/y,first:`[`},{kind:`comment`,re:/>[^\n]*/y,at:`line`,first:`>`}]}};function p(e,t){for(let n=t-1;n>=0;n--){let t=e[n];if(t===`
|
|
2
|
+
`)return!0;if(t!==` `&&t!==` `&&t!==`-`)return!1}return!0}function m(e,t){for(let n=t-1;n>=0;n--){let t=e[n];if(t!==` `&&t!==` `)return t===`
|
|
3
|
+
`||t===`|`||t===`;`||t===`&`||t===`(`||t==="`"||t===`$`}return!0}var h=(e,t)=>t===0||/\s/.test(e[t-1]);function g(e,t,n){e.lastIndex=n;let r=e.exec(t);return r&&r[0]?r[0]:void 0}function _(e){let t=[];for(let[n,r]of e.split(`
|
|
4
|
+
`).entries()){if(n&&t.push({text:`
|
|
5
|
+
`}),!r)continue;let e=/^(@@|\+\+\+ |--- |diff |index )/.test(r)?`meta`:r[0]===`+`?`add`:r[0]===`-`?`del`:void 0;t.push(e?{text:r,kind:e}:{text:r})}return t}function v(e){let t=[],n=0;for(;n<e.length;){let r=g(/<!--[\s\S]*?(?:-->|$)/y,e,n);if(r){t.push({text:r,kind:`comment`}),n+=r.length;continue}let i=g(/<\/?[A-Za-z][\w:.-]*|<![A-Za-z]+/y,e,n);if(i)for(t.push({text:i,kind:`tag`}),n+=i.length;n<e.length;){let r=g(/\/?>/y,e,n);if(r){t.push({text:r,kind:`tag`}),n+=r.length;break}let i=g(/\s+/y,e,n),a=i?void 0:g(/[^\s=>/"']+/y,e,n),o=i||a?void 0:g(/"[^"]*"?|'[^']*'?/y,e,n);i?t.push({text:i}):a?t.push({text:a,kind:`attr`}):o?t.push({text:o,kind:`string`}):t.push({text:e[n]}),n+=(i??a??o??e[n]).length}else{let r=g(/[^<]+|</y,e,n);t.push({text:r}),n+=r.length}}return t}function y(e,t){let n=[],i=``,o=(e,t)=>{t?(i&&n.push({text:i}),i=``,n.push({text:e,kind:t})):i+=e},s=0;outer:for(;s<e.length;){let n=e[s];for(let r of t.rules){if(r.first&&!r.first.includes(n)||r.at===`line`&&!p(e,s)||r.at===`word`&&!h(e,s)||r.at===`command`&&!m(e,s))continue;let i=g(r.re,e,s);if(i){o(i,r.kind===`fn`&&t.keywords?.has(i)?`keyword`:r.kind),s+=i.length;continue outer}}if(r(n)){let n=a(e,s),r=t.ci?n.toLowerCase():n,i=e[s+n.length],c;t.keywords?.has(r)?c=`keyword`:t.types?.has(r)?c=`type`:(i===`(`||t.macros&&i===`!`&&e[s+n.length+1]!==`=`)&&(c=`fn`),o(n,c),s+=n.length}else i+=n,s++}return i&&n.push({text:i}),n}function b(e){let t=[];for(let n of e){let e=t.at(-1);e&&!e.kind&&!n.kind?e.text+=n.text:t.push({...n})}return t}function x(e,n){let r=String(e??``);if(!r)return[];let i=t(n);if(i===`diff`)return b(_(r));if(i===`html`)return b(v(r));let a=i?f[i]:void 0;return a?y(r,a):[{text:r}]}export{x as highlight,t as languageOf};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg:#141413;--side:#1a1a18;--raise:#1f1f1d;--raise-2:#262624;--line:#2a2a27;--line-2:#3a3a36;--text:#e8e6e1;--muted:#9b9890;--faint:#6f6c65;--accent:#fab283;--accent-ink:#1b130d;--ok:#7fb685;--warn:#e5c07b;--err:#e06c75;--add:#7fb685;--del:#e06c75;--serif:ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;--sans:system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--mono:ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;--side-w:235px;--rail-w:260px;--col:680px;--r:8px;--font-scale:1;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html{font-size:calc(100% * var(--font-scale,1));-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}html,body{background:var(--bg);height:100%;color:var(--text);-webkit-font-smoothing:antialiased;margin:0}body{font:.875rem/1.5 var(--sans)}#root{height:100%}a{color:inherit;text-decoration:none}button,input,select,textarea{font:inherit;color:inherit}.sr-only{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.muted{color:var(--muted)}.small{font-size:.7812rem}.mono,code,pre{font-family:var(--mono)}.display{font-family:var(--serif);letter-spacing:-.01em;font-weight:400}.grow{flex:1}.row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.stack{flex-direction:column;gap:12px;display:flex}.pad{padding:12px 16px}.error{color:var(--err)}.ok{color:var(--ok)}.icon{fill:none;stroke:currentColor;stroke-width:1.8px;stroke-linecap:round;stroke-linejoin:round;flex:none;width:16px;height:16px}kbd{font:.6875rem var(--mono);color:var(--faint);border:1px solid var(--line-2);border-radius:4px;padding:0 4px}:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.btn{border-radius:var(--r);border:1px solid var(--line-2);background:var(--raise);cursor:pointer;white-space:nowrap;align-items:center;gap:6px;min-height:32px;padding:0 12px;display:inline-flex}.btn:hover:not(:disabled){background:var(--raise-2)}.btn:disabled{opacity:.45;cursor:default}.btn.small{height:auto;min-height:26px;padding:0 9px;font-size:.7812rem}.btn.primary{background:var(--accent);color:var(--accent-ink);border-color:#0000;font-weight:600}.btn.primary:hover:not(:disabled){background:#fbc29c}.btn.danger{color:var(--err)}.icon-btn{width:30px;height:30px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:6px;place-items:center;display:inline-grid}.icon-btn:hover:not(:disabled){background:var(--raise-2);color:var(--text)}.icon-btn:disabled{opacity:.4;cursor:default}.icon-btn.small{width:24px;height:24px}.icon-btn.on,.icon-btn[aria-pressed=true]{color:var(--accent)}.icon-btn.recording{color:var(--err)}input[type=search],input[type=text],input[type=password],select,textarea{background:var(--raise);border:1px solid var(--line);border-radius:var(--r);min-height:32px;padding:6px 10px}input:focus,select:focus,textarea:focus{border-color:var(--line-2);box-shadow:0 0 0 1px var(--line-2);outline:none}.chips{flex-wrap:wrap;gap:6px;display:flex}.chip{border:1px solid var(--line);min-height:26px;color:var(--muted);cursor:pointer;background:0 0;border-radius:13px;padding:0 10px;font-size:.7812rem}.chip[aria-pressed=true]{background:var(--raise-2);color:var(--text);border-color:var(--line-2)}.chip .count{color:var(--accent);margin-left:4px}.card{background:var(--raise);border:1px solid var(--line);border-radius:10px;padding:14px}.banner{border:1px solid var(--line);background:var(--side);border-radius:var(--r);justify-content:space-between;align-items:center;gap:12px;margin:8px 0;padding:8px 12px;display:flex}.banner.warn{color:var(--warn);border-width:0 0 1px;border-color:#5a4a2a;border-radius:0;margin:0}.badge{background:var(--accent);min-width:18px;height:18px;color:var(--accent-ink);border-radius:9px;place-items:center;padding:0 5px;font-size:.6875rem;font-weight:700;display:inline-grid}.badge-pill{border:1px solid var(--line-2);color:var(--muted);border-radius:8px;margin-left:6px;padding:1px 6px;font-size:.6875rem}.badge-pill.off{color:var(--err)}.status{flex:none;place-items:center;width:14px;height:14px;display:inline-grid}.status-dot{background:var(--faint);border-radius:50%;width:7px;height:7px}.status.blocked .status-dot{background:var(--warn);box-shadow:0 0 0 3px #e5c07b22}.status.error .status-dot{background:var(--err)}.spinner{border:1.6px solid #fab28344;border-top-color:var(--accent);border-radius:50%;flex:none;width:12px;height:12px;animation:.8s linear infinite spin;display:inline-block}.spinner.small{width:10px;height:10px}@keyframes spin{to{transform:rotate(360deg)}}.unread-dot{background:var(--accent);border-radius:50%;flex:none;width:6px;height:6px;margin-left:6px;display:inline-block}.online-dot{background:var(--faint);vertical-align:middle;border-radius:50%;width:6px;height:6px;margin-right:6px;display:inline-block}.online-dot.on{background:var(--ok)}.voice-dot{background:var(--ok);vertical-align:middle;border-radius:50%;flex:none;width:8px;height:8px;margin-right:6px;animation:1.8s ease-in-out infinite voice-dot-pulse;display:inline-block}@keyframes voice-dot-pulse{0%,to{box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 55%, transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb, var(--ok) 0%, transparent)}}@media (prefers-reduced-motion:reduce){.voice-dot{animation:none}}.shell{height:100%;display:flex;overflow:hidden}.sidebar{width:var(--side-w);background:var(--side);border-right:1px solid var(--line);flex-direction:column;flex:none;min-height:0;display:flex}.main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex;position:relative}.expand-sidebar{z-index:5;position:absolute;top:9px;left:8px}.shell.collapsed .topbar,.shell.collapsed .home-head{padding-left:48px}.conn-banner{color:var(--warn);text-align:center;background:#3a2f1a;padding:3px;font-size:.7812rem}.side-top{justify-content:space-between;align-items:center;padding:10px 8px 6px 14px;display:flex}.brand{align-items:center;gap:8px;display:flex}.brand-mark{flex:none}.hub-mark{color:var(--text);--mark-gap:var(--bg)}.hub-mark-gap{fill:var(--mark-gap)}.hub-mark-hub{fill:var(--accent)}.sidebar .hub-mark{--mark-gap:var(--side)}.brand-name{font-family:var(--serif);font-size:1.0625rem}.side-search{border:1px solid var(--line);border-radius:var(--r);color:var(--faint);background:var(--bg);align-items:center;gap:6px;margin:4px 10px;padding:0 8px;display:flex}.side-search input{width:100%;min-height:30px;box-shadow:none;background:0 0;border:0;padding:0}.side-new,.side-link{color:var(--text);border-radius:6px;align-items:center;gap:8px;margin:2px 8px;padding:6px 8px;display:flex}.side-new:hover,.side-link:hover,.side-link.active{background:var(--raise-2)}.side-link{color:var(--muted)}.side-head{color:var(--faint);justify-content:space-between;align-items:center;padding:10px 8px 0 16px;font-size:.75rem;display:flex}.side-scroll{flex:1;min-height:0;padding:0 6px 12px;overflow:auto}.side-section{margin-top:10px}.side-section-head{justify-content:space-between;align-items:center;height:24px;padding:0 4px 2px 10px;display:flex}.side-section-title{color:var(--faint);text-transform:none;letter-spacing:.02em;text-overflow:ellipsis;white-space:nowrap;font-size:.7188rem;overflow:hidden}.side-section.needs .side-section-title{color:var(--warn)}.side-section-head .icon-btn{opacity:0}.side-section:hover .side-section-head .icon-btn,.side-section-head .icon-btn:focus-visible{opacity:1}.side-list{margin:0;padding:0;list-style:none}.side-row{border-radius:6px;align-items:center;display:flex;position:relative}.side-row:hover,.side-row.active{background:var(--raise-2)}.side-row-link{min-width:0;color:var(--muted);flex:1;align-items:center;gap:8px;padding:5px 4px 5px 8px;display:flex}.side-row.active .side-row-link,.side-row.unread .side-row-link{color:var(--text)}.side-title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.row-more{opacity:0;margin-right:2px}.side-row:hover .row-more,.side-row.active .row-more,.row-more[aria-expanded=true],.row-more:focus-visible{opacity:1}.side-empty{color:var(--faint);padding:12px;font-size:.7812rem}.side-foot{border-top:1px solid var(--line);padding:8px}.account{color:var(--muted);border-radius:6px;align-items:center;gap:8px;padding:6px;display:flex}.account:hover{background:var(--raise-2);color:var(--text)}.account-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.avatar{background:var(--raise-2);width:24px;height:24px;color:var(--text);border-radius:50%;flex:none;place-items:center;font-size:.75rem;font-weight:600;display:inline-grid}.avatar.admin{box-shadow:0 0 0 1.5px var(--accent)}.avatar.large{width:40px;height:40px;font-size:1.0625rem}.menu{z-index:50;background:var(--raise);border:1px solid var(--line-2);border-radius:10px;min-width:200px;max-width:320px;padding:4px;position:fixed;box-shadow:0 12px 32px #0008}.menu-item{text-align:left;cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:6px;width:100%;padding:6px 8px;display:flex}.menu-item:hover:not(:disabled){background:var(--raise-2)}.menu-item.danger{color:var(--err)}.menu-check{width:14px;color:var(--accent)}.menu-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.menu-hint{color:var(--faint);font-size:.75rem}.menu-section{color:var(--faint);padding:6px 8px 2px;font-size:.7188rem}.menu-sep{background:var(--line);height:1px;margin:4px 0}.mic-picker{width:280px}.level{background:var(--bg);border-radius:3px;height:6px;margin:8px;overflow:hidden}.level span{background:var(--ok);height:100%;transition:width 80ms linear;display:block}.dialog{background:var(--raise);color:var(--text);border:1px solid var(--line-2);border-radius:12px;width:min(520px,94vw);max-height:84vh;padding:0}.dialog::backdrop{background:#0009}.dialog-head{justify-content:space-between;align-items:center;padding:12px 14px 6px;display:flex}.dialog-head h2{margin:0;font-size:.9375rem;font-weight:600}.dialog-body{max-height:calc(84vh - 56px);padding:4px 14px 14px;overflow:auto}.add-app-body{gap:10px;display:grid}.add-app .qr{width:min(260px,70vw);height:auto;image-rendering:pixelated;border-radius:8px;margin:4px auto;display:block}.add-app-link{word-break:break-all;background:var(--bg);border:1px solid var(--line-2);-webkit-user-select:all;user-select:all;border-radius:6px;padding:6px 8px;font-size:.7188rem}.add-app-countdown{font-variant-numeric:tabular-nums}.add-app-expired{justify-items:center;gap:8px;padding:24px 0;display:grid}.picker-search{width:100%;margin-bottom:8px}.picker-list{max-height:55vh;margin:0;padding:0;list-style:none;overflow:auto}.picker-group{text-transform:none;padding:8px 8px 2px;font-weight:600}.picker-note{margin:0 0 6px}.picker-opt{cursor:pointer;text-align:left;background:0 0;border:0;border-radius:6px;align-items:center;gap:8px;width:100%;padding:7px 8px;display:flex}.picker-opt:hover,.picker-opt.active{background:var(--raise-2)}.picker-opt[aria-selected=true] .po-name{color:var(--accent)}.po-name{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.po-mark{color:var(--accent);font-size:.75rem}.toast{left:50%;bottom:calc(24px + env(safe-area-inset-bottom));z-index:60;background:var(--raise-2);border:1px solid var(--line-2);border-radius:10px;align-items:center;gap:10px;max-width:92vw;padding:8px 12px;display:flex;position:fixed;transform:translate(-50%);box-shadow:0 8px 24px #0008}.toast.err{border-color:#6a3036}.toast.warn{border-color:#5a4a2a}.topbar{border-bottom:1px solid var(--line);flex:none;align-items:center;gap:8px;height:max(48px,2.4rem);padding:0 12px;display:flex}.topbar-title{flex:1;align-items:center;gap:8px;min-width:0;display:flex}.topbar .title{text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:.875rem;font-weight:500;overflow:hidden}.title-btn{all:unset;cursor:text;text-overflow:ellipsis;white-space:nowrap;border-radius:4px;display:block;overflow:hidden}.title-btn:hover,.title-btn:focus-visible{box-shadow:0 0 0 1px var(--line-2)}input[type=text].rename-field{width:100%;min-width:0;min-height:26px;font:inherit;color:var(--text);flex:1;padding:2px 6px}.topbar input.rename-field.title{max-width:520px}.rename-wrap{vertical-align:middle;flex:1;align-items:center;gap:4px;width:100%;min-width:0;max-width:100%;display:inline-flex}.topbar .rename-wrap{max-width:560px}.auto-name-btn{flex:none;font-size:14px;line-height:1}.naming{white-space:nowrap;flex:none;align-items:center;gap:4px;margin-left:6px;font-weight:400;display:inline-flex}.chip-server{color:var(--muted);border:1px solid var(--line);white-space:nowrap;border-radius:13px;align-items:center;gap:4px;max-width:260px;padding:2px 10px;font-size:.75rem;display:inline-flex;overflow:hidden}.chip-server .icon{width:12px;height:12px;margin-left:6px}.chip-evidence{color:var(--muted);border:1px dashed var(--line);white-space:nowrap;cursor:help;border-radius:13px;align-items:center;padding:2px 10px;font-size:.75rem;display:inline-flex}.chip-evidence.screen,.chip-evidence.unknown{color:var(--warn);border-color:#5a4a2a}.btn.changes{font-family:var(--mono)}.add{color:var(--add)}.del{color:var(--del)}.home-shell{flex-direction:column;height:100%;min-height:0;display:flex}.home-scroll{flex:1;overflow:auto}.home{max-width:1180px;margin:0 auto;padding:24px 28px 48px}.home-head{justify-content:space-between;align-items:center;margin-bottom:12px;display:flex}.home-head h1{margin:0;font-size:1.75rem}.home-tools{gap:8px;margin:12px 0 8px;display:flex}.search{border:1px solid var(--line);border-radius:var(--r);background:var(--raise);color:var(--faint);flex:1;align-items:center;gap:8px;padding:0 10px;display:flex}.search input{width:100%;box-shadow:none;background:0 0;border:0}.count{margin:10px 0 4px;font-size:.75rem}.sessions-table{border-collapse:collapse;table-layout:fixed;width:100%}.sessions-table th{text-align:left;color:var(--faint);border-bottom:1px solid var(--line);padding:0;font-size:.75rem;font-weight:500}.th-btn{color:inherit;cursor:pointer;background:0 0;border:0;gap:4px;padding:8px 6px;display:inline-flex}.th-btn:hover{color:var(--text)}.sort-mark{color:var(--accent)}.sessions-table td{border-bottom:1px solid var(--line);text-overflow:ellipsis;white-space:nowrap;color:var(--muted);padding:7px 6px;overflow:hidden}.sessions-table tbody tr{cursor:pointer}.sessions-table tbody tr:hover{background:var(--side)}.sessions-table .col-title a{color:var(--text)}.sessions-table tr.unread .col-title a{font-weight:600}.col-status{width:64px}.col-pick{text-align:center;width:32px}.col-pick input{vertical-align:middle;cursor:pointer;margin:0}.home-tools .btn.small{min-height:32px}.bulk-bar{border:1px solid var(--line);border-radius:var(--r);background:var(--raise);align-items:center;gap:8px;margin:8px 0 0;padding:6px 10px;font-size:.8125rem;display:flex}.bulk-bar span{margin-right:auto}.archive-inactive{flex-direction:column;gap:12px;display:flex}.archive-inactive input[type=number]{background:var(--raise);border:1px solid var(--line);border-radius:var(--r);width:120px;min-height:32px;padding:6px 10px}.dialog-actions{justify-content:flex-end;gap:8px;display:flex}.col-title{width:30%}.col-project{width:16%}.col-server,.col-source{width:9%}.col-activity{width:10%}.col-actions{text-align:right;width:40px}.empty-cell{text-align:center;padding:24px!important}.home-list{margin:0;padding:0;list-style:none}.home-item{border-bottom:1px solid var(--line);align-items:center;display:flex}.home-item-link{flex:1;align-items:center;gap:10px;min-width:0;padding:10px 4px;display:flex}.home-item-text{flex-direction:column;flex:1;min-width:0;display:flex}.home-item-title,.home-item-meta{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.home-item.unread .home-item-title{font-weight:600}.home-item-meta{font-size:.7812rem}.offline-line{margin-top:10px}.empty{text-align:center;padding:48px 16px}.empty .display{margin:0 0 6px;font-size:1.375rem}.session-layout{height:100%;min-height:0;display:flex}.session-main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex;position:relative}.scroller{overflow-anchor:none;flex:1;min-height:0;overflow:auto}.history-sentinel{height:1px}.history-edge{text-align:center;justify-content:center;align-items:center;gap:8px;margin:16px 0 0;display:flex}.history-edge.error{color:var(--err)}.column{max-width:var(--col);margin:0 auto;padding:0 20px}.column.narrow{max-width:560px;padding-top:40px}.transcript{flex-direction:column;gap:14px;padding:24px 0 8px;display:flex}.msg.user{align-self:flex-end;align-items:flex-end;gap:6px;max-width:85%;display:flex}.user-body{flex-direction:column;align-items:flex-end;gap:6px;min-width:0;display:flex}.msg-images{flex-wrap:wrap;justify-content:flex-end;gap:6px;display:flex}.msg-image{border:1px solid var(--line-2);background:var(--raise-2);border-radius:10px;display:block;overflow:hidden}.msg-image img{object-fit:contain;max-width:min(220px,60vw);max-height:220px;display:block}.file-chip{background:var(--raise-2);color:var(--muted);text-overflow:ellipsis;white-space:nowrap;border-radius:999px;align-items:center;gap:6px;max-width:240px;padding:4px 10px;font-size:.7812rem;display:inline-flex;overflow:hidden}.file-chip .icon{flex:none;width:14px;height:14px}.pill{background:var(--raise-2);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:14px;padding:8px 14px}.tick{color:var(--faint);font-size:.6875rem}.tick.heard{color:var(--ok)}.tick.failed{color:var(--err)}.tick.queued{color:var(--warn)}.hub-msg-delivery{color:var(--warn);font-size:.6875rem}.composer-unavailable{align-items:center;gap:10px;padding:10px 2px;display:flex}.composer-unavailable p{flex:1;margin:0}.msg.assistant{overflow-wrap:anywhere;line-height:1.6}.msg.assistant.live .md>:is(p,h3,h4,h5,h6):last-child:after,.msg.assistant.live .md>:is(ul,ol):last-child>li:last-child>p:last-child:after{content:"▍";color:var(--accent);animation:1s steps(2,end) infinite blink}@keyframes blink{50%{opacity:0}}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md{min-width:0}.md p{margin:0 0 .7em}.md h3,.md h4,.md h5,.md h6{letter-spacing:-.005em;margin:1.15em 0 .45em;font-weight:600;line-height:1.35}.md h3{border-bottom:1px solid var(--line);padding-bottom:.25em;font-size:1.14em}.md h4{font-size:1.05em}.md h5,.md h6{color:var(--muted);font-size:1em}.md ul,.md ol{margin:0 0 .7em;padding-left:1.5em}.md li{margin:.15em 0}.md li::marker{color:var(--faint)}.md li>p{margin:0}.md li>:is(ul,ol){margin:.15em 0 .2em}.md :is(ul,ol).loose>li{margin:.35em 0}.md :is(ul,ol).loose>li>p{margin:0 0 .5em}.md :is(ul,ol).loose>li>p:last-child{margin-bottom:0}.md li.task{list-style:none;position:relative}.md li.task>input[type=checkbox]{width:.95em;height:.95em;accent-color:var(--accent);margin:0;position:absolute;top:.3em;left:-1.35em}.md li.task>input:checked~p{color:var(--muted)}.md .inline-code{background:var(--raise-2);border:1px solid var(--line);overflow-wrap:anywhere;border-radius:5px;padding:.05em .35em;font-size:.88em}.md a .inline-code{color:inherit}.md :is(th,td) .inline-code{white-space:nowrap;overflow-wrap:normal}.md strong{color:#f3f1ec;font-weight:600}.md del{color:var(--faint)}.md mark{color:#ffd9bd;-webkit-box-decoration-break:clone;box-decoration-break:clone;background:#fab28333;border-radius:3px;padding:0 .15em}.md .md-image{color:var(--accent)}.md .md-image:before{content:"🖼 "}.md blockquote{border-left:3px solid var(--line-2);color:var(--muted);margin:0 0 .7em;padding:.15em 0 .15em .85em}.md blockquote>:last-child{margin-bottom:0}.md a{color:var(--accent);text-underline-offset:2px;overflow-wrap:anywhere;-webkit-text-decoration:underline #fab28366;text-decoration:underline #fab28366}.md a:hover{text-decoration-color:currentColor}.md hr{border:0;border-top:1px solid var(--line);margin:1em 0}.md .table-wrap{border:1px solid var(--line);border-radius:var(--r);max-width:100%;margin:0 0 .8em;overflow-x:auto}.md table{border-collapse:collapse;min-width:100%;font-size:.93em;line-height:1.45}.md th,.md td{border-bottom:1px solid var(--line);text-align:left;vertical-align:top;overflow-wrap:normal;padding:.4em .75em}.md th{background:var(--side);color:var(--text);white-space:nowrap;font-weight:600}.md tbody tr:last-child td{border-bottom:0}.md tbody tr:nth-child(2n) td{background:#ffffff05}.md th+th,.md td+td{border-left:1px solid var(--line)}.md .code-block{border:1px solid var(--line);border-radius:var(--r);background:var(--side);margin:0 0 .8em;overflow:hidden}.md .code-head{border-bottom:1px solid var(--line);color:var(--faint);align-items:center;gap:2px;padding:2px 4px 2px 10px;font-size:.8em;display:flex}.md .code-lang{font-family:var(--mono);text-transform:lowercase;flex:1}.md .code-btn{color:var(--muted);cursor:pointer;font-size:inherit;background:0 0;border:0;border-radius:5px;min-height:24px;padding:3px 8px}.md .code-btn:hover{background:var(--raise-2);color:var(--text)}.md .code-btn[aria-pressed=true]{color:var(--accent)}.md pre.code{background:0 0;border:0;border-radius:0;margin:0;padding:.7em .9em;line-height:1.5;overflow:auto}.md .code-block.wrap pre.code{white-space:pre-wrap;overflow-wrap:anywhere}.md pre code{background:0 0;padding:0}.tok-comment{color:#7d7a72;font-style:italic}.tok-string{color:#a5c58b}.tok-number{color:#e5a46c}.tok-keyword{color:#c99ae0}.tok-type{color:#e5c07b}.tok-fn{color:#7fb4e0}.tok-prop{color:#e8a0a0}.tok-attr{color:#e5c07b}.tok-tag{color:#e88f8f}.tok-meta{color:#7fb4e0}.tok-add{color:var(--add);background:#7fb6851a}.tok-del{color:var(--del);background:#e06c751a}pre.code{white-space:pre;font-size:.75rem}.fold-head{color:var(--muted);cursor:pointer;background:0 0;border:0;align-items:center;gap:6px;padding:2px 0;font-size:.8125rem;display:inline-flex}.fold-head:hover:not(:disabled){color:var(--text)}.fold-head:disabled{cursor:default}.fold-head .chev{width:13px;height:13px;transition:transform .15s}.fold-head[aria-expanded=true] .chev{transform:rotate(90deg)}.fold.has-failed .fold-head{color:#c9a0a4}.fold-body{border-left:1px solid var(--line);margin:6px 0 0 2px;padding-left:12px}.reasoning{color:var(--muted);white-space:pre-wrap;max-height:320px;font-size:.8125rem;overflow:auto}.reasoning.hidden-reasoning{font-style:italic}.tool-list{list-style:none}.tool-head{cursor:pointer;text-align:left;background:0 0;border:0;align-items:center;gap:8px;width:100%;padding:3px 0;font-size:.8125rem;display:flex}.tool-mark{width:14px;color:var(--ok);font-size:.6875rem}.tool.failed .tool-mark{color:var(--err)}.tool-name{font-family:var(--mono);font-size:.7812rem}.tool-summary{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:.7812rem;overflow:hidden}.tool-detail pre{background:var(--side);border:1px solid var(--line);border-radius:6px;max-height:360px;padding:8px;overflow:auto}.hub-msg{border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--r);max-width:85%;padding:6px 10px}.hub-msg.out{align-self:flex-end}.hub-msg-head{color:var(--muted);align-items:center;gap:6px;margin-bottom:2px;font-size:.7812rem;display:flex}.hub-msg-mark{color:var(--accent)}.hub-msg-kind{font-family:var(--mono);text-transform:uppercase;font-size:.7188rem}.hub-msg-label{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.hub-msg-text{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.875rem}.hub-note .fold-body{color:var(--muted)}.subagent{border:1px solid var(--line);border-radius:var(--r);color:var(--muted);align-items:center;gap:8px;padding:8px 10px;display:flex}.subagent:hover{background:var(--side);color:var(--text)}.sub-mark{color:var(--accent);place-items:center;width:14px;display:inline-grid}.sub-label{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.sub-label strong{color:var(--text);font-weight:500}.subagent .chev{width:14px;height:14px}.note{color:var(--muted);margin:6px 0;font-size:.7812rem}.note.err{color:var(--err)}.working-line{align-items:center;gap:8px;margin:10px 0;font-size:.8125rem;display:flex}.idle-line{margin:12px 0 20px}.empty-session{text-align:center;padding-top:18vh}.blocker{border-color:#5a4a2a;margin:12px 0}.blocker-head{color:var(--warn);margin:0 0 4px}.blocker-text{white-space:pre-wrap;overflow-wrap:anywhere;margin:0 0 10px;font-size:.7812rem}.decisions{margin:12px 0}.decisions .section-title{color:var(--warn);margin:0 0 6px;font-size:.875rem}.decision{border-color:#5a4a2a;margin:12px 0}.decision.urgency-high{border-color:var(--err)}.decision.targeted{box-shadow:0 0 0 2px var(--accent)}.decision-question{font-size:.875rem}.decision-deadline{color:var(--warn);margin:0 0 8px}.decision-deadline.overdue{color:var(--err)}.decision .badge-pill.urgency.high{color:var(--err);border-color:var(--err)}.decision .badge-pill.recommended{color:var(--accent);border-color:var(--accent)}.decision .badge-pill.safe{color:var(--ok);border-color:var(--ok)}.decision .choice.recommended>span:first-of-type{font-weight:600}.decision-why{margin:6px 0}.decision-note,.decision textarea{width:100%;margin:6px 0}.decision-tier{margin:4px 0 8px}.decision .badge-pill.agent-request{color:var(--warn);border-color:var(--warn)}.escalation-details{grid-template-columns:max-content 1fr;gap:2px 12px;margin:0 0 10px;display:grid}.escalation-details dt{color:var(--muted)}.escalation-details dd{overflow-wrap:anywhere;min-width:0;margin:0}.escalation-resource{margin:0 6px 2px 0;display:inline-block}.blocker-recommendation{color:var(--accent);margin:-4px 0 10px}.answered-by{margin:6px 0}.blocker .answered-by{margin:8px 0 0}.grant-row .grant-facts{grid-template-columns:max-content 1fr;gap:2px 12px;margin:4px 0 0;display:grid}.grant-row .grant-facts dt{color:var(--muted)}.grant-row .grant-facts dd{overflow-wrap:anywhere;min-width:0;margin:0}.grant-row.ended{opacity:.6}.badge-pill.active{color:var(--ok);border-color:var(--ok)}.grant-form{flex-direction:column;gap:12px;display:flex}.grant-form label.text-field{flex-direction:column;gap:4px;display:flex}.grant-form textarea,.grant-form input[type=text]{width:100%}.grant-verbs{border:0;flex-wrap:wrap;gap:4px 14px;margin:0;padding:0;display:flex}.audit-row .audit-result.ok{color:var(--ok)}.audit-row .audit-result.warn{color:var(--warn)}.audit-row .audit-result.err{color:var(--err)}.form-card fieldset{border:0;min-width:0;margin:0;padding:0}.form-card fieldset:disabled{opacity:.6}.form-field{flex-direction:column;gap:4px;margin:0 0 12px;display:flex}.field-title{font-weight:500}.field-desc{display:block}.choice-group{flex-direction:column;gap:4px;display:flex}.choice,.check{cursor:pointer;align-items:baseline;gap:8px;display:flex}.choice-group input[type=text],.form-field>input[type=text]{width:100%}.field-error{color:var(--err);margin:0}.dock{padding:8px 0 calc(12px + env(safe-area-inset-bottom));background:linear-gradient(transparent, var(--bg) 18px);flex:none}.composer-box{border:1px solid var(--line-2);background:var(--raise);border-radius:14px;padding:10px 10px 6px 14px}.composer-box:focus-within{border-color:#4a4a45}.composer textarea{resize:none;width:100%;min-height:24px;max-height:280px;box-shadow:none;background:0 0;border:0;padding:0;line-height:1.5}.composer.dragging .composer-box{border-color:var(--accent);border-style:dashed}.attachments{flex-wrap:wrap;gap:8px;margin:0 0 8px;padding:0;list-style:none;display:flex}.attachment{border:1px solid var(--line-2);background:var(--raise-2);border-radius:10px;justify-content:center;align-items:center;width:64px;height:64px;display:flex;position:relative;overflow:hidden}.attachment img{object-fit:cover;width:100%;height:100%;display:block}.attachment-remove{color:#fff;cursor:pointer;background:#000000a6;border:0;border-radius:50%;justify-content:center;align-items:center;width:22px;height:22px;padding:0;display:flex;position:absolute;top:3px;right:3px}.attachment-remove .icon{width:13px;height:13px}.composer-actions{align-items:center;gap:4px;margin-top:4px;display:flex}.composer-meta{align-items:center;gap:6px;padding:6px 4px 0;display:flex}.composer-meta>.muted{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.composer-error{color:var(--err);margin:0 0 6px;font-size:.7812rem}.composer-hint{margin:0 0 6px}.btn.send{border-radius:8px;min-width:30px;height:30px;padding:0 9px}.btn.send.steer{padding:0 12px}.btn.stop{height:30px;color:var(--text)}.split{border:1px solid var(--line-2);border-radius:8px;height:30px;display:inline-flex;overflow:hidden}.split button{color:var(--muted);cursor:pointer;background:0 0;border:0;place-items:center;display:grid}.split button:hover:not(:disabled){background:var(--raise-2);color:var(--text)}.split button:disabled{opacity:.4;cursor:default}.split-main{width:32px}.split.on .split-main{background:var(--accent);color:var(--accent-ink)}.split.on .split-main:hover:not(:disabled){background:var(--accent);color:var(--accent-ink);filter:brightness(1.08)}.voice-pill{border:1px solid var(--accent);min-height:28px;color:var(--accent);border-radius:999px;align-items:center;gap:6px;min-width:0;max-width:320px;padding:0 4px 0 8px;font-size:.75rem;display:inline-flex}.voice-pill .icon{flex:none;width:14px;height:14px}.voice-pill-link{color:inherit;text-overflow:ellipsis;white-space:nowrap;min-width:0;text-decoration:none;overflow:hidden}.voice-pill-link strong{color:var(--text);font-weight:500}.voice-pill .btn.small{height:22px;padding:0 8px}.voice-strip{justify-content:flex-end;padding:8px 12px 0;display:flex}.side-row.voice-here .side-title{color:var(--accent)}.voice-error{margin:0 0 8px}.split-chev{width:20px;border-left:1px solid var(--line-2)!important}.split-chev .icon{width:12px;height:12px}.split.on{border-color:var(--accent)}.split.on .split-main{color:var(--accent)}.model-chip{color:var(--faint);cursor:pointer;text-overflow:ellipsis;white-space:nowrap;background:0 0;border:0;border-radius:6px;max-width:220px;padding:2px 6px;font-size:.75rem;overflow:hidden}.model-chip:hover{background:var(--raise-2);color:var(--text)}.voice-bar{border:1px solid var(--line-2);background:var(--side);border-radius:12px;align-items:center;gap:10px;margin-bottom:8px;padding:6px 8px 6px 12px;display:flex}.voice-bar.paused{opacity:.8}.voice-bar.suspended{border-color:#5a4a2a;flex-wrap:wrap}.voice-bar.suspended .voice-state{color:var(--warn);flex:12em;min-width:0}.voice-bar.suspended .wave span{background:var(--warn)}.wave{align-items:center;gap:3px;height:22px;display:flex}.wave span{background:var(--accent);transform-origin:50%;border-radius:2px;width:3px;height:22px;transition:transform 80ms linear}.voice-bar.speaking .wave span{animation:.9s ease-in-out infinite talk}.voice-bar.speaking .wave span:nth-child(2){animation-delay:.15s}.voice-bar.speaking .wave span:nth-child(3){animation-delay:.3s}.voice-bar.speaking .wave span:nth-child(4){animation-delay:.45s}@keyframes talk{50%{transform:scaleY(.25)}}.voice-state{font-size:.8125rem}.voice-ignored{color:var(--faint);font-style:italic}.btn.end{color:var(--err)}.rail{width:var(--rail-w);border-left:1px solid var(--line);background:var(--bg);flex:none;padding:0 12px 24px;overflow:auto}.panel-handle{z-index:6;cursor:col-resize;touch-action:none;outline:none;flex:none;width:6px;margin:0 -3px;position:relative}.panel-handle:before{content:"";background:0 0;width:1px;transition:background .12s;position:absolute;top:0;bottom:0;left:50%;transform:translate(-50%)}.panel-handle:hover:before,.panel-handle.dragging:before,.panel-handle:focus-visible:before{background:var(--accent);width:2px}html.resizing,html.resizing *{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}html.resizing iframe{pointer-events:none}.rail-head{justify-content:space-between;align-items:center;height:48px;display:flex}.rail-title{color:var(--muted);font-size:.8125rem}.rail-card{border-top:1px solid var(--line);padding:12px 0}.rail-card h3{color:var(--faint);justify-content:space-between;margin:0 0 8px;font-size:.75rem;font-weight:500;display:flex}.kv{grid-template-columns:64px 1fr;gap:4px 8px;margin:0;font-size:.7812rem;display:grid}.kv dt{color:var(--faint)}.kv dd{text-overflow:ellipsis;white-space:nowrap;margin:0;overflow:hidden}.meter{background:var(--raise-2);border-radius:3px;height:6px;margin-bottom:6px;overflow:hidden}.meter span{background:var(--accent);height:100%;display:block}.meter span.hot{background:var(--warn)}.mcp-list,.file-list{margin:0;padding:0;list-style:none}.mcp-list li{align-items:center;gap:8px;padding:2px 0;font-size:.7812rem;display:flex}.mcp-name{text-overflow:ellipsis;flex:1;overflow:hidden}.mcp-dot{background:var(--faint);border-radius:50%;width:6px;height:6px}.mcp-dot.connected{background:var(--ok)}.mcp-dot.failed,.mcp-dot.needs_auth{background:var(--err)}.mcp-dot.pending{background:var(--warn)}.diffstat{font-family:var(--mono);font-size:.75rem}.file{cursor:pointer;text-align:left;background:0 0;border:0;border-radius:5px;align-items:center;gap:6px;width:100%;padding:3px 4px;font-size:.7812rem;display:flex}.file:hover{background:var(--raise-2)}.file-status{font:600 .6562rem var(--mono);width:14px;color:var(--warn)}.file-status.added{color:var(--add)}.file-status.deleted{color:var(--del)}.file-name{text-overflow:ellipsis;white-space:nowrap;text-align:left;min-width:0;font-family:var(--mono);direction:rtl;flex:1;overflow:hidden}.diff-panel{z-index:20;background:var(--side);border:1px solid var(--line-2);border-radius:12px;flex-direction:column;display:flex;position:absolute;inset:48px 12px 12px;box-shadow:0 16px 48px #000a}.diff-head{border-bottom:1px solid var(--line);align-items:center;gap:10px;padding:8px 10px;display:flex}.diff-head select{min-width:0;font-family:var(--mono);flex:1;font-size:.7812rem}.wrap-toggle{align-items:center;gap:4px;display:inline-flex}.diff-body{flex:1;overflow:auto}table.diff{border-collapse:collapse;width:100%;font:.75rem/1.55 var(--mono)}table.diff td{vertical-align:top;padding:0 8px}table.diff .ln{width:1%;color:var(--faint);text-align:right;-webkit-user-select:none;user-select:none;white-space:nowrap}table.diff .code-line{white-space:pre}.diff-body.wrap table.diff .code-line{white-space:pre-wrap;overflow-wrap:anywhere}table.diff tr.add{background:#7fb68518}table.diff tr.del{background:#e06c7518}table.diff tr.hunk td{color:var(--faint);background:var(--raise);padding:3px 8px}table.diff .sign{color:var(--faint);margin-right:8px}.settings,.new-session{flex-direction:column;height:100%;min-height:0;display:flex}.settings-body{flex:1;gap:24px;padding:0 24px;display:flex;overflow:auto}.settings-nav{flex-direction:column;flex:none;align-self:flex-start;gap:2px;width:150px;padding-top:24px;display:flex;position:sticky;top:0}.settings-nav a{color:var(--muted);border-radius:6px;padding:5px 8px}.settings-nav a:hover{background:var(--raise-2);color:var(--text)}.settings-content{flex:1;max-width:720px;padding:12px 0 48px}.settings-section{border-bottom:1px solid var(--line);padding:18px 0;scroll-margin-top:8px}.settings-section h2{font:400 1.25rem var(--serif);margin:0 0 12px}.settings-section h3{color:var(--muted);font-size:.8125rem;font-weight:500}.list{border:1px solid var(--line);border-radius:10px;margin:0;padding:0;list-style:none}.list li{border-bottom:1px solid var(--line);flex-wrap:wrap;align-items:center;gap:8px;padding:10px 12px;display:flex}.list li:last-child{border-bottom:0}.list li.disabled{opacity:.6}.name{font-weight:500}.switches{flex-direction:column;gap:10px;margin:10px 0 0;padding:0;list-style:none;display:flex}.switch{cursor:pointer;justify-content:space-between;align-items:center;gap:12px;display:flex}.speed{align-items:center;gap:12px;display:flex}.speed input{accent-color:var(--accent);flex:1}.account-card{align-items:center;gap:12px;display:flex}.field{flex-direction:column;gap:4px;display:flex}.field label,.field-label{color:var(--muted);font-size:.7812rem}.field .row input{flex:1}.field-error{color:var(--err);margin:0;font-size:.75rem}.inherit{color:var(--faint);margin:0}.speech-card{flex-direction:column;gap:10px;display:flex}.speech-card legend{color:var(--muted);padding:0 6px}.key-summary{margin:0;font-size:.8125rem}.entry{flex-direction:column;gap:14px;max-width:440px;margin:0 auto;padding:12vh 20px 40px;display:flex}.brand-big{align-items:center;gap:12px;margin:0;font-size:1.875rem;display:flex}.pair-code{font-family:var(--mono);letter-spacing:.3em;font-size:1.25rem}.greeting{margin:0 0 18px;font-size:1.625rem}details.card summary{cursor:pointer;color:var(--muted)}.drawer-backdrop,.sheet-backdrop{z-index:30;background:#0009;position:fixed;inset:0}@media (max-width:760px){.shell.phone .sidebar{z-index:40;width:min(86vw,320px);padding-top:env(safe-area-inset-top);position:fixed;inset:0 auto 0 0;box-shadow:8px 0 32px #000a}.topbar{padding-top:env(safe-area-inset-top);height:calc(48px + env(safe-area-inset-top))}.home{padding:16px 14px 40px}.home-head h1{font-size:1.5rem}.column{padding:0 14px}.chip-server,.btn.changes{display:none}.rail{z-index:40;border:1px solid var(--line-2);width:auto;max-height:72vh;padding-bottom:calc(24px + env(safe-area-inset-bottom));border-radius:16px 16px 0 0;position:fixed;bottom:0;left:0;right:0;box-shadow:0 -12px 40px #000a}.diff-panel{inset:calc(48px + env(safe-area-inset-top)) 0 0;border-radius:0}.settings-nav{display:none}.settings-body{padding:0 14px}.composer-meta .model-chip{max-width:40vw}.msg.user{max-width:92%}}.notify-status{border-radius:var(--r);border:1px solid var(--line);margin:8px 0;padding:8px 12px}.notify-status.warn{color:var(--warn);border-color:#5a4a2a}.notify-status.ok{color:var(--ok,inherit)}.notify-facts{grid-template-columns:max-content 1fr;gap:2px 12px;margin:0 0 8px;display:grid}.notify-facts dd{margin:0}.row-actions{flex-wrap:wrap;gap:8px;display:flex}.quiet-hours{flex-wrap:wrap;align-items:center;gap:8px;margin-top:6px;display:flex}.notify-summary{margin:4px 0 8px}.notify-group{margin:14px 0 0}.notify-group h4,#notifications h4{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:14px 0 0;font-size:.8125rem;font-weight:500}.notify-kind{flex-direction:column;gap:2px;min-width:0;display:flex}.notify-kind .small{line-height:1.35}.switch input[type=checkbox]{flex:none}.notify-test{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.notify-test select{max-width:60vw}p.small.warn{color:var(--warn)}p.small.err{color:var(--err,#e5484d)}p.small.ok{color:var(--ok,inherit)}.voice-bar{flex-wrap:wrap}.voice-hint{color:var(--muted,inherit);flex-basis:100%;margin:2px 0 0}button.link{color:var(--accent,#fab283);cursor:pointer;font:inherit;background:0 0;border:0;padding:0;text-decoration:underline}.tap-to-start{width:100%;margin-bottom:8px;padding:14px;font-size:1.05em;display:block}.new-column{max-width:720px;padding-top:12vh}.new-composer .composer-box{padding:14px 12px 8px 16px}.new-composer textarea{resize:none;width:100%;min-height:72px;color:inherit;font:inherit;background:0 0;border:0;outline:0;font-size:.9375rem}.icon-send{background:var(--accent);color:#1a1a1a;cursor:pointer;border:0;border-radius:50%;justify-content:center;align-items:center;width:32px;height:32px;display:inline-flex}.icon-send:disabled{opacity:.4;cursor:default}.env-meta{flex-wrap:wrap;align-items:center;gap:4px;margin-top:8px;display:flex}.meta-chip{color:var(--faint);cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:6px;max-width:280px;padding:4px 8px;font-size:.7812rem;display:inline-flex}.meta-chip:hover:not(:disabled):not(.readonly){background:var(--raise-2);color:var(--text)}.meta-chip.readonly{cursor:default}.meta-chip .icon{flex:none;width:14px;height:14px}.meta-chip .online-dot{margin:0}.chip-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.chip-caret{opacity:.6}.toggle-chip input{margin:0}.folder-menu{width:360px;max-width:calc(100vw - 16px);padding:6px}.folder-search{box-sizing:border-box;width:100%;margin-bottom:4px}.folder-list{max-height:50vh;overflow:auto}.folder-list .menu-hint{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.rail-card h3{align-items:center}.card-toggle{color:inherit;font:inherit;cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.card-toggle:hover{color:var(--text)}.card-chevron{width:12px;height:12px;transition:transform .15s;transform:rotate(90deg)}.rail-card.collapsed .card-chevron{transform:rotate(0)}.rail-card.collapsed h3{margin-bottom:0}.new-column{box-sizing:border-box;width:100%}.menu-label{flex:1 0 auto;max-width:100%}.menu-hint{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;overflow:hidden}.crumb{white-space:nowrap;text-overflow:ellipsis;align-items:center;gap:4px;max-width:30%;font-size:.8125rem;text-decoration:none;display:inline-flex;overflow:hidden}.crumb:hover{color:var(--text)}.crumb-chev{width:12px;height:12px;transform:rotate(180deg)}.crumb-sep{margin:0 4px}.sync-note{z-index:30;pointer-events:none;background:var(--side);color:var(--muted,var(--faint));border:1px solid var(--line);border-radius:999px;padding:2px 10px;font-size:.857rem;position:absolute;top:6px;left:50%;transform:translate(-50%)}.sync-note.updated{color:var(--accent)}.jobs-badge{background:var(--line);min-width:16px;height:16px;color:var(--text);border-radius:8px;flex:none;justify-content:center;align-items:center;margin-left:4px;padding:0 4px;font-size:.75rem;line-height:1;display:inline-flex}.job-list{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.job{grid-template-columns:auto 1fr;align-items:center;column-gap:8px;display:grid}.job .muted{grid-column:2}.job-command{text-overflow:ellipsis;white-space:nowrap;font-size:.893rem;overflow:hidden}.job-dot{background:var(--faint);border-radius:50%;width:7px;height:7px}.job-dot.on{background:var(--accent)}.job-dot.bad{background:var(--warn)}.retry-note{color:var(--warn);margin:0 0 6px}.activity-hint{border:1px solid var(--accent);border-radius:var(--r);background:color-mix(in srgb, var(--accent) 10%, transparent);margin:6px 0 10px;padding:8px 10px;font-size:.929rem}.scroller,.home-scroll{touch-action:pan-x pan-y}@media (pointer:coarse){input[type=search],input[type=text],input[type=password],input[type=number],select,textarea,.composer textarea,.new-composer textarea{font-size:max(16px,1rem)}}
|