@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.
- package/package.json +2 -2
- package/vendor/agent-frontend-shell/.bitseek-source.json +2 -2
- package/vendor/agent-frontend-shell/CHANGELOG.md +178 -1
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +5 -5
- package/vendor/agent-frontend-shell/api/agent_health.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +145 -104
- package/vendor/agent-frontend-shell/api/gateway_chat.py +56 -12
- package/vendor/agent-frontend-shell/api/helpers.py +4 -2
- package/vendor/agent-frontend-shell/api/models.py +202 -20
- package/vendor/agent-frontend-shell/api/paths.py +77 -0
- package/vendor/agent-frontend-shell/api/plugins.py +185 -0
- package/vendor/agent-frontend-shell/api/profiles.py +95 -16
- package/vendor/agent-frontend-shell/api/routes.py +831 -30
- package/vendor/agent-frontend-shell/api/run_journal.py +1 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +5 -4
- package/vendor/agent-frontend-shell/api/streaming.py +211 -56
- package/vendor/agent-frontend-shell/api/todo_state.py +122 -0
- package/vendor/agent-frontend-shell/api/updates.py +30 -3
- package/vendor/agent-frontend-shell/api/upload.py +251 -18
- package/vendor/agent-frontend-shell/api/workspace.py +323 -65
- package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_EN.docx +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_ZH.docx +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/00-Installation.md +174 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/01-Overview.md +128 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/02-Page-Operations.md +461 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/README.md +61 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/ai-colleagues.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/chat-area.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/kanban.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/main-page.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-notes.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-profile.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-soul.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/navigation-bar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-appearance.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-conversation.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-plugins.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-preferences.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-providers.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-system.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/sidebar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/skills.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/tasks.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/en/images/workspace-panel.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/md_to_docx.py +351 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/00-/345/256/211/350/243/205/345/220/257/345/212/250.md +174 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/01-/346/225/264/344/275/223/346/246/202/350/247/210.md +128 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/02-/351/241/265/351/235/242/346/223/215/344/275/234.md +463 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/README.md +61 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/ai-colleagues.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/chat-area.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/kanban.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/main-page.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-notes.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-profile.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-soul.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/navigation-bar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-appearance.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-conversation.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-overview.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-plugins.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-preferences.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-providers.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-system.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/sidebar.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/skills.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/tasks.png +0 -0
- package/vendor/agent-frontend-shell/bitseek_docs/zh/images/workspace-panel.png +0 -0
- package/vendor/agent-frontend-shell/build-release.sh +62 -0
- package/vendor/agent-frontend-shell/ctl.sh +1 -0
- package/vendor/agent-frontend-shell/docker-compose.local.yml +33 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +8 -0
- package/vendor/agent-frontend-shell/docker_init.bash +1 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +74 -15
- package/vendor/agent-frontend-shell/extensions/common/index.css +6 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +60 -14
- package/vendor/agent-frontend-shell/readme-simple.md +103 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +7 -0
- package/vendor/agent-frontend-shell/static/boot.js +53 -1
- package/vendor/agent-frontend-shell/static/commands.js +20 -10
- package/vendor/agent-frontend-shell/static/i18n.js +1142 -1016
- package/vendor/agent-frontend-shell/static/index.html +13 -3
- package/vendor/agent-frontend-shell/static/messages.js +48 -3
- package/vendor/agent-frontend-shell/static/panels.js +199 -30
- package/vendor/agent-frontend-shell/static/sessions.js +249 -39
- package/vendor/agent-frontend-shell/static/style.css +46 -2
- package/vendor/agent-frontend-shell/static/ui.js +323 -79
- package/vendor/agent-frontend-shell/static/workspace.js +185 -7
- package/vendor/agent-frontend-shell/README-CUSTOM.md +0 -76
- 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="
|
|
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
|
-
|
|
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
|
-
|
|
2191
|
-
|
|
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
|
-
|
|
2654
|
-
|
|
2655
|
-
let todos
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
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(
|
|
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[
|
|
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:${
|
|
2679
|
-
<div style="font-size:10px;color:var(--muted);margin-top:2px;opacity:.6">${esc(
|
|
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.
|
|
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 (
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
6249
|
-
|
|
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
|
-
|
|
6261
|
-
|
|
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
|
-
// ──
|
|
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
|
|