@bitseek/hermes-webui 0.1.0-beta.0 → 0.1.0-beta.1

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 (99) hide show
  1. package/package.json +2 -2
  2. package/vendor/agent-frontend-shell/.bitseek-source.json +2 -2
  3. package/vendor/agent-frontend-shell/CHANGELOG.md +178 -1
  4. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +5 -5
  5. package/vendor/agent-frontend-shell/api/agent_health.py +134 -0
  6. package/vendor/agent-frontend-shell/api/config.py +145 -104
  7. package/vendor/agent-frontend-shell/api/gateway_chat.py +56 -12
  8. package/vendor/agent-frontend-shell/api/helpers.py +4 -2
  9. package/vendor/agent-frontend-shell/api/models.py +202 -20
  10. package/vendor/agent-frontend-shell/api/paths.py +77 -0
  11. package/vendor/agent-frontend-shell/api/plugins.py +185 -0
  12. package/vendor/agent-frontend-shell/api/profiles.py +95 -16
  13. package/vendor/agent-frontend-shell/api/routes.py +831 -30
  14. package/vendor/agent-frontend-shell/api/run_journal.py +1 -0
  15. package/vendor/agent-frontend-shell/api/state_sync.py +5 -4
  16. package/vendor/agent-frontend-shell/api/streaming.py +211 -56
  17. package/vendor/agent-frontend-shell/api/todo_state.py +122 -0
  18. package/vendor/agent-frontend-shell/api/updates.py +30 -3
  19. package/vendor/agent-frontend-shell/api/upload.py +251 -18
  20. package/vendor/agent-frontend-shell/api/workspace.py +323 -65
  21. package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_EN.docx +0 -0
  22. package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_ZH.docx +0 -0
  23. package/vendor/agent-frontend-shell/bitseek_docs/en/00-Installation.md +174 -0
  24. package/vendor/agent-frontend-shell/bitseek_docs/en/01-Overview.md +128 -0
  25. package/vendor/agent-frontend-shell/bitseek_docs/en/02-Page-Operations.md +461 -0
  26. package/vendor/agent-frontend-shell/bitseek_docs/en/README.md +61 -0
  27. package/vendor/agent-frontend-shell/bitseek_docs/en/images/ai-colleagues.png +0 -0
  28. package/vendor/agent-frontend-shell/bitseek_docs/en/images/chat-area.png +0 -0
  29. package/vendor/agent-frontend-shell/bitseek_docs/en/images/kanban.png +0 -0
  30. package/vendor/agent-frontend-shell/bitseek_docs/en/images/main-page.png +0 -0
  31. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-notes.png +0 -0
  32. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-overview.png +0 -0
  33. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-profile.png +0 -0
  34. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-soul.png +0 -0
  35. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory.png +0 -0
  36. package/vendor/agent-frontend-shell/bitseek_docs/en/images/navigation-bar.png +0 -0
  37. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-appearance.png +0 -0
  38. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-conversation.png +0 -0
  39. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-overview.png +0 -0
  40. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-plugins.png +0 -0
  41. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-preferences.png +0 -0
  42. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-providers.png +0 -0
  43. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-system.png +0 -0
  44. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings.png +0 -0
  45. package/vendor/agent-frontend-shell/bitseek_docs/en/images/sidebar.png +0 -0
  46. package/vendor/agent-frontend-shell/bitseek_docs/en/images/skills.png +0 -0
  47. package/vendor/agent-frontend-shell/bitseek_docs/en/images/tasks.png +0 -0
  48. package/vendor/agent-frontend-shell/bitseek_docs/en/images/workspace-panel.png +0 -0
  49. package/vendor/agent-frontend-shell/bitseek_docs/md_to_docx.py +351 -0
  50. package/vendor/agent-frontend-shell/bitseek_docs/zh/00-/345/256/211/350/243/205/345/220/257/345/212/250.md +174 -0
  51. package/vendor/agent-frontend-shell/bitseek_docs/zh/01-/346/225/264/344/275/223/346/246/202/350/247/210.md +128 -0
  52. package/vendor/agent-frontend-shell/bitseek_docs/zh/02-/351/241/265/351/235/242/346/223/215/344/275/234.md +463 -0
  53. package/vendor/agent-frontend-shell/bitseek_docs/zh/README.md +61 -0
  54. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/ai-colleagues.png +0 -0
  55. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/chat-area.png +0 -0
  56. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/kanban.png +0 -0
  57. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/main-page.png +0 -0
  58. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-notes.png +0 -0
  59. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-overview.png +0 -0
  60. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-profile.png +0 -0
  61. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-soul.png +0 -0
  62. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory.png +0 -0
  63. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/navigation-bar.png +0 -0
  64. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-appearance.png +0 -0
  65. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-conversation.png +0 -0
  66. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-overview.png +0 -0
  67. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-plugins.png +0 -0
  68. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-preferences.png +0 -0
  69. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-providers.png +0 -0
  70. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-system.png +0 -0
  71. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings.png +0 -0
  72. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/sidebar.png +0 -0
  73. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/skills.png +0 -0
  74. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/tasks.png +0 -0
  75. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/workspace-panel.png +0 -0
  76. package/vendor/agent-frontend-shell/build-release.sh +62 -0
  77. package/vendor/agent-frontend-shell/ctl.sh +1 -0
  78. package/vendor/agent-frontend-shell/docker-compose.local.yml +33 -0
  79. package/vendor/agent-frontend-shell/docker-compose.yml +8 -0
  80. package/vendor/agent-frontend-shell/docker_init.bash +1 -0
  81. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +74 -15
  82. package/vendor/agent-frontend-shell/extensions/common/index.css +6 -0
  83. package/vendor/agent-frontend-shell/extensions/manifest.json +6 -0
  84. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +60 -14
  85. package/vendor/agent-frontend-shell/readme-simple.md +103 -0
  86. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  87. package/vendor/agent-frontend-shell/server.py +7 -0
  88. package/vendor/agent-frontend-shell/static/boot.js +53 -1
  89. package/vendor/agent-frontend-shell/static/commands.js +20 -10
  90. package/vendor/agent-frontend-shell/static/i18n.js +1142 -1016
  91. package/vendor/agent-frontend-shell/static/index.html +13 -3
  92. package/vendor/agent-frontend-shell/static/messages.js +48 -3
  93. package/vendor/agent-frontend-shell/static/panels.js +199 -30
  94. package/vendor/agent-frontend-shell/static/sessions.js +249 -39
  95. package/vendor/agent-frontend-shell/static/style.css +46 -2
  96. package/vendor/agent-frontend-shell/static/ui.js +323 -79
  97. package/vendor/agent-frontend-shell/static/workspace.js +185 -7
  98. package/vendor/agent-frontend-shell/README-CUSTOM.md +0 -76
  99. package/vendor/agent-frontend-shell/docker-compose.custom.yml +0 -26
