@huynle/hub 0.6.1 → 0.7.1

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