@huynle/hub 0.7.2 → 0.7.4
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 +29 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +7 -7
- package/packages/cli/package.json +1 -1
- package/packages/codex/package.json +1 -1
- package/packages/node/package.json +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/harness-events.ts +2 -2
- package/packages/protocol/src/index.ts +70 -7
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/spoke-sdk/package.json +1 -1
- package/src/central/http.ts +2 -1
- package/src/central/push.ts +14 -1
- package/src/central/registry.ts +34 -5
- package/src/central/store.ts +7 -0
- package/src/spoke/spoke.ts +8 -4
- package/src/spoke/timeline.ts +59 -3
- package/src/spoke/tracker.ts +59 -6
- package/src/status.ts +97 -6
- package/web/dist/assets/index-Dm8OmkaO.css +1 -0
- package/web/dist/assets/index-FUJbj_Du.js +20 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +4 -1
- package/web/dist/assets/index-DWnMhXk9.js +0 -20
- package/web/dist/assets/index-DmUXEnw9.css +0 -1
package/src/spoke/timeline.ts
CHANGED
|
@@ -14,7 +14,8 @@ export interface TimelineItem {
|
|
|
14
14
|
readonly partID?: string
|
|
15
15
|
readonly sessionID: string
|
|
16
16
|
/** "message": a hub message delivered to the session (TimelineMessageItem). */
|
|
17
|
-
|
|
17
|
+
/** "error": the turn failed; "retry": the provider call is being retried (until the message completes or fails). */
|
|
18
|
+
readonly kind: "user" | "assistant" | "reasoning" | "tool" | "message" | "error" | "retry"
|
|
18
19
|
readonly text?: string
|
|
19
20
|
readonly name?: string
|
|
20
21
|
readonly state?: "running" | "done" | "failed"
|
|
@@ -39,12 +40,20 @@ export interface TimelineItem {
|
|
|
39
40
|
readonly direction?: TimelineMessageItem["direction"]
|
|
40
41
|
/** Message rows: who sent it, how it was addressed, what it answers. */
|
|
41
42
|
readonly message?: TimelineMessageItem["message"]
|
|
43
|
+
/** Error and retry rows: the error's type (bounded) and HTTP status (100–599). */
|
|
44
|
+
readonly errorType?: string
|
|
45
|
+
readonly errorStatus?: number
|
|
46
|
+
/** Retry rows: the attempt number and when the next attempt starts (epoch ms). */
|
|
47
|
+
readonly attempt?: number
|
|
48
|
+
readonly next?: number
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/** Parts of one message a position leaves room for (later parts share the last slot). */
|
|
45
52
|
const POS_PARTS = 1000
|
|
46
53
|
const SUBAGENT_TOOLS = new Set(["subagent", "task"])
|
|
47
54
|
const TITLE_MAX = 160
|
|
55
|
+
const ERROR_TEXT_MAX = 1000
|
|
56
|
+
const ERROR_TYPE_MAX = 64
|
|
48
57
|
|
|
49
58
|
const record = (value: unknown): Record<string, any> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {}
|
|
50
59
|
|
|
@@ -55,7 +64,7 @@ export function timelineItems(sessionID: string, messages: readonly unknown[]):
|
|
|
55
64
|
for (const raw of messages) {
|
|
56
65
|
const message = record(raw)
|
|
57
66
|
if (typeof message.id !== "string") continue
|
|
58
|
-
const at =
|
|
67
|
+
const at = millis(message.time?.created)
|
|
59
68
|
// Messages keep the session's order even when one was created earlier than the one before it (a queued prompt).
|
|
60
69
|
const start = Math.max(at === undefined ? -Infinity : at * POS_PARTS, previous + POS_PARTS)
|
|
61
70
|
const first = (previous = Number.isFinite(start) ? start : 0)
|
|
@@ -71,7 +80,12 @@ export function timelineItems(sessionID: string, messages: readonly unknown[]):
|
|
|
71
80
|
if (row) out.push(row)
|
|
72
81
|
continue
|
|
73
82
|
}
|
|
74
|
-
if (message.type !== "assistant"
|
|
83
|
+
if (message.type !== "assistant") continue
|
|
84
|
+
const status = statusRow(sessionID, message, at, pos(Array.isArray(message.content) ? message.content.length : 0))
|
|
85
|
+
if (!Array.isArray(message.content)) {
|
|
86
|
+
if (status) out.push(status)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
75
89
|
let texts = 0
|
|
76
90
|
message.content.forEach((part: any, index: number) => {
|
|
77
91
|
if (!part || typeof part !== "object") return
|
|
@@ -98,10 +112,52 @@ export function timelineItems(sessionID: string, messages: readonly unknown[]):
|
|
|
98
112
|
})
|
|
99
113
|
}
|
|
100
114
|
})
|
|
115
|
+
if (status) out.push(status)
|
|
101
116
|
}
|
|
102
117
|
return out
|
|
103
118
|
}
|
|
104
119
|
|
|
120
|
+
/** Epoch milliseconds of an OpenCode timestamp: a number (ms) or an ISO date string. */
|
|
121
|
+
function millis(value: unknown): number | undefined {
|
|
122
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined
|
|
123
|
+
if (typeof value !== "string") return undefined
|
|
124
|
+
const ms = Date.parse(value)
|
|
125
|
+
return Number.isNaN(ms) ? undefined : ms
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The bounded type and HTTP status of an OpenCode error, and its trimmed message (else `fallback`). */
|
|
129
|
+
function errorFields(raw: unknown, fallback: string): { text: string; errorType?: string; errorStatus?: number } {
|
|
130
|
+
const error = record(raw)
|
|
131
|
+
const message = typeof error.message === "string" ? error.message.trim().slice(0, ERROR_TEXT_MAX) : ""
|
|
132
|
+
const type = typeof error.type === "string" ? error.type.trim().slice(0, ERROR_TYPE_MAX) : ""
|
|
133
|
+
const status = error.status
|
|
134
|
+
return {
|
|
135
|
+
text: message || fallback,
|
|
136
|
+
...(type ? { errorType: type } : {}),
|
|
137
|
+
...(Number.isInteger(status) && status >= 100 && status <= 599 ? { errorStatus: status } : {}),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The status row of an assistant message, after its parts: "error" when the turn failed, "retry" while the
|
|
143
|
+
* provider call is being retried (no error yet and not completed), else none.
|
|
144
|
+
*/
|
|
145
|
+
function statusRow(sessionID: string, message: Record<string, any>, at: number | undefined, pos: number): TimelineItem | undefined {
|
|
146
|
+
const base = { messageID: message.id as string, sessionID, ...(at === undefined ? {} : { at }), pos }
|
|
147
|
+
if (message.error != null) return { key: `${message.id}:error`, kind: "error", ...errorFields(message.error, "failed"), ...base }
|
|
148
|
+
if (message.retry == null || message.time?.completed != null) return undefined
|
|
149
|
+
const retry = record(message.retry)
|
|
150
|
+
const next = millis(retry.at)
|
|
151
|
+
return {
|
|
152
|
+
key: `${message.id}:retry`,
|
|
153
|
+
kind: "retry",
|
|
154
|
+
...errorFields(retry.error, "retrying"),
|
|
155
|
+
...(Number.isInteger(retry.attempt) && retry.attempt >= 0 ? { attempt: retry.attempt } : {}),
|
|
156
|
+
...(next === undefined ? {} : { next }),
|
|
157
|
+
...base,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
105
161
|
/** A message the hub delivered (synthetic, with deliverMetadata): a "message" row; other synthetic messages: none. */
|
|
106
162
|
function hubMessageRow(sessionID: string, message: Record<string, any>, at: number | undefined, pos: number): TimelineMessageItem | undefined {
|
|
107
163
|
const meta = record(message.metadata)
|
package/src/spoke/tracker.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Sessions this OpenCode process knows about, built from the events it sees, plus which of them
|
|
2
2
|
// are exposed to the central server (by mode). Pure: time is passed in.
|
|
3
3
|
import type { Mode } from "../config"
|
|
4
|
-
import { compactActivity, FORWARDED_EVENTS, type JobInfo, type SessionInfo, type TerminalInfo } from "../protocol"
|
|
4
|
+
import { compactActivity, compactRunError, FORWARDED_EVENTS, type CompactRunError, type CompactSessionStatus, type JobInfo, type SessionInfo, type TerminalInfo } from "../protocol"
|
|
5
5
|
|
|
6
6
|
/** A session is "owned" by this process while it viewed or ran it within this window. */
|
|
7
7
|
export const OWNED_WINDOW_MS = 30 * 60 * 1000
|
|
@@ -63,6 +63,21 @@ const JOBS_KNOWN = 1000
|
|
|
63
63
|
|
|
64
64
|
const RUN_END = new Set(["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"])
|
|
65
65
|
|
|
66
|
+
/** Events that end a pending retry: the run started, ended, or its output resumed. */
|
|
67
|
+
const RETRY_END = new Set([
|
|
68
|
+
"session.execution.started",
|
|
69
|
+
...RUN_END,
|
|
70
|
+
"session.text.delta",
|
|
71
|
+
"session.reasoning.delta",
|
|
72
|
+
"session.tool.called",
|
|
73
|
+
"session.tool.input.started",
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
/** Sessions whose pending retry or last failure is remembered (oldest dropped). */
|
|
77
|
+
const ERRORS_KNOWN = 1000
|
|
78
|
+
|
|
79
|
+
type RetryStatus = Extract<CompactSessionStatus, { type: "retry" }>
|
|
80
|
+
|
|
66
81
|
export class SessionTracker {
|
|
67
82
|
private readonly entries = new Map<string, Entry>()
|
|
68
83
|
/** Sessions with a run in progress. */
|
|
@@ -73,6 +88,10 @@ export class SessionTracker {
|
|
|
73
88
|
private readonly busy = new Set<string>()
|
|
74
89
|
/** Shell or terminal ID → its session, while known (running or exited, until deleted). */
|
|
75
90
|
private readonly jobs = new Map<string, { sessionID: string; running: boolean }>()
|
|
91
|
+
/** A session's pending retry (session.retry.scheduled, or a retry status), until its run moves on. */
|
|
92
|
+
private readonly retries = new Map<string, RetryStatus>()
|
|
93
|
+
/** A session's last run failure, until its next run starts. */
|
|
94
|
+
private readonly errors = new Map<string, CompactRunError>()
|
|
76
95
|
|
|
77
96
|
/** Applies an event seen in this process. Returns true when the snapshot may have changed. */
|
|
78
97
|
observe(event: EventLike, now: number): boolean {
|
|
@@ -102,6 +121,8 @@ export class SessionTracker {
|
|
|
102
121
|
case "session.deleted":
|
|
103
122
|
this.running.delete(id)
|
|
104
123
|
this.busy.delete(id)
|
|
124
|
+
this.retries.delete(id)
|
|
125
|
+
this.errors.delete(id)
|
|
105
126
|
for (const [jobID, job] of this.jobs) if (job.sessionID === id) this.jobs.delete(jobID)
|
|
106
127
|
for (const [key, sessionID] of this.waiting) if (sessionID === id) this.waiting.delete(key)
|
|
107
128
|
return this.entries.delete(id)
|
|
@@ -217,22 +238,27 @@ export class SessionTracker {
|
|
|
217
238
|
|
|
218
239
|
/** Replaces a session's status and running jobs with a snapshot (fetched at startup or after a reconnect). */
|
|
219
240
|
seed(sessionID: string, snapshot: { readonly status: "idle" | "busy" | "retry"; readonly jobs: readonly JobInfo[] }): void {
|
|
220
|
-
if (snapshot.status === "idle")
|
|
221
|
-
|
|
241
|
+
if (snapshot.status === "idle") {
|
|
242
|
+
this.busy.delete(sessionID)
|
|
243
|
+
this.retries.delete(sessionID)
|
|
244
|
+
} else this.busy.add(sessionID)
|
|
222
245
|
for (const [jobID, job] of this.jobs) if (job.sessionID === sessionID && job.running) this.jobs.delete(jobID)
|
|
223
246
|
for (const job of snapshot.jobs) this.setJob(job.id, sessionID, job.status === "running")
|
|
224
247
|
}
|
|
225
248
|
|
|
226
249
|
/** Session status and shells/terminals (compacted so the session of an exit is resolved). */
|
|
227
250
|
private trackJobs(event: EventLike): void {
|
|
228
|
-
if (event.type !== "session.status" && !event.type.startsWith("shell.") && !event.type.startsWith("pty.") && !event.type.startsWith("session.shell.")) return
|
|
251
|
+
if (event.type !== "session.status" && event.type !== "session.retry.scheduled" && !event.type.startsWith("shell.") && !event.type.startsWith("pty.") && !event.type.startsWith("session.shell.")) return
|
|
229
252
|
const compact = compactActivity({ type: event.type, data: record(event.data) }, (id) => this.jobSession(id))
|
|
230
253
|
if (!compact) return
|
|
231
254
|
const data = compact.data as Record<string, any>
|
|
232
255
|
const sessionID = data.sessionID as string
|
|
233
256
|
if (compact.type === "session.status") {
|
|
234
|
-
|
|
257
|
+
const status = data.status as CompactSessionStatus
|
|
258
|
+
if (status.type === "idle") this.busy.delete(sessionID)
|
|
235
259
|
else this.busy.add(sessionID)
|
|
260
|
+
if (status.type === "retry") remember(this.retries, sessionID, status)
|
|
261
|
+
else this.retries.delete(sessionID)
|
|
236
262
|
} else if (compact.type.endsWith(".deleted")) this.jobs.delete(data.id)
|
|
237
263
|
else if (typeof data.id === "string" && typeof data.status === "string") this.setJob(data.id, sessionID, data.status === "running")
|
|
238
264
|
}
|
|
@@ -248,7 +274,12 @@ export class SessionTracker {
|
|
|
248
274
|
this.trackJobs(event)
|
|
249
275
|
const data = record(event.data)
|
|
250
276
|
const sessionID = eventSessionID(event)
|
|
251
|
-
if (
|
|
277
|
+
if (sessionID && RETRY_END.has(event.type)) this.retries.delete(sessionID)
|
|
278
|
+
if (sessionID && event.type === "session.execution.failed") remember(this.errors, sessionID, compactRunError(data.error) ?? { message: "failed" })
|
|
279
|
+
if (event.type === "session.execution.started" && sessionID) {
|
|
280
|
+
this.running.add(sessionID)
|
|
281
|
+
this.errors.delete(sessionID)
|
|
282
|
+
}
|
|
252
283
|
else if (RUN_END.has(event.type) && sessionID) {
|
|
253
284
|
this.running.delete(sessionID)
|
|
254
285
|
// OpenCode v2 sends no idle status: a run's end ends a busy status (seeded or reported) too.
|
|
@@ -267,6 +298,21 @@ export class SessionTracker {
|
|
|
267
298
|
return this.running.has(sessionID) || this.busy.has(sessionID)
|
|
268
299
|
}
|
|
269
300
|
|
|
301
|
+
/** The session's pending retry (attempt, reason, next try, error kind), while it waits to retry. */
|
|
302
|
+
retryOf(sessionID: string): RetryStatus | undefined {
|
|
303
|
+
return this.retries.get(sessionID)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The session's last run failure, when no run started since. */
|
|
307
|
+
errorOf(sessionID: string): CompactRunError | undefined {
|
|
308
|
+
return this.errors.get(sessionID)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Sessions whose last run failed (no run started since). */
|
|
312
|
+
failed(): string[] {
|
|
313
|
+
return [...this.errors.keys()]
|
|
314
|
+
}
|
|
315
|
+
|
|
270
316
|
/** A form (question) is open in the session itself. */
|
|
271
317
|
formOpenIn(sessionID: string): boolean {
|
|
272
318
|
for (const [key, id] of this.waiting) if (id === sessionID && key.startsWith("frm ")) return true
|
|
@@ -350,6 +396,13 @@ export class SessionTracker {
|
|
|
350
396
|
}
|
|
351
397
|
}
|
|
352
398
|
|
|
399
|
+
/** Sets a key as the newest, keeping at most ERRORS_KNOWN (oldest dropped). */
|
|
400
|
+
function remember<V>(map: Map<string, V>, key: string, value: V): void {
|
|
401
|
+
map.delete(key)
|
|
402
|
+
map.set(key, value)
|
|
403
|
+
if (map.size > ERRORS_KNOWN) map.delete(map.keys().next().value!)
|
|
404
|
+
}
|
|
405
|
+
|
|
353
406
|
const workspace = (s: SessionDetails) => ({
|
|
354
407
|
...(s.project ? { project: s.project } : {}),
|
|
355
408
|
...(s.projectName ? { projectName: s.projectName } : {}),
|
package/src/status.ts
CHANGED
|
@@ -191,6 +191,53 @@ function formBlocker(form: any): Blocker {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/** A model request OpenCode retries: the attempt, why, when the next one runs (epoch ms) and the error's type. */
|
|
195
|
+
export interface RetryInfo {
|
|
196
|
+
readonly attempt?: number
|
|
197
|
+
readonly message?: string
|
|
198
|
+
readonly next?: number
|
|
199
|
+
readonly errorType?: string
|
|
200
|
+
readonly errorStatus?: number
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Short labels of OpenCode's provider error types (session.execution.failed, retry status). */
|
|
204
|
+
const PROVIDER_ERROR_LABELS: Readonly<Record<string, string>> = {
|
|
205
|
+
"provider.quota": "Quota exceeded",
|
|
206
|
+
"provider.free-limit": "Free limit reached",
|
|
207
|
+
"provider.auth": "Auth failed",
|
|
208
|
+
"provider.rate-limit": "Rate limited",
|
|
209
|
+
"provider.transport": "Network error",
|
|
210
|
+
"provider.internal": "Provider error",
|
|
211
|
+
"provider.timeout": "Timed out",
|
|
212
|
+
"provider.no-route": "No route to model",
|
|
213
|
+
"provider.unknown": "Unknown provider error",
|
|
214
|
+
"provider.content-filter": "Content filtered",
|
|
215
|
+
"provider.invalid-request": "Invalid request",
|
|
216
|
+
"provider.invalid-output": "Invalid output",
|
|
217
|
+
"provider.unsupported-operation": "Unsupported operation",
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A budget or spending limit ("provider.budget-limit", "provider.spend-limit"): like a quota. Not a rate limit. */
|
|
221
|
+
const budgetLike = (type: string): boolean =>
|
|
222
|
+
type !== "provider.rate-limit" && (type.includes("budget") || (type.startsWith("provider.") && type.includes("limit")))
|
|
223
|
+
|
|
224
|
+
/** "Quota exceeded" for "provider.quota"; budget-like types read "Budget limit reached"; undefined when unknown. */
|
|
225
|
+
export function errorTypeLabel(type: string | undefined): string | undefined {
|
|
226
|
+
if (!type) return undefined
|
|
227
|
+
return PROVIDER_ERROR_LABELS[type] ?? (type.startsWith("provider.") && budgetLike(type) ? "Budget limit reached" : undefined)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A failed run's error type the user must act on (quota, budget, auth, routing, unknown provider error). */
|
|
231
|
+
export function providerFailureNeedsUser(type: string | undefined): boolean {
|
|
232
|
+
if (!type) return false
|
|
233
|
+
return ["provider.quota", "provider.free-limit", "provider.auth", "provider.no-route", "provider.unknown"].includes(type) || budgetLike(type)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A retry whose error the user must act on: retrying will not fix a quota, free tier or auth error. */
|
|
237
|
+
export function providerRetryNeedsUser(type: string | undefined): boolean {
|
|
238
|
+
return type === "provider.quota" || type === "provider.auth" || type === "provider.free-limit"
|
|
239
|
+
}
|
|
240
|
+
|
|
194
241
|
export interface SessionStatus {
|
|
195
242
|
readonly id: string
|
|
196
243
|
readonly parentID?: string
|
|
@@ -199,7 +246,10 @@ export interface SessionStatus {
|
|
|
199
246
|
readonly detail?: string
|
|
200
247
|
readonly blockers: ReadonlyArray<Blocker>
|
|
201
248
|
/** OpenCode is retrying the model request (state stays "working"). */
|
|
202
|
-
readonly retry?:
|
|
249
|
+
readonly retry?: RetryInfo
|
|
250
|
+
/** state "error": the error's type ("provider.quota", "tool.execution", …) and HTTP status, when known. */
|
|
251
|
+
readonly errorType?: string
|
|
252
|
+
readonly errorStatus?: number
|
|
203
253
|
/** Shells and terminals of the session: running ones, then the last few that ended (oldest first). */
|
|
204
254
|
readonly jobs?: readonly JobInfo[]
|
|
205
255
|
}
|
|
@@ -216,8 +266,10 @@ interface Entry {
|
|
|
216
266
|
running: boolean
|
|
217
267
|
/** session.status: busy or retry (OpenCode's own view of the session). */
|
|
218
268
|
busy: boolean
|
|
219
|
-
retry?:
|
|
269
|
+
retry?: RetryInfo
|
|
220
270
|
error?: string
|
|
271
|
+
errorType?: string
|
|
272
|
+
errorStatus?: number
|
|
221
273
|
blockers: Blocker[]
|
|
222
274
|
jobs: JobInfo[]
|
|
223
275
|
}
|
|
@@ -258,7 +310,8 @@ export class StatusBoard {
|
|
|
258
310
|
case "session.execution.started":
|
|
259
311
|
return this.update(data.sessionID, (entry) => {
|
|
260
312
|
entry.running = true
|
|
261
|
-
entry.
|
|
313
|
+
entry.retry = undefined
|
|
314
|
+
clearError(entry)
|
|
262
315
|
})
|
|
263
316
|
case "session.execution.succeeded":
|
|
264
317
|
case "session.execution.interrupted":
|
|
@@ -266,8 +319,18 @@ export class StatusBoard {
|
|
|
266
319
|
case "session.execution.failed":
|
|
267
320
|
return this.update(data.sessionID, (entry) => {
|
|
268
321
|
endRun(entry)
|
|
269
|
-
entry
|
|
322
|
+
setError(entry, data.error)
|
|
270
323
|
})
|
|
324
|
+
// Output resumed: the retry succeeded (OpenCode sends no status for it).
|
|
325
|
+
case "session.text.delta":
|
|
326
|
+
case "session.reasoning.delta":
|
|
327
|
+
case "session.tool.called":
|
|
328
|
+
case "session.tool.input.started": {
|
|
329
|
+
const entry = typeof data.sessionID === "string" ? this.sessions.get(data.sessionID) : undefined
|
|
330
|
+
if (!entry?.retry) return false
|
|
331
|
+
entry.retry = undefined
|
|
332
|
+
return true
|
|
333
|
+
}
|
|
271
334
|
case "permission.asked":
|
|
272
335
|
return this.update(data.sessionID, (entry) => {
|
|
273
336
|
if (entry.blockers.some((b) => b.id === data.id)) return
|
|
@@ -288,6 +351,7 @@ export class StatusBoard {
|
|
|
288
351
|
case "session.seed":
|
|
289
352
|
return this.update(data.sessionID, (entry) => {
|
|
290
353
|
setStatus(entry, data.status)
|
|
354
|
+
if (typeof data.error?.message === "string" && data.error.message) setError(entry, data.error)
|
|
291
355
|
entry.jobs = entry.jobs.filter((job) => job.status !== "running")
|
|
292
356
|
for (const job of Array.isArray(data.jobs) ? data.jobs : []) if (isJob(job)) upsertJob(entry, job)
|
|
293
357
|
})
|
|
@@ -401,9 +465,34 @@ function setStatus(entry: Entry, status: any): void {
|
|
|
401
465
|
if (type === "retry") {
|
|
402
466
|
const attempt = num(status.attempt)
|
|
403
467
|
const message = text(status.message, 200)
|
|
404
|
-
|
|
468
|
+
const next = num(status.next)
|
|
469
|
+
const errorType = text(status.errorType, ERROR_TYPE_MAX)
|
|
470
|
+
const errorStatus = num(status.errorStatus)
|
|
471
|
+
entry.retry = {
|
|
472
|
+
...(attempt !== undefined ? { attempt } : {}),
|
|
473
|
+
...(message ? { message } : {}),
|
|
474
|
+
...(next !== undefined ? { next } : {}),
|
|
475
|
+
...(errorType ? { errorType } : {}),
|
|
476
|
+
...(errorStatus !== undefined ? { errorStatus } : {}),
|
|
477
|
+
}
|
|
405
478
|
} else entry.retry = undefined
|
|
406
|
-
if (type === "busy") entry
|
|
479
|
+
if (type === "busy") clearError(entry)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const ERROR_TYPE_MAX = 100
|
|
483
|
+
const ERROR_MESSAGE_MAX = 1000
|
|
484
|
+
|
|
485
|
+
/** A failed run's error: its message, type and status. */
|
|
486
|
+
function setError(entry: Entry, error: any): void {
|
|
487
|
+
entry.error = typeof error?.message === "string" && error.message ? error.message.slice(0, ERROR_MESSAGE_MAX) : "failed"
|
|
488
|
+
entry.errorType = text(error?.type, ERROR_TYPE_MAX)
|
|
489
|
+
entry.errorStatus = num(error?.status)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function clearError(entry: Entry): void {
|
|
493
|
+
entry.error = undefined
|
|
494
|
+
entry.errorType = undefined
|
|
495
|
+
entry.errorStatus = undefined
|
|
407
496
|
}
|
|
408
497
|
|
|
409
498
|
function view(entry: Entry): SessionStatus {
|
|
@@ -418,6 +507,8 @@ function view(entry: Entry): SessionStatus {
|
|
|
418
507
|
title: entry.title,
|
|
419
508
|
state,
|
|
420
509
|
...(detail ? { detail } : {}),
|
|
510
|
+
...(state === "error" && entry.errorType ? { errorType: entry.errorType } : {}),
|
|
511
|
+
...(state === "error" && entry.errorStatus !== undefined ? { errorStatus: entry.errorStatus } : {}),
|
|
421
512
|
blockers: entry.blockers.map((blocker) => structuredClone(blocker)),
|
|
422
513
|
...(retry ? { retry: { ...retry } } : {}),
|
|
423
514
|
...(entry.jobs.length ? { jobs: entry.jobs.map((job) => ({ ...job })) } : {}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg:#f6f7f9;--side:#eef0f3;--raise:#fff;--raise-2:#e8ebef;--line:#d9dde3;--line-2:#c9ced6;--text:#22252a;--muted:#69707b;--faint:#9299a3;--accent:#0e7268;--accent-ink:#fff;--ok:#16834a;--warn:#9a5b08;--err:#b42318;--add:#16834a;--del:#b42318;--violet:#8954c7;--orange:#b96508;--blue:#2769b4;--mark:#22252a;--shadow:#1018281f;--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:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#101214;--side:#171a1e;--raise:#1b1f24;--raise-2:#22272d;--line:#2d333b;--line-2:#3a424d;--text:#e7e5e0;--muted:#9ca3ad;--faint:#6f7782;--accent:#3ad3bf;--accent-ink:#04201c;--ok:#50d890;--warn:#efb851;--err:#ff8e82;--add:#50d890;--del:#ff8e82;--violet:#b68cff;--orange:#f2a340;--blue:#64a8ef;--mark:#e7e5e0;--shadow:#0006}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#101214;--side:#171a1e;--raise:#1b1f24;--raise-2:#22272d;--line:#2d333b;--line-2:#3a424d;--text:#e7e5e0;--muted:#9ca3ad;--faint:#6f7782;--accent:#3ad3bf;--accent-ink:#04201c;--ok:#50d890;--warn:#efb851;--err:#ff8e82;--add:#50d890;--del:#ff8e82;--violet:#b68cff;--orange:#f2a340;--blue:#64a8ef;--mark:#e7e5e0;--shadow:#0006}}*{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:color-mix(in srgb, var(--accent) 88%, var(--text))}.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{border-color:color-mix(in srgb, var(--warn) 45%, var(--line));color:var(--warn);border-width:0 0 1px;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 color-mix(in srgb, var(--warn) 14%, transparent)}.status.error .status-dot{background:var(--err)}.spinner{border:1.6px solid color-mix(in srgb, var(--accent) 27%, transparent);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{background:color-mix(in srgb, var(--warn) 16%, var(--bg));color:var(--warn);text-align:center;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(--mark);--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);min-width:200px;max-width:320px;box-shadow:0 12px 32px var(--shadow);border-radius:10px;padding:4px;position:fixed}.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);box-shadow:0 8px 24px var(--shadow);border-radius:10px;align-items:center;gap:10px;max-width:92vw;padding:8px 12px;display:flex;position:fixed;transform:translate(-50%)}.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:420px;padding:2px 10px;font-size:.75rem;display:inline-flex;overflow:hidden}.chip-server .place-where{text-overflow:ellipsis;min-width:0;overflow:hidden}.session-place{color:var(--muted);border-bottom:1px solid var(--line);white-space:nowrap;flex:none;align-items:center;gap:4px;padding:4px 12px;font-size:.75rem;display:flex;overflow:hidden}.session-place .place-machine{color:var(--text,inherit);font-weight:500}.session-place .place-where,.session-place .place-branch{text-overflow:ellipsis;min-width:0;overflow:hidden}.session-place .icon{flex:none;width:12px;height:12px;margin-left:4px}.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;color:var(--violet);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:color-mix(in srgb, var(--violet) 72%, 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(--orange)}.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{color:var(--ok);background:color-mix(in srgb, var(--accent) 9%, var(--raise-2));border:1px solid color-mix(in srgb, var(--accent) 24%, 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:var(--orange);font-weight:600}.md del{color:var(--faint)}.md mark{background:color-mix(in srgb, var(--orange) 18%, transparent);color:var(--orange);-webkit-box-decoration-break:clone;box-decoration-break:clone;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(--ok);text-decoration:underline;-webkit-text-decoration-color:color-mix(in srgb, var(--ok) 40%, transparent);text-decoration-color:color-mix(in srgb, var(--ok) 40%, transparent);text-underline-offset:2px;overflow-wrap:anywhere}.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:color-mix(in srgb, var(--text) 3%, transparent)}.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:var(--faint);font-style:italic}.tok-string{color:var(--ok)}.tok-number{color:var(--orange)}.tok-keyword{color:var(--violet)}.tok-type,.tok-attr{color:var(--orange)}.tok-fn,.tok-meta{color:var(--blue)}.tok-prop,.tok-tag{color:var(--err)}.tok-add{color:var(--add);background:color-mix(in srgb, var(--add) 10%, transparent)}.tok-del{color:var(--del);background:color-mix(in srgb, var(--del) 10%, transparent)}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:var(--line-2)}.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}.appearance-theme{justify-content:space-between;align-items:center;gap:12px;display:flex}.theme-toggle{border:1px solid var(--line);background:var(--raise-2);border-radius:999px;gap:2px;padding:3px;display:inline-flex}.theme-choice{min-height:28px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;align-items:center;gap:5px;padding:2px 9px;font-size:.75rem;display:inline-flex}.theme-choice:hover{color:var(--text)}.theme-choice[aria-checked=true]{background:var(--raise);color:var(--text);box-shadow:0 1px 3px var(--shadow)}.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);width:32px;height:32px;color:var(--accent-ink);cursor:pointer;border:0;border-radius:50%;justify-content:center;align-items:center;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)}}.banner.session-error{border-width:0 0 1px;border-color:color-mix(in srgb, var(--err) 45%, var(--line));background:color-mix(in srgb, var(--err) 10%, var(--bg));color:var(--text);border-radius:0;justify-content:flex-start;align-items:flex-start;margin:0}.session-error-mark,.turn-error-mark{color:var(--err);flex:none;font-weight:600;line-height:1.4}.session-error-body,.turn-error-body{flex-direction:column;gap:2px;min-width:0;display:flex}.session-error-head,.turn-error-head{color:var(--err);flex-wrap:wrap;align-items:baseline;gap:4px 8px;display:flex}.session-error-status,.turn-error-status{border:1px solid;border-radius:999px;padding:0 6px;font-size:.75rem}.session-error-where{font-size:.8125rem}.session-error-message,.turn-error-message{overflow-wrap:anywhere;white-space:pre-wrap;max-height:8em;font-size:.8125rem;overflow:auto}.msg.turn-error{border:1px solid color-mix(in srgb, var(--err) 45%, var(--line));border-radius:var(--r);background:color-mix(in srgb, var(--err) 8%, transparent);align-items:flex-start;gap:8px;padding:8px 12px;display:flex}.msg.turn-retry{color:var(--muted);overflow-wrap:anywhere;align-items:baseline;gap:8px;margin:6px 0;font-size:.8125rem;display:flex}.msg.turn-retry.waiting{color:var(--warn)}.turn-retry-mark{flex:none}.error-badge{background:var(--err);min-width:16px;height:16px;color:var(--bg);border-radius:8px;flex:none;justify-content:center;align-items:center;margin-left:4px;font-size:.75rem;font-weight:700;line-height:1;display:inline-flex}.col-now.error,.home-item-meta.error{color:var(--err)}.rail-error{color:var(--err);border:1px solid color-mix(in srgb, var(--err) 45%, var(--line));border-radius:var(--r);overflow-wrap:anywhere;margin:0 0 10px;padding:6px 10px}@media (max-width:760px){.banner.session-error{padding:8px 14px}.session-error-message{max-height:5.5em}}
|