@@ -877,6 +877,14 @@
877
877
  </div>
878
878
  </div>
879
879
  </div>
880
+ <div id="mainPlugin" class="main-view">
881
+ <div class="main-view-header">
882
+ <div class="main-view-title" id="pluginPageTitle">Plugin</div>
883
+ </div>
884
+ <div class="main-view-body">
885
+ <div id="pluginPageContainer"></div>
886
+ </div>
887
+ </div>
880
888
  <div id="mainSettings" class="main-view">
881
889
  <div class="settings-main">
882
890
  <div class="settings-pane active" id="settingsPaneConversation">
@@ -1071,7 +1079,7 @@
1071
1079
  <div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_raw_audio">Record and send the original audio file to the agent instead of converting it to text first. The agent can then transcribe it or process the raw audio (emotion, background noise, custom STT). Like Telegram's voice message behavior.</div>
1072
1080
  </div>
1073
1081
  <div class="settings-field">
1074
- <label for="settingsTtsVoice" data-i18n="settings_label_tts_voice">Voice</label>
1082
+ <label for="settingsTtsEngine" data-i18n="settings_label_tts_engine">TTS Engine</label><select id="settingsTtsEngine" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px"><option value="browser">Browser speech synthesis</option><option value="edge">Edge TTS (server)</option></select><div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_tts_engine">Choose speech engine. Edge TTS uses Microsoft neural voices via the server.</div></div><div class="settings-field"><label for="settingsTtsVoice" data-i18n="settings_label_tts_voice">Voice</label>
1075
1083
  <select id="settingsTtsVoice" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px">
1076
1084
  <option value="">Default system voice</option>
1077
1085
  </select>
@@ -1324,7 +1332,7 @@
1324
1332
  </div>
1325
1333
  </main>
1326
1334
  <button class="workspace-panel-edge-toggle has-tooltip has-tooltip--left" id="btnWorkspacePanelEdgeToggle" type="button" onclick="toggleWorkspacePanel(true)" data-tooltip="Show workspace panel" aria-label="Show workspace panel" aria-expanded="false">
1327
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
1335
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
1328
1336
  </button>
1329
1337
  <aside class="rightpanel">
1330
1338
  <div class="resize-handle" id="rightpanelResize"></div>
@@ -1335,11 +1343,13 @@
1335
1343
  <button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnUpDir" data-tooltip="Parent directory" onclick="navigateUp()" style="display:none"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></button>
1336
1344
  <button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnNewFile" data-tooltip="New file" onclick="promptNewFile()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
1337
1345
  <button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnNewFolder" data-tooltip="New folder" onclick="promptNewFolder()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></button>
1338
- <button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnRefreshPanel" data-tooltip="Refresh" onclick="if(S.session)loadDir(S.currentDir)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
1346
+ <button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnRefreshPanel" data-tooltip="Refresh" onclick="if(S.session)loadDir(S.currentDir)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10" /><polyline points="1 20 1 14 7 14" /><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" /></svg></button>
1347
+ <button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnUploadWorkspace" data-tooltip="Upload file" onclick="triggerWorkspaceUpload()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></button>
1339
1348
  <button class="panel-icon-btn has-tooltip has-tooltip--bottom" id="btnWorkspacePrefs" data-tooltip="Workspace options" data-i18n-title="workspace_options" aria-haspopup="true" aria-expanded="false" onclick="toggleWorkspacePrefsMenu(event)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg><span class="workspace-prefs-dot" id="workspacePrefsDot" hidden></span></button>
