painapple-code 1.0.0__py3-none-any.whl

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 (436) hide show
  1. painapple_code/__init__.py +30 -0
  2. painapple_code/__main__.py +18 -0
  3. painapple_code/_version.py +24 -0
  4. painapple_code/auth_middleware.py +930 -0
  5. painapple_code/cli/__init__.py +204 -0
  6. painapple_code/cli/deploy/__init__.py +15 -0
  7. painapple_code/cli/deploy/claude_seed.py +41 -0
  8. painapple_code/cli/deploy/config.py +260 -0
  9. painapple_code/cli/deploy/container.py +715 -0
  10. painapple_code/cli/deploy/launch.py +163 -0
  11. painapple_code/cli/deploy/runtime.py +223 -0
  12. painapple_code/cli/lifecycle_cmd.py +443 -0
  13. painapple_code/cli/list_cmd.py +572 -0
  14. painapple_code/cli/manage_cmd.py +670 -0
  15. painapple_code/cli/netinfo.py +125 -0
  16. painapple_code/cli/profiles.py +390 -0
  17. painapple_code/cli/serve_args.py +163 -0
  18. painapple_code/cli/serve_config.py +256 -0
  19. painapple_code/cli/setup_cmd.py +664 -0
  20. painapple_code/cli/ui.py +438 -0
  21. painapple_code/cost_analytics.py +879 -0
  22. painapple_code/data/models.defaults.yaml +18 -0
  23. painapple_code/data/models.yaml +18 -0
  24. painapple_code/data/presets.defaults.json +32 -0
  25. painapple_code/data/strings.yaml +2269 -0
  26. painapple_code/helpers.py +406 -0
  27. painapple_code/paths.py +1514 -0
  28. painapple_code/prompt_explorer.py +984 -0
  29. painapple_code/providers/__init__.py +224 -0
  30. painapple_code/providers/base.py +710 -0
  31. painapple_code/providers/claude/__init__.py +74 -0
  32. painapple_code/providers/claude/capabilities.py +218 -0
  33. painapple_code/providers/claude/errors.py +79 -0
  34. painapple_code/providers/claude/launch.py +50 -0
  35. painapple_code/providers/claude/summary.py +89 -0
  36. painapple_code/providers/claude/translate.py +74 -0
  37. painapple_code/providers/claude_sdk/__init__.py +139 -0
  38. painapple_code/providers/claude_sdk/driver.py +412 -0
  39. painapple_code/providers/codex/__init__.py +87 -0
  40. painapple_code/providers/codex/capabilities.py +321 -0
  41. painapple_code/providers/codex/errors.py +70 -0
  42. painapple_code/providers/codex/launch.py +142 -0
  43. painapple_code/providers/codex/summary.py +196 -0
  44. painapple_code/providers/codex/translate.py +204 -0
  45. painapple_code/providers/codex_app_server/__init__.py +100 -0
  46. painapple_code/providers/codex_app_server/capabilities.py +41 -0
  47. painapple_code/providers/codex_app_server/launch.py +202 -0
  48. painapple_code/providers/codex_app_server/summary.py +291 -0
  49. painapple_code/providers/codex_app_server/translate.py +263 -0
  50. painapple_code/providers/codex_app_server/transport.py +238 -0
  51. painapple_code/routes/__init__.py +0 -0
  52. painapple_code/routes/api_agents.py +441 -0
  53. painapple_code/routes/api_app.py +368 -0
  54. painapple_code/routes/api_app_commit_sections.py +127 -0
  55. painapple_code/routes/api_app_config.py +997 -0
  56. painapple_code/routes/api_app_session_prefs.py +217 -0
  57. painapple_code/routes/api_browser.py +690 -0
  58. painapple_code/routes/api_commands.py +374 -0
  59. painapple_code/routes/api_costs.py +128 -0
  60. painapple_code/routes/api_drafts.py +139 -0
  61. painapple_code/routes/api_exec.py +149 -0
  62. painapple_code/routes/api_files.py +542 -0
  63. painapple_code/routes/api_git.py +657 -0
  64. painapple_code/routes/api_logs.py +649 -0
  65. painapple_code/routes/api_plugins.py +275 -0
  66. painapple_code/routes/api_project_config.py +418 -0
  67. painapple_code/routes/api_prompts.py +231 -0
  68. painapple_code/routes/api_providers.py +53 -0
  69. painapple_code/routes/api_search.py +411 -0
  70. painapple_code/routes/api_session_stash.py +234 -0
  71. painapple_code/routes/api_session_welcome.py +151 -0
  72. painapple_code/routes/api_sessions.py +742 -0
  73. painapple_code/routes/api_shadow.py +323 -0
  74. painapple_code/routes/api_shadow_db.py +512 -0
  75. painapple_code/routes/api_shadow_files.py +332 -0
  76. painapple_code/routes/api_shadow_search.py +476 -0
  77. painapple_code/routes/api_skills.py +610 -0
  78. painapple_code/routes/api_tasks.py +147 -0
  79. painapple_code/routes/api_terminal.py +540 -0
  80. painapple_code/routes/api_upload.py +293 -0
  81. painapple_code/routes/api_viewer.py +229 -0
  82. painapple_code/routes/dependencies.py +109 -0
  83. painapple_code/routes/ws_chat.py +837 -0
  84. painapple_code/server.py +2208 -0
  85. painapple_code/server_logging.py +263 -0
  86. painapple_code/services/__init__.py +0 -0
  87. painapple_code/services/agent_session.py +2738 -0
  88. painapple_code/session_store.py +253 -0
  89. painapple_code/session_store_core.py +879 -0
  90. painapple_code/shadow_db.py +830 -0
  91. painapple_code/shadow_db_plans.py +369 -0
  92. painapple_code/shadow_db_queries.py +595 -0
  93. painapple_code/shadow_db_schema.py +676 -0
  94. painapple_code/shadow_git.py +1296 -0
  95. painapple_code/shadow_git_frontmatter.py +314 -0
  96. painapple_code/shadow_git_sections.py +520 -0
  97. painapple_code/shadow_git_summary.py +462 -0
  98. painapple_code/shadow_parser.py +560 -0
  99. painapple_code/static/css/00-variables.css +276 -0
  100. painapple_code/static/css/01-base.css +245 -0
  101. painapple_code/static/css/02-tooltips.css +36 -0
  102. painapple_code/static/css/09-left-rail.css +448 -0
  103. painapple_code/static/css/10-header.css +777 -0
  104. painapple_code/static/css/11-connection-bar.css +120 -0
  105. painapple_code/static/css/20-messages.css +750 -0
  106. painapple_code/static/css/21-tool-blocks.css +2340 -0
  107. painapple_code/static/css/22-edit-diff.css +619 -0
  108. painapple_code/static/css/23-thinking.css +1584 -0
  109. painapple_code/static/css/24-background-tasks.css +359 -0
  110. painapple_code/static/css/25-context-block.css +252 -0
  111. painapple_code/static/css/26-session-setup.css +144 -0
  112. painapple_code/static/css/30-markdown.css +854 -0
  113. painapple_code/static/css/40-input.css +2418 -0
  114. painapple_code/static/css/41-autocomplete.css +127 -0
  115. painapple_code/static/css/42-activity-strip.css +97 -0
  116. painapple_code/static/css/43-file-autocomplete.css +121 -0
  117. painapple_code/static/css/44-snippets-autocomplete.css +224 -0
  118. painapple_code/static/css/45-selection-bar.css +433 -0
  119. painapple_code/static/css/46-keyboard-bar.css +119 -0
  120. painapple_code/static/css/47-keyboard-longpress.css +116 -0
  121. painapple_code/static/css/47-token-profile.css +106 -0
  122. painapple_code/static/css/50-welcome.css +3095 -0
  123. painapple_code/static/css/51-modals.css +301 -0
  124. painapple_code/static/css/52-utilities.css +77 -0
  125. painapple_code/static/css/53-image-upload.css +456 -0
  126. painapple_code/static/css/54-todo-list.css +102 -0
  127. painapple_code/static/css/55-file-upload.css +113 -0
  128. painapple_code/static/css/56-chat-navigator.css +218 -0
  129. painapple_code/static/css/56-permission-card.css +332 -0
  130. painapple_code/static/css/56-plan-approval.css +204 -0
  131. painapple_code/static/css/56-question-form.css +602 -0
  132. painapple_code/static/css/57-swipe-indicator.css +95 -0
  133. painapple_code/static/css/58-session-families.css +627 -0
  134. painapple_code/static/css/59-about.css +196 -0
  135. painapple_code/static/css/61-log-explorer.css +723 -0
  136. painapple_code/static/css/62-json-tree.css +489 -0
  137. painapple_code/static/css/62-skills-widget.css +650 -0
  138. painapple_code/static/css/63-commands-widget.css +30 -0
  139. painapple_code/static/css/63-plugins-widget.css +174 -0
  140. painapple_code/static/css/63-terminal.css +516 -0
  141. painapple_code/static/css/64-search-files-widget.css +448 -0
  142. painapple_code/static/css/65-snippets-widget.css +426 -0
  143. painapple_code/static/css/66-active-sessions.css +979 -0
  144. painapple_code/static/css/66-agents-widget.css +265 -0
  145. painapple_code/static/css/66-file-explorer-widget.css +855 -0
  146. painapple_code/static/css/66-git-panel.css +791 -0
  147. painapple_code/static/css/66-history-explorer.css +1496 -0
  148. painapple_code/static/css/67-diff-viewer.css +602 -0
  149. painapple_code/static/css/67-editor-view.css +748 -0
  150. painapple_code/static/css/67-file-preview-widget.css +1873 -0
  151. painapple_code/static/css/68-browser-widget.css +337 -0
  152. painapple_code/static/css/68-compare-wizard.css +355 -0
  153. painapple_code/static/css/68-csv-preview.css +106 -0
  154. painapple_code/static/css/68-diff-preview.css +103 -0
  155. painapple_code/static/css/68-helpers-install-widget.css +491 -0
  156. painapple_code/static/css/68-image-annotate.css +346 -0
  157. painapple_code/static/css/68-terminal-view.css +98 -0
  158. painapple_code/static/css/68-uploads-widget.css +229 -0
  159. painapple_code/static/css/69-cost-analytics.css +537 -0
  160. painapple_code/static/css/70-config-panel.css +3736 -0
  161. painapple_code/static/css/70-excalidraw.css +136 -0
  162. painapple_code/static/css/70-prompt-explorer.css +890 -0
  163. painapple_code/static/css/71-chart.css +111 -0
  164. painapple_code/static/css/72-discussion-sidebar.css +678 -0
  165. painapple_code/static/css/74-debug-logs.css +656 -0
  166. painapple_code/static/css/80-open-dialog.css +146 -0
  167. painapple_code/static/css/80-quick-switcher.css +383 -0
  168. painapple_code/static/css/80-widget-system.css +1763 -0
  169. painapple_code/static/css/81-quick-actions.css +1020 -0
  170. painapple_code/static/css/82-context-popover.css +1233 -0
  171. painapple_code/static/css/83-context-menu.css +339 -0
  172. painapple_code/static/css/85-lazy-loading.css +102 -0
  173. painapple_code/static/css/86-zen-mode.css +708 -0
  174. painapple_code/static/css/87-grid-switcher.css +346 -0
  175. painapple_code/static/css/login.css +198 -0
  176. painapple_code/static/dist/app.bundle.js +7177 -0
  177. painapple_code/static/feature-triage.html +446 -0
  178. painapple_code/static/icons/apple-touch-icon.png +0 -0
  179. painapple_code/static/icons/favicon-32.png +0 -0
  180. painapple_code/static/icons/favicon.ico +0 -0
  181. painapple_code/static/icons/icon-128.png +0 -0
  182. painapple_code/static/icons/icon-144.png +0 -0
  183. painapple_code/static/icons/icon-152.png +0 -0
  184. painapple_code/static/icons/icon-180.png +0 -0
  185. painapple_code/static/icons/icon-192.png +0 -0
  186. painapple_code/static/icons/icon-384.png +0 -0
  187. painapple_code/static/icons/icon-512.png +0 -0
  188. painapple_code/static/icons/icon-72.png +0 -0
  189. painapple_code/static/icons/icon-96.png +0 -0
  190. painapple_code/static/js/action-delegate.js +187 -0
  191. painapple_code/static/js/activity-strip.js +193 -0
  192. painapple_code/static/js/app/context-menus.js +619 -0
  193. painapple_code/static/js/app/file-preview.js +163 -0
  194. painapple_code/static/js/app/init-ui.js +574 -0
  195. painapple_code/static/js/app/input-events.js +561 -0
  196. painapple_code/static/js/app/input-resize.js +313 -0
  197. painapple_code/static/js/app/message-actions.js +419 -0
  198. painapple_code/static/js/app/panels.js +210 -0
  199. painapple_code/static/js/app/rendering-delegators.js +246 -0
  200. painapple_code/static/js/app/server-loading.js +365 -0
  201. painapple_code/static/js/app/session-ops.js +287 -0
  202. painapple_code/static/js/app/session-switch.js +443 -0
  203. painapple_code/static/js/app/settings-helpers.js +280 -0
  204. painapple_code/static/js/app/startup-welcome.js +290 -0
  205. painapple_code/static/js/app-context.js +157 -0
  206. painapple_code/static/js/app.js +2134 -0
  207. painapple_code/static/js/auth-fetch.js +46 -0
  208. painapple_code/static/js/background-tasks.js +274 -0
  209. painapple_code/static/js/boot.js +73 -0
  210. painapple_code/static/js/caret-position.js +171 -0
  211. painapple_code/static/js/chat-navigator.js +710 -0
  212. painapple_code/static/js/chat-search.js +323 -0
  213. painapple_code/static/js/command-executor.js +176 -0
  214. painapple_code/static/js/command-store.js +497 -0
  215. painapple_code/static/js/components.js +1414 -0
  216. painapple_code/static/js/config.js +218 -0
  217. painapple_code/static/js/context-menu.js +740 -0
  218. painapple_code/static/js/controllers/chat-controller.js +4252 -0
  219. painapple_code/static/js/controllers/dialog-controller.js +217 -0
  220. painapple_code/static/js/controllers/tab-controller.js +1828 -0
  221. painapple_code/static/js/controllers/thinking-controller.js +915 -0
  222. painapple_code/static/js/diff-utils.js +486 -0
  223. painapple_code/static/js/drafts-pill.js +95 -0
  224. painapple_code/static/js/editor-view.js +815 -0
  225. painapple_code/static/js/effort-settings.js +430 -0
  226. painapple_code/static/js/feature-triage.js +811 -0
  227. painapple_code/static/js/file-autocomplete.js +516 -0
  228. painapple_code/static/js/file-tabs.js +123 -0
  229. painapple_code/static/js/gestures.js +448 -0
  230. painapple_code/static/js/grid-switcher.js +334 -0
  231. painapple_code/static/js/input-handler.js +1472 -0
  232. painapple_code/static/js/keyboard-bar.js +495 -0
  233. painapple_code/static/js/layout-switcher.js +109 -0
  234. painapple_code/static/js/linkify-utils.js +200 -0
  235. painapple_code/static/js/login.js +141 -0
  236. painapple_code/static/js/open-dialog.js +879 -0
  237. painapple_code/static/js/orphan-terminals.js +373 -0
  238. painapple_code/static/js/path-utils.js +179 -0
  239. painapple_code/static/js/perf-marks.js +102 -0
  240. painapple_code/static/js/permission-settings.js +475 -0
  241. painapple_code/static/js/pointer-input.js +59 -0
  242. painapple_code/static/js/preview/json-tree-menu.js +226 -0
  243. painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
  244. painapple_code/static/js/preview/json-tree.js +457 -0
  245. painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
  246. painapple_code/static/js/preview/preview-edit.js +367 -0
  247. painapple_code/static/js/preview/preview-events.js +361 -0
  248. painapple_code/static/js/preview/preview-history.js +567 -0
  249. painapple_code/static/js/preview/preview-inline-edit.js +869 -0
  250. painapple_code/static/js/preview/preview-poll.js +108 -0
  251. painapple_code/static/js/preview/preview-render.js +572 -0
  252. painapple_code/static/js/preview/preview-search.js +421 -0
  253. painapple_code/static/js/preview/preview-state.js +256 -0
  254. painapple_code/static/js/preview/preview-utils.js +251 -0
  255. painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
  256. painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
  257. painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
  258. painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
  259. painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
  260. painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
  261. painapple_code/static/js/preview-plugins/index.js +34 -0
  262. painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
  263. painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
  264. painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
  265. painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
  266. painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
  267. painapple_code/static/js/project-color-picker.js +110 -0
  268. painapple_code/static/js/project-colors.js +163 -0
  269. painapple_code/static/js/prompt-favorites.js +187 -0
  270. painapple_code/static/js/quick-actions-menu.js +1736 -0
  271. painapple_code/static/js/quick-actions-registry.js +1239 -0
  272. painapple_code/static/js/quick-switcher/controller.js +294 -0
  273. painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
  274. painapple_code/static/js/quick-switcher/index.js +30 -0
  275. painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
  276. painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
  277. painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
  278. painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
  279. painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
  280. painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
  281. painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
  282. painapple_code/static/js/quick-switcher/registry.js +69 -0
  283. painapple_code/static/js/quick-switcher/ui/item.js +54 -0
  284. painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
  285. painapple_code/static/js/recent-opens.js +40 -0
  286. painapple_code/static/js/scroll-manager.js +530 -0
  287. painapple_code/static/js/scroll-state-machine.js +359 -0
  288. painapple_code/static/js/selection/action-bar.js +793 -0
  289. painapple_code/static/js/selection/index.js +15 -0
  290. painapple_code/static/js/selection/selection-handler.js +877 -0
  291. painapple_code/static/js/selection/state.js +181 -0
  292. painapple_code/static/js/session/agent-progress.js +202 -0
  293. painapple_code/static/js/session/handle-agent-message.js +481 -0
  294. painapple_code/static/js/session/handle-message.js +550 -0
  295. painapple_code/static/js/session/interactive.js +587 -0
  296. painapple_code/static/js/session/message-store.js +271 -0
  297. painapple_code/static/js/session/messages.js +253 -0
  298. painapple_code/static/js/session/persistence.js +179 -0
  299. painapple_code/static/js/session/restore.js +136 -0
  300. painapple_code/static/js/session/sync.js +333 -0
  301. painapple_code/static/js/session-container-pool.js +371 -0
  302. painapple_code/static/js/session-setup-panel.js +274 -0
  303. painapple_code/static/js/session.js +1047 -0
  304. painapple_code/static/js/shortcut-hints.js +272 -0
  305. painapple_code/static/js/shortcuts.js +1146 -0
  306. painapple_code/static/js/skills-autocomplete.js +262 -0
  307. painapple_code/static/js/snippets-autocomplete.js +738 -0
  308. painapple_code/static/js/stash-refs-view.js +83 -0
  309. painapple_code/static/js/stash-ui.js +768 -0
  310. painapple_code/static/js/stash.js +785 -0
  311. painapple_code/static/js/status-bar.js +1184 -0
  312. painapple_code/static/js/token-profile.js +274 -0
  313. painapple_code/static/js/tool-renderer-blocks.js +1626 -0
  314. painapple_code/static/js/tool-renderer-thinking.js +813 -0
  315. painapple_code/static/js/tool-renderer.js +1500 -0
  316. painapple_code/static/js/tooltips.js +197 -0
  317. painapple_code/static/js/upload-manager.js +1019 -0
  318. painapple_code/static/js/utils.js +521 -0
  319. painapple_code/static/js/welcome/api.js +363 -0
  320. painapple_code/static/js/welcome/cards.js +626 -0
  321. painapple_code/static/js/welcome/context-menu.js +585 -0
  322. painapple_code/static/js/welcome/families.js +801 -0
  323. painapple_code/static/js/welcome/preview.js +234 -0
  324. painapple_code/static/js/welcome/state.js +129 -0
  325. painapple_code/static/js/welcome.js +1874 -0
  326. painapple_code/static/js/widget-system/base-widget.js +717 -0
  327. painapple_code/static/js/widget-system/device-manager.js +178 -0
  328. painapple_code/static/js/widget-system/event-bus.js +152 -0
  329. painapple_code/static/js/widget-system/icons.js +120 -0
  330. painapple_code/static/js/widget-system/index.js +49 -0
  331. painapple_code/static/js/widget-system/init.js +54 -0
  332. painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
  333. painapple_code/static/js/widget-system/types/floating.js +915 -0
  334. painapple_code/static/js/widget-system/types/index.js +40 -0
  335. painapple_code/static/js/widget-system/types/modal.js +147 -0
  336. painapple_code/static/js/widget-system/types/sidebar.js +158 -0
  337. painapple_code/static/js/widget-system/types/tab.js +146 -0
  338. painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
  339. painapple_code/static/js/widget-system/widget-manager.js +1187 -0
  340. painapple_code/static/js/widgets/about-widget.js +260 -0
  341. painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
  342. painapple_code/static/js/widgets/agents-widget.js +897 -0
  343. painapple_code/static/js/widgets/browser-widget.js +579 -0
  344. painapple_code/static/js/widgets/commands-widget.js +694 -0
  345. painapple_code/static/js/widgets/config/commit-sections.js +449 -0
  346. painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
  347. painapple_code/static/js/widgets/config/gestures.js +161 -0
  348. painapple_code/static/js/widgets/config/models-tab.js +988 -0
  349. painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
  350. painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
  351. painapple_code/static/js/widgets/config/state.js +517 -0
  352. painapple_code/static/js/widgets/config/system-controls.js +90 -0
  353. painapple_code/static/js/widgets/config-widget.js +1535 -0
  354. painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
  355. painapple_code/static/js/widgets/debug-widget.js +693 -0
  356. painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
  357. painapple_code/static/js/widgets/discussion-widget.js +1107 -0
  358. painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
  359. painapple_code/static/js/widgets/file-preview-widget.js +861 -0
  360. painapple_code/static/js/widgets/git-widget.js +794 -0
  361. painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
  362. painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
  363. painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
  364. painapple_code/static/js/widgets/image-preview-widget.js +692 -0
  365. painapple_code/static/js/widgets/index.js +176 -0
  366. painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
  367. painapple_code/static/js/widgets/plugins-widget.js +305 -0
  368. painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
  369. painapple_code/static/js/widgets/search-files-widget.js +529 -0
  370. painapple_code/static/js/widgets/skills-widget.js +843 -0
  371. painapple_code/static/js/widgets/snippets-widget.js +479 -0
  372. painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
  373. painapple_code/static/js/widgets/tasks-widget.js +379 -0
  374. painapple_code/static/js/widgets/terminal/connection.js +283 -0
  375. painapple_code/static/js/widgets/terminal/gestures.js +357 -0
  376. painapple_code/static/js/widgets/terminal/init.js +642 -0
  377. painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
  378. painapple_code/static/js/widgets/terminal/osc52.js +213 -0
  379. painapple_code/static/js/widgets/terminal/render.js +159 -0
  380. painapple_code/static/js/widgets/terminal/size.js +54 -0
  381. painapple_code/static/js/widgets/terminal/state.js +211 -0
  382. painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
  383. painapple_code/static/js/widgets/terminal-widget.js +793 -0
  384. painapple_code/static/js/widgets/uploads-widget.js +461 -0
  385. painapple_code/static/js/widgets/zen-widget.js +1001 -0
  386. painapple_code/static/login.html +51 -0
  387. painapple_code/static/manifest.json +73 -0
  388. painapple_code/static/sw.js +293 -0
  389. painapple_code/static/vendor/LICENSES.txt +769 -0
  390. painapple_code/static/vendor/README.md +103 -0
  391. painapple_code/static/vendor/codemirror.js +35 -0
  392. painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
  393. painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
  394. painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
  395. painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
  396. painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
  397. painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
  398. painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
  399. painapple_code/static/vendor/highlight.min.js +1213 -0
  400. painapple_code/static/vendor/marked.min.js +6 -0
  401. painapple_code/static/vendor/purify.min.js +3 -0
  402. painapple_code/static/vendor/xterm-addon-fit.js +2 -0
  403. painapple_code/static/vendor/xterm.css +209 -0
  404. painapple_code/static/vendor/xterm.js +2 -0
  405. painapple_code/static/web-client.html +635 -0
  406. painapple_code/subprocess_registry.py +535 -0
  407. painapple_code/tls_cert.py +79 -0
  408. painapple_code/tools/agents/shadow-git-helper.md +400 -0
  409. painapple_code/tools/excalidraw-to-svg.js +143 -0
  410. painapple_code/tools/install-helpers.sh +148 -0
  411. painapple_code/tools/shadow-git +320 -0
  412. painapple_code/tools/shadow-query +120 -0
  413. painapple_code/tools/vegalite-to-svg.js +79 -0
  414. painapple_code/turn_query.py +766 -0
  415. painapple_code/turn_tracker.py +221 -0
  416. painapple_code/utils/__init__.py +0 -0
  417. painapple_code/utils/agent_cli.py +306 -0
  418. painapple_code/utils/chart.py +103 -0
  419. painapple_code/utils/excalidraw.py +107 -0
  420. painapple_code/utils/file_lock.py +162 -0
  421. painapple_code/utils/file_paths.py +648 -0
  422. painapple_code/utils/generate_icons.py +251 -0
  423. painapple_code/utils/proc.py +205 -0
  424. painapple_code/utils/pty_backend.py +59 -0
  425. painapple_code/utils/pty_posix.py +160 -0
  426. painapple_code/utils/pty_windows.py +245 -0
  427. painapple_code/utils/token_profiles.py +91 -0
  428. painapple_code/viewer_templates.py +657 -0
  429. painapple_code/welcome_search.py +879 -0
  430. painapple_code-1.0.0.dist-info/METADATA +292 -0
  431. painapple_code-1.0.0.dist-info/RECORD +436 -0
  432. painapple_code-1.0.0.dist-info/WHEEL +5 -0
  433. painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
  434. painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
  435. painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
  436. painapple_code-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,587 @@
