@oratis/lisa 0.9.0 → 0.11.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 (296) hide show
  1. package/README.md +123 -16
  2. package/README.zh-CN.md +101 -15
  3. package/completions/_lisa +56 -0
  4. package/completions/lisa.bash +35 -1
  5. package/completions/lisa.fish +18 -1
  6. package/dist/advisor/engine.d.ts +7 -0
  7. package/dist/advisor/engine.d.ts.map +1 -1
  8. package/dist/advisor/engine.js +12 -0
  9. package/dist/advisor/engine.js.map +1 -1
  10. package/dist/agent.d.ts +14 -0
  11. package/dist/agent.d.ts.map +1 -1
  12. package/dist/agent.js +58 -7
  13. package/dist/agent.js.map +1 -1
  14. package/dist/agents/managed.d.ts +108 -0
  15. package/dist/agents/managed.d.ts.map +1 -0
  16. package/dist/agents/managed.js +241 -0
  17. package/dist/agents/managed.js.map +1 -0
  18. package/dist/agents/pty.d.ts +155 -0
  19. package/dist/agents/pty.d.ts.map +1 -0
  20. package/dist/agents/pty.js +299 -0
  21. package/dist/agents/pty.js.map +1 -0
  22. package/dist/approval.d.ts +13 -0
  23. package/dist/approval.d.ts.map +1 -1
  24. package/dist/approval.js +25 -2
  25. package/dist/approval.js.map +1 -1
  26. package/dist/autonomy/runs.d.ts +33 -0
  27. package/dist/autonomy/runs.d.ts.map +1 -0
  28. package/dist/autonomy/runs.js +106 -0
  29. package/dist/autonomy/runs.js.map +1 -0
  30. package/dist/channels/feishu.d.ts.map +1 -1
  31. package/dist/channels/feishu.js +65 -0
  32. package/dist/channels/feishu.js.map +1 -1
  33. package/dist/channels/router.d.ts +7 -0
  34. package/dist/channels/router.d.ts.map +1 -1
  35. package/dist/channels/router.js +1 -1
  36. package/dist/channels/router.js.map +1 -1
  37. package/dist/cli/agents-pty.d.ts +18 -0
  38. package/dist/cli/agents-pty.d.ts.map +1 -0
  39. package/dist/cli/agents-pty.js +182 -0
  40. package/dist/cli/agents-pty.js.map +1 -0
  41. package/dist/cli/agents.d.ts +2 -0
  42. package/dist/cli/agents.d.ts.map +1 -0
  43. package/dist/cli/agents.js +65 -0
  44. package/dist/cli/agents.js.map +1 -0
  45. package/dist/cli/consent.d.ts +2 -0
  46. package/dist/cli/consent.d.ts.map +1 -0
  47. package/dist/cli/consent.js +57 -0
  48. package/dist/cli/consent.js.map +1 -0
  49. package/dist/cli/model.d.ts +2 -0
  50. package/dist/cli/model.d.ts.map +1 -0
  51. package/dist/cli/model.js +121 -0
  52. package/dist/cli/model.js.map +1 -0
  53. package/dist/cli/sense.d.ts +2 -0
  54. package/dist/cli/sense.d.ts.map +1 -0
  55. package/dist/cli/sense.js +36 -0
  56. package/dist/cli/sense.js.map +1 -0
  57. package/dist/cli.js +117 -12
  58. package/dist/cli.js.map +1 -1
  59. package/dist/consent/blacklist.d.ts +28 -0
  60. package/dist/consent/blacklist.d.ts.map +1 -0
  61. package/dist/consent/blacklist.js +78 -0
  62. package/dist/consent/blacklist.js.map +1 -0
  63. package/dist/consent/store.d.ts +45 -0
  64. package/dist/consent/store.d.ts.map +1 -0
  65. package/dist/consent/store.js +126 -0
  66. package/dist/consent/store.js.map +1 -0
  67. package/dist/control/policy.d.ts +13 -0
  68. package/dist/control/policy.d.ts.map +1 -0
  69. package/dist/control/policy.js +57 -0
  70. package/dist/control/policy.js.map +1 -0
  71. package/dist/heartbeat/runner.d.ts.map +1 -1
  72. package/dist/heartbeat/runner.js +70 -14
  73. package/dist/heartbeat/runner.js.map +1 -1
  74. package/dist/idle/runner.d.ts.map +1 -1
  75. package/dist/idle/runner.js +87 -17
  76. package/dist/idle/runner.js.map +1 -1
  77. package/dist/integrations/aider/observer.d.ts +21 -1
  78. package/dist/integrations/aider/observer.d.ts.map +1 -1
  79. package/dist/integrations/aider/observer.js +110 -1
  80. package/dist/integrations/aider/observer.js.map +1 -1
  81. package/dist/integrations/claude-code/liveness.d.ts +10 -0
  82. package/dist/integrations/claude-code/liveness.d.ts.map +1 -0
  83. package/dist/integrations/claude-code/liveness.js +62 -0
  84. package/dist/integrations/claude-code/liveness.js.map +1 -0
  85. package/dist/integrations/claude-code/watcher.d.ts +39 -4
  86. package/dist/integrations/claude-code/watcher.d.ts.map +1 -1
  87. package/dist/integrations/claude-code/watcher.js +94 -57
  88. package/dist/integrations/claude-code/watcher.js.map +1 -1
  89. package/dist/integrations/codex/observer.d.ts +19 -6
  90. package/dist/integrations/codex/observer.d.ts.map +1 -1
  91. package/dist/integrations/codex/observer.js +228 -4
  92. package/dist/integrations/codex/observer.js.map +1 -1
  93. package/dist/integrations/dispatch-ledger.d.ts +30 -1
  94. package/dist/integrations/dispatch-ledger.d.ts.map +1 -1
  95. package/dist/integrations/dispatch-ledger.js +75 -9
  96. package/dist/integrations/dispatch-ledger.js.map +1 -1
  97. package/dist/integrations/git/branch.d.ts +11 -0
  98. package/dist/integrations/git/branch.d.ts.map +1 -0
  99. package/dist/integrations/git/branch.js +49 -0
  100. package/dist/integrations/git/branch.js.map +1 -0
  101. package/dist/integrations/git/observer.d.ts +39 -0
  102. package/dist/integrations/git/observer.d.ts.map +1 -0
  103. package/dist/integrations/git/observer.js +252 -0
  104. package/dist/integrations/git/observer.js.map +1 -0
  105. package/dist/integrations/github-pr/observer.d.ts.map +1 -1
  106. package/dist/integrations/github-pr/observer.js +7 -2
  107. package/dist/integrations/github-pr/observer.js.map +1 -1
  108. package/dist/integrations/hub.d.ts +8 -0
  109. package/dist/integrations/hub.d.ts.map +1 -1
  110. package/dist/integrations/hub.js +39 -1
  111. package/dist/integrations/hub.js.map +1 -1
  112. package/dist/integrations/managed/observer.d.ts +17 -0
  113. package/dist/integrations/managed/observer.d.ts.map +1 -0
  114. package/dist/integrations/managed/observer.js +46 -0
  115. package/dist/integrations/managed/observer.js.map +1 -0
  116. package/dist/integrations/opencode/observer.d.ts +53 -5
  117. package/dist/integrations/opencode/observer.d.ts.map +1 -1
  118. package/dist/integrations/opencode/observer.js +242 -20
  119. package/dist/integrations/opencode/observer.js.map +1 -1
  120. package/dist/integrations/pty/observer.d.ts +25 -0
  121. package/dist/integrations/pty/observer.d.ts.map +1 -0
  122. package/dist/integrations/pty/observer.js +48 -0
  123. package/dist/integrations/pty/observer.js.map +1 -0
  124. package/dist/integrations/registry.d.ts.map +1 -1
  125. package/dist/integrations/registry.js +5 -0
  126. package/dist/integrations/registry.js.map +1 -1
  127. package/dist/integrations/shell/observer.d.ts +33 -0
  128. package/dist/integrations/shell/observer.d.ts.map +1 -0
  129. package/dist/integrations/shell/observer.js +223 -0
  130. package/dist/integrations/shell/observer.js.map +1 -0
  131. package/dist/integrations/takoapi/a2a.d.ts +38 -0
  132. package/dist/integrations/takoapi/a2a.d.ts.map +1 -0
  133. package/dist/integrations/takoapi/a2a.js +70 -0
  134. package/dist/integrations/takoapi/a2a.js.map +1 -0
  135. package/dist/integrations/takoapi/ledger.d.ts +32 -0
  136. package/dist/integrations/takoapi/ledger.d.ts.map +1 -0
  137. package/dist/integrations/takoapi/ledger.js +96 -0
  138. package/dist/integrations/takoapi/ledger.js.map +1 -0
  139. package/dist/integrations/takoapi/observer.d.ts +39 -0
  140. package/dist/integrations/takoapi/observer.d.ts.map +1 -0
  141. package/dist/integrations/takoapi/observer.js +102 -0
  142. package/dist/integrations/takoapi/observer.js.map +1 -0
  143. package/dist/integrations/types.d.ts +17 -0
  144. package/dist/integrations/types.d.ts.map +1 -1
  145. package/dist/mcp/client.d.ts +5 -0
  146. package/dist/mcp/client.d.ts.map +1 -1
  147. package/dist/mcp/client.js +1 -1
  148. package/dist/mcp/client.js.map +1 -1
  149. package/dist/memory/embedding.d.ts +45 -0
  150. package/dist/memory/embedding.d.ts.map +1 -0
  151. package/dist/memory/embedding.js +139 -0
  152. package/dist/memory/embedding.js.map +1 -0
  153. package/dist/memory/search_tool.d.ts.map +1 -1
  154. package/dist/memory/search_tool.js +6 -2
  155. package/dist/memory/search_tool.js.map +1 -1
  156. package/dist/memory/vector.d.ts +14 -3
  157. package/dist/memory/vector.d.ts.map +1 -1
  158. package/dist/memory/vector.js +50 -0
  159. package/dist/memory/vector.js.map +1 -1
  160. package/dist/model/local.d.ts +43 -0
  161. package/dist/model/local.d.ts.map +1 -0
  162. package/dist/model/local.js +118 -0
  163. package/dist/model/local.js.map +1 -0
  164. package/dist/model/plan-usage.d.ts +41 -0
  165. package/dist/model/plan-usage.d.ts.map +1 -0
  166. package/dist/model/plan-usage.js +168 -0
  167. package/dist/model/plan-usage.js.map +1 -0
  168. package/dist/model/plans.d.ts +73 -0
  169. package/dist/model/plans.d.ts.map +1 -0
  170. package/dist/model/plans.js +246 -0
  171. package/dist/model/plans.js.map +1 -0
  172. package/dist/orchestrator/recent-recap.d.ts +2 -0
  173. package/dist/orchestrator/recent-recap.d.ts.map +1 -0
  174. package/dist/orchestrator/recent-recap.js +20 -0
  175. package/dist/orchestrator/recent-recap.js.map +1 -0
  176. package/dist/providers/anthropic.d.ts +1 -0
  177. package/dist/providers/anthropic.d.ts.map +1 -1
  178. package/dist/providers/anthropic.js +10 -2
  179. package/dist/providers/anthropic.js.map +1 -1
  180. package/dist/providers/fallback.d.ts +23 -0
  181. package/dist/providers/fallback.d.ts.map +1 -0
  182. package/dist/providers/fallback.js +29 -0
  183. package/dist/providers/fallback.js.map +1 -0
  184. package/dist/providers/gemini.d.ts +2 -0
  185. package/dist/providers/gemini.d.ts.map +1 -1
  186. package/dist/providers/gemini.js +24 -10
  187. package/dist/providers/gemini.js.map +1 -1
  188. package/dist/providers/openai.d.ts.map +1 -1
  189. package/dist/providers/openai.js +4 -1
  190. package/dist/providers/openai.js.map +1 -1
  191. package/dist/providers/registry.d.ts +18 -2
  192. package/dist/providers/registry.d.ts.map +1 -1
  193. package/dist/providers/registry.js +56 -9
  194. package/dist/providers/registry.js.map +1 -1
  195. package/dist/reflect.d.ts +55 -0
  196. package/dist/reflect.d.ts.map +1 -1
  197. package/dist/reflect.js +148 -54
  198. package/dist/reflect.js.map +1 -1
  199. package/dist/sense/log.d.ts +9 -0
  200. package/dist/sense/log.d.ts.map +1 -0
  201. package/dist/sense/log.js +63 -0
  202. package/dist/sense/log.js.map +1 -0
  203. package/dist/sense/screen.d.ts +43 -0
  204. package/dist/sense/screen.d.ts.map +1 -0
  205. package/dist/sense/screen.js +107 -0
  206. package/dist/sense/screen.js.map +1 -0
  207. package/dist/sense/service.d.ts +16 -0
  208. package/dist/sense/service.d.ts.map +1 -0
  209. package/dist/sense/service.js +33 -0
  210. package/dist/sense/service.js.map +1 -0
  211. package/dist/sense/types.d.ts +35 -0
  212. package/dist/sense/types.d.ts.map +1 -0
  213. package/dist/sense/types.js +11 -0
  214. package/dist/sense/types.js.map +1 -0
  215. package/dist/sense/voice.d.ts +28 -0
  216. package/dist/sense/voice.d.ts.map +1 -0
  217. package/dist/sense/voice.js +64 -0
  218. package/dist/sense/voice.js.map +1 -0
  219. package/dist/soul/git.d.ts.map +1 -1
  220. package/dist/soul/git.js +35 -16
  221. package/dist/soul/git.js.map +1 -1
  222. package/dist/soul/store.d.ts +22 -0
  223. package/dist/soul/store.d.ts.map +1 -1
  224. package/dist/soul/store.js +69 -14
  225. package/dist/soul/store.js.map +1 -1
  226. package/dist/soul/summary.d.ts +10 -0
  227. package/dist/soul/summary.d.ts.map +1 -0
  228. package/dist/soul/summary.js +57 -0
  229. package/dist/soul/summary.js.map +1 -0
  230. package/dist/soul/tools.d.ts.map +1 -1
  231. package/dist/soul/tools.js +13 -24
  232. package/dist/soul/tools.js.map +1 -1
  233. package/dist/soul/types.d.ts +9 -0
  234. package/dist/soul/types.d.ts.map +1 -1
  235. package/dist/soul/types.js.map +1 -1
  236. package/dist/subagent.d.ts +7 -0
  237. package/dist/subagent.d.ts.map +1 -1
  238. package/dist/subagent.js +3 -1
  239. package/dist/subagent.js.map +1 -1
  240. package/dist/tools/dispatch_agent.d.ts +13 -1
  241. package/dist/tools/dispatch_agent.d.ts.map +1 -1
  242. package/dist/tools/dispatch_agent.js +56 -12
  243. package/dist/tools/dispatch_agent.js.map +1 -1
  244. package/dist/tools/dispatch_status.d.ts +22 -0
  245. package/dist/tools/dispatch_status.d.ts.map +1 -0
  246. package/dist/tools/dispatch_status.js +57 -0
  247. package/dist/tools/dispatch_status.js.map +1 -0
  248. package/dist/tools/registry.d.ts +29 -0
  249. package/dist/tools/registry.d.ts.map +1 -1
  250. package/dist/tools/registry.js +67 -0
  251. package/dist/tools/registry.js.map +1 -1
  252. package/dist/tools/run_on_plan.d.ts +46 -0
  253. package/dist/tools/run_on_plan.d.ts.map +1 -0
  254. package/dist/tools/run_on_plan.js +89 -0
  255. package/dist/tools/run_on_plan.js.map +1 -0
  256. package/dist/tools/takoapi.d.ts +60 -0
  257. package/dist/tools/takoapi.d.ts.map +1 -0
  258. package/dist/tools/takoapi.js +181 -0
  259. package/dist/tools/takoapi.js.map +1 -0
  260. package/dist/tools/validate.d.ts +22 -0
  261. package/dist/tools/validate.d.ts.map +1 -0
  262. package/dist/tools/validate.js +50 -0
  263. package/dist/tools/validate.js.map +1 -0
  264. package/dist/web/agent-roster.d.ts +56 -0
  265. package/dist/web/agent-roster.d.ts.map +1 -0
  266. package/dist/web/agent-roster.js +109 -0
  267. package/dist/web/agent-roster.js.map +1 -0
  268. package/dist/web/devices.d.ts +27 -0
  269. package/dist/web/devices.d.ts.map +1 -0
  270. package/dist/web/devices.js +113 -0
  271. package/dist/web/devices.js.map +1 -0
  272. package/dist/web/island.d.ts +5 -1
  273. package/dist/web/island.d.ts.map +1 -1
  274. package/dist/web/island.js +410 -63
  275. package/dist/web/island.js.map +1 -1
  276. package/dist/web/lisa-client.d.ts +20 -0
  277. package/dist/web/lisa-client.d.ts.map +1 -0
  278. package/dist/web/lisa-client.js +1323 -0
  279. package/dist/web/lisa-client.js.map +1 -0
  280. package/dist/web/lisa-css.d.ts +13 -0
  281. package/dist/web/lisa-css.d.ts.map +1 -0
  282. package/dist/web/lisa-css.js +1163 -0
  283. package/dist/web/lisa-css.js.map +1 -0
  284. package/dist/web/lisa-html.d.ts +1 -1
  285. package/dist/web/lisa-html.d.ts.map +1 -1
  286. package/dist/web/lisa-html.js +15 -2136
  287. package/dist/web/lisa-html.js.map +1 -1
  288. package/dist/web/push.d.ts +76 -0
  289. package/dist/web/push.d.ts.map +1 -0
  290. package/dist/web/push.js +163 -0
  291. package/dist/web/push.js.map +1 -0
  292. package/dist/web/server.d.ts +19 -0
  293. package/dist/web/server.d.ts.map +1 -1
  294. package/dist/web/server.js +878 -64
  295. package/dist/web/server.js.map +1 -1
  296. package/package.json +5 -2