1340
1349
  <button class="panel-icon-btn close-preview has-tooltip has-tooltip--bottom" id="btnClearPreview" data-tooltip="Close preview"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
1341
1350
  </div>
1342
1351
  </div>
1352
+ <input type="file" id="workspaceFileInput" class="file-input-visually-hidden" multiple accept="image/*,text/*,.pdf,.json,.csv,.md,.py,.js,.ts,.yaml,.yml,.toml,.zip,.tar,.gz,.tgz,.bz2,.xz">
1343
1353
  <div class="workspace-panel-tabs" role="tablist" aria-label="Workspace panel views">
1344
1354
  <button class="workspace-panel-tab active" id="workspaceFilesTab" type="button" onclick="switchWorkspacePanelTab('files')" role="tab" aria-selected="true">Files</button>
1345
1355
  <button class="workspace-panel-tab" id="workspaceArtifactsTab" type="button" onclick="switchWorkspacePanelTab('artifacts')" role="tab" aria-selected="false">Artifacts <span id="workspaceArtifactsCount" class="workspace-artifacts-count">0</span></button>
@@ -685,6 +685,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
685
685
  closeOtherLiveStreams(activeSid);
686
686
  closeLiveStream(activeSid);
687
687
  if(!reconnecting&&typeof resetTurnWorkspaceMutations==='function') resetTurnWorkspaceMutations();
688
+ if(!reconnecting&&typeof _resetStreamScrollFollow==='function') _resetStreamScrollFollow();
688
689
 
689
690
  // On reconnect, restore accumulated text from INFLIGHT so we don't lose
690
691
  // progress made before the session switch. Without this the closure starts
@@ -753,6 +754,31 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
753
754
  return typeof _isPreservedCompressionTaskListMarkerOnlyText==='function'
754
755
  && _isPreservedCompressionTaskListMarkerOnlyText(text);
755
756
  }