1
+ /**
2
+ * Interactive question/approval mixin — Claude can pause for user input via
3
+ * AskUserQuestion (multi-question wizard) or ExitPlanMode (plan approval).
4
+ * This module owns:
5
+ * - handleAskUserQuestion: render new or merge into existing wizard
6
+ * - handleExitPlanMode: render plan-approval card with detected planFile
7
+ * - sendToolAnswer / sendToolAnswers: ship answers back over WebSocket and
8
+ * mark the message as answered
9
+ *
10
+ * Both render flows update the tab badge (?), scroll to the new card, and
11
+ * persist via sessionManager. Applied to Session.prototype via
12
+ * Object.assign in session.js.
13
+ */
14
+
15
+ import { debug } from '../config.js';
16
+ import { genId } from '../utils.js';
17
+ import { showToast } from '../context-menu.js';
18
+ import S from '../strings.js';
19
+ import { basename } from '../path-utils.js';
20
+
21
+ const getApp = () => window.app;
22
+
23
+ /**
24
+ * Coerce a tool-input `questions` value into an array of question objects.
25
+ *
26
+ * The CLI/SDK usually delivers `input.questions` already parsed as an array,
27
+ * but some turns arrive with it as a JSON-encoded *string* (e.g.
28
+ * `"[{\"question\": ...}]"`). When that happens the old `Array.isArray` guard
29
+ * silently fell through to `[]`, so the "Claude is asking" card rendered with
30
+ * no questions and no options — visibly broken. Parse the string form here so
31
+ * both shapes work.
32
+ * @param {*} raw - block.input?.questions (array, JSON string, or undefined)
33
+ * @returns {Array} normalized questions array (empty if unparseable)
34
+ */
35
+ function normalizeQuestions(raw) {
36
+ if (Array.isArray(raw)) return raw;
37
+ if (typeof raw === 'string' && raw.trim()) {
38
+ try {
39
+ const parsed = JSON.parse(raw);
40
+ if (Array.isArray(parsed)) return parsed;
41
+ } catch (e) {
42
+ debug.log('Failed to parse AskUserQuestion questions string:', e);
43
+ }
44
+ }
45
+ return [];
46
+ }
47
+
48
+ export const interactiveMethods = {
49
+ /**
50
+ * Handle AskUserQuestion tool - render interactive form for user input
51
+ * Groups multiple questions from the same turn into a wizard-style UI
52
+ * @param {Object} block - Tool use block with id, name, input
53
+ */
54
+ handleAskUserQuestion(block) {
55
+ const app = getApp();
56
+ const timestamp = this._currentServerTimestamp || new Date().toISOString();
57
+
58
+ // Check if there's an existing unanswered question message to merge with
59
+ // (multiple AskUserQuestion calls in same turn should be grouped)
60
+ const existingQuestion = this.messages.find(m =>
61
+ m.role === 'question' && !m.answered
62
+ );
63
+
64
+ // Create entry for this tool call
65
+ const questions = normalizeQuestions(block.input?.questions);
66
+ const entry = {
67
+ toolId: block.id,
68
+ questions,
69
+ answers: {}
70
+ };
71
+
72
+ if (existingQuestion) {
73
+ // Merge into existing question group
74
+ if (!existingQuestion.entries) {
75
+ // Convert single-entry format to multi-entry
76
+ existingQuestion.entries = [{
77
+ toolId: existingQuestion.toolId,
78
+ questions: Array.isArray(existingQuestion.questions) ? existingQuestion.questions : [],
79
+ answers: existingQuestion.answers || {}
80
+ }];
81
+ }
82
+ existingQuestion.entries.push(entry);
83
+ existingQuestion.timestamp = timestamp; // Update timestamp
84
+
85
+ debug.log('Merged AskUserQuestion into existing group:', block.id, 'total entries:', existingQuestion.entries.length);
86
+
87
+ // Re-render the message to show updated wizard
88
+ if (app.activeSession === this) {
89
+ const existingEl = document.getElementById(`msg-${existingQuestion.id}`);
90
+ if (existingEl) {
91
+ const newEl = app.chatCtrl.createMessageElement(existingQuestion);
92
+ existingEl.replaceWith(newEl);
93
+ }
94
+ }
95
+ } else {
96
+ // Auto-dismiss any stale unanswered questions from previous turns
97
+ // (e.g., auto-denied AskUserQuestion that was never answered by user)
98
+ const staleQuestions = this.messages.filter(m => m.role === 'question' && !m.answered);
99
+ for (const stale of staleQuestions) {
100
+ stale.answered = true;
101
+ if (app.activeSession === this) {
102
+ const el = document.getElementById(`msg-${stale.id}`);
103
+ if (el) {
104
+ const newEl = app.chatCtrl.createMessageElement(stale);
105
+ el.replaceWith(newEl);
106
+ }
107
+ }
108
+ }
109
+
110
+ // Create new question message with entries array
111
+ const msg = {
112
+ id: genId(),
113
+ role: 'question',
114
+ toolId: block.id, // Keep for backward compat
115
+ toolName: 'AskUserQuestion',
116
+ questions, // Keep for backward compat
117
+ entries: [entry], // New grouped format
118
+ answered: false,
119
+ answers: {}, // Keep for backward compat
120
+ activeTab: 0, // Track active tab in wizard
121
+ timestamp
122
+ };
123
+
124
+ this.messages.push(msg);
125
+
126
+ if (app.activeSession === this) {
127
+ app.renderMessage(msg);
128
+ }
129
+
130
+ debug.log('Created new AskUserQuestion:', block.id, questions.length, 'questions');
131
+ }
132
+
133
+ this.lastActivity = timestamp;
134
+ this.pendingQuestionId = block.id; // Track pending question
135
+ this.updateTab(); // Update tab badge (shows ? indicator)
136
+
137
+ // Scroll to the question form and update indicator
138
+ if (app.activeSession === this) {
139
+ setTimeout(() => {
140
+ const questionMsg = this.messages.find(m => m.role === 'question' && !m.answered);
141
+ if (questionMsg) {
142
+ const questionEl = document.getElementById(`msg-${questionMsg.id}`);
143
+ if (questionEl) {
144
+ questionEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
145
+ }
146
+ }
147
+ // Update indicator (will show if user scrolls away)
148
+ app.updateQuestionIndicator();
149
+ }, 100);
150
+ }
151
+
152
+ app.sessionManager.saveSessions();
153
+ },
154
+
155
+ /**
156
+ * Handle ExitPlanMode tool - render interactive plan approval UI
157
+ * @param {Object} block - Tool use block with id, name, input
158
+ */
159
+ handleExitPlanMode(block) {
160
+ const app = getApp();
161
+ const timestamp = this._currentServerTimestamp || new Date().toISOString();
162
+
163
+ // Find the plan file from session's Write tool history
164
+ let planFile = null;
165
+ for (let i = this.messages.length - 1; i >= 0; i--) {
166
+ const m = this.messages[i];
167
+ if (m.role === 'tool' && m.toolName === 'Write' && m.toolInput?.file_path?.includes('.claude/plans/')) {
168
+ planFile = m.toolInput.file_path;
169
+ break;
170
+ }
171
+ }
172
+
173
+ // Fallback: look for any recent Write with 'plan' in the filename
174
+ if (!planFile) {
175
+ for (let i = this.messages.length - 1; i >= 0; i--) {
176
+ const m = this.messages[i];
177
+ if (m.role === 'tool' && m.toolName === 'Write' && m.toolInput?.file_path) {
178
+ const fname = basename(m.toolInput.file_path).toLowerCase();
179
+ if (fname.includes('plan')) {
180
+ planFile = m.toolInput.file_path;
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ // Final fallback: use the most recent Write tool output (the plan file
188
+ // may have a non-standard name like "modular-dreaming-riddle.md")
189
+ if (!planFile) {
190
+ for (let i = this.messages.length - 1; i >= 0; i--) {
191
+ const m = this.messages[i];
192
+ if (m.role === 'tool' && m.toolName === 'Write' && m.toolInput?.file_path) {
193
+ planFile = m.toolInput.file_path;
194
+ break;
195
+ }
196
+ }
197
+ }
198
+
199
+ const msg = {
200
+ id: genId(),
201
+ role: 'plan_approval',
202
+ toolId: block.id,
203
+ toolName: 'ExitPlanMode',
204
+ planFile,
205
+ answered: false,
206
+ decision: null, // 'approve' or 'reject'
207
+ timestamp
208
+ };
209
+
210
+ this.messages.push(msg);
211
+
212
+ if (app.activeSession === this) {
213
+ app.renderMessage(msg);
214
+ }
215
+
216
+ this.lastActivity = timestamp;
217
+ this.pendingQuestionId = block.id; // Reuse ? badge mechanism
218
+ this.updateTab();
219
+
220
+ // Scroll to the approval card
221
+ if (app.activeSession === this) {
222
+ setTimeout(() => {
223
+ const el = document.getElementById(`msg-${msg.id}`);
224
+ if (el) {
225
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
226
+ }
227
+ app.updateQuestionIndicator();
228
+ }, 100);
229
+ }
230
+
231
+ app.sessionManager.saveSessions();
232
+ },
233
+
234
+ /**
235
+ * Handle a `permission_request` frame (interactive can_use_tool ask from
236
+ * the claude-sdk provider). Renders an inline approve/deny card; the
237
+ * provider process is paused until sendPermissionResponse() answers.
238
+ * Reconnect replays are deduped by requestId.
239
+ * @param {Object} frame - {request_id, tool_name, input, description, ...}
240
+ */
241
+ handlePermissionRequest(frame) {
242
+ const app = getApp();
243
+ const timestamp = this._currentServerTimestamp || new Date().toISOString();
244
+
245
+ // Dedupe: reconnect replay of a card we already have. Only replay
246
+ // frames dedupe, and only against a still-unanswered card — request
247
+ // ids are per-process counters (perm-1, perm-2, …), so a NON-replay
248
+ // frame reusing an id is a NEW ask from a restarted process, not a
249
+ // duplicate. Matching any old card here would swallow that ask and
250
+ // leave the process blocked with no card to answer.
251
+ if (frame.replay && this.messages.some(m =>
252
+ m.role === 'permission' && !m.answered && m.requestId === frame.request_id)) {
253
+ return;
254
+ }
255
+
256
+ // A non-replay ask reusing an unanswered card's request id means the
257
+ // process restarted (ids restart at perm-1 per process) — that old
258
+ // card's ask died with its process and can never be answered. Expire
259
+ // just the colliding card. Other unanswered cards are left alone:
260
+ // parallel tool calls from the live process legitimately stack asks,
261
+ // and they all must stay answerable. Cards orphaned by a process
262
+ // death are expired by expirePendingPermissionCards (session_ended)
263
+ // or lazily via the ok=false response ack.
264
+ if (!frame.replay) {
265
+ for (const stale of this.messages.filter(m =>
266
+ m.role === 'permission' && !m.answered && m.requestId === frame.request_id)) {
267
+ stale.answered = true;
268
+ stale.decision = 'expired';
269
+ stale.waitedMs = this._permWaitedMs(stale);
270
+ this._rerenderPermissionCard(stale);
271
+ }
272
+ }
273
+
274
+ const msg = {
275
+ id: genId(),
276
+ role: 'permission',
277
+ requestId: frame.request_id,
278
+ toolName: frame.tool_name,
279
+ toolInput: frame.input || {},
280
+ toolUseId: frame.tool_use_id || null,
281
+ title: frame.title || null,
282
+ description: frame.description || null,
283
+ // Engine "don't ask again" rule suggestions → "always allow" rows
284
+ suggestions: Array.isArray(frame.suggestions) ? frame.suggestions : [],
285
+ answered: false,
286
+ decision: null, // 'allow' | 'allow_always' | 'deny' | 'expired'
287
+ timestamp
288
+ };
289
+ this.messages.push(msg);
290
+
291
+ if (app.activeSession === this) {
292
+ app.renderMessage(msg);
293
+ }
294
+
295
+ // The provider process is blocked in its permission callback — show a
296
+ // paused activity state and freeze the elapsed timer until the answer.
297
+ if (!this._permPausedAt) {
298
+ this._permPausedAt = Date.now();
299
+ this._activityBeforePermission = this._lastActivity;
300
+ }
301
+ this._setActivity({
302
+ active: true,
303
+ paused: true,
304
+ icon: 'lock',
305
+ label: S.permission_card.activity_paused,
306
+ });
307
+
308
+ this.lastActivity = timestamp;
309
+ this.pendingQuestionId = frame.request_id; // Reuse ? badge mechanism
310
+ this.updateTab();
311
+
312
+ if (app.activeSession === this) {
313
+ setTimeout(() => {
314
+ const el = document.getElementById(`msg-${msg.id}`);
315
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
316
+ app.updateQuestionIndicator();
317
+ }, 100);
318
+ } else if (!frame.replay) {
319
+ // Background tab needs a decision — surface a click-to-go toast
320
+ const label = S.permission_card.toast_pending.replace('{name}', this.name || 'Session');
321
+ showToast(label, {
322
+ interactive: true,
323
+ duration: 10000,
324
+ className: 'permission-toast',
325
+ onMount: (toast) => {
326
+ toast.style.cursor = 'pointer';
327
+ toast.addEventListener('click', () => getApp().switchSession(this));
328
+ },
329
+ });
330
+ }
331
+
332
+ app.sessionManager.saveSessions();
333
+ },
334
+
335
+ /**
336
+ * Send the user's allow/deny decision for a permission card.
337
+ * @param {string} msgId - The permission message id
338
+ * @param {string} behavior - 'allow' | 'deny'
339
+ * @param {string} [feedback] - Optional deny guidance for the model
340
+ * @param {number} [suggestionIndex] - "Always allow": index into the
341
+ * request's suggestions; the engine applies that rule update itself.
342
+ */
343
+ sendPermissionResponse(msgId, behavior, feedback, suggestionIndex) {
344
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
345
+ console.error('Cannot send permission response: WebSocket not connected');
346
+ showToast(S.errors.not_connected);
347
+ return false;
348
+ }
349
+ const msg = this.messages.find(m => m.role === 'permission' && m.id === msgId);
350
+ if (!msg || msg.answered) return false;
351
+
352
+ const alwaysAllow = behavior === 'allow' && Number.isInteger(suggestionIndex);
353
+ this.ws.send(JSON.stringify({
354
+ type: 'permission_response',
355
+ request_id: msg.requestId,
356
+ behavior,
357
+ message: feedback || undefined,
358
+ suggestion_index: alwaysAllow ? suggestionIndex : undefined,
359
+ }));
360
+ debug.log('Sent permission response:', msg.requestId, behavior, suggestionIndex);
361
+
362
+ msg.answered = true;
363
+ msg.decision = alwaysAllow ? 'allow_always' : behavior;
364
+ msg.waitedMs = this._permWaitedMs(msg);
365
+ if (behavior === 'deny' && feedback) msg.feedback = feedback;
366
+ this._rerenderPermissionCard(msg);
367
+
368
+ this.pendingQuestionId = null;
369
+ this._resumePermissionActivity();
370
+ this.updateTab();
371
+ const app = getApp();
372
+ if (app.activeSession === this) {
373
+ app.updateQuestionIndicator();
374
+ }
375
+ app.sessionManager.saveSessions();
376
+ return true;
377
+ },
378
+
379
+ /**
380
+ * Server ack for a permission_response. ok=false → the request had
381
+ * expired (process restarted) — mark the card so the user knows.
382
+ */
383
+ handlePermissionResolved(frame) {
384
+ if (frame.ok) return;
385
+ const msg = this.messages.find(m => m.role === 'permission' && m.requestId === frame.request_id);
386
+ if (msg) {
387
+ msg.decision = 'expired';
388
+ msg.answered = true;
389
+ msg.waitedMs = this._permWaitedMs(msg);
390
+ this._rerenderPermissionCard(msg);
391
+ this._resumePermissionActivity();
392
+ getApp().sessionManager.saveSessions();
393
+ }
394
+ },
395
+
396
+ /**
397
+ * Find the permission card for a denial the USER made by hand (clicked
398
+ * Deny on the card). Used to tell manual denies apart from permission-mode
399
+ * auto-denies: the mode-explainer error card ("blocked by X mode — change
400
+ * the permission level…") is wrong and redundant for a manual decision —
401
+ * the answered card and the tool error (which quotes the user's feedback)
402
+ * already tell the story. Matches by tool_use_id when both sides have it,
403
+ * else falls back to tool name.
404
+ * @param {string} toolName
405
+ * @param {string|null} toolUseId
406
+ * @returns {Object|undefined} the denied card message, if any
407
+ */
408
+ findManualDenyCard(toolName, toolUseId) {
409
+ return [...this.messages].reverse().find(m =>
410
+ m.role === 'permission' && m.decision === 'deny' &&
411
+ ((toolUseId && m.toolUseId) ? m.toolUseId === toolUseId : m.toolName === toolName));
412
+ },
413
+
414
+ /**
415
+ * How long a permission ask waited before it was resolved/expired, anchored
416
+ * on the card's server timestamp (same anchor as the live elapsed ticker in
417
+ * chat-controller). Clamped to >= 0 to absorb minor client/server skew.
418
+ */
419
+ _permWaitedMs(msg) {
420
+ const start = Date.parse(msg.timestamp);
421
+ return Math.max(0, Date.now() - (Number.isNaN(start) ? Date.now() : start));
422
+ },
423
+
424
+ /**
425
+ * Expire every unanswered permission card — called on session_ended: the
426
+ * provider process died and the server dropped its pending asks (a
427
+ * response can only reach the stdin that asked), so these cards can never
428
+ * be answered. Clears the paused-activity bookkeeping without touching the
429
+ * activity strip itself; the session_ended handler sets the final state.
430
+ */
431
+ expirePendingPermissionCards() {
432
+ const pending = this.messages.filter(m => m.role === 'permission' && !m.answered);
433
+ if (!pending.length) return;
434
+ for (const msg of pending) {
435
+ msg.answered = true;
436
+ msg.decision = 'expired';
437
+ msg.waitedMs = this._permWaitedMs(msg);
438
+ this._rerenderPermissionCard(msg);
439
+ }
440
+ this._permPausedAt = null;
441
+ this._activityBeforePermission = null;
442
+ this.pendingQuestionId = null;
443
+ this.updateTab();
444
+ const app = getApp();
445
+ if (app.activeSession === this) app.updateQuestionIndicator();
446
+ app.sessionManager.saveSessions();
447
+ },
448
+
449
+ /**
450
+ * Un-pause the activity strip after the last pending ask is answered:
451
+ * shift the turn start time forward by the paused span (so the elapsed
452
+ * timer resumes where it froze) and restore the pre-ask activity.
453
+ */
454
+ _resumePermissionActivity() {
455
+ if (!this._permPausedAt) return;
456
+ // Parallel tool calls can stack asks — stay paused until all answered
457
+ if (this.messages.some(m => m.role === 'permission' && !m.answered)) return;
458
+ const pausedMs = Date.now() - this._permPausedAt;
459
+ this._permPausedAt = null;
460
+ if (this._turnStartTime) this._turnStartTime += pausedMs;
461
+ const prev = this._activityBeforePermission;
462
+ this._activityBeforePermission = null;
463
+ this._setActivity(prev
464
+ ? { ...prev, paused: false }
465
+ : { active: true, icon: 'sparkle', label: S.activity.states.working });
466
+ },
467
+
468
+ /** Re-render a permission card in place (if this session is visible). */
469
+ _rerenderPermissionCard(msg) {
470
+ const app = getApp();
471
+ if (app.activeSession !== this) return;
472
+ const el = document.getElementById(`msg-${msg.id}`);
473
+ if (el) {
474
+ el.replaceWith(app.chatCtrl.createMessageElement(msg));
475
+ }
476
+ },
477
+
478
+ /**
479
+ * Submit answers for AskUserQuestion tool
480
+ * @param {string} toolId - The tool_use_id
481
+ * @param {Object} answers - Answers keyed by question header
482
+ */
483
+ sendToolAnswer(toolId, answers, comment = '') {
484
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
485
+ console.error('Cannot send tool answer: WebSocket not connected');
486
+ return false;
487
+ }
488
+
489
+ // Find the question message to get original questions for context
490
+ const questionMsg = this.messages.find(m => m.role === 'question' && m.toolId === toolId);
491
+ const questions = questionMsg?.questions || [];
492
+
493
+ const message = {
494
+ type: 'tool_answer',
495
+ tool_use_id: toolId,
496
+ answers: answers,
497
+ questions: questions // Include original questions for server to format
498
+ };
499
+ if (comment) message.comment = comment;
500
+
501
+ this.ws.send(JSON.stringify(message));
502
+ debug.log('Sent tool answer:', toolId, answers);
503
+
504
+ // Update the question message as answered (reuse questionMsg from above)
505
+ if (questionMsg) {
506
+ questionMsg.answered = true;
507
+ questionMsg._editing = false; // leave edit mode on (re)send
508
+ questionMsg.answers = answers;
509
+ questionMsg.comment = comment;
510
+ const app = getApp();
511
+ if (app.activeSession === this) {
512
+ app.updateQuestionMessage(questionMsg);
513
+ }
514
+ app.sessionManager.saveSessions();
515
+ }
516
+
517
+ this.pendingQuestionId = null;
518
+ this.updateTab(); // Remove ? badge now that question is answered
519
+ // Clear the floating question indicator
520
+ const app = getApp();
521
+ if (app.activeSession === this) {
522
+ app.updateQuestionIndicator();
523
+ }
524
+ return true;
525
+ },
526
+
527
+ /**
528
+ * Submit answers for grouped AskUserQuestion (wizard mode)
529
+ * Sends answers for each tool entry sequentially
530
+ * @param {Array} entries - Array of {toolId, questions, answers}
531
+ * @param {Object} questionMsg - The question message object
532
+ */
533
+ sendToolAnswers(entries, questionMsg, comment = '') {
534
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
535
+ console.error('Cannot send tool answers: WebSocket not connected');
536
+ return false;
537
+ }
538
+
539
+ // Send answer for each entry. The free-text comment is shared across the
540
+ // whole card, so it rides on the LAST frame only (each frame becomes its
541
+ // own follow-up message server-side — attaching it to every entry would
542
+ // repeat the comment N times).
543
+ entries.forEach((entry, i) => {
544
+ const message = {
545
+ type: 'tool_answer',
546
+ tool_use_id: entry.toolId,
547
+ answers: entry.answers,
548
+ questions: entry.questions // Include original questions for server to format
549
+ };
550
+ if (comment && i === entries.length - 1) message.comment = comment;
551
+ this.ws.send(JSON.stringify(message));
552
+ debug.log('Sent tool answer:', entry.toolId, entry.answers);
553
+ });
554
+
555
+ // Update the question message as answered
556
+ if (questionMsg) {
557
+ questionMsg.answered = true;
558
+ questionMsg._editing = false; // leave edit mode on (re)send
559
+ questionMsg.comment = comment;
560
+ // Update answers in entries
561
+ if (questionMsg.entries) {
562
+ questionMsg.entries.forEach((e, i) => {
563
+ if (entries[i]) {
564
+ e.answers = entries[i].answers;
565
+ }
566
+ });
567
+ }
568
+ // Also update legacy answers field for backward compatibility
569
+ questionMsg.answers = entries[0]?.answers || {};
570
+
571
+ const app = getApp();
572
+ if (app.activeSession === this) {
573
+ app.updateQuestionMessage(questionMsg);
574
+ }
575
+ app.sessionManager.saveSessions();
576
+ }
577
+
578
+ this.pendingQuestionId = null;
579
+ this.updateTab(); // Remove ? badge now that question is answered
580
+ // Clear the floating question indicator
581
+ const appRef = getApp();
582
+ if (appRef.activeSession === this) {
583
+ appRef.updateQuestionIndicator();
584
+ }
585
+ return true;
586
+ },
587
+ };