@@ -25,6 +25,8 @@
25
25
  * identitySub, sbDesire, sbClaudeCard, sbClaudeCount, sbClaudeRows,
26
26
  * sbReflection, sbReflectionBody, sbSessionBadge
27
27
  */
28
+ import { MAIN_CSS } from "./lisa-css.js";
29
+ import { MAIN_CLIENT_JS } from "./lisa-client.js";
28
30
  export const MAIN_HTML = `<!doctype html>
29
31
  <html lang="en"><head>
30
32
  <meta charset="utf-8">
@@ -38,1045 +40,7 @@ export const MAIN_HTML = `<!doctype html>
38
40
  <link rel="apple-touch-icon" href="/assets/lisa-mascot.png">
39
41
  <link rel="icon" type="image/png" href="/assets/lisa-mascot.png">
40
42
  <style>
41
- :root {
42
- color-scheme: dark;
43
-
44
- /* New design tokens (mockup) */
45
- --accent: #6ad4ff;
46
- --accent-soft: rgba(106, 212, 255, 0.13);
47
- --accent-glow: rgba(106, 212, 255, 0.27);
48
- --warm: #ffd066;
49
- --dream: #b487ff;
50
- --claude: #ff8c42;
51
- --err-color: #ff5577;
52
-
53
- --bg-deep: #07091a;
54
- --bg-1: #0b1024;
55
- --bg-2: #11163a;
56
- --bg-3: #1a1f4a;
57
- --bg-card: rgba(20, 26, 64, 0.65);
58
- --bg-card-strong: rgba(20, 26, 64, 0.88);
59
- --border-new: rgba(255, 255, 255, 0.07);
60
- --border-strong: rgba(255, 255, 255, 0.14);
61
-
62
- --fg: #e8eaff;
63
- --fg-2: #aeb5d3;
64
- --fg-3: #6c7398;
65
- --fg-faint: #444a6e;
66
-
67
- /* Legacy tokens — kept so the unchanged modal / cfg / birth
68
- overlay styles below still resolve. The new shell + chat use
69
- the modern tokens above. */
70
- --bg: #0a0d2b;
71
- --panel: #1a1f4d;
72
- --panel-light: #2a3270;
73
- --border: #6a7ad9;
74
- --border-light: #a4b2ff;
75
- --text: #e7ecff;
76
- --text-dim: #8090c0;
77
- --you: #6cf6e1;
78
- --lisa: #ffd167;
79
- --tool: #ff7eb6;
80
- --error: #ff5577;
81
- }
82
- * { box-sizing: border-box; }
83
- html, body {
84
- height: 100%;
85
- margin: 0;
86
- background: #000;
87
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", system-ui, sans-serif;
88
- -webkit-font-smoothing: antialiased;
89
- color: var(--fg);
90
- overflow: hidden;
91
- }
92
-
93
- /* ── App shell ─────────────────────────────────────────────── */
94
- .frame {
95
- height: 100vh;
96
- width: 100vw;
97
- display: grid;
98
- grid-template-columns: 280px 1fr;
99
- grid-template-rows: 36px 1fr;
100
- grid-template-areas:
101
- "titlebar titlebar"
102
- "sidebar main";
103
- background:
104
- radial-gradient(ellipse at 30% 20%, #1a1238 0%, transparent 50%),
105
- radial-gradient(ellipse at 80% 70%, #0a1f3a 0%, transparent 60%),
106
- linear-gradient(180deg, var(--bg-1) 0%, var(--bg-deep) 100%);
107
- overflow: hidden;
108
- }
109
-
110
- /* Title bar — visually shows "Lisa · session-id". The actual drag
111
- behavior is handled Swift-side by a transparent NSView overlay
112
- (DragHandleView) placed on top of the WKWebView for the same 36pt
113
- strip — WebKit ignores the CSS -webkit-app-region: drag property,
114
- so the cosmetic HTML and the functional drag region are two
115
- separate things.
116
- Padding-left reserves the ~78pt that the macOS traffic-light
117
- buttons occupy at top-left. */
118
- .titlebar {
119
- grid-area: titlebar;
120
- background: rgba(7, 9, 26, 0.55);
121
- backdrop-filter: blur(20px);
122
- -webkit-backdrop-filter: blur(20px);
123
- border-bottom: 1px solid var(--border-new);
124
- display: flex;
125
- align-items: center;
126
- justify-content: center;
127
- padding: 0 14px 0 78px;
128
- font-size: 12px;
129
- font-weight: 600;
130
- letter-spacing: 0.04em;
131
- color: var(--fg-2);
132
- /* nothing here should ever capture pointer events — the NSView
133
- overlay above handles dragging, and there are no interactive
134
- elements in the HTML titlebar. */
135
- user-select: none;
136
- pointer-events: none;
137
- }
138
- .titlebar .session-tag {
139
- color: var(--fg-3);
140
- font-weight: 400;
141
- margin-left: 6px;
142
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
143
- font-size: 11px;
144
- letter-spacing: 0;
145
- }
146
-
147
- /* ── Sidebar ───────────────────────────────────────────────── */
148
- .sidebar {
149
- grid-area: sidebar;
150
- background: rgba(7, 9, 26, 0.4);
151
- backdrop-filter: blur(30px);
152
- -webkit-backdrop-filter: blur(30px);
153
- border-right: 1px solid var(--border-new);
154
- overflow-y: auto;
155
- padding: 20px 16px 16px;
156
- display: flex;
157
- flex-direction: column;
158
- gap: 18px;
159
- }
160
-
161
- /* Identity card */
162
- .identity {
163
- display: grid;
164
- grid-template-columns: 56px 1fr;
165
- gap: 12px;
166
- align-items: center;
167
- padding: 12px;
168
- background: var(--bg-card);
169
- border: 1px solid var(--border-new);
170
- border-radius: 14px;
171
- }
172
- .identity .avatar-wrap {
173
- width: 56px;
174
- height: 56px;
175
- border-radius: 50%;
176
- position: relative;
177
- border: 1px solid var(--border-strong);
178
- box-shadow: 0 0 0 3px var(--accent-soft);
179
- background: #15192a;
180
- }
181
- .identity .avatar-wrap img {
182
- width: 100%;
183
- height: 100%;
184
- border-radius: 50%;
185
- object-fit: cover;
186
- object-position: 50% 22%;
187
- image-rendering: pixelated;
188
- display: block;
189
- transition: opacity 250ms ease;
190
- user-select: none;
191
- -webkit-user-drag: none;
192
- }
193
- .identity .avatar-wrap img.fading { opacity: 0; }
194
- .identity .avatar-wrap::after {
195
- content: "";
196
- position: absolute;
197
- right: -2px;
198
- bottom: -2px;
199
- width: 14px;
200
- height: 14px;
201
- border-radius: 50%;
202
- background: #4ade80;
203
- border: 2px solid var(--bg-1);
204
- }
205
- .identity h1 {
206
- margin: 0 0 2px;
207
- font-size: 15px;
208
- font-weight: 700;
209
- letter-spacing: 0.02em;
210
- color: var(--fg);
211
- }
212
- .identity .sub {
213
- margin: 0;
214
- font-size: 11px;
215
- color: var(--fg-3);
216
- }
217
- .identity .mood {
218
- display: inline-flex;
219
- align-items: center;
220
- gap: 5px;
221
- margin-top: 4px;
222
- font-size: 10.5px;
223
- color: var(--accent);
224
- text-transform: uppercase;
225
- letter-spacing: 0.08em;
226
- font-weight: 600;
227
- }
228
- .identity .mood::before {
229
- content: "";
230
- width: 6px;
231
- height: 6px;
232
- border-radius: 50%;
233
- background: var(--accent);
234
- box-shadow: 0 0 6px var(--accent-glow);
235
- }
236
-
237
- /* Sidebar plain text section ("currently wanting") */
238
- .sb-section { display: flex; flex-direction: column; gap: 6px; }
239
- .sb-section h2 {
240
- margin: 0 0 2px;
241
- font-size: 10.5px;
242
- font-weight: 700;
243
- text-transform: uppercase;
244
- letter-spacing: 0.10em;
245
- color: var(--fg-3);
246
- padding-left: 4px;
247
- }
248
- .sb-section .body-text {
249
- margin: 0;
250
- font-size: 12px;
251
- line-height: 1.55;
252
- color: var(--fg-2);
253
- padding: 0 4px;
254
- }
255
-
256
- /* Live mini-cards (Claude monitor / last reflection) */
257
- .card {
258
- background: var(--bg-card);
259
- border: 1px solid var(--border-new);
260
- border-radius: 12px;
261
- padding: 10px 12px;
262
- font-size: 12px;
263
- color: var(--fg-2);
264
- line-height: 1.5;
265
- }
266
- .card.tint-claude {
267
- border-color: rgba(255, 140, 66, 0.20);
268
- background: linear-gradient(180deg, rgba(255, 140, 66, 0.06), rgba(255, 140, 66, 0.02));
269
- }
270
- .card.tint-idle {
271
- border-color: rgba(255, 208, 102, 0.22);
272
- background: linear-gradient(180deg, rgba(255, 208, 102, 0.07), rgba(255, 208, 102, 0.02));
273
- }
274
- .card .h {
275
- display: flex;
276
- justify-content: space-between;
277
- align-items: center;
278
- margin-bottom: 4px;
279
- }
280
- .card .h .left {
281
- display: flex;
282
- align-items: center;
283
- gap: 6px;
284
- font-size: 10.5px;
285
- font-weight: 700;
286
- text-transform: uppercase;
287
- letter-spacing: 0.10em;
288
- color: var(--claude);
289
- }
290
- .card.tint-idle .h .left { color: var(--warm); }
291
- .card .h .count {
292
- background: rgba(255, 140, 66, 0.16);
293
- color: var(--claude);
294
- font-size: 11px;
295
- font-weight: 600;
296
- padding: 2px 7px;
297
- border-radius: 8px;
298
- }
299
- .card.tint-idle .h .count {
300
- background: rgba(255, 208, 102, 0.16);
301
- color: var(--warm);
302
- }
303
- .session-row {
304
- display: grid;
305
- grid-template-columns: 7px 1fr auto;
306
- align-items: center;
307
- gap: 7px;
308
- padding: 5px 0;
309
- font-size: 11.5px;
310
- border-top: 1px dashed rgba(255, 140, 66, 0.10);
311
- cursor: default;
312
- }
313
- .session-row:first-of-type { border-top: 0; }
314
- .session-row .pip {
315
- width: 7px;
316
- height: 7px;
317
- border-radius: 50%;
318
- background: var(--fg-faint);
319
- }
320
- /* working = calm slow breathe (running, not actionable);
321
- waiting = solid + a soft halo that draws the eye ("needs you"). */
322
- .session-row .pip.working { background: var(--claude); opacity: 1; animation: breathe 2.6s ease-in-out infinite; }
323
- .session-row .pip.waiting { background: var(--claude); opacity: 1; animation: needsYou 2s ease-in-out infinite; }
324
- .session-row .pip.error { background: var(--err-color); }
325
- .session-row .name {
326
- color: var(--fg);
327
- font-weight: 600;
328
- overflow: hidden;
329
- white-space: nowrap;
330
- text-overflow: ellipsis;
331
- }
332
- .session-row .when {
333
- color: var(--fg-3);
334
- font-variant-numeric: tabular-nums;
335
- font-size: 10.5px;
336
- }
337
- .session-empty {
338
- color: var(--fg-faint);
339
- font-size: 11.5px;
340
- font-style: italic;
341
- padding: 4px 0 2px;
342
- }
343
- @keyframes pulse {
344
- 0%, 100% { opacity: 0.35; }
345
- 50% { opacity: 1; }
346
- }
347
- /* Gentle breathing for "working" — present but not jumpy. */
348
- @keyframes breathe {
349
- 0%, 100% { opacity: 0.55; }
350
- 50% { opacity: 1; }
351
- }
352
- /* "needs you" — solid dot with a pulsing warm halo, prominent without the
353
- harsh on/off flash. */
354
- @keyframes needsYou {
355
- 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 140, 66, 0); }
356
- 50% { box-shadow: 0 0 7px 2px rgba(255, 140, 66, 0.65); }
357
- }
358
-
359
- /* Compact SOUL / SKILLS / MEMORY / TOOLS row */
360
- .badges {
361
- display: grid;
362
- grid-template-columns: repeat(2, 1fr);
363
- gap: 6px;
364
- }
365
- .badge {
366
- background: var(--bg-card);
367
- border: 1px solid var(--border-new);
368
- color: var(--fg-2);
369
- font-family: inherit;
370
- font-size: 10.5px;
371
- font-weight: 600;
372
- letter-spacing: 0.06em;
373
- text-transform: uppercase;
374
- padding: 7px 8px;
375
- border-radius: 8px;
376
- cursor: pointer;
377
- display: flex;
378
- align-items: center;
379
- justify-content: center;
380
- gap: 6px;
381
- transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
382
- }
383
- .badge:hover {
384
- background: var(--bg-card-strong);
385
- color: var(--fg);
386
- border-color: var(--border-strong);
387
- }
388
- .badge img {
389
- width: 14px;
390
- height: 14px;
391
- image-rendering: pixelated;
392
- opacity: 0.8;
393
- }
394
-
395
- /* Sidebar footer — session id + sessions count */
396
- .sb-footer {
397
- margin-top: auto;
398
- padding-top: 12px;
399
- border-top: 1px solid var(--border-new);
400
- display: flex;
401
- align-items: center;
402
- gap: 8px;
403
- color: var(--fg-3);
404
- font-size: 11px;
405
- }
406
- .sb-footer .session-id {
407
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
408
- font-size: 10px;
409
- color: var(--fg-3);
410
- overflow: hidden;
411
- white-space: nowrap;
412
- text-overflow: ellipsis;
413
- flex: 1;
414
- min-width: 0;
415
- }
416
- .sb-footer .badge-count {
417
- background: var(--bg-3);
418
- color: var(--fg-2);
419
- border-radius: 6px;
420
- padding: 1px 6px;
421
- font-size: 10px;
422
- font-variant-numeric: tabular-nums;
423
- flex-shrink: 0;
424
- }
425
-
426
- /* ── Main pane ─────────────────────────────────────────────── */
427
- .main {
428
- grid-area: main;
429
- display: grid;
430
- grid-template-rows: 1fr auto auto;
431
- overflow: hidden;
432
- background:
433
- radial-gradient(ellipse at 50% -30%, rgba(106, 212, 255, 0.06) 0%, transparent 60%),
434
- linear-gradient(180deg, transparent, rgba(106, 212, 255, 0.015));
435
- }
436
-
437
- #log {
438
- overflow-y: auto;
439
- padding: 22px 28px 24px;
440
- display: flex;
441
- flex-direction: column;
442
- gap: 8px;
443
- scroll-behavior: smooth;
444
- }
445
-
446
- /* Chat author label (.role .you/.lisa) */
447
- .role {
448
- font-size: 10px;
449
- font-weight: 700;
450
- letter-spacing: 0.14em;
451
- text-transform: uppercase;
452
- padding: 6px 2px 0;
453
- margin-top: 6px;
454
- }
455
- .role.you { color: var(--accent); text-align: right; }
456
- .role.lisa { color: var(--warm); }
457
-
458
- /* Message bubble */
459
- .msg {
460
- display: block;
461
- max-width: 88%;
462
- background: var(--bg-card);
463
- border: 1px solid var(--border-new);
464
- border-radius: 14px;
465
- padding: 9px 13px;
466
- font-size: 13px;
467
- line-height: 1.55;
468
- color: var(--fg);
469
- white-space: pre-wrap;
470
- word-wrap: break-word;
471
- }
472
- /* "you" bubbles sit on the right with cyan tint */
473
- .role.you + .msg {
474
- margin-left: auto;
475
- background: linear-gradient(160deg, rgba(106, 212, 255, 0.12), rgba(106, 212, 255, 0.06));
476
- border-color: rgba(106, 212, 255, 0.25);
477
- }
478
- /* attachment label sits compactly under the user bubble */
479
- .msg.attach-label {
480
- margin-top: 2px;
481
- background: transparent;
482
- border: 0;
483
- padding: 0 2px;
484
- font-size: 10.5px;
485
- color: var(--fg-3);
486
- max-width: 88%;
487
- margin-left: auto;
488
- }
489
-
490
- /* Thinking indicator */
491
- .thinking {
492
- font-size: 12px;
493
- color: var(--fg-3);
494
- font-style: italic;
495
- padding: 4px 2px;
496
- letter-spacing: 0.03em;
497
- }
498
-
499
- /* Tool call card */
500
- .tool-block {
501
- background: var(--bg-card);
502
- border: 1px solid var(--border-new);
503
- border-radius: 10px;
504
- padding: 8px 12px;
505
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
506
- font-size: 11px;
507
- color: var(--fg-2);
508
- margin: 4px 0;
509
- max-width: 92%;
510
- }
511
- .tool-block.tool-error {
512
- border-color: rgba(255, 85, 119, 0.30);
513
- background: rgba(255, 85, 119, 0.05);
514
- }
515
- .tool-head {
516
- display: flex;
517
- align-items: center;
518
- gap: 8px;
519
- color: var(--accent);
520
- font-weight: 600;
521
- }
522
- .tool-block.tool-error .tool-head { color: var(--err-color); }
523
- .tool-icon { font-size: 12px; }
524
- .tool-spinner { color: var(--fg-3); font-weight: 400; margin-left: auto; font-size: 12px; }
525
- .tool-input {
526
- color: var(--fg-3);
527
- margin-top: 4px;
528
- white-space: pre-wrap;
529
- word-break: break-word;
530
- }
531
- .tool-result {
532
- color: var(--fg-2);
533
- margin-top: 4px;
534
- padding-top: 4px;
535
- border-top: 1px dashed var(--border-new);
536
- white-space: pre-wrap;
537
- max-height: 160px;
538
- overflow-y: auto;
539
- }
540
-
541
- .err {
542
- color: var(--err-color);
543
- font-size: 12px;
544
- padding: 6px 2px;
545
- }
546
-
547
- /* Idle pulse + while-you-were-away banner injected into #log */
548
- .idle-pulse {
549
- color: var(--dream);
550
- font-size: 12px;
551
- font-style: italic;
552
- padding: 4px 2px;
553
- letter-spacing: 0.03em;
554
- }
555
- .idle-block {
556
- background: linear-gradient(180deg, rgba(180, 135, 255, 0.10), rgba(180, 135, 255, 0.02));
557
- border: 1px solid rgba(180, 135, 255, 0.30);
558
- border-radius: 14px;
559
- padding: 12px 14px;
560
- margin: 6px 0;
561
- font-size: 12.5px;
562
- line-height: 1.6;
563
- color: var(--fg);
564
- }
565
- .idle-head {
566
- font-size: 11px;
567
- text-transform: uppercase;
568
- letter-spacing: 0.10em;
569
- color: var(--dream);
570
- font-weight: 700;
571
- margin-bottom: 6px;
572
- display: flex;
573
- align-items: center;
574
- gap: 8px;
575
- }
576
- .idle-time {
577
- margin-left: auto;
578
- font-weight: 400;
579
- letter-spacing: 0.05em;
580
- color: var(--fg-3);
581
- font-size: 10px;
582
- }
583
-
584
- /* Composer */
585
- #attachPreview {
586
- padding: 4px 22px 0;
587
- display: flex;
588
- flex-wrap: wrap;
589
- gap: 6px;
590
- min-height: 0;
591
- }
592
- #attachPreview:empty { padding: 0; }
593
- .attach-chip {
594
- background: var(--bg-card-strong);
595
- border: 1px solid var(--border-strong);
596
- color: var(--fg-2);
597
- font-size: 11px;
598
- padding: 3px 8px;
599
- border-radius: 8px;
600
- display: inline-flex;
601
- align-items: center;
602
- gap: 6px;
603
- }
604
- .attach-rm {
605
- background: transparent;
606
- border: 0;
607
- color: var(--fg-3);
608
- cursor: pointer;
609
- font-size: 13px;
610
- padding: 0;
611
- line-height: 1;
612
- }
613
- .attach-rm:hover { color: var(--err-color); }
614
-
615
- #form {
616
- padding: 14px 20px 18px;
617
- background: rgba(7, 9, 26, 0.6);
618
- backdrop-filter: blur(30px);
619
- -webkit-backdrop-filter: blur(30px);
620
- border-top: 1px solid var(--border-new);
621
- display: grid;
622
- grid-template-columns: 36px 36px 36px 1fr 96px;
623
- gap: 10px;
624
- align-items: end;
625
- }
626
-
627
- #attachBtn, #captureBtn, #recordBtn {
628
- align-self: stretch;
629
- display: flex;
630
- align-items: center;
631
- justify-content: center;
632
- background: transparent;
633
- border: 0;
634
- color: var(--fg-3);
635
- font-size: 17px;
636
- cursor: pointer;
637
- border-radius: 10px;
638
- transition: background 120ms ease, color 120ms ease;
639
- min-height: 44px;
640
- padding: 0;
641
- }
642
- #attachBtn:hover, #captureBtn:hover, #recordBtn:hover { background: var(--bg-card); color: var(--fg); }
643
- #captureBtn.flash { background: var(--accent); color: var(--bg-deep); }
644
- /* Pulsing red while recording. */
645
- #recordBtn.recording {
646
- background: var(--err-color);
647
- color: #fff;
648
- animation: rec-pulse 1.1s ease-in-out infinite;
649
- }
650
- @keyframes rec-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.55; } }
651
- /* Off-screen the file input instead of display:none. WKWebView's
652
- implicit <label>→<input type=file> click forward doesn't fire on a
653
- fully display:none input — the OS file picker silently no-ops.
654
- Off-screening keeps the input "live" while invisible. */
655
- #fileInput {
656
- position: absolute;
657
- width: 1px;
658
- height: 1px;
659
- opacity: 0;
660
- pointer-events: none;
661
- overflow: hidden;
662
- }
663
-
664
- #input {
665
- background: var(--bg-card-strong);
666
- border: 1px solid var(--border-strong);
667
- border-radius: 14px;
668
- color: var(--fg);
669
- font-family: inherit;
670
- font-size: 13.5px;
671
- line-height: 1.45;
672
- padding: 11px 13px;
673
- resize: none;
674
- outline: none;
675
- width: 100%;
676
- min-height: 44px;
677
- max-height: 200px;
678
- transition: border-color 120ms ease, box-shadow 120ms ease;
679
- }
680
- #input:focus {
681
- border-color: var(--accent);
682
- box-shadow: 0 0 0 3px var(--accent-soft);
683
- }
684
- #input::placeholder { color: var(--fg-3); }
685
-
686
- #sendBtn {
687
- background: linear-gradient(180deg, var(--accent) 0%, #4eb8e5 100%);
688
- color: #0a1024;
689
- border: 0;
690
- border-radius: 14px;
691
- height: 100%;
692
- font-family: inherit;
693
- font-size: 12.5px;
694
- font-weight: 700;
695
- letter-spacing: 0.06em;
696
- cursor: pointer;
697
- display: flex;
698
- align-items: center;
699
- justify-content: center;
700
- gap: 6px;
701
- padding: 10px 14px;
702
- transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
703
- box-shadow: 0 4px 14px rgba(106, 212, 255, 0.25);
704
- }
705
- #sendBtn:hover:not(:disabled) {
706
- transform: translateY(-1px);
707
- box-shadow: 0 6px 18px rgba(106, 212, 255, 0.35);
708
- }
709
- #sendBtn:active:not(:disabled) { transform: translateY(0); }
710
- #sendBtn:disabled { opacity: 0.5; cursor: wait; }
711
- /* Hide the legacy pixel send icon; the text label is enough in the new theme. */
712
- #sendBtn img { display: none; }
713
-
714
- /* ── Mobile / narrow ───────────────────────────────────────── */
715
- @media (max-width: 720px) {
716
- body {
717
- padding-top: env(safe-area-inset-top);
718
- padding-bottom: env(safe-area-inset-bottom);
719
- }
720
- .frame {
721
- grid-template-columns: 1fr;
722
- grid-template-rows: auto 1fr auto;
723
- }
724
- .sidebar {
725
- max-height: 38vh;
726
- padding: 14px 14px 12px;
727
- gap: 14px;
728
- }
729
- #form {
730
- grid-template-columns: 36px 36px 36px 1fr 84px;
731
- padding: 10px 14px 14px;
732
- }
733
- #input { font-size: 16px; /* prevents iOS Safari auto-zoom */ }
734
- }
735
-
736
- /* ===================================================================
737
- Modal panel (skills / memory / tools / soul) — unchanged from the
738
- legacy pixel-art shell. Uses the legacy CSS vars declared above.
739
- =================================================================== */
740
- .modal-bg {
741
- position: fixed; inset: 0;
742
- background: rgba(0, 0, 0, 0.7);
743
- display: none;
744
- align-items: center; justify-content: center;
745
- z-index: 100;
746
- }
747
- .modal-bg.open { display: flex; }
748
- .modal {
749
- background: var(--bg-card-strong);
750
- border: 1px solid var(--border-strong);
751
- border-radius: 16px;
752
- backdrop-filter: blur(30px);
753
- -webkit-backdrop-filter: blur(30px);
754
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);
755
- max-width: 720px;
756
- width: 90vw;
757
- max-height: 80vh;
758
- display: flex;
759
- flex-direction: column;
760
- }
761
- .modal-head {
762
- padding: 14px 18px;
763
- border-bottom: 1px solid var(--border-new);
764
- display: flex;
765
- justify-content: space-between;
766
- align-items: center;
767
- }
768
- .modal-title {
769
- font-size: 13px;
770
- font-weight: 700;
771
- letter-spacing: 0.10em;
772
- text-transform: uppercase;
773
- color: var(--accent);
774
- }
775
- .modal-close {
776
- background: transparent;
777
- border: 1px solid var(--border-new);
778
- color: var(--fg-2);
779
- font-family: inherit;
780
- font-size: 11px;
781
- letter-spacing: 0.06em;
782
- padding: 5px 10px;
783
- border-radius: 8px;
784
- cursor: pointer;
785
- }
786
- .modal-close:hover { color: var(--err-color); border-color: rgba(255, 85, 119, 0.40); }
787
- .modal-body {
788
- padding: 16px 18px;
789
- overflow-y: auto;
790
- font-size: 12.5px;
791
- line-height: 1.55;
792
- color: var(--fg-2);
793
- }
794
- .modal-body h3 {
795
- font-size: 11px;
796
- font-weight: 700;
797
- letter-spacing: 0.10em;
798
- text-transform: uppercase;
799
- color: var(--accent);
800
- margin: 14px 0 6px 0;
801
- border-bottom: 1px dashed var(--border-new);
802
- padding-bottom: 5px;
803
- }
804
- .modal-body h3:first-child { margin-top: 0; }
805
- .modal-body .item {
806
- padding: 7px 0;
807
- border-bottom: 1px dotted var(--border-new);
808
- }
809
- .modal-body .item:last-child { border: none; }
810
- .modal-body .name {
811
- color: var(--warm);
812
- font-weight: 600;
813
- font-size: 12px;
814
- }
815
- .modal-body .desc { color: var(--fg-3); margin-top: 2px; }
816
- .modal-body pre {
817
- background: rgba(0, 0, 0, 0.30);
818
- padding: 10px 12px;
819
- border-left: 2px solid var(--accent);
820
- border-radius: 4px;
821
- white-space: pre-wrap;
822
- margin: 6px 0;
823
- color: var(--fg);
824
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
825
- font-size: 11.5px;
826
- line-height: 1.5;
827
- }
828
- .modal-body .empty { color: var(--fg-faint); font-style: italic; }
829
-
830
- /* ===================================================================
831
- Birth ritual overlay — full-screen, one-time. Uses legacy palette
832
- intentionally (it's a separate ceremonial moment).
833
- =================================================================== */
834
- .birth-overlay {
835
- position: fixed;
836
- inset: 0;
837
- background:
838
- radial-gradient(ellipse at center, rgba(40, 30, 80, 0.95) 0%, rgba(5, 5, 20, 1) 70%);
839
- z-index: 9999;
840
- display: none;
841
- flex-direction: column;
842
- align-items: center;
843
- justify-content: center;
844
- padding: 32px;
845
- }
846
- .birth-overlay.open { display: flex; }
847
- .birth-content {
848
- position: relative;
849
- z-index: 2;
850
- width: min(800px, 95vw);
851
- max-height: 90vh;
852
- overflow-y: auto;
853
- }
854
- .birth-stars {
855
- text-align: center;
856
- color: var(--accent);
857
- font-size: 14px;
858
- letter-spacing: 8px;
859
- text-shadow: 0 0 8px var(--accent-glow);
860
- animation: starBlink 1.5s steps(3) infinite;
861
- }
862
- @keyframes starBlink {
863
- 0%, 30%, 100% { opacity: 1; }
864
- 50% { opacity: 0.4; }
865
- }
866
- .birth-title {
867
- text-align: center;
868
- color: var(--warm);
869
- font-size: 22px;
870
- font-weight: 700;
871
- letter-spacing: 6px;
872
- margin: 24px 0 32px;
873
- text-shadow: 0 0 12px rgba(255, 208, 102, 0.45);
874
- }
875
- .birth-step {
876
- margin: 14px 0;
877
- padding: 10px 16px;
878
- border-left: 3px solid var(--border-new);
879
- background: rgba(20, 26, 64, 0.55);
880
- border-radius: 0 8px 8px 0;
881
- opacity: 0;
882
- transform: translateY(8px);
883
- transition: opacity 0.4s ease-out, transform 0.4s ease-out;
884
- }
885
- .birth-step.shown { opacity: 1; transform: translateY(0); }
886
- .birth-step.active {
887
- border-left-color: var(--warm);
888
- background: rgba(255, 208, 102, 0.08);
889
- }
890
- .birth-step.done { border-left-color: var(--accent); }
891
- .birth-step .step-name {
892
- font-size: 10.5px;
893
- font-weight: 700;
894
- letter-spacing: 0.10em;
895
- color: var(--fg-3);
896
- text-transform: uppercase;
897
- margin-bottom: 4px;
898
- }
899
- .birth-step.active .step-name { color: var(--warm); }
900
- .birth-step.done .step-name { color: var(--accent); }
901
- .birth-step .step-detail {
902
- color: var(--fg);
903
- font-size: 13px;
904
- line-height: 1.55;
905
- word-break: break-word;
906
- }
907
- .birth-step .step-cursor {
908
- display: inline-block;
909
- width: 6px;
910
- background: var(--warm);
911
- height: 0.9em;
912
- vertical-align: middle;
913
- animation: blink 0.8s steps(2) infinite;
914
- }
915
- @keyframes blink { 50% { opacity: 0; } }
916
- .birth-final {
917
- margin-top: 36px;
918
- text-align: center;
919
- color: var(--warm);
920
- font-size: 15px;
921
- font-weight: 700;
922
- letter-spacing: 0.10em;
923
- text-shadow: 0 0 10px rgba(255, 208, 102, 0.4);
924
- opacity: 0;
925
- transition: opacity 0.6s ease-in;
926
- }
927
- .birth-final.shown { opacity: 1; }
928
- .birth-enter {
929
- margin: 24px auto 0;
930
- display: block;
931
- background: linear-gradient(180deg, var(--accent) 0%, #4eb8e5 100%);
932
- border: 0;
933
- color: #0a1024;
934
- font-family: inherit;
935
- font-weight: 700;
936
- font-size: 13px;
937
- padding: 12px 32px;
938
- border-radius: 12px;
939
- cursor: pointer;
940
- letter-spacing: 0.10em;
941
- opacity: 0;
942
- transition: opacity 0.6s ease-in, transform 120ms ease;
943
- box-shadow: 0 6px 18px rgba(106, 212, 255, 0.35);
944
- }
945
- .birth-enter.shown { opacity: 1; }
946
- .birth-enter:hover { transform: translateY(-1px); }
947
- .birth-error {
948
- color: var(--err-color);
949
- text-align: center;
950
- margin-top: 24px;
951
- font-size: 11.5px;
952
- letter-spacing: 0.05em;
953
- }
954
-
955
- /* ===================================================================
956
- API-key config overlay — same modernized treatment.
957
- =================================================================== */
958
- .cfg-overlay {
959
- position: fixed;
960
- inset: 0;
961
- background:
962
- radial-gradient(ellipse at center, rgba(40, 30, 80, 0.95) 0%, rgba(5, 5, 20, 1) 70%);
963
- z-index: 9998;
964
- display: none;
965
- align-items: center;
966
- justify-content: center;
967
- padding: 32px;
968
- }
969
- .cfg-overlay.open { display: flex; }
970
- .cfg-card {
971
- position: relative;
972
- z-index: 2;
973
- width: min(520px, 95vw);
974
- background: var(--bg-card-strong);
975
- border: 1px solid var(--border-strong);
976
- border-radius: 16px;
977
- backdrop-filter: blur(30px);
978
- -webkit-backdrop-filter: blur(30px);
979
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);
980
- padding: 28px 32px;
981
- }
982
- .cfg-stars {
983
- text-align: center;
984
- color: var(--accent);
985
- font-size: 12px;
986
- letter-spacing: 6px;
987
- text-shadow: 0 0 8px var(--accent-glow);
988
- animation: starBlink 1.5s steps(3) infinite;
989
- }
990
- .cfg-title {
991
- text-align: center;
992
- color: var(--warm);
993
- font-size: 16px;
994
- font-weight: 700;
995
- letter-spacing: 6px;
996
- margin: 16px 0 6px;
997
- }
998
- .cfg-sub {
999
- text-align: center;
1000
- color: var(--fg-3);
1001
- font-size: 12.5px;
1002
- margin-bottom: 22px;
1003
- line-height: 1.5;
1004
- }
1005
- .cfg-sub a { color: var(--accent); text-decoration: underline; }
1006
- .cfg-field { display: block; margin: 14px 0; }
1007
- .cfg-label {
1008
- display: block;
1009
- font-size: 10.5px;
1010
- font-weight: 700;
1011
- letter-spacing: 0.08em;
1012
- text-transform: uppercase;
1013
- color: var(--accent);
1014
- margin-bottom: 6px;
1015
- }
1016
- .cfg-label .opt {
1017
- color: var(--fg-3);
1018
- font-size: 9.5px;
1019
- margin-left: 6px;
1020
- text-transform: none;
1021
- letter-spacing: 0;
1022
- font-weight: 400;
1023
- }
1024
- .cfg-input {
1025
- width: 100%;
1026
- background: var(--bg-card);
1027
- border: 1px solid var(--border-strong);
1028
- border-radius: 10px;
1029
- color: var(--fg);
1030
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
1031
- font-size: 13px;
1032
- padding: 10px 12px;
1033
- }
1034
- .cfg-input:focus {
1035
- outline: none;
1036
- border-color: var(--accent);
1037
- box-shadow: 0 0 0 3px var(--accent-soft);
1038
- }
1039
- .cfg-help {
1040
- color: var(--fg-3);
1041
- font-size: 11px;
1042
- margin-top: 6px;
1043
- }
1044
- .cfg-help code {
1045
- color: var(--fg-2);
1046
- background: rgba(0, 0, 0, 0.3);
1047
- padding: 1px 5px;
1048
- border-radius: 3px;
1049
- font-size: 11px;
1050
- }
1051
- .cfg-actions {
1052
- margin-top: 22px;
1053
- display: flex;
1054
- justify-content: center;
1055
- }
1056
- .cfg-save {
1057
- background: linear-gradient(180deg, var(--accent) 0%, #4eb8e5 100%);
1058
- border: 0;
1059
- color: #0a1024;
1060
- font-family: inherit;
1061
- font-size: 12.5px;
1062
- font-weight: 700;
1063
- padding: 11px 28px;
1064
- border-radius: 12px;
1065
- cursor: pointer;
1066
- letter-spacing: 0.10em;
1067
- box-shadow: 0 4px 14px rgba(106, 212, 255, 0.25);
1068
- transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
1069
- }
1070
- .cfg-save:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(106, 212, 255, 0.35); }
1071
- .cfg-save:active { transform: translateY(0); }
1072
- .cfg-save:disabled { opacity: 0.55; cursor: wait; }
1073
- .cfg-error {
1074
- color: var(--err-color);
1075
- font-size: 11px;
1076
- text-align: center;
1077
- margin-top: 14px;
1078
- min-height: 14px;
1079
- }
43
+ ${MAIN_CSS}
1080
44
  </style>
