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,1057 @@
1
+ /**
2
+ * Log Explorer Widget - Session log viewer using the widget system
3
+ *
4
+ * Provides a bottom-sheet/sidebar panel for viewing session logs.
5
+ * Migrated from log-explorer.js to use the modular widget system.
6
+ *
7
+ * Features:
8
+ * - Four tabs: Messages, Raw, Tools, System
9
+ * - Pagination with "Load More"
10
+ * - Filtering (role, errors only)
11
+ * - Sort order toggle
12
+ * - JSON tree rendering with expand/collapse
13
+ * - Transform to sidebar for desktop use
14
+ */
15
+
16
+ import S from '../strings.js';
17
+ import { CONFIG } from '../config.js';
18
+ import { escapeHtml, formatSize, highlightThinkingKeywords, formatTimePrecise } from '../utils.js';
19
+ import { WidgetManager, WidgetBus, ICONS } from '../widget-system/index.js';
20
+ import { isThinkingKeywordsHighlightingEnabled } from './config-widget.js';
21
+ import { renderJsonTree } from '../preview/json-tree.js';
22
+ import { copyToClipboard, showToast } from '../context-menu.js';
23
+ import { engineAuthorLabel } from '../status-bar.js';
24
+
25
+ // ─────────────────────────────────────────────────────────────────────
26
+ // Icons (specific to log explorer)
27
+ // ─────────────────────────────────────────────────────────────────────
28
+
29
+ const LOG_ICONS = {
30
+ logs: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
31
+ user: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
32
+ assistant: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
33
+ tool: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
34
+ result: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>',
35
+ thinking: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v.01"/><path d="M12 8a2 2 0 0 1 2 2c0 .74-.4 1.38-1 1.73V13a1 1 0 0 1-2 0v-1.27c-.6-.35-1-.99-1-1.73a2 2 0 0 1 2-2z"/></svg>',
36
+ error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
37
+ event: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>',
38
+ input: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>',
39
+ output: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>',
40
+ chevron: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>',
41
+ external: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg>',
42
+ refresh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
43
+ sortDesc: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 4h13M3 8h9M3 12h5M17 8v12M17 20l-4-4M17 20l4-4"/></svg>',
44
+ sortAsc: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h13M3 16h9M3 20h5M17 4v12M17 4l-4 4M17 4l4 4"/></svg>',
45
+ copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
46
+ check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>',
47
+ };
48
+
49
+ // Role colors and icons
50
+ const ROLE_CONFIG = {
51
+ user: { icon: LOG_ICONS.user, color: '#3b82f6', label: S.widgets.log_explorer.message_types.user },
52
+ assistant: { icon: LOG_ICONS.assistant, color: '#22c55e', label: S.widgets.log_explorer.message_types.assistant },
53
+ tool: { icon: LOG_ICONS.tool, color: '#f59e0b', label: S.widgets.log_explorer.message_types.tool },
54
+ thinking: { icon: LOG_ICONS.thinking, color: '#a855f7', label: S.widgets.log_explorer.message_types.thinking },
55
+ result: { icon: LOG_ICONS.result, color: '#64748b', label: S.widgets.log_explorer.message_types.result },
56
+ };
57
+
58
+ // Raw direction config
59
+ const DIR_CONFIG = {
60
+ in: { icon: LOG_ICONS.input, color: '#3b82f6', label: 'Input' },
61
+ out: { icon: LOG_ICONS.output, color: '#22c55e', label: 'Output' },
62
+ event: { icon: LOG_ICONS.event, color: '#64748b', label: 'Event' },
63
+ error: { icon: LOG_ICONS.error, color: '#ef4444', label: 'Error' },
64
+ };
65
+
66
+ // ─────────────────────────────────────────────────────────────────────
67
+ // State Management
68
+ // ─────────────────────────────────────────────────────────────────────
69
+
70
+ class LogExplorerState {
71
+ constructor() {
72
+ this.currentTab = 'messages'; // 'messages', 'raw', 'tools', 'system'
73
+ this.sessionId = null;
74
+ this.sessionMeta = null;
75
+
76
+ // Pagination
77
+ this.messagesOffset = 0;
78
+ this.rawOffset = 0;
79
+ this.hasMoreMessages = false;
80
+ this.hasMoreRaw = false;
81
+
82
+ // Filters
83
+ this.roleFilter = null;
84
+ this.dirFilter = null;
85
+ this.errorsOnly = false;
86
+
87
+ // Sort order: 'desc' (newest first) or 'asc' (oldest first)
88
+ this.sortOrder = 'desc';
89
+
90
+ // DOM references (updated on render)
91
+ this.container = null;
92
+ this.tabsEl = null;
93
+ this.contentEl = null;
94
+ this.loadingEl = null;
95
+ this.emptyEl = null;
96
+ this.errorEl = null;
97
+ this.loadMoreBtn = null;
98
+ this.sessionInfoEl = null;
99
+ this.sortBtn = null;
100
+ }
101
+
102
+ reset() {
103
+ this.currentTab = 'messages';
104
+ this.sessionMeta = null;
105
+ this.messagesOffset = 0;
106
+ this.rawOffset = 0;
107
+ this.hasMoreMessages = false;
108
+ this.hasMoreRaw = false;
109
+ this.roleFilter = null;
110
+ this.dirFilter = null;
111
+ this.errorsOnly = false;
112
+ this.sortOrder = 'desc';
113
+ }
114
+ }
115
+
116
+ // Per-session state map
117
+ const states = new Map();
118
+
119
+ function getState(sessionId) {
120
+ if (!sessionId) sessionId = WidgetManager.currentSessionId;
121
+ if (!states.has(sessionId)) states.set(sessionId, new LogExplorerState());
122
+ return states.get(sessionId);
123
+ }
124
+
125
+ function destroyState(sessionId) {
126
+ states.delete(sessionId);
127
+ }
128
+
129
+ // Resolve a session identifier to its server-side storeId. ctx.sessionId may
130
+ // be the client `sess_xxx` ID (assigned at Session construction) for a fresh
131
+ // session that hasn't yet received a server-assigned storeId — but the logs
132
+ // API endpoints key on storeId. Returns null if the session has no storeId
133
+ // yet (fresh, no message sent), meaning there are no server-side logs.
134
+ function resolveStoreId(maybeId) {
135
+ if (!maybeId) return null;
136
+ const sessions = window.app?.sessionManager?.sessions || [];
137
+ const session = sessions.find(s => s.id === maybeId || s.storeId === maybeId);
138
+ if (session) return session.storeId || null;
139
+ // Unknown to the session manager — could be a historical session ID
140
+ // opened directly. Client IDs are sess_-prefixed; anything else is treated
141
+ // as already a storeId.
142
+ if (maybeId.startsWith('sess_')) return null;
143
+ return maybeId;
144
+ }
145
+
146
+ // ─────────────────────────────────────────────────────────────────────
147
+ // Widget Registration
148
+ // ─────────────────────────────────────────────────────────────────────
149
+
150
+ export function registerLogExplorerWidget() {
151
+ WidgetManager.register('log-explorer', {
152
+ id: 'log-explorer',
153
+ type: 'bottom-sheet',
154
+ title: S.widgets.titles.logs,
155
+ icon: LOG_ICONS.logs,
156
+ shortcut: 'Alt+L',
157
+
158
+ heights: { half: '45vh', full: '85vh' },
159
+ sessionAware: true,
160
+
161
+ render: renderLogExplorer,
162
+ onOpen: handleOpen,
163
+ onClose: handleClose,
164
+
165
+ headerActions: [
166
+ {
167
+ id: 'sort',
168
+ icon: LOG_ICONS.sortDesc,
169
+ title: S.widgets.header_actions.sort_order,
170
+ onClick: toggleSortOrder
171
+ },
172
+ {
173
+ id: 'refresh',
174
+ icon: LOG_ICONS.refresh,
175
+ title: S.widgets.header_actions.refresh,
176
+ onClick: refresh
177
+ },
178
+ {
179
+ id: 'external',
180
+ icon: LOG_ICONS.external,
181
+ title: S.widgets.header_actions.open_viewer,
182
+ onClick: openExternal
183
+ }
184
+ ],
185
+
186
+ onDestroy: (sessionId) => {
187
+ destroyState(sessionId);
188
+ }
189
+ });
190
+ }
191
+
192
+ // ─────────────────────────────────────────────────────────────────────
193
+ // Lifecycle Handlers
194
+ // ─────────────────────────────────────────────────────────────────────
195
+
196
+ function handleOpen() {
197
+ const state = getState();
198
+ // Always re-resolve: storeId may have been assigned since last open
199
+ // (fresh session that has now sent its first message).
200
+ const sessionKey = state.sessionId || WidgetManager.currentSessionId;
201
+ state.sessionId = resolveStoreId(sessionKey);
202
+ if (state.sessionId) {
203
+ loadOverview();
204
+ } else {
205
+ showEmpty(S.widgets.log_explorer.no_logs_yet);
206
+ }
207
+ }
208
+
209
+ function handleClose() {
210
+ // Nothing special needed on close
211
+ }
212
+
213
+ // ─────────────────────────────────────────────────────────────────────
214
+ // Main Render Function
215
+ // ─────────────────────────────────────────────────────────────────────
216
+
217
+ function renderLogExplorer(container, ctx) {
218
+ const state = getState(ctx.sessionId);
219
+ state.container = container;
220
+ if (ctx.sessionId) state.sessionId = resolveStoreId(ctx.sessionId);
221
+
222
+ container.innerHTML = `
223
+ <div class="log-explorer-inner">
224
+ <div class="logs-header">
225
+ <div class="logs-session-info">
226
+ <span class="logs-session-name">Loading...</span>
227
+ </div>
228
+ </div>
229
+ <div class="logs-tabs">
230
+ <button class="logs-tab active" data-tab="messages">Messages</button>
231
+ <button class="logs-tab" data-tab="raw">Raw</button>
232
+ <button class="logs-tab" data-tab="tools">Tools</button>
233
+ <button class="logs-tab" data-tab="system">System</button>
234
+ </div>
235
+ <div class="logs-body">
236
+ <div class="logs-loading" hidden>Loading...</div>
237
+ <div class="logs-empty" hidden>No data</div>
238
+ <div class="logs-error" hidden></div>
239
+ <div class="logs-content"></div>
240
+ <button class="logs-load-more" hidden>Load more</button>
241
+ </div>
242
+ </div>
243
+ `;
244
+
245
+ // Cache DOM references
246
+ state.sessionInfoEl = container.querySelector('.logs-session-info');
247
+ state.tabsEl = container.querySelector('.logs-tabs');
248
+ state.contentEl = container.querySelector('.logs-content');
249
+ state.loadingEl = container.querySelector('.logs-loading');
250
+ state.emptyEl = container.querySelector('.logs-empty');
251
+ state.errorEl = container.querySelector('.logs-error');
252
+ state.loadMoreBtn = container.querySelector('.logs-load-more');
253
+
254
+ // Event listeners
255
+ setupEventListeners();
256
+
257
+ // Load data if we have a session
258
+ if (state.sessionId) {
259
+ loadOverview();
260
+ } else if (ctx.sessionId) {
261
+ // Session exists but has no storeId yet (fresh, no message sent)
262
+ showEmpty(S.widgets.log_explorer.no_logs_yet);
263
+ }
264
+ }
265
+
266
+ function setupEventListeners() {
267
+ const state = getState();
268
+ // Tab switching
269
+ state.tabsEl?.addEventListener('click', (e) => {
270
+ const tab = e.target.closest('[data-tab]');
271
+ if (tab) {
272
+ switchTab(tab.dataset.tab);
273
+ }
274
+ });
275
+
276
+ // Load more
277
+ state.loadMoreBtn?.addEventListener('click', loadMore);
278
+
279
+ // Delegated click handlers for content
280
+ state.contentEl?.addEventListener('click', handleContentClick);
281
+ }
282
+
283
+ function handleContentClick(e) {
284
+ const state = getState();
285
+
286
+ // Copy a raw entry to the clipboard (sits inside the header, so intercept
287
+ // before the expand/collapse handler swallows the click).
288
+ const copyBtn = e.target.closest('.log-raw-copy');
289
+ if (copyBtn) {
290
+ e.stopPropagation();
291
+ e.preventDefault();
292
+ let text = '';
293
+ try {
294
+ text = decodeURIComponent(escape(atob(copyBtn.dataset.copy || '')));
295
+ } catch { text = ''; }
296
+ copyToClipboard(text).then(ok => {
297
+ if (ok) {
298
+ copyBtn.classList.add('copied');
299
+ copyBtn.innerHTML = LOG_ICONS.check;
300
+ showToast(S.toast.copied);
301
+ setTimeout(() => {
302
+ copyBtn.classList.remove('copied');
303
+ copyBtn.innerHTML = LOG_ICONS.copy;
304
+ }, 1200);
305
+ }
306
+ });
307
+ return;
308
+ }
309
+
310
+ // Log item expand/collapse
311
+ const expandable = e.target.closest('.log-item-expandable');
312
+ if (expandable && !e.target.closest('.json-toggle') && !e.target.closest('.json-parse-btn')) {
313
+ if (e.target.closest('.log-item-header')) {
314
+ expandable.classList.toggle('expanded');
315
+ }
316
+ }
317
+
318
+ // JSON tree node toggle
319
+ const jsonToggle = e.target.closest('.json-toggle');
320
+ if (jsonToggle) {
321
+ e.stopPropagation();
322
+ const collapsible = jsonToggle.closest('.json-collapsible');
323
+ if (collapsible) {
324
+ collapsible.classList.toggle('expanded');
325
+ }
326
+ }
327
+
328
+ // Nested JSON parse button
329
+ const parseBtn = e.target.closest('.json-parse-btn');
330
+ if (parseBtn) {
331
+ e.stopPropagation();
332
+ const container = parseBtn.closest('.json-nested-container');
333
+ if (container) {
334
+ const nestedContent = container.querySelector('.json-nested-content');
335
+ const preview = container.querySelector('.json-string-preview');
336
+ if (nestedContent) {
337
+ const isHidden = nestedContent.hidden;
338
+ nestedContent.hidden = !isHidden;
339
+ parseBtn.classList.toggle('active', isHidden);
340
+ if (preview) preview.hidden = isHidden;
341
+ }
342
+ }
343
+ }
344
+
345
+ // Tool file link
346
+ const toolLink = e.target.closest('[data-tool-file]');
347
+ if (toolLink) {
348
+ e.preventDefault();
349
+ showToolOutput(toolLink.dataset.toolFile);
350
+ }
351
+
352
+ // Filter button
353
+ const filterBtn = e.target.closest('[data-filter]');
354
+ if (filterBtn) {
355
+ const filter = filterBtn.dataset.filter;
356
+ if (filter === 'errors') {
357
+ state.errorsOnly = !state.errorsOnly;
358
+ loadRaw(false);
359
+ }
360
+ }
361
+ }
362
+
363
+ // ─────────────────────────────────────────────────────────────────────
364
+ // Data Loading
365
+ // ─────────────────────────────────────────────────────────────────────
366
+
367
+ async function loadOverview() {
368
+ const state = getState();
369
+ if (!state.sessionId) return;
370
+
371
+ showLoading();
372
+
373
+ try {
374
+ const response = await fetch(`${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs`);
375
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
376
+
377
+ const data = await response.json();
378
+ state.sessionMeta = data.meta;
379
+ updateSessionInfo(data);
380
+ updateTabCounts(data.files);
381
+
382
+ await loadCurrentTab();
383
+ } catch (err) {
384
+ showError(`Failed to load logs: ${err.message}`);
385
+ }
386
+ }
387
+
388
+ async function loadCurrentTab() {
389
+ const state = getState();
390
+ switch (state.currentTab) {
391
+ case 'messages':
392
+ await loadMessages(false);
393
+ break;
394
+ case 'raw':
395
+ await loadRaw(false);
396
+ break;
397
+ case 'tools':
398
+ await loadTools();
399
+ break;
400
+ case 'system':
401
+ renderSystemLogs();
402
+ break;
403
+ }
404
+ }
405
+
406
+ async function loadMessages(append = false) {
407
+ const state = getState();
408
+ if (!state.sessionId) return;
409
+ if (!append) {
410
+ state.messagesOffset = 0;
411
+ showLoading();
412
+ }
413
+
414
+ try {
415
+ let url = `${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/messages?offset=${state.messagesOffset}&limit=50`;
416
+ url += `&sort=${state.sortOrder}`;
417
+ if (state.roleFilter) {
418
+ url += `&role=${state.roleFilter}`;
419
+ }
420
+
421
+ const response = await fetch(url);
422
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
423
+
424
+ const data = await response.json();
425
+ state.hasMoreMessages = data.has_more;
426
+ state.messagesOffset = data.offset + data.messages.length;
427
+
428
+ if (append) {
429
+ appendMessages(data.messages);
430
+ } else {
431
+ renderMessages(data.messages, data.total);
432
+ }
433
+
434
+ updateLoadMore(state.hasMoreMessages);
435
+ } catch (err) {
436
+ showError(`Failed to load messages: ${err.message}`);
437
+ }
438
+ }
439
+
440
+ async function loadRaw(append = false) {
441
+ const state = getState();
442
+ if (!state.sessionId) return;
443
+ if (!append) {
444
+ state.rawOffset = 0;
445
+ showLoading();
446
+ }
447
+
448
+ try {
449
+ let url = `${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/raw?offset=${state.rawOffset}&limit=100`;
450
+ url += `&sort=${state.sortOrder}`;
451
+ if (state.dirFilter) {
452
+ url += `&direction=${state.dirFilter}`;
453
+ }
454
+ if (state.errorsOnly) {
455
+ url += `&errors_only=true`;
456
+ }
457
+
458
+ const response = await fetch(url);
459
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
460
+
461
+ const data = await response.json();
462
+ state.hasMoreRaw = data.has_more;
463
+ state.rawOffset = data.offset + data.entries.length;
464
+
465
+ if (append) {
466
+ appendRawEntries(data.entries);
467
+ } else {
468
+ renderRaw(data.entries, data.total);
469
+ }
470
+
471
+ updateLoadMore(state.hasMoreRaw);
472
+ } catch (err) {
473
+ showError(`Failed to load raw log: ${err.message}`);
474
+ }
475
+ }
476
+
477
+ async function loadTools() {
478
+ const state = getState();
479
+ if (!state.sessionId) return;
480
+ showLoading();
481
+
482
+ try {
483
+ const response = await fetch(`${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/tools`);
484
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
485
+
486
+ const data = await response.json();
487
+ renderTools(data.files);
488
+ updateLoadMore(false);
489
+ } catch (err) {
490
+ showError(`Failed to load tools: ${err.message}`);
491
+ }
492
+ }
493
+
494
+ async function showToolOutput(filename) {
495
+ const state = getState();
496
+ if (!state.sessionId) return;
497
+
498
+ try {
499
+ const response = await fetch(`${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/tools/${encodeURIComponent(filename)}`);
500
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
501
+
502
+ const data = await response.json();
503
+ const content = escapeHtml(data.content);
504
+
505
+ state.contentEl.innerHTML = `
506
+ <div class="log-tool-preview">
507
+ <div class="log-tool-preview-header">
508
+ <button class="log-tool-preview-back" data-tooltip="Back to list">
509
+ ${LOG_ICONS.input}
510
+ </button>
511
+ <span class="log-tool-preview-name">${escapeHtml(filename)}</span>
512
+ <span class="log-tool-preview-size">${formatSize(data.size)}</span>
513
+ </div>
514
+ <pre class="log-tool-preview-content"><code>${content}</code></pre>
515
+ </div>
516
+ `;
517
+
518
+ state.contentEl.querySelector('.log-tool-preview-back')?.addEventListener('click', () => {
519
+ loadTools();
520
+ });
521
+ } catch (err) {
522
+ showError(`Failed to load tool output: ${err.message}`);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Render system logs from the active session (client-side only)
528
+ * System logs are stored in-memory per session (connection events, errors, etc.)
529
+ */
530
+ function renderSystemLogs() {
531
+ const state = getState();
532
+ // Get system logs from the session matching our state.sessionId
533
+ const session = window.app?.sessionManager?.sessions?.find(s => s.storeId === state.sessionId);
534
+ const logs = session?.systemLogs || [];
535
+
536
+ if (logs.length === 0) {
537
+ showEmpty('No system events');
538
+ updateLoadMore(false);
539
+ return;
540
+ }
541
+
542
+ // Sort according to current sort order
543
+ const sortedLogs = [...logs].sort((a, b) => {
544
+ const aTime = new Date(a.timestamp).getTime();
545
+ const bTime = new Date(b.timestamp).getTime();
546
+ return state.sortOrder === 'desc' ? bTime - aTime : aTime - bTime;
547
+ });
548
+
549
+ const html = sortedLogs.map(entry => {
550
+ const time = formatTimePrecise(entry.timestamp);
551
+ const typeClass = entry.type || 'info';
552
+ const typeIcon = typeClass === 'error' ? LOG_ICONS.error :
553
+ typeClass === 'warning' ? LOG_ICONS.event :
554
+ LOG_ICONS.event;
555
+
556
+ return `
557
+ <div class="log-item log-system-item ${typeClass}">
558
+ <div class="log-item-header">
559
+ <span class="log-badge ${typeClass}">
560
+ ${typeIcon}
561
+ <span class="log-badge-label">${typeClass}</span>
562
+ </span>
563
+ <span class="log-time">${time}</span>
564
+ </div>
565
+ <div class="log-system-text">${escapeHtml(entry.text)}</div>
566
+ </div>
567
+ `;
568
+ }).join('');
569
+
570
+ state.contentEl.innerHTML = html;
571
+ state.loadingEl.hidden = true;
572
+ state.emptyEl.hidden = true;
573
+ updateLoadMore(false);
574
+ }
575
+
576
+ function loadMore() {
577
+ const state = getState();
578
+ if (state.currentTab === 'messages' && state.hasMoreMessages) {
579
+ loadMessages(true);
580
+ } else if (state.currentTab === 'raw' && state.hasMoreRaw) {
581
+ loadRaw(true);
582
+ }
583
+ }
584
+
585
+ function refresh() {
586
+ const state = getState();
587
+ // Re-resolve in case storeId was assigned since the widget opened
588
+ // (fresh session that has now sent its first message).
589
+ const sessionKey = state.sessionId || WidgetManager.currentSessionId;
590
+ state.sessionId = resolveStoreId(sessionKey);
591
+ if (state.sessionId) {
592
+ loadOverview();
593
+ } else {
594
+ showEmpty(S.widgets.log_explorer.no_logs_yet);
595
+ }
596
+ }
597
+
598
+ // ─────────────────────────────────────────────────────────────────────
599
+ // UI Updates
600
+ // ─────────────────────────────────────────────────────────────────────
601
+
602
+ function updateSessionInfo(data) {
603
+ const state = getState();
604
+ if (!state.sessionInfoEl) return;
605
+
606
+ const meta = data.meta || {};
607
+ const cost = meta.total_cost ? `$${meta.total_cost.toFixed(4)}` : '';
608
+ const name = meta.name || 'Session';
609
+
610
+ state.sessionInfoEl.innerHTML = `
611
+ <span class="logs-session-name">${escapeHtml(name)}</span>
612
+ ${cost ? `<span class="logs-session-cost">${cost}</span>` : ''}
613
+ `;
614
+ }
615
+
616
+ function updateTabCounts(files) {
617
+ const state = getState();
618
+ if (!state.tabsEl) return;
619
+
620
+ const messagesTab = state.tabsEl.querySelector('[data-tab="messages"]');
621
+ const rawTab = state.tabsEl.querySelector('[data-tab="raw"]');
622
+ const toolsTab = state.tabsEl.querySelector('[data-tab="tools"]');
623
+ const systemTab = state.tabsEl.querySelector('[data-tab="system"]');
624
+
625
+ if (messagesTab) {
626
+ const count = files.messages?.lines || 0;
627
+ messagesTab.innerHTML = `Messages <span class="tab-count">${count}</span>`;
628
+ }
629
+ if (rawTab) {
630
+ const count = files.raw?.lines || 0;
631
+ rawTab.innerHTML = `Raw <span class="tab-count">${count}</span>`;
632
+ }
633
+ if (toolsTab) {
634
+ const count = files.tools?.count || 0;
635
+ toolsTab.innerHTML = `Tools <span class="tab-count">${count}</span>`;
636
+ }
637
+ if (systemTab) {
638
+ // System logs are client-side, get count from session
639
+ const session = window.app?.sessionManager?.sessions?.find(s => s.storeId === state.sessionId);
640
+ const count = session?.systemLogs?.length || 0;
641
+ systemTab.innerHTML = `System <span class="tab-count">${count}</span>`;
642
+ }
643
+ }
644
+
645
+ function switchTab(tab) {
646
+ const state = getState();
647
+ state.currentTab = tab;
648
+
649
+ // Update tab UI
650
+ state.tabsEl?.querySelectorAll('[data-tab]').forEach(t => {
651
+ t.classList.toggle('active', t.dataset.tab === tab);
652
+ });
653
+
654
+ loadCurrentTab();
655
+ }
656
+
657
+ function showLoading() {
658
+ const state = getState();
659
+ if (state.loadingEl) state.loadingEl.hidden = false;
660
+ if (state.contentEl) state.contentEl.innerHTML = '';
661
+ if (state.emptyEl) state.emptyEl.hidden = true;
662
+ if (state.errorEl) state.errorEl.hidden = true;
663
+ if (state.loadMoreBtn) state.loadMoreBtn.hidden = true;
664
+ }
665
+
666
+ function showError(message) {
667
+ const state = getState();
668
+ if (state.loadingEl) state.loadingEl.hidden = true;
669
+ if (state.emptyEl) state.emptyEl.hidden = true;
670
+ if (state.errorEl) {
671
+ state.errorEl.hidden = false;
672
+ state.errorEl.textContent = message;
673
+ }
674
+ }
675
+
676
+ function showEmpty(message) {
677
+ const state = getState();
678
+ if (state.loadingEl) state.loadingEl.hidden = true;
679
+ if (state.errorEl) state.errorEl.hidden = true;
680
+ if (state.contentEl) state.contentEl.innerHTML = '';
681
+ if (state.emptyEl) {
682
+ state.emptyEl.hidden = false;
683
+ state.emptyEl.textContent = message || 'No data';
684
+ }
685
+ if (state.loadMoreBtn) state.loadMoreBtn.hidden = true;
686
+ }
687
+
688
+ function updateLoadMore(hasMore) {
689
+ const state = getState();
690
+ if (state.loadMoreBtn) state.loadMoreBtn.hidden = !hasMore;
691
+ }
692
+
693
+ function toggleSortOrder() {
694
+ const state = getState();
695
+ state.sortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc';
696
+ updateSortButton();
697
+ loadCurrentTab();
698
+ }
699
+
700
+ function updateSortButton() {
701
+ const state = getState();
702
+ const widget = WidgetManager.get('log-explorer');
703
+ if (!widget) return;
704
+
705
+ const sortAction = widget.headerActions?.find(a => a.id === 'sort');
706
+ if (sortAction) {
707
+ const icon = state.sortOrder === 'desc' ? LOG_ICONS.sortDesc : LOG_ICONS.sortAsc;
708
+ const title = state.sortOrder === 'desc' ? 'Newest first (click to reverse)' : 'Oldest first (click to reverse)';
709
+ // Update button in widget header
710
+ const btn = widget.container?.querySelector('[data-action="sort"]');
711
+ if (btn) {
712
+ btn.innerHTML = icon;
713
+ btn.setAttribute('data-tooltip', title);
714
+ }
715
+ }
716
+ }
717
+
718
+ function openExternal() {
719
+ const state = getState();
720
+ if (state.sessionId) {
721
+ const path = `sessions/${state.sessionId}/raw.jsonl`;
722
+ window.open(`/view?path=${encodeURIComponent(path)}`, '_blank');
723
+ }
724
+ }
725
+
726
+ // ─────────────────────────────────────────────────────────────────────
727
+ // Rendering: Messages Tab
728
+ // ─────────────────────────────────────────────────────────────────────
729
+
730
+ function renderMessages(messages, total) {
731
+ const state = getState();
732
+ if (state.loadingEl) state.loadingEl.hidden = true;
733
+ if (state.errorEl) state.errorEl.hidden = true;
734
+
735
+ if (messages.length === 0) {
736
+ if (state.emptyEl) state.emptyEl.hidden = false;
737
+ if (state.contentEl) state.contentEl.innerHTML = '';
738
+ return;
739
+ }
740
+
741
+ if (state.emptyEl) state.emptyEl.hidden = true;
742
+
743
+ const orderLabel = state.sortOrder === 'desc' ? 'newest first' : 'oldest first';
744
+
745
+ let html = `<div class="logs-summary">Showing ${messages.length} of ${total} messages (${orderLabel})</div>`;
746
+ html += '<div class="logs-messages-list">';
747
+
748
+ for (const msg of messages) {
749
+ html += renderMessage(msg);
750
+ }
751
+
752
+ html += '</div>';
753
+ if (state.contentEl) state.contentEl.innerHTML = html;
754
+ }
755
+
756
+ function appendMessages(messages) {
757
+ const state = getState();
758
+ const list = state.contentEl?.querySelector('.logs-messages-list');
759
+ if (!list) return;
760
+
761
+ for (const msg of messages) {
762
+ list.insertAdjacentHTML('beforeend', renderMessage(msg));
763
+ }
764
+ }
765
+
766
+ // Label for assistant rows: the session's own ENGINE ("Codex"), title-cased
767
+ // to match the other role labels. A session the manager doesn't know (a
768
+ // historical ID opened directly) has no engine to read, so it keeps the
769
+ // static strings.yaml label rather than guessing the box default.
770
+ function assistantLabel() {
771
+ const id = getState().sessionId || WidgetManager.currentSessionId;
772
+ const session = (window.app?.sessionManager?.sessions || [])
773
+ .find(s => s.id === id || s.storeId === id);
774
+ if (!session) return ROLE_CONFIG.assistant.label;
775
+ const label = engineAuthorLabel(session);
776
+ return escapeHtml(label.charAt(0).toUpperCase() + label.slice(1));
777
+ }
778
+
779
+ function renderMessage(msg) {
780
+ const role = msg.role || 'unknown';
781
+ const base = ROLE_CONFIG[role] || { icon: LOG_ICONS.logs, color: '#888', label: role };
782
+ // Copy, never mutate — ROLE_CONFIG is module-level and shared by every
783
+ // session's log view.
784
+ const config = role === 'assistant' ? { ...base, label: assistantLabel() } : base;
785
+ const timestamp = formatTimePrecise(msg.timestamp);
786
+
787
+ let content = '';
788
+ let extra = '';
789
+
790
+ switch (role) {
791
+ case 'user':
792
+ const userContent = escapeHtml(msg.content || '').slice(0, 200);
793
+ content = isThinkingKeywordsHighlightingEnabled() ? highlightThinkingKeywords(userContent) : userContent;
794
+ if (msg.has_images) {
795
+ extra = `<span class="log-badge">${msg.image_count} image${msg.image_count > 1 ? 's' : ''}</span>`;
796
+ }
797
+ break;
798
+
799
+ case 'assistant':
800
+ content = escapeHtml(msg.content || '').slice(0, 200);
801
+ break;
802
+
803
+ case 'tool':
804
+ content = `<span class="log-tool-name">${escapeHtml(msg.tool_name || 'Tool')}</span>`;
805
+ if (msg.tool_output_file) {
806
+ extra = `<a href="#" data-tool-file="${escapeHtml(msg.tool_output_file)}" class="log-tool-link">View output</a>`;
807
+ }
808
+ break;
809
+
810
+ case 'thinking':
811
+ const toolCount = (msg.tools || []).length;
812
+ content = `Thinking (${toolCount} tool${toolCount !== 1 ? 's' : ''})`;
813
+ break;
814
+
815
+ case 'result':
816
+ const cost = msg.cost_usd ? `$${msg.cost_usd.toFixed(4)}` : '';
817
+ const duration = msg.duration_ms ? `${(msg.duration_ms / 1000).toFixed(1)}s` : '';
818
+ const turns = msg.num_turns || 1;
819
+ content = `${turns} turn${turns !== 1 ? 's' : ''} | ${duration} | ${cost}`;
820
+ if (msg.is_error) {
821
+ extra = '<span class="log-badge error">Error</span>';
822
+ }
823
+ break;
824
+
825
+ default:
826
+ content = JSON.stringify(msg).slice(0, 100);
827
+ }
828
+
829
+ const isExpandable = role === 'tool' || role === 'thinking';
830
+
831
+ return `
832
+ <div class="log-item ${isExpandable ? 'log-item-expandable' : ''}" data-role="${role}">
833
+ <div class="log-item-header">
834
+ <span class="log-item-icon" style="color: ${config.color}">${config.icon}</span>
835
+ <span class="log-item-role">${config.label}</span>
836
+ <span class="log-item-time">${timestamp}</span>
837
+ ${extra}
838
+ ${isExpandable ? `<span class="log-item-chevron">${LOG_ICONS.chevron}</span>` : ''}
839
+ </div>
840
+ <div class="log-item-content">${content}</div>
841
+ ${isExpandable ? renderExpandedContent(msg) : ''}
842
+ </div>
843
+ `;
844
+ }
845
+
846
+ function renderExpandedContent(msg) {
847
+ if (msg.role === 'tool') {
848
+ const input = msg.tool_input ? JSON.stringify(msg.tool_input, null, 2) : '';
849
+ const output = msg.tool_output || '';
850
+ const error = msg.tool_error || '';
851
+
852
+ return `
853
+ <div class="log-item-expanded">
854
+ ${input ? `<div class="log-expanded-section"><div class="log-expanded-label">Input</div><pre>${escapeHtml(input)}</pre></div>` : ''}
855
+ ${output ? `<div class="log-expanded-section"><div class="log-expanded-label">Output</div><pre>${escapeHtml(output.slice(0, 1000))}${output.length > 1000 ? '...' : ''}</pre></div>` : ''}
856
+ ${error ? `<div class="log-expanded-section error"><div class="log-expanded-label">Error</div><pre>${escapeHtml(error)}</pre></div>` : ''}
857
+ </div>
858
+ `;
859
+ }
860
+
861
+ if (msg.role === 'thinking') {
862
+ const tools = msg.tools || [];
863
+ let toolsHtml = tools.map(t => `
864
+ <div class="log-thinking-tool">
865
+ <span class="log-tool-name">${escapeHtml(t.toolName || 'Tool')}</span>
866
+ ${t.toolOutput ? '<span class="log-badge">completed</span>' : '<span class="log-badge pending">pending</span>'}
867
+ </div>
868
+ `).join('');
869
+
870
+ return `
871
+ <div class="log-item-expanded">
872
+ <div class="log-expanded-section"><div class="log-expanded-label">Thinking</div><pre>${escapeHtml((msg.content || '').slice(0, 500))}${(msg.content || '').length > 500 ? '...' : ''}</pre></div>
873
+ ${toolsHtml ? `<div class="log-expanded-section"><div class="log-expanded-label">Tools</div>${toolsHtml}</div>` : ''}
874
+ </div>
875
+ `;
876
+ }
877
+
878
+ return '';
879
+ }
880
+
881
+ // ─────────────────────────────────────────────────────────────────────
882
+ // Rendering: Raw Tab
883
+ // ─────────────────────────────────────────────────────────────────────
884
+
885
+ function renderRaw(entries, total) {
886
+ const state = getState();
887
+ if (state.loadingEl) state.loadingEl.hidden = true;
888
+ if (state.errorEl) state.errorEl.hidden = true;
889
+
890
+ if (entries.length === 0) {
891
+ if (state.emptyEl) state.emptyEl.hidden = false;
892
+ if (state.contentEl) state.contentEl.innerHTML = '';
893
+ return;
894
+ }
895
+
896
+ if (state.emptyEl) state.emptyEl.hidden = true;
897
+
898
+ const orderLabel = state.sortOrder === 'desc' ? 'newest first' : 'oldest first';
899
+
900
+ let html = `
901
+ <div class="logs-summary">
902
+ Showing ${entries.length} of ${total} entries (${orderLabel})
903
+ <div class="logs-filters">
904
+ <button class="log-filter ${state.errorsOnly ? 'active' : ''}" data-filter="errors">Errors only</button>
905
+ </div>
906
+ </div>
907
+ `;
908
+ html += '<div class="logs-raw-list">';
909
+
910
+ for (const entry of entries) {
911
+ html += renderRawEntry(entry);
912
+ }
913
+
914
+ html += '</div>';
915
+ if (state.contentEl) state.contentEl.innerHTML = html;
916
+ }
917
+
918
+ function appendRawEntries(entries) {
919
+ const state = getState();
920
+ const list = state.contentEl?.querySelector('.logs-raw-list');
921
+ if (!list) return;
922
+
923
+ for (const entry of entries) {
924
+ list.insertAdjacentHTML('beforeend', renderRawEntry(entry));
925
+ }
926
+ }
927
+
928
+ function renderRawEntry(entry) {
929
+ const dir = entry.dir || 'unknown';
930
+ const config = DIR_CONFIG[dir] || { icon: LOG_ICONS.logs, color: '#888', label: dir };
931
+ const timestamp = formatTimePrecise(entry.ts);
932
+
933
+ let content = '';
934
+ if (dir === 'event') {
935
+ content = escapeHtml(entry.event || '');
936
+ } else if (dir === 'error') {
937
+ content = `<span class="log-error-text">${escapeHtml(entry.error || '')}</span>`;
938
+ if (entry.context) {
939
+ content += `<pre class="log-error-context">${escapeHtml(entry.context.slice(0, 500))}</pre>`;
940
+ }
941
+ } else if (dir === 'in') {
942
+ content = `Input: ${entry.size || 0} bytes`;
943
+ } else if (dir === 'out') {
944
+ const type = entry.type || '';
945
+ const subtype = entry.subtype ? ` (${entry.subtype})` : '';
946
+ content = `<span class="log-raw-type">${type}${subtype}</span>`;
947
+ if (entry.truncated) {
948
+ content += ' <span class="log-badge">truncated</span>';
949
+ }
950
+ }
951
+
952
+ const isExpandable = dir === 'out' && entry.data;
953
+ let expandedContent = '';
954
+ if (isExpandable) {
955
+ expandedContent = renderJsonData(entry.data);
956
+ }
957
+
958
+ // Copy payload: the raw `out` JSON string verbatim when present (so what you
959
+ // paste matches the on-disk raw.jsonl entry), otherwise the whole entry
960
+ // pretty-printed. Base64 to survive quotes/newlines in a data attribute
961
+ // (unicode-safe, same idiom as tool-renderer-blocks.js).
962
+ const copyText = (dir === 'out' && entry.data)
963
+ ? entry.data
964
+ : JSON.stringify(entry, null, 2);
965
+ const copyEncoded = btoa(unescape(encodeURIComponent(copyText)));
966
+
967
+ return `
968
+ <div class="log-raw-item ${isExpandable ? 'log-item-expandable' : ''}" data-dir="${dir}">
969
+ <div class="log-item-header">
970
+ <span class="log-item-icon" style="color: ${config.color}">${config.icon}</span>
971
+ <span class="log-item-role">${config.label}</span>
972
+ <span class="log-item-time">${timestamp}</span>
973
+ <button class="log-raw-copy" data-copy="${copyEncoded}" data-tooltip="${S.widgets.log_explorer.copy_entry}" aria-label="${S.widgets.log_explorer.copy_entry}">${LOG_ICONS.copy}</button>
974
+ ${isExpandable ? `<span class="log-item-chevron">${LOG_ICONS.chevron}</span>` : ''}
975
+ </div>
976
+ <div class="log-item-content">${content}</div>
977
+ ${isExpandable ? `<div class="log-item-expanded">${expandedContent}</div>` : ''}
978
+ </div>
979
+ `;
980
+ }
981
+
982
+ // ─────────────────────────────────────────────────────────────────────
983
+ // Rendering: Tools Tab
984
+ // ─────────────────────────────────────────────────────────────────────
985
+
986
+ function renderTools(files) {
987
+ const state = getState();
988
+ if (state.loadingEl) state.loadingEl.hidden = true;
989
+ if (state.errorEl) state.errorEl.hidden = true;
990
+
991
+ if (files.length === 0) {
992
+ if (state.emptyEl) {
993
+ state.emptyEl.hidden = false;
994
+ state.emptyEl.textContent = 'No tool output files';
995
+ }
996
+ if (state.contentEl) state.contentEl.innerHTML = '';
997
+ return;
998
+ }
999
+
1000
+ if (state.emptyEl) state.emptyEl.hidden = true;
1001
+
1002
+ let html = '<div class="logs-tools-list">';
1003
+
1004
+ for (const file of files) {
1005
+ html += `
1006
+ <div class="log-tool-item" data-tool-file="${escapeHtml(file.name)}">
1007
+ <div class="log-tool-item-icon">${LOG_ICONS.tool}</div>
1008
+ <div class="log-tool-item-info">
1009
+ <div class="log-tool-item-name">${escapeHtml(file.tool_name || file.name)}</div>
1010
+ <div class="log-tool-item-meta">
1011
+ <span>${formatSize(file.size)}</span>
1012
+ <span>${formatTimePrecise(file.modified)}</span>
1013
+ </div>
1014
+ </div>
1015
+ <div class="log-tool-item-chevron">${LOG_ICONS.chevron}</div>
1016
+ </div>
1017
+ `;
1018
+ }
1019
+
1020
+ html += '</div>';
1021
+ if (state.contentEl) state.contentEl.innerHTML = html;
1022
+ }
1023
+
1024
+ // ─────────────────────────────────────────────────────────────────────
1025
+ // JSON Tree Rendering (shared module)
1026
+ // ─────────────────────────────────────────────────────────────────────
1027
+
1028
+ function renderJsonData(dataStr) {
1029
+ if (!dataStr) return '';
1030
+ try {
1031
+ return renderJsonTree(JSON.parse(dataStr));
1032
+ } catch (e) {
1033
+ const truncated = dataStr.length > 2000;
1034
+ const display = truncated ? dataStr.slice(0, 2000) + '...' : dataStr;
1035
+ return `<pre class="json-raw">${escapeHtml(display)}</pre>`;
1036
+ }
1037
+ }
1038
+
1039
+ // ─────────────────────────────────────────────────────────────────────
1040
+ // Public API
1041
+ // ─────────────────────────────────────────────────────────────────────
1042
+
1043
+ export function openLogExplorer(sessionId = null) {
1044
+ if (sessionId) {
1045
+ const state = getState(sessionId);
1046
+ state.sessionId = sessionId;
1047
+ }
1048
+ const widget = WidgetManager.get('log-explorer');
1049
+ widget?.open();
1050
+ }
1051
+
1052
+ export function closeLogExplorer() {
1053
+ const widget = WidgetManager.get('log-explorer');
1054
+ widget?.close();
1055
+ }
1056
+
1057
+ export { destroyState as destroyLogExplorerState };