@runcore-sh/runcore 0.1.2

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 (1112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/dist/activity/log.d.ts +37 -0
  4. package/dist/activity/log.d.ts.map +1 -0
  5. package/dist/activity/log.js +259 -0
  6. package/dist/activity/log.js.map +1 -0
  7. package/dist/adapters/storage/gdrive-backup.d.ts +20 -0
  8. package/dist/adapters/storage/gdrive-backup.d.ts.map +1 -0
  9. package/dist/adapters/storage/gdrive-backup.js +244 -0
  10. package/dist/adapters/storage/gdrive-backup.js.map +1 -0
  11. package/dist/adapters/storage/local.d.ts +19 -0
  12. package/dist/adapters/storage/local.d.ts.map +1 -0
  13. package/dist/adapters/storage/local.js +101 -0
  14. package/dist/adapters/storage/local.js.map +1 -0
  15. package/dist/adapters/storage/types.d.ts +44 -0
  16. package/dist/adapters/storage/types.d.ts.map +1 -0
  17. package/dist/adapters/storage/types.js +6 -0
  18. package/dist/adapters/storage/types.js.map +1 -0
  19. package/dist/agents/autonomous.d.ts +67 -0
  20. package/dist/agents/autonomous.d.ts.map +1 -0
  21. package/dist/agents/autonomous.js +710 -0
  22. package/dist/agents/autonomous.js.map +1 -0
  23. package/dist/agents/commit.d.ts +22 -0
  24. package/dist/agents/commit.d.ts.map +1 -0
  25. package/dist/agents/commit.js +120 -0
  26. package/dist/agents/commit.js.map +1 -0
  27. package/dist/agents/continue.d.ts +19 -0
  28. package/dist/agents/continue.d.ts.map +1 -0
  29. package/dist/agents/continue.js +158 -0
  30. package/dist/agents/continue.js.map +1 -0
  31. package/dist/agents/cooldown.d.ts +127 -0
  32. package/dist/agents/cooldown.d.ts.map +1 -0
  33. package/dist/agents/cooldown.js +396 -0
  34. package/dist/agents/cooldown.js.map +1 -0
  35. package/dist/agents/dedup-guard.d.ts +15 -0
  36. package/dist/agents/dedup-guard.d.ts.map +1 -0
  37. package/dist/agents/dedup-guard.js +128 -0
  38. package/dist/agents/dedup-guard.js.map +1 -0
  39. package/dist/agents/index.d.ts +34 -0
  40. package/dist/agents/index.d.ts.map +1 -0
  41. package/dist/agents/index.js +51 -0
  42. package/dist/agents/index.js.map +1 -0
  43. package/dist/agents/instance-manager.d.ts +262 -0
  44. package/dist/agents/instance-manager.d.ts.map +1 -0
  45. package/dist/agents/instance-manager.js +850 -0
  46. package/dist/agents/instance-manager.js.map +1 -0
  47. package/dist/agents/locks.d.ts +81 -0
  48. package/dist/agents/locks.d.ts.map +1 -0
  49. package/dist/agents/locks.js +234 -0
  50. package/dist/agents/locks.js.map +1 -0
  51. package/dist/agents/memory.d.ts +37 -0
  52. package/dist/agents/memory.d.ts.map +1 -0
  53. package/dist/agents/memory.js +92 -0
  54. package/dist/agents/memory.js.map +1 -0
  55. package/dist/agents/monitor.d.ts +16 -0
  56. package/dist/agents/monitor.d.ts.map +1 -0
  57. package/dist/agents/monitor.js +235 -0
  58. package/dist/agents/monitor.js.map +1 -0
  59. package/dist/agents/orchestration.d.ts +218 -0
  60. package/dist/agents/orchestration.d.ts.map +1 -0
  61. package/dist/agents/orchestration.js +715 -0
  62. package/dist/agents/orchestration.js.map +1 -0
  63. package/dist/agents/recover.d.ts +30 -0
  64. package/dist/agents/recover.d.ts.map +1 -0
  65. package/dist/agents/recover.js +166 -0
  66. package/dist/agents/recover.js.map +1 -0
  67. package/dist/agents/reflection.d.ts +36 -0
  68. package/dist/agents/reflection.d.ts.map +1 -0
  69. package/dist/agents/reflection.js +198 -0
  70. package/dist/agents/reflection.js.map +1 -0
  71. package/dist/agents/runtime/bus.d.ts +46 -0
  72. package/dist/agents/runtime/bus.d.ts.map +1 -0
  73. package/dist/agents/runtime/bus.js +174 -0
  74. package/dist/agents/runtime/bus.js.map +1 -0
  75. package/dist/agents/runtime/config.d.ts +14 -0
  76. package/dist/agents/runtime/config.d.ts.map +1 -0
  77. package/dist/agents/runtime/config.js +100 -0
  78. package/dist/agents/runtime/config.js.map +1 -0
  79. package/dist/agents/runtime/driver.d.ts +25 -0
  80. package/dist/agents/runtime/driver.d.ts.map +1 -0
  81. package/dist/agents/runtime/driver.js +215 -0
  82. package/dist/agents/runtime/driver.js.map +1 -0
  83. package/dist/agents/runtime/errors.d.ts +30 -0
  84. package/dist/agents/runtime/errors.d.ts.map +1 -0
  85. package/dist/agents/runtime/errors.js +40 -0
  86. package/dist/agents/runtime/errors.js.map +1 -0
  87. package/dist/agents/runtime/index.d.ts +29 -0
  88. package/dist/agents/runtime/index.d.ts.map +1 -0
  89. package/dist/agents/runtime/index.js +54 -0
  90. package/dist/agents/runtime/index.js.map +1 -0
  91. package/dist/agents/runtime/lifecycle.d.ts +46 -0
  92. package/dist/agents/runtime/lifecycle.d.ts.map +1 -0
  93. package/dist/agents/runtime/lifecycle.js +116 -0
  94. package/dist/agents/runtime/lifecycle.js.map +1 -0
  95. package/dist/agents/runtime/manager.d.ts +129 -0
  96. package/dist/agents/runtime/manager.d.ts.map +1 -0
  97. package/dist/agents/runtime/manager.js +947 -0
  98. package/dist/agents/runtime/manager.js.map +1 -0
  99. package/dist/agents/runtime/registry.d.ts +66 -0
  100. package/dist/agents/runtime/registry.d.ts.map +1 -0
  101. package/dist/agents/runtime/registry.js +195 -0
  102. package/dist/agents/runtime/registry.js.map +1 -0
  103. package/dist/agents/runtime/resources.d.ts +49 -0
  104. package/dist/agents/runtime/resources.d.ts.map +1 -0
  105. package/dist/agents/runtime/resources.js +146 -0
  106. package/dist/agents/runtime/resources.js.map +1 -0
  107. package/dist/agents/runtime/types.d.ts +168 -0
  108. package/dist/agents/runtime/types.d.ts.map +1 -0
  109. package/dist/agents/runtime/types.js +24 -0
  110. package/dist/agents/runtime/types.js.map +1 -0
  111. package/dist/agents/runtime.d.ts +240 -0
  112. package/dist/agents/runtime.d.ts.map +1 -0
  113. package/dist/agents/runtime.js +577 -0
  114. package/dist/agents/runtime.js.map +1 -0
  115. package/dist/agents/spawn.d.ts +49 -0
  116. package/dist/agents/spawn.d.ts.map +1 -0
  117. package/dist/agents/spawn.js +975 -0
  118. package/dist/agents/spawn.js.map +1 -0
  119. package/dist/agents/store.d.ts +29 -0
  120. package/dist/agents/store.d.ts.map +1 -0
  121. package/dist/agents/store.js +174 -0
  122. package/dist/agents/store.js.map +1 -0
  123. package/dist/agents/triage.d.ts +23 -0
  124. package/dist/agents/triage.d.ts.map +1 -0
  125. package/dist/agents/triage.js +81 -0
  126. package/dist/agents/triage.js.map +1 -0
  127. package/dist/agents/types.d.ts +37 -0
  128. package/dist/agents/types.d.ts.map +1 -0
  129. package/dist/agents/types.js +2 -0
  130. package/dist/agents/types.js.map +1 -0
  131. package/dist/agents/workflow.d.ts +137 -0
  132. package/dist/agents/workflow.d.ts.map +1 -0
  133. package/dist/agents/workflow.js +542 -0
  134. package/dist/agents/workflow.js.map +1 -0
  135. package/dist/auth/crypto.d.ts +22 -0
  136. package/dist/auth/crypto.d.ts.map +1 -0
  137. package/dist/auth/crypto.js +42 -0
  138. package/dist/auth/crypto.js.map +1 -0
  139. package/dist/auth/identity.d.ts +89 -0
  140. package/dist/auth/identity.d.ts.map +1 -0
  141. package/dist/auth/identity.js +264 -0
  142. package/dist/auth/identity.js.map +1 -0
  143. package/dist/avatar/client.d.ts +34 -0
  144. package/dist/avatar/client.d.ts.map +1 -0
  145. package/dist/avatar/client.js +172 -0
  146. package/dist/avatar/client.js.map +1 -0
  147. package/dist/avatar/sidecar.d.ts +16 -0
  148. package/dist/avatar/sidecar.d.ts.map +1 -0
  149. package/dist/avatar/sidecar.js +125 -0
  150. package/dist/avatar/sidecar.js.map +1 -0
  151. package/dist/board/provider.d.ts +13 -0
  152. package/dist/board/provider.d.ts.map +1 -0
  153. package/dist/board/provider.js +19 -0
  154. package/dist/board/provider.js.map +1 -0
  155. package/dist/board/types.d.ts +76 -0
  156. package/dist/board/types.d.ts.map +1 -0
  157. package/dist/board/types.js +7 -0
  158. package/dist/board/types.js.map +1 -0
  159. package/dist/brain/skills.d.ts +177 -0
  160. package/dist/brain/skills.d.ts.map +1 -0
  161. package/dist/brain/skills.js +452 -0
  162. package/dist/brain/skills.js.map +1 -0
  163. package/dist/brain.d.ts +42 -0
  164. package/dist/brain.d.ts.map +1 -0
  165. package/dist/brain.js +98 -0
  166. package/dist/brain.js.map +1 -0
  167. package/dist/browser/sessions.d.ts +23 -0
  168. package/dist/browser/sessions.d.ts.map +1 -0
  169. package/dist/browser/sessions.js +121 -0
  170. package/dist/browser/sessions.js.map +1 -0
  171. package/dist/cache/file.d.ts +56 -0
  172. package/dist/cache/file.d.ts.map +1 -0
  173. package/dist/cache/file.js +176 -0
  174. package/dist/cache/file.js.map +1 -0
  175. package/dist/cache/index.d.ts +64 -0
  176. package/dist/cache/index.d.ts.map +1 -0
  177. package/dist/cache/index.js +108 -0
  178. package/dist/cache/index.js.map +1 -0
  179. package/dist/cache/keys.d.ts +29 -0
  180. package/dist/cache/keys.d.ts.map +1 -0
  181. package/dist/cache/keys.js +52 -0
  182. package/dist/cache/keys.js.map +1 -0
  183. package/dist/cache/llm-cache.d.ts +70 -0
  184. package/dist/cache/llm-cache.d.ts.map +1 -0
  185. package/dist/cache/llm-cache.js +165 -0
  186. package/dist/cache/llm-cache.js.map +1 -0
  187. package/dist/cache/memory.d.ts +53 -0
  188. package/dist/cache/memory.d.ts.map +1 -0
  189. package/dist/cache/memory.js +114 -0
  190. package/dist/cache/memory.js.map +1 -0
  191. package/dist/calendar/google-adapter.d.ts +16 -0
  192. package/dist/calendar/google-adapter.d.ts.map +1 -0
  193. package/dist/calendar/google-adapter.js +163 -0
  194. package/dist/calendar/google-adapter.js.map +1 -0
  195. package/dist/calendar/index.d.ts +8 -0
  196. package/dist/calendar/index.d.ts.map +1 -0
  197. package/dist/calendar/index.js +7 -0
  198. package/dist/calendar/index.js.map +1 -0
  199. package/dist/calendar/routes.d.ts +7 -0
  200. package/dist/calendar/routes.d.ts.map +1 -0
  201. package/dist/calendar/routes.js +199 -0
  202. package/dist/calendar/routes.js.map +1 -0
  203. package/dist/calendar/store.d.ts +73 -0
  204. package/dist/calendar/store.d.ts.map +1 -0
  205. package/dist/calendar/store.js +373 -0
  206. package/dist/calendar/store.js.map +1 -0
  207. package/dist/calendar/types.d.ts +99 -0
  208. package/dist/calendar/types.d.ts.map +1 -0
  209. package/dist/calendar/types.js +7 -0
  210. package/dist/calendar/types.js.map +1 -0
  211. package/dist/capabilities/definitions/board.d.ts +7 -0
  212. package/dist/capabilities/definitions/board.d.ts.map +1 -0
  213. package/dist/capabilities/definitions/board.js +232 -0
  214. package/dist/capabilities/definitions/board.js.map +1 -0
  215. package/dist/capabilities/definitions/browser.d.ts +18 -0
  216. package/dist/capabilities/definitions/browser.d.ts.map +1 -0
  217. package/dist/capabilities/definitions/browser.js +242 -0
  218. package/dist/capabilities/definitions/browser.js.map +1 -0
  219. package/dist/capabilities/definitions/calendar-context.d.ts +8 -0
  220. package/dist/capabilities/definitions/calendar-context.d.ts.map +1 -0
  221. package/dist/capabilities/definitions/calendar-context.js +41 -0
  222. package/dist/capabilities/definitions/calendar-context.js.map +1 -0
  223. package/dist/capabilities/definitions/calendar.d.ts +7 -0
  224. package/dist/capabilities/definitions/calendar.d.ts.map +1 -0
  225. package/dist/capabilities/definitions/calendar.js +173 -0
  226. package/dist/capabilities/definitions/calendar.js.map +1 -0
  227. package/dist/capabilities/definitions/docs.d.ts +6 -0
  228. package/dist/capabilities/definitions/docs.d.ts.map +1 -0
  229. package/dist/capabilities/definitions/docs.js +62 -0
  230. package/dist/capabilities/definitions/docs.js.map +1 -0
  231. package/dist/capabilities/definitions/email-context.d.ts +7 -0
  232. package/dist/capabilities/definitions/email-context.d.ts.map +1 -0
  233. package/dist/capabilities/definitions/email-context.js +55 -0
  234. package/dist/capabilities/definitions/email-context.js.map +1 -0
  235. package/dist/capabilities/definitions/email.d.ts +7 -0
  236. package/dist/capabilities/definitions/email.d.ts.map +1 -0
  237. package/dist/capabilities/definitions/email.js +94 -0
  238. package/dist/capabilities/definitions/email.js.map +1 -0
  239. package/dist/capabilities/definitions/task-done.d.ts +10 -0
  240. package/dist/capabilities/definitions/task-done.d.ts.map +1 -0
  241. package/dist/capabilities/definitions/task-done.js +83 -0
  242. package/dist/capabilities/definitions/task-done.js.map +1 -0
  243. package/dist/capabilities/definitions/vault-context.d.ts +12 -0
  244. package/dist/capabilities/definitions/vault-context.d.ts.map +1 -0
  245. package/dist/capabilities/definitions/vault-context.js +62 -0
  246. package/dist/capabilities/definitions/vault-context.js.map +1 -0
  247. package/dist/capabilities/definitions/web-search-context.d.ts +22 -0
  248. package/dist/capabilities/definitions/web-search-context.d.ts.map +1 -0
  249. package/dist/capabilities/definitions/web-search-context.js +60 -0
  250. package/dist/capabilities/definitions/web-search-context.js.map +1 -0
  251. package/dist/capabilities/index.d.ts +18 -0
  252. package/dist/capabilities/index.d.ts.map +1 -0
  253. package/dist/capabilities/index.js +21 -0
  254. package/dist/capabilities/index.js.map +1 -0
  255. package/dist/capabilities/registry.d.ts +84 -0
  256. package/dist/capabilities/registry.d.ts.map +1 -0
  257. package/dist/capabilities/registry.js +248 -0
  258. package/dist/capabilities/registry.js.map +1 -0
  259. package/dist/capabilities/types.d.ts +157 -0
  260. package/dist/capabilities/types.d.ts.map +1 -0
  261. package/dist/capabilities/types.js +35 -0
  262. package/dist/capabilities/types.js.map +1 -0
  263. package/dist/channels/whatsapp.d.ts +88 -0
  264. package/dist/channels/whatsapp.d.ts.map +1 -0
  265. package/dist/channels/whatsapp.js +200 -0
  266. package/dist/channels/whatsapp.js.map +1 -0
  267. package/dist/cli/backup.d.ts +13 -0
  268. package/dist/cli/backup.d.ts.map +1 -0
  269. package/dist/cli/backup.js +176 -0
  270. package/dist/cli/backup.js.map +1 -0
  271. package/dist/cli.d.ts +12 -0
  272. package/dist/cli.d.ts.map +1 -0
  273. package/dist/cli.js +231 -0
  274. package/dist/cli.js.map +1 -0
  275. package/dist/config/defaults.d.ts +45 -0
  276. package/dist/config/defaults.d.ts.map +1 -0
  277. package/dist/config/defaults.js +54 -0
  278. package/dist/config/defaults.js.map +1 -0
  279. package/dist/contacts/index.d.ts +3 -0
  280. package/dist/contacts/index.d.ts.map +1 -0
  281. package/dist/contacts/index.js +2 -0
  282. package/dist/contacts/index.js.map +1 -0
  283. package/dist/contacts/store.d.ts +58 -0
  284. package/dist/contacts/store.d.ts.map +1 -0
  285. package/dist/contacts/store.js +278 -0
  286. package/dist/contacts/store.js.map +1 -0
  287. package/dist/contacts/types.d.ts +47 -0
  288. package/dist/contacts/types.d.ts.map +1 -0
  289. package/dist/contacts/types.js +5 -0
  290. package/dist/contacts/types.js.map +1 -0
  291. package/dist/context/assembler.d.ts +26 -0
  292. package/dist/context/assembler.d.ts.map +1 -0
  293. package/dist/context/assembler.js +65 -0
  294. package/dist/context/assembler.js.map +1 -0
  295. package/dist/context/compaction.d.ts +34 -0
  296. package/dist/context/compaction.d.ts.map +1 -0
  297. package/dist/context/compaction.js +84 -0
  298. package/dist/context/compaction.js.map +1 -0
  299. package/dist/context/index.d.ts +3 -0
  300. package/dist/context/index.d.ts.map +1 -0
  301. package/dist/context/index.js +2 -0
  302. package/dist/context/index.js.map +1 -0
  303. package/dist/core/registry/index.d.ts +12 -0
  304. package/dist/core/registry/index.d.ts.map +1 -0
  305. package/dist/core/registry/index.js +14 -0
  306. package/dist/core/registry/index.js.map +1 -0
  307. package/dist/core/registry/publisher.d.ts +22 -0
  308. package/dist/core/registry/publisher.d.ts.map +1 -0
  309. package/dist/core/registry/publisher.js +195 -0
  310. package/dist/core/registry/publisher.js.map +1 -0
  311. package/dist/core/registry/registry.d.ts +92 -0
  312. package/dist/core/registry/registry.d.ts.map +1 -0
  313. package/dist/core/registry/registry.js +254 -0
  314. package/dist/core/registry/registry.js.map +1 -0
  315. package/dist/core/registry/search.d.ts +12 -0
  316. package/dist/core/registry/search.d.ts.map +1 -0
  317. package/dist/core/registry/search.js +132 -0
  318. package/dist/core/registry/search.js.map +1 -0
  319. package/dist/core/registry/store.d.ts +55 -0
  320. package/dist/core/registry/store.d.ts.map +1 -0
  321. package/dist/core/registry/store.js +185 -0
  322. package/dist/core/registry/store.js.map +1 -0
  323. package/dist/core/registry/types.d.ts +141 -0
  324. package/dist/core/registry/types.d.ts.map +1 -0
  325. package/dist/core/registry/types.js +30 -0
  326. package/dist/core/registry/types.js.map +1 -0
  327. package/dist/core/registry/versions.d.ts +56 -0
  328. package/dist/core/registry/versions.d.ts.map +1 -0
  329. package/dist/core/registry/versions.js +101 -0
  330. package/dist/core/registry/versions.js.map +1 -0
  331. package/dist/credentials/store.d.ts +59 -0
  332. package/dist/credentials/store.d.ts.map +1 -0
  333. package/dist/credentials/store.js +178 -0
  334. package/dist/credentials/store.js.map +1 -0
  335. package/dist/files/agent-api.d.ts +50 -0
  336. package/dist/files/agent-api.d.ts.map +1 -0
  337. package/dist/files/agent-api.js +126 -0
  338. package/dist/files/agent-api.js.map +1 -0
  339. package/dist/files/compress.d.ts +20 -0
  340. package/dist/files/compress.d.ts.map +1 -0
  341. package/dist/files/compress.js +83 -0
  342. package/dist/files/compress.js.map +1 -0
  343. package/dist/files/extract.d.ts +11 -0
  344. package/dist/files/extract.d.ts.map +1 -0
  345. package/dist/files/extract.js +33 -0
  346. package/dist/files/extract.js.map +1 -0
  347. package/dist/files/gdrive.d.ts +56 -0
  348. package/dist/files/gdrive.d.ts.map +1 -0
  349. package/dist/files/gdrive.js +246 -0
  350. package/dist/files/gdrive.js.map +1 -0
  351. package/dist/files/ingest-folder.d.ts +22 -0
  352. package/dist/files/ingest-folder.d.ts.map +1 -0
  353. package/dist/files/ingest-folder.js +71 -0
  354. package/dist/files/ingest-folder.js.map +1 -0
  355. package/dist/files/ingest.d.ts +13 -0
  356. package/dist/files/ingest.d.ts.map +1 -0
  357. package/dist/files/ingest.js +127 -0
  358. package/dist/files/ingest.js.map +1 -0
  359. package/dist/files/manager.d.ts +117 -0
  360. package/dist/files/manager.d.ts.map +1 -0
  361. package/dist/files/manager.js +306 -0
  362. package/dist/files/manager.js.map +1 -0
  363. package/dist/files/store.d.ts +41 -0
  364. package/dist/files/store.d.ts.map +1 -0
  365. package/dist/files/store.js +271 -0
  366. package/dist/files/store.js.map +1 -0
  367. package/dist/files/templates.d.ts +45 -0
  368. package/dist/files/templates.d.ts.map +1 -0
  369. package/dist/files/templates.js +179 -0
  370. package/dist/files/templates.js.map +1 -0
  371. package/dist/files/types.d.ts +115 -0
  372. package/dist/files/types.d.ts.map +1 -0
  373. package/dist/files/types.js +20 -0
  374. package/dist/files/types.js.map +1 -0
  375. package/dist/files/validate.d.ts +15 -0
  376. package/dist/files/validate.d.ts.map +1 -0
  377. package/dist/files/validate.js +213 -0
  378. package/dist/files/validate.js.map +1 -0
  379. package/dist/files/version.d.ts +31 -0
  380. package/dist/files/version.d.ts.map +1 -0
  381. package/dist/files/version.js +129 -0
  382. package/dist/files/version.js.map +1 -0
  383. package/dist/github/client.d.ts +83 -0
  384. package/dist/github/client.d.ts.map +1 -0
  385. package/dist/github/client.js +408 -0
  386. package/dist/github/client.js.map +1 -0
  387. package/dist/github/commit-analysis.d.ts +30 -0
  388. package/dist/github/commit-analysis.d.ts.map +1 -0
  389. package/dist/github/commit-analysis.js +276 -0
  390. package/dist/github/commit-analysis.js.map +1 -0
  391. package/dist/github/contributor-stats.d.ts +18 -0
  392. package/dist/github/contributor-stats.d.ts.map +1 -0
  393. package/dist/github/contributor-stats.js +119 -0
  394. package/dist/github/contributor-stats.js.map +1 -0
  395. package/dist/github/issue-sla.d.ts +25 -0
  396. package/dist/github/issue-sla.d.ts.map +1 -0
  397. package/dist/github/issue-sla.js +220 -0
  398. package/dist/github/issue-sla.js.map +1 -0
  399. package/dist/github/issue-triage.d.ts +49 -0
  400. package/dist/github/issue-triage.d.ts.map +1 -0
  401. package/dist/github/issue-triage.js +286 -0
  402. package/dist/github/issue-triage.js.map +1 -0
  403. package/dist/github/pr-readiness.d.ts +18 -0
  404. package/dist/github/pr-readiness.d.ts.map +1 -0
  405. package/dist/github/pr-readiness.js +197 -0
  406. package/dist/github/pr-readiness.js.map +1 -0
  407. package/dist/github/pr-review.d.ts +17 -0
  408. package/dist/github/pr-review.d.ts.map +1 -0
  409. package/dist/github/pr-review.js +410 -0
  410. package/dist/github/pr-review.js.map +1 -0
  411. package/dist/github/release-notes.d.ts +32 -0
  412. package/dist/github/release-notes.d.ts.map +1 -0
  413. package/dist/github/release-notes.js +227 -0
  414. package/dist/github/release-notes.js.map +1 -0
  415. package/dist/github/repo-health.d.ts +17 -0
  416. package/dist/github/repo-health.d.ts.map +1 -0
  417. package/dist/github/repo-health.js +303 -0
  418. package/dist/github/repo-health.js.map +1 -0
  419. package/dist/github/retry.d.ts +39 -0
  420. package/dist/github/retry.d.ts.map +1 -0
  421. package/dist/github/retry.js +117 -0
  422. package/dist/github/retry.js.map +1 -0
  423. package/dist/github/types.d.ts +527 -0
  424. package/dist/github/types.d.ts.map +1 -0
  425. package/dist/github/types.js +8 -0
  426. package/dist/github/types.js.map +1 -0
  427. package/dist/github/webhooks.d.ts +36 -0
  428. package/dist/github/webhooks.d.ts.map +1 -0
  429. package/dist/github/webhooks.js +153 -0
  430. package/dist/github/webhooks.js.map +1 -0
  431. package/dist/goals/loop.d.ts +27 -0
  432. package/dist/goals/loop.d.ts.map +1 -0
  433. package/dist/goals/loop.js +239 -0
  434. package/dist/goals/loop.js.map +1 -0
  435. package/dist/goals/notifications.d.ts +20 -0
  436. package/dist/goals/notifications.d.ts.map +1 -0
  437. package/dist/goals/notifications.js +101 -0
  438. package/dist/goals/notifications.js.map +1 -0
  439. package/dist/goals/timer.d.ts +21 -0
  440. package/dist/goals/timer.d.ts.map +1 -0
  441. package/dist/goals/timer.js +60 -0
  442. package/dist/goals/timer.js.map +1 -0
  443. package/dist/google/auth.d.ts +84 -0
  444. package/dist/google/auth.d.ts.map +1 -0
  445. package/dist/google/auth.js +323 -0
  446. package/dist/google/auth.js.map +1 -0
  447. package/dist/google/calendar-timer.d.ts +20 -0
  448. package/dist/google/calendar-timer.d.ts.map +1 -0
  449. package/dist/google/calendar-timer.js +91 -0
  450. package/dist/google/calendar-timer.js.map +1 -0
  451. package/dist/google/calendar.d.ts +126 -0
  452. package/dist/google/calendar.d.ts.map +1 -0
  453. package/dist/google/calendar.js +270 -0
  454. package/dist/google/calendar.js.map +1 -0
  455. package/dist/google/docs.d.ts +87 -0
  456. package/dist/google/docs.d.ts.map +1 -0
  457. package/dist/google/docs.js +309 -0
  458. package/dist/google/docs.js.map +1 -0
  459. package/dist/google/gmail-send.d.ts +58 -0
  460. package/dist/google/gmail-send.d.ts.map +1 -0
  461. package/dist/google/gmail-send.js +219 -0
  462. package/dist/google/gmail-send.js.map +1 -0
  463. package/dist/google/gmail-timer.d.ts +34 -0
  464. package/dist/google/gmail-timer.d.ts.map +1 -0
  465. package/dist/google/gmail-timer.js +223 -0
  466. package/dist/google/gmail-timer.js.map +1 -0
  467. package/dist/google/gmail.d.ts +172 -0
  468. package/dist/google/gmail.d.ts.map +1 -0
  469. package/dist/google/gmail.js +470 -0
  470. package/dist/google/gmail.js.map +1 -0
  471. package/dist/google/tasks-timer.d.ts +20 -0
  472. package/dist/google/tasks-timer.d.ts.map +1 -0
  473. package/dist/google/tasks-timer.js +107 -0
  474. package/dist/google/tasks-timer.js.map +1 -0
  475. package/dist/google/tasks.d.ts +167 -0
  476. package/dist/google/tasks.d.ts.map +1 -0
  477. package/dist/google/tasks.js +331 -0
  478. package/dist/google/tasks.js.map +1 -0
  479. package/dist/google/temporal.d.ts +76 -0
  480. package/dist/google/temporal.d.ts.map +1 -0
  481. package/dist/google/temporal.js +176 -0
  482. package/dist/google/temporal.js.map +1 -0
  483. package/dist/health/alert-defaults.d.ts +12 -0
  484. package/dist/health/alert-defaults.d.ts.map +1 -0
  485. package/dist/health/alert-defaults.js +88 -0
  486. package/dist/health/alert-defaults.js.map +1 -0
  487. package/dist/health/alert-types.d.ts +97 -0
  488. package/dist/health/alert-types.d.ts.map +1 -0
  489. package/dist/health/alert-types.js +8 -0
  490. package/dist/health/alert-types.js.map +1 -0
  491. package/dist/health/alerting.d.ts +66 -0
  492. package/dist/health/alerting.d.ts.map +1 -0
  493. package/dist/health/alerting.js +373 -0
  494. package/dist/health/alerting.js.map +1 -0
  495. package/dist/health/checker.d.ts +32 -0
  496. package/dist/health/checker.d.ts.map +1 -0
  497. package/dist/health/checker.js +138 -0
  498. package/dist/health/checker.js.map +1 -0
  499. package/dist/health/checks/openrouter.d.ts +29 -0
  500. package/dist/health/checks/openrouter.d.ts.map +1 -0
  501. package/dist/health/checks/openrouter.js +75 -0
  502. package/dist/health/checks/openrouter.js.map +1 -0
  503. package/dist/health/checks.d.ts +26 -0
  504. package/dist/health/checks.d.ts.map +1 -0
  505. package/dist/health/checks.js +122 -0
  506. package/dist/health/checks.js.map +1 -0
  507. package/dist/health/components.d.ts +38 -0
  508. package/dist/health/components.d.ts.map +1 -0
  509. package/dist/health/components.js +112 -0
  510. package/dist/health/components.js.map +1 -0
  511. package/dist/health/index.d.ts +19 -0
  512. package/dist/health/index.d.ts.map +1 -0
  513. package/dist/health/index.js +23 -0
  514. package/dist/health/index.js.map +1 -0
  515. package/dist/health/recovery.d.ts +42 -0
  516. package/dist/health/recovery.d.ts.map +1 -0
  517. package/dist/health/recovery.js +138 -0
  518. package/dist/health/recovery.js.map +1 -0
  519. package/dist/health/types.d.ts +68 -0
  520. package/dist/health/types.d.ts.map +1 -0
  521. package/dist/health/types.js +5 -0
  522. package/dist/health/types.js.map +1 -0
  523. package/dist/index.d.ts +25 -0
  524. package/dist/index.d.ts.map +1 -0
  525. package/dist/index.js +22 -0
  526. package/dist/index.js.map +1 -0
  527. package/dist/instance.d.ts +24 -0
  528. package/dist/instance.d.ts.map +1 -0
  529. package/dist/instance.js +48 -0
  530. package/dist/instance.js.map +1 -0
  531. package/dist/integrations/github.d.ts +83 -0
  532. package/dist/integrations/github.d.ts.map +1 -0
  533. package/dist/integrations/github.js +331 -0
  534. package/dist/integrations/github.js.map +1 -0
  535. package/dist/integrations/google-tasks.d.ts +232 -0
  536. package/dist/integrations/google-tasks.d.ts.map +1 -0
  537. package/dist/integrations/google-tasks.js +432 -0
  538. package/dist/integrations/google-tasks.js.map +1 -0
  539. package/dist/learning/extractor.d.ts +28 -0
  540. package/dist/learning/extractor.d.ts.map +1 -0
  541. package/dist/learning/extractor.js +135 -0
  542. package/dist/learning/extractor.js.map +1 -0
  543. package/dist/lib/BasePlugin.d.ts +58 -0
  544. package/dist/lib/BasePlugin.d.ts.map +1 -0
  545. package/dist/lib/BasePlugin.js +181 -0
  546. package/dist/lib/BasePlugin.js.map +1 -0
  547. package/dist/lib/PluginRegistry.d.ts +56 -0
  548. package/dist/lib/PluginRegistry.d.ts.map +1 -0
  549. package/dist/lib/PluginRegistry.js +172 -0
  550. package/dist/lib/PluginRegistry.js.map +1 -0
  551. package/dist/lib/brain-io.d.ts +60 -0
  552. package/dist/lib/brain-io.d.ts.map +1 -0
  553. package/dist/lib/brain-io.js +180 -0
  554. package/dist/lib/brain-io.js.map +1 -0
  555. package/dist/lib/encryption-config.d.ts +16 -0
  556. package/dist/lib/encryption-config.d.ts.map +1 -0
  557. package/dist/lib/encryption-config.js +40 -0
  558. package/dist/lib/encryption-config.js.map +1 -0
  559. package/dist/lib/encryption.d.ts +19 -0
  560. package/dist/lib/encryption.d.ts.map +1 -0
  561. package/dist/lib/encryption.js +65 -0
  562. package/dist/lib/encryption.js.map +1 -0
  563. package/dist/lib/key-store.d.ts +24 -0
  564. package/dist/lib/key-store.d.ts.map +1 -0
  565. package/dist/lib/key-store.js +38 -0
  566. package/dist/lib/key-store.js.map +1 -0
  567. package/dist/lib/schema-migration.d.ts +34 -0
  568. package/dist/lib/schema-migration.d.ts.map +1 -0
  569. package/dist/lib/schema-migration.js +77 -0
  570. package/dist/lib/schema-migration.js.map +1 -0
  571. package/dist/library/brain-shadow.d.ts +10 -0
  572. package/dist/library/brain-shadow.d.ts.map +1 -0
  573. package/dist/library/brain-shadow.js +158 -0
  574. package/dist/library/brain-shadow.js.map +1 -0
  575. package/dist/library/index.d.ts +7 -0
  576. package/dist/library/index.d.ts.map +1 -0
  577. package/dist/library/index.js +6 -0
  578. package/dist/library/index.js.map +1 -0
  579. package/dist/library/routes.d.ts +7 -0
  580. package/dist/library/routes.d.ts.map +1 -0
  581. package/dist/library/routes.js +473 -0
  582. package/dist/library/routes.js.map +1 -0
  583. package/dist/library/store.d.ts +54 -0
  584. package/dist/library/store.d.ts.map +1 -0
  585. package/dist/library/store.js +403 -0
  586. package/dist/library/store.js.map +1 -0
  587. package/dist/library/types.d.ts +56 -0
  588. package/dist/library/types.d.ts.map +1 -0
  589. package/dist/library/types.js +12 -0
  590. package/dist/library/types.js.map +1 -0
  591. package/dist/llm/cache.d.ts +42 -0
  592. package/dist/llm/cache.d.ts.map +1 -0
  593. package/dist/llm/cache.js +104 -0
  594. package/dist/llm/cache.js.map +1 -0
  595. package/dist/llm/complete.d.ts +24 -0
  596. package/dist/llm/complete.d.ts.map +1 -0
  597. package/dist/llm/complete.js +56 -0
  598. package/dist/llm/complete.js.map +1 -0
  599. package/dist/llm/errors.d.ts +28 -0
  600. package/dist/llm/errors.d.ts.map +1 -0
  601. package/dist/llm/errors.js +82 -0
  602. package/dist/llm/errors.js.map +1 -0
  603. package/dist/llm/ollama.d.ts +21 -0
  604. package/dist/llm/ollama.d.ts.map +1 -0
  605. package/dist/llm/ollama.js +116 -0
  606. package/dist/llm/ollama.js.map +1 -0
  607. package/dist/llm/openrouter.d.ts +13 -0
  608. package/dist/llm/openrouter.d.ts.map +1 -0
  609. package/dist/llm/openrouter.js +105 -0
  610. package/dist/llm/openrouter.js.map +1 -0
  611. package/dist/llm/providers/anthropic.d.ts +8 -0
  612. package/dist/llm/providers/anthropic.d.ts.map +1 -0
  613. package/dist/llm/providers/anthropic.js +189 -0
  614. package/dist/llm/providers/anthropic.js.map +1 -0
  615. package/dist/llm/providers/index.d.ts +20 -0
  616. package/dist/llm/providers/index.d.ts.map +1 -0
  617. package/dist/llm/providers/index.js +47 -0
  618. package/dist/llm/providers/index.js.map +1 -0
  619. package/dist/llm/providers/ollama.d.ts +13 -0
  620. package/dist/llm/providers/ollama.d.ts.map +1 -0
  621. package/dist/llm/providers/ollama.js +188 -0
  622. package/dist/llm/providers/ollama.js.map +1 -0
  623. package/dist/llm/providers/openai.d.ts +8 -0
  624. package/dist/llm/providers/openai.d.ts.map +1 -0
  625. package/dist/llm/providers/openai.js +144 -0
  626. package/dist/llm/providers/openai.js.map +1 -0
  627. package/dist/llm/providers/openrouter.d.ts +7 -0
  628. package/dist/llm/providers/openrouter.d.ts.map +1 -0
  629. package/dist/llm/providers/openrouter.js +158 -0
  630. package/dist/llm/providers/openrouter.js.map +1 -0
  631. package/dist/llm/providers/types.d.ts +29 -0
  632. package/dist/llm/providers/types.d.ts.map +1 -0
  633. package/dist/llm/providers/types.js +6 -0
  634. package/dist/llm/providers/types.js.map +1 -0
  635. package/dist/llm/retry.d.ts +29 -0
  636. package/dist/llm/retry.d.ts.map +1 -0
  637. package/dist/llm/retry.js +139 -0
  638. package/dist/llm/retry.js.map +1 -0
  639. package/dist/memory/file-backed.d.ts +36 -0
  640. package/dist/memory/file-backed.d.ts.map +1 -0
  641. package/dist/memory/file-backed.js +178 -0
  642. package/dist/memory/file-backed.js.map +1 -0
  643. package/dist/memory/index.d.ts +7 -0
  644. package/dist/memory/index.d.ts.map +1 -0
  645. package/dist/memory/index.js +6 -0
  646. package/dist/memory/index.js.map +1 -0
  647. package/dist/memory/long-term.d.ts +38 -0
  648. package/dist/memory/long-term.d.ts.map +1 -0
  649. package/dist/memory/long-term.js +58 -0
  650. package/dist/memory/long-term.js.map +1 -0
  651. package/dist/memory/vector-index.d.ts +55 -0
  652. package/dist/memory/vector-index.d.ts.map +1 -0
  653. package/dist/memory/vector-index.js +207 -0
  654. package/dist/memory/vector-index.js.map +1 -0
  655. package/dist/memory/visual.d.ts +53 -0
  656. package/dist/memory/visual.d.ts.map +1 -0
  657. package/dist/memory/visual.js +218 -0
  658. package/dist/memory/visual.js.map +1 -0
  659. package/dist/memory/working.d.ts +12 -0
  660. package/dist/memory/working.d.ts.map +1 -0
  661. package/dist/memory/working.js +34 -0
  662. package/dist/memory/working.js.map +1 -0
  663. package/dist/metrics/aggregator.d.ts +58 -0
  664. package/dist/metrics/aggregator.d.ts.map +1 -0
  665. package/dist/metrics/aggregator.js +253 -0
  666. package/dist/metrics/aggregator.js.map +1 -0
  667. package/dist/metrics/collector.d.ts +26 -0
  668. package/dist/metrics/collector.d.ts.map +1 -0
  669. package/dist/metrics/collector.js +346 -0
  670. package/dist/metrics/collector.js.map +1 -0
  671. package/dist/metrics/firewall-metrics.d.ts +95 -0
  672. package/dist/metrics/firewall-metrics.d.ts.map +1 -0
  673. package/dist/metrics/firewall-metrics.js +261 -0
  674. package/dist/metrics/firewall-metrics.js.map +1 -0
  675. package/dist/metrics/index.d.ts +20 -0
  676. package/dist/metrics/index.d.ts.map +1 -0
  677. package/dist/metrics/index.js +23 -0
  678. package/dist/metrics/index.js.map +1 -0
  679. package/dist/metrics/instruments.d.ts +89 -0
  680. package/dist/metrics/instruments.d.ts.map +1 -0
  681. package/dist/metrics/instruments.js +172 -0
  682. package/dist/metrics/instruments.js.map +1 -0
  683. package/dist/metrics/middleware.d.ts +12 -0
  684. package/dist/metrics/middleware.d.ts.map +1 -0
  685. package/dist/metrics/middleware.js +47 -0
  686. package/dist/metrics/middleware.js.map +1 -0
  687. package/dist/metrics/prometheus.d.ts +39 -0
  688. package/dist/metrics/prometheus.d.ts.map +1 -0
  689. package/dist/metrics/prometheus.js +115 -0
  690. package/dist/metrics/prometheus.js.map +1 -0
  691. package/dist/metrics/registry.d.ts +58 -0
  692. package/dist/metrics/registry.d.ts.map +1 -0
  693. package/dist/metrics/registry.js +145 -0
  694. package/dist/metrics/registry.js.map +1 -0
  695. package/dist/metrics/reporter.d.ts +21 -0
  696. package/dist/metrics/reporter.d.ts.map +1 -0
  697. package/dist/metrics/reporter.js +207 -0
  698. package/dist/metrics/reporter.js.map +1 -0
  699. package/dist/metrics/store.d.ts +47 -0
  700. package/dist/metrics/store.d.ts.map +1 -0
  701. package/dist/metrics/store.js +209 -0
  702. package/dist/metrics/store.js.map +1 -0
  703. package/dist/metrics/system.d.ts +20 -0
  704. package/dist/metrics/system.d.ts.map +1 -0
  705. package/dist/metrics/system.js +109 -0
  706. package/dist/metrics/system.js.map +1 -0
  707. package/dist/metrics/types.d.ts +101 -0
  708. package/dist/metrics/types.d.ts.map +1 -0
  709. package/dist/metrics/types.js +6 -0
  710. package/dist/metrics/types.js.map +1 -0
  711. package/dist/modules/index.d.ts +6 -0
  712. package/dist/modules/index.d.ts.map +1 -0
  713. package/dist/modules/index.js +6 -0
  714. package/dist/modules/index.js.map +1 -0
  715. package/dist/modules/registry.d.ts +36 -0
  716. package/dist/modules/registry.d.ts.map +1 -0
  717. package/dist/modules/registry.js +155 -0
  718. package/dist/modules/registry.js.map +1 -0
  719. package/dist/modules/types.d.ts +37 -0
  720. package/dist/modules/types.d.ts.map +1 -0
  721. package/dist/modules/types.js +9 -0
  722. package/dist/modules/types.js.map +1 -0
  723. package/dist/notifications/channel.d.ts +25 -0
  724. package/dist/notifications/channel.d.ts.map +1 -0
  725. package/dist/notifications/channel.js +83 -0
  726. package/dist/notifications/channel.js.map +1 -0
  727. package/dist/notifications/email.d.ts +27 -0
  728. package/dist/notifications/email.d.ts.map +1 -0
  729. package/dist/notifications/email.js +72 -0
  730. package/dist/notifications/email.js.map +1 -0
  731. package/dist/notifications/index.d.ts +16 -0
  732. package/dist/notifications/index.d.ts.map +1 -0
  733. package/dist/notifications/index.js +12 -0
  734. package/dist/notifications/index.js.map +1 -0
  735. package/dist/notifications/phone.d.ts +16 -0
  736. package/dist/notifications/phone.d.ts.map +1 -0
  737. package/dist/notifications/phone.js +48 -0
  738. package/dist/notifications/phone.js.map +1 -0
  739. package/dist/notifications/sms.d.ts +26 -0
  740. package/dist/notifications/sms.d.ts.map +1 -0
  741. package/dist/notifications/sms.js +65 -0
  742. package/dist/notifications/sms.js.map +1 -0
  743. package/dist/notifications/webhook.d.ts +28 -0
  744. package/dist/notifications/webhook.d.ts.map +1 -0
  745. package/dist/notifications/webhook.js +65 -0
  746. package/dist/notifications/webhook.js.map +1 -0
  747. package/dist/openloop/foldback.d.ts +24 -0
  748. package/dist/openloop/foldback.d.ts.map +1 -0
  749. package/dist/openloop/foldback.js +127 -0
  750. package/dist/openloop/foldback.js.map +1 -0
  751. package/dist/openloop/index.d.ts +11 -0
  752. package/dist/openloop/index.d.ts.map +1 -0
  753. package/dist/openloop/index.js +9 -0
  754. package/dist/openloop/index.js.map +1 -0
  755. package/dist/openloop/lifecycle.d.ts +16 -0
  756. package/dist/openloop/lifecycle.d.ts.map +1 -0
  757. package/dist/openloop/lifecycle.js +304 -0
  758. package/dist/openloop/lifecycle.js.map +1 -0
  759. package/dist/openloop/resolution-scanner.d.ts +17 -0
  760. package/dist/openloop/resolution-scanner.d.ts.map +1 -0
  761. package/dist/openloop/resolution-scanner.js +551 -0
  762. package/dist/openloop/resolution-scanner.js.map +1 -0
  763. package/dist/openloop/scanner.d.ts +28 -0
  764. package/dist/openloop/scanner.d.ts.map +1 -0
  765. package/dist/openloop/scanner.js +587 -0
  766. package/dist/openloop/scanner.js.map +1 -0
  767. package/dist/openloop/store.d.ts +41 -0
  768. package/dist/openloop/store.d.ts.map +1 -0
  769. package/dist/openloop/store.js +154 -0
  770. package/dist/openloop/store.js.map +1 -0
  771. package/dist/openloop/types.d.ts +94 -0
  772. package/dist/openloop/types.d.ts.map +1 -0
  773. package/dist/openloop/types.js +6 -0
  774. package/dist/openloop/types.js.map +1 -0
  775. package/dist/plugins/google-tasks/index.d.ts +55 -0
  776. package/dist/plugins/google-tasks/index.d.ts.map +1 -0
  777. package/dist/plugins/google-tasks/index.js +135 -0
  778. package/dist/plugins/google-tasks/index.js.map +1 -0
  779. package/dist/pulse/activation-event.d.ts +66 -0
  780. package/dist/pulse/activation-event.d.ts.map +1 -0
  781. package/dist/pulse/activation-event.js +139 -0
  782. package/dist/pulse/activation-event.js.map +1 -0
  783. package/dist/pulse/activation-log.d.ts +37 -0
  784. package/dist/pulse/activation-log.d.ts.map +1 -0
  785. package/dist/pulse/activation-log.js +101 -0
  786. package/dist/pulse/activation-log.js.map +1 -0
  787. package/dist/pulse/index.d.ts +11 -0
  788. package/dist/pulse/index.d.ts.map +1 -0
  789. package/dist/pulse/index.js +13 -0
  790. package/dist/pulse/index.js.map +1 -0
  791. package/dist/pulse/pressure.d.ts +69 -0
  792. package/dist/pulse/pressure.d.ts.map +1 -0
  793. package/dist/pulse/pressure.js +304 -0
  794. package/dist/pulse/pressure.js.map +1 -0
  795. package/dist/pulse/types.d.ts +89 -0
  796. package/dist/pulse/types.d.ts.map +1 -0
  797. package/dist/pulse/types.js +6 -0
  798. package/dist/pulse/types.js.map +1 -0
  799. package/dist/queue/grooming.d.ts +16 -0
  800. package/dist/queue/grooming.d.ts.map +1 -0
  801. package/dist/queue/grooming.js +269 -0
  802. package/dist/queue/grooming.js.map +1 -0
  803. package/dist/queue/provider.d.ts +50 -0
  804. package/dist/queue/provider.d.ts.map +1 -0
  805. package/dist/queue/provider.js +131 -0
  806. package/dist/queue/provider.js.map +1 -0
  807. package/dist/queue/store.d.ts +97 -0
  808. package/dist/queue/store.d.ts.map +1 -0
  809. package/dist/queue/store.js +448 -0
  810. package/dist/queue/store.js.map +1 -0
  811. package/dist/queue/types.d.ts +47 -0
  812. package/dist/queue/types.d.ts.map +1 -0
  813. package/dist/queue/types.js +22 -0
  814. package/dist/queue/types.js.map +1 -0
  815. package/dist/rate-limit.d.ts +26 -0
  816. package/dist/rate-limit.d.ts.map +1 -0
  817. package/dist/rate-limit.js +74 -0
  818. package/dist/rate-limit.js.map +1 -0
  819. package/dist/registry/discovery.d.ts +30 -0
  820. package/dist/registry/discovery.d.ts.map +1 -0
  821. package/dist/registry/discovery.js +171 -0
  822. package/dist/registry/discovery.js.map +1 -0
  823. package/dist/registry/index.d.ts +14 -0
  824. package/dist/registry/index.d.ts.map +1 -0
  825. package/dist/registry/index.js +18 -0
  826. package/dist/registry/index.js.map +1 -0
  827. package/dist/registry/installer.d.ts +45 -0
  828. package/dist/registry/installer.d.ts.map +1 -0
  829. package/dist/registry/installer.js +175 -0
  830. package/dist/registry/installer.js.map +1 -0
  831. package/dist/registry/registry.d.ts +70 -0
  832. package/dist/registry/registry.d.ts.map +1 -0
  833. package/dist/registry/registry.js +153 -0
  834. package/dist/registry/registry.js.map +1 -0
  835. package/dist/registry/store.d.ts +69 -0
  836. package/dist/registry/store.d.ts.map +1 -0
  837. package/dist/registry/store.js +242 -0
  838. package/dist/registry/store.js.map +1 -0
  839. package/dist/registry/types.d.ts +116 -0
  840. package/dist/registry/types.d.ts.map +1 -0
  841. package/dist/registry/types.js +9 -0
  842. package/dist/registry/types.js.map +1 -0
  843. package/dist/registry/validator.d.ts +24 -0
  844. package/dist/registry/validator.d.ts.map +1 -0
  845. package/dist/registry/validator.js +203 -0
  846. package/dist/registry/validator.js.map +1 -0
  847. package/dist/scheduling/index.d.ts +4 -0
  848. package/dist/scheduling/index.d.ts.map +1 -0
  849. package/dist/scheduling/index.js +3 -0
  850. package/dist/scheduling/index.js.map +1 -0
  851. package/dist/scheduling/store.d.ts +41 -0
  852. package/dist/scheduling/store.d.ts.map +1 -0
  853. package/dist/scheduling/store.js +237 -0
  854. package/dist/scheduling/store.js.map +1 -0
  855. package/dist/scheduling/timer.d.ts +25 -0
  856. package/dist/scheduling/timer.d.ts.map +1 -0
  857. package/dist/scheduling/timer.js +118 -0
  858. package/dist/scheduling/timer.js.map +1 -0
  859. package/dist/scheduling/types.d.ts +43 -0
  860. package/dist/scheduling/types.d.ts.map +1 -0
  861. package/dist/scheduling/types.js +5 -0
  862. package/dist/scheduling/types.js.map +1 -0
  863. package/dist/search/brain-docs.d.ts +20 -0
  864. package/dist/search/brain-docs.d.ts.map +1 -0
  865. package/dist/search/brain-docs.js +103 -0
  866. package/dist/search/brain-docs.js.map +1 -0
  867. package/dist/search/browse.d.ts +45 -0
  868. package/dist/search/browse.d.ts.map +1 -0
  869. package/dist/search/browse.js +225 -0
  870. package/dist/search/browse.js.map +1 -0
  871. package/dist/search/classify.d.ts +23 -0
  872. package/dist/search/classify.d.ts.map +1 -0
  873. package/dist/search/classify.js +132 -0
  874. package/dist/search/classify.js.map +1 -0
  875. package/dist/search/client.d.ts +32 -0
  876. package/dist/search/client.d.ts.map +1 -0
  877. package/dist/search/client.js +72 -0
  878. package/dist/search/client.js.map +1 -0
  879. package/dist/search/perplexity.d.ts +13 -0
  880. package/dist/search/perplexity.d.ts.map +1 -0
  881. package/dist/search/perplexity.js +41 -0
  882. package/dist/search/perplexity.js.map +1 -0
  883. package/dist/search/sidecar.d.ts +20 -0
  884. package/dist/search/sidecar.d.ts.map +1 -0
  885. package/dist/search/sidecar.js +103 -0
  886. package/dist/search/sidecar.js.map +1 -0
  887. package/dist/server.d.ts +8 -0
  888. package/dist/server.d.ts.map +1 -0
  889. package/dist/server.js +4851 -0
  890. package/dist/server.js.map +1 -0
  891. package/dist/services/backlogReview.d.ts +66 -0
  892. package/dist/services/backlogReview.d.ts.map +1 -0
  893. package/dist/services/backlogReview.js +285 -0
  894. package/dist/services/backlogReview.js.map +1 -0
  895. package/dist/services/backup.d.ts +43 -0
  896. package/dist/services/backup.d.ts.map +1 -0
  897. package/dist/services/backup.js +334 -0
  898. package/dist/services/backup.js.map +1 -0
  899. package/dist/services/credit-monitor.d.ts +40 -0
  900. package/dist/services/credit-monitor.d.ts.map +1 -0
  901. package/dist/services/credit-monitor.js +147 -0
  902. package/dist/services/credit-monitor.js.map +1 -0
  903. package/dist/services/morningBriefing.d.ts +125 -0
  904. package/dist/services/morningBriefing.d.ts.map +1 -0
  905. package/dist/services/morningBriefing.js +660 -0
  906. package/dist/services/morningBriefing.js.map +1 -0
  907. package/dist/services/routine-patterns.d.ts +21 -0
  908. package/dist/services/routine-patterns.d.ts.map +1 -0
  909. package/dist/services/routine-patterns.js +46 -0
  910. package/dist/services/routine-patterns.js.map +1 -0
  911. package/dist/services/traceInsights.d.ts +53 -0
  912. package/dist/services/traceInsights.d.ts.map +1 -0
  913. package/dist/services/traceInsights.js +762 -0
  914. package/dist/services/traceInsights.js.map +1 -0
  915. package/dist/services/training.d.ts +63 -0
  916. package/dist/services/training.d.ts.map +1 -0
  917. package/dist/services/training.js +697 -0
  918. package/dist/services/training.js.map +1 -0
  919. package/dist/services/whatsapp.d.ts +45 -0
  920. package/dist/services/whatsapp.d.ts.map +1 -0
  921. package/dist/services/whatsapp.js +194 -0
  922. package/dist/services/whatsapp.js.map +1 -0
  923. package/dist/sessions/store.d.ts +21 -0
  924. package/dist/sessions/store.d.ts.map +1 -0
  925. package/dist/sessions/store.js +47 -0
  926. package/dist/sessions/store.js.map +1 -0
  927. package/dist/settings.d.ts +111 -0
  928. package/dist/settings.d.ts.map +1 -0
  929. package/dist/settings.js +256 -0
  930. package/dist/settings.js.map +1 -0
  931. package/dist/skills/index.d.ts +10 -0
  932. package/dist/skills/index.d.ts.map +1 -0
  933. package/dist/skills/index.js +11 -0
  934. package/dist/skills/index.js.map +1 -0
  935. package/dist/skills/loader.d.ts +22 -0
  936. package/dist/skills/loader.d.ts.map +1 -0
  937. package/dist/skills/loader.js +65 -0
  938. package/dist/skills/loader.js.map +1 -0
  939. package/dist/skills/registry.d.ts +161 -0
  940. package/dist/skills/registry.d.ts.map +1 -0
  941. package/dist/skills/registry.js +664 -0
  942. package/dist/skills/registry.js.map +1 -0
  943. package/dist/skills/types.d.ts +83 -0
  944. package/dist/skills/types.d.ts.map +1 -0
  945. package/dist/skills/types.js +31 -0
  946. package/dist/skills/types.js.map +1 -0
  947. package/dist/skills/validator.d.ts +36 -0
  948. package/dist/skills/validator.d.ts.map +1 -0
  949. package/dist/skills/validator.js +114 -0
  950. package/dist/skills/validator.js.map +1 -0
  951. package/dist/slack/channels.d.ts +102 -0
  952. package/dist/slack/channels.d.ts.map +1 -0
  953. package/dist/slack/channels.js +277 -0
  954. package/dist/slack/channels.js.map +1 -0
  955. package/dist/slack/client.d.ts +151 -0
  956. package/dist/slack/client.d.ts.map +1 -0
  957. package/dist/slack/client.js +468 -0
  958. package/dist/slack/client.js.map +1 -0
  959. package/dist/slack/retry.d.ts +36 -0
  960. package/dist/slack/retry.d.ts.map +1 -0
  961. package/dist/slack/retry.js +100 -0
  962. package/dist/slack/retry.js.map +1 -0
  963. package/dist/slack/types.d.ts +271 -0
  964. package/dist/slack/types.d.ts.map +1 -0
  965. package/dist/slack/types.js +52 -0
  966. package/dist/slack/types.js.map +1 -0
  967. package/dist/slack/webhooks.d.ts +55 -0
  968. package/dist/slack/webhooks.d.ts.map +1 -0
  969. package/dist/slack/webhooks.js +285 -0
  970. package/dist/slack/webhooks.js.map +1 -0
  971. package/dist/stt/client.d.ts +18 -0
  972. package/dist/stt/client.d.ts.map +1 -0
  973. package/dist/stt/client.js +66 -0
  974. package/dist/stt/client.js.map +1 -0
  975. package/dist/stt/sidecar.d.ts +16 -0
  976. package/dist/stt/sidecar.d.ts.map +1 -0
  977. package/dist/stt/sidecar.js +115 -0
  978. package/dist/stt/sidecar.js.map +1 -0
  979. package/dist/tracing/bridge.d.ts +14 -0
  980. package/dist/tracing/bridge.d.ts.map +1 -0
  981. package/dist/tracing/bridge.js +70 -0
  982. package/dist/tracing/bridge.js.map +1 -0
  983. package/dist/tracing/correlation.d.ts +34 -0
  984. package/dist/tracing/correlation.d.ts.map +1 -0
  985. package/dist/tracing/correlation.js +49 -0
  986. package/dist/tracing/correlation.js.map +1 -0
  987. package/dist/tracing/index.d.ts +15 -0
  988. package/dist/tracing/index.d.ts.map +1 -0
  989. package/dist/tracing/index.js +18 -0
  990. package/dist/tracing/index.js.map +1 -0
  991. package/dist/tracing/init.d.ts +42 -0
  992. package/dist/tracing/init.d.ts.map +1 -0
  993. package/dist/tracing/init.js +81 -0
  994. package/dist/tracing/init.js.map +1 -0
  995. package/dist/tracing/instrument.d.ts +39 -0
  996. package/dist/tracing/instrument.d.ts.map +1 -0
  997. package/dist/tracing/instrument.js +145 -0
  998. package/dist/tracing/instrument.js.map +1 -0
  999. package/dist/tracing/middleware.d.ts +18 -0
  1000. package/dist/tracing/middleware.d.ts.map +1 -0
  1001. package/dist/tracing/middleware.js +69 -0
  1002. package/dist/tracing/middleware.js.map +1 -0
  1003. package/dist/tracing/tracer.d.ts +105 -0
  1004. package/dist/tracing/tracer.d.ts.map +1 -0
  1005. package/dist/tracing/tracer.js +327 -0
  1006. package/dist/tracing/tracer.js.map +1 -0
  1007. package/dist/tts/client.d.ts +18 -0
  1008. package/dist/tts/client.d.ts.map +1 -0
  1009. package/dist/tts/client.js +48 -0
  1010. package/dist/tts/client.js.map +1 -0
  1011. package/dist/tts/sidecar.d.ts +16 -0
  1012. package/dist/tts/sidecar.d.ts.map +1 -0
  1013. package/dist/tts/sidecar.js +148 -0
  1014. package/dist/tts/sidecar.js.map +1 -0
  1015. package/dist/twilio/call.d.ts +22 -0
  1016. package/dist/twilio/call.d.ts.map +1 -0
  1017. package/dist/twilio/call.js +79 -0
  1018. package/dist/twilio/call.js.map +1 -0
  1019. package/dist/types/plugin.d.ts +342 -0
  1020. package/dist/types/plugin.d.ts.map +1 -0
  1021. package/dist/types/plugin.js +10 -0
  1022. package/dist/types/plugin.js.map +1 -0
  1023. package/dist/types.d.ts +97 -0
  1024. package/dist/types.d.ts.map +1 -0
  1025. package/dist/types.js +5 -0
  1026. package/dist/types.js.map +1 -0
  1027. package/dist/utils/logger.d.ts +53 -0
  1028. package/dist/utils/logger.d.ts.map +1 -0
  1029. package/dist/utils/logger.js +169 -0
  1030. package/dist/utils/logger.js.map +1 -0
  1031. package/dist/vault/matcher.d.ts +39 -0
  1032. package/dist/vault/matcher.d.ts.map +1 -0
  1033. package/dist/vault/matcher.js +197 -0
  1034. package/dist/vault/matcher.js.map +1 -0
  1035. package/dist/vault/personal.d.ts +60 -0
  1036. package/dist/vault/personal.d.ts.map +1 -0
  1037. package/dist/vault/personal.js +162 -0
  1038. package/dist/vault/personal.js.map +1 -0
  1039. package/dist/vault/store.d.ts +39 -0
  1040. package/dist/vault/store.d.ts.map +1 -0
  1041. package/dist/vault/store.js +111 -0
  1042. package/dist/vault/store.js.map +1 -0
  1043. package/dist/webhooks/config.d.ts +64 -0
  1044. package/dist/webhooks/config.d.ts.map +1 -0
  1045. package/dist/webhooks/config.js +214 -0
  1046. package/dist/webhooks/config.js.map +1 -0
  1047. package/dist/webhooks/event-log.d.ts +90 -0
  1048. package/dist/webhooks/event-log.d.ts.map +1 -0
  1049. package/dist/webhooks/event-log.js +132 -0
  1050. package/dist/webhooks/event-log.js.map +1 -0
  1051. package/dist/webhooks/handler.d.ts +92 -0
  1052. package/dist/webhooks/handler.d.ts.map +1 -0
  1053. package/dist/webhooks/handler.js +103 -0
  1054. package/dist/webhooks/handler.js.map +1 -0
  1055. package/dist/webhooks/handlers.d.ts +100 -0
  1056. package/dist/webhooks/handlers.d.ts.map +1 -0
  1057. package/dist/webhooks/handlers.js +178 -0
  1058. package/dist/webhooks/handlers.js.map +1 -0
  1059. package/dist/webhooks/index.d.ts +29 -0
  1060. package/dist/webhooks/index.d.ts.map +1 -0
  1061. package/dist/webhooks/index.js +33 -0
  1062. package/dist/webhooks/index.js.map +1 -0
  1063. package/dist/webhooks/mount.d.ts +77 -0
  1064. package/dist/webhooks/mount.d.ts.map +1 -0
  1065. package/dist/webhooks/mount.js +400 -0
  1066. package/dist/webhooks/mount.js.map +1 -0
  1067. package/dist/webhooks/registry.d.ts +52 -0
  1068. package/dist/webhooks/registry.d.ts.map +1 -0
  1069. package/dist/webhooks/registry.js +143 -0
  1070. package/dist/webhooks/registry.js.map +1 -0
  1071. package/dist/webhooks/relay.d.ts +25 -0
  1072. package/dist/webhooks/relay.d.ts.map +1 -0
  1073. package/dist/webhooks/relay.js +53 -0
  1074. package/dist/webhooks/relay.js.map +1 -0
  1075. package/dist/webhooks/retry.d.ts +92 -0
  1076. package/dist/webhooks/retry.d.ts.map +1 -0
  1077. package/dist/webhooks/retry.js +270 -0
  1078. package/dist/webhooks/retry.js.map +1 -0
  1079. package/dist/webhooks/router.d.ts +94 -0
  1080. package/dist/webhooks/router.d.ts.map +1 -0
  1081. package/dist/webhooks/router.js +290 -0
  1082. package/dist/webhooks/router.js.map +1 -0
  1083. package/dist/webhooks/twilio.d.ts +63 -0
  1084. package/dist/webhooks/twilio.d.ts.map +1 -0
  1085. package/dist/webhooks/twilio.js +129 -0
  1086. package/dist/webhooks/twilio.js.map +1 -0
  1087. package/dist/webhooks/types.d.ts +142 -0
  1088. package/dist/webhooks/types.d.ts.map +1 -0
  1089. package/dist/webhooks/types.js +8 -0
  1090. package/dist/webhooks/types.js.map +1 -0
  1091. package/dist/webhooks/verify.d.ts +51 -0
  1092. package/dist/webhooks/verify.d.ts.map +1 -0
  1093. package/dist/webhooks/verify.js +98 -0
  1094. package/dist/webhooks/verify.js.map +1 -0
  1095. package/package.json +70 -0
  1096. package/public/board.html +1316 -0
  1097. package/public/browser.html +600 -0
  1098. package/public/help.html +655 -0
  1099. package/public/index.html +4689 -0
  1100. package/public/library.html +3642 -0
  1101. package/public/observatory.html +1693 -0
  1102. package/public/ops.html +1129 -0
  1103. package/public/share-modal.js +211 -0
  1104. package/skills/README.md +34 -0
  1105. package/skills/core-architecture.md +33 -0
  1106. package/skills/form-fill.md +98 -0
  1107. package/skills/form-review.md +110 -0
  1108. package/skills/form-scout.md +94 -0
  1109. package/skills/log-decision.md +27 -0
  1110. package/skills/onboard.md +98 -0
  1111. package/skills/voice-guide.md +22 -0
  1112. package/skills/write-blog.md +43 -0
@@ -0,0 +1,4689 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Core</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: #0a0a0a;
12
+ --surface: #141414;
13
+ --border: #252525;
14
+ --text: #e0e0e0;
15
+ --text-dim: #707070;
16
+ --accent: #7c6fef;
17
+ --accent-dim: #5a4fc4;
18
+ --error: #ef4444;
19
+ --user-bg: #1a1a2e;
20
+ --assistant-bg: #141414;
21
+ --radius: 8px;
22
+ --max-width: 640px;
23
+ }
24
+
25
+ body {
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ height: 100dvh;
30
+ display: flex;
31
+ flex-direction: column;
32
+ overflow: hidden;
33
+ }
34
+
35
+ /* --- Shared --- */
36
+
37
+ .screen { display: none; flex-direction: column; height: 100dvh; }
38
+ .screen.active { display: flex; }
39
+
40
+ .center-wrap {
41
+ flex: 1;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ padding: 24px;
46
+ }
47
+
48
+ .card {
49
+ width: 100%;
50
+ max-width: 420px;
51
+ background: var(--surface);
52
+ border: 1px solid var(--border);
53
+ border-radius: 12px;
54
+ padding: 32px;
55
+ }
56
+
57
+ .card h1 {
58
+ font-size: 20px;
59
+ font-weight: 600;
60
+ margin-bottom: 4px;
61
+ }
62
+
63
+ .card p {
64
+ color: var(--text-dim);
65
+ font-size: 14px;
66
+ line-height: 1.5;
67
+ margin-bottom: 20px;
68
+ }
69
+
70
+ .card label {
71
+ display: block;
72
+ font-size: 12px;
73
+ font-weight: 500;
74
+ color: var(--text-dim);
75
+ margin-bottom: 6px;
76
+ text-transform: uppercase;
77
+ letter-spacing: 0.5px;
78
+ }
79
+
80
+ .card input {
81
+ width: 100%;
82
+ padding: 10px 12px;
83
+ background: var(--bg);
84
+ border: 1px solid var(--border);
85
+ border-radius: var(--radius);
86
+ color: var(--text);
87
+ font-size: 15px;
88
+ font-family: inherit;
89
+ outline: none;
90
+ margin-bottom: 16px;
91
+ transition: border-color 0.15s;
92
+ }
93
+
94
+ .card input:focus { border-color: var(--accent); }
95
+
96
+ .card input::placeholder { color: var(--text-dim); }
97
+
98
+ .btn {
99
+ display: inline-block;
100
+ padding: 10px 20px;
101
+ background: var(--accent);
102
+ color: #fff;
103
+ border: none;
104
+ border-radius: var(--radius);
105
+ font-size: 14px;
106
+ font-weight: 500;
107
+ font-family: inherit;
108
+ cursor: pointer;
109
+ transition: background 0.15s;
110
+ width: 100%;
111
+ text-align: center;
112
+ }
113
+
114
+ .btn:hover { background: var(--accent-dim); }
115
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
116
+
117
+ .link-btn {
118
+ background: none;
119
+ border: none;
120
+ color: var(--accent);
121
+ font-size: 13px;
122
+ cursor: pointer;
123
+ font-family: inherit;
124
+ padding: 0;
125
+ margin-top: 12px;
126
+ }
127
+
128
+ .link-btn:hover { text-decoration: underline; }
129
+
130
+ .error-msg {
131
+ color: var(--error);
132
+ font-size: 13px;
133
+ margin-bottom: 12px;
134
+ display: none;
135
+ }
136
+
137
+ .error-msg.visible { display: block; }
138
+
139
+ .step { display: none; }
140
+ .step.active { display: block; }
141
+
142
+ /* --- Chat screen --- */
143
+
144
+ #chat-screen {
145
+ flex-direction: column;
146
+ }
147
+
148
+ .chat-header {
149
+ padding: 12px 20px;
150
+ border-bottom: 1px solid var(--border);
151
+ background: var(--surface);
152
+ font-size: 14px;
153
+ font-weight: 500;
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 8px;
157
+ flex-shrink: 0;
158
+ }
159
+
160
+ .chat-header .dot {
161
+ width: 8px;
162
+ height: 8px;
163
+ border-radius: 50%;
164
+ background: #22c55e;
165
+ }
166
+
167
+ .chat-header .header-nav {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 8px;
171
+ margin-left: 12px;
172
+ }
173
+
174
+ .chat-header .header-nav a {
175
+ color: var(--text-dim);
176
+ text-decoration: none;
177
+ font-size: 13px;
178
+ padding: 4px 10px;
179
+ border-radius: 6px;
180
+ border: 1px solid var(--border);
181
+ transition: all 0.15s;
182
+ }
183
+
184
+ .chat-header .header-nav a:hover { color: var(--text); border-color: var(--accent); background: rgba(109,93,252,0.1); }
185
+ .chat-header .header-nav a.active { color: var(--text); border-color: var(--border); background: var(--surface2); }
186
+
187
+ .header-nav .nav-divider {
188
+ width: 1px;
189
+ height: 18px;
190
+ background: var(--border);
191
+ margin: 0 4px;
192
+ align-self: center;
193
+ }
194
+
195
+ .messages {
196
+ flex: 1;
197
+ overflow-y: auto;
198
+ padding: 16px 0;
199
+ scroll-behavior: smooth;
200
+ }
201
+
202
+ .message {
203
+ padding: 12px 20px;
204
+ max-width: var(--max-width);
205
+ margin: 0 auto;
206
+ width: 100%;
207
+ }
208
+
209
+ .message .role {
210
+ font-size: 11px;
211
+ font-weight: 600;
212
+ text-transform: uppercase;
213
+ letter-spacing: 0.5px;
214
+ margin-bottom: 4px;
215
+ color: var(--text-dim);
216
+ }
217
+
218
+ .message.user .role { color: var(--accent); }
219
+
220
+ .model-tag {
221
+ font-size: 10px;
222
+ font-weight: 400;
223
+ color: var(--text-dim);
224
+ margin-left: 6px;
225
+ font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
226
+ }
227
+
228
+ .message .content {
229
+ font-size: 15px;
230
+ line-height: 1.6;
231
+ word-wrap: break-word;
232
+ }
233
+
234
+ .message .content pre {
235
+ background: var(--surface);
236
+ border: 1px solid var(--border);
237
+ border-radius: 6px;
238
+ padding: 8px 12px;
239
+ overflow-x: auto;
240
+ margin: 6px 0;
241
+ }
242
+
243
+ .message .content code {
244
+ background: var(--surface);
245
+ padding: 1px 4px;
246
+ border-radius: 3px;
247
+ font-size: 13px;
248
+ }
249
+
250
+ .message .content pre code {
251
+ background: none;
252
+ padding: 0;
253
+ }
254
+
255
+ .message .content ul {
256
+ margin: 4px 0;
257
+ padding-left: 20px;
258
+ }
259
+
260
+ .message .content strong { font-weight: 600; }
261
+ .message .content em { font-style: italic; }
262
+
263
+ /* Streaming cursor — blinking caret while tokens arrive */
264
+ .streaming-cursor::after {
265
+ content: "";
266
+ display: inline-block;
267
+ width: 2px;
268
+ height: 1em;
269
+ background: var(--accent, #6d5dfc);
270
+ margin-left: 2px;
271
+ vertical-align: text-bottom;
272
+ animation: cursor-blink 0.8s steps(2) infinite;
273
+ }
274
+ @keyframes cursor-blink {
275
+ 0%, 100% { opacity: 1; }
276
+ 50% { opacity: 0; }
277
+ }
278
+
279
+ .composer {
280
+ border-top: 1px solid var(--border);
281
+ background: var(--surface);
282
+ padding: 12px 20px;
283
+ flex-shrink: 0;
284
+ }
285
+
286
+ .composer-inner {
287
+ max-width: var(--max-width);
288
+ margin: 0 auto;
289
+ display: flex;
290
+ gap: 8px;
291
+ align-items: flex-end;
292
+ }
293
+
294
+ .composer textarea {
295
+ flex: 1;
296
+ padding: 10px 12px;
297
+ background: var(--bg);
298
+ border: 1px solid var(--border);
299
+ border-radius: var(--radius);
300
+ color: var(--text);
301
+ font-size: 15px;
302
+ font-family: inherit;
303
+ outline: none;
304
+ resize: none;
305
+ min-height: 42px;
306
+ max-height: 160px;
307
+ line-height: 1.4;
308
+ transition: border-color 0.15s;
309
+ }
310
+
311
+ .composer textarea:focus { border-color: var(--accent); }
312
+ .composer textarea::placeholder { color: var(--text-dim); }
313
+
314
+ .message.system {
315
+ text-align: center;
316
+ }
317
+
318
+ .message.system .content {
319
+ color: var(--text-dim);
320
+ font-size: 13px;
321
+ font-style: italic;
322
+ border-left: 2px solid var(--border);
323
+ padding-left: 12px;
324
+ text-align: left;
325
+ display: inline-block;
326
+ }
327
+
328
+ /* ─── Proactive question greeting ─── */
329
+
330
+ .dash-greeting {
331
+ max-width: var(--max-width);
332
+ margin: 12px auto 0;
333
+ padding: 0 20px;
334
+ }
335
+
336
+ .greeting-text {
337
+ font-size: 12px;
338
+ font-weight: 600;
339
+ text-transform: uppercase;
340
+ letter-spacing: 0.5px;
341
+ color: var(--text-dim);
342
+ margin-bottom: 8px;
343
+ }
344
+
345
+ .question-chips {
346
+ display: flex;
347
+ flex-direction: column;
348
+ gap: 6px;
349
+ }
350
+
351
+ .question-chip {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 8px;
355
+ padding: 10px 14px;
356
+ background: var(--surface);
357
+ border: 1px solid var(--border);
358
+ border-radius: var(--radius);
359
+ cursor: pointer;
360
+ transition: border-color 0.15s, background 0.15s;
361
+ font-size: 14px;
362
+ color: var(--text);
363
+ line-height: 1.4;
364
+ }
365
+
366
+ .question-chip:hover {
367
+ border-color: var(--accent);
368
+ background: rgba(109,93,252,0.06);
369
+ }
370
+
371
+ .question-chip .chip-icon {
372
+ flex-shrink: 0;
373
+ width: 22px;
374
+ height: 22px;
375
+ border-radius: 50%;
376
+ background: var(--accent);
377
+ color: #fff;
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ font-size: 13px;
382
+ font-weight: 700;
383
+ }
384
+
385
+ .question-chip .chip-text { flex: 1; }
386
+
387
+ .question-chip .chip-dismiss {
388
+ flex-shrink: 0;
389
+ width: 20px;
390
+ height: 20px;
391
+ border: none;
392
+ background: transparent;
393
+ color: var(--text-dim);
394
+ cursor: pointer;
395
+ font-size: 14px;
396
+ border-radius: 50%;
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ opacity: 0;
401
+ transition: opacity 0.15s, background 0.15s;
402
+ }
403
+
404
+ .question-chip:hover .chip-dismiss { opacity: 1; }
405
+ .question-chip .chip-dismiss:hover { background: var(--surface2); color: var(--text); }
406
+
407
+ .composer button {
408
+ padding: 10px 16px;
409
+ background: var(--accent);
410
+ color: #fff;
411
+ border: none;
412
+ border-radius: var(--radius);
413
+ font-size: 14px;
414
+ font-weight: 500;
415
+ cursor: pointer;
416
+ font-family: inherit;
417
+ transition: background 0.15s;
418
+ white-space: nowrap;
419
+ }
420
+
421
+ .composer button:hover { background: var(--accent-dim); }
422
+ .composer button:disabled { opacity: 0.5; cursor: not-allowed; }
423
+
424
+ .composer-actions {
425
+ display: flex;
426
+ gap: 4px;
427
+ align-items: center;
428
+ }
429
+
430
+ .composer-actions button {
431
+ padding: 8px;
432
+ background: transparent;
433
+ color: var(--text-dim);
434
+ border: 1px solid var(--border);
435
+ border-radius: var(--radius);
436
+ font-size: 16px;
437
+ cursor: pointer;
438
+ line-height: 1;
439
+ }
440
+
441
+ .composer-actions button:hover { background: var(--surface); color: var(--text); }
442
+ #incognito-btn.active { color: #ef4444; background: var(--surface); }
443
+
444
+ /* Incognito mode — red border signals local-only */
445
+ .composer.incognito textarea {
446
+ border-color: #ef4444 !important;
447
+ box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.25);
448
+ }
449
+ .composer.incognito textarea.redacted {
450
+ color: transparent;
451
+ text-shadow: 0 0 8px rgba(224, 224, 224, 0.5);
452
+ }
453
+ .composer.incognito .incognito-label {
454
+ display: block;
455
+ }
456
+ .incognito-label {
457
+ display: none;
458
+ font-size: 11px;
459
+ color: #ef4444;
460
+ text-align: center;
461
+ padding: 2px 0;
462
+ letter-spacing: 0.05em;
463
+ }
464
+ .composer.incognito #attach-btn,
465
+ .composer.incognito #screenshot-btn {
466
+ opacity: 0.2;
467
+ pointer-events: none;
468
+ }
469
+ /* Incognito — selective PII redaction */
470
+ .pii-redact {
471
+ filter: blur(5px);
472
+ transition: filter 0.2s;
473
+ user-select: none;
474
+ cursor: pointer;
475
+ border-radius: 3px;
476
+ padding: 0 2px;
477
+ background: rgba(239, 68, 68, 0.08);
478
+ }
479
+ .pii-redact:hover {
480
+ filter: none;
481
+ user-select: auto;
482
+ background: rgba(239, 68, 68, 0.15);
483
+ }
484
+ /* Block all selection/copy on incognito messages */
485
+ .message.incognito {
486
+ user-select: none;
487
+ -webkit-user-select: none;
488
+ border-left: 3px solid #ef4444;
489
+ padding-left: 12px;
490
+ position: relative;
491
+ }
492
+ .message.incognito .content {
493
+ user-select: none;
494
+ -webkit-user-select: none;
495
+ }
496
+ .message.incognito .role::after {
497
+ content: " \1F512";
498
+ font-size: 11px;
499
+ opacity: 0.5;
500
+ }
501
+ /* Screenshot protection: hide incognito content from print/capture */
502
+ .message.incognito.screen-capture .content { filter: blur(20px) !important; }
503
+ @media print {
504
+ .message.incognito .content { display: none !important; }
505
+ .message.incognito::after {
506
+ content: "[incognito content hidden]";
507
+ color: var(--text-dim);
508
+ font-style: italic;
509
+ }
510
+ }
511
+
512
+ .attachment-preview {
513
+ display: flex;
514
+ gap: 8px;
515
+ padding: 4px 8px;
516
+ flex-wrap: wrap;
517
+ }
518
+
519
+ .attachment-preview .thumb {
520
+ position: relative;
521
+ width: 60px;
522
+ height: 60px;
523
+ border-radius: 6px;
524
+ overflow: hidden;
525
+ border: 1px solid var(--border);
526
+ }
527
+
528
+ .attachment-preview .thumb img {
529
+ width: 100%;
530
+ height: 100%;
531
+ object-fit: cover;
532
+ }
533
+
534
+ .attachment-preview .thumb .remove-btn {
535
+ position: absolute;
536
+ top: -4px;
537
+ right: -4px;
538
+ width: 18px;
539
+ height: 18px;
540
+ background: #e44;
541
+ color: #fff;
542
+ border: none;
543
+ border-radius: 50%;
544
+ font-size: 11px;
545
+ line-height: 18px;
546
+ text-align: center;
547
+ cursor: pointer;
548
+ padding: 0;
549
+ }
550
+
551
+ /* --- Vault slide-over --- */
552
+
553
+ .chat-header .spacer { flex: 1; }
554
+
555
+ .gear-btn {
556
+ background: none;
557
+ border: none;
558
+ color: var(--text-dim);
559
+ font-size: 18px;
560
+ cursor: pointer;
561
+ padding: 4px 6px;
562
+ border-radius: 4px;
563
+ transition: color 0.15s, background 0.15s;
564
+ line-height: 1;
565
+ }
566
+
567
+ .gear-btn:hover { color: var(--text); background: var(--border); }
568
+ a.gear-btn { text-decoration: none; }
569
+
570
+ .vault-overlay {
571
+ display: none;
572
+ position: fixed;
573
+ inset: 0;
574
+ background: rgba(0,0,0,0.5);
575
+ z-index: 100;
576
+ }
577
+
578
+ .vault-overlay.open { display: block; }
579
+
580
+ .vault-panel {
581
+ position: fixed;
582
+ top: 0;
583
+ right: 0;
584
+ bottom: 0;
585
+ width: 380px;
586
+ max-width: 90vw;
587
+ background: var(--surface);
588
+ border-left: 1px solid var(--border);
589
+ z-index: 101;
590
+ display: flex;
591
+ flex-direction: column;
592
+ transform: translateX(100%);
593
+ transition: transform 0.2s ease;
594
+ }
595
+
596
+ .vault-overlay.open .vault-panel { transform: translateX(0); }
597
+
598
+ .vault-header {
599
+ display: flex;
600
+ align-items: center;
601
+ justify-content: space-between;
602
+ padding: 16px 20px;
603
+ border-bottom: 1px solid var(--border);
604
+ flex-shrink: 0;
605
+ }
606
+
607
+ .vault-header h2 { font-size: 16px; font-weight: 600; }
608
+
609
+ .settings-nav {
610
+ display: flex;
611
+ align-items: center;
612
+ gap: 4px;
613
+ }
614
+
615
+ .settings-nav a {
616
+ display: flex;
617
+ align-items: center;
618
+ justify-content: center;
619
+ width: 30px;
620
+ height: 30px;
621
+ border-radius: 6px;
622
+ color: var(--text-dim);
623
+ text-decoration: none;
624
+ transition: color 0.15s, background 0.15s;
625
+ }
626
+
627
+ .settings-nav a:hover {
628
+ color: var(--text);
629
+ background: var(--border);
630
+ }
631
+
632
+ .vault-close {
633
+ background: none;
634
+ border: none;
635
+ color: var(--text-dim);
636
+ font-size: 20px;
637
+ cursor: pointer;
638
+ padding: 2px 6px;
639
+ border-radius: 4px;
640
+ line-height: 1;
641
+ }
642
+
643
+ .vault-close:hover { color: var(--text); background: var(--border); }
644
+
645
+ .vault-body {
646
+ flex: 1;
647
+ overflow-y: auto;
648
+ padding: 16px 20px;
649
+ }
650
+
651
+ .vault-empty {
652
+ color: var(--text-dim);
653
+ font-size: 13px;
654
+ text-align: center;
655
+ padding: 24px 0;
656
+ }
657
+
658
+ .vault-key-item {
659
+ display: flex;
660
+ align-items: center;
661
+ justify-content: space-between;
662
+ padding: 10px 12px;
663
+ background: var(--bg);
664
+ border: 1px solid var(--border);
665
+ border-radius: var(--radius);
666
+ margin-bottom: 8px;
667
+ }
668
+
669
+ .vault-key-info {
670
+ flex: 1;
671
+ min-width: 0;
672
+ }
673
+
674
+ .vault-key-name {
675
+ font-size: 13px;
676
+ font-weight: 600;
677
+ font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
678
+ word-break: break-all;
679
+ }
680
+
681
+ .vault-key-label {
682
+ font-size: 11px;
683
+ color: var(--text-dim);
684
+ margin-top: 2px;
685
+ }
686
+
687
+ .vault-key-delete {
688
+ background: none;
689
+ border: none;
690
+ color: var(--text-dim);
691
+ font-size: 14px;
692
+ cursor: pointer;
693
+ padding: 4px 8px;
694
+ border-radius: 4px;
695
+ margin-left: 8px;
696
+ flex-shrink: 0;
697
+ }
698
+
699
+ .vault-key-delete:hover { color: var(--error); background: rgba(239,68,68,0.1); }
700
+
701
+ .vault-add {
702
+ border-top: 1px solid var(--border);
703
+ padding: 16px 20px;
704
+ flex-shrink: 0;
705
+ }
706
+
707
+ .vault-add h3 {
708
+ font-size: 13px;
709
+ font-weight: 600;
710
+ margin-bottom: 12px;
711
+ color: var(--text-dim);
712
+ text-transform: uppercase;
713
+ letter-spacing: 0.5px;
714
+ }
715
+
716
+ .vault-add input {
717
+ width: 100%;
718
+ padding: 8px 10px;
719
+ background: var(--bg);
720
+ border: 1px solid var(--border);
721
+ border-radius: var(--radius);
722
+ color: var(--text);
723
+ font-size: 13px;
724
+ font-family: inherit;
725
+ outline: none;
726
+ margin-bottom: 8px;
727
+ }
728
+
729
+ .vault-add input:focus { border-color: var(--accent); }
730
+ .vault-add input::placeholder { color: var(--text-dim); }
731
+
732
+ .vault-add-btn {
733
+ width: 100%;
734
+ padding: 8px 12px;
735
+ background: var(--accent);
736
+ color: #fff;
737
+ border: none;
738
+ border-radius: var(--radius);
739
+ font-size: 13px;
740
+ font-weight: 500;
741
+ cursor: pointer;
742
+ font-family: inherit;
743
+ transition: background 0.15s;
744
+ }
745
+
746
+ .vault-add-btn:hover { background: var(--accent-dim); }
747
+ .vault-add-btn:disabled { opacity: 0.5; cursor: not-allowed; }
748
+
749
+ .vault-error {
750
+ color: var(--error);
751
+ font-size: 12px;
752
+ margin-bottom: 8px;
753
+ display: none;
754
+ }
755
+
756
+ .vault-error.visible { display: block; }
757
+
758
+ /* --- Settings sections --- */
759
+
760
+ .settings-section {
761
+ padding-bottom: 16px;
762
+ margin-bottom: 16px;
763
+ border-bottom: 1px solid var(--border);
764
+ }
765
+
766
+ .settings-section:last-child {
767
+ border-bottom: none;
768
+ margin-bottom: 0;
769
+ }
770
+
771
+ .settings-section-title {
772
+ font-size: 13px;
773
+ font-weight: 600;
774
+ color: var(--text-dim);
775
+ text-transform: uppercase;
776
+ letter-spacing: 0.5px;
777
+ margin-bottom: 8px;
778
+ }
779
+
780
+ .settings-hint {
781
+ font-size: 12px;
782
+ color: var(--text-dim);
783
+ line-height: 1.4;
784
+ margin-bottom: 10px;
785
+ }
786
+
787
+ .prompt-textarea {
788
+ width: 100%;
789
+ padding: 10px 12px;
790
+ background: var(--bg);
791
+ border: 1px solid var(--border);
792
+ border-radius: var(--radius);
793
+ color: var(--text);
794
+ font-size: 13px;
795
+ font-family: inherit;
796
+ outline: none;
797
+ resize: vertical;
798
+ min-height: 80px;
799
+ line-height: 1.5;
800
+ transition: border-color 0.15s;
801
+ }
802
+
803
+ .prompt-textarea:focus { border-color: var(--accent); }
804
+ .prompt-textarea::placeholder { color: var(--text-dim); }
805
+
806
+ /* Personality trait sliders */
807
+ .personality-traits { display: flex; flex-direction: column; gap: 14px; margin-bottom: 14px; }
808
+ .trait-row {
809
+ display: flex;
810
+ align-items: center;
811
+ gap: 10px;
812
+ font-size: 12px;
813
+ }
814
+ .trait-label { width: 72px; text-align: right; color: var(--text-dim); flex-shrink: 0; }
815
+ .trait-label-right { width: 72px; text-align: left; color: var(--text-dim); flex-shrink: 0; }
816
+ .trait-slider {
817
+ flex: 1;
818
+ position: relative;
819
+ height: 20px;
820
+ display: flex;
821
+ align-items: center;
822
+ justify-content: space-between;
823
+ padding: 0 4px;
824
+ }
825
+ .trait-slider::before {
826
+ content: "";
827
+ position: absolute;
828
+ left: 4px; right: 4px;
829
+ top: 50%;
830
+ height: 2px;
831
+ background: var(--border);
832
+ transform: translateY(-50%);
833
+ border-radius: 1px;
834
+ }
835
+ .trait-dot {
836
+ width: 14px; height: 14px;
837
+ border-radius: 50%;
838
+ border: 2px solid var(--border);
839
+ background: transparent;
840
+ cursor: pointer;
841
+ position: relative;
842
+ z-index: 1;
843
+ transition: background 0.15s, border-color 0.15s;
844
+ padding: 0;
845
+ }
846
+ .trait-dot:hover { border-color: var(--accent); }
847
+ .trait-dot.active {
848
+ background: var(--accent);
849
+ border-color: var(--accent);
850
+ }
851
+ .custom-rules-toggle { margin-top: 4px; }
852
+ .custom-rules-toggle summary {
853
+ font-size: 12px;
854
+ color: var(--text-dim);
855
+ cursor: pointer;
856
+ user-select: none;
857
+ padding: 4px 0;
858
+ }
859
+ .custom-rules-toggle summary:hover { color: var(--text); }
860
+
861
+ .prompt-actions {
862
+ display: flex;
863
+ align-items: center;
864
+ gap: 10px;
865
+ margin-top: 8px;
866
+ }
867
+
868
+ .prompt-actions .vault-add-btn { width: auto; }
869
+
870
+ .prompt-status {
871
+ font-size: 12px;
872
+ color: #22c55e;
873
+ opacity: 0;
874
+ transition: opacity 0.3s;
875
+ }
876
+
877
+ .prompt-status.visible { opacity: 1; }
878
+ .prompt-status.error { color: var(--error); }
879
+
880
+ /* --- Toggle switch --- */
881
+
882
+ .toggle-row {
883
+ display: flex;
884
+ align-items: center;
885
+ justify-content: space-between;
886
+ padding: 8px 0;
887
+ }
888
+
889
+ .toggle-label {
890
+ font-size: 14px;
891
+ font-weight: 500;
892
+ }
893
+
894
+ .toggle-sublabel {
895
+ font-size: 12px;
896
+ color: var(--text-dim);
897
+ margin-top: 2px;
898
+ }
899
+
900
+ .toggle {
901
+ position: relative;
902
+ width: 44px;
903
+ height: 24px;
904
+ flex-shrink: 0;
905
+ }
906
+
907
+ .toggle input { opacity: 0; width: 0; height: 0; }
908
+
909
+ .toggle-track {
910
+ position: absolute;
911
+ inset: 0;
912
+ background: var(--border);
913
+ border-radius: 12px;
914
+ cursor: pointer;
915
+ transition: background 0.2s;
916
+ }
917
+
918
+ .toggle-track::after {
919
+ content: "";
920
+ position: absolute;
921
+ top: 3px;
922
+ left: 3px;
923
+ width: 18px;
924
+ height: 18px;
925
+ background: var(--text-dim);
926
+ border-radius: 50%;
927
+ transition: transform 0.2s, background 0.2s;
928
+ }
929
+
930
+ .toggle input:checked + .toggle-track { background: var(--accent); }
931
+ .toggle input:checked + .toggle-track::after { transform: translateX(20px); background: #fff; }
932
+
933
+ /* --- Model inputs --- */
934
+
935
+ .model-field {
936
+ margin-top: 12px;
937
+ }
938
+
939
+ .model-field label {
940
+ display: block;
941
+ font-size: 12px;
942
+ font-weight: 500;
943
+ color: var(--text-dim);
944
+ margin-bottom: 4px;
945
+ }
946
+
947
+ .model-field input {
948
+ width: 100%;
949
+ padding: 8px 10px;
950
+ background: var(--bg);
951
+ border: 1px solid var(--border);
952
+ border-radius: var(--radius);
953
+ color: var(--text);
954
+ font-size: 13px;
955
+ font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
956
+ outline: none;
957
+ transition: border-color 0.15s;
958
+ }
959
+
960
+ .model-field input:focus { border-color: var(--accent); }
961
+ .model-field input::placeholder { color: var(--text-dim); font-family: inherit; }
962
+
963
+ .settings-saved {
964
+ font-size: 12px;
965
+ color: #22c55e;
966
+ opacity: 0;
967
+ transition: opacity 0.3s;
968
+ margin-left: 8px;
969
+ }
970
+
971
+ .settings-saved.visible { opacity: 1; }
972
+
973
+ /* --- Activity tab --- */
974
+
975
+ .tab-group {
976
+ display: flex;
977
+ gap: 2px;
978
+ background: var(--bg);
979
+ border-radius: 6px;
980
+ padding: 2px;
981
+ margin-left: 8px;
982
+ }
983
+
984
+ .tab-btn {
985
+ position: relative;
986
+ background: none;
987
+ border: none;
988
+ color: var(--text-dim);
989
+ font-size: 12px;
990
+ font-weight: 500;
991
+ font-family: inherit;
992
+ padding: 4px 12px;
993
+ border-radius: 4px;
994
+ cursor: pointer;
995
+ transition: color 0.15s, background 0.15s;
996
+ }
997
+
998
+ .tab-btn:hover { color: var(--text); }
999
+ .tab-btn.active { color: var(--text); background: var(--border); }
1000
+
1001
+ .tab-badge {
1002
+ position: absolute;
1003
+ top: -4px;
1004
+ right: -6px;
1005
+ background: var(--accent);
1006
+ color: #fff;
1007
+ font-size: 10px;
1008
+ font-weight: 600;
1009
+ min-width: 16px;
1010
+ height: 16px;
1011
+ line-height: 16px;
1012
+ text-align: center;
1013
+ border-radius: 8px;
1014
+ padding: 0 4px;
1015
+ display: none;
1016
+ }
1017
+
1018
+ .tab-badge.visible { display: block; }
1019
+
1020
+ #activity-view {
1021
+ flex: 1;
1022
+ display: none;
1023
+ flex-direction: column;
1024
+ min-height: 0;
1025
+ }
1026
+
1027
+ #activity-view.active { display: flex; }
1028
+
1029
+ #activity-entries {
1030
+ flex: 1;
1031
+ overflow-y: auto;
1032
+ padding: 16px 0;
1033
+ }
1034
+
1035
+ .activity-entry {
1036
+ display: flex;
1037
+ align-items: baseline;
1038
+ gap: 10px;
1039
+ padding: 6px 20px;
1040
+ max-width: var(--max-width);
1041
+ margin: 0 auto;
1042
+ width: 100%;
1043
+ font-size: 13px;
1044
+ line-height: 1.5;
1045
+ }
1046
+
1047
+ .activity-time {
1048
+ color: var(--text-dim);
1049
+ font-size: 11px;
1050
+ font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
1051
+ flex-shrink: 0;
1052
+ min-width: 40px;
1053
+ }
1054
+
1055
+ .activity-source {
1056
+ font-size: 10px;
1057
+ font-weight: 600;
1058
+ text-transform: uppercase;
1059
+ letter-spacing: 0.3px;
1060
+ padding: 1px 6px;
1061
+ border-radius: 3px;
1062
+ flex-shrink: 0;
1063
+ }
1064
+
1065
+ .activity-source.goal-loop { background: rgba(59,130,246,0.15); color: #60a5fa; }
1066
+ .activity-source.learn { background: rgba(34,197,94,0.15); color: #4ade80; }
1067
+ .activity-source.search { background: rgba(245,158,11,0.15); color: #fbbf24; }
1068
+ .activity-source.system { background: rgba(120,120,120,0.15); color: #9ca3af; }
1069
+ .activity-source.agent { background: rgba(168,85,247,0.15); color: #c084fc; }
1070
+
1071
+ /* Agent waiting indicator (inline in chat) */
1072
+ .agent-status {
1073
+ margin: 8px 0;
1074
+ padding: 10px 14px;
1075
+ background: rgba(168,85,247,0.08);
1076
+ border: 1px solid rgba(168,85,247,0.25);
1077
+ border-radius: var(--radius);
1078
+ font-size: 13px;
1079
+ color: #c084fc;
1080
+ display: flex;
1081
+ align-items: center;
1082
+ gap: 10px;
1083
+ }
1084
+ .agent-status .agent-spinner {
1085
+ width: 14px; height: 14px;
1086
+ border: 2px solid rgba(168,85,247,0.3);
1087
+ border-top-color: #c084fc;
1088
+ border-radius: 50%;
1089
+ animation: spin 0.8s linear infinite;
1090
+ flex-shrink: 0;
1091
+ }
1092
+ .agent-status .agent-label { flex: 1; }
1093
+ .agent-status .agent-cancel {
1094
+ padding: 3px 10px;
1095
+ background: rgba(239,68,68,0.15);
1096
+ color: #f87171;
1097
+ border: 1px solid rgba(239,68,68,0.3);
1098
+ border-radius: 4px;
1099
+ font-size: 12px;
1100
+ cursor: pointer;
1101
+ font-family: inherit;
1102
+ }
1103
+ .agent-status .agent-cancel:hover { background: rgba(239,68,68,0.25); }
1104
+ .agent-status.done { border-color: rgba(34,197,94,0.3); background: rgba(34,197,94,0.08); color: #4ade80; }
1105
+ .agent-status.failed { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.08); color: #f87171; }
1106
+ @keyframes spin { to { transform: rotate(360deg); } }
1107
+
1108
+ /* Board item cards (inline in chat) */
1109
+ .board-cards { display: flex; flex-direction: column; gap: 6px; margin: 8px 0; }
1110
+ .board-card {
1111
+ display: flex;
1112
+ align-items: center;
1113
+ gap: 10px;
1114
+ padding: 10px 14px;
1115
+ background: rgba(59,130,246,0.08);
1116
+ border: 1px solid rgba(59,130,246,0.25);
1117
+ border-radius: var(--radius);
1118
+ font-size: 13px;
1119
+ color: #93c5fd;
1120
+ transition: opacity 0.3s, border-color 0.3s;
1121
+ }
1122
+ .board-card.card-done { opacity: 0.45; border-color: rgba(34,197,94,0.3); }
1123
+ .board-id {
1124
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
1125
+ font-size: 12px;
1126
+ padding: 2px 6px;
1127
+ background: rgba(59,130,246,0.15);
1128
+ border-radius: 4px;
1129
+ color: #60a5fa;
1130
+ flex-shrink: 0;
1131
+ font-weight: 600;
1132
+ }
1133
+ .board-title { flex: 1; color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1134
+ .board-priority {
1135
+ font-size: 11px;
1136
+ padding: 1px 5px;
1137
+ border-radius: 3px;
1138
+ flex-shrink: 0;
1139
+ }
1140
+ .board-priority.p1 { background: rgba(239,68,68,0.2); color: #f87171; }
1141
+ .board-priority.p2 { background: rgba(245,158,11,0.2); color: #fbbf24; }
1142
+ .board-priority.p3 { background: rgba(59,130,246,0.15); color: #93c5fd; }
1143
+ .board-priority.p4 { background: rgba(120,120,120,0.15); color: #9ca3af; }
1144
+ .board-state {
1145
+ font-size: 11px;
1146
+ padding: 2px 8px;
1147
+ border-radius: 10px;
1148
+ flex-shrink: 0;
1149
+ font-weight: 500;
1150
+ }
1151
+ .board-state.state-started { background: rgba(245,158,11,0.2); color: #fbbf24; }
1152
+ .board-state.state-completed { background: rgba(34,197,94,0.2); color: #4ade80; }
1153
+ .board-state.state-cancelled { background: rgba(239,68,68,0.2); color: #f87171; }
1154
+ .board-state.state-unstarted { background: rgba(59,130,246,0.15); color: #60a5fa; }
1155
+ .board-state.state-backlog { background: rgba(120,120,120,0.15); color: #9ca3af; }
1156
+ .board-state.state-triage { background: rgba(168,85,247,0.2); color: #c084fc; }
1157
+ .board-assignee { font-size: 11px; color: var(--text-dim); flex-shrink: 0; }
1158
+ .board-actions { flex-shrink: 0; }
1159
+ .board-actions button {
1160
+ padding: 3px 10px;
1161
+ background: rgba(34,197,94,0.15);
1162
+ color: #4ade80;
1163
+ border: 1px solid rgba(34,197,94,0.3);
1164
+ border-radius: 4px;
1165
+ font-size: 11px;
1166
+ cursor: pointer;
1167
+ font-family: inherit;
1168
+ }
1169
+ .board-actions button:hover { background: rgba(34,197,94,0.25); }
1170
+ .board-actions button:disabled { opacity: 0.4; cursor: default; }
1171
+
1172
+ .activity-body { flex: 1; min-width: 0; }
1173
+
1174
+ .activity-summary {
1175
+ color: var(--text);
1176
+ }
1177
+
1178
+ .activity-detail {
1179
+ color: var(--text-dim);
1180
+ font-size: 12px;
1181
+ margin-top: 2px;
1182
+ }
1183
+
1184
+ .activity-empty {
1185
+ color: var(--text-dim);
1186
+ font-size: 13px;
1187
+ text-align: center;
1188
+ padding: 40px 20px;
1189
+ }
1190
+
1191
+ .activity-entry {
1192
+ cursor: pointer;
1193
+ border-left: 2px solid transparent;
1194
+ transition: background 0.15s, border-color 0.15s;
1195
+ }
1196
+
1197
+ .activity-entry:hover {
1198
+ background: rgba(124, 111, 239, 0.04);
1199
+ }
1200
+
1201
+ .activity-entry.selected {
1202
+ border-left-color: var(--accent);
1203
+ background: rgba(124, 111, 239, 0.08);
1204
+ }
1205
+
1206
+ .activity-entry.selected .activity-summary {
1207
+ color: #fff;
1208
+ }
1209
+
1210
+ .activity-entry.has-backref {
1211
+ border-left-color: rgba(124, 111, 239, 0.3);
1212
+ }
1213
+
1214
+ .activity-lineage {
1215
+ color: var(--accent);
1216
+ font-size: 14px;
1217
+ margin-left: 6px;
1218
+ opacity: 0.6;
1219
+ }
1220
+
1221
+ /* Stream controls bar */
1222
+ .stream-controls {
1223
+ display: flex;
1224
+ align-items: center;
1225
+ gap: 10px;
1226
+ padding: 8px 20px;
1227
+ border-bottom: 1px solid var(--border);
1228
+ flex-shrink: 0;
1229
+ }
1230
+
1231
+ .stream-freeze-btn {
1232
+ background: none;
1233
+ border: 1px solid var(--border);
1234
+ color: var(--text-dim);
1235
+ font-size: 12px;
1236
+ font-family: inherit;
1237
+ padding: 4px 10px;
1238
+ border-radius: 4px;
1239
+ cursor: pointer;
1240
+ transition: color 0.15s, border-color 0.15s;
1241
+ display: flex;
1242
+ align-items: center;
1243
+ gap: 5px;
1244
+ }
1245
+
1246
+ .stream-freeze-btn:hover { color: var(--text); border-color: var(--text-dim); }
1247
+
1248
+ .stream-freeze-btn.frozen {
1249
+ color: var(--accent);
1250
+ border-color: var(--accent);
1251
+ animation: pulse-freeze 2s ease-in-out infinite;
1252
+ }
1253
+
1254
+ @keyframes pulse-freeze {
1255
+ 0%, 100% { opacity: 1; }
1256
+ 50% { opacity: 0.6; }
1257
+ }
1258
+
1259
+ .stream-selection-count {
1260
+ font-size: 12px;
1261
+ color: var(--text-dim);
1262
+ display: none;
1263
+ }
1264
+
1265
+ .stream-selection-count.visible { display: inline; }
1266
+
1267
+ .stream-branch-btn {
1268
+ background: var(--accent);
1269
+ color: #fff;
1270
+ border: none;
1271
+ font-size: 12px;
1272
+ font-weight: 500;
1273
+ font-family: inherit;
1274
+ padding: 4px 12px;
1275
+ border-radius: 4px;
1276
+ cursor: pointer;
1277
+ display: none;
1278
+ transition: background 0.15s;
1279
+ }
1280
+
1281
+ .stream-branch-btn:hover { background: var(--accent-dim); }
1282
+ .stream-branch-btn.visible { display: inline-block; }
1283
+
1284
+ /* Branch panel */
1285
+ .branch-panel {
1286
+ display: none;
1287
+ flex-direction: column;
1288
+ max-height: 40vh;
1289
+ border-top: 1px solid var(--border);
1290
+ background: var(--surface);
1291
+ }
1292
+
1293
+ .branch-panel.open { display: flex; }
1294
+
1295
+ .branch-header {
1296
+ display: flex;
1297
+ align-items: center;
1298
+ gap: 8px;
1299
+ padding: 8px 16px;
1300
+ font-size: 12px;
1301
+ color: var(--text-dim);
1302
+ border-bottom: 1px solid var(--border);
1303
+ flex-shrink: 0;
1304
+ }
1305
+
1306
+ .branch-header-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1307
+
1308
+ .branch-close-btn {
1309
+ background: none;
1310
+ border: none;
1311
+ color: var(--text-dim);
1312
+ font-size: 16px;
1313
+ cursor: pointer;
1314
+ padding: 0 4px;
1315
+ line-height: 1;
1316
+ }
1317
+
1318
+ .branch-close-btn:hover { color: var(--text); }
1319
+
1320
+ .branch-messages {
1321
+ flex: 1;
1322
+ overflow-y: auto;
1323
+ padding: 12px 16px;
1324
+ }
1325
+
1326
+ .branch-message {
1327
+ margin-bottom: 10px;
1328
+ font-size: 13px;
1329
+ line-height: 1.5;
1330
+ max-width: var(--max-width);
1331
+ }
1332
+
1333
+ .branch-message.user { color: var(--text-dim); }
1334
+ .branch-message.assistant { color: var(--text); }
1335
+
1336
+ .branch-message .branch-role {
1337
+ font-size: 11px;
1338
+ font-weight: 600;
1339
+ text-transform: uppercase;
1340
+ letter-spacing: 0.3px;
1341
+ margin-bottom: 2px;
1342
+ color: var(--text-dim);
1343
+ }
1344
+
1345
+ .branch-composer {
1346
+ display: flex;
1347
+ gap: 8px;
1348
+ padding: 8px 16px;
1349
+ border-top: 1px solid var(--border);
1350
+ flex-shrink: 0;
1351
+ }
1352
+
1353
+ .branch-input {
1354
+ flex: 1;
1355
+ padding: 8px 12px;
1356
+ background: var(--bg);
1357
+ border: 1px solid var(--border);
1358
+ border-radius: var(--radius);
1359
+ color: var(--text);
1360
+ font-size: 13px;
1361
+ font-family: inherit;
1362
+ outline: none;
1363
+ }
1364
+
1365
+ .branch-input:focus { border-color: var(--accent); }
1366
+
1367
+ .branch-send-btn {
1368
+ background: var(--accent);
1369
+ color: #fff;
1370
+ border: none;
1371
+ font-size: 13px;
1372
+ font-family: inherit;
1373
+ padding: 8px 16px;
1374
+ border-radius: var(--radius);
1375
+ cursor: pointer;
1376
+ }
1377
+
1378
+ .branch-send-btn:hover { background: var(--accent-dim); }
1379
+
1380
+ #send-btn.stop {
1381
+ background: #dc2626;
1382
+ }
1383
+
1384
+ #send-btn.stop:hover {
1385
+ background: #b91c1c;
1386
+ }
1387
+
1388
+ /* --- Avatar + Voice --- */
1389
+
1390
+ .chat-body {
1391
+ display: flex;
1392
+ flex: 1;
1393
+ overflow: hidden;
1394
+ min-height: 0;
1395
+ }
1396
+
1397
+
1398
+ /* ─── Thread sidebar ─── */
1399
+
1400
+ .thread-sidebar {
1401
+ width: 260px;
1402
+ flex-shrink: 0;
1403
+ display: flex;
1404
+ flex-direction: column;
1405
+ border-right: 1px solid var(--border);
1406
+ background: var(--surface);
1407
+ overflow: hidden;
1408
+ }
1409
+
1410
+ .thread-sidebar.hidden { display: none; }
1411
+
1412
+ .thread-sidebar-header {
1413
+ display: flex;
1414
+ align-items: center;
1415
+ gap: 8px;
1416
+ padding: 12px 14px;
1417
+ border-bottom: 1px solid var(--border);
1418
+ flex-shrink: 0;
1419
+ }
1420
+
1421
+ .thread-sidebar-header span {
1422
+ font-size: 12px;
1423
+ font-weight: 600;
1424
+ text-transform: uppercase;
1425
+ letter-spacing: 0.5px;
1426
+ color: var(--text-dim);
1427
+ flex: 1;
1428
+ }
1429
+
1430
+ .thread-new-btn {
1431
+ padding: 4px 10px;
1432
+ font-size: 12px;
1433
+ background: var(--accent);
1434
+ color: #fff;
1435
+ border: none;
1436
+ border-radius: 4px;
1437
+ cursor: pointer;
1438
+ font-weight: 500;
1439
+ }
1440
+ .thread-new-btn:hover { background: var(--accent-dim); }
1441
+
1442
+ .thread-list {
1443
+ flex: 1;
1444
+ overflow-y: auto;
1445
+ padding: 6px 0;
1446
+ }
1447
+
1448
+ .thread-item {
1449
+ display: flex;
1450
+ flex-direction: column;
1451
+ padding: 10px 14px;
1452
+ cursor: pointer;
1453
+ border-left: 3px solid transparent;
1454
+ transition: background 0.1s, border-color 0.1s;
1455
+ }
1456
+
1457
+ .thread-item:hover { background: rgba(255,255,255,0.03); }
1458
+
1459
+ .thread-item.active {
1460
+ background: rgba(109,93,252,0.08);
1461
+ border-left-color: var(--accent);
1462
+ }
1463
+
1464
+ .thread-item-title {
1465
+ font-size: 13px;
1466
+ font-weight: 500;
1467
+ color: var(--text);
1468
+ white-space: nowrap;
1469
+ overflow: hidden;
1470
+ text-overflow: ellipsis;
1471
+ }
1472
+
1473
+ .thread-item-meta {
1474
+ font-size: 11px;
1475
+ color: var(--text-dim);
1476
+ margin-top: 2px;
1477
+ }
1478
+
1479
+ .thread-toggle-btn {
1480
+ width: 32px;
1481
+ height: 32px;
1482
+ border: 1px solid var(--border);
1483
+ background: var(--surface);
1484
+ color: var(--text-dim);
1485
+ border-radius: 6px;
1486
+ cursor: pointer;
1487
+ font-size: 14px;
1488
+ display: flex;
1489
+ align-items: center;
1490
+ justify-content: center;
1491
+ flex-shrink: 0;
1492
+ }
1493
+ .thread-toggle-btn:hover { color: var(--text); border-color: var(--accent); }
1494
+
1495
+ .thread-current-label {
1496
+ font-size: 12px;
1497
+ color: var(--text-dim);
1498
+ max-width: 160px;
1499
+ white-space: nowrap;
1500
+ overflow: hidden;
1501
+ text-overflow: ellipsis;
1502
+ cursor: pointer;
1503
+ }
1504
+ .thread-current-label:hover { color: var(--text); }
1505
+
1506
+ .mic-btn {
1507
+ width: 42px;
1508
+ height: 42px;
1509
+ border-radius: 50%;
1510
+ border: 1px solid var(--border);
1511
+ background: var(--bg);
1512
+ color: var(--text-dim);
1513
+ font-size: 18px;
1514
+ cursor: pointer;
1515
+ display: flex;
1516
+ align-items: center;
1517
+ justify-content: center;
1518
+ transition: all 0.15s;
1519
+ flex-shrink: 0;
1520
+ }
1521
+
1522
+ .mic-btn:hover { border-color: var(--accent); color: var(--text); }
1523
+
1524
+ .mic-btn.listening {
1525
+ border-color: #ef4444;
1526
+ background: rgba(239,68,68,0.1);
1527
+ color: #ef4444;
1528
+ animation: mic-listen 2s ease-in-out infinite;
1529
+ }
1530
+
1531
+ @keyframes mic-listen {
1532
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.3); }
1533
+ 50% { box-shadow: 0 0 0 4px rgba(239,68,68,0); }
1534
+ }
1535
+
1536
+ .mic-btn.recording {
1537
+ border-color: #f59e0b;
1538
+ background: rgba(245,158,11,0.1);
1539
+ color: #f59e0b;
1540
+ animation: mic-pulse 1.2s ease-in-out infinite;
1541
+ }
1542
+
1543
+ @keyframes mic-pulse {
1544
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(245,158,11,0.3); }
1545
+ 50% { box-shadow: 0 0 0 6px rgba(245,158,11,0); }
1546
+ }
1547
+
1548
+ .mic-btn.hidden { display: none; }
1549
+
1550
+ .speaker-btn {
1551
+ background: none;
1552
+ border: none;
1553
+ color: var(--text-dim);
1554
+ cursor: pointer;
1555
+ font-size: 14px;
1556
+ padding: 2px 4px;
1557
+ margin-left: 6px;
1558
+ opacity: 0.6;
1559
+ transition: opacity 0.15s;
1560
+ }
1561
+
1562
+ .speaker-btn:hover { opacity: 1; }
1563
+
1564
+
1565
+ /* UI update toast */
1566
+ .ui-update-toast {
1567
+ position: fixed;
1568
+ top: 16px;
1569
+ left: 50%;
1570
+ transform: translateX(-50%) translateY(-100px);
1571
+ background: var(--surface);
1572
+ border: 1px solid var(--accent);
1573
+ border-radius: var(--radius);
1574
+ padding: 12px 20px;
1575
+ display: flex;
1576
+ align-items: center;
1577
+ gap: 12px;
1578
+ z-index: 10000;
1579
+ opacity: 0;
1580
+ transition: transform 0.3s ease, opacity 0.3s ease;
1581
+ box-shadow: 0 4px 20px rgba(124, 111, 239, 0.15);
1582
+ font-size: 13px;
1583
+ color: var(--text);
1584
+ }
1585
+ .ui-update-toast.visible {
1586
+ transform: translateX(-50%) translateY(0);
1587
+ opacity: 1;
1588
+ }
1589
+ .ui-update-toast button {
1590
+ background: var(--accent);
1591
+ color: #fff;
1592
+ border: none;
1593
+ border-radius: 4px;
1594
+ padding: 5px 14px;
1595
+ cursor: pointer;
1596
+ font-size: 12px;
1597
+ font-weight: 600;
1598
+ }
1599
+ .ui-update-toast button:hover { opacity: 0.85; }
1600
+ .ui-update-toast .dismiss {
1601
+ background: none;
1602
+ color: var(--text-dim);
1603
+ padding: 5px 8px;
1604
+ font-size: 16px;
1605
+ }
1606
+
1607
+ /* ─── Mobile ─── */
1608
+ @media (max-width: 640px) {
1609
+ /* Header: hide full nav, show hamburger-style overflow */
1610
+ .chat-header {
1611
+ padding: 8px 12px;
1612
+ gap: 6px;
1613
+ flex-wrap: wrap;
1614
+ }
1615
+ .chat-header .header-nav {
1616
+ order: 10;
1617
+ width: 100%;
1618
+ overflow-x: auto;
1619
+ -webkit-overflow-scrolling: touch;
1620
+ scrollbar-width: none;
1621
+ margin-left: 0;
1622
+ padding: 4px 0;
1623
+ gap: 6px;
1624
+ }
1625
+ .chat-header .header-nav::-webkit-scrollbar { display: none; }
1626
+ .chat-header .header-nav a {
1627
+ font-size: 12px;
1628
+ padding: 4px 8px;
1629
+ white-space: nowrap;
1630
+ flex-shrink: 0;
1631
+ }
1632
+ .nav-divider { display: none; }
1633
+
1634
+ /* Tab group compact */
1635
+ .tab-group { margin-left: 4px; }
1636
+ .tab-btn { font-size: 11px; padding: 3px 8px; }
1637
+
1638
+ /* Thread sidebar: full overlay on mobile */
1639
+ .thread-sidebar {
1640
+ position: fixed;
1641
+ top: 0; left: 0; bottom: 0;
1642
+ width: 280px;
1643
+ z-index: 50;
1644
+ box-shadow: 4px 0 20px rgba(0,0,0,0.5);
1645
+ }
1646
+ .thread-sidebar.hidden { display: none; }
1647
+
1648
+ /* Messages: tighter padding */
1649
+ .message { padding: 10px 12px; }
1650
+ .message .content { font-size: 14px; }
1651
+ .message .role { font-size: 10px; }
1652
+
1653
+ /* Composer: stack on small screens */
1654
+ .composer { padding: 8px 10px; }
1655
+ .composer-inner { gap: 6px; }
1656
+ .composer textarea {
1657
+ font-size: 16px; /* prevents iOS zoom on focus */
1658
+ padding: 8px 10px;
1659
+ min-height: 38px;
1660
+ }
1661
+ .composer button { padding: 8px 12px; font-size: 13px; }
1662
+ .composer-actions button { padding: 6px; font-size: 14px; }
1663
+ .mic-btn { width: 34px; height: 34px; font-size: 16px; }
1664
+
1665
+ /* Settings panel: full width on mobile */
1666
+ .vault-panel { width: 100vw; max-width: 100vw; }
1667
+
1668
+ /* Proactive chips: horizontal scroll */
1669
+ .question-chips { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
1670
+ .question-chip { flex-shrink: 0; }
1671
+
1672
+ /* Gear buttons: bigger tap targets */
1673
+ .gear-btn { padding: 6px 8px; font-size: 20px; }
1674
+
1675
+ /* Toast: full width on mobile */
1676
+ .ui-update-toast {
1677
+ left: 8px;
1678
+ right: 8px;
1679
+ transform: translateX(0) translateY(-100px);
1680
+ width: auto;
1681
+ }
1682
+ .ui-update-toast.visible {
1683
+ transform: translateX(0) translateY(0);
1684
+ }
1685
+
1686
+ /* Thread toggle: always visible */
1687
+ .thread-toggle-btn { display: inline-flex !important; }
1688
+
1689
+ /* Hide stream controls text on narrow */
1690
+ .stream-controls { font-size: 12px; }
1691
+
1692
+ /* Code blocks: smaller on mobile */
1693
+ .message .content pre { font-size: 12px; padding: 6px 8px; }
1694
+ .message .content code { font-size: 12px; }
1695
+ }
1696
+
1697
+ /* Small phone (< 380px) */
1698
+ @media (max-width: 380px) {
1699
+ .composer-actions button:not(#incognito-btn) { display: none; }
1700
+ .chat-header .header-nav a { font-size: 11px; padding: 3px 6px; }
1701
+ .gear-btn { font-size: 18px; padding: 4px 6px; }
1702
+ }
1703
+ </style>
1704
+ </head>
1705
+ <body>
1706
+
1707
+ <!-- Pairing screen -->
1708
+ <div id="pair-screen" class="screen">
1709
+ <div class="center-wrap">
1710
+ <div class="card">
1711
+ <!-- Step 1: Pairing code (manual flow only) -->
1712
+ <div id="pair-step-1" class="step active">
1713
+ <h1>Hello.</h1>
1714
+ <p>Enter the pairing code from your terminal to get started.</p>
1715
+ <div id="pair-error" class="error-msg"></div>
1716
+ <label for="pair-code">Pairing Code</label>
1717
+ <input type="text" id="pair-code" placeholder="amber-castle-seven-river-oak-noon" autocomplete="off" spellcheck="false">
1718
+ <button class="btn" id="pair-code-btn">Verify</button>
1719
+ </div>
1720
+
1721
+ <!-- Step 2: Name -->
1722
+ <div id="pair-step-2" class="step">
1723
+ <h1>Hello.</h1>
1724
+ <p>What should I call you?</p>
1725
+ <div id="pair-error-2" class="error-msg"></div>
1726
+ <label for="pair-name">Your Name</label>
1727
+ <input type="text" id="pair-name" placeholder="First name is fine" autocomplete="off">
1728
+ <button class="btn" id="pair-name-btn">Continue</button>
1729
+ </div>
1730
+
1731
+ <!-- Step 2b: Safe word -->
1732
+ <div id="pair-step-2b" class="step">
1733
+ <h1 id="pair-greeting">Nice to meet you.</h1>
1734
+ <p>Choose a safe word — something only we'll know. This encrypts your data.</p>
1735
+ <div id="pair-error-2b" class="error-msg"></div>
1736
+ <label for="pair-safeword">Safe Word</label>
1737
+ <input type="password" id="pair-safeword" placeholder="Something memorable">
1738
+ <label for="pair-safeword-confirm">Confirm Safe Word</label>
1739
+ <input type="password" id="pair-safeword-confirm" placeholder="Type it again">
1740
+ <button class="btn" id="pair-sw-btn">Continue</button>
1741
+ </div>
1742
+
1743
+ <!-- Step 3: Recovery -->
1744
+ <div id="pair-step-3" class="step">
1745
+ <h1>Almost there.</h1>
1746
+ <p>Set a recovery question in case you forget the safe word. Make it something personal.</p>
1747
+ <div id="pair-error-3" class="error-msg"></div>
1748
+ <label for="pair-question">Recovery Question</label>
1749
+ <input type="text" id="pair-question" placeholder="What was the name of...?">
1750
+ <label for="pair-answer">Recovery Answer</label>
1751
+ <input type="text" id="pair-answer" placeholder="Your answer">
1752
+ <button class="btn" id="pair-recovery-btn">Continue</button>
1753
+ </div>
1754
+
1755
+ <!-- Step 4: Name the agent -->
1756
+ <div id="pair-step-4" class="step">
1757
+ <h1 id="pair-agent-greeting">One more thing.</h1>
1758
+ <p>What would you like to call me?</p>
1759
+ <div id="pair-error-4" class="error-msg"></div>
1760
+ <label for="pair-agent-name">Agent Name</label>
1761
+ <input type="text" id="pair-agent-name" placeholder="Dash, Atlas, Nova... anything" autocomplete="off">
1762
+ <button class="btn" id="pair-finish-btn">Let's go</button>
1763
+ </div>
1764
+
1765
+ <!-- Step 5: Transition -->
1766
+ <div id="pair-step-5" class="step">
1767
+ <h1 id="pair-ready-msg">Setting things up...</h1>
1768
+ <p id="pair-ready-sub" style="opacity:0.6;"></p>
1769
+ </div>
1770
+ </div>
1771
+ </div>
1772
+ </div>
1773
+
1774
+ <!-- Auth screen -->
1775
+ <div id="auth-screen" class="screen">
1776
+ <div class="center-wrap">
1777
+ <div class="card">
1778
+ <div id="auth-main" class="step active">
1779
+ <h1>Welcome back.</h1>
1780
+ <p>What's the word?</p>
1781
+ <div id="auth-error" class="error-msg"></div>
1782
+ <label for="auth-safeword">Safe Word</label>
1783
+ <input type="password" id="auth-safeword" placeholder="Enter safe word">
1784
+ <button class="btn" id="auth-btn">Enter</button>
1785
+ <button class="link-btn" id="forgot-btn">Forgot the safe word?</button>
1786
+ </div>
1787
+
1788
+ <div id="auth-recover" class="step">
1789
+ <h1>Recovery</h1>
1790
+ <p id="recover-question-text"></p>
1791
+ <div id="recover-error" class="error-msg"></div>
1792
+ <label for="recover-answer">Your Answer</label>
1793
+ <input type="text" id="recover-answer" placeholder="Answer the question">
1794
+ <label for="recover-new-sw">New Safe Word</label>
1795
+ <input type="password" id="recover-new-sw" placeholder="Choose a new safe word">
1796
+ <button class="btn" id="recover-btn">Reset Safe Word</button>
1797
+ <button class="link-btn" id="back-to-auth-btn">Back</button>
1798
+ </div>
1799
+ </div>
1800
+ </div>
1801
+ </div>
1802
+
1803
+ <!-- Chat screen -->
1804
+ <div id="chat-screen" class="screen">
1805
+ <div class="chat-header">
1806
+ <span class="dot"></span>
1807
+ <a href="/" style="color:inherit;text-decoration:none;font-weight:600;" id="agent-name-header">Core</a>
1808
+ <nav class="header-nav" id="header-nav">
1809
+ <a href="/" class="active">Chat</a>
1810
+ <a href="/library">Library</a>
1811
+ <a href="/personal">Personal</a>
1812
+ <a href="/life">Life</a>
1813
+ <a href="/registry">Registry</a>
1814
+ <span class="nav-divider"></span>
1815
+ <a href="/observatory">Observatory</a>
1816
+ <a href="/ops">Operations</a>
1817
+ <a href="/board">Board</a>
1818
+ <a href="/roadmap">Roadmap</a>
1819
+ </nav>
1820
+ <button class="thread-toggle-btn" id="thread-toggle-btn" title="Toggle threads">&#9776;</button>
1821
+ <span class="thread-current-label" id="thread-current-label" title="Click to toggle threads"></span>
1822
+ <div class="tab-group" id="tab-group">
1823
+ <button class="tab-btn active" id="tab-chat" onclick="window.__switchTab('chat')">Chat</button>
1824
+ <button class="tab-btn" id="tab-activity" onclick="window.__switchTab('activity')">Stream<span class="tab-badge" id="activity-badge"></span></button>
1825
+ </div>
1826
+ <span class="spacer"></span>
1827
+ <button class="gear-btn" onclick="openShareModal()" title="Share Core">&#9993;</button>
1828
+ <button class="gear-btn" id="vault-open-btn" title="Settings">&#9881;</button>
1829
+ </div>
1830
+ <div class="chat-body">
1831
+ <div class="thread-sidebar hidden" id="thread-sidebar">
1832
+ <div class="thread-sidebar-header">
1833
+ <span>Threads</span>
1834
+ <button class="thread-new-btn" id="thread-new-btn">+ New</button>
1835
+ </div>
1836
+ <div class="thread-list" id="thread-list"></div>
1837
+ </div>
1838
+ <div style="display:flex;flex-direction:column;flex:1;min-width:0;">
1839
+ <div id="dash-greeting" class="dash-greeting" style="display:none;">
1840
+ <div class="greeting-text" id="greeting-text">Thinking...</div>
1841
+ <div id="question-chips" class="question-chips"></div>
1842
+ </div>
1843
+ <div class="messages" id="messages"></div>
1844
+ <div id="activity-view">
1845
+ <div class="stream-controls">
1846
+ <button class="stream-freeze-btn" id="stream-freeze-btn" title="Freeze/unfreeze stream">&#9654; Live</button>
1847
+ <span class="stream-selection-count" id="stream-selection-count"></span>
1848
+ <span style="flex:1;"></span>
1849
+ <button class="stream-branch-btn" id="stream-branch-btn">Branch</button>
1850
+ </div>
1851
+ <div id="activity-entries"><div class="activity-empty" id="activity-empty">No activity yet. Background actions will appear here.</div></div>
1852
+ <div class="branch-panel" id="branch-panel">
1853
+ <div class="branch-header">
1854
+ <span class="branch-header-text" id="branch-header-text"></span>
1855
+ <button class="branch-close-btn" id="branch-close-btn" title="Close branch">&times;</button>
1856
+ </div>
1857
+ <div class="branch-messages" id="branch-messages"></div>
1858
+ <div class="branch-composer">
1859
+ <input class="branch-input" id="branch-input" placeholder="Ask about these entries..." />
1860
+ <button class="branch-send-btn" id="branch-send-btn">Send</button>
1861
+ </div>
1862
+ </div>
1863
+ </div>
1864
+ <div class="composer" id="composer">
1865
+ <div class="incognito-label">incognito — local only, no memory</div>
1866
+ <div class="attachment-preview" id="attachment-preview"></div>
1867
+ <div class="composer-inner">
1868
+ <button class="mic-btn" id="mic-btn" title="Click to speak">&#127908;</button>
1869
+ <div class="composer-actions">
1870
+ <button id="attach-btn" title="Attach image">&#128206;</button>
1871
+ <button id="screenshot-btn" title="Screen capture">&#128247;</button>
1872
+ <button id="incognito-btn" title="Incognito — local only, no memory, plain text">&#128373;</button>
1873
+ </div>
1874
+ <input type="file" id="file-input" accept="image/*,.pdf,.docx,.doc,.txt,.md,.csv,.json,.js,.ts,.py,.html,.css,.xml,.yaml,.yml,.toml,.log,.sh,.bat,.c,.cpp,.java,.go,.rs,.rb,.php,.sql,.r,.swift" multiple style="display:none;" />
1875
+ <textarea id="chat-input" placeholder="Say something..." rows="1"></textarea>
1876
+ <button id="send-btn">Send</button>
1877
+ </div>
1878
+ </div>
1879
+ </div>
1880
+ </div>
1881
+ </div>
1882
+
1883
+ <!-- Settings slide-over -->
1884
+ <div class="vault-overlay" id="vault-overlay">
1885
+ <div class="vault-panel" id="vault-panel">
1886
+ <div class="vault-header">
1887
+ <h2>Settings</h2>
1888
+ <nav class="settings-nav">
1889
+ <a href="/board" title="Board"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></a>
1890
+ <a href="/ops" title="Ops"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg></a>
1891
+ <a href="/observatory" title="Observatory"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg></a>
1892
+ <a href="/library" title="Library"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></a>
1893
+ <a href="/registry" title="Registry"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></a>
1894
+ </nav>
1895
+ <button class="vault-close" id="vault-close-btn">&times;</button>
1896
+ </div>
1897
+ <div class="vault-body" id="vault-body">
1898
+ <div style="text-align:center;margin-bottom:12px;">
1899
+ <a href="/registry" style="color:var(--accent);font-size:13px;text-decoration:none;">Manage Services &amp; Capabilities &rarr;</a>
1900
+ </div>
1901
+ <!-- Personality section -->
1902
+ <div class="settings-section">
1903
+ <h3 class="settings-section-title">Personality</h3>
1904
+ <p class="settings-hint">Customize behavior. Slide traits away from center to shape the personality.</p>
1905
+ <div class="personality-traits" id="personality-traits">
1906
+ <div class="trait-row" data-trait="verbosity">
1907
+ <span class="trait-label">Concise</span>
1908
+ <div class="trait-slider">
1909
+ <button class="trait-dot" data-value="1" title="Concise"></button>
1910
+ <button class="trait-dot active" data-value="2" title="Balanced"></button>
1911
+ <button class="trait-dot" data-value="3" title="Verbose"></button>
1912
+ </div>
1913
+ <span class="trait-label-right">Verbose</span>
1914
+ </div>
1915
+ <div class="trait-row" data-trait="assertiveness">
1916
+ <span class="trait-label">Gentle</span>
1917
+ <div class="trait-slider">
1918
+ <button class="trait-dot" data-value="1" title="Gentle"></button>
1919
+ <button class="trait-dot active" data-value="2" title="Moderate"></button>
1920
+ <button class="trait-dot" data-value="3" title="Pushy"></button>
1921
+ </div>
1922
+ <span class="trait-label-right">Pushy</span>
1923
+ </div>
1924
+ <div class="trait-row" data-trait="tone">
1925
+ <span class="trait-label">Edgy</span>
1926
+ <div class="trait-slider">
1927
+ <button class="trait-dot" data-value="1" title="Edgy"></button>
1928
+ <button class="trait-dot active" data-value="2" title="Neutral"></button>
1929
+ <button class="trait-dot" data-value="3" title="Safe"></button>
1930
+ </div>
1931
+ <span class="trait-label-right">Safe</span>
1932
+ </div>
1933
+ <div class="trait-row" data-trait="formality">
1934
+ <span class="trait-label">Casual</span>
1935
+ <div class="trait-slider">
1936
+ <button class="trait-dot" data-value="1" title="Casual"></button>
1937
+ <button class="trait-dot active" data-value="2" title="Adaptive"></button>
1938
+ <button class="trait-dot" data-value="3" title="Formal"></button>
1939
+ </div>
1940
+ <span class="trait-label-right">Formal</span>
1941
+ </div>
1942
+ <div class="trait-row" data-trait="curiosity">
1943
+ <span class="trait-label">Task-focused</span>
1944
+ <div class="trait-slider">
1945
+ <button class="trait-dot" data-value="1" title="Task-focused"></button>
1946
+ <button class="trait-dot active" data-value="2" title="Balanced"></button>
1947
+ <button class="trait-dot" data-value="3" title="Exploratory"></button>
1948
+ </div>
1949
+ <span class="trait-label-right">Exploratory</span>
1950
+ </div>
1951
+ </div>
1952
+ <details class="custom-rules-toggle">
1953
+ <summary>Custom rules</summary>
1954
+ <textarea class="prompt-textarea" id="custom-rules-textarea" placeholder="One rule per line, e.g.&#10;Don't use emojis&#10;Always greet me by name" rows="4"></textarea>
1955
+ </details>
1956
+ <div class="prompt-actions">
1957
+ <button class="vault-add-btn" id="prompt-save-btn">Save</button>
1958
+ <span class="prompt-status" id="prompt-status"></span>
1959
+ </div>
1960
+ </div>
1961
+ <!-- Security section -->
1962
+ <div class="settings-section" id="security-settings-section">
1963
+ <h3 class="settings-section-title">Security</h3>
1964
+ <div class="toggle-row">
1965
+ <div>
1966
+ <div class="toggle-label">Safe Word Mode</div>
1967
+ <div class="toggle-sublabel" id="safeword-mode-label">Require safe word on every page load</div>
1968
+ </div>
1969
+ <select id="safeword-mode-select" style="background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:13px;outline:none;">
1970
+ <option value="always">Every load</option>
1971
+ <option value="restart">After restart only</option>
1972
+ </select>
1973
+ </div>
1974
+ <div class="prompt-actions" style="margin-top: 10px;">
1975
+ <button class="vault-add-btn" id="security-save-btn">Save</button>
1976
+ <span class="settings-saved" id="security-save-status">Saved</span>
1977
+ </div>
1978
+ </div>
1979
+ <!-- Network / Mesh section -->
1980
+ <div class="settings-section" id="mesh-settings-section">
1981
+ <h3 class="settings-section-title">Network</h3>
1982
+ <div class="toggle-row">
1983
+ <div>
1984
+ <div class="toggle-label">LAN Announcement</div>
1985
+ <div class="toggle-sublabel">Announce this instance on the local network via mDNS. Disable on shared or public networks.</div>
1986
+ </div>
1987
+ <label class="toggle">
1988
+ <input type="checkbox" id="mesh-lan-toggle">
1989
+ <span class="toggle-track"></span>
1990
+ </label>
1991
+ </div>
1992
+ <div class="toggle-row" style="margin-top: 8px;">
1993
+ <div>
1994
+ <div class="toggle-label">Allow Incoming</div>
1995
+ <div class="toggle-sublabel">Accept mesh connections from discovered peers on this network.</div>
1996
+ </div>
1997
+ <label class="toggle">
1998
+ <input type="checkbox" id="mesh-incoming-toggle">
1999
+ <span class="toggle-track"></span>
2000
+ </label>
2001
+ </div>
2002
+ <div class="prompt-actions" style="margin-top: 10px;">
2003
+ <button class="vault-add-btn" id="mesh-save-btn">Save</button>
2004
+ <span class="settings-saved" id="mesh-save-status">Saved</span>
2005
+ </div>
2006
+ </div>
2007
+ <!-- LLM Provider section -->
2008
+ <div class="settings-section" id="llm-settings-section">
2009
+ <h3 class="settings-section-title">LLM Provider</h3>
2010
+ <div class="toggle-row">
2011
+ <div>
2012
+ <div class="toggle-label">Airplane Mode</div>
2013
+ <div class="toggle-sublabel">ON = Ollama (local) &middot; OFF = OpenRouter (cloud)</div>
2014
+ </div>
2015
+ <label class="toggle">
2016
+ <input type="checkbox" id="airplane-toggle">
2017
+ <span class="toggle-track"></span>
2018
+ </label>
2019
+ </div>
2020
+ <div class="model-field">
2021
+ <label>Chat model</label>
2022
+ <input type="text" id="model-chat" placeholder="auto (provider default)">
2023
+ </div>
2024
+ <div class="model-field">
2025
+ <label>Utility model</label>
2026
+ <input type="text" id="model-utility" placeholder="auto (provider default)">
2027
+ </div>
2028
+ <div class="prompt-actions" style="margin-top: 10px;">
2029
+ <button class="vault-add-btn" id="llm-save-btn">Save</button>
2030
+ <span class="settings-saved" id="llm-save-status">Saved</span>
2031
+ </div>
2032
+ </div>
2033
+ <!-- Voice section -->
2034
+ <div class="settings-section" id="voice-settings-section">
2035
+ <h3 class="settings-section-title">Voice</h3>
2036
+ <div class="toggle-row">
2037
+ <div>
2038
+ <div class="toggle-label">Text-to-Speech</div>
2039
+ <div class="toggle-sublabel" id="tts-status-label">Checking...</div>
2040
+ </div>
2041
+ <label class="toggle">
2042
+ <input type="checkbox" id="tts-toggle">
2043
+ <span class="toggle-track"></span>
2044
+ </label>
2045
+ </div>
2046
+ <div class="toggle-row">
2047
+ <div>
2048
+ <div class="toggle-label">Speech-to-Text</div>
2049
+ <div class="toggle-sublabel" id="stt-status-label">Checking...</div>
2050
+ </div>
2051
+ <label class="toggle">
2052
+ <input type="checkbox" id="stt-toggle">
2053
+ <span class="toggle-track"></span>
2054
+ </label>
2055
+ </div>
2056
+ <div class="toggle-row">
2057
+ <div>
2058
+ <div class="toggle-label">Auto-play responses</div>
2059
+ <div class="toggle-sublabel">Automatically speak replies</div>
2060
+ </div>
2061
+ <label class="toggle">
2062
+ <input type="checkbox" id="autoplay-toggle">
2063
+ <span class="toggle-track"></span>
2064
+ </label>
2065
+ </div>
2066
+ <div class="prompt-actions" style="margin-top: 10px;">
2067
+ <button class="vault-add-btn" id="voice-save-btn">Save</button>
2068
+ <span class="settings-saved" id="voice-save-status">Saved</span>
2069
+ </div>
2070
+ </div>
2071
+ <!-- Google Workspace section -->
2072
+ <div class="settings-section" id="google-settings-section">
2073
+ <h3 class="settings-section-title">Google Workspace</h3>
2074
+ <div class="toggle-row">
2075
+ <div>
2076
+ <div class="toggle-label" id="google-status-label">Checking...</div>
2077
+ <div class="toggle-sublabel" id="google-scopes-label"></div>
2078
+ </div>
2079
+ <span id="google-status-dot" style="width:10px;height:10px;border-radius:50%;background:var(--text-dim);flex-shrink:0;"></span>
2080
+ </div>
2081
+ <p class="settings-hint" id="google-hint">Add <code>GOOGLE_CLIENT_ID</code> and <code>GOOGLE_CLIENT_SECRET</code> in the vault below, then click Connect.</p>
2082
+ <button class="vault-add-btn" id="google-connect-btn" style="margin-top:8px;">Connect Google</button>
2083
+ </div>
2084
+
2085
+ <!-- Task Board section -->
2086
+ <div class="settings-section" id="board-settings-section">
2087
+ <h3 class="settings-section-title">Task Board</h3>
2088
+ <div class="toggle-row">
2089
+ <div>
2090
+ <div class="toggle-label" id="board-provider-label">Checking...</div>
2091
+ <div class="toggle-sublabel" id="board-status-label">Checking connection...</div>
2092
+ </div>
2093
+ <span id="board-status-dot" style="width:10px;height:10px;border-radius:50%;background:var(--text-dim);flex-shrink:0;"></span>
2094
+ </div>
2095
+ <p class="settings-hint" id="board-hint">Add <code>LINEAR_API_KEY</code> in the vault below to connect your Linear workspace. Use <code>issues</code> or <code>todo &lt;title&gt;</code> in chat.</p>
2096
+ </div>
2097
+ <!-- Key Vault section -->
2098
+ <div class="settings-section">
2099
+ <h3 class="settings-section-title">Key Vault</h3>
2100
+ <div class="vault-empty" id="vault-empty">No keys stored yet.</div>
2101
+ <div id="vault-list"></div>
2102
+ </div>
2103
+ </div>
2104
+ <div class="vault-add">
2105
+ <h3>Add Key</h3>
2106
+ <div class="vault-error" id="vault-error"></div>
2107
+ <input type="text" id="vault-key-name" placeholder="KEY_NAME (e.g. OPENROUTER_API_KEY)">
2108
+ <input type="password" id="vault-key-value" placeholder="Value">
2109
+ <input type="text" id="vault-key-label" placeholder="Label (optional)">
2110
+ <button class="vault-add-btn" id="vault-add-btn">Add Key</button>
2111
+ </div>
2112
+ </div>
2113
+ </div>
2114
+
2115
+ <script>
2116
+ (function() {
2117
+ // --- State ---
2118
+ let sessionId = null;
2119
+ let userName = null;
2120
+ let agentName = "Core";
2121
+ let streaming = false;
2122
+ let abortController = null;
2123
+
2124
+ // Voice state
2125
+ let voiceStatus = { tts: false, stt: false };
2126
+ let ttsAutoPlay = true;
2127
+ let micRecording = false;
2128
+ let mediaRecorder = null;
2129
+ let micStream = null;
2130
+ let currentAudio = null;
2131
+
2132
+ // Convert any audio blob to WAV PCM16 (whisper-server needs WAV, browser records webm)
2133
+ async function blobToWav(blob) {
2134
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
2135
+ const arrayBuf = await blob.arrayBuffer();
2136
+ const decoded = await audioCtx.decodeAudioData(arrayBuf);
2137
+ const samples = decoded.getChannelData(0);
2138
+ const numSamples = samples.length;
2139
+ const buffer = new ArrayBuffer(44 + numSamples * 2);
2140
+ const view = new DataView(buffer);
2141
+ const sr = decoded.sampleRate;
2142
+ // RIFF header
2143
+ const writeStr = (off, s) => { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); };
2144
+ writeStr(0, "RIFF");
2145
+ view.setUint32(4, 36 + numSamples * 2, true);
2146
+ writeStr(8, "WAVE");
2147
+ writeStr(12, "fmt ");
2148
+ view.setUint32(16, 16, true);
2149
+ view.setUint16(20, 1, true);
2150
+ view.setUint16(22, 1, true);
2151
+ view.setUint32(24, sr, true);
2152
+ view.setUint32(28, sr * 2, true);
2153
+ view.setUint16(32, 2, true);
2154
+ view.setUint16(34, 16, true);
2155
+ writeStr(36, "data");
2156
+ view.setUint32(40, numSamples * 2, true);
2157
+ let off = 44;
2158
+ for (let i = 0; i < numSamples; i++, off += 2) {
2159
+ const s = Math.max(-1, Math.min(1, samples[i]));
2160
+ view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
2161
+ }
2162
+ audioCtx.close();
2163
+ return new Blob([buffer], { type: "audio/wav" });
2164
+ }
2165
+
2166
+ // Pairing step state (collected across steps)
2167
+ let pairCode = "";
2168
+ let pairName = "";
2169
+ let pairSafeWord = "";
2170
+ let pairAgentName = "";
2171
+ let isFirstRun = false;
2172
+
2173
+ // --- DOM refs ---
2174
+ const pairScreen = document.getElementById("pair-screen");
2175
+ const authScreen = document.getElementById("auth-screen");
2176
+ const chatScreen = document.getElementById("chat-screen");
2177
+ const messagesEl = document.getElementById("messages");
2178
+ const chatInput = document.getElementById("chat-input");
2179
+ const sendBtn = document.getElementById("send-btn");
2180
+ const fileInput = document.getElementById("file-input");
2181
+ const attachBtn = document.getElementById("attach-btn");
2182
+ const screenshotBtn = document.getElementById("screenshot-btn");
2183
+ const attachmentPreview = document.getElementById("attachment-preview");
2184
+ const composerEl = document.getElementById("composer");
2185
+ const incognitoBtn = document.getElementById("incognito-btn");
2186
+
2187
+ // --- Incognito mode state ---
2188
+ let incognitoMode = false;
2189
+ let incognitoHistory = [];
2190
+ let incognitoModel = "llama3.1:8b"; // updated on toggle via /api/local-model
2191
+ const defaultPlaceholder = "Say something...";
2192
+ const incognitoPlaceholder = "incognito — local only...";
2193
+
2194
+ incognitoBtn.addEventListener("click", async () => {
2195
+ incognitoMode = !incognitoMode;
2196
+ composerEl.classList.toggle("incognito", incognitoMode);
2197
+ incognitoBtn.classList.toggle("active", incognitoMode);
2198
+ chatInput.placeholder = incognitoMode ? incognitoPlaceholder : defaultPlaceholder;
2199
+ if (incognitoMode) {
2200
+ incognitoHistory = [];
2201
+ // Auto-detect best local model
2202
+ try {
2203
+ const r = await fetch("/api/local-model");
2204
+ if (r.ok) { const d = await r.json(); incognitoModel = d.model; }
2205
+ } catch {}
2206
+ addMessage("system", "Incognito mode on — " + incognitoModel + ", local only, no memory.");
2207
+ } else {
2208
+ chatInput.classList.remove("redacted");
2209
+ addMessage("system", "Incognito mode off — back to normal.");
2210
+ }
2211
+ chatInput.focus();
2212
+ });
2213
+
2214
+ // In incognito: block copy from chat messages
2215
+ document.addEventListener("copy", (e) => {
2216
+ if (!incognitoMode) return;
2217
+ const sel = window.getSelection();
2218
+ if (!sel || !sel.rangeCount) return;
2219
+ // Check if selection touches an incognito message
2220
+ const anchor = sel.anchorNode?.parentElement?.closest?.(".message.incognito");
2221
+ const focus = sel.focusNode?.parentElement?.closest?.(".message.incognito");
2222
+ if (anchor || focus) {
2223
+ e.preventDefault();
2224
+ e.clipboardData?.setData("text/plain", "[redacted — incognito]");
2225
+ }
2226
+ });
2227
+
2228
+ // In incognito: strip paste to plain text, block file attach
2229
+ chatInput.addEventListener("paste", (e) => {
2230
+ if (!incognitoMode) return; // normal mode — let default happen
2231
+ e.preventDefault();
2232
+ const text = (e.clipboardData || window.clipboardData).getData("text/plain");
2233
+ document.execCommand("insertText", false, text);
2234
+ });
2235
+ chatInput.addEventListener("drop", (e) => { if (incognitoMode) e.preventDefault(); });
2236
+ chatInput.addEventListener("dragover", (e) => { if (incognitoMode) e.preventDefault(); });
2237
+
2238
+ // Screenshot protection: blur incognito messages on PrintScreen / screen capture
2239
+ function setScreenCapture(on) {
2240
+ document.querySelectorAll(".message.incognito").forEach(el => {
2241
+ el.classList.toggle("screen-capture", on);
2242
+ });
2243
+ }
2244
+ // PrintScreen key detection (Windows/Linux)
2245
+ document.addEventListener("keyup", (e) => {
2246
+ if (e.key === "PrintScreen") {
2247
+ setScreenCapture(true);
2248
+ setTimeout(() => setScreenCapture(false), 3000);
2249
+ }
2250
+ });
2251
+ // Windows Snipping Tool: Win+Shift+S triggers blur on keydown
2252
+ document.addEventListener("keydown", (e) => {
2253
+ if (e.key === "s" && e.shiftKey && e.metaKey) {
2254
+ setScreenCapture(true);
2255
+ setTimeout(() => setScreenCapture(false), 5000);
2256
+ }
2257
+ });
2258
+ // Visibility change: blur when tab loses focus (screen share, some capture tools)
2259
+ document.addEventListener("visibilitychange", () => {
2260
+ if (document.hidden) {
2261
+ setScreenCapture(true);
2262
+ } else {
2263
+ setTimeout(() => setScreenCapture(false), 500);
2264
+ }
2265
+ });
2266
+
2267
+ // Auto-redact: visible on hover/focus, blurs when mouse leaves and not focused
2268
+ chatInput.addEventListener("mouseenter", () => {
2269
+ if (incognitoMode) chatInput.classList.remove("redacted");
2270
+ });
2271
+ chatInput.addEventListener("mouseleave", () => {
2272
+ if (incognitoMode && document.activeElement !== chatInput && chatInput.value.trim()) chatInput.classList.add("redacted");
2273
+ });
2274
+ chatInput.addEventListener("focus", () => {
2275
+ if (incognitoMode) chatInput.classList.remove("redacted");
2276
+ });
2277
+ chatInput.addEventListener("blur", () => {
2278
+ if (incognitoMode && chatInput.value.trim()) chatInput.classList.add("redacted");
2279
+ });
2280
+
2281
+ // --- Thread state ---
2282
+ const threadSidebar = document.getElementById("thread-sidebar");
2283
+ const threadListEl = document.getElementById("thread-list");
2284
+ const threadToggleBtn = document.getElementById("thread-toggle-btn");
2285
+ const threadNewBtn = document.getElementById("thread-new-btn");
2286
+ const threadCurrentLabel = document.getElementById("thread-current-label");
2287
+ let currentThreadId = null;
2288
+ let threadSidebarOpen = false;
2289
+ let threads = [];
2290
+
2291
+ // Mobile thread backdrop
2292
+ const threadBackdrop = document.createElement("div");
2293
+ threadBackdrop.style.cssText = "display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:49;";
2294
+ document.body.appendChild(threadBackdrop);
2295
+ threadBackdrop.addEventListener("click", () => toggleThreadSidebar());
2296
+
2297
+ function toggleThreadSidebar() {
2298
+ threadSidebarOpen = !threadSidebarOpen;
2299
+ threadSidebar.classList.toggle("hidden", !threadSidebarOpen);
2300
+ // Show backdrop on mobile when sidebar is open
2301
+ const isMobile = window.innerWidth <= 640;
2302
+ threadBackdrop.style.display = (threadSidebarOpen && isMobile) ? "block" : "none";
2303
+ if (threadSidebarOpen) loadThreads();
2304
+ }
2305
+
2306
+ threadToggleBtn.addEventListener("click", toggleThreadSidebar);
2307
+ threadCurrentLabel.addEventListener("click", toggleThreadSidebar);
2308
+
2309
+ async function loadThreads() {
2310
+ try {
2311
+ const data = await api("/api/threads?sessionId=" + encodeURIComponent(sessionId));
2312
+ threads = data.threads || [];
2313
+ renderThreadList();
2314
+ } catch (err) {
2315
+ console.log("Failed to load threads:", err.message);
2316
+ }
2317
+ }
2318
+
2319
+ function renderThreadList() {
2320
+ threadListEl.innerHTML = "";
2321
+ // Add "Main chat" as the default option
2322
+ const mainItem = document.createElement("div");
2323
+ mainItem.className = "thread-item" + (currentThreadId === null ? " active" : "");
2324
+ mainItem.innerHTML = '<div class="thread-item-title">Main chat</div><div class="thread-item-meta">Default conversation</div>';
2325
+ mainItem.addEventListener("click", () => switchThread(null));
2326
+ threadListEl.appendChild(mainItem);
2327
+
2328
+ for (const t of threads) {
2329
+ const item = document.createElement("div");
2330
+ item.className = "thread-item" + (currentThreadId === t.id ? " active" : "");
2331
+ item.dataset.id = t.id;
2332
+ const ago = timeAgo(t.updatedAt);
2333
+ item.innerHTML = '<div class="thread-item-title">' + escapeHtml(t.title) + '</div>'
2334
+ + '<div class="thread-item-meta">' + ago + '</div>';
2335
+ item.addEventListener("click", () => switchThread(t.id));
2336
+ threadListEl.appendChild(item);
2337
+ }
2338
+ }
2339
+
2340
+ function escapeHtml(text) {
2341
+ const div = document.createElement("div");
2342
+ div.textContent = text;
2343
+ return div.innerHTML;
2344
+ }
2345
+
2346
+ function timeAgo(iso) {
2347
+ const diff = Date.now() - new Date(iso).getTime();
2348
+ const mins = Math.floor(diff / 60000);
2349
+ if (mins < 1) return "just now";
2350
+ if (mins < 60) return mins + "m ago";
2351
+ const hours = Math.floor(mins / 60);
2352
+ if (hours < 24) return hours + "h ago";
2353
+ const days = Math.floor(hours / 24);
2354
+ return days + "d ago";
2355
+ }
2356
+
2357
+ async function switchThread(threadId) {
2358
+ if (threadId === currentThreadId) return;
2359
+ currentThreadId = threadId;
2360
+ messagesEl.innerHTML = "";
2361
+ renderThreadList();
2362
+ updateThreadLabel();
2363
+
2364
+ if (threadId) {
2365
+ // Load thread-specific history
2366
+ try {
2367
+ const data = await api("/api/threads/" + encodeURIComponent(threadId) + "/history?sessionId=" + encodeURIComponent(sessionId));
2368
+ if (data.messages) {
2369
+ for (const msg of data.messages) {
2370
+ let content = msg.content;
2371
+ if (typeof content === "string" && content.includes("[AGENT_REQUEST]")) {
2372
+ content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
2373
+ }
2374
+ addMessage(msg.role === "user" ? "user" : "assistant", content);
2375
+ }
2376
+ }
2377
+ } catch (err) {
2378
+ console.log("Failed to load thread history:", err.message);
2379
+ }
2380
+ } else {
2381
+ // Load main session history
2382
+ try {
2383
+ const data = await api("/api/history?sessionId=" + encodeURIComponent(sessionId));
2384
+ if (data.messages) {
2385
+ for (const msg of data.messages) {
2386
+ let content = msg.content;
2387
+ if (typeof content === "string" && content.includes("[AGENT_REQUEST]")) {
2388
+ content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
2389
+ }
2390
+ addMessage(msg.role === "user" ? "user" : "assistant", content);
2391
+ }
2392
+ }
2393
+ } catch (err) {
2394
+ console.log("Failed to load history:", err.message);
2395
+ }
2396
+ }
2397
+ }
2398
+
2399
+ function updateThreadLabel() {
2400
+ if (currentThreadId) {
2401
+ const t = threads.find(t => t.id === currentThreadId);
2402
+ threadCurrentLabel.textContent = t ? t.title : "Thread";
2403
+ } else {
2404
+ threadCurrentLabel.textContent = "";
2405
+ }
2406
+ }
2407
+
2408
+ threadNewBtn.addEventListener("click", async () => {
2409
+ try {
2410
+ const data = await fetch("/api/threads", {
2411
+ method: "POST",
2412
+ headers: { "Content-Type": "application/json" },
2413
+ body: JSON.stringify({ sessionId }),
2414
+ }).then(r => r.json());
2415
+
2416
+ if (data.thread) {
2417
+ await loadThreads();
2418
+ await switchThread(data.thread.id);
2419
+ }
2420
+ } catch (err) {
2421
+ console.error("Failed to create thread:", err.message);
2422
+ }
2423
+ });
2424
+
2425
+ // --- Attached files ---
2426
+ let pendingImages = []; // { data: base64, mimeType: string }
2427
+ let pendingFiles = []; // { name: string, text: string }
2428
+
2429
+ const TEXT_EXTENSIONS = [".txt",".md",".csv",".json",".js",".ts",".py",".html",".css",".xml",".yaml",".yml",".toml",".log",".sh",".bat",".c",".cpp",".java",".go",".rs",".rb",".php",".sql",".r",".swift"];
2430
+ const EXTRACT_EXTENSIONS = [".pdf",".docx",".doc"];
2431
+
2432
+ function getFileExt(name) { return "." + name.split(".").pop().toLowerCase(); }
2433
+
2434
+ attachBtn.addEventListener("click", () => { if (incognitoMode) return; fileInput.click(); });
2435
+ fileInput.addEventListener("change", async () => {
2436
+ for (const file of fileInput.files) {
2437
+ const ext = getFileExt(file.name);
2438
+
2439
+ if (file.type.startsWith("image/")) {
2440
+ // Image: base64 for vision
2441
+ const reader = new FileReader();
2442
+ reader.onload = () => {
2443
+ const base64 = reader.result.split(",")[1];
2444
+ pendingImages.push({ data: base64, mimeType: file.type });
2445
+ renderAttachmentPreviews();
2446
+ };
2447
+ reader.readAsDataURL(file);
2448
+ } else if (TEXT_EXTENSIONS.includes(ext)) {
2449
+ // Text file: read client-side
2450
+ const text = await file.text();
2451
+ pendingFiles.push({ name: file.name, text: text.slice(0, 50000) });
2452
+ renderAttachmentPreviews();
2453
+ } else if (EXTRACT_EXTENSIONS.includes(ext)) {
2454
+ // PDF/DOCX: extract server-side
2455
+ const formData = new FormData();
2456
+ formData.append("file", file);
2457
+ try {
2458
+ const res = await fetch("/api/extract", { method: "POST", body: formData });
2459
+ const data = await res.json();
2460
+ if (data.text) {
2461
+ pendingFiles.push({ name: file.name, text: data.text });
2462
+ renderAttachmentPreviews();
2463
+ } else {
2464
+ addMessage("system", "Failed to extract: " + (data.error || file.name));
2465
+ }
2466
+ } catch (err) {
2467
+ addMessage("system", "Upload failed: " + err.message);
2468
+ }
2469
+ } else {
2470
+ // Try as text
2471
+ try {
2472
+ const text = await file.text();
2473
+ pendingFiles.push({ name: file.name, text: text.slice(0, 50000) });
2474
+ renderAttachmentPreviews();
2475
+ } catch {
2476
+ addMessage("system", "Unsupported file: " + file.name);
2477
+ }
2478
+ }
2479
+ }
2480
+ fileInput.value = "";
2481
+ });
2482
+
2483
+ screenshotBtn.addEventListener("click", async () => {
2484
+ if (incognitoMode) return;
2485
+ try {
2486
+ // Use html2canvas-style approach: capture the visible page
2487
+ const canvas = document.createElement("canvas");
2488
+ const rect = document.body.getBoundingClientRect();
2489
+ canvas.width = Math.min(window.innerWidth, 1280);
2490
+ canvas.height = Math.min(window.innerHeight, 960);
2491
+ // Use the Screen Capture API
2492
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: { width: 1280, height: 960 } });
2493
+ const video = document.createElement("video");
2494
+ video.srcObject = stream;
2495
+ await video.play();
2496
+ // Wait a frame for the video to render
2497
+ await new Promise(r => setTimeout(r, 100));
2498
+ canvas.width = video.videoWidth;
2499
+ canvas.height = video.videoHeight;
2500
+ canvas.getContext("2d").drawImage(video, 0, 0);
2501
+ stream.getTracks().forEach(t => t.stop());
2502
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.8);
2503
+ const base64 = dataUrl.split(",")[1];
2504
+ pendingImages.push({ data: base64, mimeType: "image/jpeg" });
2505
+ renderAttachmentPreviews();
2506
+ } catch (err) {
2507
+ console.log("Screen capture cancelled or failed:", err.message);
2508
+ }
2509
+ });
2510
+
2511
+ function renderAttachmentPreviews() {
2512
+ attachmentPreview.innerHTML = "";
2513
+ pendingImages.forEach((img, i) => {
2514
+ const thumb = document.createElement("div");
2515
+ thumb.className = "thumb";
2516
+ const imgEl = document.createElement("img");
2517
+ imgEl.src = "data:" + img.mimeType + ";base64," + img.data;
2518
+ thumb.appendChild(imgEl);
2519
+ const removeBtn = document.createElement("button");
2520
+ removeBtn.className = "remove-btn";
2521
+ removeBtn.textContent = "x";
2522
+ removeBtn.addEventListener("click", () => { pendingImages.splice(i, 1); renderAttachmentPreviews(); });
2523
+ thumb.appendChild(removeBtn);
2524
+ attachmentPreview.appendChild(thumb);
2525
+ });
2526
+ pendingFiles.forEach((file, i) => {
2527
+ const chip = document.createElement("div");
2528
+ chip.className = "thumb";
2529
+ chip.style.cssText = "width:auto;height:auto;padding:4px 8px;display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text);";
2530
+ chip.textContent = file.name;
2531
+ const removeBtn = document.createElement("button");
2532
+ removeBtn.className = "remove-btn";
2533
+ removeBtn.textContent = "x";
2534
+ removeBtn.addEventListener("click", () => { pendingFiles.splice(i, 1); renderAttachmentPreviews(); });
2535
+ chip.appendChild(removeBtn);
2536
+ attachmentPreview.appendChild(chip);
2537
+ });
2538
+ }
2539
+
2540
+ // --- Screen switching ---
2541
+ function showScreen(id) {
2542
+ document.querySelectorAll(".screen").forEach(s => s.classList.remove("active"));
2543
+ document.getElementById(id).classList.add("active");
2544
+ }
2545
+
2546
+ function showStep(parentId, stepId) {
2547
+ const parent = document.getElementById(parentId);
2548
+ parent.querySelectorAll(".step").forEach(s => s.classList.remove("active"));
2549
+ document.getElementById(stepId).classList.add("active");
2550
+ }
2551
+
2552
+ function showError(id, msg) {
2553
+ const el = document.getElementById(id);
2554
+ el.textContent = msg;
2555
+ el.classList.add("visible");
2556
+ }
2557
+
2558
+ function clearError(id) {
2559
+ const el = document.getElementById(id);
2560
+ el.textContent = "";
2561
+ el.classList.remove("visible");
2562
+ }
2563
+
2564
+ // --- API helpers ---
2565
+ async function api(path, opts = {}) {
2566
+ const res = await fetch(path, {
2567
+ method: opts.method || "GET",
2568
+ headers: opts.body ? { "Content-Type": "application/json" } : {},
2569
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
2570
+ });
2571
+ const data = await res.json();
2572
+ if (!res.ok) throw new Error(data.error || "Request failed");
2573
+ return data;
2574
+ }
2575
+
2576
+ // --- Init ---
2577
+ async function init() {
2578
+ try {
2579
+ // Check for startup token in URL (zero-friction local auth)
2580
+ const urlParams = new URLSearchParams(window.location.search);
2581
+ const token = urlParams.get("token");
2582
+
2583
+ if (token) {
2584
+ // Clean the token from the URL (don't leave it in browser history)
2585
+ window.history.replaceState({}, "", window.location.pathname);
2586
+
2587
+ try {
2588
+ const tokenResult = await api("/api/auth/token?t=" + encodeURIComponent(token));
2589
+ if (tokenResult.valid) {
2590
+ if (tokenResult.needsPairing) {
2591
+ // First run: skip code entry, go straight to name + safe word
2592
+ showScreen("pair-screen");
2593
+ showStep("pair-screen", "pair-step-2");
2594
+ pairCode = "__auto_token__";
2595
+ isFirstRun = true;
2596
+ document.getElementById("pair-name").focus();
2597
+ return;
2598
+ }
2599
+ // Already paired — try to resume session
2600
+ const check = await api("/api/auth/active-session");
2601
+ if (check.valid) {
2602
+ sessionId = check.sessionId;
2603
+ userName = check.name;
2604
+ enterChat();
2605
+ return;
2606
+ }
2607
+ }
2608
+ } catch (e) {
2609
+ console.log("Token auth failed, falling back to normal flow:", e);
2610
+ }
2611
+ }
2612
+
2613
+ const status = await api("/api/status");
2614
+ if (status.agentName) agentName = status.agentName;
2615
+ if (status.paired) {
2616
+ // "restart" mode: try to resume session
2617
+ if (status.safeWordMode === "restart") {
2618
+ // First try sessionStorage (set by enterChat on previous visit)
2619
+ const storedSid = sessionStorage.getItem("dash_sid");
2620
+ if (storedSid) {
2621
+ try {
2622
+ const check = await api("/api/auth/validate?sessionId=" + encodeURIComponent(storedSid));
2623
+ if (check.valid) {
2624
+ sessionId = storedSid;
2625
+ userName = check.name;
2626
+ enterChat();
2627
+ return;
2628
+ }
2629
+ } catch {}
2630
+ sessionStorage.removeItem("dash_sid");
2631
+ }
2632
+ // No stored session — check if server has an active session
2633
+ // (user may have navigated from /library or /board without going through index.html first)
2634
+ try {
2635
+ const check = await api("/api/auth/active-session");
2636
+ if (check.valid) {
2637
+ sessionId = check.sessionId;
2638
+ userName = check.name;
2639
+ enterChat();
2640
+ return;
2641
+ }
2642
+ } catch {}
2643
+ }
2644
+ showScreen("auth-screen");
2645
+ document.getElementById("auth-safeword").focus();
2646
+ } else if (status.needsCode) {
2647
+ showScreen("pair-screen");
2648
+ document.getElementById("pair-code").focus();
2649
+ } else {
2650
+ // No pairing code yet — unusual, show pairing screen anyway
2651
+ showScreen("pair-screen");
2652
+ }
2653
+ } catch (err) {
2654
+ console.error("Init failed:", err);
2655
+ showScreen("pair-screen");
2656
+ }
2657
+ }
2658
+
2659
+ // --- Pairing flow ---
2660
+
2661
+ // Step 1: Verify code (client-side only, actual verify happens on submit)
2662
+ document.getElementById("pair-code-btn").addEventListener("click", () => {
2663
+ clearError("pair-error");
2664
+ const code = document.getElementById("pair-code").value.trim();
2665
+ if (!code) {
2666
+ showError("pair-error", "Enter the pairing code");
2667
+ return;
2668
+ }
2669
+ pairCode = code;
2670
+ isFirstRun = true;
2671
+ showStep("pair-screen", "pair-step-2");
2672
+ document.getElementById("pair-name").focus();
2673
+ });
2674
+
2675
+ document.getElementById("pair-code").addEventListener("keydown", (e) => {
2676
+ if (e.key === "Enter") document.getElementById("pair-code-btn").click();
2677
+ });
2678
+
2679
+ // Step 2: Name only
2680
+ document.getElementById("pair-name-btn").addEventListener("click", () => {
2681
+ clearError("pair-error-2");
2682
+ const name = document.getElementById("pair-name").value.trim();
2683
+ if (!name) { showError("pair-error-2", "Enter your name"); return; }
2684
+ pairName = name;
2685
+ document.getElementById("pair-greeting").textContent = `Nice to meet you, ${name}.`;
2686
+ showStep("pair-screen", "pair-step-2b");
2687
+ document.getElementById("pair-safeword").focus();
2688
+ });
2689
+
2690
+ document.getElementById("pair-name").addEventListener("keydown", (e) => {
2691
+ if (e.key === "Enter") document.getElementById("pair-name-btn").click();
2692
+ });
2693
+
2694
+ // Step 2b: Safe word with confirmation
2695
+ document.getElementById("pair-sw-btn").addEventListener("click", () => {
2696
+ clearError("pair-error-2b");
2697
+ const sw = document.getElementById("pair-safeword").value.trim();
2698
+ const confirm = document.getElementById("pair-safeword-confirm").value.trim();
2699
+ if (!sw) { showError("pair-error-2b", "Choose a safe word"); return; }
2700
+ if (sw !== confirm) { showError("pair-error-2b", "Safe words don't match"); return; }
2701
+ pairSafeWord = sw;
2702
+ document.getElementById("pair-agent-greeting").textContent = `Thanks, ${pairName}.`;
2703
+ showStep("pair-screen", "pair-step-4");
2704
+ document.getElementById("pair-agent-name").focus();
2705
+ });
2706
+
2707
+ document.getElementById("pair-safeword-confirm").addEventListener("keydown", (e) => {
2708
+ if (e.key === "Enter") document.getElementById("pair-sw-btn").click();
2709
+ });
2710
+
2711
+ // Step 3: Recovery → go to agent naming
2712
+ document.getElementById("pair-recovery-btn").addEventListener("click", () => {
2713
+ clearError("pair-error-3");
2714
+ const question = document.getElementById("pair-question").value.trim();
2715
+ const answer = document.getElementById("pair-answer").value.trim();
2716
+ if (!question) { showError("pair-error-3", "Enter a recovery question"); return; }
2717
+ if (!answer) { showError("pair-error-3", "Enter a recovery answer"); return; }
2718
+ document.getElementById("pair-agent-greeting").textContent = `Thanks, ${pairName}.`;
2719
+ showStep("pair-screen", "pair-step-4");
2720
+ document.getElementById("pair-agent-name").focus();
2721
+ });
2722
+
2723
+ document.getElementById("pair-answer").addEventListener("keydown", (e) => {
2724
+ if (e.key === "Enter") document.getElementById("pair-recovery-btn").click();
2725
+ });
2726
+
2727
+ // Step 4: Name the agent → submit all
2728
+ document.getElementById("pair-finish-btn").addEventListener("click", async () => {
2729
+ clearError("pair-error-4");
2730
+ const agentName = document.getElementById("pair-agent-name").value.trim();
2731
+ if (!agentName) { showError("pair-error-4", "Give me a name"); return; }
2732
+ pairAgentName = agentName;
2733
+
2734
+ const btn = document.getElementById("pair-finish-btn");
2735
+ btn.disabled = true;
2736
+ btn.textContent = "Setting up...";
2737
+
2738
+ try {
2739
+ const result = await api("/api/pair", {
2740
+ method: "POST",
2741
+ body: {
2742
+ code: pairCode,
2743
+ name: pairName,
2744
+ safeWord: pairSafeWord,
2745
+ agentName: pairAgentName,
2746
+ },
2747
+ });
2748
+ sessionId = result.sessionId;
2749
+ userName = result.name;
2750
+
2751
+ // Transition moment
2752
+ showStep("pair-screen", "pair-step-5");
2753
+ document.getElementById("pair-ready-msg").textContent = `${pairAgentName} is ready.`;
2754
+ document.getElementById("pair-ready-sub").textContent = `Nice to meet you, ${pairName}.`;
2755
+ await new Promise(r => setTimeout(r, 1800));
2756
+ enterChat();
2757
+ } catch (err) {
2758
+ showError("pair-error-4", err.message);
2759
+ if (err.message.toLowerCase().includes("code")) {
2760
+ showStep("pair-screen", "pair-step-1");
2761
+ showError("pair-error", err.message);
2762
+ }
2763
+ } finally {
2764
+ btn.disabled = false;
2765
+ btn.textContent = "Let's go";
2766
+ }
2767
+ });
2768
+
2769
+ document.getElementById("pair-agent-name").addEventListener("keydown", (e) => {
2770
+ if (e.key === "Enter") document.getElementById("pair-finish-btn").click();
2771
+ });
2772
+
2773
+ // --- Auth flow ---
2774
+
2775
+ document.getElementById("auth-btn").addEventListener("click", async () => {
2776
+ clearError("auth-error");
2777
+ const sw = document.getElementById("auth-safeword").value.trim();
2778
+ if (!sw) { showError("auth-error", "Enter your safe word"); return; }
2779
+
2780
+ const btn = document.getElementById("auth-btn");
2781
+ btn.disabled = true;
2782
+
2783
+ try {
2784
+ const result = await api("/api/auth", {
2785
+ method: "POST",
2786
+ body: { safeWord: sw },
2787
+ });
2788
+ sessionId = result.sessionId;
2789
+ userName = result.name;
2790
+ enterChat();
2791
+ } catch (err) {
2792
+ showError("auth-error", err.message);
2793
+ } finally {
2794
+ btn.disabled = false;
2795
+ }
2796
+ });
2797
+
2798
+ document.getElementById("auth-safeword").addEventListener("keydown", (e) => {
2799
+ if (e.key === "Enter") document.getElementById("auth-btn").click();
2800
+ });
2801
+
2802
+ // --- Recovery flow ---
2803
+
2804
+ document.getElementById("forgot-btn").addEventListener("click", async () => {
2805
+ try {
2806
+ const data = await api("/api/recover");
2807
+ document.getElementById("recover-question-text").textContent = data.question;
2808
+ showStep("auth-screen", "auth-recover");
2809
+ document.getElementById("recover-answer").focus();
2810
+ } catch (err) {
2811
+ showError("auth-error", err.message);
2812
+ }
2813
+ });
2814
+
2815
+ document.getElementById("back-to-auth-btn").addEventListener("click", () => {
2816
+ showStep("auth-screen", "auth-main");
2817
+ });
2818
+
2819
+ document.getElementById("recover-btn").addEventListener("click", async () => {
2820
+ clearError("recover-error");
2821
+ const answer = document.getElementById("recover-answer").value.trim();
2822
+ const newSw = document.getElementById("recover-new-sw").value.trim();
2823
+ if (!answer) { showError("recover-error", "Enter your answer"); return; }
2824
+ if (!newSw) { showError("recover-error", "Choose a new safe word"); return; }
2825
+
2826
+ const btn = document.getElementById("recover-btn");
2827
+ btn.disabled = true;
2828
+
2829
+ try {
2830
+ const result = await api("/api/recover", {
2831
+ method: "POST",
2832
+ body: { answer, newSafeWord: newSw },
2833
+ });
2834
+ sessionId = result.sessionId;
2835
+ userName = result.name;
2836
+ enterChat();
2837
+ } catch (err) {
2838
+ showError("recover-error", err.message);
2839
+ } finally {
2840
+ btn.disabled = false;
2841
+ }
2842
+ });
2843
+
2844
+ document.getElementById("recover-new-sw").addEventListener("keydown", (e) => {
2845
+ if (e.key === "Enter") document.getElementById("recover-btn").click();
2846
+ });
2847
+
2848
+ // --- Chat ---
2849
+
2850
+ async function enterChat() {
2851
+ // Persist session for "restart" mode — survives page refresh but not browser close
2852
+ if (sessionId) sessionStorage.setItem("dash_sid", sessionId);
2853
+ // Update header with agent name
2854
+ const hdr = document.getElementById("agent-name-header");
2855
+ if (hdr) hdr.textContent = agentName;
2856
+ document.title = agentName;
2857
+
2858
+ // First-run: simplified UI — just chat, settings, share
2859
+ if (isFirstRun) {
2860
+ const nav = document.getElementById("header-nav");
2861
+ const tabs = document.getElementById("tab-group");
2862
+ const threadBtn = document.getElementById("thread-toggle-btn");
2863
+ const threadLabel = document.getElementById("thread-current-label");
2864
+ if (nav) nav.style.display = "none";
2865
+ if (tabs) tabs.style.display = "none";
2866
+ if (threadBtn) threadBtn.style.display = "none";
2867
+ if (threadLabel) threadLabel.style.display = "none";
2868
+ }
2869
+
2870
+ showScreen("chat-screen");
2871
+ chatInput.focus();
2872
+
2873
+ // Load stored conversation history
2874
+ try {
2875
+ const data = await api("/api/history?sessionId=" + encodeURIComponent(sessionId));
2876
+ if (data.messages && data.messages.length > 0) {
2877
+ messagesEl.innerHTML = "";
2878
+ for (const msg of data.messages) {
2879
+ let content = msg.content;
2880
+ if (content.includes("[AGENT_REQUEST]")) {
2881
+ content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
2882
+ }
2883
+ addMessage(msg.role === "user" ? "user" : "assistant", content);
2884
+ }
2885
+ }
2886
+ } catch (err) {
2887
+ console.log("No history to restore:", err.message);
2888
+ }
2889
+
2890
+ // First-run: agent introduces itself
2891
+ if (isFirstRun) {
2892
+ isFirstRun = false;
2893
+ const intro = `Hey ${userName || "there"}! I'm ${agentName}. I'm your personal AI — everything stays local on your machine. No cloud, no tracking, just us.\n\nHere's what I can help with:\n- **Chat** about anything — I'll remember our conversations\n- **Settings** (gear icon) to customize my personality\n- **Share** (envelope icon) to invite someone to run their own instance\n\nWhat would you like to talk about first?`;
2894
+ addMessage("assistant", intro);
2895
+ }
2896
+
2897
+ startActivityPolling();
2898
+
2899
+ // Pre-load threads list
2900
+ await loadThreads();
2901
+
2902
+ // Restore thread + scroll after toast refresh
2903
+ try {
2904
+ const restore = sessionStorage.getItem("_dash_restore");
2905
+ if (restore) {
2906
+ sessionStorage.removeItem("_dash_restore");
2907
+ const { thread, scroll } = JSON.parse(restore);
2908
+ if (thread) await switchThread(thread);
2909
+ if (scroll && messagesEl) requestAnimationFrame(() => { messagesEl.scrollTop = scroll; });
2910
+ }
2911
+ } catch (e) { console.log("Restore failed:", e); }
2912
+
2913
+ // Initialize voice features (non-blocking)
2914
+ initVoice();
2915
+
2916
+ // Load proactive questions ("Dash is wondering...")
2917
+ loadPendingQuestions();
2918
+
2919
+ // Auto-open settings if linked from another page via ?settings=open
2920
+ if (new URLSearchParams(location.search).get('settings') === 'open') {
2921
+ document.getElementById('vault-open-btn')?.click();
2922
+ history.replaceState({}, '', '/');
2923
+ }
2924
+ }
2925
+
2926
+ function loadPendingQuestions() {
2927
+ const greetingEl = document.getElementById("dash-greeting");
2928
+ const chipsEl = document.getElementById("question-chips");
2929
+ if (!greetingEl || !chipsEl) return;
2930
+
2931
+ fetch("/api/pending-questions")
2932
+ .then(r => r.json())
2933
+ .then(data => {
2934
+ const questions = data.questions;
2935
+ if (!questions || questions.length === 0) {
2936
+ greetingEl.style.display = "none";
2937
+ return;
2938
+ }
2939
+
2940
+ chipsEl.innerHTML = "";
2941
+ for (const q of questions) {
2942
+ const chip = document.createElement("div");
2943
+ chip.className = "question-chip";
2944
+ chip.dataset.questionId = q.id;
2945
+
2946
+ const icon = document.createElement("span");
2947
+ icon.className = "chip-icon";
2948
+ icon.textContent = "?";
2949
+
2950
+ const text = document.createElement("span");
2951
+ text.className = "chip-text";
2952
+ text.textContent = q.question;
2953
+
2954
+ const dismiss = document.createElement("button");
2955
+ dismiss.className = "chip-dismiss";
2956
+ dismiss.textContent = "\u00d7";
2957
+ dismiss.title = "Dismiss";
2958
+ dismiss.addEventListener("click", (e) => {
2959
+ e.stopPropagation();
2960
+ fetch("/api/dismiss-question", {
2961
+ method: "POST",
2962
+ headers: { "Content-Type": "application/json" },
2963
+ body: JSON.stringify({ questionId: q.id }),
2964
+ });
2965
+ chip.remove();
2966
+ if (chipsEl.children.length === 0) greetingEl.style.display = "none";
2967
+ });
2968
+
2969
+ chip.appendChild(icon);
2970
+ chip.appendChild(text);
2971
+ chip.appendChild(dismiss);
2972
+
2973
+ chip.addEventListener("click", () => {
2974
+ chatInput.value = q.question;
2975
+ greetingEl.style.display = "none";
2976
+ sendMessage();
2977
+ });
2978
+
2979
+ chipsEl.appendChild(chip);
2980
+ }
2981
+
2982
+ greetingEl.style.display = "block";
2983
+ })
2984
+ .catch(() => {
2985
+ greetingEl.style.display = "none";
2986
+ });
2987
+
2988
+ // Hide greeting when user starts typing their own message
2989
+ chatInput.addEventListener("input", function hideGreeting() {
2990
+ if (chatInput.value.trim().length > 0) {
2991
+ greetingEl.style.display = "none";
2992
+ chatInput.removeEventListener("input", hideGreeting);
2993
+ }
2994
+ });
2995
+ }
2996
+
2997
+ // Lightweight markdown → HTML (bold, italic, code, bullets, line breaks)
2998
+ function renderMarkdown(text) {
2999
+ let html = text
3000
+ .replace(/&/g, "&amp;")
3001
+ .replace(/</g, "&lt;")
3002
+ .replace(/>/g, "&gt;");
3003
+ // Code blocks (``` ... ```)
3004
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
3005
+ // Inline code
3006
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
3007
+ // Bold
3008
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
3009
+ // Italic
3010
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
3011
+ // Bullet lists (lines starting with - or *)
3012
+ html = html.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
3013
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
3014
+ // Numbered lists
3015
+ html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
3016
+ // Line breaks (but not inside pre/ul)
3017
+ html = html.replace(/\n/g, '<br>');
3018
+ // Clean up extra <br> inside block elements
3019
+ html = html.replace(/<br><\/(ul|pre|li)>/g, '</$1>');
3020
+ html = html.replace(/<(ul|pre)><br>/g, '<$1>');
3021
+ return html;
3022
+ }
3023
+
3024
+ // Streaming-aware markdown: auto-close incomplete patterns so partial
3025
+ // markdown renders progressively as tokens arrive instead of staying
3026
+ // unformatted until the closing syntax appears.
3027
+ function renderStreamingMarkdown(text) {
3028
+ var patched = text;
3029
+
3030
+ // 1. Auto-close unclosed fenced code blocks
3031
+ var codeBlockTicks = patched.match(/```/g);
3032
+ if (codeBlockTicks && codeBlockTicks.length % 2 !== 0) {
3033
+ patched += "\n```";
3034
+ }
3035
+
3036
+ // Strip code blocks + inline code for further pattern counting
3037
+ var noCodeBlocks = patched.replace(/```[\s\S]*?```/g, "");
3038
+ var noInline = noCodeBlocks.replace(/`[^`]*`/g, "");
3039
+
3040
+ // 2. Auto-close unclosed inline code
3041
+ var backticks = noCodeBlocks.match(/`/g);
3042
+ if (backticks && backticks.length % 2 !== 0) {
3043
+ patched += "`";
3044
+ }
3045
+
3046
+ // 3. Auto-close unclosed bold **
3047
+ var boldMarkers = noInline.match(/\*\*/g);
3048
+ if (boldMarkers && boldMarkers.length % 2 !== 0) {
3049
+ patched += "**";
3050
+ }
3051
+
3052
+ // 4. Auto-close unclosed italic * (after stripping bold pairs)
3053
+ var afterBold = noInline.replace(/\*\*/g, "");
3054
+ var italicMarkers = afterBold.match(/\*/g);
3055
+ if (italicMarkers && italicMarkers.length % 2 !== 0) {
3056
+ patched += "*";
3057
+ }
3058
+
3059
+ return renderMarkdown(patched);
3060
+ }
3061
+
3062
+ function friendlyLLMError(raw) {
3063
+ if (/402|credits|afford/i.test(raw)) return "I'm out of API credits — top up at openrouter.ai/settings/credits and try again.";
3064
+ if (/429|rate.limit/i.test(raw)) return "Rate limited by the LLM provider. Wait a moment and try again.";
3065
+ if (/401|unauthorized|invalid.*key/i.test(raw)) return "API key is invalid or expired. Check Settings.";
3066
+ if (/timeout|timed.out/i.test(raw)) return "LLM request timed out. Try again.";
3067
+ if (/5\d{2}|server.error|internal/i.test(raw)) return "LLM provider is having issues. Try again shortly.";
3068
+ return "LLM error: " + raw;
3069
+ }
3070
+
3071
+ // PII detection — wraps sensitive patterns in redaction spans
3072
+ const PII_PATTERNS = [
3073
+ // Phone numbers: (213) 248-4250, 213-248-4250, 213.248.4250, +1 213 248 4250, 5555555555
3074
+ /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
3075
+ // Email addresses
3076
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
3077
+ // SSN: 123-45-6789 or 123 45 6789
3078
+ /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g,
3079
+ // Credit card-ish: 4 groups of 4 digits
3080
+ /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
3081
+ // IP addresses
3082
+ /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
3083
+ // Common profanity
3084
+ /\b(?:fuck|shit|damn|ass|bitch|hell|crap|dick|cock|pussy|bastard|asshole|bullshit|motherfuck\w*)\b/gi,
3085
+ ];
3086
+
3087
+ function redactPII(text) {
3088
+ // Collect all match ranges
3089
+ const ranges = [];
3090
+ for (const pat of PII_PATTERNS) {
3091
+ pat.lastIndex = 0;
3092
+ let m;
3093
+ while ((m = pat.exec(text)) !== null) {
3094
+ ranges.push([m.index, m.index + m[0].length]);
3095
+ }
3096
+ }
3097
+ if (ranges.length === 0) return null; // nothing to redact
3098
+
3099
+ // Sort and merge overlapping ranges
3100
+ ranges.sort((a, b) => a[0] - b[0]);
3101
+ const merged = [ranges[0]];
3102
+ for (let i = 1; i < ranges.length; i++) {
3103
+ const last = merged[merged.length - 1];
3104
+ if (ranges[i][0] <= last[1]) {
3105
+ last[1] = Math.max(last[1], ranges[i][1]);
3106
+ } else {
3107
+ merged.push(ranges[i]);
3108
+ }
3109
+ }
3110
+
3111
+ // Build HTML with redaction spans
3112
+ let html = "";
3113
+ let cursor = 0;
3114
+ for (const [start, end] of merged) {
3115
+ html += escapeHtml(text.slice(cursor, start));
3116
+ html += '<span class="pii-redact">' + escapeHtml(text.slice(start, end)) + '</span>';
3117
+ cursor = end;
3118
+ }
3119
+ html += escapeHtml(text.slice(cursor));
3120
+ return html;
3121
+ }
3122
+
3123
+ function escapeHtml(s) {
3124
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3125
+ }
3126
+
3127
+ function addMessage(role, content) {
3128
+ const div = document.createElement("div");
3129
+ div.className = "message " + role;
3130
+ if (role === "system") {
3131
+ div.innerHTML = '<div class="content"></div>';
3132
+ } else {
3133
+ div.innerHTML = '<div class="role">' + (role === "user" ? userName || "You" : agentName) + '</div>'
3134
+ + '<div class="content"></div>';
3135
+ }
3136
+ // Render markdown for assistant messages, plain text for user
3137
+ if (role === "assistant") {
3138
+ div.querySelector(".content").innerHTML = renderMarkdown(content);
3139
+ } else {
3140
+ div.querySelector(".content").textContent = content;
3141
+ }
3142
+ messagesEl.appendChild(div);
3143
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3144
+ return div;
3145
+ }
3146
+
3147
+ function createStreamingMessage() {
3148
+ const div = document.createElement("div");
3149
+ div.className = "message assistant";
3150
+ div.innerHTML = '<div class="role">' + agentName + '<span class="model-tag" id="streaming-model-tag"></span></div><div class="content"></div>';
3151
+ messagesEl.appendChild(div);
3152
+ return div.querySelector(".content");
3153
+ }
3154
+
3155
+ function setButtonStop() {
3156
+ sendBtn.textContent = "Stop";
3157
+ sendBtn.classList.add("stop");
3158
+ sendBtn.disabled = false;
3159
+ }
3160
+
3161
+ function setButtonSend() {
3162
+ sendBtn.textContent = "Send";
3163
+ sendBtn.classList.remove("stop");
3164
+ sendBtn.disabled = false;
3165
+ }
3166
+
3167
+ async function sendMessage() {
3168
+ const text = chatInput.value.trim();
3169
+ if (!text && pendingImages.length === 0 && pendingFiles.length === 0) return;
3170
+ if (streaming) return;
3171
+
3172
+ // Stop any currently playing TTS audio
3173
+ stopSpeaking();
3174
+
3175
+ // Build message text with file content prepended
3176
+ let msgText = text || (pendingImages.length > 0 ? "What do you see in this image?" : "");
3177
+ const fileTexts = pendingFiles.map(f => "--- " + f.name + " ---\n" + f.text + "\n--- end " + f.name + " ---");
3178
+ if (fileTexts.length > 0) {
3179
+ msgText = fileTexts.join("\n\n") + "\n\n" + msgText;
3180
+ }
3181
+ const imgPayload = pendingImages.length > 0 ? [...pendingImages] : undefined;
3182
+
3183
+ // Build display text
3184
+ const attachCount = pendingImages.length + pendingFiles.length;
3185
+ const attachLabel = attachCount > 0 ? " [+" + attachCount + " file" + (attachCount > 1 ? "s" : "") + "]" : "";
3186
+
3187
+ chatInput.value = "";
3188
+ pendingImages = [];
3189
+ pendingFiles = [];
3190
+ renderAttachmentPreviews();
3191
+ autoResize();
3192
+ const userMsg = addMessage("user", text + attachLabel);
3193
+ if (incognitoMode) {
3194
+ userMsg.classList.add("incognito");
3195
+ const redacted = redactPII(text + attachLabel);
3196
+ if (redacted) userMsg.querySelector(".content").innerHTML = redacted;
3197
+ }
3198
+
3199
+ streaming = true;
3200
+ abortController = new AbortController();
3201
+ setButtonStop();
3202
+
3203
+ // --- Incognito: route to local Ollama, skip server entirely ---
3204
+ if (incognitoMode) {
3205
+ incognitoHistory.push({ role: "user", content: msgText });
3206
+ const ollamaMessages = [
3207
+ { role: "system", content: "You are " + agentName + ", running in incognito mode. Local only. No memory. No network. Be concise." },
3208
+ ...incognitoHistory.slice(-10)
3209
+ ];
3210
+ // 30s timeout for local model
3211
+ const ollamaTimeout = setTimeout(() => { if (abortController) abortController.abort(); }, 35000);
3212
+ try {
3213
+ const res = await fetch("http://localhost:11434/api/chat", {
3214
+ method: "POST",
3215
+ headers: { "Content-Type": "application/json" },
3216
+ body: JSON.stringify({ model: incognitoModel, messages: ollamaMessages, stream: true }),
3217
+ signal: abortController.signal,
3218
+ });
3219
+ if (!res.ok) {
3220
+ addMessage("system", "Ollama error: " + res.status + " — is Ollama running?");
3221
+ return;
3222
+ }
3223
+ const contentEl = createStreamingMessage();
3224
+ contentEl.closest(".message").classList.add("incognito");
3225
+ contentEl.classList.add("streaming-cursor");
3226
+ const tag = document.getElementById("streaming-model-tag");
3227
+ if (tag) { tag.textContent = "ollama/local"; tag.removeAttribute("id"); }
3228
+ const reader = res.body.getReader();
3229
+ const decoder = new TextDecoder();
3230
+ let buffer = "", fullReply = "";
3231
+ while (true) {
3232
+ const { done, value } = await reader.read();
3233
+ if (done) break;
3234
+ buffer += decoder.decode(value, { stream: true });
3235
+ const lines = buffer.split("\n");
3236
+ buffer = lines.pop() || "";
3237
+ for (const line of lines) {
3238
+ if (!line.trim()) continue;
3239
+ try {
3240
+ const parsed = JSON.parse(line);
3241
+ const token = parsed.message?.content || "";
3242
+ fullReply += token;
3243
+ contentEl.innerHTML = renderStreamingMarkdown(fullReply);
3244
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3245
+ } catch {}
3246
+ }
3247
+ }
3248
+ contentEl.classList.remove("streaming-cursor");
3249
+ const redactedReply = redactPII(fullReply);
3250
+ contentEl.innerHTML = redactedReply || (fullReply ? renderMarkdown(fullReply) : "");
3251
+ incognitoHistory.push({ role: "assistant", content: fullReply });
3252
+ } catch (err) {
3253
+ if (err.name === "AbortError") {
3254
+ addMessage("system", "Local model timed out or was stopped.");
3255
+ } else {
3256
+ addMessage("system", "Ollama connection failed — is it running?");
3257
+ }
3258
+ var cursorEl = messagesEl.querySelector(".streaming-cursor");
3259
+ if (cursorEl) cursorEl.classList.remove("streaming-cursor");
3260
+ } finally {
3261
+ clearTimeout(ollamaTimeout);
3262
+ streaming = false;
3263
+ abortController = null;
3264
+ setButtonSend();
3265
+ chatInput.focus();
3266
+ }
3267
+ return;
3268
+ }
3269
+
3270
+ try {
3271
+ const payload = { sessionId, message: msgText };
3272
+ if (imgPayload) payload.images = imgPayload;
3273
+ if (currentThreadId) payload.threadId = currentThreadId;
3274
+
3275
+ const res = await fetch("/api/chat", {
3276
+ method: "POST",
3277
+ headers: { "Content-Type": "application/json" },
3278
+ body: JSON.stringify(payload),
3279
+ signal: abortController.signal,
3280
+ });
3281
+
3282
+ if (!res.ok) {
3283
+ const data = await res.json();
3284
+ if (data.error && data.error.includes("session")) {
3285
+ showScreen("auth-screen");
3286
+ return;
3287
+ }
3288
+ addMessage("system", "Error: " + (data.error || "Request failed"));
3289
+ return;
3290
+ }
3291
+
3292
+ // System command response (learn) — not streamed
3293
+ const contentType = res.headers.get("content-type") || "";
3294
+ if (contentType.includes("application/json")) {
3295
+ const data = await res.json();
3296
+ if (data.system) {
3297
+ if (data.boardItems && data.boardItems.issues) {
3298
+ renderBoardCards(data.boardItems.issues);
3299
+ } else {
3300
+ addMessage("system", data.content);
3301
+ }
3302
+ return;
3303
+ }
3304
+ }
3305
+
3306
+ // Streamed chat response
3307
+ const contentEl = createStreamingMessage();
3308
+ contentEl.classList.add("streaming-cursor");
3309
+ const reader = res.body.getReader();
3310
+ const decoder = new TextDecoder();
3311
+ let buffer = "";
3312
+ let streamingText = "";
3313
+
3314
+ while (true) {
3315
+ const { done, value } = await reader.read();
3316
+ if (done) break;
3317
+
3318
+ buffer += decoder.decode(value, { stream: true });
3319
+ const lines = buffer.split("\n");
3320
+ buffer = lines.pop() || "";
3321
+
3322
+ for (const line of lines) {
3323
+ const trimmed = line.trim();
3324
+ if (!trimmed.startsWith("data: ")) continue;
3325
+ try {
3326
+ const data = JSON.parse(trimmed.slice(6));
3327
+ if (data.meta) {
3328
+ const tag = document.getElementById("streaming-model-tag");
3329
+ if (tag) {
3330
+ tag.textContent = data.meta.model;
3331
+ tag.removeAttribute("id");
3332
+ }
3333
+ }
3334
+ if (data.token) {
3335
+ streamingText += data.token;
3336
+ contentEl.innerHTML = renderStreamingMarkdown(streamingText);
3337
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3338
+ }
3339
+ if (data.agentSpawned) {
3340
+ // Server confirmed it parsed and spawned an agent — start polling
3341
+ console.log("[agent] Server spawned:", data.agentSpawned.label);
3342
+ startAgentPoll();
3343
+ }
3344
+ if (data.agentError) {
3345
+ console.error("[agent] Spawn failed:", data.agentError);
3346
+ addMessage("system", "Agent spawn failed: " + data.agentError.label + " — " + data.agentError.error);
3347
+ }
3348
+ if (data.boardItems && data.boardItems.issues) {
3349
+ if (data.boardItems.issues.length > 0) {
3350
+ renderBoardCards(data.boardItems.issues);
3351
+ } else if (data.boardItems.empty) {
3352
+ addMessage("system", "No items found for that view.");
3353
+ }
3354
+ }
3355
+ if (data.error) {
3356
+ addMessage("system", friendlyLLMError(data.error));
3357
+ }
3358
+ } catch {}
3359
+ }
3360
+ }
3361
+ // Strip [AGENT_REQUEST]...[/AGENT_REQUEST] and start agent poll
3362
+ let finalText = streamingText;
3363
+ if (finalText.includes("[AGENT_REQUEST]")) {
3364
+ // Count how many blocks were found
3365
+ const blockMatches = finalText.match(/\[AGENT_REQUEST\]/g);
3366
+ console.log("[agent] Detected " + (blockMatches ? blockMatches.length : 0) + " AGENT_REQUEST block(s) in response");
3367
+ finalText = finalText
3368
+ .replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "")
3369
+ .trim();
3370
+ startAgentPoll();
3371
+ } else if (finalText.includes("AGENT_REQUEST")) {
3372
+ // Partial detection — LLM wrote something agent-related but not in proper format
3373
+ console.warn("[agent] Response mentions AGENT_REQUEST but no complete blocks found");
3374
+ }
3375
+
3376
+ // Strip [BOARD_VIEW]...[/BOARD_VIEW] blocks from visible text
3377
+ if (finalText.includes("[BOARD_VIEW]")) {
3378
+ finalText = finalText
3379
+ .replace(/\s*\[BOARD_VIEW\][\s\S]*?\[\/BOARD_VIEW\]\s*/g, "")
3380
+ .replace(/\s*\[BOARD_VIEW\]\s*\{[\s\S]*?\}\s*/g, "")
3381
+ .trim();
3382
+ }
3383
+
3384
+ // Strip [BOARD_ACTION]...[/BOARD_ACTION] blocks from visible text
3385
+ if (finalText.includes("[BOARD_ACTION]")) {
3386
+ finalText = finalText
3387
+ .replace(/\s*\[BOARD_ACTION\][\s\S]*?\[\/BOARD_ACTION\]\s*/g, "")
3388
+ .replace(/\s*\[BOARD_ACTION\]\s*\{[\s\S]*?\}\s*\]?\s*/g, "")
3389
+ .trim();
3390
+ }
3391
+
3392
+ // Remove streaming cursor and re-render final clean markdown
3393
+ if (contentEl) {
3394
+ contentEl.classList.remove("streaming-cursor");
3395
+ contentEl.innerHTML = finalText ? renderMarkdown(finalText) : "";
3396
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3397
+ }
3398
+
3399
+ // Auto-play TTS for assistant response
3400
+ if (contentEl && voiceStatus.tts && ttsAutoPlay) {
3401
+ if (finalText) speakText(finalText);
3402
+ }
3403
+ } catch (err) {
3404
+ if (err.name === "AbortError") {
3405
+ // User clicked Stop — no error message needed
3406
+ } else {
3407
+ addMessage("system", "Connection error: " + err.message);
3408
+ }
3409
+ // Remove streaming cursor on error/abort too
3410
+ var cursorEl = messagesEl.querySelector(".streaming-cursor");
3411
+ if (cursorEl) cursorEl.classList.remove("streaming-cursor");
3412
+ } finally {
3413
+ streaming = false;
3414
+ abortController = null;
3415
+ setButtonSend();
3416
+ chatInput.focus();
3417
+ // Refresh threads to pick up auto-generated titles
3418
+ if (currentThreadId) loadThreads();
3419
+ }
3420
+ }
3421
+
3422
+ // --- Agent status polling ---
3423
+ // Polls /api/agents/tasks and dynamically creates/updates status bars
3424
+ // for all running/pending tasks. Keyed by task ID, not label.
3425
+ var agentPollTimer = null;
3426
+ var agentElements = {}; // taskId → DOM element
3427
+ var agentPollSince = null; // ISO timestamp: only show tasks created after this
3428
+
3429
+ function startAgentPoll() {
3430
+ if (!agentPollSince) {
3431
+ // Record a timestamp slightly in the past to account for clock skew
3432
+ // between client and server, and to catch tasks being created right now
3433
+ var d = new Date();
3434
+ d.setSeconds(d.getSeconds() - 10);
3435
+ agentPollSince = d.toISOString();
3436
+ console.log("[agent] Poll started, looking for tasks since", agentPollSince);
3437
+ }
3438
+ if (!agentPollTimer) {
3439
+ // Delay first poll to give server time to create all tasks
3440
+ setTimeout(pollAgentTasks, 1500);
3441
+ agentPollTimer = setInterval(pollAgentTasks, 3000);
3442
+ }
3443
+ }
3444
+
3445
+ async function pollAgentTasks() {
3446
+ try {
3447
+ var data = await api("/api/agents/tasks");
3448
+ if (!data.tasks || data.tasks.length === 0) {
3449
+ console.log("[agent] Poll: no tasks returned from API");
3450
+ return;
3451
+ }
3452
+
3453
+ // Count how many tasks pass the time filter
3454
+ var recentCount = 0;
3455
+ var hasRunning = false;
3456
+
3457
+ for (var i = 0; i < data.tasks.length; i++) {
3458
+ var task = data.tasks[i];
3459
+
3460
+ // Only track tasks created after we started polling
3461
+ if (agentPollSince && task.createdAt < agentPollSince) continue;
3462
+ recentCount++;
3463
+
3464
+ var el = agentElements[task.id];
3465
+
3466
+ if (task.status === "running" || task.status === "pending") {
3467
+ hasRunning = true;
3468
+
3469
+ // Create indicator if it doesn't exist
3470
+ if (!el) {
3471
+ el = document.createElement("div");
3472
+ el.className = "agent-status";
3473
+ el.innerHTML = '<div class="agent-spinner"></div>'
3474
+ + '<span class="agent-label">Running agent: <strong>' + task.label + '</strong></span>'
3475
+ + '<button class="agent-cancel">Cancel</button>';
3476
+ messagesEl.appendChild(el);
3477
+ agentElements[task.id] = el;
3478
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3479
+ }
3480
+
3481
+ // Wire cancel button
3482
+ (function(taskId, label, elem) {
3483
+ var btn = elem.querySelector(".agent-cancel");
3484
+ if (btn) btn.onclick = async function() {
3485
+ try {
3486
+ await api("/api/agents/tasks/" + taskId + "/cancel", { method: "POST" });
3487
+ elem.className = "agent-status failed";
3488
+ elem.innerHTML = '<span class="agent-label">Agent cancelled: <strong>' + label + '</strong></span>';
3489
+ } catch {}
3490
+ };
3491
+ })(task.id, task.label, el);
3492
+
3493
+ } else if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
3494
+ if (el) {
3495
+ // Resolve existing indicator
3496
+ var isDone = task.status === "completed";
3497
+ el.className = "agent-status " + (isDone ? "done" : "failed");
3498
+ var resultText = isDone ? "completed" : task.status;
3499
+ if (task.resultSummary) {
3500
+ resultText += " — " + task.resultSummary.slice(0, 200);
3501
+ }
3502
+ el.innerHTML = '<span class="agent-label">Agent ' + resultText + ': <strong>'
3503
+ + task.label + '</strong></span>';
3504
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3505
+ } else {
3506
+ // Task completed before we ever saw it running — show result directly
3507
+ console.log("[agent] Task completed before first poll:", task.id, task.label, task.status);
3508
+ el = document.createElement("div");
3509
+ var isDone2 = task.status === "completed";
3510
+ el.className = "agent-status " + (isDone2 ? "done" : "failed");
3511
+ var rt = isDone2 ? "completed" : task.status;
3512
+ if (task.resultSummary) rt += " — " + task.resultSummary.slice(0, 200);
3513
+ el.innerHTML = '<span class="agent-label">Agent ' + rt + ': <strong>' + task.label + '</strong></span>';
3514
+ messagesEl.appendChild(el);
3515
+ agentElements[task.id] = el;
3516
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3517
+ }
3518
+ }
3519
+ }
3520
+
3521
+ if (recentCount === 0) {
3522
+ console.log("[agent] Poll: " + data.tasks.length + " total tasks but none after", agentPollSince);
3523
+ }
3524
+
3525
+ // Stop polling when no tasks are running
3526
+ if (!hasRunning) {
3527
+ clearInterval(agentPollTimer);
3528
+ agentPollTimer = null;
3529
+ agentPollSince = null;
3530
+ agentElements = {};
3531
+ }
3532
+ } catch (e) { console.error("[agent] Poll error:", e); }
3533
+ }
3534
+
3535
+ // --- Board card rendering ---
3536
+ function escapeHtml(str) {
3537
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3538
+ }
3539
+
3540
+ function renderBoardCards(issues) {
3541
+ var container = document.createElement("div");
3542
+ container.className = "board-cards";
3543
+ var priorityLabels = { 1: "Urgent", 2: "High", 3: "Medium", 4: "Low" };
3544
+ for (var i = 0; i < issues.length; i++) {
3545
+ var issue = issues[i];
3546
+ var card = document.createElement("div");
3547
+ card.className = "board-card";
3548
+ card.dataset.issueId = issue.id;
3549
+
3550
+ var html = '<span class="board-id">' + escapeHtml(issue.identifier) + '</span>'
3551
+ + '<span class="board-title">' + escapeHtml(issue.title) + '</span>';
3552
+
3553
+ if (issue.priority >= 1 && issue.priority <= 4) {
3554
+ html += '<span class="board-priority p' + issue.priority + '">' + priorityLabels[issue.priority] + '</span>';
3555
+ }
3556
+
3557
+ html += '<span class="board-state state-' + escapeHtml(issue.stateType || "unstarted") + '">' + escapeHtml(issue.state) + '</span>';
3558
+
3559
+ if (issue.assignee) {
3560
+ html += '<span class="board-assignee">' + escapeHtml(issue.assignee) + '</span>';
3561
+ }
3562
+
3563
+ var doneStates = ["Done", "Cancelled", "done", "cancelled"];
3564
+ if (doneStates.indexOf(issue.state) === -1) {
3565
+ html += '<span class="board-actions"><button data-issue-id="' + escapeHtml(issue.id) + '">Done</button></span>';
3566
+ }
3567
+
3568
+ card.innerHTML = html;
3569
+ container.appendChild(card);
3570
+ }
3571
+
3572
+ // Wire up Done buttons
3573
+ var buttons = container.querySelectorAll(".board-actions button");
3574
+ for (var b = 0; b < buttons.length; b++) {
3575
+ (function(btn) {
3576
+ btn.addEventListener("click", async function() {
3577
+ var issueId = btn.dataset.issueId;
3578
+ btn.disabled = true;
3579
+ btn.textContent = "...";
3580
+ try {
3581
+ var resp = await api("/api/board/issues/" + issueId + "?sessionId=" + encodeURIComponent(sessionKey), {
3582
+ method: "PATCH",
3583
+ headers: { "Content-Type": "application/json" },
3584
+ body: JSON.stringify({ stateId: "done" }),
3585
+ });
3586
+ if (resp.ok) {
3587
+ var parentCard = btn.closest(".board-card");
3588
+ if (parentCard) parentCard.classList.add("card-done");
3589
+ var stateEl = parentCard ? parentCard.querySelector(".board-state") : null;
3590
+ if (stateEl) {
3591
+ stateEl.className = "board-state state-completed";
3592
+ stateEl.textContent = "Done";
3593
+ }
3594
+ btn.textContent = "Done";
3595
+ } else {
3596
+ btn.textContent = "Fail";
3597
+ btn.disabled = false;
3598
+ }
3599
+ } catch {
3600
+ btn.textContent = "Fail";
3601
+ btn.disabled = false;
3602
+ }
3603
+ });
3604
+ })(buttons[b]);
3605
+ }
3606
+
3607
+ messagesEl.appendChild(container);
3608
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3609
+ }
3610
+
3611
+ sendBtn.addEventListener("click", () => {
3612
+ if (streaming && abortController) {
3613
+ abortController.abort();
3614
+ } else {
3615
+ sendMessage();
3616
+ }
3617
+ });
3618
+
3619
+ chatInput.addEventListener("keydown", (e) => {
3620
+ if (e.key === "Enter" && !e.shiftKey) {
3621
+ e.preventDefault();
3622
+ if (streaming && abortController) {
3623
+ abortController.abort();
3624
+ } else {
3625
+ sendMessage();
3626
+ }
3627
+ }
3628
+ });
3629
+
3630
+ // Auto-resize textarea
3631
+ function autoResize() {
3632
+ chatInput.style.height = "auto";
3633
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 160) + "px";
3634
+ }
3635
+
3636
+ chatInput.addEventListener("input", autoResize);
3637
+
3638
+ // --- Vault ---
3639
+
3640
+ const vaultOverlay = document.getElementById("vault-overlay");
3641
+ const vaultPanel = document.getElementById("vault-panel");
3642
+ const vaultBody = document.getElementById("vault-body");
3643
+ const vaultList = document.getElementById("vault-list");
3644
+ const vaultEmpty = document.getElementById("vault-empty");
3645
+ const vaultError = document.getElementById("vault-error");
3646
+
3647
+ function openSettings() {
3648
+ vaultOverlay.classList.add("open");
3649
+ loadPersonality();
3650
+ loadSecuritySettings();
3651
+ loadMeshSettings();
3652
+ loadLlmSettings();
3653
+ loadVaultKeys();
3654
+ }
3655
+
3656
+ function closeVault() {
3657
+ vaultOverlay.classList.remove("open");
3658
+ clearVaultError();
3659
+ }
3660
+
3661
+ function showVaultError(msg) {
3662
+ vaultError.textContent = msg;
3663
+ vaultError.classList.add("visible");
3664
+ }
3665
+
3666
+ function clearVaultError() {
3667
+ vaultError.textContent = "";
3668
+ vaultError.classList.remove("visible");
3669
+ }
3670
+
3671
+ // --- Personality (trait sliders + custom rules) ---
3672
+
3673
+ const personalityTraits = document.getElementById("personality-traits");
3674
+ const customRulesTextarea = document.getElementById("custom-rules-textarea");
3675
+ const promptStatus = document.getElementById("prompt-status");
3676
+
3677
+ // Dot click handler — activate clicked dot, deactivate siblings
3678
+ personalityTraits.addEventListener("click", (e) => {
3679
+ const dot = e.target.closest(".trait-dot");
3680
+ if (!dot) return;
3681
+ const slider = dot.closest(".trait-slider");
3682
+ slider.querySelectorAll(".trait-dot").forEach(d => d.classList.remove("active"));
3683
+ dot.classList.add("active");
3684
+ });
3685
+
3686
+ async function loadPersonality() {
3687
+ try {
3688
+ const data = await api("/api/personality?sessionId=" + encodeURIComponent(sessionId));
3689
+ // Set trait dots
3690
+ if (data.traits) {
3691
+ for (const [trait, value] of Object.entries(data.traits)) {
3692
+ const row = personalityTraits.querySelector(`[data-trait="${trait}"]`);
3693
+ if (!row) continue;
3694
+ row.querySelectorAll(".trait-dot").forEach(d => {
3695
+ d.classList.toggle("active", d.dataset.value === String(value));
3696
+ });
3697
+ }
3698
+ }
3699
+ // Set custom rules
3700
+ customRulesTextarea.value = (data.customRules || []).join("\n");
3701
+ } catch {
3702
+ // defaults already set in HTML
3703
+ customRulesTextarea.value = "";
3704
+ }
3705
+ }
3706
+
3707
+ async function savePersonality() {
3708
+ const btn = document.getElementById("prompt-save-btn");
3709
+ btn.disabled = true;
3710
+ promptStatus.textContent = "";
3711
+ promptStatus.className = "prompt-status";
3712
+
3713
+ // Collect trait values
3714
+ const traits = {};
3715
+ personalityTraits.querySelectorAll(".trait-row").forEach(row => {
3716
+ const trait = row.dataset.trait;
3717
+ const active = row.querySelector(".trait-dot.active");
3718
+ traits[trait] = active ? parseInt(active.dataset.value) : 2;
3719
+ });
3720
+
3721
+ // Collect custom rules (non-empty lines)
3722
+ const customRules = customRulesTextarea.value
3723
+ .split("\n")
3724
+ .map(l => l.trim())
3725
+ .filter(Boolean);
3726
+
3727
+ try {
3728
+ await api("/api/personality", {
3729
+ method: "PUT",
3730
+ body: { sessionId, traits, customRules },
3731
+ });
3732
+ promptStatus.textContent = "Saved";
3733
+ promptStatus.classList.add("visible");
3734
+ setTimeout(() => promptStatus.classList.remove("visible"), 2000);
3735
+ } catch (err) {
3736
+ promptStatus.textContent = err.message;
3737
+ promptStatus.className = "prompt-status visible error";
3738
+ } finally {
3739
+ btn.disabled = false;
3740
+ }
3741
+ }
3742
+
3743
+ document.getElementById("prompt-save-btn").addEventListener("click", savePersonality);
3744
+
3745
+ // --- LLM Settings ---
3746
+
3747
+ const airplaneToggle = document.getElementById("airplane-toggle");
3748
+ const modelChatInput = document.getElementById("model-chat");
3749
+ const modelUtilityInput = document.getElementById("model-utility");
3750
+ const llmSaveStatus = document.getElementById("llm-save-status");
3751
+
3752
+ async function loadLlmSettings() {
3753
+ try {
3754
+ const data = await api("/api/settings");
3755
+ airplaneToggle.checked = data.airplaneMode;
3756
+ modelChatInput.value = data.models.chat === "auto" ? "" : data.models.chat;
3757
+ modelUtilityInput.value = data.models.utility === "auto" ? "" : data.models.utility;
3758
+ } catch {
3759
+ // defaults are fine
3760
+ }
3761
+ }
3762
+
3763
+ async function saveLlmSettings() {
3764
+ const btn = document.getElementById("llm-save-btn");
3765
+ btn.disabled = true;
3766
+ llmSaveStatus.classList.remove("visible");
3767
+
3768
+ try {
3769
+ await api("/api/settings", {
3770
+ method: "PUT",
3771
+ body: {
3772
+ airplaneMode: airplaneToggle.checked,
3773
+ models: {
3774
+ chat: modelChatInput.value.trim() || "auto",
3775
+ utility: modelUtilityInput.value.trim() || "auto",
3776
+ },
3777
+ },
3778
+ });
3779
+ llmSaveStatus.classList.add("visible");
3780
+ setTimeout(() => llmSaveStatus.classList.remove("visible"), 2000);
3781
+ } catch (err) {
3782
+ llmSaveStatus.textContent = err.message;
3783
+ llmSaveStatus.classList.add("visible");
3784
+ setTimeout(() => {
3785
+ llmSaveStatus.textContent = "Saved";
3786
+ llmSaveStatus.classList.remove("visible");
3787
+ }, 3000);
3788
+ } finally {
3789
+ btn.disabled = false;
3790
+ }
3791
+ }
3792
+
3793
+ document.getElementById("llm-save-btn").addEventListener("click", saveLlmSettings);
3794
+
3795
+ // --- Security settings ---
3796
+ const safewordModeSelect = document.getElementById("safeword-mode-select");
3797
+ const safewordModeLabel = document.getElementById("safeword-mode-label");
3798
+ const securitySaveStatus = document.getElementById("security-save-status");
3799
+
3800
+ function updateSafewordLabel() {
3801
+ safewordModeLabel.textContent = safewordModeSelect.value === "always"
3802
+ ? "Require safe word on every page load"
3803
+ : "Skip safe word until server or browser restarts";
3804
+ }
3805
+ safewordModeSelect.addEventListener("change", updateSafewordLabel);
3806
+
3807
+ async function loadSecuritySettings() {
3808
+ try {
3809
+ const data = await api("/api/settings");
3810
+ safewordModeSelect.value = data.safeWordMode || "always";
3811
+ updateSafewordLabel();
3812
+ } catch {}
3813
+ }
3814
+
3815
+ async function saveSecuritySettings() {
3816
+ const btn = document.getElementById("security-save-btn");
3817
+ btn.disabled = true;
3818
+ securitySaveStatus.classList.remove("visible");
3819
+ try {
3820
+ await api("/api/settings", {
3821
+ method: "PUT",
3822
+ body: { safeWordMode: safewordModeSelect.value },
3823
+ });
3824
+ // If switching to "always", clear stored session so next load requires safe word
3825
+ if (safewordModeSelect.value === "always") {
3826
+ sessionStorage.removeItem("dash_sid");
3827
+ }
3828
+ securitySaveStatus.classList.add("visible");
3829
+ setTimeout(() => securitySaveStatus.classList.remove("visible"), 2000);
3830
+ } catch (err) {
3831
+ securitySaveStatus.textContent = err.message;
3832
+ securitySaveStatus.classList.add("visible");
3833
+ setTimeout(() => {
3834
+ securitySaveStatus.textContent = "Saved";
3835
+ securitySaveStatus.classList.remove("visible");
3836
+ }, 3000);
3837
+ } finally {
3838
+ btn.disabled = false;
3839
+ }
3840
+ }
3841
+
3842
+ document.getElementById("security-save-btn").addEventListener("click", saveSecuritySettings);
3843
+
3844
+ // --- Mesh / Network settings ---
3845
+ const meshLanToggle = document.getElementById("mesh-lan-toggle");
3846
+ const meshIncomingToggle = document.getElementById("mesh-incoming-toggle");
3847
+ const meshSaveStatus = document.getElementById("mesh-save-status");
3848
+
3849
+ async function loadMeshSettings() {
3850
+ try {
3851
+ const data = await api("/api/settings");
3852
+ meshLanToggle.checked = data.mesh?.lanAnnounce ?? false;
3853
+ meshIncomingToggle.checked = data.mesh?.allowIncoming ?? false;
3854
+ } catch {}
3855
+ }
3856
+
3857
+ async function saveMeshSettings() {
3858
+ const btn = document.getElementById("mesh-save-btn");
3859
+ btn.disabled = true;
3860
+ meshSaveStatus.classList.remove("visible");
3861
+ try {
3862
+ await api("/api/settings", {
3863
+ method: "PUT",
3864
+ body: { mesh: { lanAnnounce: meshLanToggle.checked, allowIncoming: meshIncomingToggle.checked } },
3865
+ });
3866
+ meshSaveStatus.classList.add("visible");
3867
+ setTimeout(() => meshSaveStatus.classList.remove("visible"), 3000);
3868
+ } catch (err) {
3869
+ meshSaveStatus.textContent = "Error";
3870
+ meshSaveStatus.classList.add("visible");
3871
+ setTimeout(() => { meshSaveStatus.textContent = "Saved"; meshSaveStatus.classList.remove("visible"); }, 3000);
3872
+ } finally {
3873
+ btn.disabled = false;
3874
+ }
3875
+ }
3876
+
3877
+ document.getElementById("mesh-save-btn").addEventListener("click", saveMeshSettings);
3878
+
3879
+ async function loadVaultKeys() {
3880
+ try {
3881
+ const data = await api("/api/vault?sessionId=" + encodeURIComponent(sessionId));
3882
+ renderVaultKeys(data.keys || []);
3883
+ } catch (err) {
3884
+ vaultList.innerHTML = "";
3885
+ vaultEmpty.style.display = "block";
3886
+ }
3887
+ }
3888
+
3889
+ function renderVaultKeys(keys) {
3890
+ vaultList.innerHTML = "";
3891
+ if (keys.length === 0) {
3892
+ vaultEmpty.style.display = "block";
3893
+ return;
3894
+ }
3895
+ vaultEmpty.style.display = "none";
3896
+ for (const k of keys) {
3897
+ const item = document.createElement("div");
3898
+ item.className = "vault-key-item";
3899
+ item.innerHTML = '<div class="vault-key-info">'
3900
+ + '<div class="vault-key-name"></div>'
3901
+ + (k.label ? '<div class="vault-key-label"></div>' : '')
3902
+ + '</div>'
3903
+ + '<button class="vault-key-delete" title="Delete">&times;</button>';
3904
+ item.querySelector(".vault-key-name").textContent = k.name;
3905
+ if (k.label) item.querySelector(".vault-key-label").textContent = k.label;
3906
+ item.querySelector(".vault-key-delete").addEventListener("click", () => deleteVaultKeyUI(k.name));
3907
+ vaultList.appendChild(item);
3908
+ }
3909
+ }
3910
+
3911
+ async function addVaultKey() {
3912
+ clearVaultError();
3913
+ const name = document.getElementById("vault-key-name").value.trim();
3914
+ const value = document.getElementById("vault-key-value").value;
3915
+ const label = document.getElementById("vault-key-label").value.trim();
3916
+
3917
+ if (!name) { showVaultError("Key name is required"); return; }
3918
+ if (!value) { showVaultError("Key value is required"); return; }
3919
+
3920
+ const btn = document.getElementById("vault-add-btn");
3921
+ btn.disabled = true;
3922
+
3923
+ try {
3924
+ await api("/api/vault/" + encodeURIComponent(name) + "?sessionId=" + encodeURIComponent(sessionId), {
3925
+ method: "PUT",
3926
+ body: { value, label: label || undefined },
3927
+ });
3928
+ document.getElementById("vault-key-name").value = "";
3929
+ document.getElementById("vault-key-value").value = "";
3930
+ document.getElementById("vault-key-label").value = "";
3931
+ await loadVaultKeys();
3932
+ } catch (err) {
3933
+ showVaultError(err.message);
3934
+ } finally {
3935
+ btn.disabled = false;
3936
+ }
3937
+ }
3938
+
3939
+ async function deleteVaultKeyUI(name) {
3940
+ if (!confirm("Delete " + name + " from vault?")) return;
3941
+ try {
3942
+ await api("/api/vault/" + encodeURIComponent(name) + "?sessionId=" + encodeURIComponent(sessionId), {
3943
+ method: "DELETE",
3944
+ });
3945
+ await loadVaultKeys();
3946
+ } catch (err) {
3947
+ showVaultError(err.message);
3948
+ }
3949
+ }
3950
+
3951
+ document.getElementById("vault-open-btn").addEventListener("click", openSettings);
3952
+ document.getElementById("vault-close-btn").addEventListener("click", closeVault);
3953
+
3954
+ // Close on overlay click (outside panel)
3955
+ vaultOverlay.addEventListener("click", (e) => {
3956
+ if (e.target === vaultOverlay) closeVault();
3957
+ });
3958
+
3959
+ // Close on Escape
3960
+ document.addEventListener("keydown", (e) => {
3961
+ if (e.key === "Escape" && vaultOverlay.classList.contains("open")) closeVault();
3962
+ });
3963
+
3964
+ document.getElementById("vault-add-btn").addEventListener("click", addVaultKey);
3965
+
3966
+ // Enter to add
3967
+ document.getElementById("vault-key-label").addEventListener("keydown", (e) => {
3968
+ if (e.key === "Enter") addVaultKey();
3969
+ });
3970
+ document.getElementById("vault-key-value").addEventListener("keydown", (e) => {
3971
+ if (e.key === "Enter") addVaultKey();
3972
+ });
3973
+
3974
+ // --- Activity log ---
3975
+
3976
+ let activeTab = "chat";
3977
+ let lastActivityId = 0;
3978
+ let unseenCount = 0;
3979
+ const activityBuffer = [];
3980
+ let activityPollTimer = null;
3981
+ let streamFrozen = false;
3982
+ const selectedEntryIds = new Set();
3983
+ let branchAbortController = null;
3984
+ let activeBranchEntryIds = [];
3985
+
3986
+ const activityView = document.getElementById("activity-view");
3987
+ const activityBadge = document.getElementById("activity-badge");
3988
+ const activityEmpty = document.getElementById("activity-empty");
3989
+ const activityEntries = document.getElementById("activity-entries");
3990
+ const freezeBtn = document.getElementById("stream-freeze-btn");
3991
+ const selectionCount = document.getElementById("stream-selection-count");
3992
+ const branchBtn = document.getElementById("stream-branch-btn");
3993
+ const branchPanel = document.getElementById("branch-panel");
3994
+ const branchHeaderText = document.getElementById("branch-header-text");
3995
+ const branchMessages = document.getElementById("branch-messages");
3996
+ const branchInput = document.getElementById("branch-input");
3997
+ const branchSendBtn = document.getElementById("branch-send-btn");
3998
+ const branchCloseBtn = document.getElementById("branch-close-btn");
3999
+
4000
+ window.__switchTab = function(tab) {
4001
+ if (tab === activeTab) return;
4002
+ activeTab = tab;
4003
+
4004
+ document.getElementById("tab-chat").classList.toggle("active", tab === "chat");
4005
+ document.getElementById("tab-activity").classList.toggle("active", tab === "activity");
4006
+
4007
+ if (tab === "chat") {
4008
+ messagesEl.style.display = "";
4009
+ composerEl.style.display = "";
4010
+ activityView.classList.remove("active");
4011
+ closeBranch();
4012
+ chatInput.focus();
4013
+ } else {
4014
+ messagesEl.style.display = "none";
4015
+ composerEl.style.display = "none";
4016
+ activityView.classList.add("active");
4017
+ // Flush buffer
4018
+ if (activityBuffer.length > 0) {
4019
+ for (const entry of activityBuffer) appendActivityEntry(entry);
4020
+ activityBuffer.length = 0;
4021
+ }
4022
+ unseenCount = 0;
4023
+ updateBadge();
4024
+ }
4025
+ };
4026
+
4027
+ function updateBadge() {
4028
+ if (unseenCount > 0) {
4029
+ activityBadge.textContent = unseenCount > 99 ? "99+" : String(unseenCount);
4030
+ activityBadge.classList.add("visible");
4031
+ } else {
4032
+ activityBadge.classList.remove("visible");
4033
+ }
4034
+ }
4035
+
4036
+ function appendActivityEntry(entry) {
4037
+ activityEmpty.style.display = "none";
4038
+ const row = document.createElement("div");
4039
+ row.className = "activity-entry";
4040
+ if (entry.backref) row.classList.add("has-backref");
4041
+ row.setAttribute("data-id", String(entry.id));
4042
+ row.setAttribute("data-trace", entry.traceId || "");
4043
+ if (entry.backref) row.setAttribute("data-backref", entry.backref);
4044
+ const time = new Date(entry.timestamp);
4045
+ const hh = String(time.getHours()).padStart(2, "0");
4046
+ const mm = String(time.getMinutes()).padStart(2, "0");
4047
+ let html = '<span class="activity-time">' + hh + ':' + mm + '</span>'
4048
+ + '<span class="activity-source ' + entry.source + '">' + entry.source + '</span>'
4049
+ + '<span class="activity-body"><span class="activity-summary"></span>'
4050
+ + (entry.backref ? '<span class="activity-lineage" title="Branched from ' + entry.backref + '">&#8627;</span>' : '')
4051
+ + '</span>';
4052
+ row.innerHTML = html;
4053
+ row.querySelector(".activity-summary").textContent = entry.summary;
4054
+ if (entry.detail) {
4055
+ const detailEl = document.createElement("div");
4056
+ detailEl.className = "activity-detail";
4057
+ detailEl.textContent = entry.detail;
4058
+ row.querySelector(".activity-body").appendChild(detailEl);
4059
+ }
4060
+ row.addEventListener("click", () => {
4061
+ const id = parseInt(row.getAttribute("data-id"), 10);
4062
+ if (row.classList.contains("selected")) {
4063
+ row.classList.remove("selected");
4064
+ selectedEntryIds.delete(id);
4065
+ } else {
4066
+ row.classList.add("selected");
4067
+ selectedEntryIds.add(id);
4068
+ }
4069
+ updateSelectionUI();
4070
+ });
4071
+ activityEntries.appendChild(row);
4072
+ if (!streamFrozen) {
4073
+ activityEntries.scrollTop = activityEntries.scrollHeight;
4074
+ }
4075
+ }
4076
+
4077
+ async function pollActivity() {
4078
+ if (!sessionId) return;
4079
+ try {
4080
+ const data = await api("/api/activity?sessionId=" + encodeURIComponent(sessionId) + "&since=" + lastActivityId);
4081
+ const items = data.activities || [];
4082
+ if (items.length === 0) return;
4083
+ lastActivityId = items[items.length - 1].id;
4084
+ if (activeTab === "activity") {
4085
+ for (const entry of items) appendActivityEntry(entry);
4086
+ } else {
4087
+ activityBuffer.push(...items);
4088
+ unseenCount += items.length;
4089
+ updateBadge();
4090
+ }
4091
+ } catch (err) {
4092
+ // Silently ignore poll errors
4093
+ }
4094
+ }
4095
+
4096
+ function startActivityPolling() {
4097
+ if (activityPollTimer) return;
4098
+ activityPollTimer = setInterval(pollActivity, 5000);
4099
+ pollActivity(); // immediate first poll
4100
+ }
4101
+
4102
+ // --- Stream controls ---
4103
+
4104
+ freezeBtn.addEventListener("click", () => {
4105
+ streamFrozen = !streamFrozen;
4106
+ freezeBtn.classList.toggle("frozen", streamFrozen);
4107
+ freezeBtn.innerHTML = streamFrozen ? "&#9724; Frozen" : "&#9654; Live";
4108
+ if (!streamFrozen) {
4109
+ activityEntries.scrollTop = activityEntries.scrollHeight;
4110
+ }
4111
+ });
4112
+
4113
+ function updateSelectionUI() {
4114
+ const count = selectedEntryIds.size;
4115
+ if (count > 0) {
4116
+ selectionCount.textContent = count + " selected";
4117
+ selectionCount.classList.add("visible");
4118
+ branchBtn.classList.add("visible");
4119
+ } else {
4120
+ selectionCount.classList.remove("visible");
4121
+ branchBtn.classList.remove("visible");
4122
+ }
4123
+ }
4124
+
4125
+ branchBtn.addEventListener("click", openBranch);
4126
+ branchCloseBtn.addEventListener("click", closeBranch);
4127
+ branchSendBtn.addEventListener("click", sendBranch);
4128
+ branchInput.addEventListener("keydown", (e) => {
4129
+ if (e.key === "Enter" && !e.shiftKey) {
4130
+ e.preventDefault();
4131
+ sendBranch();
4132
+ }
4133
+ });
4134
+
4135
+ function openBranch() {
4136
+ activeBranchEntryIds = [...selectedEntryIds];
4137
+ // Build header from selected DOM rows
4138
+ const summaries = [];
4139
+ activityEntries.querySelectorAll(".activity-entry.selected").forEach((row) => {
4140
+ const s = row.querySelector(".activity-summary");
4141
+ if (s) summaries.push(s.textContent);
4142
+ });
4143
+ branchHeaderText.textContent = summaries.length <= 3
4144
+ ? summaries.join(" / ")
4145
+ : summaries.slice(0, 2).join(" / ") + " + " + (summaries.length - 2) + " more";
4146
+ branchMessages.innerHTML = "";
4147
+ branchPanel.classList.add("open");
4148
+ branchInput.focus();
4149
+ }
4150
+
4151
+ function closeBranch() {
4152
+ branchPanel.classList.remove("open");
4153
+ if (branchAbortController) {
4154
+ branchAbortController.abort();
4155
+ branchAbortController = null;
4156
+ }
4157
+ // Clear selection
4158
+ selectedEntryIds.clear();
4159
+ activeBranchEntryIds = [];
4160
+ activityEntries.querySelectorAll(".activity-entry.selected").forEach((row) => {
4161
+ row.classList.remove("selected");
4162
+ });
4163
+ updateSelectionUI();
4164
+ }
4165
+
4166
+ async function sendBranch() {
4167
+ const question = branchInput.value.trim();
4168
+ if (!question || !activeBranchEntryIds.length) return;
4169
+ branchInput.value = "";
4170
+
4171
+ // Append user message
4172
+ const userDiv = document.createElement("div");
4173
+ userDiv.className = "branch-message user";
4174
+ userDiv.innerHTML = '<div class="branch-role">' + (userName || "You") + '</div><div class="branch-content"></div>';
4175
+ userDiv.querySelector(".branch-content").textContent = question;
4176
+ branchMessages.appendChild(userDiv);
4177
+ branchMessages.scrollTop = branchMessages.scrollHeight;
4178
+
4179
+ // Append assistant placeholder
4180
+ const asstDiv = document.createElement("div");
4181
+ asstDiv.className = "branch-message assistant";
4182
+ asstDiv.innerHTML = '<div class="branch-role">' + agentName + '</div><div class="branch-content"></div>';
4183
+ branchMessages.appendChild(asstDiv);
4184
+ const contentEl = asstDiv.querySelector(".branch-content");
4185
+
4186
+ branchAbortController = new AbortController();
4187
+
4188
+ try {
4189
+ const res = await fetch("/api/branch", {
4190
+ method: "POST",
4191
+ headers: { "Content-Type": "application/json" },
4192
+ body: JSON.stringify({
4193
+ sessionId,
4194
+ entryIds: activeBranchEntryIds,
4195
+ question,
4196
+ }),
4197
+ signal: branchAbortController.signal,
4198
+ });
4199
+
4200
+ if (!res.ok) {
4201
+ const err = await res.json().catch(() => ({}));
4202
+ contentEl.textContent = "Error: " + (err.error || res.statusText);
4203
+ return;
4204
+ }
4205
+
4206
+ const reader = res.body.getReader();
4207
+ const decoder = new TextDecoder();
4208
+ let buffer = "";
4209
+ let fullText = "";
4210
+
4211
+ while (true) {
4212
+ const { done, value } = await reader.read();
4213
+ if (done) break;
4214
+
4215
+ buffer += decoder.decode(value, { stream: true });
4216
+ const lines = buffer.split("\n");
4217
+ buffer = lines.pop() || "";
4218
+
4219
+ for (const line of lines) {
4220
+ const trimmed = line.trim();
4221
+ if (!trimmed.startsWith("data: ")) continue;
4222
+ try {
4223
+ const data = JSON.parse(trimmed.slice(6));
4224
+ if (data.token) {
4225
+ fullText += data.token;
4226
+ contentEl.textContent = fullText;
4227
+ branchMessages.scrollTop = branchMessages.scrollHeight;
4228
+ }
4229
+ if (data.error) {
4230
+ contentEl.textContent += "\n" + friendlyLLMError(data.error);
4231
+ }
4232
+ } catch {}
4233
+ }
4234
+ }
4235
+
4236
+ // Render markdown on completion
4237
+ if (fullText) {
4238
+ contentEl.innerHTML = renderMarkdown(fullText);
4239
+ branchMessages.scrollTop = branchMessages.scrollHeight;
4240
+ }
4241
+ } catch (err) {
4242
+ if (err.name !== "AbortError") {
4243
+ contentEl.textContent = "Connection error: " + err.message;
4244
+ }
4245
+ } finally {
4246
+ branchAbortController = null;
4247
+ }
4248
+ }
4249
+
4250
+ // --- Voice ---
4251
+
4252
+ const micBtn = document.getElementById("mic-btn");
4253
+
4254
+ async function initVoice() {
4255
+ try {
4256
+ const data = await api("/api/voice-status");
4257
+ voiceStatus = { tts: !!data.tts, stt: !!data.stt };
4258
+ } catch {
4259
+ voiceStatus = { tts: false, stt: false };
4260
+ }
4261
+
4262
+ // Load settings for autoPlay and toggle states
4263
+ try {
4264
+ const settings = await api("/api/settings");
4265
+ ttsAutoPlay = settings.tts?.autoPlay !== false;
4266
+ document.getElementById("tts-toggle").checked = settings.tts?.enabled !== false;
4267
+ document.getElementById("stt-toggle").checked = settings.stt?.enabled !== false;
4268
+ document.getElementById("autoplay-toggle").checked = ttsAutoPlay;
4269
+ document.getElementById("tts-status-label").textContent = voiceStatus.tts ? "Piper running" : "Not available";
4270
+ document.getElementById("stt-status-label").textContent = voiceStatus.stt ? "Whisper running" : "Not available";
4271
+ } catch {}
4272
+
4273
+ // Check board provider status
4274
+ try {
4275
+ const boardStatus = await api("/api/board/status");
4276
+ const providerLabel = document.getElementById("board-provider-label");
4277
+ const statusLabel = document.getElementById("board-status-label");
4278
+ const statusDot = document.getElementById("board-status-dot");
4279
+ const hint = document.getElementById("board-hint");
4280
+ if (boardStatus.available) {
4281
+ providerLabel.textContent = boardStatus.provider || "Task Board";
4282
+ statusLabel.textContent = boardStatus.user ? `Connected as ${boardStatus.user.name}` : "Connected";
4283
+ statusDot.style.background = "#22c55e";
4284
+ hint.textContent = 'Use "issues" or "todo <title>" in chat to manage your board.';
4285
+ } else {
4286
+ providerLabel.textContent = "Not connected";
4287
+ statusLabel.textContent = "No provider configured";
4288
+ statusDot.style.background = "var(--text-dim)";
4289
+ }
4290
+ } catch {
4291
+ document.getElementById("board-provider-label").textContent = "Not connected";
4292
+ document.getElementById("board-status-label").textContent = "Could not check status";
4293
+ }
4294
+
4295
+ // Check Google Workspace status
4296
+ try {
4297
+ const googleStatus = await api("/api/google/status");
4298
+ const gLabel = document.getElementById("google-status-label");
4299
+ const gScopes = document.getElementById("google-scopes-label");
4300
+ const gDot = document.getElementById("google-status-dot");
4301
+ const gHint = document.getElementById("google-hint");
4302
+ const gBtn = document.getElementById("google-connect-btn");
4303
+ if (googleStatus.authenticated) {
4304
+ gLabel.textContent = "Connected";
4305
+ gScopes.textContent = "Calendar, Gmail, Docs, Sheets, Tasks";
4306
+ gDot.style.background = "#22c55e";
4307
+ gHint.textContent = 'Ask about your "schedule", "inbox", or "email from [name]" in chat.';
4308
+ gBtn.textContent = "Reconnect Google";
4309
+ } else if (googleStatus.configured) {
4310
+ gLabel.textContent = "Configured";
4311
+ gScopes.textContent = "Not yet authorized";
4312
+ gDot.style.background = "#eab308";
4313
+ gHint.textContent = "Client ID and secret are set. Click Connect to authorize with Google.";
4314
+ } else {
4315
+ gLabel.textContent = "Not configured";
4316
+ gScopes.textContent = "";
4317
+ gDot.style.background = "var(--text-dim)";
4318
+ gBtn.style.display = "none";
4319
+ }
4320
+ } catch {
4321
+ document.getElementById("google-status-label").textContent = "Not connected";
4322
+ }
4323
+
4324
+ // Google connect button
4325
+ document.getElementById("google-connect-btn").addEventListener("click", function() {
4326
+ window.open("/api/google/auth", "_blank");
4327
+ });
4328
+
4329
+ // Listen for Google OAuth callback to refresh status
4330
+ window.addEventListener("message", async function(e) {
4331
+ if (e.data === "google-connected") {
4332
+ try {
4333
+ const gs = await api("/api/google/status");
4334
+ const gLabel = document.getElementById("google-status-label");
4335
+ const gScopes = document.getElementById("google-scopes-label");
4336
+ const gDot = document.getElementById("google-status-dot");
4337
+ const gHint = document.getElementById("google-hint");
4338
+ const gBtn = document.getElementById("google-connect-btn");
4339
+ if (gs.authenticated) {
4340
+ gLabel.textContent = "Connected";
4341
+ gScopes.textContent = "Calendar, Gmail, Docs, Sheets, Tasks";
4342
+ gDot.style.background = "#22c55e";
4343
+ gHint.textContent = 'Ask about your "schedule", "inbox", or "email from [name]" in chat.';
4344
+ gBtn.textContent = "Reconnect Google";
4345
+ }
4346
+ } catch {}
4347
+ }
4348
+ });
4349
+
4350
+ }
4351
+
4352
+ // TTS: speak a text string — fetches WAV from server, plays in browser
4353
+ async function speakText(text) {
4354
+ if (!voiceStatus.tts || !text) return;
4355
+ const ttsText = text.slice(0, 2000);
4356
+ try {
4357
+ const res = await fetch("/api/tts?text=" + encodeURIComponent(ttsText));
4358
+ if (!res.ok) return;
4359
+ const arrayBuf = await res.arrayBuffer();
4360
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
4361
+ const audioBuffer = await audioCtx.decodeAudioData(arrayBuf);
4362
+ const source = audioCtx.createBufferSource();
4363
+ source.buffer = audioBuffer;
4364
+ source.connect(audioCtx.destination);
4365
+ source.onended = () => { currentAudio = null; };
4366
+ source.start(0);
4367
+ currentAudio = { source, ctx: audioCtx };
4368
+ } catch (err) {
4369
+ console.log("TTS playback error:", err);
4370
+ }
4371
+ }
4372
+
4373
+ // Stop any currently playing TTS audio
4374
+ function stopSpeaking() {
4375
+ if (currentAudio) {
4376
+ try {
4377
+ if (currentAudio.source) currentAudio.source.stop();
4378
+ if (currentAudio.ctx) currentAudio.ctx.close();
4379
+ } catch {}
4380
+ currentAudio = null;
4381
+ }
4382
+ }
4383
+
4384
+ // --- Voice: always-listen mode + barge-in ---
4385
+ let alwaysListening = false;
4386
+ let listenStream = null;
4387
+ let listenInterval = null;
4388
+ let bargeInAnalyser = null;
4389
+
4390
+ // Whisper hallucination patterns — filter these out
4391
+ const WHISPER_NOISE = /^\[.*\]$|^[\s.!?,;:…—–-]*$|^\(.*\)$/;
4392
+ let sttBackoff = 0; // ms to wait after errors
4393
+ let speechBuffer = ""; // Accumulates text across chunks until user pauses
4394
+ let silenceCount = 0; // Consecutive silent chunks
4395
+
4396
+ // Is Dash currently talking? (streaming or TTS)
4397
+ function isDashTalking() {
4398
+ return !!currentAudio || streaming;
4399
+ }
4400
+
4401
+ // Barge-in: stop TTS playback + abort streaming
4402
+ function bargeIn() {
4403
+ console.log("[barge-in] Stopping Dash");
4404
+ stopSpeaking();
4405
+ if (streaming && abortController) {
4406
+ abortController.abort();
4407
+ }
4408
+ }
4409
+
4410
+ // Send accumulated speech buffer as a message
4411
+ function flushSpeechBuffer() {
4412
+ const fullMessage = speechBuffer.trim();
4413
+ speechBuffer = "";
4414
+ silenceCount = 0;
4415
+ micBtn.classList.remove("recording");
4416
+ micBtn.classList.add("listening");
4417
+ if (fullMessage && !isDashTalking()) {
4418
+ console.log("[listen] Sending:", JSON.stringify(fullMessage));
4419
+ chatInput.value = fullMessage;
4420
+ autoResize();
4421
+ sendMessage();
4422
+ }
4423
+ }
4424
+
4425
+ // Record a short audio chunk from the mic and transcribe via local Whisper
4426
+ async function listenChunk() {
4427
+ if (!alwaysListening || !listenStream) return;
4428
+ try {
4429
+ const recorder = new MediaRecorder(listenStream, { mimeType: "audio/webm;codecs=opus" });
4430
+ const chunks = [];
4431
+ recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
4432
+
4433
+ const done = new Promise((resolve) => { recorder.onstop = resolve; });
4434
+ recorder.start();
4435
+ setTimeout(() => { if (recorder.state === "recording") recorder.stop(); }, 3000);
4436
+ await done;
4437
+
4438
+ if (chunks.length === 0 || !alwaysListening) return;
4439
+ const webmBlob = new Blob(chunks, { type: "audio/webm" });
4440
+
4441
+ // Convert webm → WAV (whisper-server requires WAV format)
4442
+ const wavBlob = await blobToWav(webmBlob);
4443
+
4444
+ // Voice activity check — skip silence to prevent Whisper hallucinations
4445
+ const wavBuf = await wavBlob.arrayBuffer();
4446
+ const pcm = new Int16Array(wavBuf, 44);
4447
+ let energy = 0;
4448
+ for (let i = 0; i < pcm.length; i++) energy += (pcm[i] / 32768) ** 2;
4449
+ const chunkRms = Math.sqrt(energy / pcm.length);
4450
+
4451
+ if (chunkRms < 0.01) {
4452
+ // Silent chunk — if we have accumulated speech, user has paused → send it
4453
+ silenceCount++;
4454
+ if (speechBuffer && silenceCount >= 1) {
4455
+ flushSpeechBuffer();
4456
+ }
4457
+ return;
4458
+ }
4459
+
4460
+ silenceCount = 0;
4461
+ const wavBlobFinal = new Blob([wavBuf], { type: "audio/wav" });
4462
+
4463
+ // Send to local Whisper for transcription
4464
+ const res = await fetch("/api/stt", { method: "POST", body: wavBlobFinal });
4465
+ if (!res.ok) {
4466
+ sttBackoff = Math.min((sttBackoff || 1000) * 2, 10000);
4467
+ console.log("[listen] STT error", res.status, "— backing off", sttBackoff, "ms");
4468
+ await new Promise(r => setTimeout(r, sttBackoff));
4469
+ return;
4470
+ }
4471
+ sttBackoff = 0;
4472
+ const data = await res.json();
4473
+ const text = (data.text || "").trim();
4474
+ if (!text) return;
4475
+
4476
+ console.log("[listen]", JSON.stringify(text));
4477
+ if (WHISPER_NOISE.test(text)) return;
4478
+
4479
+ // If Dash is talking, user speaking = barge-in (interrupt)
4480
+ if (isDashTalking()) {
4481
+ bargeIn();
4482
+ return;
4483
+ }
4484
+
4485
+ // Accumulate speech — show in input box as preview
4486
+ speechBuffer += (speechBuffer ? " " : "") + text;
4487
+ chatInput.value = speechBuffer;
4488
+ autoResize();
4489
+ micBtn.classList.remove("listening");
4490
+ micBtn.classList.add("recording");
4491
+ } catch (err) {
4492
+ console.log("[listen] error:", err.message);
4493
+ }
4494
+ }
4495
+
4496
+ // Start always-listening loop
4497
+ async function startListening() {
4498
+ try {
4499
+ listenStream = await navigator.mediaDevices.getUserMedia({ audio: true });
4500
+ alwaysListening = true;
4501
+ micBtn.classList.add("listening");
4502
+ micBtn.title = "Mic on — speaking (click to stop)";
4503
+ console.log("[mic] Always-listening started");
4504
+
4505
+ // Set up barge-in volume monitor — uses RMS of time-domain waveform
4506
+ const bargeCtx = new AudioContext();
4507
+ // Resume AudioContext if Chrome suspended it
4508
+ if (bargeCtx.state === "suspended") await bargeCtx.resume();
4509
+ const bargeSource = bargeCtx.createMediaStreamSource(listenStream);
4510
+ bargeInAnalyser = bargeCtx.createAnalyser();
4511
+ bargeInAnalyser.fftSize = 2048;
4512
+ bargeSource.connect(bargeInAnalyser);
4513
+ const waveData = new Uint8Array(bargeInAnalyser.fftSize);
4514
+ let bargeDebounce = 0;
4515
+ let bargeLogTimer = 0;
4516
+
4517
+ function checkBarge() {
4518
+ if (!alwaysListening) { bargeCtx.close(); return; }
4519
+ bargeInAnalyser.getByteTimeDomainData(waveData);
4520
+ let sum = 0;
4521
+ for (let i = 0; i < waveData.length; i++) {
4522
+ const v = (waveData[i] - 128) / 128;
4523
+ sum += v * v;
4524
+ }
4525
+ const rms = Math.sqrt(sum / waveData.length);
4526
+
4527
+ const now = Date.now();
4528
+ if (rms > 0.04 && isDashTalking() && now - bargeDebounce > 1500) {
4529
+ console.log("[barge-in] Voice detected (RMS:", rms.toFixed(3), ") — stopping Dash");
4530
+ bargeDebounce = now;
4531
+ bargeIn();
4532
+ }
4533
+ requestAnimationFrame(checkBarge);
4534
+ }
4535
+ checkBarge();
4536
+
4537
+ // Continuous listen loop (record → transcribe → repeat)
4538
+ (async function loop() {
4539
+ while (alwaysListening) {
4540
+ await listenChunk();
4541
+ // Gap between chunks — gives whisper-server breathing room
4542
+ await new Promise(r => setTimeout(r, 500));
4543
+ }
4544
+ })();
4545
+ } catch (err) {
4546
+ console.log("[mic] Mic access denied:", err.message);
4547
+ addMessage("system", "Mic access denied: " + err.message);
4548
+ }
4549
+ }
4550
+
4551
+ function stopListening() {
4552
+ alwaysListening = false;
4553
+ if (listenStream) {
4554
+ listenStream.getTracks().forEach(t => t.stop());
4555
+ listenStream = null;
4556
+ }
4557
+ micBtn.classList.remove("listening", "recording");
4558
+ micBtn.title = "Click to talk";
4559
+ console.log("[mic] Stopped listening");
4560
+ }
4561
+
4562
+ // Click mic to toggle
4563
+ micBtn.addEventListener("click", function(e) {
4564
+ e.preventDefault();
4565
+ e.stopPropagation();
4566
+ if (alwaysListening) {
4567
+ stopListening();
4568
+ } else if (voiceStatus.stt) {
4569
+ startListening();
4570
+ } else {
4571
+ addMessage("system", "STT not available — Whisper sidecar not running");
4572
+ }
4573
+ });
4574
+
4575
+ // Single-shot recording fallback (click without always-listen)
4576
+ async function startRecording() {
4577
+ if (micRecording || alwaysListening) return;
4578
+
4579
+ try {
4580
+ micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
4581
+ mediaRecorder = new MediaRecorder(micStream, { mimeType: "audio/webm;codecs=opus" });
4582
+ const chunks = [];
4583
+ mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
4584
+
4585
+ mediaRecorder.onstop = async () => {
4586
+ micStream.getTracks().forEach(t => t.stop());
4587
+ micStream = null;
4588
+ if (chunks.length === 0) return;
4589
+ const webmBlob = new Blob(chunks, { type: "audio/webm" });
4590
+ micBtn.classList.remove("recording");
4591
+ micRecording = false;
4592
+ try {
4593
+ const wavBlob = await blobToWav(webmBlob);
4594
+ const res = await fetch("/api/stt", { method: "POST", body: wavBlob });
4595
+ if (res.ok) {
4596
+ const data = await res.json();
4597
+ if (data.text && data.text.trim()) {
4598
+ chatInput.value = data.text.trim();
4599
+ autoResize();
4600
+ sendMessage();
4601
+ }
4602
+ }
4603
+ } catch (err) { console.log("STT error:", err); }
4604
+ };
4605
+
4606
+ mediaRecorder.start();
4607
+ micRecording = true;
4608
+ micBtn.classList.add("recording");
4609
+ } catch (err) {
4610
+ console.log("Mic access denied:", err);
4611
+ }
4612
+ }
4613
+
4614
+ function stopRecording() {
4615
+ if (!micRecording || !mediaRecorder) return;
4616
+ mediaRecorder.stop();
4617
+ // State cleanup happens in onstop handler
4618
+ }
4619
+
4620
+
4621
+ // Voice settings save
4622
+ document.getElementById("voice-save-btn").addEventListener("click", async function() {
4623
+ const btn = this;
4624
+ btn.disabled = true;
4625
+ const statusEl = document.getElementById("voice-save-status");
4626
+ statusEl.classList.remove("visible");
4627
+
4628
+ try {
4629
+ const result = await api("/api/settings", {
4630
+ method: "PUT",
4631
+ body: {
4632
+ tts: {
4633
+ enabled: document.getElementById("tts-toggle").checked,
4634
+ autoPlay: document.getElementById("autoplay-toggle").checked,
4635
+ },
4636
+ stt: {
4637
+ enabled: document.getElementById("stt-toggle").checked,
4638
+ },
4639
+ },
4640
+ });
4641
+ ttsAutoPlay = result.tts?.autoPlay !== false;
4642
+ statusEl.classList.add("visible");
4643
+ setTimeout(() => statusEl.classList.remove("visible"), 2000);
4644
+ } catch (err) {
4645
+ statusEl.textContent = err.message;
4646
+ statusEl.classList.add("visible");
4647
+ setTimeout(() => {
4648
+ statusEl.textContent = "Saved";
4649
+ statusEl.classList.remove("visible");
4650
+ }, 3000);
4651
+ } finally {
4652
+ btn.disabled = false;
4653
+ }
4654
+ });
4655
+
4656
+
4657
+ // --- Start ---
4658
+ init();
4659
+ })();
4660
+ </script>
4661
+
4662
+ <script src="/public/share-modal.js"></script>
4663
+
4664
+ <!-- UI update toast -->
4665
+ <div class="ui-update-toast" id="ui-update-toast">
4666
+ <span>Hi — there's been a tweak to this UI. Want to try it?</span>
4667
+ <button onclick="try{sessionStorage.setItem('_dash_restore',JSON.stringify({thread:typeof currentThreadId!=='undefined'?currentThreadId:null,scroll:document.getElementById('messages')?.scrollTop||0}))}catch(e){}location.reload()">Refresh</button>
4668
+ <button class="dismiss" onclick="this.parentElement.classList.remove('visible')">&times;</button>
4669
+ </div>
4670
+ <script>
4671
+ (function() {
4672
+ let knownVersion = null;
4673
+ async function checkUiVersion() {
4674
+ try {
4675
+ const r = await fetch("/api/ui-version");
4676
+ if (!r.ok) return;
4677
+ const { version } = await r.json();
4678
+ if (!knownVersion) { knownVersion = version; return; }
4679
+ if (version !== knownVersion) {
4680
+ document.getElementById("ui-update-toast").classList.add("visible");
4681
+ }
4682
+ } catch {}
4683
+ }
4684
+ checkUiVersion();
4685
+ setInterval(checkUiVersion, 30000);
4686
+ })();
4687
+ </script>
4688
+ </body>
4689
+ </html>