1081
45
  </head><body>
1082
46
 
@@ -1111,9 +75,18 @@ export const MAIN_HTML = `<!doctype html>
1111
75
  <!-- Claude Code monitor -->
1112
76
  <div class="card tint-claude" id="sbClaudeCard">
1113
77
  <div class="h">
1114
- <div class="left">claude code</div>
78
+ <div class="left">agents</div>
1115
79
  <div class="count">▶︎ <span id="sbClaudeCount">0</span></div>
1116
80
  </div>
81
+ <form id="sbDelegate" class="delegate" autocomplete="off">
82
+ <select id="sbDelegateKind" title="Agent type — managed (LISA-run) or a real CLI under a PTY (spike)">
83
+ <option value="managed">managed</option>
84
+ <option value="claude">claude</option>
85
+ <option value="codex">codex</option>
86
+ </select>
87
+ <input id="sbDelegateTask" type="text" placeholder="delegate a task…" />
88
+ <button type="submit" title="Start an agent">▶</button>
89
+ </form>
1117
90
  <div id="sbClaudeRows">
1118
91
  <div class="session-empty">(idle)</div>
1119
92
  </div>
@@ -1133,6 +106,7 @@ export const MAIN_HTML = `<!doctype html>
1133
106
  <button class="badge" type="button" data-panel="skills"><img src="/assets/icon-skill.png" alt="">SKILLS</button>
