@bitseek/hermes-webui 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. package/vendor/agent-frontend-shell/static/workspace.js +680 -0
@@ -0,0 +1,680 @@
1
+ async function api(path,opts={}){
2
+ // Strip leading slash so URL resolves relative to location.href (supports subpath mounts)
3
+ const rel = path.startsWith('/') ? path.slice(1) : path;
4
+ const url=new URL(rel,document.baseURI||location.href);
5
+ const timeoutMs=Object.prototype.hasOwnProperty.call(opts,'timeoutMs')?opts.timeoutMs:30000;
6
+ const timeoutToast=opts.timeoutToast!==false;
7
+ // Retry up to 2 times on network errors (e.g. stale keep-alive after long idle).
8
+ // Server errors (4xx/5xx) and client-side timeouts are NOT retried.
9
+ let lastErr;
10
+ for(let attempt=0;attempt<3;attempt++){
11
+ let controller=null;
12
+ let timeoutId=null;
13
+ let didTimeout=false;
14
+ let upstreamSignal=null;
15
+ let upstreamAbort=null;
16
+ try{
17
+ const fetchOpts={...opts};
18
+ delete fetchOpts.timeoutMs;
19
+ delete fetchOpts.timeoutToast;
20
+
21
+ const useTimeout=Number.isFinite(Number(timeoutMs))&&Number(timeoutMs)>0;
22
+ if(useTimeout&&typeof AbortController!=='undefined'){
23
+ controller=new AbortController();
24
+ upstreamSignal=fetchOpts.signal||null;
25
+ if(upstreamSignal){
26
+ upstreamAbort=()=>controller.abort(upstreamSignal.reason);
27
+ if(upstreamSignal.aborted) upstreamAbort();
28
+ else upstreamSignal.addEventListener('abort',upstreamAbort,{once:true});
29
+ }
30
+ fetchOpts.signal=controller.signal;
31
+ }
32
+ const requestPromise=(async()=>{
33
+ const res=await fetch(url.href,{credentials:'include',headers:{'Content-Type':'application/json'},...fetchOpts});
34
+ if(!res.ok){
35
+ // 401 means the auth session expired. Redirect to login so the user can
36
+ // re-authenticate. This is especially important for iOS PWA (standalone mode)
37
+ // and for subpath mounts like /hermes/, where /login escapes to the site root.
38
+ if(res.status===401){window.location.href='login?next='+encodeURIComponent(window.location.pathname+window.location.search);return;}
39
+ const text=await res.text();
40
+ // Parse JSON error body and surface the human-readable message,
41
+ // rather than showing raw JSON like {"error":"Profile 'x' does not exist."}
42
+ let message=text;
43
+ try{const j=JSON.parse(text);message=j.error||j.message||text;}catch(e){}
44
+ // Attach the raw HTTP context so callers can branch on status (404 stale-session
45
+ // cleanup, 401 redirect, 503 retry, etc.) without re-parsing the message string.
46
+ const err=new Error(message);
47
+ err.status=res.status;
48
+ err.statusText=res.statusText;
49
+ err.body=text;
50
+ throw err;
51
+ }
52
+ const ct=res.headers.get('content-type')||'';
53
+ return ct.includes('application/json')?await res.json():await res.text();
54
+ })();
55
+ return useTimeout?await Promise.race([
56
+ requestPromise,
57
+ new Promise((_,reject)=>{
58
+ timeoutId=setTimeout(()=>{
59
+ didTimeout=true;
60
+ if(controller) controller.abort();
61
+ const err=new Error('Request timed out. Please try again.');
62
+ err.name='TimeoutError';
63
+ err.timeout=true;
64
+ reject(err);
65
+ },Number(timeoutMs));
66
+ })
67
+ ]):await requestPromise;
68
+ }catch(e){
69
+ lastErr=e;
70
+ const isTimeout=didTimeout||(e&&(e.timeout===true||e.name==='TimeoutError'));
71
+ if(isTimeout){
72
+ const err=(e&&e.name==='TimeoutError')?e:new Error('Request timed out. Please try again.');
73
+ err.name='TimeoutError';
74
+ err.timeout=true;
75
+ if(timeoutToast&&typeof showToast==='function') showToast('Request timed out. Please try again.',5000,'error');
76
+ throw err;
77
+ }
78
+ // Only retry on network errors (TypeError from fetch), not on HTTP errors
79
+ // that were already thrown above. Re-throw 401 redirects immediately.
80
+ if(e.message&&/401/.test(e.message)) throw e;
81
+ if(attempt<2 && e instanceof TypeError) continue;
82
+ throw e;
83
+ }finally{
84
+ if(timeoutId) clearTimeout(timeoutId);
85
+ if(upstreamSignal&&upstreamAbort) upstreamSignal.removeEventListener('abort',upstreamAbort);
86
+ }
87
+ }
88
+ throw lastErr;
89
+ }
90
+
91
+ function recordClientSSEError(source, details={}){
92
+ try{
93
+ const payload={
94
+ event:'sse_error',
95
+ source:String(source||'unknown'),
96
+ ready_state:details.ready_state,
97
+ session_id:details.session_id||null,
98
+ stream_id:details.stream_id||null,
99
+ visibility_state:(typeof document!=='undefined'&&document.visibilityState)||'unknown',
100
+ online:(typeof navigator!=='undefined'&&typeof navigator.onLine==='boolean')?navigator.onLine:null,
101
+ url_path:(typeof location!=='undefined'&&location.pathname)||'/',
102
+ reason:details.reason||'EventSource.onerror',
103
+ };
104
+ void api('/api/client-events/log',{method:'POST',body:JSON.stringify(payload),timeoutMs:3000,timeoutToast:false}).catch(()=>{});
105
+ }catch(_){}
106
+ }
107
+
108
+ // Persist/restore expanded directory state per workspace in localStorage
109
+ function _wsExpandKey(){
110
+ const ws=S.session&&S.session.workspace;
111
+ return ws?'hermes-webui-expanded:'+ws:null;
112
+ }
113
+ function _saveExpandedDirs(){
114
+ const key=_wsExpandKey();if(!key)return;
115
+ try{localStorage.setItem(key,JSON.stringify([...(S._expandedDirs||new Set())]));}catch(e){}
116
+ }
117
+ function _restoreExpandedDirs(){
118
+ const key=_wsExpandKey();
119
+ if(!key){S._expandedDirs=new Set();return;}
120
+ try{
121
+ const raw=localStorage.getItem(key);
122
+ S._expandedDirs=raw?new Set(JSON.parse(raw)):new Set();
123
+ }catch(e){S._expandedDirs=new Set();}
124
+ }
125
+
126
+ let _workspacePanelActiveTab = 'files';
127
+ let _renderSessionArtifactsTimer = null;
128
+
129
+ function _setWorkspacePanelTabDataset(){
130
+ const panel = document.querySelector('.rightpanel');
131
+ if(panel) panel.dataset.activeTab = _workspacePanelActiveTab;
132
+ }
133
+
134
+ function scheduleRenderSessionArtifacts(){
135
+ if(_renderSessionArtifactsTimer) clearTimeout(_renderSessionArtifactsTimer);
136
+ _renderSessionArtifactsTimer = setTimeout(()=>{
137
+ _renderSessionArtifactsTimer = null;
138
+ renderSessionArtifacts();
139
+ }, 100);
140
+ }
141
+
142
+ if(typeof document !== 'undefined'){
143
+ if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _setWorkspacePanelTabDataset, {once:true});
144
+ else _setWorkspacePanelTabDataset();
145
+ }
146
+
147
+ function switchWorkspacePanelTab(tab){
148
+ _workspacePanelActiveTab = tab === 'artifacts' ? 'artifacts' : 'files';
149
+ _setWorkspacePanelTabDataset();
150
+ const filesTab = $('workspaceFilesTab');
151
+ const artifactsTab = $('workspaceArtifactsTab');
152
+ if(filesTab){
153
+ filesTab.classList.toggle('active', _workspacePanelActiveTab === 'files');
154
+ filesTab.setAttribute('aria-selected', _workspacePanelActiveTab === 'files' ? 'true' : 'false');
155
+ }
156
+ if(artifactsTab){
157
+ artifactsTab.classList.toggle('active', _workspacePanelActiveTab === 'artifacts');
158
+ artifactsTab.setAttribute('aria-selected', _workspacePanelActiveTab === 'artifacts' ? 'true' : 'false');
159
+ }
160
+ const artifacts = $('workspaceArtifacts');
161
+ if(artifacts) artifacts.hidden = _workspacePanelActiveTab !== 'artifacts';
162
+ if(_workspacePanelActiveTab === 'artifacts') renderSessionArtifacts();
163
+ }
164
+
165
+ const ARTIFACT_IGNORE_RE = /(^|\/)(?:\.git|\.hg|\.svn|node_modules|\.venv|venv|__pycache__|dist|build|\.next|\.cache)(?:\/|$)/;
166
+ // Canonical Hermes mutators plus MCP filesystem aliases that can create/edit files.
167
+ const ARTIFACT_MUTATION_TOOLS = new Set(['write_file','patch','edit_file','create_file','mcp_filesystem_write_file','mcp_filesystem_edit_file']);
168
+
169
+ function _normalizeArtifactPath(path){
170
+ if(!path) return '';
171
+ path = String(path).trim().replace(/[\`"'<>),.;:]+$/g,'').replace(/^[\`"'(<]+/g,'');
172
+ if(!path || path.length > 240 || path.includes('://')) return '';
173
+ // Canonicalize workspace-relative prefixes so a file-tree open ("foo.md") and a
174
+ // tool arg recorded as "./foo.md" or "~/foo.md" compare equal for mutation
175
+ // tracking; otherwise an agent edit via a ./-prefixed path leaves the open
176
+ // preview stale (#3262 / pre-release regression-gate finding).
177
+ path = path.replace(/^~\//,'').replace(/^(?:\.\/)+/,'');
178
+ if(!path) return '';
179
+ if(ARTIFACT_IGNORE_RE.test(path)) return '';
180
+ if(!/[./]/.test(path)) return '';
181
+ return path;
182
+ }
183
+
184
+ function _artifactCandidatesFromText(text){
185
+ if(!text || typeof text !== 'string') return [];
186
+ const out = [];
187
+ const seen = new Set();
188
+ const add = (path) => {
189
+ path = _normalizeArtifactPath(path);
190
+ if(!path || seen.has(path)) return;
191
+ seen.add(path); out.push({path, kind:'diff'});
192
+ };
193
+ // Fallback text mining is intentionally narrow: only diff/patch fences imply
194
+ // the session changed a file. Prose mentions such as "edited package.json" are
195
+ // too noisy for an Artifacts list that should track write/edit outputs.
196
+ const fenced = /```(?:diff|patch)\s*\n[\s\S]*?```/gi;
197
+ let m;
198
+ while((m = fenced.exec(text))){
199
+ const block = m[0];
200
+ const fm = block.match(/(?:^|\n)(?:\+\+\+|---)\s+(?:[ab]\/)?([^\n\t]+)/);
201
+ if(fm) add(fm[1].trim());
202
+ }
203
+ return out;
204
+ }
205
+
206
+ function _artifactCandidatesFromToolCall(tc){
207
+ if(!tc) return [];
208
+ const name = String(tc.name || '').replace(/^functions\./,'');
209
+ const args = tc.arguments || tc.args || tc.input || {};
210
+ const result = tc.result || tc.output || tc.snippet || '';
211
+ const out = [];
212
+ const add = (path, source=name || 'tool') => {
213
+ path = _normalizeArtifactPath(path);
214
+ if(path) out.push({path, kind:source});
215
+ };
216
+ if(ARTIFACT_MUTATION_TOOLS.has(name) && args && typeof args === 'object'){
217
+ for(const key of ['path','file_path','source','destination']) add(args[key]);
218
+ if(Array.isArray(args.paths)) args.paths.forEach(p=>add(p));
219
+ if(Array.isArray(args.edits)) args.edits.forEach(e=>add(e&&e.path));
220
+ }
221
+ const resultText = typeof result === 'string' ? result : (result ? JSON.stringify(result) : '');
222
+ // Tool results may include unified diffs from patch-style tools; scan those
223
+ // narrowly after structured args so diff headers can still contribute paths.
224
+ for(const a of _artifactCandidatesFromText(resultText)) out.push(a);
225
+ if(!out.length && ARTIFACT_MUTATION_TOOLS.has(name)){
226
+ const argsText = typeof args === 'string' ? args : JSON.stringify(args || {});
227
+ for(const a of _artifactCandidatesFromText(argsText)) out.push(a);
228
+ }
229
+ return out;
230
+ }
231
+
232
+ const _turnMutatedPreviewPaths = new Set();
233
+
234
+ function resetTurnWorkspaceMutations(){
235
+ _turnMutatedPreviewPaths.clear();
236
+ }
237
+
238
+ function noteWorkspaceMutationsFromToolCall(tc){
239
+ for(const a of _artifactCandidatesFromToolCall(tc)){
240
+ const path=_normalizeArtifactPath(a.path);
241
+ if(path) _turnMutatedPreviewPaths.add(path);
242
+ }
243
+ }
244
+
245
+ function noteWorkspaceMutationsFromToolCalls(toolCalls){
246
+ if(!Array.isArray(toolCalls)) return;
247
+ for(const tc of toolCalls) noteWorkspaceMutationsFromToolCall(tc);
248
+ }
249
+
250
+ function _isOpenPreviewPathMutated(){
251
+ if(!_previewCurrentPath) return false;
252
+ const current=_normalizeArtifactPath(_previewCurrentPath);
253
+ return !!(current&&_turnMutatedPreviewPaths.has(current));
254
+ }
255
+
256
+ async function refreshOpenPreviewIfMutated(){
257
+ if(typeof _previewDirty!=='undefined'&&_previewDirty) return;
258
+ if(!_isOpenPreviewPathMutated()) return;
259
+ if(!_previewCurrentPath||!S.session) return;
260
+ await openFile(_previewCurrentPath, { bustCache: true });
261
+ }
262
+
263
+ function collectSessionArtifacts(){
264
+ const items = [];
265
+ const seen = new Set();
266
+ const push = (path, source) => {
267
+ path = _normalizeArtifactPath(path);
268
+ if(!path || seen.has(path)) return;
269
+ seen.add(path); items.push({path, source});
270
+ };
271
+ for(const tc of (S.toolCalls || [])){
272
+ for(const a of _artifactCandidatesFromToolCall(tc)) push(a.path, a.kind || tc.name || 'tool');
273
+ }
274
+ for(const msg of (S.messages || [])){
275
+ const text = msg && (msg.content || msg.text || msg.message || '');
276
+ for(const a of _artifactCandidatesFromText(text)) push(a.path, a.kind);
277
+ }
278
+ return items.slice(0, 50);
279
+ }
280
+
281
+ function renderSessionArtifacts(){
282
+ const root = $('workspaceArtifacts');
283
+ const count = $('workspaceArtifactsCount');
284
+ if(!root) return;
285
+ const items = collectSessionArtifacts();
286
+ if(count) count.textContent = String(items.length);
287
+ if(!S.session){
288
+ root.innerHTML = '<div class="workspace-artifact-empty">Open a conversation to see files changed in this session.</div>';
289
+ return;
290
+ }
291
+ if(!items.length){
292
+ root.innerHTML = '<div class="workspace-artifact-empty">No artifacts detected yet. Files created or edited during this session will appear here.</div>';
293
+ return;
294
+ }
295
+ root.innerHTML = items.map(item => `<button type="button" class="workspace-artifact-item" data-artifact-path="${esc(item.path)}" onclick="openArtifactPath(this.dataset.artifactPath)"><div class="workspace-artifact-path">${esc(item.path)}</div><div class="workspace-artifact-meta">${esc(item.source || 'session')}</div></button>`).join('');
296
+ }
297
+
298
+ async function _workspacePathExists(path){
299
+ if(!S.session||!path) return false;
300
+ const parts=String(path).split('/').filter(Boolean);
301
+ const name=parts.pop();
302
+ if(!name) return false;
303
+ const dir=parts.length?parts.join('/'):'.';
304
+ const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(dir)}`);
305
+ return (data.entries||[]).some(entry=>entry&&((entry.path===path)||entry.name===name));
306
+ }
307
+
308
+ async function openArtifactPath(path){
309
+ if(!path) return;
310
+ switchWorkspacePanelTab('files');
311
+ const rel = path.replace(/^~\//,'').replace(/^\.\//,'');
312
+ try{
313
+ if(!(await _workspacePathExists(rel))){
314
+ setStatus(t('file_open_failed'));
315
+ return;
316
+ }
317
+ }catch(_){
318
+ setStatus(t('file_open_failed'));
319
+ return;
320
+ }
321
+ openFile(rel);
322
+ }
323
+
324
+ async function loadDir(path, opts={}){
325
+ const preservePreview=!!(opts&&opts.preservePreview);
326
+ if(!S.session)return;
327
+ const sessionId=S.session.session_id;
328
+ try{
329
+ if(!path||path==='.'){
330
+ S._dirCache={};
331
+ _restoreExpandedDirs(); // restore per-workspace expanded state on root load
332
+ }
333
+ S.currentDir=path||'.';
334
+ const data=await api(`/api/list?session_id=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(path)}`);
335
+ if(!S.session||S.session.session_id!==sessionId)return;
336
+ S.entries=data.entries||[];renderBreadcrumb();renderFileTree();
337
+ // #2673 — refresh Artifacts tab when its source data (the file tree) updates.
338
+ if(typeof renderSessionArtifacts==='function') renderSessionArtifacts();
339
+ // Pre-fetch contents of restored expanded dirs so they render without a second click
340
+ // (parallelized — avoids serial waterfall when multiple dirs are expanded)
341
+ if(!path||path==='.'){
342
+ const expanded=S._expandedDirs||new Set();
343
+ const pending=[...expanded].filter(dirPath=>!S._dirCache[dirPath]);
344
+ if(pending.length){
345
+ const results=await Promise.all(pending.map(dirPath=>
346
+ api(`/api/list?session_id=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(dirPath)}`)
347
+ .then(dc=>({dirPath,entries:dc.entries||[]}))
348
+ .catch(()=>({dirPath,entries:[]}))
349
+ ));
350
+ if(!S.session||S.session.session_id!==sessionId)return;
351
+ for(const {dirPath,entries} of results) S._dirCache[dirPath]=entries;
352
+ }
353
+ if(expanded.size>0)renderFileTree();
354
+ }
355
+ if(!preservePreview&&typeof clearPreview==='function'){
356
+ if(typeof _previewDirty!=='undefined'&&_previewDirty){
357
+ showConfirmDialog({title:t('unsaved_confirm'),message:'',confirmLabel:'Discard',danger:true,focusCancel:true}).then(ok=>{if(ok)clearPreview({keepPanelOpen:true});});
358
+ }else{
359
+ clearPreview({keepPanelOpen:true});
360
+ }
361
+ }else if(preservePreview){
362
+ await refreshOpenPreviewIfMutated();
363
+ }
364
+ // Fetch git info for workspace root (non-blocking)
365
+ if(!path||path==='.') _refreshGitBadge();
366
+ }catch(e){console.warn('loadDir',e);}
367
+ }
368
+
369
+ async function _refreshGitBadge(){
370
+ const badge=$('gitBadge');
371
+ if(!badge||!S.session)return;
372
+ const sessionId=S.session.session_id;
373
+ try{
374
+ const data=await api(`/api/git-info?session_id=${encodeURIComponent(sessionId)}`);
375
+ if(!S.session||S.session.session_id!==sessionId)return;
376
+ if(data.git&&data.git.is_git){
377
+ const g=data.git;
378
+ let text=g.branch||'git';
379
+ if(g.dirty>0) text+=` \u00b7 ${g.dirty}\u2206`; // middot + delta
380
+ if(g.behind>0) text+=` \u2193${g.behind}`;
381
+ if(g.ahead>0) text+=` \u2191${g.ahead}`;
382
+ badge.textContent=text;
383
+ badge.className='git-badge'+(g.dirty>0?' dirty':'');
384
+ badge.style.display='';
385
+ } else {
386
+ badge.style.display='none';
387
+ badge.textContent='';
388
+ }
389
+ }catch(e){
390
+ if(!S.session||S.session.session_id!==sessionId)return;
391
+ badge.style.display='none';
392
+ }
393
+ }
394
+
395
+ function navigateUp(){
396
+ if(!S.session||S.currentDir==='.')return;
397
+ const parts=S.currentDir.split('/');
398
+ parts.pop();
399
+ loadDir(parts.length?parts.join('/'):'.');
400
+ }
401
+
402
+ // File extension sets for preview routing (must match server-side sets)
403
+ const IMAGE_EXTS = new Set(['.png','.jpg','.jpeg','.gif','.svg','.webp','.ico','.bmp']);
404
+ const MD_EXTS = new Set(['.md','.markdown','.mdown']);
405
+ const HTML_EXTS = new Set(['.html','.htm']);
406
+ const PDF_EXTS = new Set(['.pdf']);
407
+ const AUDIO_EXTS = new Set(['.mp3','.wav','.m4a','.aac','.ogg','.oga','.opus','.flac']);
408
+ const VIDEO_EXTS = new Set(['.mp4','.mov','.m4v','.webm','.ogv','.avi','.mkv']);
409
+ const MD_PREVIEW_RICH_RENDER_MAX_BYTES = 64 * 1024;
410
+ const MD_PREVIEW_RICH_RENDER_MAX_LINES = 1500;
411
+ // Binary formats that should download rather than preview
412
+ const DOWNLOAD_EXTS = new Set([
413
+ '.docx','.doc','.xlsx','.xls','.pptx','.ppt','.odt','.ods','.odp',
414
+ '.zip','.tar','.gz','.bz2','.7z','.rar',
415
+ '.exe','.dmg','.pkg','.deb','.rpm',
416
+ '.woff','.woff2','.ttf','.otf','.eot',
417
+ '.bin','.dat','.db','.sqlite','.pyc','.class','.so','.dylib','.dll',
418
+ ]);
419
+
420
+ function fileExt(p){ const i=p.lastIndexOf('.'); return i>=0?p.slice(i).toLowerCase():''; }
421
+
422
+ function markdownPreviewByteLength(content){
423
+ const text=String(content||'');
424
+ if(typeof Blob==='function') return new Blob([text]).size;
425
+ if(typeof TextEncoder==='function') return new TextEncoder().encode(text).length;
426
+ return unescape(encodeURIComponent(text)).length;
427
+ }
428
+
429
+ function markdownPreviewLineCount(content){
430
+ const text=String(content||'');
431
+ if(!text) return 1;
432
+ return text.split('\n').length;
433
+ }
434
+
435
+ function shouldRenderMarkdownPreviewAsPlainText(content){
436
+ return markdownPreviewByteLength(content)>MD_PREVIEW_RICH_RENDER_MAX_BYTES
437
+ || markdownPreviewLineCount(content)>MD_PREVIEW_RICH_RENDER_MAX_LINES;
438
+ }
439
+
440
+ function largeMarkdownPlainTextStatus(content){
441
+ const bytes=markdownPreviewByteLength(content);
442
+ const lines=markdownPreviewLineCount(content);
443
+ const sizeLabel=bytes>=1024?`${Math.round(bytes/1024)} KB`:`${bytes} B`;
444
+ return `Large markdown file (${sizeLabel}, ${lines} lines) shown as plain text. Click Edit to view raw.`;
445
+ }
446
+
447
+ let _previewCurrentPath = ''; // relative path of currently previewed file
448
+ let _previewCurrentMode = ''; // 'code' | 'md' | 'image' | 'html' | 'pdf' | 'audio' | 'video'
449
+ let _previewDirty = false; // true when edits are unsaved
450
+
451
+ function showPreview(mode){
452
+ // mode: 'code' | 'image' | 'md' | 'html' | 'pdf' | 'audio' | 'video'
453
+ $('previewCode').style.display = mode==='code' ? '' : 'none';
454
+ $('previewImgWrap').style.display = mode==='image' ? '' : 'none';
455
+ const mediaWrap=$('previewMediaWrap'); if(mediaWrap) mediaWrap.style.display = (mode==='audio'||mode==='video') ? '' : 'none';
456
+ const pdfWrap=$('previewPdfWrap'); if(pdfWrap) pdfWrap.style.display = mode==='pdf' ? '' : 'none';
457
+ $('previewMd').style.display = mode==='md' ? '' : 'none';
458
+ $('previewHtmlWrap').style.display = mode==='html' ? '' : 'none';
459
+ $('previewEditArea').style.display = 'none'; // start in read-only
460
+ const badge=$('previewBadge');
461
+ badge.className='preview-badge '+mode;
462
+ badge.textContent = mode==='image'?'image':mode==='audio'?'audio':mode==='video'?'video':mode==='pdf'?'pdf':mode==='md'?'md':mode==='html'?'html':fileExt($('previewPathText').textContent)||'text';
463
+ _previewCurrentMode = mode;
464
+ _previewDirty = false;
465
+ updateEditBtn();
466
+ // Show "Open in browser" button for iframe-backed document previews
467
+ const openBtn=$('btnOpenInBrowser');
468
+ if(openBtn) openBtn.style.display = (mode==='html'||mode==='pdf')?'inline-flex':'none';
469
+ }
470
+
471
+ function updateEditBtn(){
472
+ const btn=$('btnEditFile');
473
+ if(!btn)return;
474
+ const editable = _previewCurrentMode==='code'||_previewCurrentMode==='md';
475
+ btn.style.display = editable?'':'none';
476
+ const editing = $('previewEditArea').style.display!=='none';
477
+ btn.innerHTML = editing ? `&#128190; ${t('save')}` : `&#9998; ${t('edit')}`;
478
+ btn.title = editing ? t('save_title') : t('edit_title');
479
+ btn.style.color = editing ? 'var(--blue)' : '';
480
+ if(_previewDirty) btn.innerHTML = '&#128190; Save*';
481
+ }
482
+
483
+ async function toggleEditMode(){
484
+ const editing = $('previewEditArea').style.display!=='none';
485
+ if(editing){
486
+ // Save
487
+ if(!S.session||!_previewCurrentPath)return;
488
+ const content=$('previewEditArea').value;
489
+ try{
490
+ await api('/api/file/save',{method:'POST',body:JSON.stringify({
491
+ session_id:S.session.session_id, path:_previewCurrentPath, content
492
+ })});
493
+ _previewDirty=false;
494
+ // Update read-only views
495
+ if(_previewCurrentMode==='code') $('previewCode').textContent=content;
496
+ else { $('previewMd').innerHTML=renderMd(content); requestAnimationFrame(()=>{if(typeof renderKatexBlocks==='function')renderKatexBlocks();}); }
497
+ $('previewEditArea').style.display='none';
498
+ if(_previewCurrentMode==='code') $('previewCode').style.display='';
499
+ else $('previewMd').style.display='';
500
+ showToast(t('saved'));
501
+ }catch(e){setStatus(t('save_failed')+e.message);}
502
+ }else{
503
+ // Enter edit mode: populate textarea with current content
504
+ const currentText = _previewCurrentMode==='code'
505
+ ? $('previewCode').textContent
506
+ : _previewRawContent||'';
507
+ $('previewEditArea').value=currentText;
508
+ $('previewEditArea').style.display='';
509
+ if(_previewCurrentMode==='code') $('previewCode').style.display='none';
510
+ else $('previewMd').style.display='none';
511
+ // Escape cancels the edit without saving
512
+ $('previewEditArea').onkeydown=e=>{
513
+ if(e.key==='Escape'){e.preventDefault();cancelEditMode();}
514
+ };
515
+ }
516
+ updateEditBtn();
517
+ }
518
+
519
+ let _previewRawContent = ''; // raw text for md files (to populate editor)
520
+
521
+ function cancelEditMode(){
522
+ // Discard changes and return to read-only view
523
+ $('previewEditArea').style.display='none';
524
+ $('previewEditArea').onkeydown=null;
525
+ if(_previewCurrentMode==='code') $('previewCode').style.display='';
526
+ else $('previewMd').style.display='';
527
+ _previewDirty=false;
528
+ updateEditBtn();
529
+ }
530
+
531
+ async function openFile(path, opts={}){
532
+ if(!S.session)return;
533
+ const ext=fileExt(path);
534
+ const bustCache=!!(opts&&opts.bustCache);
535
+ const cacheBust=bustCache?`&_=${Date.now()}`:'';
536
+
537
+ // Binary/download-only formats: trigger browser download, don't preview
538
+ if(DOWNLOAD_EXTS.has(ext)){
539
+ downloadFile(path);
540
+ return;
541
+ }
542
+
543
+ $('previewPathText').textContent=path;
544
+ $('previewArea').classList.add('visible');
545
+ $('fileTree').style.display='none';
546
+
547
+ _previewCurrentPath = path;
548
+ renderFileBreadcrumb(path);
549
+ if(IMAGE_EXTS.has(ext)){
550
+ // Image: load via raw endpoint, show as <img>
551
+ showPreview('image');
552
+ const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}${cacheBust}`;
553
+ $('previewImg').alt=path;
554
+ $('previewImg').src=url;
555
+ $('previewImg').onerror=()=>setStatus(t('image_load_failed'));
556
+ } else if(AUDIO_EXTS.has(ext)||VIDEO_EXTS.has(ext)){
557
+ const mode=VIDEO_EXTS.has(ext)?'video':'audio';
558
+ showPreview(mode);
559
+ const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1${cacheBust}`;
560
+ const wrap=$('previewMediaWrap');
561
+ if(wrap){
562
+ wrap.innerHTML=(typeof _mediaPlayerHtml==='function')
563
+ ? _mediaPlayerHtml(mode,url,path.split('/').pop()||path)
564
+ : `<${mode} src="${url.replace(/"/g,'%22')}" controls preload="metadata"></${mode}>`;
565
+ if(typeof _applyMediaPlaybackPreferences==='function') _applyMediaPlaybackPreferences(wrap);
566
+ }
567
+ } else if(PDF_EXTS.has(ext)){
568
+ showPreview('pdf');
569
+ const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1${cacheBust}`;
570
+ const frame=$('previewPdfFrame');
571
+ if(frame){
572
+ frame.src=''; // clear first to avoid stale content
573
+ frame.src=url;
574
+ frame.title=`PDF preview: ${path.split('/').pop()||path}`;
575
+ }
576
+ } else if(MD_EXTS.has(ext)){
577
+ // Markdown: fetch text, render with renderMd, display as formatted HTML
578
+ try{
579
+ const data=await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
580
+ _previewRawContent = data.content;
581
+ if(shouldRenderMarkdownPreviewAsPlainText(data.content)){
582
+ showPreview('code');
583
+ $('previewCode').textContent=data.content;
584
+ setStatus(largeMarkdownPlainTextStatus(data.content));
585
+ return;
586
+ }
587
+ showPreview('md');
588
+ $('previewMd').innerHTML=renderMd(data.content);
589
+ requestAnimationFrame(()=>{if(typeof renderKatexBlocks==='function')renderKatexBlocks();});
590
+ }catch(e){setStatus(t('file_open_failed'));}
591
+ } else if(HTML_EXTS.has(ext)){
592
+ // HTML: render in sandboxed iframe via raw endpoint.
593
+ // SECURITY TRADEOFF: We use sandbox="allow-scripts" which lets inline JS run
594
+ // but prevents access to the parent frame (origin isolation). This is a
595
+ // deliberate choice — the user is previewing their own workspace files, so
596
+ // blocking scripts entirely would break most HTML documents. The sandbox
597
+ // still prevents the preview from navigating the parent, accessing cookies,
598
+ // or reading other origin data. If a stricter mode is needed, remove
599
+ // allow-scripts (or add sandbox="") to disable all JS execution.
600
+ showPreview('html');
601
+ const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1${cacheBust}`;
602
+ const iframe=$('previewHtmlIframe');
603
+ if(iframe){
604
+ iframe.src=''; // clear first to avoid stale content
605
+ iframe.src=url;
606
+ }
607
+ } else {
608
+ // Plain code / text -- but fall back to download if server signals binary
609
+ try{
610
+ const data=await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
611
+ if(data.binary){
612
+ // Server flagged this as binary content
613
+ downloadFile(path);
614
+ return;
615
+ }
616
+ showPreview('code');
617
+ $('previewCode').textContent=data.content;
618
+ }catch(e){
619
+ // If it's a 400/too-large error, offer download instead
620
+ downloadFile(path);
621
+ }
622
+ }
623
+ }
624
+
625
+ function downloadFile(path){
626
+ if(!S.session)return;
627
+ // Trigger browser download via the raw file endpoint with content-disposition attachment
628
+ const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&download=1`;
629
+ const filename=path.split('/').pop();
630
+ const a=document.createElement('a');
631
+ a.href=url;a.download=filename;
632
+ document.body.appendChild(a);a.click();
633
+ setTimeout(()=>document.body.removeChild(a),100);
634
+ showToast(t('downloading',filename),2000);
635
+ }
636
+
637
+
638
+ // ── Render breadcrumb for file preview mode ──────────────────────────────────
639
+ function renderFileBreadcrumb(filePath) {
640
+ const bar = $('breadcrumbBar');
641
+ if (!bar) return;
642
+ bar.style.display = 'flex';
643
+ const upBtn = $('btnUpDir');
644
+ if (upBtn) upBtn.style.display = '';
645
+
646
+ bar.innerHTML = '';
647
+ // Root
648
+ const root = document.createElement('span');
649
+ root.className = 'breadcrumb-seg breadcrumb-link';
650
+ root.textContent = '~';
651
+ root.onclick = () => { loadDir('.'); };
652
+ bar.appendChild(root);
653
+
654
+ const parts = filePath.split('/');
655
+ let accumulated = '';
656
+ for (let i = 0; i < parts.length; i++) {
657
+ const sep = document.createElement('span');
658
+ sep.className = 'breadcrumb-sep';
659
+ sep.textContent = '/';
660
+ bar.appendChild(sep);
661
+
662
+ accumulated += (accumulated ? '/' : '') + parts[i];
663
+ const seg = document.createElement('span');
664
+ seg.textContent = parts[i];
665
+ if (i < parts.length - 1) {
666
+ seg.className = 'breadcrumb-seg breadcrumb-link';
667
+ const target = accumulated;
668
+ seg.onclick = () => { loadDir(target); };
669
+ } else {
670
+ seg.className = 'breadcrumb-seg breadcrumb-current';
671
+ }
672
+ bar.appendChild(seg);
673
+ }
674
+ }
675
+
676
+ function openInBrowser(){
677
+ if(!_previewCurrentPath||!S.session) return;
678
+ const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(_previewCurrentPath)}`;
679
+ window.open(url,'_blank');
680
+ }