@huynle/hub 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/Dockerfile +3 -0
  3. package/README.md +148 -11
  4. package/package.json +3 -1
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/main.ts +4 -4
  7. package/packages/claude-code/src/wrapper.ts +3 -2
  8. package/packages/cli/dist/hub-cli.js +19334 -7849
  9. package/packages/cli/package.json +11 -0
  10. package/packages/cli/src/acting.ts +70 -0
  11. package/packages/cli/src/args.ts +91 -0
  12. package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
  13. package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
  14. package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
  15. package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
  16. package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
  17. package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
  18. package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
  19. package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
  20. package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
  21. package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
  22. package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
  23. package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
  24. package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
  25. package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
  26. package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
  27. package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
  28. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
  29. package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
  30. package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
  31. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
  32. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
  33. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
  34. package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
  35. package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
  36. package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
  37. package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
  38. package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
  39. package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
  40. package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
  41. package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
  42. package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
  43. package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
  44. package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
  45. package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
  46. package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
  47. package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
  48. package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
  49. package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
  50. package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
  51. package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
  52. package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
  53. package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
  54. package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
  55. package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
  56. package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
  57. package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
  58. package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
  59. package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
  60. package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
  61. package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
  62. package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
  63. package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
  64. package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
  65. package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
  66. package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
  67. package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
  68. package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
  69. package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
  70. package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
  71. package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
  72. package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
  73. package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
  74. package/packages/cli/src/bootstrap/preview/index.ts +5 -0
  75. package/packages/cli/src/bootstrap/preview/links.ts +31 -0
  76. package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
  77. package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
  78. package/packages/cli/src/bootstrap/preview/render.ts +30 -0
  79. package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
  80. package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
  81. package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
  82. package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
  83. package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
  84. package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
  85. package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
  86. package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
  87. package/packages/cli/src/bootstrap/release/README.md +115 -0
  88. package/packages/cli/src/bootstrap/release/assets.ts +39 -0
  89. package/packages/cli/src/bootstrap/release/cli.ts +332 -0
  90. package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
  91. package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
  92. package/packages/cli/src/bootstrap/release/pins.ts +192 -0
  93. package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
  94. package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
  95. package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
  96. package/packages/cli/src/bootstrap/release/verify.ts +88 -0
  97. package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
  98. package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
  99. package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
  100. package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
  101. package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
  102. package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
  103. package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
  104. package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
  105. package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
  106. package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
  107. package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
  108. package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
  109. package/packages/cli/src/cli.ts +814 -0
  110. package/packages/cli/src/credentials.ts +75 -0
  111. package/packages/cli/src/decisions.ts +106 -0
  112. package/packages/cli/src/format.ts +185 -0
  113. package/packages/cli/src/grants.ts +105 -0
  114. package/packages/cli/src/http.ts +86 -0
  115. package/packages/cli/src/launches.ts +157 -0
  116. package/packages/cli/src/login.ts +160 -0
  117. package/packages/cli/src/main.ts +63 -0
  118. package/packages/cli/src/mcp.ts +879 -0
  119. package/packages/cli/src/messaging.ts +108 -0
  120. package/packages/cli/src/node.ts +279 -0
  121. package/packages/cli/src/openshell.ts +166 -0
  122. package/packages/cli/src/setup/commands.ts +809 -0
  123. package/packages/cli/src/setup/contributors.ts +171 -0
  124. package/packages/cli/src/setup/dispatch.ts +74 -0
  125. package/packages/cli/src/setup/launch-driver.ts +109 -0
  126. package/packages/cli/src/setup/runtime.ts +456 -0
  127. package/packages/cli/src/setup/spoke-probe.ts +120 -0
  128. package/packages/cli/src/setup/state.ts +244 -0
  129. package/packages/cli/src/setup/user-bin.ts +39 -0
  130. package/packages/cli/src/spoke-services.ts +109 -0
  131. package/packages/cli/src/watch.ts +28 -0
  132. package/packages/codex/package.json +1 -1
  133. package/packages/codex/src/main.ts +3 -2
  134. package/packages/codex/src/spoke.ts +4 -1
  135. package/packages/codex/src/wrapper.ts +7 -3
  136. package/packages/node/package.json +1 -1
  137. package/packages/node/src/harnesses.ts +39 -4
  138. package/packages/node/src/run.ts +1 -1
  139. package/packages/preset/package.json +1 -1
  140. package/packages/protocol/package.json +1 -1
  141. package/packages/protocol/src/harness-events.ts +2 -2
  142. package/packages/protocol/src/index.ts +70 -7
  143. package/packages/runtime-openshell/package.json +1 -1
  144. package/packages/runtime-sdk/package.json +1 -1
  145. package/packages/sdk/package.json +1 -1
  146. package/packages/sdk/src/client.ts +55 -4
  147. package/packages/sdk/src/errors.ts +1 -0
  148. package/packages/sdk/src/sse.ts +4 -1
  149. package/packages/sdk/src/types.ts +59 -0
  150. package/packages/spoke-sdk/package.json +1 -1
  151. package/packages/spoke-sdk/src/enroll.ts +26 -4
  152. package/src/central/agent/app-tokens.ts +27 -17
  153. package/src/central/agent/archive-inactive.ts +66 -0
  154. package/src/central/agent/directory-routes.ts +155 -12
  155. package/src/central/agent/directory.ts +1 -1
  156. package/src/central/agent/errors.ts +2 -0
  157. package/src/central/agent/event-cursor.ts +49 -0
  158. package/src/central/agent/event-writers.ts +181 -0
  159. package/src/central/agent/events-routes.ts +148 -0
  160. package/src/central/agent/events.ts +277 -0
  161. package/src/central/agent/names.ts +1 -1
  162. package/src/central/agent/read-routes.ts +32 -36
  163. package/src/central/agent/routes.ts +14 -11
  164. package/src/central/agent/watch.ts +65 -174
  165. package/src/central/agent/webhook-routes.ts +10 -2
  166. package/src/central/agent/webhooks.ts +49 -5
  167. package/src/central/auth.ts +127 -1
  168. package/src/central/backend/index.ts +3 -0
  169. package/src/central/backend/types.ts +2 -0
  170. package/src/central/http.ts +116 -32
  171. package/src/central/mcp-http.ts +116 -0
  172. package/src/central/push.ts +14 -1
  173. package/src/central/registry.ts +34 -5
  174. package/src/central/store.ts +60 -0
  175. package/src/config.ts +70 -7
  176. package/src/index.ts +90 -91
  177. package/src/spoke/launchers.ts +109 -3
  178. package/src/spoke/spoke.ts +8 -4
  179. package/src/spoke/timeline.ts +59 -3
  180. package/src/spoke/tracker.ts +59 -6
  181. package/src/status.ts +97 -6
  182. package/web/dist/apple-touch-icon.png +0 -0
  183. package/web/dist/assets/index-Dm8OmkaO.css +1 -0
  184. package/web/dist/assets/index-FUJbj_Du.js +20 -0
  185. package/web/dist/favicon.ico +0 -0
  186. package/web/dist/icon-192.png +0 -0
  187. package/web/dist/icon-512.png +0 -0
  188. package/web/dist/icon-maskable-512.png +0 -0
  189. package/web/dist/icon.svg +5 -5
  190. package/web/dist/index.html +10 -4
  191. package/web/dist/manifest.webmanifest +2 -2
  192. package/web/dist/sw.js +4 -1
  193. package/web/dist/assets/index-D9zMbxSg.css +0 -1
  194. package/web/dist/assets/index-db1z4duA.js +0 -20
@@ -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") this.busy.delete(sessionID)
221
- else this.busy.add(sessionID)
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
- if (data.status.type === "idle") this.busy.delete(sessionID)
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 (event.type === "session.execution.started" && sessionID) this.running.add(sessionID)
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?: { readonly attempt?: number; readonly message?: string }
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?: { attempt?: number; message?: string }
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.error = undefined
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.error = String(data.error?.message ?? "failed")
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
- entry.retry = { ...(attempt !== undefined ? { attempt } : {}), ...(message ? { message } : {}) }
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.error = undefined
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 })) } : {}),
Binary file
@@ -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;c‍ursor: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 *{c‍ursor: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}}