samagotchi 0.2.0

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 (243) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE +21 -0
  4. data/README.md +126 -0
  5. data/bin/chi +1140 -0
  6. data/docs/architecture.md +299 -0
  7. data/docs/cli.md +490 -0
  8. data/docs/configuration.md +494 -0
  9. data/docs/desktop.md +97 -0
  10. data/docs/guardrails.md +218 -0
  11. data/docs/hooks.md +309 -0
  12. data/docs/internals/background-tasks.md +26 -0
  13. data/docs/internals/context-telemetry.md +36 -0
  14. data/docs/internals/gemma4-contract.md +23 -0
  15. data/docs/internals/tool-guardrails.md +45 -0
  16. data/docs/memory.md +85 -0
  17. data/docs/plugins.md +819 -0
  18. data/docs/releasing.md +135 -0
  19. data/docs/sessions.md +155 -0
  20. data/lib/samagotchi/bridge/bounded_queue.rb +70 -0
  21. data/lib/samagotchi/bridge/card_store.rb +126 -0
  22. data/lib/samagotchi/bridge/event_id.rb +25 -0
  23. data/lib/samagotchi/bridge/ring_buffer.rb +63 -0
  24. data/lib/samagotchi/bridge/sse_writer.rb +248 -0
  25. data/lib/samagotchi/bridge/turn_accumulator.rb +189 -0
  26. data/lib/samagotchi/bridge.rb +993 -0
  27. data/lib/samagotchi/bridge_client/event_stream.rb +158 -0
  28. data/lib/samagotchi/bridge_client/sse_parser.rb +51 -0
  29. data/lib/samagotchi/bridge_client.rb +330 -0
  30. data/lib/samagotchi/bundle_needs.rb +97 -0
  31. data/lib/samagotchi/bundles/btw/manifest.yml +10 -0
  32. data/lib/samagotchi/bundles/btw/plugin.rb +100 -0
  33. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +82 -0
  34. data/lib/samagotchi/bundles/guardrails/guardrails.md +14 -0
  35. data/lib/samagotchi/bundles/guardrails/manifest.yml +8 -0
  36. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +210 -0
  37. data/lib/samagotchi/bundles/known-names/known_names.md +3 -0
  38. data/lib/samagotchi/bundles/known-names/manifest.yml +14 -0
  39. data/lib/samagotchi/bundles/loop-guard/manifest.yml +10 -0
  40. data/lib/samagotchi/bundles/loop-guard/plugin.rb +158 -0
  41. data/lib/samagotchi/bundles/mcp/manifest.yml +11 -0
  42. data/lib/samagotchi/bundles/mcp/plugin.rb +631 -0
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +149 -0
  44. data/lib/samagotchi/bundles/system/delegated.md +10 -0
  45. data/lib/samagotchi/bundles/system/identity.md +7 -0
  46. data/lib/samagotchi/bundles/system/manifest.yml +11 -0
  47. data/lib/samagotchi/bundles/system/memory_guide.md +107 -0
  48. data/lib/samagotchi/bundles/system/self_map.md +55 -0
  49. data/lib/samagotchi/cancellation_controller.rb +78 -0
  50. data/lib/samagotchi/client.rb +429 -0
  51. data/lib/samagotchi/commands/registry.rb +112 -0
  52. data/lib/samagotchi/config.rb +910 -0
  53. data/lib/samagotchi/context_note.rb +77 -0
  54. data/lib/samagotchi/context_quote.rb +21 -0
  55. data/lib/samagotchi/context_usage.rb +66 -0
  56. data/lib/samagotchi/context_window.rb +76 -0
  57. data/lib/samagotchi/debug_log.rb +110 -0
  58. data/lib/samagotchi/desktop/macos/App.swift +102 -0
  59. data/lib/samagotchi/desktop/macos/ChiRunner.swift +201 -0
  60. data/lib/samagotchi/desktop/macos/Hotkey.swift +42 -0
  61. data/lib/samagotchi/desktop/macos/Info.plist.erb +42 -0
  62. data/lib/samagotchi/desktop/macos/Panel.swift +383 -0
  63. data/lib/samagotchi/desktop/macos.rb +255 -0
  64. data/lib/samagotchi/desktop.rb +21 -0
  65. data/lib/samagotchi/desktop_command.rb +143 -0
  66. data/lib/samagotchi/engine.rb +2807 -0
  67. data/lib/samagotchi/guardrails/approval.rb +125 -0
  68. data/lib/samagotchi/guardrails/approvals.rb +177 -0
  69. data/lib/samagotchi/guardrails/context.rb +71 -0
  70. data/lib/samagotchi/guardrails/gate.rb +125 -0
  71. data/lib/samagotchi/guardrails/load_failures.rb +46 -0
  72. data/lib/samagotchi/guardrails/protected_paths.rb +77 -0
  73. data/lib/samagotchi/guardrails/rules.rb +199 -0
  74. data/lib/samagotchi/guardrails/targets.rb +119 -0
  75. data/lib/samagotchi/guardrails/verdict.rb +134 -0
  76. data/lib/samagotchi/guardrails.rb +18 -0
  77. data/lib/samagotchi/hooks/bundle_loader.rb +158 -0
  78. data/lib/samagotchi/hooks/loader.rb +162 -0
  79. data/lib/samagotchi/hooks/registry.rb +261 -0
  80. data/lib/samagotchi/hooks.rb +30 -0
  81. data/lib/samagotchi/host_registry.rb +315 -0
  82. data/lib/samagotchi/idle_client.rb +147 -0
  83. data/lib/samagotchi/idle_recap.rb +549 -0
  84. data/lib/samagotchi/idle_reminders.rb +101 -0
  85. data/lib/samagotchi/idle_scheduler.rb +76 -0
  86. data/lib/samagotchi/image_store.rb +393 -0
  87. data/lib/samagotchi/installed_gem.rb +38 -0
  88. data/lib/samagotchi/kernel_loop.rb +1017 -0
  89. data/lib/samagotchi/launch_mode.rb +34 -0
  90. data/lib/samagotchi/llm/backend.rb +28 -0
  91. data/lib/samagotchi/llm/chat_loop.rb +450 -0
  92. data/lib/samagotchi/llm/errors.rb +329 -0
  93. data/lib/samagotchi/llm/http.rb +412 -0
  94. data/lib/samagotchi/llm/model_result.rb +72 -0
  95. data/lib/samagotchi/llm/native_backend.rb +50 -0
  96. data/lib/samagotchi/llm/native_tool_normalizer.rb +277 -0
  97. data/lib/samagotchi/llm/openai_chat.rb +403 -0
  98. data/lib/samagotchi/llm/usage.rb +79 -0
  99. data/lib/samagotchi/log.rb +200 -0
  100. data/lib/samagotchi/log_line.rb +127 -0
  101. data/lib/samagotchi/log_path.rb +31 -0
  102. data/lib/samagotchi/log_subscriber.rb +163 -0
  103. data/lib/samagotchi/memory_bundle/builder.rb +364 -0
  104. data/lib/samagotchi/memory_bundle/index_updater.rb +123 -0
  105. data/lib/samagotchi/memory_bundle/installer.rb +528 -0
  106. data/lib/samagotchi/memory_bundle/listing.rb +72 -0
  107. data/lib/samagotchi/memory_bundle/manifest.rb +225 -0
  108. data/lib/samagotchi/memory_bundle/merger.rb +52 -0
  109. data/lib/samagotchi/memory_bundle/placeholder.rb +37 -0
  110. data/lib/samagotchi/memory_bundle/provenance.rb +257 -0
  111. data/lib/samagotchi/memory_bundle/source.rb +153 -0
  112. data/lib/samagotchi/memory_bundle/status.rb +107 -0
  113. data/lib/samagotchi/memory_bundle/system_bundle.rb +161 -0
  114. data/lib/samagotchi/memory_bundle/uninstaller.rb +128 -0
  115. data/lib/samagotchi/memory_bundle.rb +17 -0
  116. data/lib/samagotchi/memory_paths.rb +101 -0
  117. data/lib/samagotchi/model_overlay.rb +53 -0
  118. data/lib/samagotchi/model_profile.rb +309 -0
  119. data/lib/samagotchi/muted_memories.rb +66 -0
  120. data/lib/samagotchi/note_command.rb +163 -0
  121. data/lib/samagotchi/output_formatter.rb +100 -0
  122. data/lib/samagotchi/owner_lock.rb +110 -0
  123. data/lib/samagotchi/pending_input_queue.rb +48 -0
  124. data/lib/samagotchi/plugin/api.rb +362 -0
  125. data/lib/samagotchi/plugin/context.rb +193 -0
  126. data/lib/samagotchi/plugin/loader.rb +126 -0
  127. data/lib/samagotchi/plugin/service.rb +117 -0
  128. data/lib/samagotchi/plugin/sessions.rb +150 -0
  129. data/lib/samagotchi/plugin/side_question.rb +60 -0
  130. data/lib/samagotchi/plugin/tool_result.rb +24 -0
  131. data/lib/samagotchi/project_scope.rb +25 -0
  132. data/lib/samagotchi/prompt.rb +119 -0
  133. data/lib/samagotchi/prompt_literal_guard.rb +70 -0
  134. data/lib/samagotchi/recap_store.rb +92 -0
  135. data/lib/samagotchi/reminder_store.rb +165 -0
  136. data/lib/samagotchi/self_report.rb +195 -0
  137. data/lib/samagotchi/send_command.rb +170 -0
  138. data/lib/samagotchi/served_model.rb +32 -0
  139. data/lib/samagotchi/session.rb +508 -0
  140. data/lib/samagotchi/session_commands.rb +527 -0
  141. data/lib/samagotchi/session_delete_command.rb +105 -0
  142. data/lib/samagotchi/session_manager.rb +1049 -0
  143. data/lib/samagotchi/session_metrics.rb +466 -0
  144. data/lib/samagotchi/session_observer.rb +117 -0
  145. data/lib/samagotchi/terminal_ui/attach_launcher.rb +118 -0
  146. data/lib/samagotchi/terminal_ui/attached_loop.rb +1037 -0
  147. data/lib/samagotchi/terminal_ui/attached_view.rb +264 -0
  148. data/lib/samagotchi/terminal_ui/event_renderer.rb +192 -0
  149. data/lib/samagotchi/terminal_ui/formatting.rb +291 -0
  150. data/lib/samagotchi/terminal_ui/image_input.rb +36 -0
  151. data/lib/samagotchi/terminal_ui/input_support.rb +324 -0
  152. data/lib/samagotchi/terminal_ui/legacy_surface.rb +111 -0
  153. data/lib/samagotchi/terminal_ui/line_reader.rb +113 -0
  154. data/lib/samagotchi/terminal_ui/live_region.rb +36 -0
  155. data/lib/samagotchi/terminal_ui/plain_surface.rb +51 -0
  156. data/lib/samagotchi/terminal_ui/question_prompt.rb +153 -0
  157. data/lib/samagotchi/terminal_ui/question_slot.rb +131 -0
  158. data/lib/samagotchi/terminal_ui/reline_seam.rb +216 -0
  159. data/lib/samagotchi/terminal_ui/repl_input.rb +138 -0
  160. data/lib/samagotchi/terminal_ui/screen.rb +316 -0
  161. data/lib/samagotchi/terminal_ui/surface.rb +47 -0
  162. data/lib/samagotchi/terminal_ui/thinking_line.rb +101 -0
  163. data/lib/samagotchi/terminal_ui.rb +1992 -0
  164. data/lib/samagotchi/thinking_ticker.rb +110 -0
  165. data/lib/samagotchi/thought_stream_splitter.rb +149 -0
  166. data/lib/samagotchi/token_usage.rb +88 -0
  167. data/lib/samagotchi/tool_activity.rb +216 -0
  168. data/lib/samagotchi/tool_call_parser.rb +637 -0
  169. data/lib/samagotchi/tool_declarations.rb +561 -0
  170. data/lib/samagotchi/tool_runner.rb +211 -0
  171. data/lib/samagotchi/tools/args.rb +259 -0
  172. data/lib/samagotchi/tools/ask_user_question.rb +152 -0
  173. data/lib/samagotchi/tools/builtins.rb +122 -0
  174. data/lib/samagotchi/tools/cancel_reminder.rb +21 -0
  175. data/lib/samagotchi/tools/delegate.rb +167 -0
  176. data/lib/samagotchi/tools/delegate_result.rb +53 -0
  177. data/lib/samagotchi/tools/delegate_wait.rb +153 -0
  178. data/lib/samagotchi/tools/edit.rb +155 -0
  179. data/lib/samagotchi/tools/execute.rb +214 -0
  180. data/lib/samagotchi/tools/list_reminders.rb +20 -0
  181. data/lib/samagotchi/tools/list_sessions.rb +74 -0
  182. data/lib/samagotchi/tools/memory.rb +256 -0
  183. data/lib/samagotchi/tools/output_guardrails.rb +93 -0
  184. data/lib/samagotchi/tools/peers.rb +18 -0
  185. data/lib/samagotchi/tools/read.rb +182 -0
  186. data/lib/samagotchi/tools/register_reminder.rb +53 -0
  187. data/lib/samagotchi/tools/registry.rb +60 -0
  188. data/lib/samagotchi/tools/send_note.rb +49 -0
  189. data/lib/samagotchi/tools/task_create.rb +29 -0
  190. data/lib/samagotchi/tools/task_get.rb +39 -0
  191. data/lib/samagotchi/tools/task_list.rb +43 -0
  192. data/lib/samagotchi/tools/task_runtime.rb +311 -0
  193. data/lib/samagotchi/tools/task_stop.rb +29 -0
  194. data/lib/samagotchi/tools/task_wait.rb +104 -0
  195. data/lib/samagotchi/tools/tool_path.rb +18 -0
  196. data/lib/samagotchi/tools/web_fetch.rb +163 -0
  197. data/lib/samagotchi/tools/write.rb +26 -0
  198. data/lib/samagotchi/turn_flow.rb +242 -0
  199. data/lib/samagotchi/turn_note.rb +76 -0
  200. data/lib/samagotchi/turn_tally.rb +101 -0
  201. data/lib/samagotchi/version.rb +7 -0
  202. data/lib/samagotchi/vision_context.rb +132 -0
  203. data/lib/samagotchi/vision_support.rb +109 -0
  204. data/lib/samagotchi/web/app.rb +1349 -0
  205. data/lib/samagotchi/web/markdown_renderer.rb +107 -0
  206. data/lib/samagotchi/web/message_parts.rb +169 -0
  207. data/lib/samagotchi/web/public/activity.js +100 -0
  208. data/lib/samagotchi/web/public/annotations.js +67 -0
  209. data/lib/samagotchi/web/public/app.js +2382 -0
  210. data/lib/samagotchi/web/public/card.js +74 -0
  211. data/lib/samagotchi/web/public/chat_view.js +360 -0
  212. data/lib/samagotchi/web/public/chunk_router.js +25 -0
  213. data/lib/samagotchi/web/public/command_complete.js +39 -0
  214. data/lib/samagotchi/web/public/composer_size.js +19 -0
  215. data/lib/samagotchi/web/public/copy.js +142 -0
  216. data/lib/samagotchi/web/public/ctx.js +35 -0
  217. data/lib/samagotchi/web/public/data.js +256 -0
  218. data/lib/samagotchi/web/public/format.js +232 -0
  219. data/lib/samagotchi/web/public/hold.js +78 -0
  220. data/lib/samagotchi/web/public/images.js +77 -0
  221. data/lib/samagotchi/web/public/index.html +568 -0
  222. data/lib/samagotchi/web/public/init_row.js +60 -0
  223. data/lib/samagotchi/web/public/model_pick.js +23 -0
  224. data/lib/samagotchi/web/public/question_card.js +100 -0
  225. data/lib/samagotchi/web/public/route.js +17 -0
  226. data/lib/samagotchi/web/public/scope.js +36 -0
  227. data/lib/samagotchi/web/public/scroll.js +24 -0
  228. data/lib/samagotchi/web/public/sentences.js +88 -0
  229. data/lib/samagotchi/web/public/sessions_list.js +60 -0
  230. data/lib/samagotchi/web/public/strip.js +25 -0
  231. data/lib/samagotchi/web/public/tally.js +37 -0
  232. data/lib/samagotchi/web/public/thinking_ticker.js +79 -0
  233. data/lib/samagotchi/web/public/timing.js +185 -0
  234. data/lib/samagotchi/web/public/turn_events.js +209 -0
  235. data/lib/samagotchi/web/public/turn_model.js +204 -0
  236. data/lib/samagotchi/web/public/turn_view.js +587 -0
  237. data/lib/samagotchi/web/server.rb +183 -0
  238. data/lib/samagotchi/web/session_hub.rb +329 -0
  239. data/lib/samagotchi/web/session_summary.rb +85 -0
  240. data/lib/samagotchi/worker.rb +635 -0
  241. data/lib/samagotchi/worker_idle_exit.rb +87 -0
  242. data/lib/samagotchi.rb +12 -0
  243. metadata +374 -0