757
+ function _streamRecoveryControlMessageText(text){
758
+ const normalized=String(text||'').replace(/\s+/g,' ').trim();
759
+ if(!normalized) return false;
760
+ const systemRecovery=/^\[System:/i.test(normalized)
761
+ && /previous response was cut off by a network error/i.test(normalized)
762
+ && /continue exactly where you left off/i.test(normalized);
763
+ const backendRecovery=/^the live worker stopped before this run finished\.?$/i.test(normalized);
764
+ return !!(systemRecovery || backendRecovery);
765
+ }
766
+ function _streamRecoveryControlMessage(m){
767
+ if(!m||m.role==='tool') return false;
768
+ if(m.recovery_control===true) return true;
769
+ // Backward-compat ONLY for pre-marker persisted sessions: match the two
770
+ // fully-anchored synthetic recovery strings. Do NOT fall back to
771
+ // provider_details_label — a genuine "Response interrupted" card the user
772
+ // SHOULD see also carries the 'Interruption details' label, and filtering
773
+ // on it would drop a real interruption from the transcript (the inverse
774
+ // data-loss class flagged on the sibling #3300). Marker + strict text only.
775
+ const text=String(typeof msgContent==='function'?msgContent(m):(m.content||''));
776
+ return _streamRecoveryControlMessageText(text);
777
+ }
778
+ function _filterRecoveryControlMessages(messages){
779
+ if(!Array.isArray(messages)) return [];
780
+ return messages.filter((m)=>!_streamRecoveryControlMessage(m));
781
+ }
756
782
  function _replaceMarkerOnlyAssistantWithStreamError(messages){
757
783
  if(!Array.isArray(messages)) return false;
758
784
  const msg=[...messages].reverse().find(m=>m&&m.role==='assistant');
@@ -1884,6 +1910,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
1884
1910
  const _prevCacheRead=(S.session&&S.session.cache_read_tokens)||0;
1885
1911
  const _prevCacheWrite=(S.session&&S.session.cache_write_tokens)||0;
1886
1912
  S.session=d.session;S.messages=_carryForwardEphemeralTurnFields(S.messages||[], d.session.messages||[]);if(typeof _messagesTruncated!=='undefined')_messagesTruncated=!!d.session._messages_truncated;
1913
+ S.messages=_filterRecoveryControlMessages(S.messages || []);
1887
1914
  if(S.session&&S.session.session_id){
1888
1915
  try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
1889
1916
  if(typeof _setActiveSessionUrl==='function') _setActiveSessionUrl(S.session.session_id);
@@ -2169,6 +2196,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
2169
2196
  if(S.session&&S.session.session_id===activeSid){
2170
2197
  S.activeStreamId=null;
2171
2198
  clearLiveToolCards();if(!assistantText)removeThinking();
2199
+ let isRecoveryControlMessage=false;
2172
2200
  try{
2173
2201
  const d=JSON.parse(e.data);
2174
2202
  const isRateLimit=d.type==='rate_limit';
@@ -2178,17 +2206,33 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
2178
2206
  const isModelNotFound=d.type==='model_not_found';
2179
2207
  const isCancelled=d.type==='cancelled';
2180
2208
  const isInterrupted=d.type==='interrupted';
2209
+ isRecoveryControlMessage=isInterrupted && (d.recovery_control===true || _streamRecoveryControlMessageText(d.message));
2181
2210
  const isNoResponse=d.type==='no_response'||d.type==='silent_failure';
2182
2211
  const label=isCancelled?'Task cancelled':isInterrupted?'Response interrupted':isQuotaExhausted?'Out of credits':isRateLimit?'Rate limit reached':isGatewayAuthError?(typeof t==='function'?t('gateway_auth_label'):'Gateway authentication failed'):isAuthMismatch?(typeof t==='function'?t('provider_mismatch_label'):'Provider mismatch'):isModelNotFound?(typeof t==='function'?t('model_not_found_label'):'Model not found'):isNoResponse?'No response from provider':'Error';
2183
2212
  const hint=d.hint?`\n\n*${d.hint}*`:'';
2184
2213
  const details=d.details?String(d.details).replace(/```/g,'`\u200b``'):'';
2185
2214
  const detailsLabel=isCancelled?'Cancellation details':isInterrupted?'Interruption details':undefined;
2186
- S.messages.push({role:'assistant',content:`**${label}:** ${d.message}${hint}`,provider_details:details,provider_details_label:detailsLabel});
2215
+ if(isRecoveryControlMessage){
2216
+ if(typeof showToast==='function') showToast('Stream recovery signal received. Restoring transcript...',3500,'error');
2217
+ } else {
2218
+ S.messages.push({role:'assistant',content:`**${label}:** ${d.message}${hint}`,provider_details:details,provider_details_label:detailsLabel});
2219
+ }
2187
2220
  }catch(_){
2188
2221
  S.messages.push({role:'assistant',content:'**Error:** An error occurred. Check server logs.'});
2189
2222
  }
2190
- _markSessionViewed(activeSid, S.messages.length);
2191
- renderMessages({preserveScroll:true});
2223
+ if(isRecoveryControlMessage){
2224
+ (async()=>{
2225
+ if(await _restoreSettledSession(source)) return;
2226
+ if(S.session&&S.session.session_id===activeSid){
2227
+ S.messages=_filterRecoveryControlMessages(S.messages||[]);
2228
+ _markSessionViewed(activeSid, S.messages.length);
2229
+ renderMessages({preserveScroll:true});
2230
+ }
2231
+ })();
2232
+ } else {
2233
+ _markSessionViewed(activeSid, S.messages.length);
2234
+ renderMessages({preserveScroll:true});
2235
+ }
2192
2236
  }else if(typeof trackBackgroundError==='function'){
2193
2237
  const _errTitle=(typeof _allSessions!=='undefined'&&_allSessions.find(s=>s.session_id===activeSid)||{}).title||null;
2194
2238
  try{const d=JSON.parse(e.data);trackBackgroundError(activeSid,_errTitle,d.message||'Error');}
@@ -2381,6 +2425,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
2381
2425
  S.session=session;
2382
2426
  const _nextMsgs3018=(session.messages||[]).filter(m=>m&&m.role);
2383
2427
  S.messages=_carryForwardEphemeralTurnFields(S.messages||[], _nextMsgs3018);
2428
+ S.messages=_filterRecoveryControlMessages(S.messages || []);
2384
2429
  if(S.session&&S.session.session_id){
2385
2430
  try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
2386
2431
  if(typeof _setActiveSessionUrl==='function') _setActiveSessionUrl(S.session.session_id);
@@ -243,7 +243,7 @@ async function switchPanel(name, opts = {}) {
243
243
  // showing-<name> class on <main>; no class means chat (the default).
244
244
  const mainEl = document.querySelector('main.main');
245
245
  if (mainEl) {
246
- ['settings','skills','memory','tasks','kanban','workspaces','profiles','insights','logs'].forEach(p => {
246
+ ['settings','skills','memory','tasks','kanban','workspaces','profiles','insights','logs','plugin'].forEach(p => {
247
247
  mainEl.classList.toggle('showing-' + p, nextPanel === p);
248
248
  });
249
249
  }
@@ -2650,37 +2650,50 @@ async function loadKanbanTask(taskId){
2650
2650
  function loadTodos() {
2651
2651
  const panel = $('todoPanel');
2652
2652
  if (!panel) return;
2653
- const sourceMessages = (S.session && Array.isArray(S.session.messages) && S.session.messages.length) ? S.session.messages : S.messages;
2654
- // Parse the most recent todo state from message history
2655
- let todos = [];
2656
- for (let i = sourceMessages.length - 1; i >= 0; i--) {
2657
- const m = sourceMessages[i];
2658
- if (m && m.role === 'tool') {
2659
- try {
2660
- const d = JSON.parse(typeof m.content === 'string' ? m.content : JSON.stringify(m.content));
2661
- if (d && Array.isArray(d.todos) && d.todos.length) {
2662
- todos = d.todos;
2663
- break;
2664
- }
2665
- } catch(e) {}
2666
- }
2653
+
2654
+ const sessionTodoState = S.session && S.session.todo_state;
2655
+ let todos;
2656
+ if (sessionTodoState && Array.isArray(sessionTodoState.todos)) {
2657
+ todos = sessionTodoState.todos;
2658
+ } else {
2659
+ todos = _legacyTodosFromMessages();
2667
2660
  }
2661
+
2668
2662
  if (!todos.length) {
2669
2663
  panel.innerHTML = `<div style="color:var(--muted);font-size:12px;padding:4px 0">${esc(t('todos_no_active'))}</div>`;
2670
2664
  return;
2671
2665
  }
2672
2666
  const statusIcon = {pending:li('square',14), in_progress:li('loader',14), completed:li('check',14), cancelled:li('x',14)};
2673
2667
  const statusColor = {pending:'var(--muted)', in_progress:'var(--blue)', completed:'rgba(100,200,100,.8)', cancelled:'rgba(200,100,100,.5)'};
2674
- panel.innerHTML = todos.map(t => `
2668
+ panel.innerHTML = todos.map(todo => `
2675
2669
  <div style="display:flex;align-items:flex-start;gap:10px;padding:6px 0;border-bottom:1px solid var(--border);">
2676
- <span style="font-size:14px;display:inline-flex;align-items:center;flex-shrink:0;margin-top:1px;color:${statusColor[t.status]||'var(--muted)'}">${statusIcon[t.status]||li('square',14)}</span>
2670
+ <span style="font-size:14px;display:inline-flex;align-items:center;flex-shrink:0;margin-top:1px;color:${statusColor[todo.status]||'var(--muted)'}">${statusIcon[todo.status]||li('square',14)}</span>
2677
2671
  <div style="flex:1;min-width:0">
2678
- <div style="font-size:13px;color:${t.status==='completed'?'var(--muted)':t.status==='in_progress'?'var(--text)':'var(--text)'};${t.status==='completed'?'text-decoration:line-through;opacity:.5':''};line-height:1.4">${esc(t.content)}</div>
2679
- <div style="font-size:10px;color:var(--muted);margin-top:2px;opacity:.6">${esc(t.id)} · ${esc(t.status)}</div>
2672
+ <div style="font-size:13px;color:${todo.status==='completed'?'var(--muted)':todo.status==='in_progress'?'var(--text)':'var(--text)'};${todo.status==='completed'?'text-decoration:line-through;opacity:.5':''};line-height:1.4">${esc(todo.content)}</div>
2673
+ <div style="font-size:10px;color:var(--muted);margin-top:2px;opacity:.6">${esc(todo.id)} · ${esc(todo.status)}</div>
2680
2674
  </div>
2681
2675
  </div>`).join('');
2682
2676
  }
2683
2677
 
2678
+ function _legacyTodosFromMessages() {
2679
+ const sourceMessages = (S.session && Array.isArray(S.session.messages) && S.session.messages.length) ? S.session.messages : S.messages;
2680
+ if (!Array.isArray(sourceMessages)) return [];
2681
+ for (let i = sourceMessages.length - 1; i >= 0; i--) {
2682
+ const m = sourceMessages[i];
2683
+ if (!m || m.role !== 'tool') continue;
2684
+ let content = m.content;
2685
+ if (typeof content !== 'string') {
2686
+ try { content = JSON.stringify(content); } catch (_) { continue; }
2687
+ }
2688
+ if (!content || content.indexOf('"todos"') < 0) continue;
2689
+ try {
2690
+ const d = JSON.parse(content);
2691
+ if (d && Array.isArray(d.todos) && d.todos.length) return d.todos;
2692
+ } catch (_) {}
2693
+ }
2694
+ return [];
2695
+ }
2696
+
2684
2697
  // ────────────────────────────────────────────────────────────────────────────
2685
2698
  // Kanban: multi-board switcher + create/rename/archive modal
2686
2699
  // ────────────────────────────────────────────────────────────────────────────
@@ -5035,7 +5048,7 @@ async function loadProfilesPanel() {
5035
5048
  const meta = [];
5036
5049
  if (p.model) meta.push(p.model.split('/').pop());
5037
5050
  if (p.provider) meta.push(p.provider);
5038
- if (p.skill_count) meta.push(t('profile_skill_count', p.skill_count));
5051
+ if (p.total_skills && p.total_skills > 0) meta.push(t('profile_skill_count', p.total_skills).replace(String(p.total_skills), `${p.enabled_skills} / ${p.total_skills}`));
5039
5052
  const gwDot = p.gateway_running
5040
5053
  ? `<span class="profile-opt-badge running" title="${esc(t('profile_gateway_running'))}"></span>`
5041
5054
  : `<span class="profile-opt-badge stopped" title="${esc(t('profile_gateway_stopped'))}"></span>`;
@@ -5109,7 +5122,7 @@ function _renderProfileDetail(p, activeName){
5109
5122
  if (p.provider) rows.push(`<div class="detail-row"><div class="detail-row-label">Provider</div><div class="detail-row-value">${esc(p.provider)}</div></div>`);
5110
5123
  if (p.base_url) rows.push(`<div class="detail-row"><div class="detail-row-label">Base URL</div><div class="detail-row-value"><code>${esc(p.base_url)}</code></div></div>`);
5111
5124
  rows.push(`<div class="detail-row"><div class="detail-row-label">API key</div><div class="detail-row-value">${p.has_env ? esc(t('profile_api_keys_configured')) : '<span style="color:var(--muted)">Not configured</span>'}</div></div>`);
5112
- if (typeof p.skill_count === 'number') rows.push(`<div class="detail-row"><div class="detail-row-label">Skills</div><div class="detail-row-value">${esc(t('profile_skill_count', p.skill_count))}</div></div>`);
5125
+ if (p.total_skills && p.total_skills > 0) rows.push(`<div class="detail-row"><div class="detail-row-label">Skills</div><div class="detail-row-value">${esc(t('profile_skill_count', p.total_skills).replace(String(p.total_skills), `${p.enabled_skills} / ${p.total_skills}`))}</div></div>`);
5113
5126
  if (p.default_workspace) rows.push(`<div class="detail-row"><div class="detail-row-label">Default space</div><div class="detail-row-value"><code>${esc(p.default_workspace)}</code></div></div>`);
5114
5127
  body.innerHTML = `
5115
5128
  <div class="main-view-content">
@@ -5199,7 +5212,7 @@ function renderProfileDropdown(data) {
5199
5212
  opt.className = 'profile-opt' + (p.name === active ? ' active' : '');
5200
5213
  const meta = [];
5201
5214
  if (p.model) meta.push(p.model.split('/').pop());
5202
- if (p.skill_count) meta.push(t('profile_skill_count', p.skill_count));
5215
+ if (p.total_skills && p.total_skills > 0) meta.push(t('profile_skill_count', p.total_skills).replace(String(p.total_skills), `${p.enabled_skills} / ${p.total_skills}`));
5203
5216
  const gwDot = `<span class="profile-opt-badge ${p.gateway_running ? 'running' : 'stopped'}"></span>`;
5204
5217
  const checkmark = p.name === active ? ' <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--link)" stroke-width="3" style="vertical-align:-1px"><polyline points="20 6 9 17 4 12"/></svg>' : '';
5205
5218
  const defaultBadge = p.is_default ? ` <span style="opacity:.5;font-weight:400">${esc(t('profile_default_label'))}</span>` : '';
@@ -5322,8 +5335,18 @@ async function switchToProfile(name) {
5322
5335
  if (S.session && !sessionInProgress) {
5323
5336
  S.session.model = modelToUse;
5324
5337
  S.session.model_provider = modelState.model_provider||providerId||null;
5338
+ S.session.profile = data.active || name;
5325
5339
  }
5326
5340
  }
5341
+ // #3331 follow-up (Codex gate): retag the in-memory session's profile on
5342
+ // ANY profile switch, even when the switched-to profile returns no
5343
+ // default_model (empty session / model-less profile). Without this the
5344
+ // profile chip + project-picker filter keep the stale profile after a
5345
+ // switch to a model-less profile. Guarded by !sessionInProgress like the
5346
+ // model patch above (don't touch a session about to be replaced).
5347
+ if (S.session && !sessionInProgress) {
5348
+ S.session.profile = data.active || name;
5349
+ }
5327
5350
 
5328
5351
  // ── Apply workspace ────────────────────────────────────────────────────
5329
5352
  if (data.default_workspace) {
@@ -5690,6 +5713,16 @@ function _toggleTabVisibilityChip(panel){
5690
5713
  }
5691
5714
 
5692
5715
  function switchSettingsSection(name){
5716
+ // If the main content is not showing settings, switch back first
5717
+ if (_currentPanel !== 'settings') {
5718
+ _currentPanel = 'settings';
5719
+ var mainEl = document.querySelector('main.main');
5720
+ if (mainEl) {
5721
+ ['settings','skills','memory','tasks','kanban','workspaces','profiles','insights','logs','plugin'].forEach(function(p) {
5722
+ mainEl.classList.toggle('showing-' + p, p === 'settings');
5723
+ });
5724
+ }
5725
+ }
5693
5726
  const section=(name==='appearance'||name==='preferences'||name==='providers'||name==='plugins'||name==='system')?name:'conversation';
5694
5727
  _settingsSection=section;
5695
5728
  _currentSettingsSection=section;
@@ -6243,12 +6276,45 @@ async function loadSettingsPanel(){
6243
6276
  if(typeof window._applyVoiceModePref==='function') window._applyVoiceModePref();
6244
6277
  };
6245
6278
  }
6246
- // Populate voice selector from speechSynthesis
6279
+ // TTS engine selector
6280
+ const ttsEngineSel=$('settingsTtsEngine');
6281
+ if(ttsEngineSel){
6282
+ const saved=localStorage.getItem('hermes-tts-engine')||'browser';
6283
+ ttsEngineSel.value=saved;
6284
+ ttsEngineSel.onchange=function(){
6285
+ localStorage.setItem('hermes-tts-engine',this.value);
6286
+ window._populateTtsVoices();
6287
+ };
6288
+ }
6289
+ // Populate voice selector based on engine
6247
6290
  const ttsVoiceSel=$('settingsTtsVoice');
6248
- if(ttsVoiceSel&&'speechSynthesis' in window){
6249
- const populateVoices=()=>{
6291
+ window._populateTtsVoices=function(){
6292
+ if(!ttsVoiceSel) return;
6293
+ const engine=localStorage.getItem('hermes-tts-engine')||'browser';
6294
+ const current=localStorage.getItem('hermes-tts-voice')||'';
6295
+ if(engine==='edge'){
6296
+ const edgeVoices=[
6297
+ {value:'zh-CN-XiaoxiaoNeural',label:'Xiaoxiao (Chinese, Female)'},
6298
+ {value:'zh-CN-XiaoyiNeural',label:'Xiaoyi (Chinese, Female)'},
6299
+ {value:'zh-CN-YunxiNeural',label:'Yunxi (Chinese, Male)'},
6300
+ {value:'zh-CN-YunjianNeural',label:'Yunjian (Chinese, Male)'},
6301
+ {value:'zh-CN-YunyangNeural',label:'Yunyang (Chinese, Male)'},
6302
+ {value:'en-US-AriaNeural',label:'Aria (English, Female)'},
6303
+ {value:'en-US-GuyNeural',label:'Guy (English, Male)'},
6304
+ ];
6305
+ ttsVoiceSel.innerHTML='<option value="">Default (Xiaoxiao)</option>';
6306
+ edgeVoices.forEach(v=>{
6307
+ const opt=document.createElement('option');
6308
+ opt.value=v.value;opt.textContent=v.label;
6309
+ if(v.value===current) opt.selected=true;
6310
+ ttsVoiceSel.appendChild(opt);
6311
+ });
6312
+ } else {
6313
+ if(!('speechSynthesis' in window)){
6314
+ ttsVoiceSel.innerHTML='<option value="">Speech synthesis not available</option>';
6315
+ return;
6316
+ }
6250
6317
  const voices=speechSynthesis.getVoices();
6251
- const current=localStorage.getItem('hermes-tts-voice')||'';
6252
6318
  ttsVoiceSel.innerHTML='<option value="">Default system voice</option>';
6253
6319
  voices.forEach(v=>{
6254
6320
  const opt=document.createElement('option');
@@ -6256,9 +6322,14 @@ async function loadSettingsPanel(){
6256
6322
  if(v.name===current) opt.selected=true;
6257
6323
  ttsVoiceSel.appendChild(opt);
6258
6324
  });
6259
- };
6260
- populateVoices();
6261
- speechSynthesis.addEventListener('voiceschanged',populateVoices,{once:true});
6325
+ }
6326
+ };
6327
+ if(ttsVoiceSel&&'speechSynthesis' in window){
6328
+ window._populateTtsVoices();
6329
+ speechSynthesis.addEventListener('voiceschanged',function(){
6330
+ const engine=localStorage.getItem('hermes-tts-engine')||'browser';
6331
+ if(engine==='browser') window._populateTtsVoices();
6332
+ },{once:false});
6262
6333
  ttsVoiceSel.onchange=function(){localStorage.setItem('hermes-tts-voice',this.value);};
6263
6334
  }
6264
6335
  // TTS rate/pitch sliders
@@ -6355,6 +6426,17 @@ async function loadSettingsPanel(){
6355
6426
 
6356
6427
  // ── Plugins panel (read-only plugin/hook visibility) ───────────────────────
6357
6428
 
6429
+ async function handlePluginEnableToggle(pluginKey, checked){
6430
+ try{
6431
+ const body={dashboard_plugins:{}};
6432
+ body.dashboard_plugins[pluginKey]=!!checked;
6433
+ await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
6434
+ loadPluginsPanel();
6435
+ }catch(e){
6436
+ showToast(t('settings_save_failed')+e.message);
6437
+ }
6438
+ }
6439
+
6358
6440
  async function loadPluginsPanel(){
6359
6441
  const list=$('pluginsList');
6360
6442
  const empty=$('pluginsEmpty');
@@ -6399,6 +6481,33 @@ function _buildPluginCard(plugin){
6399
6481
  : '<span class="plugin-hook-empty">'+t(isProvider?'plugins_provider_no_hooks':'plugins_no_hooks')+'</span>';
6400
6482
  const version=(plugin&&plugin.version)?' · v'+esc(plugin.version):'';
6401
6483
  const desc=(plugin&&plugin.description)?esc(plugin.description):t('plugins_no_description');
6484
+ const enabled=plugin&&plugin.enabled!==false;
6485
+ const tab=plugin&&plugin.tab;
6486
+ const isDashboardPlugin=!!(tab&&tab.path);
6487
+ // No inline onclick/onchange: an inline handler interpolates tab.path/key into
6488
+ // a JS-string-in-attribute context where HTML-escaping is insufficient (a
6489
+ // crafted value could break out). Render inert markup + bind listeners below
6490
+ // with the raw closure values.
6491
+ const openBtn=enabled&&tab&&tab.path
6492
+ ? `<a href="${esc(tab.path)}" class="plugin-open-btn">${esc(tab.label||plugin.name||'Open')} \u2197</a>`
6493
+ : '';
6494
+ const toggleHtml=enabled&&isDashboardPlugin
6495
+ ? `<div class="plugin-card-footer-row">
6496
+ <span class="plugin-toggle-label">${t('plugins_enable_toggle')||'Enabled'}</span>
6497
+ <label class="plugin-toggle-switch">
6498
+ <input type="checkbox" class="plugin-enable-toggle" checked>
6499
+ <span class="plugin-toggle-slider"></span>
6500
+ </label>
6501
+ </div>`
6502
+ : (isDashboardPlugin
6503
+ ? `<div class="plugin-card-footer-row">
6504
+ <span class="plugin-toggle-label">${t('plugins_enable_toggle')||'Enable'}</span>
6505
+ <label class="plugin-toggle-switch">
6506
+ <input type="checkbox" class="plugin-enable-toggle">
6507
+ <span class="plugin-toggle-slider"></span>
6508
+ </label>
6509
+ </div>`
6510
+ : '');
6402
6511
  let badgeText;
6403
6512
  let badgeClass;
6404
6513
  if(isProvider){
@@ -6423,12 +6532,72 @@ function _buildPluginCard(plugin){
6423
6532
  <div class="provider-card-hint">${desc}</div>
6424
6533
  <div class="provider-card-label">${t('plugins_registered_hooks')}</div>
6425
6534
  <div class="plugin-hook-list">${hookHtml}</div>
6535
+ ${openBtn ? `<div class="plugin-card-footer">${openBtn}</div>` : ''}
6536
+ ${toggleHtml}
6426
6537
  </div>
6427
6538
  `;
6539
+ // Bind handlers with the RAW closure values (not interpolated into inline JS),
6540
+ // so a hostile tab.path/key can't break out of a JS-string attribute context.
6541
+ if(tab&&tab.path){
6542
+ const _openEl=card.querySelector('.plugin-open-btn');
6543
+ if(_openEl){
6544
+ const _p=tab.path, _l=tab.label||plugin.name;
6545
+ _openEl.addEventListener('click', function(ev){ switchPluginPage(ev, _p, _l); });
6546
+ }
6547
+ }
6548
+ if(isDashboardPlugin){
6549
+ const _tog=card.querySelector('.plugin-enable-toggle');
6550
+ if(_tog){
6551
+ const _k=plugin.key;
6552
+ _tog.addEventListener('change', function(){ handlePluginEnableToggle(_k, this.checked); });
6553
+ }
6554
+ }
6428
6555
  return card;
6429
6556
  }
6430
6557
 
6431
- // ── Providers panel ───────────────────────────────────────────────────────
6558
+ // ── Plugin pages ─────────────────────────────────────────────────────────────
6559
+
6560
+ let _currentPluginPage = null;
6561
+
6562
+ async function switchPluginPage(event, path, label) {
6563
+ if (event) {
6564
+ event.preventDefault();
6565
+ event.stopPropagation();
6566
+ }
6567
+ if (!_currentPluginPage || _currentPluginPage.path !== path) {
6568
+ await _loadPluginPage(path, label);
6569
+ }
6570
+ // Update _currentPanel so clicking sidebar items won't short-circuit,
6571
+ // but keep the sidebar panel views intact (no panelPlugin exists).
6572
+ _currentPanel = 'plugin';
6573
+ const mainEl = document.querySelector('main.main');
6574
+ if (mainEl) {
6575
+ ['settings','skills','memory','tasks','kanban','workspaces','profiles','insights','logs','plugin'].forEach(p => {
6576
+ mainEl.classList.toggle('showing-' + p, p === 'plugin');
6577
+ });
6578
+ }
6579
+ }
6580
+
6581
+ async function _loadPluginPage(path, label) {
6582
+ const container = $('pluginPageContainer');
6583
+ const titleEl = $('pluginPageTitle');
6584
+ if (!container) return;
6585
+ if (titleEl) titleEl.textContent = label || path;
6586
+ container.innerHTML = '';
6587
+
6588
+ // Use an iframe for full isolation (styles, scripts, modals stay sandboxed).
6589
+ // Security note: plugins are locally-installed (~/.hermes/plugins/), similar
6590
+ // trust model to VS Code extensions — only install plugins you trust.
6591
+ const iframe = document.createElement('iframe');
6592
+ iframe.src = path;
6593
+ iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;';
6594
+ iframe.setAttribute('title', label || 'Plugin');
6595
+ iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups');
6596
+ container.appendChild(iframe);
6597
+ _currentPluginPage = { path, label };
6598
+ }
6599
+
6600
+ // ── Providers panel ─────────────────────────────────────────────────────────
6432
6601
 
6433
6602
  const _providerCardEls = new Map(); // providerId → {card, statusDot, input, saveBtn, removeBtn}
6434
6603