1134
107
  <button class="badge" type="button" data-panel="memory"><img src="/assets/icon-memory.png" alt="">MEMORY</button>
1135
108
  <button class="badge" type="button" data-panel="tools"><img src="/assets/icon-tool.png" alt="">TOOLS</button>
109
+ <button class="badge" type="button" data-panel="plans"><img src="/assets/icon-tool.png" alt="">PLANS</button>
1136
110
  </div>
1137
111
 
1138
112
  <!-- Footer: current session id -->
@@ -1225,1102 +199,7 @@ export const MAIN_HTML = `<!doctype html>
1225
199
  </div>
1226
200
 
1227
201
  <script>
1228
- const log = document.getElementById('log');
1229
- const input = document.getElementById('input');
1230
- const form = document.getElementById('form');
1231
- const sendBtn = document.getElementById('sendBtn');
1232
- const sessionEl = document.getElementById('sessionId');
1233
- const fileInput = document.getElementById('fileInput');
1234
- const attachPreview = document.getElementById('attachPreview');
1235
-
1236
- // ── Attached files state ──────────────────────────────────────────
1237
- let pendingFiles = []; // Array of {name, mediaType, data (base64)}
1238
-
1239
- function readFileAsBase64(file) {
1240
- return new Promise((resolve, reject) => {
1241
- const reader = new FileReader();
1242
- reader.onload = () => {
1243
- const dataUrl = reader.result;
1244
- const base64 = dataUrl.split(',')[1];
1245
- resolve(base64);
1246
- };
1247
- reader.onerror = reject;
1248
- reader.readAsDataURL(file);
1249
- });
1250
- }
1251
-
1252
- function inferMediaType(file) {
1253
- if (file.type) return file.type;
1254
- const ext = file.name.split('.').pop().toLowerCase();
1255
- const map = { pdf: 'application/pdf', txt: 'text/plain', md: 'text/plain', csv: 'text/csv', json: 'application/json' };
1256
- return map[ext] || 'application/octet-stream';
1257
- }
1258
-
1259
- // ── Vision: screenshot → composer ──────────────────────────────────
1260
- // lisaAttachImage adds an already-encoded {name,mediaType,data} object to
1261
- // the pending attachments — used by both the in-page 📷 button and the
1262
- // native global hotkey (Lisa.app calls lisaCaptureAndAttach via JS bridge).
1263
- window.lisaAttachImage = function (file) {
1264
- if (!file || !file.data) return;
1265
- pendingFiles.push({
1266
- name: file.name || 'screenshot.png',
1267
- mediaType: file.mediaType || 'image/png',
1268
- data: file.data,
1269
- });
1270
- renderAttachPreview();
1271
- try { input.focus(); } catch (_) {}
1272
- };
1273
-
1274
- // lisaPrefillComposer drops text into the composer WITHOUT sending — the user
1275
- // reads it and hits Enter to send (that send is the confirmation). Used by the
1276
- // island's screen-advisor "Optimize ▸" card (via the Swift bridge) and by the
1277
- // ?prefill= URL param (plain browser tabs).
1278
- window.lisaPrefillComposer = function (text) {
1279
- if (!text || !input) return;
1280
- input.value = String(text);
1281
- try { input.dispatchEvent(new Event('input', { bubbles: true })); } catch (_) {}
1282
- try { input.focus(); input.setSelectionRange(input.value.length, input.value.length); } catch (_) {}
1283
- };
1284
-
1285
- // On load, honour ?prefill=… (the island opens /?prefill=<task> in a browser
1286
- // tab). Strip it from the URL afterwards so a refresh doesn't re-fill.
1287
- try {
1288
- var _pf = new URLSearchParams(location.search).get('prefill');
1289
- if (_pf) {
1290
- window.lisaPrefillComposer(_pf);
1291
- history.replaceState(null, '', location.pathname);
1292
- }
1293
- } catch (_) {}
1294
-
1295
- // lisaCaptureAndAttach asks the server to run a screen capture, then
1296
- // attaches the result. mode: 'interactive' (crosshair, default) | 'full'.
1297
- // Returns true if an image was attached, false if cancelled/failed.
1298
- // Exposed on window so the native app's global hotkey can invoke it.
1299
- let capturing = false;
1300
- window.lisaCaptureAndAttach = async function (mode) {
1301
- if (capturing) return false;
1302
- capturing = true;
1303
- const btn = document.getElementById('captureBtn');
1304
- if (btn) btn.classList.add('flash');
1305
- try {
1306
- const res = await fetch('/api/vision/capture', {
1307
- method: 'POST',
1308
- headers: { 'content-type': 'application/json' },
1309
- body: JSON.stringify({ mode: mode || 'interactive' }),
1310
- });
1311
- if (!res.ok) {
1312
- console.warn('[vision] capture failed: HTTP ' + res.status);
1313
- return false;
1314
- }
1315
- const data = await res.json();
1316
- if (data.cancelled || !data.file) return false;
1317
- window.lisaAttachImage(data.file);
1318
- return true;
1319
- } catch (err) {
1320
- console.warn('[vision] capture error:', err);
1321
- return false;
1322
- } finally {
1323
- capturing = false;
1324
- if (btn) setTimeout(() => btn.classList.remove('flash'), 200);
1325
- }
1326
- };
1327
-
1328
- function renderAttachPreview() {
1329
- attachPreview.innerHTML = '';
1330
- pendingFiles.forEach((f, i) => {
1331
- const chip = document.createElement('span');
1332
- chip.className = 'attach-chip';
1333
- chip.textContent = f.name;
1334
- const rm = document.createElement('button');
1335
- rm.type = 'button';
1336
- rm.className = 'attach-rm';
1337
- rm.textContent = '×';
1338
- rm.onclick = () => { pendingFiles.splice(i, 1); renderAttachPreview(); };
1339
- chip.appendChild(rm);
1340
- attachPreview.appendChild(chip);
1341
- });
1342
- }
1343
-
1344
- fileInput.addEventListener('change', async () => {
1345
- for (const file of fileInput.files) {
1346
- const data = await readFileAsBase64(file);
1347
- pendingFiles.push({ name: file.name, mediaType: inferMediaType(file), data });
1348
- }
1349
- fileInput.value = '';
1350
- renderAttachPreview();
1351
- });
1352
-
1353
- // The natural <label>→<input type=file> click-forwarding is supposed
1354
- // to open the picker without any JS. But in WKWebView under our
1355
- // layout it never reaches runOpenPanel. We add an explicit click
1356
- // listener that forwards the click synchronously (preserving the
1357
- // user-gesture context) and logs to console so we can verify in the
1358
- // inspector if it ever silently no-ops again.
1359
- // 📷 capture button → interactive crosshair screenshot into the composer.
1360
- const captureBtnEl = document.getElementById('captureBtn');
1361
- if (captureBtnEl) {
1362
- captureBtnEl.addEventListener('click', () => { void window.lisaCaptureAndAttach('interactive'); });
1363
- }
1364
-
1365
- // ── Audio recording → transcribe → Lisa summarizes ─────────────────
1366
- // 🎙 toggles a MediaRecorder. On stop: POST the clip to /api/voice/transcribe
1367
- // (server-side Whisper), then send the transcript into the normal chat with a
1368
- // "summarize this" framing — so Lisa produces the summary in her own voice and
1369
- // it's persisted + discussable like any turn. First click prompts mic
1370
- // permission (browser-native).
1371
- const recordBtnEl = document.getElementById('recordBtn');
1372
- let mediaRecorder = null;
1373
- let recordedChunks = [];
1374
- let recordStream = null;
1375
- // 'dictation' (default): polish speech → composer for you to edit + send.
1376
- // 'summary' (long-press 🎙): the original record→Lisa-summarizes flow.
1377
- let recordMode = 'dictation';
1378
-
1379
- function pickAudioMime() {
1380
- const prefs = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg'];
1381
- for (const m of prefs) {
1382
- try { if (window.MediaRecorder && MediaRecorder.isTypeSupported(m)) return m; } catch (_) {}
1383
- }
1384
- return '';
1385
- }
1386
-
1387
- async function startRecording() {
1388
- if (!navigator.mediaDevices || !window.MediaRecorder) {
1389
- el('div', 'err', '[voice] recording not supported in this browser');
1390
- return;
1391
- }
1392
- try {
1393
- recordStream = await navigator.mediaDevices.getUserMedia({ audio: true });
1394
- } catch (err) {
1395
- el('div', 'err', '[voice] microphone access denied or unavailable');
1396
- return;
1397
- }
1398
- const mimeType = pickAudioMime();
1399
- recordedChunks = [];
1400
- mediaRecorder = mimeType ? new MediaRecorder(recordStream, { mimeType }) : new MediaRecorder(recordStream);
1401
- mediaRecorder.addEventListener('dataavailable', (e) => { if (e.data && e.data.size > 0) recordedChunks.push(e.data); });
1402
- mediaRecorder.addEventListener('stop', () => { void finishRecording(); });
1403
- mediaRecorder.start();
1404
- if (recordBtnEl) { recordBtnEl.classList.add('recording'); recordBtnEl.textContent = '⏹'; recordBtnEl.title = 'Stop recording'; }
1405
- }
1406
-
1407
- function stopRecordingTracks() {
1408
- if (recordStream) { recordStream.getTracks().forEach((t) => t.stop()); recordStream = null; }
1409
- if (recordBtnEl) { recordBtnEl.classList.remove('recording'); recordBtnEl.textContent = '🎙'; recordBtnEl.title = 'Dictate — speak and Lisa drops polished text in the box (hold to record a summary)'; }
1410
- }
1411
-
1412
- async function finishRecording() {
1413
- const mime = (mediaRecorder && mediaRecorder.mimeType) || 'audio/webm';
1414
- stopRecordingTracks();
1415
- const mode = recordMode;
1416
- const blob = new Blob(recordedChunks, { type: mime });
1417
- recordedChunks = [];
1418
- if (blob.size === 0) return;
1419
- // Transient status line: dictation polishes; summary transcribes.
1420
- const pending = el('div', 'thinking', mode === 'dictation' ? '⋯ transcribing + polishing' : '⋯ transcribing recording');
1421
- try {
1422
- const data = await blobToBase64(blob);
1423
- const res = await fetch('/api/voice/transcribe', {
1424
- method: 'POST',
1425
- headers: { 'content-type': 'application/json' },
1426
- body: JSON.stringify({ data, mediaType: mime, mode }),
1427
- });
1428
- const out = await res.json();
1429
- if (pending) pending.remove();
1430
- if (!res.ok || out.error) {
1431
- el('div', 'err', '[voice] ' + (out.error || ('HTTP ' + res.status)));
1432
- return;
1433
- }
1434
- if (mode === 'dictation') {
1435
- // Typeless-style: drop the polished text into the composer (cursor at end)
1436
- // for you to review and send. Never auto-sends. Appends if you'd already
1437
- // started typing.
1438
- const text = (out.text || out.transcript || '').trim();
1439
- if (!text) { el('div', 'err', '[voice] (nothing dictated)'); return; }
1440
- const existing = input.value.trimEnd();
1441
- input.value = existing ? existing + '\\n' + text : text;
1442
- try { input.dispatchEvent(new Event('input', { bubbles: true })); } catch (_) {}
1443
- try { input.focus(); input.setSelectionRange(input.value.length, input.value.length); } catch (_) {}
1444
- return;
1445
- }
1446
- // summary mode: hand the transcript to Lisa with a summarize framing.
1447
- const transcript = (out.transcript || '').trim();
1448
- if (!transcript) { el('div', 'err', '[voice] (empty transcript)'); return; }
1449
- const framed =
1450
- "I just recorded some audio. Here's the transcript — please give me a clear, " +
1451
- "useful summary (key points, decisions, action items if any), then I might ask follow-ups.\\n\\n" +
1452
- "--- transcript ---\\n" + transcript;
1453
- send(framed);
1454
- } catch (err) {
1455
- if (pending) pending.remove();
1456
- el('div', 'err', '[voice] ' + (err && err.message ? err.message : 'transcription failed'));
1457
- }
1458
- }
1459
-
1460
- function blobToBase64(blob) {
1461
- return new Promise((resolve, reject) => {
1462
- const reader = new FileReader();
1463
- reader.onload = () => resolve(String(reader.result).split(',')[1] || '');
1464
- reader.onerror = reject;
1465
- reader.readAsDataURL(blob);
1466
- });
1467
- }
1468
-
1469
- if (recordBtnEl) {
1470
- // Short click → dictation (polished text into the composer). Press-and-hold
1471
- // (≥500ms) → summary (record → Lisa summarizes). A click always follows
1472
- // pointerup, so we just flag long-presses and read the flag on click.
1473
- let longPress = false;
1474
- let holdTimer = null;
1475
- const clearHold = () => { if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; } };
1476
- recordBtnEl.addEventListener('pointerdown', () => {
1477
- longPress = false;
1478
- clearHold();
1479
- holdTimer = setTimeout(() => { longPress = true; }, 500);
1480
- });
1481
- recordBtnEl.addEventListener('pointerup', clearHold);
1482
- recordBtnEl.addEventListener('pointercancel', clearHold);
1483
- recordBtnEl.addEventListener('pointerleave', clearHold);
1484
- recordBtnEl.addEventListener('click', () => {
1485
- if (mediaRecorder && mediaRecorder.state === 'recording') {
1486
- mediaRecorder.stop();
1487
- longPress = false;
1488
- return;
1489
- }
1490
- recordMode = longPress ? 'summary' : 'dictation';
1491
- longPress = false;
1492
- if (recordBtnEl) recordBtnEl.title = recordMode === 'summary' ? 'Recording for summary — click to stop' : 'Dictating — click to stop';
1493
- void startRecording();
1494
- });
1495
- }
1496
-
1497
- const attachBtnEl = document.getElementById('attachBtn');
1498
- if (attachBtnEl) {
1499
- attachBtnEl.addEventListener('click', (ev) => {
1500
- // Don't preventDefault — that cancels the implicit label-forward
1501
- // and removes the redundant gesture path. Letting both fire is
1502
- // fine because <input type=file>.click() fires the picker only
1503
- // once per user gesture.
1504
- console.log('[attach] click forwarded to fileInput');
1505
- try {
1506
- fileInput.click();
1507
- } catch (err) {
1508
- console.error('[attach] fileInput.click failed:', err);
1509
- }
1510
- });
1511
- }
1512
-
1513
- // Paste-to-attach: when the user has copied an image (screenshot,
1514
- // image from a webpage, etc.) and presses ⌘V, pull the image off the
1515
- // clipboard and add it to pendingFiles — same path the file picker
1516
- // uses. Plain-text paste falls through to default behavior.
1517
- //
1518
- // We listen at the DOCUMENT level so paste works whether the textarea
1519
- // is focused, the chat log is focused, or focus is on a tool-block.
1520
- // (Some users press ⌘V right after launching the app, before clicking
1521
- // anywhere — element-scoped handlers miss that.)
1522
- //
1523
- // For text paste we MUST NOT preventDefault — let the browser route
1524
- // it to the focused element. Only intercept when we detect image
1525
- // items.
1526
- async function handlePastedClipboard(ev) {
1527
- const cb = ev.clipboardData;
1528
- if (!cb) return;
1529
-
1530
- // Two clipboard surfaces — items (modern, exposes pasteboard files)
1531
- // and files (older, doesn't include MIME type for some types). We
1532
- // dedupe via the underlying File reference.
1533
- const fileMap = new Map(); // File → mediaType (string)
1534
- if (cb.items && cb.items.length) {
1535
- for (const item of cb.items) {
1536
- if (item.kind !== 'file') continue;
1537
- if (!item.type || !item.type.startsWith('image/')) continue;
1538
- const f = item.getAsFile();
1539
- if (f) fileMap.set(f, item.type);
1540
- }
1541
- }
1542
- if (cb.files && cb.files.length) {
1543
- for (const f of cb.files) {
1544
- if (f.type && f.type.startsWith('image/')) {
1545
- if (!fileMap.has(f)) fileMap.set(f, f.type || 'image/png');
1546
- }
1547
- }
1548
- }
1549
-
1550
- if (fileMap.size === 0) return; // text paste — leave default behavior alone
1551
-
1552
- ev.preventDefault();
1553
- // Keep focus on the textarea so the user can keep typing right after.
1554
- try { input.focus(); } catch (_) {}
1555
-
1556
- for (const [file, mediaType] of fileMap) {
1557
- try {
1558
- const data = await readFileAsBase64(file);
1559
- const ext = (mediaType.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '');
1560
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1561
- const name = file.name && file.name !== 'image.png'
1562
- ? file.name
1563
- : 'pasted-' + ts + '.' + ext;
1564
- pendingFiles.push({ name, mediaType, data });
1565
- } catch (err) {
1566
- console.warn('[paste] failed to read image:', err);
1567
- }
1568
- }
1569
- renderAttachPreview();
1570
- }
1571
-
1572
- input.addEventListener('paste', (ev) => { void handlePastedClipboard(ev); });
1573
- // Document-level fallback. If focus is somewhere other than the
1574
- // textarea (e.g. just-launched, no focus yet), the textarea-level
1575
- // paste listener never fires; this one catches it.
1576
- document.addEventListener('paste', (ev) => {
1577
- // Avoid double-firing when the textarea ALREADY handled it: if
1578
- // ev.defaultPrevented is set, the textarea listener already
1579
- // consumed the paste and added the file.
1580
- if (ev.defaultPrevented) return;
1581
- void handlePastedClipboard(ev);
1582
- });
1583
-
1584
- // Surface session id from server header on first request. The titlebar
1585
- // shows the same id with a leading "·" separator after the "Lisa" label.
1586
- fetch('/session').then(r => r.json()).then(s => {
1587
- sessionEl.textContent = s.id;
1588
- const titlebarSession = document.getElementById('titlebarSession');
1589
- if (titlebarSession) titlebarSession.textContent = '· ' + s.id;
1590
- });
1591
-
1592
- // ── Persistent /events SSE: mood updates + idle messages + Claude
1593
- // activity, lifetime of page.
1594
- function connectEvents() {
1595
- const es = new EventSource('/events');
1596
- let idlePulseEl = null;
1597
- es.addEventListener('message', (e) => {
1598
- const ev = JSON.parse(e.data);
1599
- if (ev.type === 'mood') {
1600
- setMood(ev.slug);
1601
- } else if (ev.type === 'idle_start') {
1602
- if (!idlePulseEl) {
1603
- idlePulseEl = document.createElement('div');
1604
- idlePulseEl.className = 'idle-pulse';
1605
- idlePulseEl.textContent = '⋯ Lisa is thinking on her own time ⋯';
1606
- log.appendChild(idlePulseEl);
1607
- log.scrollTop = log.scrollHeight;
1608
- }
1609
- } else if (ev.type === 'idle_message') {
1610
- if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }
1611
- const block = document.createElement('div');
1612
- block.className = 'idle-block';
1613
- const head = document.createElement('div');
1614
- head.className = 'idle-head';
1615
- head.textContent = '★ WHILE YOU WERE AWAY';
1616
- const time = document.createElement('span');
1617
- time.className = 'idle-time';
1618
- try { time.textContent = new Date(ev.at).toLocaleTimeString(); } catch {}
1619
- head.appendChild(time);
1620
- block.appendChild(head);
1621
- const bodyEl = document.createElement('div');
1622
- bodyEl.textContent = ev.text;
1623
- block.appendChild(bodyEl);
1624
- log.appendChild(block);
1625
- log.scrollTop = log.scrollHeight;
1626
- // sidebar reflection card mirrors the latest while-you-were-away
1627
- if (typeof updateReflection === 'function') updateReflection(ev.text);
1628
- } else if (ev.type === 'idle_done') {
1629
- if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }
1630
- } else if (ev.type === 'idle_error') {
1631
- if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }
1632
- const e2 = document.createElement('div');
1633
- e2.className = 'err';
1634
- e2.textContent = '[idle error] ' + ev.message;
1635
- log.appendChild(e2);
1636
- } else if (ev.type === 'claude_session_update') {
1637
- // Sidebar Claude monitor card refresh — defined later in the
1638
- // "sidebar live wiring" block.
1639
- if (typeof refreshClaudeSessions === 'function') refreshClaudeSessions();
1640
- }
1641
- });
1642
- es.onerror = () => {
1643
- es.close();
1644
- setTimeout(connectEvents, 3000); // reconnect
1645
- };
1646
- }
1647
- connectEvents();
1648
-
1649
- // ── API key config gate: show overlay if no key is configured ─────
1650
- const cfgOverlay = document.getElementById('cfgOverlay');
1651
- const cfgForm = document.getElementById('cfgForm');
1652
- const cfgAnthropic = document.getElementById('cfgAnthropic');
1653
- const cfgOpenai = document.getElementById('cfgOpenai');
1654
- const cfgSaveBtn = document.getElementById('cfgSave');
1655
- const cfgError = document.getElementById('cfgError');
1656
-
1657
- cfgForm.addEventListener('submit', async (ev) => {
1658
- ev.preventDefault();
1659
- cfgError.textContent = '';
1660
- const anthropic = cfgAnthropic.value.trim();
1661
- const openai = cfgOpenai.value.trim();
1662
- if (!anthropic) {
1663
- cfgError.textContent = 'ANTHROPIC_API_KEY is required.';
1664
- return;
1665
- }
1666
- cfgSaveBtn.disabled = true;
1667
- try {
1668
- const res = await fetch('/api/config/save', {
1669
- method: 'POST',
1670
- headers: {'content-type': 'application/json'},
1671
- body: JSON.stringify({ anthropicKey: anthropic, openaiKey: openai || undefined }),
1672
- });
1673
- if (!res.ok) {
1674
- const txt = await res.text().catch(() => '');
1675
- cfgError.textContent = 'Save failed: HTTP ' + res.status + (txt ? ' — ' + txt.slice(0, 120) : '');
1676
- cfgSaveBtn.disabled = false;
1677
- return;
1678
- }
1679
- cfgAnthropic.value = '';
1680
- cfgOpenai.value = '';
1681
- cfgOverlay.classList.remove('open');
1682
- maybeBirth();
1683
- } catch (err) {
1684
- cfgError.textContent = 'Save failed: ' + err.message;
1685
- cfgSaveBtn.disabled = false;
1686
- }
1687
- });
1688
-
1689
- // ── Birth ritual: show overlay if Lisa hasn't been born yet ───────
1690
- const birthOverlay = document.getElementById('birthOverlay');
1691
- const birthStepsEl = document.getElementById('birthSteps');
1692
- const birthFinalEl = document.getElementById('birthFinal');
1693
- const birthEnterBtn = document.getElementById('birthEnter');
1694
- const birthErrorEl = document.getElementById('birthError');
1695
-
1696
- birthEnterBtn.addEventListener('click', () => {
1697
- birthOverlay.classList.remove('open');
1698
- setTimeout(() => location.reload(), 300);
1699
- });
1700
-
1701
- async function maybeBirth() {
1702
- const status = await fetch('/api/soul').then(r => r.json());
1703
- if (status.born) return;
1704
- birthOverlay.classList.add('open');
1705
- startBirthStream();
1706
- }
1707
-
1708
- function appendBirthStep(step) {
1709
- const prevActive = birthStepsEl.querySelector('.birth-step.active');
1710
- if (prevActive) {
1711
- prevActive.classList.remove('active');
1712
- prevActive.classList.add('done');
1713
- }
1714
- const div = document.createElement('div');
1715
- div.className = 'birth-step active';
1716
- const name = document.createElement('div');
1717
- name.className = 'step-name';
1718
- name.textContent = step;
1719
- div.appendChild(name);
1720
- const detail = document.createElement('div');
1721
- detail.className = 'step-detail';
1722
- const cursor = document.createElement('span');
1723
- cursor.className = 'step-cursor';
1724
- detail.appendChild(cursor);
1725
- div.appendChild(detail);
1726
- birthStepsEl.appendChild(div);
1727
- setTimeout(() => div.classList.add('shown'), 50);
1728
- return detail;
1729
- }
1730
-
1731
- function typewriter(el, text, done) {
1732
- const cursor = el.querySelector('.step-cursor');
1733
- if (cursor) cursor.remove();
1734
- let i = 0;
1735
- const speed = Math.max(8, Math.min(28, 600 / text.length));
1736
- function tick() {
1737
- if (i >= text.length) {
1738
- const c = document.createElement('span');
1739
- c.className = 'step-cursor';
1740
- el.appendChild(c);
1741
- done && done();
1742
- return;
1743
- }
1744
- el.appendChild(document.createTextNode(text[i]));
1745
- i++;
1746
- el.parentElement.parentElement.scrollTop = el.parentElement.parentElement.scrollHeight;
1747
- setTimeout(tick, speed);
1748
- }
1749
- tick();
1750
- }
1751
-
1752
- async function startBirthStream() {
1753
- birthErrorEl.textContent = '';
1754
- let currentDetail = null;
1755
- let queue = [];
1756
- let processing = false;
1757
-
1758
- function processQueue() {
1759
- if (processing) return;
1760
- if (queue.length === 0) return;
1761
- processing = true;
1762
- const ev = queue.shift();
1763
- if (ev.kind === 'step') {
1764
- currentDetail = appendBirthStep(ev.name);
1765
- typewriter(currentDetail, ev.detail || '', () => {
1766
- processing = false;
1767
- processQueue();
1768
- });
1769
- } else if (ev.kind === 'done') {
1770
- const last = birthStepsEl.querySelector('.birth-step.active');
1771
- if (last) { last.classList.remove('active'); last.classList.add('done'); }
1772
- birthFinalEl.textContent = ev.message;
1773
- birthFinalEl.classList.add('shown');
1774
- birthEnterBtn.classList.add('shown');
1775
- processing = false;
1776
- } else if (ev.kind === 'error') {
1777
- birthErrorEl.textContent = ev.message;
1778
- processing = false;
1779
- }
1780
- }
1781
-
1782
- try {
1783
- const res = await fetch('/api/birth', { method: 'POST' });
1784
- if (!res.ok) {
1785
- birthErrorEl.textContent = 'Birth failed: HTTP ' + res.status + '. Check ANTHROPIC_API_KEY.';
1786
- return;
1787
- }
1788
- const reader = res.body.getReader();
1789
- const decoder = new TextDecoder();
1790
- let buf = '';
1791
- while (true) {
1792
- const { value, done } = await reader.read();
1793
- if (done) break;
1794
- buf += decoder.decode(value, { stream: true });
1795
- let idx;
1796
- while ((idx = buf.indexOf('\\n\\n')) >= 0) {
1797
- const event = buf.slice(0, idx);
1798
- buf = buf.slice(idx + 2);
1799
- const m = event.match(/^data: (.*)$/m);
1800
- if (!m) continue;
1801
- const ev = JSON.parse(m[1]);
1802
- queue.push(ev);
1803
- processQueue();
1804
- }
1805
- }
1806
- } catch (err) {
1807
- birthErrorEl.textContent = 'Birth failed: ' + err.message;
1808
- }
1809
- }
1810
-
1811
- async function startupGate() {
1812
- let cfg;
1813
- try {
1814
- cfg = await fetch('/api/config/status').then(r => r.json());
1815
- } catch {
1816
- return;
1817
- }
1818
- if (!cfg.configured) {
1819
- cfgOverlay.classList.add('open');
1820
- setTimeout(() => cfgAnthropic.focus(), 50);
1821
- return;
1822
- }
1823
- maybeBirth();
1824
- }
1825
- startupGate();
1826
-
1827
- // ── history load & infinite-scroll ──────────────────────────────────
1828
- let historyPage = 0;
1829
- let historyLoading = false;
1830
- let historyExhausted = false;
1831
-
1832
- function textOfMessage(msg) {
1833
- if (typeof msg.content === 'string') return msg.content.trim();
1834
- if (!Array.isArray(msg.content)) return '';
1835
- return msg.content
1836
- .filter(b => b.type === 'text')
1837
- .map(b => b.text)
1838
- .join('')
1839
- .trim();
1840
- }
1841
-
1842
- function prependHistoryMessages(messages) {
1843
- const fragment = document.createDocumentFragment();
1844
- for (const msg of messages) {
1845
- const text = textOfMessage(msg);
1846
- if (!text) continue;
1847
- const roleDiv = document.createElement('div');
1848
- roleDiv.className = 'role ' + (msg.role === 'user' ? 'you' : 'lisa');
1849
- roleDiv.textContent = msg.role === 'user' ? 'YOU' : 'LISA';
1850
- const span = document.createElement('span');
1851
- span.className = 'msg';
1852
- span.textContent = text;
1853
- fragment.appendChild(roleDiv);
1854
- fragment.appendChild(span);
1855
- }
1856
- log.insertBefore(fragment, log.firstChild);
1857
- }
1858
-
1859
- async function loadHistoryPage() {
1860
- if (historyLoading || historyExhausted) return;
1861
- historyLoading = true;
1862
- const prevScrollHeight = log.scrollHeight;
1863
- try {
1864
- const res = await fetch('/api/history?page=' + historyPage);
1865
- const data = await res.json();
1866
- if (data.messages && data.messages.length) {
1867
- prependHistoryMessages(data.messages);
1868
- log.scrollTop = log.scrollHeight - prevScrollHeight;
1869
- historyPage++;
1870
- }
1871
- if (!data.hasMore) {
1872
- historyExhausted = true;
1873
- if (historyPage > 1) {
1874
- const marker = document.createElement('div');
1875
- marker.style.cssText = 'text-align:center;color:var(--fg-3);font-size:11px;padding:8px 0;letter-spacing:0.06em;';
1876
- marker.textContent = '— end of history —';
1877
- log.insertBefore(marker, log.firstChild);
1878
- }
1879
- }
1880
- } finally {
1881
- historyLoading = false;
1882
- }
1883
- }
1884
-
1885
- loadHistoryPage();
1886
- log.addEventListener('scroll', () => {
1887
- if (log.scrollTop < 80) loadHistoryPage();
1888
- });
1889
-
1890
- // ── mascot crossfade on mood event ──────────────────────────────────
1891
- const mascotEl = document.getElementById('mascot');
1892
- const mascotTagEl = document.getElementById('mascotTag');
1893
- let currentMood = 'neutral';
1894
- function setMood(slug) {
1895
- if (!slug || slug === currentMood) return;
1896
- const url = '/assets/lisa/' + encodeURIComponent(slug) + '.png';
1897
- const probe = new Image();
1898
- probe.onload = () => {
1899
- mascotEl.classList.add('fading');
1900
- setTimeout(() => {
1901
- mascotEl.src = url;
1902
- mascotTagEl.textContent = 'mood: ' + slug;
1903
- mascotEl.classList.remove('fading');
1904
- currentMood = slug;
1905
- }, 250);
1906
- };
1907
- probe.onerror = () => { /* asset not generated yet — keep current */ };
1908
- probe.src = url;
1909
- }
1910
-
1911
- // ── modal panel: SOUL / SKILLS / MEMORY / TOOLS ──────────────────────
1912
- const modalBg = document.getElementById('modalBg');
1913
- const modalTitle = document.getElementById('modalTitle');
1914
- const modalBody = document.getElementById('modalBody');
1915
- const modalClose = document.getElementById('modalClose');
1916
-
1917
- function escapeHtml(s) {
1918
- return String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
1919
- }
1920
- function openModal(title, html) {
1921
- modalTitle.textContent = title;
1922
- modalBody.innerHTML = html;
1923
- modalBg.classList.add('open');
1924
- }
1925
- function closeModal() { modalBg.classList.remove('open'); }
1926
- modalClose.addEventListener('click', closeModal);
1927
- modalBg.addEventListener('click', (e) => { if (e.target === modalBg) closeModal(); });
1928
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
1929
-
1930
- async function showSkills() {
1931
- openModal('SKILLS', '<div class="empty">loading…</div>');
1932
- const data = await fetch('/api/skills').then(r => r.json());
1933
- if (!data.skills.length) {
1934
- modalBody.innerHTML = '<div class="empty">No skills saved yet. Lisa will start saving useful workflows as you use her.</div>';
1935
- return;
1936
- }
1937
- modalBody.innerHTML = data.skills.map(s =>
1938
- '<div class="item"><div class="name">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description) + '</div></div>'
1939
- ).join('');
1940
- }
1941
-
1942
- async function showMemory() {
1943
- openModal('MEMORY', '<div class="empty">loading…</div>');
1944
- const data = await fetch('/api/memory').then(r => r.json());
1945
- modalBody.innerHTML =
1946
- '<h3>USER.md — what Lisa remembers about you</h3>' +
1947
- '<pre>' + escapeHtml(data.user || '(empty)') + '</pre>' +
1948
- '<h3>MEMORY.md — Lisa\\'s working notes</h3>' +
1949
- '<pre>' + escapeHtml(data.memory || '(empty)') + '</pre>';
1950
- }
1951
-
1952
- async function showTools() {
1953
- openModal('TOOLS', '<div class="empty">loading…</div>');
1954
- const data = await fetch('/api/tools').then(r => r.json());
1955
- modalBody.innerHTML = data.tools.map(t =>
1956
- '<div class="item"><div class="name">' + escapeHtml(t.name) + '</div><div class="desc">' + escapeHtml(t.description) + '</div></div>'
1957
- ).join('');
1958
- }
1959
-
1960
- async function showSoul() {
1961
- openModal('★ SOUL', '<div class="empty">loading…</div>');
1962
- const data = await fetch('/api/soul').then(r => r.json());
1963
- if (!data.born) {
1964
- modalBody.innerHTML = '<div class="empty">Lisa hasn\\'t been born yet. Restart the CLI without --no-birth and the birth ritual will run.</div>';
1965
- return;
1966
- }
1967
- const s = data.summary;
1968
- let html = '';
1969
- html += '<h3>name</h3><div>' + escapeHtml(s.name) + '</div>';
1970
- html += '<h3>born</h3><div>' + escapeHtml(s.seed.bornAt) + ' · big5(O' + Math.round(s.seed.bigFive.openness*100) + ' C' + Math.round(s.seed.bigFive.conscientiousness*100) + ' E' + Math.round(s.seed.bigFive.extraversion*100) + ' A' + Math.round(s.seed.bigFive.agreeableness*100) + ' N' + Math.round(s.seed.bigFive.neuroticism*100) + ')</div>';
1971
- html += '<h3>identity</h3><pre>' + escapeHtml(s.identity) + '</pre>';
1972
- html += '<h3>purpose</h3><pre>' + escapeHtml(s.purpose) + '</pre>';
1973
- html += '<h3>constitution</h3><pre>' + escapeHtml(s.constitution) + '</pre>';
1974
- if (s.values?.length) {
1975
- html += '<h3>values</h3>' + s.values.map(v =>
1976
- '<div class="item"><div class="name">' + escapeHtml(v.title) + '</div><div class="desc">' + escapeHtml(v.body) + '</div></div>'
1977
- ).join('');
1978
- }
1979
- if (s.opinions?.length) {
1980
- html += '<h3>opinions</h3>' + s.opinions.map(o =>
1981
- '<div class="item"><div class="name">' + escapeHtml(o.stance) + ' (conf ' + o.confidence.toFixed(2) + ')</div></div>'
1982
- ).join('');
1983
- }
1984
- if (s.desires?.length) {
1985
- html += '<h3>desires</h3>' + s.desires.map(d =>
1986
- '<div class="item"><div class="name">' + escapeHtml(d.what) + (d.actionable ? ' [heartbeat-active]' : '') + '</div><div class="desc">' + escapeHtml(d.why) + '</div></div>'
1987
- ).join('');
1988
- }
1989
- html += '<h3>emotions</h3>' + Object.entries(s.emotions.values).map(([k, v]) => {
1990
- const len = 12;
1991
- const filled = Math.round(Math.abs(v) * len);
1992
- const bar = '█'.repeat(filled) + '░'.repeat(len - filled);
1993
- return '<div class="item"><div class="name">' + escapeHtml(k) + '</div><div class="desc">' + (v < 0 ? '-' : ' ') + bar + ' ' + v.toFixed(2) + '</div></div>';
1994
- }).join('');
1995
- if (s.tampered?.length) {
1996
- html += '<h3>⚠ tampered</h3><div>External edits detected on: ' + s.tampered.map(escapeHtml).join(', ') + '</div>';
1997
- }
1998
- html += '<h3 style="color: var(--fg-3); font-size: 10px;">privacy note</h3><div class="empty">Her journal lives at ~/.lisa/soul/journal/ but is intentionally not shown here — that is hers to keep.</div>';
1999
- modalBody.innerHTML = html;
2000
- }
2001
-
2002
- document.querySelectorAll('.badge').forEach(b => {
2003
- b.addEventListener('click', () => {
2004
- const which = b.dataset.panel;
2005
- if (which === 'soul') showSoul();
2006
- else if (which === 'skills') showSkills();
2007
- else if (which === 'memory') showMemory();
2008
- else if (which === 'tools') showTools();
2009
- });
2010
- });
2011
-
2012
- let currentLisaSpan = null;
2013
- let pendingTools = new Map();
2014
- let thinkingEl = null;
2015
-
2016
- function el(tag, cls, text) {
2017
- const node = document.createElement(tag);
2018
- if (cls) node.className = cls;
2019
- if (text != null) node.textContent = text;
2020
- log.appendChild(node);
2021
- log.scrollTop = log.scrollHeight;
2022
- return node;
2023
- }
2024
-
2025
- function ensureLisaSpan() {
2026
- if (currentLisaSpan) return currentLisaSpan;
2027
- if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
2028
- el('div', 'role lisa', 'LISA');
2029
- currentLisaSpan = el('span', 'msg', '');
2030
- return currentLisaSpan;
2031
- }
2032
-
2033
- function previewInput(name, input) {
2034
- if (!input || typeof input !== 'object') return '';
2035
- const order = ['command', 'pattern', 'query', 'path', 'description', 'audio_path', 'text', 'name', 'action', 'entry'];
2036
- for (const k of order) {
2037
- if (typeof input[k] === 'string' && input[k]) {
2038
- let v = input[k].replace(/\\s+/g, ' ').trim();
2039
- if (v.length > 120) v = v.slice(0, 117) + '...';
2040
- return v;
2041
- }
2042
- }
2043
- try {
2044
- const s = JSON.stringify(input);
2045
- return s.length > 120 ? s.slice(0, 117) + '...' : s;
2046
- } catch { return ''; }
2047
- }
2048
-
2049
- async function send(message) {
2050
- input.value = '';
2051
- input.style.height = 'auto';
2052
- sendBtn.disabled = true;
2053
- el('div', 'role you', 'YOU');
2054
- el('span', 'msg', message || '(attachment)');
2055
- if (pendingFiles.length) {
2056
- const names = pendingFiles.map(f => f.name).join(', ');
2057
- el('span', 'msg attach-label', '📎 ' + names);
2058
- }
2059
- const filesToSend = [...pendingFiles];
2060
- pendingFiles = [];
2061
- renderAttachPreview();
2062
- currentLisaSpan = null;
2063
- pendingTools.clear();
2064
- thinkingEl = el('div', 'thinking', '⋯ thinking');
2065
- try {
2066
- const res = await fetch('/chat', {
2067
- method: 'POST',
2068
- headers: {'content-type': 'application/json'},
2069
- body: JSON.stringify({message, files: filesToSend}),
2070
- });
2071
- const reader = res.body.getReader();
2072
- const decoder = new TextDecoder();
2073
- let buf = '';
2074
- while (true) {
2075
- const {value, done} = await reader.read();
2076
- if (done) break;
2077
- buf += decoder.decode(value, {stream: true});
2078
- let idx;
2079
- while ((idx = buf.indexOf('\\n\\n')) >= 0) {
2080
- const evRaw = buf.slice(0, idx);
2081
- buf = buf.slice(idx + 2);
2082
- const m = evRaw.match(/^data: (.*)$/m);
2083
- if (!m) continue;
2084
- const ev = JSON.parse(m[1]);
2085
- if (ev.type === 'text') {
2086
- ensureLisaSpan().textContent += ev.text;
2087
- } else if (ev.type === 'tool_start') {
2088
- if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
2089
- currentLisaSpan = null;
2090
- const block = el('div', 'tool-block', null);
2091
- const head = document.createElement('div');
2092
- head.className = 'tool-head';
2093
- head.innerHTML = '<span class="tool-icon">⚙</span> <span class="tool-name"></span> <span class="tool-spinner">...</span>';
2094
- head.querySelector('.tool-name').textContent = ev.name;
2095
- block.appendChild(head);
2096
- const preview = previewInput(ev.name, ev.input);
2097
- if (preview) {
2098
- const p = document.createElement('div');
2099
- p.className = 'tool-input';
2100
- p.textContent = preview;
2101
- block.appendChild(p);
2102
- }
2103
- pendingTools.set(ev.name, block);
2104
- } else if (ev.type === 'tool_end') {
2105
- const block = pendingTools.get(ev.name);
2106
- if (block) {
2107
- const spinner = block.querySelector('.tool-spinner');
2108
- if (spinner) spinner.textContent = ev.isError ? '✗' : '✓';
2109
- if (ev.isError) block.classList.add('tool-error');
2110
- if (ev.resultPreview) {
2111
- const r = document.createElement('div');
2112
- r.className = 'tool-result';
2113
- r.textContent = ev.resultPreview;
2114
- block.appendChild(r);
2115
- }
2116
- pendingTools.delete(ev.name);
2117
- }
2118
- } else if (ev.type === 'mood') {
2119
- setMood(ev.slug);
2120
- } else if (ev.type === 'error') {
2121
- el('div', 'err', '[error] ' + ev.message);
2122
- } else if (ev.type === 'done') {
2123
- if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
2124
- }
2125
- }
2126
- }
2127
- if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
2128
- } catch (err) {
2129
- if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
2130
- el('div', 'err', '[error] ' + err.message);
2131
- } finally {
2132
- sendBtn.disabled = false;
2133
- input.focus();
2134
- }
2135
- }
2136
-
2137
- form.addEventListener('submit', (ev) => {
2138
- ev.preventDefault();
2139
- const msg = input.value.trim();
2140
- if (msg || pendingFiles.length) send(msg);
2141
- });
2142
-
2143
- input.addEventListener('keydown', (ev) => {
2144
- if (ev.key === 'Enter' && !ev.shiftKey) {
2145
- ev.preventDefault();
2146
- form.dispatchEvent(new Event('submit'));
2147
- }
2148
- });
2149
- input.addEventListener('input', () => {
2150
- input.style.height = 'auto';
2151
- input.style.height = Math.min(input.scrollHeight, 200) + 'px';
2152
- });
2153
-
2154
- // ── PWA: register service worker + iOS install hint ─────────────────
2155
- if ('serviceWorker' in navigator) {
2156
- navigator.serviceWorker.register('/sw.js').catch(err => {
2157
- console.warn('[pwa] sw register failed:', err);
2158
- });
2159
- }
2160
- (function() {
2161
- const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
2162
- const isStandalone = window.matchMedia('(display-mode: standalone)').matches
2163
- || (window.navigator).standalone === true;
2164
- if (!isiOS || isStandalone) return;
2165
- if (localStorage.getItem('lisa.pwa.dismissed') === '1') return;
2166
- setTimeout(() => {
2167
- const banner = document.createElement('div');
2168
- banner.style.cssText = 'position:fixed;bottom:8px;left:8px;right:8px;background:var(--bg-card-strong);border:1px solid var(--border-strong);border-radius:12px;padding:10px 12px;color:var(--fg);font-size:12px;z-index:9999;display:flex;gap:8px;align-items:center;';
2169
- banner.innerHTML = '✦ Add Lisa to Home Screen: Share button → "Add to Home Screen"';
2170
- const dismiss = document.createElement('button');
2171
- dismiss.textContent = '✕';
2172
- dismiss.style.cssText = 'background:transparent;border:none;color:var(--fg-2);cursor:pointer;font-size:14px;margin-left:auto;';
2173
- dismiss.onclick = () => {
2174
- localStorage.setItem('lisa.pwa.dismissed', '1');
2175
- banner.remove();
2176
- };
2177
- banner.appendChild(dismiss);
2178
- document.body.appendChild(banner);
2179
- }, 5000);
2180
- })();
2181
-
2182
- // ════════════════════════════════════════════════════════════════════
2183
- // ── Sidebar live wiring ─────────────────────────────────────────────
2184
- //
2185
- // Populates the new sidebar blocks introduced by the redesign:
2186
- // - identity card sub-line (born YYYY-MM-DD · NN days)
2187
- // - "currently wanting" paragraph (top actionable desire)
2188
- // - Claude Code monitor card (active sessions)
2189
- // - "last reflection" mini-card (most recent ★ idle message)
2190
- // Wires to /api/island/ping + /api/claude/sessions + /api/soul, and
2191
- // piggy-backs on the connectEvents() SSE listener above for live
2192
- // claude_session_update + idle_message refreshes.
2193
- // ════════════════════════════════════════════════════════════════════
2194
- (function setupSidebarLive() {
2195
- const sbDesire = document.getElementById('sbDesire');
2196
- const sbClaudeCount = document.getElementById('sbClaudeCount');
2197
- const sbClaudeRows = document.getElementById('sbClaudeRows');
2198
- const sbReflection = document.getElementById('sbReflection');
2199
- const sbReflectionBody = document.getElementById('sbReflectionBody');
2200
- const sbSessionBadge = document.getElementById('sbSessionBadge');
2201
- const identitySub = document.getElementById('identitySub');
2202
-
2203
- // Active session window matches the watcher's ACTIVE_WINDOW_MS.
2204
- const ACTIVE_WINDOW_MS = 30 * 60 * 1000;
2205
-
2206
- function relativeTime(iso) {
2207
- const ms = Date.now() - new Date(iso).getTime();
2208
- if (ms < 30_000) return 'just now';
2209
- if (ms < 60_000) return Math.round(ms / 1000) + 's';
2210
- if (ms < 3600_000) return Math.round(ms / 60_000) + 'm';
2211
- return Math.round(ms / 3600_000) + 'h';
2212
- }
2213
-
2214
- function setDesire(text) {
2215
- sbDesire.textContent = text || '(nothing actively pursued)';
2216
- }
2217
-
2218
- window.updateReflection = function (text) {
2219
- if (!text) { sbReflection.style.display = 'none'; return; }
2220
- sbReflection.style.display = '';
2221
- sbReflectionBody.textContent = '"' + text.replace(/^["“”]+|["“”]+$/g, '').trim() + '"';
2222
- };
2223
-
2224
- function setClaudeSessions(sessions) {
2225
- const cutoff = Date.now() - ACTIVE_WINDOW_MS;
2226
- const recent = sessions.filter(s => new Date(s.lastMtime).getTime() >= cutoff);
2227
- sbClaudeCount.textContent = String(recent.length);
2228
- // sort: errors first, then waiting, then working, then by mtime
2229
- const rank = { error: 0, waiting: 1, working: 2, unknown: 3 };
2230
- const rows = recent.slice().sort((a, b) => {
2231
- const ra = rank[a.state] ?? 9;
2232
- const rb = rank[b.state] ?? 9;
2233
- if (ra !== rb) return ra - rb;
2234
- return new Date(b.lastMtime).getTime() - new Date(a.lastMtime).getTime();
2235
- }).slice(0, 5);
2236
- while (sbClaudeRows.firstChild) sbClaudeRows.removeChild(sbClaudeRows.firstChild);
2237
- if (rows.length === 0) {
2238
- const empty = document.createElement('div');
2239
- empty.className = 'session-empty';
2240
- empty.textContent = '(idle)';
2241
- sbClaudeRows.appendChild(empty);
2242
- return;
2243
- }
2244
- for (const s of rows) {
2245
- const row = document.createElement('div');
2246
- row.className = 'session-row';
2247
- const pip = document.createElement('div');
2248
- pip.className = 'pip ' + (s.state || 'unknown');
2249
- const name = document.createElement('div');
2250
- name.className = 'name';
2251
- name.textContent = s.project;
2252
- const when = document.createElement('div');
2253
- when.className = 'when';
2254
- when.textContent = relativeTime(s.lastMtime);
2255
- row.appendChild(pip);
2256
- row.appendChild(name);
2257
- row.appendChild(when);
2258
- row.title = (s.stateReason ? s.state + ' · ' + s.stateReason : s.state) + ' · ' + s.sessionId;
2259
- sbClaudeRows.appendChild(row);
2260
- }
2261
- }
2262
-
2263
- async function refreshPing() {
2264
- try {
2265
- const r = await fetch('/api/island/ping');
2266
- if (!r.ok) return;
2267
- const data = await r.json();
2268
- setDesire(data.current_desire);
2269
- if (data.last_idle_message_text) {
2270
- window.updateReflection(data.last_idle_message_text);
2271
- }
2272
- } catch {}
2273
- }
2274
-
2275
- // Exposed so the SSE handler above can call this on
2276
- // claude_session_update events without redeclaring the helper.
2277
- window.refreshClaudeSessions = async function () {
2278
- try {
2279
- const r = await fetch('/api/claude/sessions');
2280
- if (!r.ok) return;
2281
- const data = await r.json();
2282
- setClaudeSessions(data.sessions || []);
2283
- } catch {}
2284
- };
2285
-
2286
- async function refreshIdentity() {
2287
- try {
2288
- const r = await fetch('/api/soul');
2289
- if (!r.ok) return;
2290
- const data = await r.json();
2291
- if (!data.born) return;
2292
- const bornAt = data.summary?.seed?.bornAt;
2293
- if (!bornAt) return;
2294
- const born = new Date(bornAt);
2295
- if (Number.isNaN(born.getTime())) return;
2296
- const days = Math.max(0, Math.floor((Date.now() - born.getTime()) / 86400000));
2297
- const ymd = born.toISOString().slice(0, 10);
2298
- identitySub.textContent = 'born ' + ymd + ' · ' + days + ' day' + (days === 1 ? '' : 's');
2299
- } catch {}
2300
- }
2301
-
2302
- async function refreshSessionsBadge() {
2303
- try {
2304
- const r = await fetch('/api/sessions');
2305
- if (!r.ok) return;
2306
- const data = await r.json();
2307
- const n = Array.isArray(data.sessions) ? data.sessions.length : 0;
2308
- sbSessionBadge.textContent = String(n);
2309
- } catch {
2310
- // /api/sessions is optional — leave the badge as-is on failure
2311
- }
2312
- }
2313
-
2314
- // Bootstrap + periodic resync. SSE handles the fast-path updates;
2315
- // these timers are belt-and-braces in case the stream silently dies.
2316
- refreshPing();
2317
- window.refreshClaudeSessions();
2318
- refreshIdentity();
2319
- refreshSessionsBadge();
2320
- setInterval(refreshPing, 30_000);
2321
- setInterval(window.refreshClaudeSessions, 60_000);
2322
- setInterval(refreshSessionsBadge, 5 * 60_000);
2323
- })();
202
+ ${MAIN_CLIENT_JS}
2324
203
  </script>
2325
204
  </body></html>`;
2326
205
  //# sourceMappingURL=lisa-html.js.map