@@ -0,0 +1,568 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Chi</title>
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <style>
8
+ :root{--bg:#0b0d11;--bg-panel:#151821;--bg-inset:#0f1115;--bg-raised:#22262f;--bg-hover:#1b1f27;--border:#1f232b;--border-strong:#2d333f;--text:#e6e8eb;--text-bright:#f0f3f6;--text-2:#c9d1d9;--text-3:#9aa4b2;--text-muted:#8b949e;--text-faint:#6e7681;--accent:#7aa5ff;--accent-strong:#1f6feb;--accent-bg:#1b2336;--accent-ring:rgba(122,165,255,.18);--primary:#2a68d1;--ok:#3fb950;--ok-text:#7ee787;--ok-bg:#1a2e1a;--warn:#d29922;--warn-text:#e3b341;--warn-bright:#f2cc60;--warn-bg:#2b2108;--warn-border:#6b5416;--danger:#f85149;--danger-bg:#da3633;--danger-text:#ff7b72;--danger-bg-dim:#3d1f1f;--think:#8957e5;--think-text:#d2a8ff;--accent-border:#223150;--accent-tint:color-mix(in srgb,var(--accent) 7%,transparent);
9
+ --glass-bg:color-mix(in srgb,var(--bg-panel) 70%,transparent);--glass-border:rgba(255,255,255,.08);--glass-highlight:rgba(255,255,255,.05);--glass-shadow:0 8px 30px rgba(0,0,0,.35)}
10
+ *{box-sizing:border-box;margin:0;padding:0}
11
+ body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:radial-gradient(1100px 700px at 8% 0%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 70%),radial-gradient(900px 700px at 100% 100%,color-mix(in srgb,var(--think) 9%,transparent),transparent 70%),var(--bg);background-attachment:fixed;color:var(--text);height:100vh;display:flex;flex-direction:column}
12
+ /* Glass: the composer card, the strip cards, the all-sessions cards and the question cards (by selector, so app.js is untouched). */
13
+ #dock,#topStrip .card,#allList .card,.bubble.question,.bubble.plugin-card{background:var(--glass-bg);-webkit-backdrop-filter:blur(18px) saturate(1.35);backdrop-filter:blur(18px) saturate(1.35);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
14
+ #topStrip .card:hover,#allList .card:hover{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);border-color:rgba(255,255,255,.14)}
15
+ #topStrip .card.active,#allList .card.active{background:color-mix(in srgb,var(--accent-bg) 75%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,transparent)}
16
+ @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){#dock,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel)}.bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel))}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel))}}
17
+ @media (prefers-reduced-transparency:reduce){#dock,#topStrip .card,#allList .card,#allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}.bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}}
18
+ body.no-glass #dock,body.no-glass #topStrip .card,body.no-glass #allList .card,body.no-glass #allHead input{background:var(--bg-panel);-webkit-backdrop-filter:none;backdrop-filter:none}
19
+ body.no-glass .bubble.question{background:color-mix(in srgb,var(--warn) 7%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
20
+ body.no-glass .bubble.plugin-card{background:color-mix(in srgb,var(--accent) 6%,var(--bg-panel));-webkit-backdrop-filter:none;backdrop-filter:none}
21
+ button,input,textarea,select{font:inherit}
22
+ button{background:var(--primary);border:0;color:#fff;padding:8px 12px;border-radius:8px;cursor:pointer;transition:filter .12s ease,background .12s ease}
23
+ button:hover:not(:disabled){filter:brightness(1.12)}
24
+ button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
25
+ button.ghost{background:var(--bg-raised);color:var(--text-2)}
26
+ button.danger{background:var(--danger-bg)}
27
+ button:disabled{opacity:.5;cursor:not-allowed}
28
+ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:auto;background:transparent;min-height:0}
29
+ #topStrip{display:grid;grid-template-columns:repeat(var(--strip-cols,3),minmax(0,440px)) 150px;gap:12px;min-height:88px;align-content:start;transition:opacity 280ms ease, transform 280ms ease}
30
+ #topStrip:empty{display:grid}
31
+ #center{flex:1;display:flex;flex-direction:column;min-height:45vh;background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
32
+ #stage{flex:1;display:flex;flex-direction:column;position:relative;min-height:160px;background:var(--bg);overflow:hidden}
33
+ #firstPeek{display:none;padding:8px 12px;background:var(--accent-bg);color:var(--text);border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
34
+ #firstPeek.visible{display:block}
35
+ #composer #cancelBtn{order:2;background:var(--danger-bg)}
36
+ #history{flex:1;overflow:auto;padding:16px max(16px,calc((100% - 960px)/2));display:flex;flex-direction:column;gap:12px;background:var(--bg);min-height:160px}
37
+ /* No child shrinks to fit: a held (overflow:hidden) answer bubble would, clipped under the dock with nothing to scroll. */
38
+ #history>*{flex-shrink:0}
39
+ .bubble{background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;padding:12px 14px;white-space:pre-wrap;word-break:break-word;font-size:14px;line-height:1.5}
40
+ .bubble.output,.bubble.user{font-size:15px;line-height:1.55}
41
+ .bubble.output{border-left:3px solid var(--accent-strong)}
42
+ .bubble.output.streaming{border-left-color:var(--accent);opacity:.95}
43
+ /* A generation opens its answer bubble empty: hidden until text streams (the thinking box shows progress meanwhile). */
44
+ .bubble.output.streaming:empty{display:none}
45
+ /* The answer's pop: the promoted bubble is held at the narration box's height (inline max-height, scrolled to its tail) until the rendered markdown lands, then grows to its content while the content fades in; both classes go 400 ms later. */
46
+ .bubble.output.boxed{overflow:hidden;transition:max-height 360ms ease}
47
+ .bubble.output.revealing>:not(.copy-btn){animation:fadeIn 240ms ease}
48
+ @media (prefers-reduced-motion:reduce){.bubble.output.boxed{transition:none}.bubble.output.revealing>:not(.copy-btn){animation:none}}
49
+ .turn-timing{margin-top:8px;color:var(--text-muted);font-size:11px;font-variant-numeric:tabular-nums}
50
+ .turn-timing.live{color:var(--accent)}
51
+ .bubble.output.markdown{white-space:normal}
52
+ .bubble.output.markdown pre{overflow:auto;padding:10px;border-radius:8px;background:var(--bg-inset)}
53
+ .bubble.output.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
54
+ .bubble.output.markdown :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:1px 5px;color:var(--text)}
55
+ .bubble.output.markdown pre{font-size:13px;line-height:1.45;border:1px solid var(--border)}
56
+ .bubble.output.markdown h1,.bubble.output.markdown h2,.bubble.output.markdown h3,.bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6{font-weight:600;line-height:1.3;margin:18px 0 8px;color:var(--text-bright)}
57
+ .bubble.output.markdown h1{font-size:19px}
58
+ .bubble.output.markdown h2{font-size:17px}
59
+ .bubble.output.markdown h3{font-size:15.5px}
60
+ .bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6{font-size:15px;color:var(--text-2)}
61
+ .bubble.output.markdown h1:first-child,.bubble.output.markdown h2:first-child,.bubble.output.markdown h3:first-child,.bubble.output.markdown>.copy-btn:first-child+:is(h1,h2,h3){margin-top:2px}
62
+ .bubble.output.markdown strong{color:var(--text-bright)}
63
+ .bubble.output.markdown hr{border:0;border-top:1px solid var(--border);margin:14px 0}
64
+ .bubble.output.markdown img{max-width:100%}
65
+ .bubble.output.markdown p,.bubble.output.markdown ul,.bubble.output.markdown ol,.bubble.output.markdown blockquote,.bubble.output.markdown table,.bubble.output.markdown pre{margin:0 0 12px}
66
+ .bubble.output.markdown p:last-child,.bubble.output.markdown ul:last-child,.bubble.output.markdown ol:last-child,.bubble.output.markdown blockquote:last-child,.bubble.output.markdown table:last-child,.bubble.output.markdown pre:last-child{margin-bottom:0}
67
+ .bubble.output.markdown ul,.bubble.output.markdown ol{padding-left:1.5em}
68
+ .bubble.output.markdown li+li{margin-top:4px}
69
+ .bubble.output.markdown li>ul,.bubble.output.markdown li>ol{margin:4px 0 0}
70
+ .bubble.output.markdown blockquote{border-left:3px solid var(--border-strong);padding-left:12px;color:var(--text-muted)}
71
+ .bubble.output.markdown table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
72
+ .bubble.output.markdown th,.bubble.output.markdown td{border:1px solid var(--border-strong);padding:5px 8px;font-size:14px}
73
+ .bubble.output.markdown th{background:var(--bg-inset);font-weight:600}
74
+ /* Plugins' slow setup (chi.init, init_row.js): a quiet line per task over the composer. */
75
+ #initRow{display:flex;flex-direction:column;gap:2px;padding:6px 12px 2px;font-size:12px;line-height:1.4;color:var(--text-3)}
76
+ #initRow[hidden]{display:none}
77
+ .init-task{display:flex;align-items:center;gap:7px;min-width:0;transition:opacity .4s ease}
78
+ .init-task.fading{opacity:0}
79
+ .init-bundle{flex:none;padding:0 5px;border:1px solid var(--border-strong);border-radius:4px;font-size:11px;color:var(--text-muted)}
80
+ .init-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
81
+ .init-task.done .init-text{color:var(--ok-text)}
82
+ .init-mark{flex:none;width:10px;text-align:center;color:var(--ok)}
83
+ .init-spin{flex:none;width:10px;height:10px;border:1.5px solid var(--border-strong);border-top-color:var(--accent);border-radius:50%;animation:init-spin .8s linear infinite}
84
+ @keyframes init-spin{to{transform:rotate(360deg)}}
85
+ @media (prefers-reduced-motion:reduce){.init-spin{animation:none;border-top-color:var(--accent)}.init-task{transition:none}}
86
+ .markdown-warning{margin:0 16px 4px;padding:8px 10px;border:1px solid var(--warn);border-radius:8px;background:var(--warn-bg);color:var(--warn-bright);font-size:12px;line-height:1.4}
87
+ .bubble.user{border-left:3px solid var(--accent);background:var(--accent-bg);align-self:flex-end;max-width:85%;margin-left:auto;display:flex;align-items:center;justify-content:space-between;gap:10px}
88
+ .bubble.user.queued{border-left-style:dashed;opacity:.8}
89
+ .bubble.user.steered{border-left-style:dashed;border-left-color:var(--accent);opacity:.9}
90
+ .bubble.user.failed{border-left-color:var(--danger);opacity:.8}
91
+ .bubble.user .state-badge{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:2px 7px;border-radius:999px;background:rgba(255,255,255,.08);color:var(--text);font-size:11px;line-height:1.2;white-space:nowrap;flex-shrink:0}
92
+ .bubble.user .state-badge::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.8;box-shadow:0 0 0 1px rgba(255,255,255,.18)}
93
+ .bubble.user.queued .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
94
+ .bubble.user.steered .state-badge::before{background:var(--accent);box-shadow:0 0 0 1px var(--accent-ring)}
95
+ .bubble.user.failed .state-badge::before{background:var(--danger);box-shadow:0 0 0 1px color-mix(in srgb,var(--danger) 40%,transparent)}
96
+ .bubble.user .origin-label{font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}
97
+ .bubble.user .user-message{flex:1;min-width:0}
98
+ .bubble.user blockquote{border-left:3px solid var(--border-strong);padding-left:10px;color:var(--text-3);margin:2px 0 6px}
99
+ .bubble.user blockquote:last-child{margin-bottom:0}
100
+ .bubble.tool{border-left:3px solid var(--ok);background:var(--bg-panel);font-size:13px;color:var(--text-2)}
101
+ .bubble.cancel{border-left:3px solid var(--danger);background:color-mix(in srgb,var(--danger) 8%,var(--bg-panel));color:var(--danger-text);font-size:13px}
102
+ .bubble.recap{border-left:3px solid var(--think-text);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));font-size:13px}
103
+ .bubble.recap summary{cursor:pointer;color:var(--think-text);font-size:12px}
104
+ .bubble.recap .recap-body{white-space:pre-wrap;margin-top:6px}
105
+ .bubble.recap.stale{opacity:.7;border-left-style:dashed}
106
+ .bubble.activity{border-left:3px solid var(--accent);background:var(--bg-panel);padding:8px 12px}
107
+ .bubble.activity>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
108
+ .bubble.activity>summary::-webkit-details-marker{display:none}
109
+ .bubble.activity>summary::before{content:"▸ ";color:var(--accent)}
110
+ .bubble.activity[open]>summary::before{content:"▾ "}
111
+ .activity-body{margin-top:6px}
112
+ .activity-row{display:flex;gap:8px;align-items:baseline;font-size:13px;color:var(--text-2);padding:5px 2px;border-top:1px solid var(--border)}
113
+ .activity-row:first-child{border-top:none}
114
+ .activity-status{font-size:11px;min-width:52px;text-transform:uppercase;letter-spacing:.03em;font-weight:600}
115
+ .activity-status.running{color:var(--warn)}
116
+ .activity-status.ok{color:var(--ok)}
117
+ .activity-status.error{color:var(--danger)}
118
+ .activity-tool{color:var(--accent);font-weight:600}
119
+ .activity-params{color:var(--text-muted);word-break:break-all}
120
+ .activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
121
+ .activity-output{margin:4px 0 2px 60px;font-size:12px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word;background:var(--bg);border-radius:6px;padding:6px 8px;max-height:160px;overflow:auto}
122
+ /* A row's status and tool name never wrap (a phone broke them letter by
123
+ letter); the params take what is left. The output goes on its own line
124
+ under the call instead of a column beside it: on a desktop it lines up
125
+ under the tool name (the status is 52 px + the 8 px gap), on a phone it
126
+ takes the full width and the params break inside the rest of the first
127
+ line rather than drop to a line of their own. */
128
+ .activity-row{flex-wrap:wrap}
129
+ .activity-status,.activity-tool{flex-shrink:0;white-space:nowrap}
130
+ .activity-params{min-width:0}
131
+ .activity-output{flex:1 1 100%;min-width:0}
132
+ @media(max-width:600px){
133
+ .activity-params{flex:1 1 0}
134
+ .activity-output{margin-left:0}
135
+ }
136
+ .bubble.thinking{border-left:3px solid var(--think);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));padding:8px 12px}
137
+ .bubble.thinking:not([open]),.bubble.activity:not([open]){padding:3px 10px;background:transparent;border-color:transparent;align-self:flex-start;border-radius:6px;transition:background .12s ease}
138
+ .bubble.thinking:not([open]){border-left-color:var(--think)}
139
+ .bubble.activity:not([open]){border-left-color:var(--accent)}
140
+ .bubble.thinking:not([open]):hover,.bubble.activity:not([open]):hover{background:var(--bg-panel)}
141
+ .bubble.thinking:not([open])>summary,.bubble.activity:not([open])>summary{font-size:12px;font-weight:500}
142
+ .bubble.thinking>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;font-style:italic;user-select:none;list-style:none}
143
+ .bubble.thinking>summary::-webkit-details-marker{display:none}
144
+ .bubble.thinking>summary::before{content:"▸ ";color:var(--think)}
145
+ .bubble.thinking[open]>summary::before{content:"▾ "}
146
+ .thinking-body{margin-top:6px;font-size:13px;color:var(--text-3);white-space:pre-wrap;word-break:break-word;font-style:italic;max-height:240px;overflow:auto}
147
+ /* Turn view (web.turn_view): one block per turn, its steps as collapsed rows, the live one open at the bottom. */
148
+ .turn-work{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;background:var(--bg-panel);padding:8px 12px;font-size:14px}
149
+ .turn-work>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
150
+ .turn-work>summary::-webkit-details-marker{display:none}
151
+ .turn-work>summary::before{content:"▸ ";color:var(--accent)}
152
+ .turn-work[open]>summary::before{content:"▾ "}
153
+ .turn-work:not([open]){padding:3px 10px;background:transparent;border-color:transparent;border-left-color:var(--accent);align-self:flex-start;border-radius:6px;transition:background .12s ease}
154
+ .turn-work:not([open]):hover{background:var(--bg-panel)}
155
+ .turn-work:not([open])>summary{font-size:12px;font-weight:500}
156
+ .gen{margin-top:8px;padding-top:6px;border-top:1px solid var(--border)}
157
+ .gen>summary{cursor:pointer;color:var(--text-muted);font-size:12px;font-weight:500;user-select:none;list-style:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
158
+ .gen>summary::-webkit-details-marker{display:none}
159
+ .gen>summary::before{content:"▸ ";color:var(--text-faint)}
160
+ .gen[open]>summary::before{content:"▾ "}
161
+ .gen.live{border-top-color:transparent;background:var(--accent-tint);border-radius:8px;padding:6px 9px 8px;margin-left:-9px;margin-right:-9px}
162
+ .gen.live>summary{color:var(--accent);white-space:normal}
163
+ /* The live step's marker: a pulsing accent dot in place of the chevron (the block's own left bar stays the only one). */
164
+ .gen.live>summary::before{content:"";display:inline-block;width:7px;height:7px;margin:0 8px 1px 2px;border-radius:50%;background:var(--accent);vertical-align:middle;animation:step-pulse 1.6s ease-in-out infinite}
165
+ @keyframes step-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 55%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 0%,transparent)}}
166
+ @media (prefers-reduced-motion:reduce){.gen.live>summary::before{animation:none}}
167
+ /* A step's thinking is a one-line ticker: the newest complete sentence,
168
+ ellipsed to one line (the collapsed .bubble.thinking look, scoped to .gen
169
+ so it never reaches .bubble.thinking after promote). The prefix is a
170
+ .thinking-tag; the sentence a text node. A subtle fade on change, removed
171
+ at close (reload parity) and off under prefers-reduced-motion. */
172
+ .gen details.thinking>summary{cursor:pointer;color:var(--text-muted);font-size:12px;font-weight:500;font-style:italic;user-select:none;list-style:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
173
+ .gen details.thinking>summary::-webkit-details-marker{display:none}
174
+ .gen details.thinking>summary::before{content:"▸ ";color:var(--think)}
175
+ .gen details.thinking[open]>summary::before{content:"▾ "}
176
+ .gen details.thinking .thinking-tag{color:var(--think-text)}
177
+ .gen details.thinking summary{transition:opacity 120ms ease}
178
+ .gen details.thinking summary.thinking-fade{animation:thinking-fade 120ms ease-out}
179
+ @keyframes thinking-fade{from{opacity:.4}to{opacity:1}}
180
+ @media (prefers-reduced-motion:reduce){.gen details.thinking summary.thinking-fade{animation:none}}
181
+ .gen-text{margin-top:6px;white-space:pre-wrap;word-break:break-word;font-size:15px;line-height:1.55}
182
+ /* The live step's narration box: the complete sentences so far, one per line, three visual lines high (3 × the 1.55 line-height), scrolled to the newest; each new line fades in. */
183
+ .gen-text.streaming>.narration{max-height:4.65em;overflow:auto;scrollbar-width:thin;white-space:normal}
184
+ .narration>.sentence{animation:fadeIn 160ms ease}
185
+ @media (prefers-reduced-motion:reduce){.narration>.sentence{animation:none}}
186
+ .gen:not(.live) .gen-text{font-size:14px;color:var(--text-2)}
187
+ .gen-text:empty{display:none}
188
+ .gen .activity-body{margin-top:6px}
189
+ /* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. */
190
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)){border:0;padding:0;background:transparent}
191
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body))>summary,.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen>summary{display:none}
192
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen{margin:0;padding:0;border:0;background:transparent}
193
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen-text{margin:0;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:12px 14px;opacity:.95}
194
+ /* A question card asks for input like the composer card, so it wears the same glass (the recipe above); only its warm tint is its own. */
195
+ .bubble.question{border-left:3px solid var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--glass-bg));padding:10px 12px}
196
+ /* Collapsed (resolved) question: one line, like the thinking and activity blocks.
197
+ The left bar keeps --warn here; the existing answered/cancelled/denied classes
198
+ (which apply whether open or not) recolour it once resolved. */
199
+ .bubble.question:not([open]){padding:3px 10px;background:transparent;border-color:transparent;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;align-self:flex-start;border-radius:6px;transition:background .12s ease}
200
+ .bubble.question:not([open]){border-left-color:var(--warn)}
201
+ .bubble.question:not([open]):hover{background:var(--bg-panel)}
202
+ .bubble.question:not([open])>summary{font-size:12px;font-weight:500}
203
+ .bubble.question>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
204
+ .bubble.question>summary::-webkit-details-marker{display:none}
205
+ .bubble.question>summary::before{content:"▸ ";color:var(--warn)}
206
+ .bubble.question[open]>summary::before{content:"▾ "}
207
+ .bubble.command{border-left:3px solid var(--text-faint);background:var(--bg-panel);font-size:13px}
208
+ .bubble.note{border-left:3px dotted var(--text-faint);background:transparent;font-size:12px;color:var(--text-muted);opacity:.85}
209
+ .note-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);margin-bottom:2px}
210
+ .note-text{white-space:pre-wrap;max-height:8em;overflow:auto}
211
+ .bubble.command.busy{border-left-color:var(--warn)}
212
+ .bubble.command.failed{border-left-color:var(--danger)}
213
+ .command-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-3)}
214
+ .command-output{margin:6px 0 0;white-space:pre-wrap;font-size:12px;color:var(--text-2)}
215
+ .bubble.continue{border-left:3px solid var(--accent);background:var(--bg-panel);padding:10px 12px}
216
+ .bubble.continue.resolved{border-left-color:var(--text-muted);opacity:.8}
217
+ .continue-text{font-size:14px;color:var(--text);margin-bottom:6px}
218
+ .continue-context{font-size:12px;color:var(--text-3);margin-bottom:8px;white-space:pre-wrap}
219
+ .continue-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
220
+ .continue-reason{flex:1;min-width:140px;background:var(--bg-inset);border:1px solid var(--border-strong);border-radius:8px;color:var(--text);padding:6px 8px}
221
+ .bubble.question.submitting{opacity:.7}
222
+ .bubble.question.answered{border-left-color:var(--ok)}
223
+ .bubble.question.cancelled{border-left-color:var(--text-muted);opacity:.75}
224
+ /* A settled card stops floating: the highlight stays, the drop shadow goes. */
225
+ .bubble.question.answered,.bubble.question.cancelled{box-shadow:inset 0 1px 0 var(--glass-highlight)}
226
+ .question-text{font-size:14px;color:var(--text);margin-bottom:8px;white-space:pre-wrap}
227
+ .question-options{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}
228
+ .question-option{display:flex;gap:8px;align-items:center;font-size:13px;color:var(--text-2);padding:6px 8px;border:1px solid var(--border);border-radius:8px;cursor:pointer}
229
+ .question-option:hover{background:var(--bg-hover)}
230
+ .question-option.selected{border-color:var(--ok);color:var(--text)}
231
+ .question-option input{margin:0;accent-color:var(--warn)}
232
+ .question-freeform{width:100%;box-sizing:border-box;background:var(--bg-inset);border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:8px;font-size:13px;margin-bottom:8px}
233
+ .question-error{color:var(--danger);font-size:12px;margin-bottom:8px}
234
+ .question-error.hidden{display:none}
235
+ .question-result{margin-top:6px;font-size:12px;color:var(--ok);font-weight:600}
236
+ .bubble.question.cancelled .question-result{color:var(--text-muted)}
237
+ .question-submit{align-self:flex-end;font-size:13px}
238
+ .question-dismiss{font-size:13px;margin-left:8px}
239
+ .bubble.question.approval{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--glass-bg))}
240
+ .bubble.question.approval.answered:not(.denied){border-left-color:var(--ok)}
241
+ .bubble.question.approval:not([open]){background:transparent}
242
+ .bubble.question.approval:not([open]):hover{background:var(--bg-panel)}
243
+ .bubble.question.answered>summary::before{color:var(--ok)}
244
+ .bubble.question.cancelled>summary::before{color:var(--text-muted)}
245
+ .bubble.question.approval.answered.denied>summary::before{color:var(--danger)}
246
+ .bubble.question.approval.answered.denied{border-left-color:var(--danger)}
247
+ .bubble.question.approval.answered.denied .question-result{color:var(--danger)}
248
+ .bubble.question.approval.answered.denied .question-option.selected{border-color:var(--danger)}
249
+ .approval-details{margin-bottom:8px;font-size:13px;color:var(--text-2)}
250
+ .approval-tool{font-weight:700;color:var(--text);margin-bottom:4px}
251
+ .approval-what{margin:0 0 6px;padding:8px 10px;background:var(--bg-inset);border:1px solid var(--warn-border);border-radius:6px;white-space:pre-wrap;word-break:break-all;color:var(--warn-bright);font-size:12.5px}
252
+ .approval-where,.approval-why{font-size:12px;color:var(--text-muted);word-break:break-all}
253
+ .approval-why{color:var(--warn-text)}
254
+ .approval-label{color:var(--text-faint)}
255
+ .bubble.guardrail-warning{border-left:3px solid var(--danger);background:color-mix(in srgb,var(--danger) 8%,var(--bg-panel));color:var(--danger-text);font-size:13px;padding:8px 12px;white-space:pre-wrap}
256
+ .bubble.hook-notice{border-left:3px solid var(--text-faint);background:var(--bg-panel);color:var(--text-3);font-size:13px;padding:8px 12px;white-space:pre-wrap}
257
+ .bubble.hook-notice.warn{border-left-color:var(--warn);color:var(--warn-text)}
258
+ .gen .hook-notice{font-size:13px;color:var(--text-3);padding:5px 2px 5px 8px;border-top:1px solid var(--border);border-left:2px solid var(--text-faint);white-space:pre-wrap;word-break:break-word}
259
+ .gen .activity-body>.hook-notice:first-child{border-top:none}
260
+ .gen .hook-notice.warn{border-left-color:var(--warn);color:var(--warn-text)}
261
+ /* A plugin's card (card.js): the question card's glass with a cool tint; its actions are commands the session runs. */
262
+ .bubble.plugin-card{border-left:3px solid var(--accent);background:color-mix(in srgb,var(--accent) 6%,var(--glass-bg));padding:10px 12px;transition:box-shadow .3s ease}
263
+ .bubble.plugin-card.warn{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--glass-bg))}
264
+ .bubble.plugin-card.card-updated{box-shadow:inset 0 1px 0 var(--glass-highlight),0 0 0 1px var(--accent-ring),var(--glass-shadow)}
265
+ .card-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}
266
+ .card-title{font-size:14px;font-weight:600;color:var(--text-bright)}
267
+ .card-source{font-size:11px;color:var(--text-faint);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
268
+ .card-body{font-size:13.5px;color:var(--text-2);white-space:normal;line-height:1.5}
269
+ .card-body p{margin:0 0 6px}.card-body p:last-child{margin-bottom:0}
270
+ .card-body ul,.card-body ol{margin:0 0 6px 20px}
271
+ .card-body pre{margin:0;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;padding:8px 10px}
272
+ .card-body>pre:only-child{background:transparent;border:none;padding:0;font-family:inherit;font-size:inherit}
273
+ .card-body :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:1px 5px;font-size:.88em}
274
+ .card-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
275
+ .card-action{font-size:12.5px;padding:4px 10px}
276
+ .card-error{color:var(--danger);font-size:12px;margin-top:6px}
277
+ .card-error.hidden{display:none}
278
+ /* In a step: a row, lighter (no float), like the step's notices. */
279
+ .gen .bubble.plugin-card{margin:6px 0 4px;box-shadow:inset 0 1px 0 var(--glass-highlight);padding:8px 10px}
280
+ .hint{color:var(--text-muted);font-size:13px;padding:12px}
281
+ #annotateBtn{position:fixed;z-index:20;font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px rgba(0,0,0,.5)}
282
+ #annotateBtn[hidden]{display:none}
283
+ /* Copy buttons (copy.js): top-right in an answer and a code block, left of a prompt (its right edge is the page's); shown on hover or focus, always (dim) on touch. They hold no text: "copied ✓" is CSS content. */
284
+ .bubble.output,.bubble.user{position:relative}
285
+ .copy-btn{position:absolute;top:6px;right:6px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-muted);font-size:11px;line-height:1;white-space:nowrap;user-select:none;-webkit-user-select:none;opacity:0;transition:opacity .12s ease,color .12s ease}
286
+ .copy-btn svg{display:block}
287
+ .copy-btn:hover:not(:disabled){filter:none;color:var(--text-bright)}
288
+ .bubble:hover>.copy-btn,.code-wrap:hover>.copy-btn,.copy-btn:focus-visible,.copy-btn[data-copied]{opacity:1}
289
+ .copy-btn[data-copied],.copy-btn[data-copied]:hover:not(:disabled){color:var(--ok-text)}
290
+ .copy-btn[data-copied] svg{display:none}
291
+ .copy-btn[data-copied]::after{content:"copied \2713";padding:0 2px}
292
+ .bubble.user>.copy-btn{right:calc(100% + 4px);top:4px;background:transparent;border-color:transparent}
293
+ .bubble.user>.copy-btn:hover{background:var(--bg-raised);border-color:var(--border-strong)}
294
+ .code-wrap{position:relative;margin:0 0 12px}
295
+ .code-wrap:last-child{margin-bottom:0}
296
+ .bubble.output.markdown .code-wrap>pre{margin:0}
297
+ .code-wrap>.copy-btn{top:5px;right:5px}
298
+ @media (hover:none){.copy-btn{opacity:.55}}
299
+ /* The dock is the composer card: the text field, a toolbar row, the info bar as its footer. */
300
+ #dock{display:flex;flex-direction:column;flex-shrink:0;margin:0 12px 12px;border-radius:16px;position:relative;transition:border-color .12s ease,box-shadow .12s ease}
301
+ /* In a session the card floats over the history, so the glass blurs it; the history's bottom padding follows the card (--dock-h, set from app.js). */
302
+ /* The session panel lets the page's glow through (the history's own layers go clear), so the card and its footer read as glass over it, not as a flat box on black. */
303
+ body.has-session #center{background:color-mix(in srgb,var(--bg) 35%,transparent);border-color:var(--glass-border);position:relative}
304
+ body.has-session #stage,body.has-session #history{background:transparent}
305
+ @media (prefers-reduced-transparency:reduce){body.has-session #center{background:var(--bg);border-color:var(--border)}}
306
+ body.no-glass.has-session #center{background:var(--bg);border-color:var(--border)}
307
+ body.has-session #dock{position:absolute;left:16px;right:16px;bottom:16px;margin:0;z-index:3}
308
+ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
309
+ #dock:focus-within{border-color:color-mix(in srgb,var(--accent) 70%,transparent);box-shadow:0 0 0 3px var(--accent-ring),var(--glass-shadow)}
310
+ #composer{display:flex;flex-direction:column;gap:8px;padding:14px 14px 10px}
311
+ #composer .compose-col{flex:1;display:flex;flex-direction:column;gap:6px;min-width:0}
312
+ #composer.dropping{outline:2px dashed var(--accent);outline-offset:-4px}
313
+ #chips{display:flex;flex-wrap:wrap;gap:6px}
314
+ #chips[hidden]{display:none}
315
+ .chip{display:inline-flex;align-items:center;gap:6px;background:var(--accent-bg);border:1px solid var(--border-strong);border-radius:8px;padding:3px 4px 3px 3px;font-size:12px;max-width:220px}
316
+ .chip img{width:32px;height:32px;object-fit:cover;border-radius:5px}
317
+ .chip .chip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
318
+ .chip .chip-remove{background:transparent;color:var(--text-3);padding:0 6px;min-height:0;font-size:14px}
319
+ .thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
320
+ .thumb{max-height:120px;max-width:200px;border-radius:6px;border:1px solid var(--border-strong);cursor:zoom-in;object-fit:contain;background:var(--bg)}
321
+ .activity-row .thumbs{flex-basis:100%}
322
+ #lightbox{position:fixed;inset:0;z-index:50;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;cursor:zoom-out}
323
+ #lightbox[hidden]{display:none}
324
+ #lightbox img{max-width:96vw;max-height:94vh;border-radius:6px}
325
+ #composer textarea{flex:1 1 auto;min-height:72px;resize:none;background:transparent;border:0;color:var(--text);padding:2px;border-radius:0;font-size:15px;line-height:1.45}
326
+ #composeGrip{position:absolute;left:0;right:0;top:-5px;height:10px;cursor:ns-resize;z-index:2;touch-action:none}
327
+ #composeGrip::after{content:"";position:absolute;left:50%;top:3px;width:36px;height:4px;margin-left:-18px;border-radius:2px;background:var(--border-strong);opacity:0;transition:opacity .12s ease}
328
+ #composer:hover #composeGrip::after,#composeGrip.dragging::after{opacity:1}
329
+ #composeGrip:hover::after,#composeGrip.dragging::after{background:var(--accent)}
330
+ #composer textarea::placeholder{color:var(--text-faint)}
331
+ /* Key hint: its line is always reserved, so focusing never shifts the layout. */
332
+ #composeHint{order:1;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:11px;line-height:14px;color:var(--text-faint);padding-left:2px;opacity:0;transition:opacity .12s ease;user-select:none}
333
+ #composer:focus-within #composeHint:not(:empty){opacity:1}
334
+ #composer textarea:focus{outline:none}
335
+ /* The toolbar row: model picker · hint · Cancel · Send (the JS display toggles decide what shows). */
336
+ #composer .actions-col{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px}
337
+ #composer .actions-col button{white-space:nowrap;font-size:12px;padding:0 12px;height:28px;min-height:28px;line-height:28px}
338
+ #composer .actions-col .split{order:3;display:flex;gap:4px;margin-left:auto}
339
+ #composer #actionBtn{min-width:64px;font-weight:600}
340
+ /* The model picker (start page only): reads as a label, "host:model ▾", left of Start; a native select under a drawn caret. */
341
+ /* A select is as wide as its widest option: a hidden copy of the chosen name (data-value) sizes the box to that name instead, and the select fills it. */
342
+ .model-pick{order:0;position:relative;display:inline-block;max-width:min(50%,380px);height:28px;border-radius:8px;overflow:hidden;color:var(--text-muted);font-size:12px;transition:background .12s ease,color .12s ease}
343
+ .model-pick[hidden],body.has-session .model-pick{display:none}
344
+ .model-pick:hover,.model-pick:focus-within{background:color-mix(in srgb,var(--bg-hover) 75%,transparent);color:var(--text-2)}
345
+ .model-pick::before{content:attr(data-value);display:block;visibility:hidden;padding:0 22px 0 8px;white-space:pre;line-height:28px}
346
+ .model-pick::after{content:"";position:absolute;right:9px;top:50%;width:5px;height:5px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:translateY(-70%) rotate(45deg);pointer-events:none}
347
+ .model-pick select{position:absolute;inset:0;appearance:none;-webkit-appearance:none;width:100%;height:28px;padding:0 22px 0 8px;border:0;border-radius:8px;background:transparent;color:inherit;font-size:12px;line-height:28px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;color-scheme:dark}
348
+ .model-pick select:focus{outline:none}
349
+ .model-pick select:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
350
+ .model-pick option{color:var(--text);background:var(--bg-panel)}
351
+ @media(max-width:600px){.model-pick{max-width:60%}}
352
+ /* The / autocomplete: a list over the composer card's top edge. It wears the glass edge and shadow, but opaque:
353
+ inside the dock's own backdrop-filter a second blur sees nothing, and the history would show through its text. */
354
+ #commandList{position:absolute;left:10px;width:min(640px,calc(100% - 20px));bottom:calc(100% + 8px);max-height:min(260px,40vh);overflow-y:auto;border-radius:12px;padding:4px;z-index:20;display:flex;flex-direction:column;gap:1px;background:var(--bg-panel);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
355
+ #commandList[hidden]{display:none}
356
+ #commandList .command-option{display:flex;align-items:baseline;gap:10px;padding:7px 10px;border-radius:8px;cursor:pointer;min-width:0}
357
+ #commandList .command-option.active{background:color-mix(in srgb,var(--accent-bg) 85%,transparent)}
358
+ #commandList .command-name{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;font-weight:600;color:var(--text);white-space:nowrap}
359
+ #commandList .command-desc{flex:1;min-width:0;font-size:13px;color:var(--text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
360
+ #commandList .command-note{font-size:11px;color:var(--text-faint);white-space:nowrap}
361
+ @media(max-width:600px){#commandList{left:6px;width:calc(100% - 12px)}#commandList .command-option{flex-wrap:wrap;row-gap:0;padding:8px 10px}#commandList .command-desc{flex-basis:100%;order:3}}
362
+ #infoBar{display:flex;padding:8px 14px 10px;border-top:1px solid var(--glass-border);background:transparent;border-radius:0 0 16px 16px;color:var(--text-2);gap:10px;align-items:flex-start;flex-wrap:wrap;flex-shrink:0;transition:opacity 280ms ease}
363
+ #infoBar.visible{display:flex}
364
+ #infoBar .info{flex:1;min-width:0;font-size:13px;line-height:1.4;word-break:break-word;display:flex;flex-direction:column;gap:2px}
365
+ #infoBar .meta{font-size:11px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
366
+ #infoBar .meta .id{font-family:ui-monospace,monospace;font-weight:700;font-size:12px;color:var(--accent)}
367
+ #infoBar .meta .copy-attach{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong)}
368
+ #infoBar .meta .copy-attach:hover{color:#fff;border-color:var(--border-strong)}
369
+ #infoBar .meta .status{font-size:11px;padding:2px 6px;border-radius:999px}
370
+ #infoBar .meta .dir{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);word-break:break-all}
371
+ #infoBar .meta .model{font-size:11px;padding:2px 6px;border-radius:6px;background:var(--bg-hover);color:var(--text-3)}
372
+ #infoBar .meta .model.served-mismatch{background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border)}
373
+ #infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;line-height:1.4;max-height:2.8em}
374
+ #infoBar .preview-line .preview, #infoBar .preview-line .mem{word-break:break-word}
375
+ #infoBar .mem{font-size:12px;color:var(--text-muted)}
376
+ #infoBar button{padding:4px 12px;border-radius:8px;font-size:12px;font-weight:600}
377
+ #infoBar #infoStopBtn{background:var(--bg-raised);color:var(--warn-text);border:1px solid var(--border-strong)}
378
+ #infoBar #infoStopBtn:hover{background:var(--warn-bg);border-color:var(--warn-border)}
379
+ #infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
380
+ #infoBar #infoDeleteBtn:hover{color:#fff;background:var(--danger-bg);border-color:var(--danger-bg)}
381
+ /* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable) and the first-message line, the strip card's title, goes. */
382
+ @media(max-width:600px){
383
+ /* The key hint: cut off at this width, and phones have no Shift+Enter. */
384
+ #composeHint{display:none}
385
+ #infoBar{flex-wrap:nowrap;align-items:center;gap:6px;padding:6px 10px 8px}
386
+ #infoBar .preview-line{display:none}
387
+ #infoBar .meta{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
388
+ #infoBar .meta::-webkit-scrollbar{display:none}
389
+ #infoBar .meta>*{flex-shrink:0}
390
+ #infoBar .meta .id{order:-2}
391
+ #infoBar .meta .status{order:-1}
392
+ #infoBar .meta .dir{word-break:normal}
393
+ #infoBar button{padding:4px 8px}
394
+ }
395
+ #empty{position:absolute;inset:0;display:grid;place-items:center;color:var(--text-muted);padding:24px;text-align:center;background:var(--bg);z-index:1}
396
+ #empty.hidden{display:none !important}
397
+ /* Start page (no session): the composer alone, centered; no info bar. */
398
+ body:not(.has-session) #center{justify-content:safe center;background:transparent}
399
+ body:not(.has-session) #stage{flex:0 0 auto;min-height:0;background:transparent}
400
+ body:not(.has-session) #history,body:not(.has-session) #markdownWarning{display:none}
401
+ body:not(.has-session) #empty{position:static;background:transparent;padding:0}
402
+ body:not(.has-session) #dock{width:min(720px,calc(100% - 32px));margin:0 auto}
403
+ body:not(.has-session) #infoBar{display:none}
404
+ #notice{width:min(720px,calc(100% - 32px));margin:0 auto 12px;padding:8px 12px;border:1px solid var(--border-strong);border-radius:8px;color:var(--text-muted);font-size:13px;text-align:center}
405
+ body.has-session #notice{display:none}
406
+ #topBar{display:flex;align-items:center;gap:12px;margin-bottom:-6px}
407
+ #newBtn.brand{background:transparent;border:0;padding:2px 4px;min-height:0;display:inline-flex;align-items:baseline;gap:10px;color:var(--text)}
408
+ #newBtn .brand-name{font-size:18px;font-weight:700;letter-spacing:.02em;color:var(--accent)}
409
+ #newBtn .brand-new{font-size:12px;color:var(--text-muted);display:none}
410
+ body.has-session #newBtn .brand-new{display:inline}
411
+ #newBtn:hover .brand-new{color:var(--text-2)}
412
+ .strip-side{display:flex;flex-direction:column;gap:6px;min-width:0}
413
+ /* The scope chip: "<project> · all", or "all projects" and where a new chat starts (start page only). */
414
+ #scope{display:inline-flex;align-items:center;gap:8px;min-width:0;font-size:12px;color:var(--text-muted)}
415
+ #scope[hidden]{display:none}
416
+ #scope .scope-name{flex-shrink:0;padding:2px 9px;border:1px solid var(--border-strong);border-radius:999px;color:var(--text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40vw}
417
+ #scope .scope-name.error{color:var(--warn-text);border-color:var(--warn-border)}
418
+ #scope .scope-all{color:var(--text-muted);text-decoration:none}
419
+ #scope .scope-all:hover{color:var(--accent);text-decoration:underline}
420
+ #scope .scope-where{display:inline-flex;gap:5px;min-width:0;white-space:nowrap;font-size:11px}
421
+ /* The path cuts on the left, so its last folders stay in view. */
422
+ #scope .scope-where .path{overflow:hidden;text-overflow:ellipsis;direction:rtl;min-width:0;font-family:ui-monospace,monospace}
423
+ body.has-session #scope .scope-where{display:none}
424
+ /* A phone has no room for both; the hidden strip's pill (count, live) wins. */
425
+ @media(max-width:600px){body.strip-hidden #scope .scope-where{display:none}}
426
+ /* A phone's top bar stays one row: "chi +" (the rest of "+ new chat" goes),
427
+ tighter gaps, nothing wraps, and the "← project" link cuts with an
428
+ ellipsis before the pill does. */
429
+ @media(max-width:600px){
430
+ #topBar{gap:8px}
431
+ #scope{gap:6px}
432
+ #newBtn .brand-new-word{display:none}
433
+ #newBtn .brand-new,#scope .scope-back{white-space:nowrap}
434
+ #scope .scope-back{min-width:0;overflow:hidden;text-overflow:ellipsis}
435
+ #stripShow{padding:3px 8px}
436
+ #newBtn .brand-new{font-size:16px}
437
+ }
438
+ /* The ▾ glyph draws tiny at the pill's 11 px: at 20 px it is about as tall
439
+ as the pill text's capitals (phone and desktop alike). */
440
+ #stripShow .pill-arrow{font-size:20px;line-height:0;vertical-align:-3px}
441
+ .card .folder{font-family:ui-monospace,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;color:var(--text-3)}
442
+ .card .parent{font-family:ui-monospace,monospace;white-space:nowrap;color:var(--text-3)}
443
+ #infoBar .meta a.parent{text-decoration:none}
444
+ #infoBar .meta a.parent:hover{color:var(--accent)}
445
+ .scope-error{grid-column:1/-1;font-size:13px;color:var(--warn-text)}
446
+ .strip-toggle{font-size:11px;padding:3px 10px;background:transparent;color:var(--text-muted);border:1px solid var(--border);border-radius:999px}
447
+ .strip-toggle:hover{color:var(--text-2);border-color:var(--border-strong)}
448
+ #stripShow{display:none;white-space:nowrap;flex-shrink:0}
449
+ #stripShow .live{color:var(--ok-text);white-space:nowrap}
450
+ #stripShow .live-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:4px;vertical-align:1px}
451
+ #stripHide{align-self:flex-end;border:0;padding:0 2px;border-radius:4px}
452
+ #stripHide:hover{text-decoration:underline;color:var(--text-2)}
453
+ body.strip-hidden #stripShow{display:block}
454
+ body.strip-hidden #topStrip, body.all-view #stripShow{display:none}
455
+ #allTile{flex:1;flex-direction:column;justify-content:center;align-items:flex-start;text-align:left;color:var(--text-2);font-size:13px;font-weight:600;width:100%}
456
+ #allTile .count{font-size:11px;font-weight:400;color:var(--text-muted)}
457
+ #allView{display:none;flex:1;min-height:0;flex-direction:column;gap:12px}
458
+ body.all-view #allView{display:flex}
459
+ body.all-view #center, body.all-view #topStrip{display:none}
460
+ #allHead{display:flex;gap:8px;align-items:center}
461
+ /* The search field is glass too (no shadow: it is a field, not a card); the focus ring below replaces the highlight. */
462
+ #allHead input{flex:1;min-width:0;background:var(--glass-bg);-webkit-backdrop-filter:blur(18px) saturate(1.35);backdrop-filter:blur(18px) saturate(1.35);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight);color:var(--text);padding:8px 10px;border-radius:8px}
463
+ #allHead input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
464
+ #allPath{font-size:11px;color:var(--text-muted);margin-top:-4px;word-break:break-all}
465
+ #allPath[hidden]{display:none}
466
+ #allPath code{font-family:ui-monospace,monospace;background:var(--bg-hover);padding:1px 4px;border-radius:4px}
467
+ #allHead .count{font-size:11px;color:var(--text-muted);white-space:nowrap;font-variant-numeric:tabular-nums}
468
+ #allList{flex:1;min-height:0;overflow:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;align-content:start;padding:2px}
469
+ .card.skeleton{pointer-events:none;border-style:dashed;opacity:.9;animation:skeleton-pulse 1.2s ease-in-out infinite}
470
+ .card.skeleton .line{height:10px;background:var(--bg-raised);border-radius:6px}
471
+ .card.skeleton .line.long{width:78%}
472
+ .card.skeleton .line.long2{width:62%}
473
+ .card.skeleton .line.tiny{width:30%;height:8px}
474
+ @keyframes skeleton-pulse{0%,100%{opacity:1}50%{opacity:.55}}
475
+ @keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
476
+ #topStrip:not(.loading) .card, #allList .card{animation:fadeIn .28s ease}
477
+ .card{padding:12px;border-radius:10px;background:var(--bg-panel);border:1px solid var(--border);cursor:pointer;display:flex;flex-direction:column;gap:6px;min-height:72px}
478
+ .card:hover{border-color:var(--border-strong);background:var(--bg-hover)}
479
+ #allList .card{position:relative}
480
+ .card-del{position:absolute;top:6px;right:6px;padding:2px 7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-muted);font-size:12px;line-height:1.2;cursor:pointer;opacity:0;transition:opacity .15s}
481
+ #allList .card:hover .card-del, .card-del:focus-visible{opacity:1}
482
+ .card-del:hover{color:#fff;background:var(--danger-bg);border-color:var(--danger-bg)}
483
+ @media (hover:none){.card-del{opacity:1}}
484
+ .card.active{background:var(--accent-bg);border-color:var(--accent-border)}
485
+ .card .preview{font-size:14px;line-height:1.4;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
486
+ .card .time{font-variant-numeric:tabular-nums;white-space:nowrap}
487
+ #topStrip .card .preview{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
488
+ #allList .card .preview{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;word-break:break-word;line-height:1.4}
489
+ .card .recap{font-size:12px;line-height:1.4;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:-2px 0 2px}
490
+ .card .meta{font-size:11px;color:var(--text-muted);display:flex;gap:6px;align-items:center}
491
+ /* A card's status is a dot (its word is the tooltip); the .status.* pills below are the info bar's. */
492
+ .card .dot{display:inline-block;width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--text-muted)}
493
+ .card .dot.running{background:var(--ok);animation:dot-pulse 1.6s ease-in-out infinite}
494
+ .card .dot.idle{background:var(--text-muted)}
495
+ .card .dot.stopped{background:transparent;border:1px solid var(--text-faint)}
496
+ .card .dot.completed{background:var(--accent)}
497
+ .card .dot.error{background:var(--danger)}
498
+ @keyframes dot-pulse{0%,100%{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)}}
499
+ @media (prefers-reduced-motion:reduce){.card .dot.running{animation:none}}
500
+ .status.running{background:var(--ok-bg);color:var(--ok-text)}
501
+ .status.idle{background:var(--bg-raised)}
502
+ .status.completed{background:var(--accent-bg);color:var(--accent)}
503
+ .status.error{background:var(--danger-bg-dim);color:var(--danger-text)}
504
+ .card .owner{font-size:11px;padding:2px 6px;border-radius:999px}
505
+ .card .owner.worker{background:var(--ok-bg);color:var(--ok-text)}
506
+ .card .owner.tui{background:var(--warn-bg);color:var(--warn-text)}
507
+ @media(max-width:900px){
508
+ #topStrip{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:72%;overflow-x:auto;min-height:0}
509
+ #topStrip #allTile{width:auto}
510
+ }
511
+ a{color:var(--accent)}
512
+ </style>
513
+ </head>
514
+ <body>
515
+ <main>
516
+ <div id="topBar">
517
+ <button type="button" id="newBtn" class="brand" title="New chat"><span class="brand-name">chi</span><span class="brand-new">+<span class="brand-new-word"> new chat</span></span></button>
518
+ <div id="scope" hidden></div>
519
+ <button type="button" class="strip-toggle" id="stripShow" title="Show the latest sessions"><span class="pill-arrow" aria-hidden="true">▾</span> sessions</button>
520
+ </div>
521
+ <section id="topStrip" aria-label="Latest sessions" class="loading"><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div></section>
522
+ <section id="center">
523
+ <div id="firstPeek"></div>
524
+ <div id="stage">
525
+ <div id="markdownWarning" class="markdown-warning" hidden></div>
526
+ <div id="history"></div>
527
+ <div id="empty"></div>
528
+ <div id="notice" role="status" hidden></div>
529
+ </div>
530
+ <div id="lightbox" hidden><img alt=""></div>
531
+ <div id="dock">
532
+ <div id="initRow" role="status" aria-live="polite" hidden></div>
533
+ <div id="composer" style="position:relative">
534
+ <div id="commandList" role="listbox" aria-label="Commands" hidden></div>
535
+ <div id="composeGrip" title="Drag to make the message box taller (double-click resets)"></div>
536
+ <div class="compose-col">
537
+ <div id="chips" hidden></div>
538
+ <textarea id="prompt" placeholder="Ask chi anything…" aria-controls="commandList" aria-autocomplete="list" aria-expanded="false"></textarea>
539
+ </div>
540
+ <div class="actions-col">
541
+ <label id="modelPick" class="model-pick" title="The model the new chat starts on" hidden><select id="modelSelect" aria-label="Model for the new chat"></select></label>
542
+ <div id="composeHint">Enter to start · Shift+Enter for a newline</div>
543
+ <button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Cancel</button>
544
+ <div class="split">
545
+ <button id="actionBtn">Start</button>
546
+ </div>
547
+ </div>
548
+ </div>
549
+ <div id="infoBar">
550
+ <div class="info" id="infoText"></div>
551
+ <button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop</button>
552
+ <button class="ghost" id="infoDeleteBtn" title="Delete session (for good)" style="display:none">delete</button>
553
+ </div>
554
+ </div>
555
+ </section>
556
+ <section id="allView" aria-label="All sessions">
557
+ <div id="allHead">
558
+ <button class="ghost" id="allBack" title="Back to the chat (Esc)">← Back</button>
559
+ <input id="filter" type="search" placeholder="Search… (preview / id / status)" autocomplete="off">
560
+ <span class="count" id="count"></span>
561
+ </div>
562
+ <div id="allPath" hidden>Stored in <code></code></div>
563
+ <div id="allList"></div>
564
+ </section>
565
+ </main>
566
+ <script type="module" src="/assets/app.js"></script>
567
+ </body>
568
+ </html>