create-walle 0.9.3 → 0.9.4
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/README.md +2 -1
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +5 -1
- package/template/claude-task-manager/public/css/walle.css +317 -0
- package/template/claude-task-manager/public/index.html +404 -101
- package/template/claude-task-manager/public/js/walle.js +1256 -86
- package/template/claude-task-manager/server.js +189 -14
- package/template/docs/site/api/README.md +146 -0
- package/template/docs/site/skills/README.md +99 -5
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +54 -0
- package/template/wall-e/api-walle.js +452 -3
- package/template/wall-e/brain.js +45 -1
- package/template/wall-e/channels/telegram-channel.js +96 -0
- package/template/wall-e/chat.js +61 -2
- package/template/wall-e/coding-context.js +252 -0
- package/template/wall-e/coding-orchestrator.js +625 -0
- package/template/wall-e/coding-review.js +189 -0
- package/template/wall-e/core-tasks.js +12 -3
- package/template/wall-e/deploy.sh +4 -4
- package/template/wall-e/fly.toml +2 -2
- package/template/wall-e/package.json +4 -1
- package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
- package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
- package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
- package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
- package/template/wall-e/skills/_templates/manual-action.md +19 -0
- package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
- package/template/wall-e/skills/_templates/script-runner.md +21 -0
- package/template/wall-e/skills/claude-code-reader.js +16 -4
- package/template/wall-e/skills/skill-executor.js +23 -1
- package/template/wall-e/skills/skill-validator.js +73 -0
- package/template/wall-e/tests/brain.test.js +3 -3
- package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
- package/template/wall-e/tests/coding-context.test.js +212 -0
- package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
- package/template/wall-e/tests/coding-review.test.js +141 -0
- package/template/claude-task-manager/package-lock.json +0 -1607
- package/template/claude-task-manager/tests/test-ai-search.js +0 -61
- package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
- package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
- package/template/claude-task-manager/tests/test-features-v2.js +0 -127
- package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
- package/template/claude-task-manager/tests/test-insights.js +0 -124
- package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
- package/template/claude-task-manager/tests/test-permissions.js +0 -122
- package/template/claude-task-manager/tests/test-pin.js +0 -51
- package/template/claude-task-manager/tests/test-prompts.js +0 -164
- package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
- package/template/claude-task-manager/tests/test-review.js +0 -104
- package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
- package/template/claude-task-manager/tests/test-send-final.js +0 -30
- package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
- package/template/claude-task-manager/tests/test-send-integration.js +0 -107
- package/template/claude-task-manager/tests/test-send-visual.js +0 -34
- package/template/claude-task-manager/tests/test-session-create.js +0 -147
- package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
- package/template/claude-task-manager/tests/test-url-hash.js +0 -68
- package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
- package/template/claude-task-manager/tests/test-ux-review.js +0 -130
- package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
- package/template/claude-task-manager/tests/test-zoom.js +0 -92
- package/template/claude-task-manager/tests/test-zoom2.js +0 -67
- package/template/docs/openclaw-vs-walle-comparison.md +0 -103
- package/template/docs/ux-improvement-plan.md +0 -84
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
- package/template/wall-e/package-lock.json +0 -533
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
|
@@ -55,6 +55,13 @@ function apiPut(path, body) {
|
|
|
55
55
|
}).then(function(r) { return r.json(); });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function apiDelete(path) {
|
|
59
|
+
var token = window._ctmState?.token || '';
|
|
60
|
+
return fetch(WALLE_BASE + '/api/wall-e' + path + '?token=' + token, {
|
|
61
|
+
method: 'DELETE'
|
|
62
|
+
}).then(function(r) { return r.json(); });
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
// Sanitize all user content — uses DOMPurify if available, otherwise manual escaping.
|
|
59
66
|
// SECURITY: Every piece of user-generated data MUST pass through this function before
|
|
60
67
|
// being placed into innerHTML. This prevents XSS by either sanitizing via DOMPurify
|
|
@@ -174,9 +181,14 @@ function renderMarkdown(s) {
|
|
|
174
181
|
|
|
175
182
|
function timeAgo(ts) {
|
|
176
183
|
if (!ts) return '';
|
|
177
|
-
|
|
184
|
+
// SQLite datetime('now') stores UTC without timezone suffix — append Z if missing
|
|
185
|
+
var s = String(ts);
|
|
186
|
+
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(s) && !/[Z+-]\d{0,4}$/.test(s)) s += 'Z';
|
|
187
|
+
var d = new Date(s);
|
|
188
|
+
if (isNaN(d.getTime())) return ts;
|
|
178
189
|
var now = new Date();
|
|
179
190
|
var diff = Math.floor((now - d) / 1000);
|
|
191
|
+
if (diff < 0) return 'just now';
|
|
180
192
|
if (diff < 60) return diff + 's ago';
|
|
181
193
|
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
|
|
182
194
|
if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
|
|
@@ -208,6 +220,13 @@ WE._closeMoreTabsOutside = function(e) {
|
|
|
208
220
|
if (!document.getElementById('we-more-wrap').contains(e.target)) WE.closeMoreTabs();
|
|
209
221
|
};
|
|
210
222
|
|
|
223
|
+
// Stop background pollers when leaving WALL-E tab (prevents jank in session terminals)
|
|
224
|
+
WE.pausePollers = function() {
|
|
225
|
+
Object.keys(_logPollers).forEach(_stopLogPoller);
|
|
226
|
+
if (_taskRefreshTimer) { clearInterval(_taskRefreshTimer); _taskRefreshTimer = null; }
|
|
227
|
+
if (_thinkingTimer) { clearInterval(_thinkingTimer); _thinkingTimer = null; }
|
|
228
|
+
};
|
|
229
|
+
|
|
211
230
|
WE.showView = function(view) {
|
|
212
231
|
// Clean up task pollers/timers when leaving tasks view
|
|
213
232
|
if (currentView === 'tasks' && view !== 'tasks') {
|
|
@@ -804,11 +823,13 @@ WE._toggleAnswered = function() {
|
|
|
804
823
|
WE.renderStatus = function() {
|
|
805
824
|
Promise.all([
|
|
806
825
|
api('/status'),
|
|
807
|
-
api('/stats')
|
|
826
|
+
api('/stats'),
|
|
827
|
+
api('/config')
|
|
808
828
|
]).then(function(results) {
|
|
809
829
|
var status = results[0].data || results[0] || {};
|
|
810
830
|
var stats = results[1].data || results[1] || {};
|
|
811
|
-
|
|
831
|
+
var config = results[2].data || results[2] || {};
|
|
832
|
+
renderStatusContent(status, stats, config);
|
|
812
833
|
// Try to load daily brief
|
|
813
834
|
var today = new Date().toISOString().slice(0, 10);
|
|
814
835
|
api('/brief?date=' + today).then(function(brief) {
|
|
@@ -833,7 +854,7 @@ WE.renderStatus = function() {
|
|
|
833
854
|
});
|
|
834
855
|
};
|
|
835
856
|
|
|
836
|
-
function renderStatusContent(status, stats) {
|
|
857
|
+
function renderStatusContent(status, stats, config) {
|
|
837
858
|
var body = document.getElementById('walle-body');
|
|
838
859
|
if (!body) return;
|
|
839
860
|
|
|
@@ -867,11 +888,133 @@ function renderStatusContent(status, stats) {
|
|
|
867
888
|
html += '</div>';
|
|
868
889
|
}
|
|
869
890
|
|
|
891
|
+
// Channels settings
|
|
892
|
+
html += renderChannelsSettings(config);
|
|
893
|
+
|
|
870
894
|
html += '<div id="walle-daily-brief"><div class="walle-empty">No daily summary yet.</div></div>';
|
|
871
895
|
|
|
872
896
|
safeSetHtml(body, html);
|
|
873
897
|
}
|
|
874
898
|
|
|
899
|
+
function renderChannelsSettings(config) {
|
|
900
|
+
var channels = (config && config.channels) || {};
|
|
901
|
+
var tg = channels.telegram || {};
|
|
902
|
+
var slack = channels.slack_dm || {};
|
|
903
|
+
var imsg = channels.imessage || {};
|
|
904
|
+
|
|
905
|
+
var html = '<div class="walle-section-title" style="margin-top:16px">Channels</div>';
|
|
906
|
+
html += '<div class="walle-card" style="padding:12px 16px">';
|
|
907
|
+
|
|
908
|
+
// Telegram
|
|
909
|
+
html += '<div style="margin-bottom:14px">';
|
|
910
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">';
|
|
911
|
+
html += '<strong style="font-size:13px">Telegram</strong>';
|
|
912
|
+
html += '<label style="display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer">';
|
|
913
|
+
html += '<input type="checkbox" id="we-cfg-tg-enabled" ' + (tg.enabled ? 'checked' : '') + '> Enabled';
|
|
914
|
+
html += '</label>';
|
|
915
|
+
html += '</div>';
|
|
916
|
+
html += '<div style="display:flex;flex-direction:column;gap:6px">';
|
|
917
|
+
html += '<div style="display:flex;align-items:center;gap:8px">';
|
|
918
|
+
html += '<label style="font-size:11px;color:var(--fg-muted,#888);width:80px;flex-shrink:0">Bot Token</label>';
|
|
919
|
+
html += '<input type="password" id="we-cfg-tg-token" value="' + esc(tg.bot_token || '') + '" placeholder="From @BotFather" style="flex:1;font-size:12px;padding:4px 8px;background:var(--bg-input,#1a1a2e);color:var(--fg,#e0e0e0);border:1px solid var(--border,#333);border-radius:4px">';
|
|
920
|
+
html += '</div>';
|
|
921
|
+
html += '<div style="display:flex;align-items:center;gap:8px">';
|
|
922
|
+
html += '<label style="font-size:11px;color:var(--fg-muted,#888);width:80px;flex-shrink:0">Owner Chat ID</label>';
|
|
923
|
+
html += '<input type="text" id="we-cfg-tg-chatid" value="' + esc(String(tg.owner_chat_id || '')) + '" placeholder="Send /whoami to your bot" style="flex:1;font-size:12px;padding:4px 8px;background:var(--bg-input,#1a1a2e);color:var(--fg,#e0e0e0);border:1px solid var(--border,#333);border-radius:4px">';
|
|
924
|
+
html += '</div>';
|
|
925
|
+
html += '<div style="font-size:10px;color:var(--fg-dim,#666);margin-top:2px">Create a bot via <a href="https://t.me/BotFather" target="_blank" style="color:var(--accent,#7b68ee)">@BotFather</a>, then message it and send /whoami to get your chat ID.</div>';
|
|
926
|
+
html += '</div></div>';
|
|
927
|
+
|
|
928
|
+
// Divider
|
|
929
|
+
html += '<div style="border-top:1px solid var(--border,#333);margin:10px 0"></div>';
|
|
930
|
+
|
|
931
|
+
// iMessage
|
|
932
|
+
html += '<div style="margin-bottom:14px">';
|
|
933
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">';
|
|
934
|
+
html += '<strong style="font-size:13px">iMessage</strong>';
|
|
935
|
+
html += '<label style="display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer">';
|
|
936
|
+
html += '<input type="checkbox" id="we-cfg-imsg-enabled" ' + (imsg.enabled ? 'checked' : '') + '> Enabled';
|
|
937
|
+
html += '</label>';
|
|
938
|
+
html += '</div>';
|
|
939
|
+
html += '<div style="display:flex;align-items:center;gap:8px">';
|
|
940
|
+
html += '<label style="font-size:11px;color:var(--fg-muted,#888);width:80px;flex-shrink:0">Buddy ID</label>';
|
|
941
|
+
html += '<input type="text" id="we-cfg-imsg-buddy" value="' + esc(imsg.buddy_id || '') + '" placeholder="Phone or email" style="flex:1;font-size:12px;padding:4px 8px;background:var(--bg-input,#1a1a2e);color:var(--fg,#e0e0e0);border:1px solid var(--border,#333);border-radius:4px">';
|
|
942
|
+
html += '</div></div>';
|
|
943
|
+
|
|
944
|
+
// Divider
|
|
945
|
+
html += '<div style="border-top:1px solid var(--border,#333);margin:10px 0"></div>';
|
|
946
|
+
|
|
947
|
+
// Slack DM
|
|
948
|
+
html += '<div style="margin-bottom:14px">';
|
|
949
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">';
|
|
950
|
+
html += '<strong style="font-size:13px">Slack DM</strong>';
|
|
951
|
+
html += '<label style="display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer">';
|
|
952
|
+
html += '<input type="checkbox" id="we-cfg-slack-enabled" ' + (slack.enabled ? 'checked' : '') + '> Enabled';
|
|
953
|
+
html += '</label>';
|
|
954
|
+
html += '</div>';
|
|
955
|
+
html += '<div style="display:flex;align-items:center;gap:8px">';
|
|
956
|
+
html += '<label style="font-size:11px;color:var(--fg-muted,#888);width:80px;flex-shrink:0">Bot Token</label>';
|
|
957
|
+
html += '<input type="password" id="we-cfg-slack-token" value="' + esc(slack.bot_token || '') + '" placeholder="xoxb-..." style="flex:1;font-size:12px;padding:4px 8px;background:var(--bg-input,#1a1a2e);color:var(--fg,#e0e0e0);border:1px solid var(--border,#333);border-radius:4px">';
|
|
958
|
+
html += '</div></div>';
|
|
959
|
+
|
|
960
|
+
// Save button
|
|
961
|
+
html += '<div style="display:flex;gap:8px;margin-top:12px">';
|
|
962
|
+
html += '<button class="walle-btn primary" onclick="WE._saveChannelConfig()" id="we-cfg-save-btn">Save</button>';
|
|
963
|
+
html += '<span id="we-cfg-save-msg" style="font-size:11px;color:var(--fg-muted,#888);align-self:center"></span>';
|
|
964
|
+
html += '</div>';
|
|
965
|
+
|
|
966
|
+
html += '</div>';
|
|
967
|
+
return html;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
WE._saveChannelConfig = function() {
|
|
971
|
+
var payload = {
|
|
972
|
+
channels: {
|
|
973
|
+
telegram: {
|
|
974
|
+
enabled: document.getElementById('we-cfg-tg-enabled').checked,
|
|
975
|
+
bot_token: document.getElementById('we-cfg-tg-token').value.trim(),
|
|
976
|
+
owner_chat_id: parseInt(document.getElementById('we-cfg-tg-chatid').value.trim(), 10) || null
|
|
977
|
+
},
|
|
978
|
+
imessage: {
|
|
979
|
+
enabled: document.getElementById('we-cfg-imsg-enabled').checked,
|
|
980
|
+
buddy_id: document.getElementById('we-cfg-imsg-buddy').value.trim()
|
|
981
|
+
},
|
|
982
|
+
slack_dm: {
|
|
983
|
+
enabled: document.getElementById('we-cfg-slack-enabled').checked,
|
|
984
|
+
bot_token: document.getElementById('we-cfg-slack-token').value.trim()
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
var btn = document.getElementById('we-cfg-save-btn');
|
|
990
|
+
var msg = document.getElementById('we-cfg-save-msg');
|
|
991
|
+
btn.disabled = true;
|
|
992
|
+
btn.textContent = 'Saving...';
|
|
993
|
+
msg.textContent = '';
|
|
994
|
+
|
|
995
|
+
fetch('/api/wall-e/config', {
|
|
996
|
+
method: 'PUT',
|
|
997
|
+
headers: { 'Content-Type': 'application/json' },
|
|
998
|
+
body: JSON.stringify(payload)
|
|
999
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
1000
|
+
btn.disabled = false;
|
|
1001
|
+
btn.textContent = 'Save';
|
|
1002
|
+
if (data.error) {
|
|
1003
|
+
msg.textContent = 'Error: ' + data.error;
|
|
1004
|
+
msg.style.color = '#e74c3c';
|
|
1005
|
+
} else {
|
|
1006
|
+
msg.textContent = data.message || 'Saved!';
|
|
1007
|
+
msg.style.color = '#2ecc71';
|
|
1008
|
+
setTimeout(function() { msg.textContent = ''; }, 4000);
|
|
1009
|
+
}
|
|
1010
|
+
}).catch(function(err) {
|
|
1011
|
+
btn.disabled = false;
|
|
1012
|
+
btn.textContent = 'Save';
|
|
1013
|
+
msg.textContent = 'Error: ' + err.message;
|
|
1014
|
+
msg.style.color = '#e74c3c';
|
|
1015
|
+
});
|
|
1016
|
+
};
|
|
1017
|
+
|
|
875
1018
|
// ---- Chat View ----
|
|
876
1019
|
var chatHistory = [];
|
|
877
1020
|
var chatHistoryLoaded = false;
|
|
@@ -882,6 +1025,39 @@ var activeChatController = null; // AbortController for in-flight request
|
|
|
882
1025
|
var lastEscTime = 0; // for double-ESC detection
|
|
883
1026
|
var chatAttachments = []; // pending image/file attachments [{type, name, data}]
|
|
884
1027
|
var editingMessageIndex = -1; // index of message being edited, -1 = none
|
|
1028
|
+
// ChatGPT-style branching: when user edits & resends, old continuation is saved
|
|
1029
|
+
// chatBranches[splitIndex] = [ [branch0_messages], [branch1_messages], ... ]
|
|
1030
|
+
// chatBranchActive[splitIndex] = which branch index is currently shown
|
|
1031
|
+
var chatBranches = {}; // { splitIndex: [ [...msgs], [...msgs] ] }
|
|
1032
|
+
var chatBranchActive = {}; // { splitIndex: activeIdx }
|
|
1033
|
+
|
|
1034
|
+
// Persist branches to SQLite via API so they survive page reloads.
|
|
1035
|
+
// Also saves a snapshot of chatHistory so it can be reconstructed if chat_messages
|
|
1036
|
+
// table gets cleared (e.g. by a failed _syncDbFull).
|
|
1037
|
+
function _saveBranches() {
|
|
1038
|
+
apiPost('/chat/branches', {
|
|
1039
|
+
session_id: 'default',
|
|
1040
|
+
branches: chatBranches,
|
|
1041
|
+
active: chatBranchActive,
|
|
1042
|
+
history: chatHistory,
|
|
1043
|
+
}).catch(function(e) { console.warn('[WALL-E] Branch save failed:', e); });
|
|
1044
|
+
}
|
|
1045
|
+
function _loadBranches(callback) {
|
|
1046
|
+
api('/chat/branches?session_id=default').then(function(resp) {
|
|
1047
|
+
var d = resp.data || {};
|
|
1048
|
+
chatBranches = d.branches || {};
|
|
1049
|
+
chatBranchActive = d.active || {};
|
|
1050
|
+
// If chatHistory is empty but we have a saved snapshot, restore from it
|
|
1051
|
+
if (chatHistory.length === 0 && Array.isArray(d.history) && d.history.length > 0) {
|
|
1052
|
+
chatHistory = d.history;
|
|
1053
|
+
}
|
|
1054
|
+
if (callback) callback();
|
|
1055
|
+
}).catch(function() {
|
|
1056
|
+
chatBranches = {};
|
|
1057
|
+
chatBranchActive = {};
|
|
1058
|
+
if (callback) callback();
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
885
1061
|
var chatSearchQuery = ''; // current search query
|
|
886
1062
|
var chatSearchResults = []; // search results from API
|
|
887
1063
|
var chatSearchDone = false; // true when search has completed
|
|
@@ -918,7 +1094,21 @@ WE.renderChat = function() {
|
|
|
918
1094
|
}
|
|
919
1095
|
}
|
|
920
1096
|
}
|
|
921
|
-
|
|
1097
|
+
// Load branches from DB, reconcile chatHistory with active branch, then render
|
|
1098
|
+
_loadBranches(function() {
|
|
1099
|
+
// Reconstruct chatHistory from active branch data if available.
|
|
1100
|
+
// This handles cases where chat_messages DB was cleared but branches survived.
|
|
1101
|
+
var splitKeys = Object.keys(chatBranches).map(Number).sort(function(a,b){return a-b;});
|
|
1102
|
+
for (var si = 0; si < splitKeys.length; si++) {
|
|
1103
|
+
var splitIdx = splitKeys[si];
|
|
1104
|
+
var activeIdx = chatBranchActive[splitIdx] || 0;
|
|
1105
|
+
var branchTail = (chatBranches[splitIdx] || [])[activeIdx];
|
|
1106
|
+
if (Array.isArray(branchTail) && branchTail.length > 0 && branchTail[0] && branchTail[0].role) {
|
|
1107
|
+
chatHistory = chatHistory.slice(0, splitIdx).concat(branchTail);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
renderChatUI();
|
|
1111
|
+
});
|
|
922
1112
|
}).catch(function() { renderChatUI(); });
|
|
923
1113
|
return;
|
|
924
1114
|
}
|
|
@@ -991,6 +1181,16 @@ function renderChatUI() {
|
|
|
991
1181
|
html += '<div class="walle-chat-msg-role user">You';
|
|
992
1182
|
html += ' <button class="we-edit-btn" onclick="WE._editMessage(' + i + ')" title="Edit & resend">✎</button>';
|
|
993
1183
|
html += ' <button class="we-edit-btn we-delete-btn" onclick="WE._deleteMessage(' + i + ')" title="Delete this exchange">🗑</button>';
|
|
1184
|
+
// Branch navigation arrows (ChatGPT-style)
|
|
1185
|
+
if (chatBranches[i] && chatBranches[i].length > 1) {
|
|
1186
|
+
var bIdx = chatBranchActive[i] || 0;
|
|
1187
|
+
var bTotal = chatBranches[i].length;
|
|
1188
|
+
html += '<span class="we-branch-nav">';
|
|
1189
|
+
html += '<button class="we-branch-btn' + (bIdx <= 0 ? ' disabled' : '') + '" onclick="event.stopPropagation();WE._switchBranch(' + i + ',-1)" title="Previous version">‹</button>';
|
|
1190
|
+
html += '<span class="we-branch-label">' + (bIdx + 1) + '/' + bTotal + '</span>';
|
|
1191
|
+
html += '<button class="we-branch-btn' + (bIdx >= bTotal - 1 ? ' disabled' : '') + '" onclick="event.stopPropagation();WE._switchBranch(' + i + ',1)" title="Next version">›</button>';
|
|
1192
|
+
html += '</span>';
|
|
1193
|
+
}
|
|
994
1194
|
html += '</div>';
|
|
995
1195
|
if (editingMessageIndex === i) {
|
|
996
1196
|
html += '<div class="we-edit-row">';
|
|
@@ -1411,10 +1611,25 @@ function _finishStreaming(reply) {
|
|
|
1411
1611
|
chatThinkingState.startTime = null;
|
|
1412
1612
|
if (reply) {
|
|
1413
1613
|
chatHistory.push({ role: 'assistant', text: reply });
|
|
1614
|
+
// Update the active branch with the assistant reply
|
|
1615
|
+
_updateActiveBranch();
|
|
1414
1616
|
}
|
|
1415
1617
|
WE.renderChat();
|
|
1416
1618
|
}
|
|
1417
1619
|
|
|
1620
|
+
// Keep active branch data in sync with chatHistory
|
|
1621
|
+
function _updateActiveBranch() {
|
|
1622
|
+
Object.keys(chatBranches).forEach(function(k) {
|
|
1623
|
+
var splitIdx = parseInt(k);
|
|
1624
|
+
var activeIdx = chatBranchActive[splitIdx] || 0;
|
|
1625
|
+
// Update the active branch's messages from the current chatHistory
|
|
1626
|
+
if (splitIdx < chatHistory.length) {
|
|
1627
|
+
chatBranches[splitIdx][activeIdx] = chatHistory.slice(splitIdx);
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
_saveBranches();
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1418
1633
|
WE._sendChat = function() {
|
|
1419
1634
|
var input = document.getElementById('walle-chat-input');
|
|
1420
1635
|
if (!input || (!input.value.trim() && chatAttachments.length === 0)) return;
|
|
@@ -1524,9 +1739,27 @@ WE._submitEdit = function(idx) {
|
|
|
1524
1739
|
var newText = editInput.value.trim();
|
|
1525
1740
|
editingMessageIndex = -1;
|
|
1526
1741
|
|
|
1742
|
+
// Save old continuation as a branch before truncating
|
|
1743
|
+
var oldTail = chatHistory.slice(idx); // includes the old user msg + assistant reply + rest
|
|
1744
|
+
if (oldTail.length > 0) {
|
|
1745
|
+
if (!chatBranches[idx]) {
|
|
1746
|
+
// First edit at this point — save the original as branch 0
|
|
1747
|
+
chatBranches[idx] = [oldTail];
|
|
1748
|
+
chatBranchActive[idx] = 1; // new branch will be index 1
|
|
1749
|
+
} else {
|
|
1750
|
+
chatBranchActive[idx] = chatBranches[idx].length; // new branch index
|
|
1751
|
+
}
|
|
1752
|
+
// New branch starts with just the user message — assistant reply added by _finishStreaming
|
|
1753
|
+
chatBranches[idx].push([{ role: 'user', text: newText }]);
|
|
1754
|
+
_saveBranches();
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1527
1757
|
// Branch: truncate history from this point and resend
|
|
1528
|
-
|
|
1529
|
-
chatHistory.
|
|
1758
|
+
var prefix = chatHistory.slice(0, idx); // messages to preserve
|
|
1759
|
+
chatHistory = prefix.concat([{ role: 'user', text: newText }]);
|
|
1760
|
+
|
|
1761
|
+
// Sync DB: clear and re-insert the prefix. The backend's chat() will add user+assistant.
|
|
1762
|
+
_syncDbPrefix(prefix);
|
|
1530
1763
|
|
|
1531
1764
|
// Add to user message history
|
|
1532
1765
|
if (userMessageHistory.length === 0 || userMessageHistory[0] !== newText) {
|
|
@@ -1537,6 +1770,63 @@ WE._submitEdit = function(idx) {
|
|
|
1537
1770
|
_streamChat(newText, []);
|
|
1538
1771
|
};
|
|
1539
1772
|
|
|
1773
|
+
// Switch to a different branch at a given split point
|
|
1774
|
+
WE._switchBranch = function(splitIdx, direction) {
|
|
1775
|
+
var branches = chatBranches[splitIdx];
|
|
1776
|
+
if (!branches) return;
|
|
1777
|
+
var cur = chatBranchActive[splitIdx] || 0;
|
|
1778
|
+
var next = cur + direction;
|
|
1779
|
+
if (next < 0 || next >= branches.length) return;
|
|
1780
|
+
|
|
1781
|
+
// Save current tail back to its branch slot
|
|
1782
|
+
chatBranches[splitIdx][cur] = chatHistory.slice(splitIdx);
|
|
1783
|
+
|
|
1784
|
+
// Switch to the target branch
|
|
1785
|
+
chatBranchActive[splitIdx] = next;
|
|
1786
|
+
chatHistory = chatHistory.slice(0, splitIdx).concat(branches[next]);
|
|
1787
|
+
|
|
1788
|
+
// Clear any sub-branches that started after this split point
|
|
1789
|
+
// (they belong to the old branch's indices and are invalid now)
|
|
1790
|
+
Object.keys(chatBranches).forEach(function(k) {
|
|
1791
|
+
if (parseInt(k) > splitIdx) {
|
|
1792
|
+
delete chatBranches[k];
|
|
1793
|
+
delete chatBranchActive[k];
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
_saveBranches();
|
|
1798
|
+
renderChatUI();
|
|
1799
|
+
// Note: we intentionally do NOT call _syncDbFull() here.
|
|
1800
|
+
// _syncDbFull clears chat_messages then re-inserts — if inserts fail, the DB
|
|
1801
|
+
// is left empty and the next reload loses history. Branch data (persisted via
|
|
1802
|
+
// _saveBranches) is sufficient; chatHistory is reconstructed on load.
|
|
1803
|
+
};
|
|
1804
|
+
|
|
1805
|
+
// Sync DB: clear session and re-insert messages. In-memory chatHistory is the
|
|
1806
|
+
// source of truth; DB sync is best-effort. Errors are logged but don't block UI.
|
|
1807
|
+
function _syncDbPrefix(messages) {
|
|
1808
|
+
apiPost('/chat/clear', { session_id: 'default' }).then(function() {
|
|
1809
|
+
var chain = Promise.resolve();
|
|
1810
|
+
messages.forEach(function(msg) {
|
|
1811
|
+
chain = chain.then(function() {
|
|
1812
|
+
return apiPost('/chat/insert', {
|
|
1813
|
+
session_id: 'default',
|
|
1814
|
+
role: msg.role,
|
|
1815
|
+
content: msg.text,
|
|
1816
|
+
});
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
return chain;
|
|
1820
|
+
}).catch(function(err) {
|
|
1821
|
+
console.warn('[WALL-E] DB sync failed (in-memory history is unaffected):', err);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Sync DB with full chatHistory (used after branch switch, not during edit)
|
|
1826
|
+
function _syncDbFull() {
|
|
1827
|
+
_syncDbPrefix(chatHistory);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1540
1830
|
// ---- Chat Select & Export ----
|
|
1541
1831
|
WE._toggleSelectMode = function() {
|
|
1542
1832
|
chatSelectMode = !chatSelectMode;
|
|
@@ -1856,6 +2146,15 @@ WE.renderTasks = function() {
|
|
|
1856
2146
|
]).then(function(results) {
|
|
1857
2147
|
var tasks = results[0].data || [];
|
|
1858
2148
|
var items = results[1].data || [];
|
|
2149
|
+
// Rewrite Slack URLs to enterprise domain across all text fields
|
|
2150
|
+
tasks.forEach(function(t) {
|
|
2151
|
+
if (t.source === 'slack') {
|
|
2152
|
+
var fix = function(s) { return s ? s.replace(/https:\/\/(\w+)\.slack\.com\//g, 'https://$1.enterprise.slack.com/') : s; };
|
|
2153
|
+
t.source_ref = fix(t.source_ref);
|
|
2154
|
+
t.description = fix(t.description);
|
|
2155
|
+
t.result = fix(t.result);
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
1859
2158
|
cache.allTasks = tasks;
|
|
1860
2159
|
cache.briefingItems = items;
|
|
1861
2160
|
// Index items by task_id and skill for quick lookup
|
|
@@ -1919,18 +2218,20 @@ WE._reconnectSlack = function() {
|
|
|
1919
2218
|
if (resp.data && resp.data.authenticated) {
|
|
1920
2219
|
clearInterval(poll);
|
|
1921
2220
|
if (typeof showToast === 'function') showToast('Slack reconnected! Resuming tasks...', '#5c940d', 5000);
|
|
1922
|
-
//
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
var
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2221
|
+
// Fetch fresh task list, then resume paused/failed Slack tasks and clear errors
|
|
2222
|
+
api('/tasks').then(function(taskResp) {
|
|
2223
|
+
var freshTasks = (taskResp.data || taskResp || []);
|
|
2224
|
+
var updates = [];
|
|
2225
|
+
freshTasks.forEach(function(t) {
|
|
2226
|
+
var isSlackTask = t.script && t.script.includes('slack');
|
|
2227
|
+
var needsResume = isSlackTask && (t.status === 'paused' || t.status === 'failed');
|
|
2228
|
+
var needsClearError = isSlackTask && t.error && (t.error.includes('Slack token expired') || t.error.includes('invalid_auth'));
|
|
2229
|
+
if (needsResume || needsClearError) {
|
|
2230
|
+
updates.push(apiPut('/tasks/' + t.id, { status: 'pending', error: null, result: null }));
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
return Promise.all(updates);
|
|
2234
|
+
}).then(function() {
|
|
1934
2235
|
WE.renderTasks();
|
|
1935
2236
|
}).catch(function() {
|
|
1936
2237
|
WE.renderTasks();
|
|
@@ -2093,11 +2394,25 @@ function _renderTasksContentInner(tasks) {
|
|
|
2093
2394
|
html += '</div>';
|
|
2094
2395
|
}
|
|
2095
2396
|
|
|
2096
|
-
// Slack auth banner
|
|
2097
|
-
var
|
|
2098
|
-
return t.
|
|
2397
|
+
// Slack auth banner — only show for tasks that are still actionable (not completed/cancelled)
|
|
2398
|
+
var slackErrorTasks = allTasks.filter(function(t) {
|
|
2399
|
+
return t.status !== 'completed' && t.status !== 'cancelled' && t.status !== 'dismissed' &&
|
|
2400
|
+
t.error && (t.error.includes('Slack token expired') || t.error.includes('invalid_auth'));
|
|
2099
2401
|
});
|
|
2100
|
-
if (
|
|
2402
|
+
if (slackErrorTasks.length > 0) {
|
|
2403
|
+
// Check if Slack is actually connected now — if so, auto-clear stale errors
|
|
2404
|
+
if (!WE._slackAutoClearing) {
|
|
2405
|
+
WE._slackAutoClearing = true;
|
|
2406
|
+
api('/slack/status').then(function(resp) {
|
|
2407
|
+
WE._slackAutoClearing = false;
|
|
2408
|
+
if (resp.data && resp.data.authenticated) {
|
|
2409
|
+
var updates = slackErrorTasks.map(function(t) {
|
|
2410
|
+
return apiPut('/tasks/' + t.id, { status: 'pending', error: null, result: null });
|
|
2411
|
+
});
|
|
2412
|
+
Promise.all(updates).then(function() { WE.renderTasks(); });
|
|
2413
|
+
}
|
|
2414
|
+
}).catch(function() { WE._slackAutoClearing = false; });
|
|
2415
|
+
}
|
|
2101
2416
|
html += '<div class="we-task-alert">';
|
|
2102
2417
|
html += '<span class="we-task-alert-icon">\u26A0\uFE0F</span>';
|
|
2103
2418
|
html += '<span class="we-task-alert-text">Slack disconnected — tasks paused. </span>';
|
|
@@ -2615,10 +2930,9 @@ function renderTaskCard(t) {
|
|
|
2615
2930
|
html += '<span class="we-task-running-timer" data-start-ms="' + startMs + '" style="color:#228be6">\u23F3 ' + elStr + '</span>';
|
|
2616
2931
|
}
|
|
2617
2932
|
if (t.run_count > 0 && !isExpanded) html += '<span>' + t.run_count + ' runs</span>';
|
|
2618
|
-
// Slack source_ref link
|
|
2933
|
+
// Slack source_ref link
|
|
2619
2934
|
if (t.source === 'slack' && t.source_ref) {
|
|
2620
|
-
|
|
2621
|
-
html += '<a class="we-src-link" href="' + esc(slackUrl) + '" target="_blank" onclick="event.stopPropagation()" title="Open Slack thread">\u2197 thread</a>';
|
|
2935
|
+
html += '<a class="we-src-link" href="' + esc(t.source_ref) + '" target="_blank" onclick="event.stopPropagation()" title="Open Slack thread">\u2197 thread</a>';
|
|
2622
2936
|
}
|
|
2623
2937
|
// Compact inline actions
|
|
2624
2938
|
if (!isExpanded) {
|
|
@@ -3271,77 +3585,191 @@ WE._saveTaskEdit = function(id) {
|
|
|
3271
3585
|
};
|
|
3272
3586
|
|
|
3273
3587
|
// ---- Skills View ----
|
|
3588
|
+
|
|
3589
|
+
// Skills state
|
|
3590
|
+
var _skillsData = [];
|
|
3591
|
+
var _skillsSuggestions = {};
|
|
3592
|
+
var _skillsFilter = { search: '', category: 'All', status: 'All', sort: 'name' };
|
|
3593
|
+
var _skillDetailOpen = null; // skill id or null
|
|
3594
|
+
var _skillDetailTab = 'overview';
|
|
3595
|
+
var _skillEditorMode = null; // null, 'create', or skill id
|
|
3596
|
+
|
|
3274
3597
|
WE.renderSkills = function() {
|
|
3275
3598
|
Promise.all([
|
|
3276
3599
|
api('/skills'),
|
|
3277
3600
|
api('/skills/suggestions')
|
|
3278
3601
|
]).then(function(results) {
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
renderSkillsContent(
|
|
3602
|
+
_skillsData = results[0].data || [];
|
|
3603
|
+
if (!Array.isArray(_skillsData)) _skillsData = [];
|
|
3604
|
+
_skillsSuggestions = results[1].data || {};
|
|
3605
|
+
renderSkillsContent(_skillsData, _skillsSuggestions);
|
|
3283
3606
|
}).catch(function(err) {
|
|
3284
3607
|
var body = document.getElementById('walle-body');
|
|
3285
3608
|
if (body) { body.textContent = ''; var d = document.createElement('div'); d.className = 'walle-empty'; d.textContent = 'Failed to load skills: ' + (err.message || ''); body.appendChild(d); }
|
|
3286
3609
|
});
|
|
3287
3610
|
};
|
|
3288
3611
|
|
|
3612
|
+
// Re-filter and re-render only the card list (no API call, preserves search focus)
|
|
3613
|
+
function _refilterSkillCards() {
|
|
3614
|
+
var container = document.getElementById('we-skills-card-list');
|
|
3615
|
+
if (!container) return;
|
|
3616
|
+
var filtered = filterSkills(_skillsData, _skillsFilter);
|
|
3617
|
+
var html = '';
|
|
3618
|
+
if (filtered.length === 0) {
|
|
3619
|
+
html = '<div class="walle-empty">No skills match your filters.</div>';
|
|
3620
|
+
}
|
|
3621
|
+
filtered.forEach(function(s) { html += renderSkillCard(s); });
|
|
3622
|
+
safeSetHtml(container, html);
|
|
3623
|
+
// Update category pills active state
|
|
3624
|
+
var pills = document.querySelectorAll('#we-skills-cat-pills .we-skills-pill');
|
|
3625
|
+
pills.forEach(function(pill) {
|
|
3626
|
+
if (pill.getAttribute('data-cat') === _skillsFilter.category) pill.classList.add('active');
|
|
3627
|
+
else pill.classList.remove('active');
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
function getSkillCategories(skills) {
|
|
3632
|
+
var cats = new Set();
|
|
3633
|
+
skills.forEach(function(s) {
|
|
3634
|
+
(s.tags || []).forEach(function(t) { cats.add(t); });
|
|
3635
|
+
});
|
|
3636
|
+
return ['All'].concat(Array.from(cats).sort());
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
function filterSkills(skills, f) {
|
|
3640
|
+
var result = skills;
|
|
3641
|
+
if (f.search) {
|
|
3642
|
+
var q = f.search.toLowerCase();
|
|
3643
|
+
result = result.filter(function(s) {
|
|
3644
|
+
return (s.name || '').toLowerCase().includes(q) ||
|
|
3645
|
+
(s.description || '').toLowerCase().includes(q) ||
|
|
3646
|
+
(s.tags || []).join(' ').toLowerCase().includes(q);
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
if (f.category && f.category !== 'All') {
|
|
3650
|
+
result = result.filter(function(s) { return (s.tags || []).indexOf(f.category) !== -1; });
|
|
3651
|
+
}
|
|
3652
|
+
if (f.status && f.status !== 'All') {
|
|
3653
|
+
if (f.status === 'Enabled') result = result.filter(function(s) { return s.enabled; });
|
|
3654
|
+
else if (f.status === 'Disabled') result = result.filter(function(s) { return !s.enabled; });
|
|
3655
|
+
else if (f.status === 'Failing') result = result.filter(function(s) { return s.last_result === 'failure'; });
|
|
3656
|
+
}
|
|
3657
|
+
// Sort
|
|
3658
|
+
result.sort(function(a, b) {
|
|
3659
|
+
if (f.sort === 'last_run') return (b.last_run || '').localeCompare(a.last_run || '');
|
|
3660
|
+
if (f.sort === 'runs') return ((b.success_count + b.failure_count) - (a.success_count + a.failure_count));
|
|
3661
|
+
if (f.sort === 'success') {
|
|
3662
|
+
var aRate = (a.success_count + a.failure_count) > 0 ? a.success_count / (a.success_count + a.failure_count) : -1;
|
|
3663
|
+
var bRate = (b.success_count + b.failure_count) > 0 ? b.success_count / (b.success_count + b.failure_count) : -1;
|
|
3664
|
+
return bRate - aRate;
|
|
3665
|
+
}
|
|
3666
|
+
return (a.name || '').localeCompare(b.name || '');
|
|
3667
|
+
});
|
|
3668
|
+
return result;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3289
3671
|
function renderSkillsContent(skills, suggestions) {
|
|
3290
3672
|
var body = document.getElementById('walle-body');
|
|
3291
3673
|
if (!body) return;
|
|
3292
3674
|
var html = '';
|
|
3293
3675
|
|
|
3676
|
+
// Analytics summary bar
|
|
3677
|
+
var totalSkills = skills.length;
|
|
3678
|
+
var enabledCount = skills.filter(function(s) { return s.enabled; }).length;
|
|
3679
|
+
var now = new Date();
|
|
3680
|
+
var day = 24 * 60 * 60 * 1000;
|
|
3681
|
+
var ran24h = skills.filter(function(s) { return s.last_run && (now - new Date(s.last_run)) < day; }).length;
|
|
3682
|
+
var totalRuns = 0, totalSuccess = 0, failingCount = 0;
|
|
3683
|
+
skills.forEach(function(s) {
|
|
3684
|
+
totalRuns += (s.success_count || 0) + (s.failure_count || 0);
|
|
3685
|
+
totalSuccess += s.success_count || 0;
|
|
3686
|
+
if (s.last_result === 'failure' && s.enabled) failingCount++;
|
|
3687
|
+
});
|
|
3688
|
+
var overallRate = totalRuns > 0 ? Math.round(totalSuccess / totalRuns * 100) : 0;
|
|
3689
|
+
|
|
3690
|
+
// Count by source
|
|
3691
|
+
var srcCounts = {};
|
|
3692
|
+
skills.forEach(function(s) { var src = s.source || 'unknown'; srcCounts[src] = (srcCounts[src] || 0) + 1; });
|
|
3693
|
+
var srcParts = Object.keys(srcCounts).map(function(k) { return srcCounts[k] + ' ' + k; });
|
|
3694
|
+
|
|
3695
|
+
html += '<div class="we-skills-analytics">';
|
|
3696
|
+
html += '<div class="we-skills-analytics-item"><div class="we-skills-analytics-value">' + totalSkills + '</div><div class="we-skills-analytics-label">Total (' + esc(srcParts.join(', ')) + ')</div></div>';
|
|
3697
|
+
html += '<div class="we-skills-analytics-item"><div class="we-skills-analytics-value">' + enabledCount + '</div><div class="we-skills-analytics-label">Enabled</div></div>';
|
|
3698
|
+
html += '<div class="we-skills-analytics-item"><div class="we-skills-analytics-value">' + ran24h + '</div><div class="we-skills-analytics-label">Run in 24h</div></div>';
|
|
3699
|
+
html += '<div class="we-skills-analytics-item"><div class="we-skills-analytics-value">' + overallRate + '%</div><div class="we-skills-analytics-label">Success Rate</div></div>';
|
|
3700
|
+
if (failingCount > 0) {
|
|
3701
|
+
html += '<div class="we-skills-analytics-item failing" onclick="WE._filterSkills(\'Failing\')"><div class="we-skills-analytics-value">' + failingCount + '</div><div class="we-skills-analytics-label">Failing</div></div>';
|
|
3702
|
+
}
|
|
3703
|
+
html += '</div>';
|
|
3704
|
+
|
|
3705
|
+
// Toolbar: search + filters + sort + actions
|
|
3706
|
+
var categories = getSkillCategories(skills);
|
|
3707
|
+
html += '<div class="we-skills-toolbar">';
|
|
3708
|
+
html += '<input type="text" class="we-skills-search" id="we-skills-search" placeholder="Search skills..." value="' + esc(_skillsFilter.search) + '">';
|
|
3709
|
+
|
|
3710
|
+
html += '<div class="we-skills-pills" id="we-skills-cat-pills">';
|
|
3711
|
+
categories.forEach(function(c) {
|
|
3712
|
+
var active = _skillsFilter.category === c ? ' active' : '';
|
|
3713
|
+
html += '<button class="we-skills-pill' + active + '" data-cat="' + esc(c) + '">' + esc(c) + '</button>';
|
|
3714
|
+
});
|
|
3715
|
+
html += '</div>';
|
|
3716
|
+
|
|
3717
|
+
html += '<select class="walle-filter-select" id="we-skills-status">';
|
|
3718
|
+
['All', 'Enabled', 'Disabled', 'Failing'].forEach(function(s) {
|
|
3719
|
+
html += '<option' + (_skillsFilter.status === s ? ' selected' : '') + '>' + s + '</option>';
|
|
3720
|
+
});
|
|
3721
|
+
html += '</select>';
|
|
3722
|
+
|
|
3723
|
+
html += '<select class="walle-filter-select" id="we-skills-sort">';
|
|
3724
|
+
[['name','Name'],['last_run','Last Run'],['success','Success Rate'],['runs','Run Count']].forEach(function(p) {
|
|
3725
|
+
html += '<option value="' + p[0] + '"' + (_skillsFilter.sort === p[0] ? ' selected' : '') + '>' + p[1] + '</option>';
|
|
3726
|
+
});
|
|
3727
|
+
html += '</select>';
|
|
3728
|
+
|
|
3729
|
+
html += '<button class="walle-btn primary" onclick="WE._openSkillEditor()">+ New Skill</button>';
|
|
3730
|
+
html += '<button class="walle-btn" onclick="WE._importSkillPrompt()">Import</button>';
|
|
3731
|
+
html += '</div>';
|
|
3732
|
+
|
|
3294
3733
|
// Check for Slack ingest skill and show special progress section
|
|
3295
3734
|
var slackSkill = skills.find(function(s) { return s.name === 'ingest-slack-history'; });
|
|
3296
3735
|
if (slackSkill) {
|
|
3297
|
-
html += '<div class="walle-section-title">Slack History Ingestion</div>';
|
|
3298
3736
|
html += '<div class="walle-card" id="walle-slack-ingest-card">';
|
|
3299
|
-
html += '<div class="walle-card-body"><div class="walle-loading">Loading progress...</div></div>';
|
|
3737
|
+
html += '<div class="walle-card-body"><div class="walle-loading">Loading Slack ingest progress...</div></div>';
|
|
3300
3738
|
html += '</div>';
|
|
3301
3739
|
}
|
|
3302
3740
|
|
|
3303
|
-
//
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3741
|
+
// Filtered skill cards (in a container for fast re-filter without full re-render)
|
|
3742
|
+
var filtered = filterSkills(skills, _skillsFilter);
|
|
3743
|
+
html += '<div id="we-skills-card-list">';
|
|
3744
|
+
if (filtered.length === 0) {
|
|
3745
|
+
html += '<div class="walle-empty">No skills match your filters.</div>';
|
|
3307
3746
|
}
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
? Math.round(s.success_count / (s.success_count + s.failure_count) * 100) + '%' : 'N/A';
|
|
3311
|
-
var enabledLabel = s.enabled ? 'Enabled' : 'Disabled';
|
|
3312
|
-
var enabledColor = s.enabled ? '#5c940d' : '#888';
|
|
3313
|
-
html += '<div class="walle-action-card">';
|
|
3314
|
-
html += '<div class="walle-action-header">';
|
|
3315
|
-
html += '<span class="walle-action-type">' + esc(s.name) + '</span>';
|
|
3316
|
-
html += '<span style="color:' + enabledColor + ';font-size:11px">' + enabledLabel + ' | Success: ' + esc(rate) + '</span>';
|
|
3317
|
-
html += '</div>';
|
|
3318
|
-
html += '<div class="walle-card-body">' + esc(s.description || '') + '</div>';
|
|
3319
|
-
html += '<div class="walle-card-meta">Trigger: ' + esc(s.trigger_type || 'interval') + ' | Runs: ' + (s.success_count + s.failure_count) + ' | Last: ' + esc(timeAgo(s.last_run)) + '</div>';
|
|
3320
|
-
html += '<div class="walle-action-buttons" style="margin-top:6px">';
|
|
3321
|
-
html += '<button class="walle-btn primary" onclick="WE._runSkill(\'' + esc(s.id) + '\')">Run Now</button>';
|
|
3322
|
-
html += '<button class="walle-btn" onclick="WE._toggleSkill(\'' + esc(s.id) + '\',' + (s.enabled ? 0 : 1) + ')">' + (s.enabled ? 'Disable' : 'Enable') + '</button>';
|
|
3323
|
-
html += '</div></div>';
|
|
3747
|
+
filtered.forEach(function(s) {
|
|
3748
|
+
html += renderSkillCard(s);
|
|
3324
3749
|
});
|
|
3750
|
+
html += '</div>';
|
|
3325
3751
|
|
|
3326
3752
|
// Suggestions from Claude Code
|
|
3327
|
-
var mcpServers = suggestions.mcpServers || [];
|
|
3328
|
-
var claudeSkills = suggestions.claudeSkills || [];
|
|
3329
|
-
var skillSuggestions = suggestions.suggestions || [];
|
|
3753
|
+
var mcpServers = (suggestions && suggestions.mcpServers) || [];
|
|
3754
|
+
var claudeSkills = (suggestions && suggestions.claudeSkills) || [];
|
|
3755
|
+
var skillSuggestions = (suggestions && suggestions.suggestions) || [];
|
|
3330
3756
|
|
|
3331
3757
|
if (mcpServers.length > 0 || claudeSkills.length > 0) {
|
|
3332
|
-
html += '<div class="walle-section-title">Claude Code Capabilities
|
|
3758
|
+
html += '<div class="walle-section-title">Claude Code Capabilities</div>';
|
|
3333
3759
|
if (mcpServers.length > 0) {
|
|
3334
3760
|
html += '<div class="walle-card"><div class="walle-card-title">MCP Servers (' + mcpServers.length + ')</div><div class="walle-card-body">';
|
|
3335
3761
|
mcpServers.forEach(function(m) { html += esc(m.name) + ' (' + esc(m.command) + ')<br>'; });
|
|
3336
3762
|
html += '</div></div>';
|
|
3337
3763
|
}
|
|
3338
3764
|
if (claudeSkills.length > 0) {
|
|
3339
|
-
html +=
|
|
3340
|
-
claudeSkills.forEach(function(s) { html += '<b>' + esc(s.name) + '</b>: ' + esc(s.description || '').slice(0, 100) + '<br>'; });
|
|
3341
|
-
html += '</div></div>';
|
|
3765
|
+
html += _renderClaudeSkillsSection(claudeSkills);
|
|
3342
3766
|
}
|
|
3343
3767
|
}
|
|
3344
3768
|
|
|
3769
|
+
// Filter out suggestions that already exist as learned skills
|
|
3770
|
+
var existingNames = new Set(skills.map(function(s) { return s.name; }));
|
|
3771
|
+
skillSuggestions = skillSuggestions.filter(function(s) { return !existingNames.has(s.name); });
|
|
3772
|
+
|
|
3345
3773
|
if (skillSuggestions.length > 0) {
|
|
3346
3774
|
html += '<div class="walle-section-title">Suggested Skills</div>';
|
|
3347
3775
|
skillSuggestions.forEach(function(s) {
|
|
@@ -3353,41 +3781,783 @@ function renderSkillsContent(skills, suggestions) {
|
|
|
3353
3781
|
});
|
|
3354
3782
|
}
|
|
3355
3783
|
|
|
3784
|
+
// Detail panel backdrop + panel container
|
|
3785
|
+
html += '<div class="we-skill-detail-backdrop" id="we-skill-backdrop" onclick="WE._closeSkillDetail()"></div>';
|
|
3786
|
+
html += '<div class="we-skill-detail" id="we-skill-detail"></div>';
|
|
3787
|
+
|
|
3356
3788
|
safeSetHtml(body, html);
|
|
3357
3789
|
|
|
3358
|
-
//
|
|
3790
|
+
// Bind search/filter events — use _refilterSkillCards() to avoid re-fetching and losing focus
|
|
3791
|
+
var searchInput = document.getElementById('we-skills-search');
|
|
3792
|
+
if (searchInput) {
|
|
3793
|
+
searchInput.addEventListener('input', function() {
|
|
3794
|
+
_skillsFilter.search = this.value;
|
|
3795
|
+
_refilterSkillCards();
|
|
3796
|
+
});
|
|
3797
|
+
}
|
|
3798
|
+
var catPills = document.querySelectorAll('#we-skills-cat-pills .we-skills-pill');
|
|
3799
|
+
catPills.forEach(function(pill) {
|
|
3800
|
+
pill.addEventListener('click', function() {
|
|
3801
|
+
_skillsFilter.category = this.getAttribute('data-cat');
|
|
3802
|
+
_refilterSkillCards();
|
|
3803
|
+
});
|
|
3804
|
+
});
|
|
3805
|
+
var statusSel = document.getElementById('we-skills-status');
|
|
3806
|
+
if (statusSel) statusSel.addEventListener('change', function() { _skillsFilter.status = this.value; _refilterSkillCards(); });
|
|
3807
|
+
var sortSel = document.getElementById('we-skills-sort');
|
|
3808
|
+
if (sortSel) sortSel.addEventListener('change', function() { _skillsFilter.sort = this.value; _refilterSkillCards(); });
|
|
3809
|
+
|
|
3810
|
+
// Bind Claude Code Skills search
|
|
3811
|
+
_bindCcSearch();
|
|
3812
|
+
|
|
3813
|
+
// Load Slack ingest progress if applicable
|
|
3359
3814
|
if (slackSkill) {
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3815
|
+
_loadSlackIngestProgress(slackSkill);
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
// Re-open detail panel if it was open
|
|
3819
|
+
if (_skillDetailOpen) {
|
|
3820
|
+
var detailSkill = skills.find(function(s) { return s.id === _skillDetailOpen; });
|
|
3821
|
+
if (detailSkill) _showSkillDetail(detailSkill);
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
function renderSkillCard(s) {
|
|
3826
|
+
var total = (s.success_count || 0) + (s.failure_count || 0);
|
|
3827
|
+
var rate = total > 0 ? Math.round(s.success_count / total * 100) : -1;
|
|
3828
|
+
var rateStr = rate >= 0 ? rate + '%' : 'N/A';
|
|
3829
|
+
var stripeClass = !s.enabled ? 'disabled' : (s.last_result === 'failure' ? 'failing' : 'healthy');
|
|
3830
|
+
var rateBarClass = rate >= 80 ? '' : rate >= 50 ? ' mid' : ' low';
|
|
3831
|
+
|
|
3832
|
+
var h = '<div class="we-skill-card" onclick="WE._openSkillDetail(\'' + esc(s.id) + '\')">';
|
|
3833
|
+
h += '<div class="we-skill-card-stripe ' + stripeClass + '"></div>';
|
|
3834
|
+
h += '<div class="we-skill-card-body">';
|
|
3835
|
+
|
|
3836
|
+
// Header: name + badges
|
|
3837
|
+
h += '<div class="we-skill-card-header">';
|
|
3838
|
+
h += '<span class="we-skill-card-name">' + esc(s.name) + '</span>';
|
|
3839
|
+
if (s.version && s.version !== '0.0.0') h += '<span class="we-skill-card-badge version">v' + esc(s.version) + '</span>';
|
|
3840
|
+
if (s.source) h += '<span class="we-skill-card-badge ' + esc(s.source) + '">' + esc(s.source) + '</span>';
|
|
3841
|
+
if (s.execution) h += '<span class="we-skill-card-badge ' + esc(s.execution) + '">' + esc(s.execution) + '</span>';
|
|
3842
|
+
h += '</div>';
|
|
3843
|
+
|
|
3844
|
+
// Tags
|
|
3845
|
+
var tags = s.tags || [];
|
|
3846
|
+
if (tags.length > 0) {
|
|
3847
|
+
h += '<div class="we-skill-card-tags">';
|
|
3848
|
+
tags.forEach(function(t) {
|
|
3849
|
+
var tagClass = '';
|
|
3850
|
+
if (['sync','data'].indexOf(t) !== -1) tagClass = ' ' + t;
|
|
3851
|
+
else if (['communication','slack','email'].indexOf(t) !== -1) tagClass = ' communication';
|
|
3852
|
+
else if (['coding','orchestration'].indexOf(t) !== -1) tagClass = ' coding';
|
|
3853
|
+
else if (['monitoring'].indexOf(t) !== -1) tagClass = ' monitoring';
|
|
3854
|
+
else if (['automation'].indexOf(t) !== -1) tagClass = ' automation';
|
|
3855
|
+
h += '<span class="we-skill-tag' + tagClass + '">' + esc(t) + '</span>';
|
|
3856
|
+
});
|
|
3857
|
+
h += '</div>';
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
// Description
|
|
3861
|
+
h += '<div class="we-skill-card-desc">' + esc(s.description || '') + '</div>';
|
|
3862
|
+
|
|
3863
|
+
// Stats row
|
|
3864
|
+
h += '<div class="we-skill-card-stats">';
|
|
3865
|
+
if (rate >= 0) {
|
|
3866
|
+
h += '<span><span class="we-skill-rate-bar"><span class="we-skill-rate-fill' + rateBarClass + '" style="width:' + rate + '%"></span></span> ' + rateStr + '</span>';
|
|
3867
|
+
}
|
|
3868
|
+
h += '<span>' + total + ' runs</span>';
|
|
3869
|
+
h += '<span>' + esc(timeAgo(s.last_run)) + '</span>';
|
|
3870
|
+
if (s.last_result) {
|
|
3871
|
+
var dotColor = s.last_result === 'success' ? '#5c940d' : '#e03131';
|
|
3872
|
+
h += '<span style="color:' + dotColor + '">●</span>';
|
|
3873
|
+
}
|
|
3874
|
+
h += '</div>';
|
|
3875
|
+
|
|
3876
|
+
h += '</div>'; // end card-body
|
|
3877
|
+
|
|
3878
|
+
// Actions column (stop propagation to prevent detail open)
|
|
3879
|
+
h += '<div class="we-skill-card-actions" onclick="event.stopPropagation()">';
|
|
3880
|
+
h += '<button class="walle-btn primary" style="padding:4px 10px;font-size:11px" onclick="WE._runSkill(\'' + esc(s.id) + '\')">Run</button>';
|
|
3881
|
+
h += '<label class="we-toggle"><input type="checkbox"' + (s.enabled ? ' checked' : '') + ' onchange="WE._toggleSkill(\'' + esc(s.id) + '\',this.checked?1:0)"><span class="we-toggle-slider"></span></label>';
|
|
3882
|
+
h += '<div class="we-skill-overflow">';
|
|
3883
|
+
h += '<button class="we-skill-overflow-btn" onclick="WE._toggleOverflow(this)">⋯</button>';
|
|
3884
|
+
h += '<div class="we-skill-overflow-menu">';
|
|
3885
|
+
h += '<div class="we-skill-overflow-item" onclick="WE._openSkillDetail(\'' + esc(s.id) + '\')">View Details</div>';
|
|
3886
|
+
if (s.source !== 'bundled') h += '<div class="we-skill-overflow-item" onclick="WE._openSkillEditor(\'' + esc(s.id) + '\')">Edit</div>';
|
|
3887
|
+
h += '<div class="we-skill-overflow-item" onclick="WE._exportSkill(\'' + esc(s.id) + '\')">Export</div>';
|
|
3888
|
+
if (s.source !== 'bundled') h += '<div class="we-skill-overflow-item danger" onclick="WE._deleteSkill(\'' + esc(s.id) + '\',\'' + esc(s.name) + '\')">Delete</div>';
|
|
3889
|
+
h += '</div></div>';
|
|
3890
|
+
h += '</div>';
|
|
3891
|
+
|
|
3892
|
+
h += '</div>'; // end card
|
|
3893
|
+
return h;
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
// ---- Claude Code Skills Section (grouped, collapsed, searchable) ----
|
|
3897
|
+
|
|
3898
|
+
function _categorizeClaudeSkill(s) {
|
|
3899
|
+
var n = (s.name || '').toLowerCase();
|
|
3900
|
+
var d = (s.description || '').toLowerCase();
|
|
3901
|
+
if (/financ|lbo|dcf|comps|earnings|model|valuation|bond|swap|portfolio|option|equity|fx|macro|tax/.test(n + ' ' + d)) return 'Finance';
|
|
3902
|
+
if (/invest|deal|cim|teaser|pitch|buyer|merger|ic.memo|process.letter|sector/.test(n + ' ' + d)) return 'Investment Banking';
|
|
3903
|
+
if (/slack|email|message|channel|standup|announcement/.test(n + ' ' + d)) return 'Communication';
|
|
3904
|
+
if (/code|review|pr|commit|debug|test|feature|git|lint/.test(n + ' ' + d)) return 'Development';
|
|
3905
|
+
if (/pua|ralph|agent|loop|high.agency/.test(n + ' ' + d)) return 'Productivity';
|
|
3906
|
+
if (/frontend|design|ux|ui|css|web/.test(n + ' ' + d)) return 'Design';
|
|
3907
|
+
if (/skill|plugin|setup|claude|config|management/.test(n + ' ' + d)) return 'System';
|
|
3908
|
+
if (/ppt|deck|pdf|sheet|xls|data.?pack|strip|report/.test(n + ' ' + d)) return 'Documents';
|
|
3909
|
+
return 'Other';
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
function _renderClaudeSkillsSection(claudeSkills) {
|
|
3913
|
+
// Group by category
|
|
3914
|
+
var groups = {};
|
|
3915
|
+
claudeSkills.forEach(function(s) {
|
|
3916
|
+
var cat = _categorizeClaudeSkill(s);
|
|
3917
|
+
if (!groups[cat]) groups[cat] = [];
|
|
3918
|
+
groups[cat].push(s);
|
|
3919
|
+
});
|
|
3920
|
+
var sortedCats = Object.keys(groups).sort();
|
|
3921
|
+
|
|
3922
|
+
var h = '<div class="we-cc-skills">';
|
|
3923
|
+
h += '<div class="we-cc-skills-header" onclick="WE._toggleCcSkills()">';
|
|
3924
|
+
h += '<span class="we-cc-skills-toggle" id="we-cc-toggle">▶</span> ';
|
|
3925
|
+
h += 'Claude Code Skills (' + claudeSkills.length + ')';
|
|
3926
|
+
h += '<input type="text" class="we-cc-search" id="we-cc-search" placeholder="Search..." onclick="event.stopPropagation()">';
|
|
3927
|
+
h += '</div>';
|
|
3928
|
+
h += '<div class="we-cc-skills-body" id="we-cc-body" style="display:none">';
|
|
3929
|
+
|
|
3930
|
+
sortedCats.forEach(function(cat) {
|
|
3931
|
+
var skills = groups[cat];
|
|
3932
|
+
h += '<div class="we-cc-group" data-cat="' + esc(cat) + '">';
|
|
3933
|
+
h += '<div class="we-cc-group-header" onclick="WE._toggleCcGroup(this)">';
|
|
3934
|
+
h += '<span class="we-cc-group-toggle">▶</span> ' + esc(cat) + ' <span class="we-cc-group-count">(' + skills.length + ')</span>';
|
|
3935
|
+
h += '</div>';
|
|
3936
|
+
h += '<div class="we-cc-group-body" style="display:none">';
|
|
3937
|
+
skills.forEach(function(s) {
|
|
3938
|
+
h += '<div class="we-cc-skill-card" data-name="' + esc(s.name) + '" data-desc="' + esc(s.description || '') + '">';
|
|
3939
|
+
h += '<div class="we-cc-skill-name">' + esc(s.name) + '</div>';
|
|
3940
|
+
h += '<div class="we-cc-skill-desc">' + esc((s.description || '').slice(0, 120)) + '</div>';
|
|
3941
|
+
h += '</div>';
|
|
3942
|
+
});
|
|
3943
|
+
h += '</div></div>';
|
|
3944
|
+
});
|
|
3945
|
+
|
|
3946
|
+
h += '</div></div>';
|
|
3947
|
+
return h;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
WE._toggleCcSkills = function() {
|
|
3951
|
+
var body = document.getElementById('we-cc-body');
|
|
3952
|
+
var toggle = document.getElementById('we-cc-toggle');
|
|
3953
|
+
if (!body) return;
|
|
3954
|
+
var show = body.style.display === 'none';
|
|
3955
|
+
body.style.display = show ? '' : 'none';
|
|
3956
|
+
if (toggle) toggle.textContent = show ? '▼' : '▶';
|
|
3957
|
+
};
|
|
3958
|
+
|
|
3959
|
+
WE._toggleCcGroup = function(header) {
|
|
3960
|
+
var body = header.nextElementSibling;
|
|
3961
|
+
var toggle = header.querySelector('.we-cc-group-toggle');
|
|
3962
|
+
if (!body) return;
|
|
3963
|
+
var show = body.style.display === 'none';
|
|
3964
|
+
body.style.display = show ? '' : 'none';
|
|
3965
|
+
if (toggle) toggle.textContent = show ? '▼' : '▶';
|
|
3966
|
+
};
|
|
3967
|
+
|
|
3968
|
+
// Bind search after render (called from renderSkillsContent event binding section)
|
|
3969
|
+
function _bindCcSearch() {
|
|
3970
|
+
var input = document.getElementById('we-cc-search');
|
|
3971
|
+
if (!input) return;
|
|
3972
|
+
input.addEventListener('input', function() {
|
|
3973
|
+
var q = this.value.toLowerCase();
|
|
3974
|
+
var body = document.getElementById('we-cc-body');
|
|
3975
|
+
if (!body) return;
|
|
3976
|
+
// Show the body if searching
|
|
3977
|
+
if (q && body.style.display === 'none') {
|
|
3978
|
+
body.style.display = '';
|
|
3979
|
+
var toggle = document.getElementById('we-cc-toggle');
|
|
3980
|
+
if (toggle) toggle.textContent = '▼';
|
|
3981
|
+
}
|
|
3982
|
+
// Filter cards and auto-expand matching groups
|
|
3983
|
+
var groups = body.querySelectorAll('.we-cc-group');
|
|
3984
|
+
groups.forEach(function(g) {
|
|
3985
|
+
var cards = g.querySelectorAll('.we-cc-skill-card');
|
|
3986
|
+
var visibleCount = 0;
|
|
3987
|
+
cards.forEach(function(c) {
|
|
3988
|
+
var name = (c.getAttribute('data-name') || '').toLowerCase();
|
|
3989
|
+
var desc = (c.getAttribute('data-desc') || '').toLowerCase();
|
|
3990
|
+
var match = !q || name.includes(q) || desc.includes(q);
|
|
3991
|
+
c.style.display = match ? '' : 'none';
|
|
3992
|
+
if (match) visibleCount++;
|
|
3993
|
+
});
|
|
3994
|
+
g.style.display = visibleCount > 0 ? '' : 'none';
|
|
3995
|
+
// Auto-expand groups with matches when searching
|
|
3996
|
+
var groupBody = g.querySelector('.we-cc-group-body');
|
|
3997
|
+
var groupToggle = g.querySelector('.we-cc-group-toggle');
|
|
3998
|
+
if (q && visibleCount > 0 && groupBody) {
|
|
3999
|
+
groupBody.style.display = '';
|
|
4000
|
+
if (groupToggle) groupToggle.textContent = '▼';
|
|
3379
4001
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
4002
|
+
// Update count
|
|
4003
|
+
var countEl = g.querySelector('.we-cc-group-count');
|
|
4004
|
+
if (countEl) countEl.textContent = q ? '(' + visibleCount + ')' : '(' + cards.length + ')';
|
|
4005
|
+
});
|
|
4006
|
+
});
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
WE._toggleOverflow = function(btn) {
|
|
4010
|
+
var menu = btn.nextElementSibling;
|
|
4011
|
+
// Close all other open menus first
|
|
4012
|
+
document.querySelectorAll('.we-skill-overflow-menu.open').forEach(function(m) {
|
|
4013
|
+
if (m !== menu) m.classList.remove('open');
|
|
4014
|
+
});
|
|
4015
|
+
menu.classList.toggle('open');
|
|
4016
|
+
// Close on outside click
|
|
4017
|
+
var closer = function(e) {
|
|
4018
|
+
if (!btn.contains(e.target) && !menu.contains(e.target)) {
|
|
4019
|
+
menu.classList.remove('open');
|
|
4020
|
+
document.removeEventListener('click', closer);
|
|
4021
|
+
}
|
|
4022
|
+
};
|
|
4023
|
+
setTimeout(function() { document.addEventListener('click', closer); }, 0);
|
|
4024
|
+
};
|
|
4025
|
+
|
|
4026
|
+
WE._filterSkills = function(status) {
|
|
4027
|
+
_skillsFilter.status = status;
|
|
4028
|
+
// Update dropdown to match
|
|
4029
|
+
var statusSel = document.getElementById('we-skills-status');
|
|
4030
|
+
if (statusSel) statusSel.value = status;
|
|
4031
|
+
_refilterSkillCards();
|
|
4032
|
+
};
|
|
4033
|
+
|
|
4034
|
+
// ---- Skill Detail Panel ----
|
|
4035
|
+
|
|
4036
|
+
WE._openSkillDetail = function(id) {
|
|
4037
|
+
var skill = _skillsData.find(function(s) { return s.id === id; });
|
|
4038
|
+
if (!skill) return;
|
|
4039
|
+
_skillDetailOpen = id;
|
|
4040
|
+
_showSkillDetail(skill);
|
|
4041
|
+
};
|
|
4042
|
+
|
|
4043
|
+
WE._closeSkillDetail = function() {
|
|
4044
|
+
_skillDetailOpen = null;
|
|
4045
|
+
var panel = document.getElementById('we-skill-detail');
|
|
4046
|
+
var backdrop = document.getElementById('we-skill-backdrop');
|
|
4047
|
+
if (panel) panel.classList.remove('open');
|
|
4048
|
+
if (backdrop) backdrop.classList.remove('open');
|
|
4049
|
+
};
|
|
4050
|
+
|
|
4051
|
+
function _showSkillDetail(skill) {
|
|
4052
|
+
var panel = document.getElementById('we-skill-detail');
|
|
4053
|
+
var backdrop = document.getElementById('we-skill-backdrop');
|
|
4054
|
+
if (!panel) return;
|
|
4055
|
+
|
|
4056
|
+
var tabs = ['overview', 'config', 'history', 'source', 'logs'];
|
|
4057
|
+
var h = '<div class="we-skill-detail-header">';
|
|
4058
|
+
h += '<button class="we-skill-detail-close" onclick="WE._closeSkillDetail()">✕</button>';
|
|
4059
|
+
h += '<div class="we-skill-detail-title">' + esc(skill.name) + '</div>';
|
|
4060
|
+
h += '<label class="we-toggle"><input type="checkbox"' + (skill.enabled ? ' checked' : '') + ' onchange="WE._toggleSkill(\'' + esc(skill.id) + '\',this.checked?1:0)"><span class="we-toggle-slider"></span></label>';
|
|
4061
|
+
h += '</div>';
|
|
4062
|
+
|
|
4063
|
+
h += '<div class="we-skill-detail-tabs">';
|
|
4064
|
+
tabs.forEach(function(t) {
|
|
4065
|
+
var active = _skillDetailTab === t ? ' active' : '';
|
|
4066
|
+
h += '<button class="we-skill-detail-tab' + active + '" onclick="WE._setDetailTab(\'' + t + '\',\'' + esc(skill.id) + '\')">' + t.charAt(0).toUpperCase() + t.slice(1) + '</button>';
|
|
4067
|
+
});
|
|
4068
|
+
h += '</div>';
|
|
4069
|
+
|
|
4070
|
+
h += '<div class="we-skill-detail-content" id="we-skill-detail-content">';
|
|
4071
|
+
h += _renderDetailTab(skill, _skillDetailTab);
|
|
4072
|
+
h += '</div>';
|
|
4073
|
+
|
|
4074
|
+
safeSetHtml(panel, h);
|
|
4075
|
+
panel.classList.add('open');
|
|
4076
|
+
if (backdrop) backdrop.classList.add('open');
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
WE._setDetailTab = function(tab, skillId) {
|
|
4080
|
+
_skillDetailTab = tab;
|
|
4081
|
+
var skill = _skillsData.find(function(s) { return s.id === skillId; });
|
|
4082
|
+
if (!skill) return;
|
|
4083
|
+
// Update tabs active state
|
|
4084
|
+
document.querySelectorAll('.we-skill-detail-tab').forEach(function(t) {
|
|
4085
|
+
t.classList.toggle('active', t.textContent.toLowerCase() === tab);
|
|
4086
|
+
});
|
|
4087
|
+
var content = document.getElementById('we-skill-detail-content');
|
|
4088
|
+
if (content) safeSetHtml(content, _renderDetailTab(skill, tab));
|
|
4089
|
+
};
|
|
4090
|
+
|
|
4091
|
+
function _renderDetailTab(skill, tab) {
|
|
4092
|
+
if (tab === 'overview') return _renderOverviewTab(skill);
|
|
4093
|
+
if (tab === 'config') return _renderConfigTab(skill);
|
|
4094
|
+
if (tab === 'history') return _renderHistoryTab(skill);
|
|
4095
|
+
if (tab === 'source') return _renderSourceTab(skill);
|
|
4096
|
+
if (tab === 'logs') return _renderLogsTab(skill);
|
|
4097
|
+
return '';
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
function _renderOverviewTab(skill) {
|
|
4101
|
+
var h = '';
|
|
4102
|
+
h += '<div style="margin-bottom:12px">';
|
|
4103
|
+
h += '<div style="font-size:13px;color:var(--fg);line-height:1.5">' + esc(skill.description || 'No description') + '</div>';
|
|
4104
|
+
h += '</div>';
|
|
4105
|
+
|
|
4106
|
+
// Metadata
|
|
4107
|
+
h += '<div class="walle-card" style="margin-bottom:10px">';
|
|
4108
|
+
h += '<div class="walle-card-title">Details</div><div class="walle-card-body">';
|
|
4109
|
+
h += '<div style="display:grid;grid-template-columns:100px 1fr;gap:4px 12px;font-size:12px">';
|
|
4110
|
+
h += '<span style="color:#888">Version</span><span>' + esc(skill.version || '—') + '</span>';
|
|
4111
|
+
h += '<span style="color:#888">Author</span><span>' + esc(skill.author || '—') + '</span>';
|
|
4112
|
+
h += '<span style="color:#888">Source</span><span>' + esc(skill.source || '—') + '</span>';
|
|
4113
|
+
h += '<span style="color:#888">Execution</span><span>' + esc(skill.execution || skill.trigger_type || '—') + '</span>';
|
|
4114
|
+
h += '<span style="color:#888">Trigger</span><span>' + esc(skill.trigger_type || '—') + '</span>';
|
|
4115
|
+
if (skill.skill_dir) h += '<span style="color:#888">Path</span><span style="font-size:11px;word-break:break-all">' + esc(skill.skill_dir) + '</span>';
|
|
4116
|
+
h += '</div></div></div>';
|
|
4117
|
+
|
|
4118
|
+
// Tags
|
|
4119
|
+
var tags = skill.tags || [];
|
|
4120
|
+
if (tags.length > 0) {
|
|
4121
|
+
h += '<div style="margin-bottom:10px"><strong style="font-size:12px;color:#888">Tags:</strong> ';
|
|
4122
|
+
tags.forEach(function(t) { h += '<span class="we-skill-tag">' + esc(t) + '</span> '; });
|
|
4123
|
+
h += '</div>';
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
// Permissions
|
|
4127
|
+
var perms = skill.permissions || [];
|
|
4128
|
+
if (perms.length > 0) {
|
|
4129
|
+
h += '<div style="margin-bottom:10px"><strong style="font-size:12px;color:#888">Permissions:</strong> ';
|
|
4130
|
+
perms.forEach(function(p) { h += '<span class="we-skill-tag">' + esc(p) + '</span> '; });
|
|
4131
|
+
h += '</div>';
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
// Requirements check
|
|
4135
|
+
var requires = skill.requires || {};
|
|
4136
|
+
if (Object.keys(requires).length > 0) {
|
|
4137
|
+
h += '<div class="walle-card"><div class="walle-card-title">Requirements</div><div class="walle-card-body">';
|
|
4138
|
+
h += '<div id="we-skill-req-check"><div class="walle-loading">Checking...</div></div>';
|
|
4139
|
+
h += '</div></div>';
|
|
4140
|
+
// Fire validation
|
|
4141
|
+
apiPost('/skills/' + skill.id + '/validate', {}).then(function(result) {
|
|
4142
|
+
var el = document.getElementById('we-skill-req-check');
|
|
4143
|
+
if (!el) return;
|
|
4144
|
+
var v = result.data || {};
|
|
4145
|
+
var ih = '<ul class="we-req-list">';
|
|
4146
|
+
if (v.valid) {
|
|
4147
|
+
ih += '<li class="we-req-item"><span class="we-req-icon ok">✓</span>All requirements met</li>';
|
|
3382
4148
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
4149
|
+
(v.missing || []).forEach(function(m) {
|
|
4150
|
+
ih += '<li class="we-req-item"><span class="we-req-icon missing">✗</span><span>' + esc(m.type) + ': <strong>' + esc(m.name) + '</strong></span><span class="we-req-suggestion">' + esc(m.suggestion || '') + '</span></li>';
|
|
4151
|
+
});
|
|
4152
|
+
ih += '</ul>';
|
|
4153
|
+
safeSetHtml(el, ih);
|
|
3386
4154
|
}).catch(function() {
|
|
3387
|
-
var
|
|
3388
|
-
if (
|
|
4155
|
+
var el = document.getElementById('we-skill-req-check');
|
|
4156
|
+
if (el) el.textContent = 'Validation unavailable';
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
// Stats
|
|
4161
|
+
var total = (skill.success_count || 0) + (skill.failure_count || 0);
|
|
4162
|
+
var rate = total > 0 ? Math.round(skill.success_count / total * 100) : -1;
|
|
4163
|
+
h += '<div class="walle-card"><div class="walle-card-title">Statistics</div><div class="walle-card-body">';
|
|
4164
|
+
h += '<div style="display:grid;grid-template-columns:100px 1fr;gap:4px 12px;font-size:12px">';
|
|
4165
|
+
h += '<span style="color:#888">Total Runs</span><span>' + total + '</span>';
|
|
4166
|
+
h += '<span style="color:#888">Successes</span><span style="color:#5c940d">' + (skill.success_count || 0) + '</span>';
|
|
4167
|
+
h += '<span style="color:#888">Failures</span><span style="color:#e03131">' + (skill.failure_count || 0) + '</span>';
|
|
4168
|
+
h += '<span style="color:#888">Success Rate</span><span>' + (rate >= 0 ? rate + '%' : 'N/A') + '</span>';
|
|
4169
|
+
h += '<span style="color:#888">Last Run</span><span>' + esc(timeAgo(skill.last_run)) + '</span>';
|
|
4170
|
+
h += '<span style="color:#888">Last Result</span><span>' + esc(skill.last_result || '—') + '</span>';
|
|
4171
|
+
h += '</div></div></div>';
|
|
4172
|
+
|
|
4173
|
+
return h;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
function _renderConfigTab(skill) {
|
|
4177
|
+
var schema = skill.config_schema || {};
|
|
4178
|
+
var keys = Object.keys(schema);
|
|
4179
|
+
if (keys.length === 0) return '<div class="walle-empty">This skill has no configurable options.</div>';
|
|
4180
|
+
|
|
4181
|
+
var saved = {};
|
|
4182
|
+
try { saved = JSON.parse(skill.config_values || '{}'); } catch (e) { saved = {}; }
|
|
4183
|
+
|
|
4184
|
+
var h = '<form class="we-config-form" onsubmit="WE._saveConfig(event,\'' + esc(skill.id) + '\')">';
|
|
4185
|
+
keys.forEach(function(k) {
|
|
4186
|
+
var field = schema[k];
|
|
4187
|
+
var val = saved[k] !== undefined ? saved[k] : (field.default !== undefined ? field.default : '');
|
|
4188
|
+
h += '<div class="we-config-field">';
|
|
4189
|
+
h += '<label class="we-config-label">' + esc(k) + '</label>';
|
|
4190
|
+
if (field.description) h += '<div class="we-config-help">' + esc(field.description) + '</div>';
|
|
4191
|
+
|
|
4192
|
+
if (field.type === 'boolean') {
|
|
4193
|
+
h += '<label class="we-toggle"><input type="checkbox" name="' + esc(k) + '"' + (val ? ' checked' : '') + '><span class="we-toggle-slider"></span></label>';
|
|
4194
|
+
} else if (field.enum) {
|
|
4195
|
+
h += '<select class="we-config-select" name="' + esc(k) + '">';
|
|
4196
|
+
field.enum.forEach(function(opt) {
|
|
4197
|
+
h += '<option' + (String(val) === String(opt) ? ' selected' : '') + '>' + esc(opt) + '</option>';
|
|
4198
|
+
});
|
|
4199
|
+
h += '</select>';
|
|
4200
|
+
} else if (field.type === 'number') {
|
|
4201
|
+
h += '<input type="number" class="we-config-input" name="' + esc(k) + '" value="' + esc(String(val)) + '">';
|
|
4202
|
+
} else {
|
|
4203
|
+
h += '<input type="text" class="we-config-input" name="' + esc(k) + '" value="' + esc(String(val)) + '">';
|
|
4204
|
+
}
|
|
4205
|
+
h += '</div>';
|
|
4206
|
+
});
|
|
4207
|
+
h += '<button type="submit" class="walle-btn primary we-config-save">Save Config</button>';
|
|
4208
|
+
h += '</form>';
|
|
4209
|
+
return h;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
WE._saveConfig = function(e, skillId) {
|
|
4213
|
+
e.preventDefault();
|
|
4214
|
+
var form = e.target;
|
|
4215
|
+
var schema = {};
|
|
4216
|
+
var skill = _skillsData.find(function(s) { return s.id === skillId; });
|
|
4217
|
+
if (skill && skill.config_schema) schema = skill.config_schema;
|
|
4218
|
+
var values = {};
|
|
4219
|
+
Object.keys(schema).forEach(function(k) {
|
|
4220
|
+
var el = form.elements[k];
|
|
4221
|
+
if (!el) return;
|
|
4222
|
+
if (schema[k].type === 'boolean') values[k] = el.checked;
|
|
4223
|
+
else if (schema[k].type === 'number') values[k] = parseFloat(el.value) || 0;
|
|
4224
|
+
else values[k] = el.value;
|
|
4225
|
+
});
|
|
4226
|
+
apiPut('/skills/' + skillId + '/config', values).then(function() {
|
|
4227
|
+
if (typeof showToast === 'function') showToast('Config saved', 'var(--accent)');
|
|
4228
|
+
}).catch(function(err) {
|
|
4229
|
+
if (typeof showToast === 'function') showToast('Error: ' + (err.message || ''), '#f00');
|
|
4230
|
+
});
|
|
4231
|
+
};
|
|
4232
|
+
|
|
4233
|
+
function _renderHistoryTab(skill) {
|
|
4234
|
+
var h = '<div id="we-skill-history"><div class="walle-loading">Loading execution history...</div></div>';
|
|
4235
|
+
// Load async
|
|
4236
|
+
api('/skills/' + skill.id + '/executions?limit=30').then(function(result) {
|
|
4237
|
+
var el = document.getElementById('we-skill-history');
|
|
4238
|
+
if (!el) return;
|
|
4239
|
+
var execs = result.data || [];
|
|
4240
|
+
if (execs.length === 0) { safeSetHtml(el, '<div class="walle-empty">No execution history.</div>'); return; }
|
|
4241
|
+
var ih = '<table class="we-exec-table"><thead><tr><th>Time</th><th>Status</th><th>Duration</th><th>Memories</th><th>Error</th></tr></thead><tbody>';
|
|
4242
|
+
execs.forEach(function(ex) {
|
|
4243
|
+
ih += '<tr>';
|
|
4244
|
+
ih += '<td>' + esc(timeAgo(ex.created_at)) + '</td>';
|
|
4245
|
+
ih += '<td><span class="we-exec-status ' + esc(ex.status) + '">' + esc(ex.status) + '</span></td>';
|
|
4246
|
+
ih += '<td>' + (ex.duration_ms ? (ex.duration_ms / 1000).toFixed(1) + 's' : '—') + '</td>';
|
|
4247
|
+
ih += '<td>' + (ex.memories_created || 0) + '</td>';
|
|
4248
|
+
ih += '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(ex.error || '') + '">' + esc(ex.error || '—') + '</td>';
|
|
4249
|
+
ih += '</tr>';
|
|
3389
4250
|
});
|
|
4251
|
+
ih += '</tbody></table>';
|
|
4252
|
+
safeSetHtml(el, ih);
|
|
4253
|
+
}).catch(function() {
|
|
4254
|
+
var el = document.getElementById('we-skill-history');
|
|
4255
|
+
if (el) el.textContent = 'Failed to load history.';
|
|
4256
|
+
});
|
|
4257
|
+
return h;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
function _renderSourceTab(skill) {
|
|
4261
|
+
var h = '<div id="we-skill-source"><div class="walle-loading">Loading source...</div></div>';
|
|
4262
|
+
api('/skills/' + skill.id + '/source').then(function(result) {
|
|
4263
|
+
var el = document.getElementById('we-skill-source');
|
|
4264
|
+
if (!el) return;
|
|
4265
|
+
var data = result.data || {};
|
|
4266
|
+
var ih = '';
|
|
4267
|
+
if (data.path) ih += '<div style="font-size:11px;color:#888;margin-bottom:8px">' + esc(data.path) + '</div>';
|
|
4268
|
+
ih += '<div class="we-skill-source-view">' + esc(data.content || 'Source not available') + '</div>';
|
|
4269
|
+
safeSetHtml(el, ih);
|
|
4270
|
+
}).catch(function() {
|
|
4271
|
+
var el = document.getElementById('we-skill-source');
|
|
4272
|
+
if (el) el.textContent = 'Source not available.';
|
|
4273
|
+
});
|
|
4274
|
+
return h;
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
function _renderLogsTab(skill) {
|
|
4278
|
+
var h = '<div id="we-skill-logs"><div class="walle-loading">Loading last execution logs...</div></div>';
|
|
4279
|
+
api('/skills/' + skill.id + '/executions?limit=1').then(function(result) {
|
|
4280
|
+
var el = document.getElementById('we-skill-logs');
|
|
4281
|
+
if (!el) return;
|
|
4282
|
+
var execs = result.data || [];
|
|
4283
|
+
if (execs.length === 0) { safeSetHtml(el, '<div class="walle-empty">No execution logs.</div>'); return; }
|
|
4284
|
+
var ex = execs[0];
|
|
4285
|
+
var ih = '<div style="margin-bottom:8px;font-size:12px;color:#888">' + esc(timeAgo(ex.created_at)) + ' — <span class="we-exec-status ' + esc(ex.status) + '">' + esc(ex.status) + '</span>';
|
|
4286
|
+
if (ex.duration_ms) ih += ' (' + (ex.duration_ms / 1000).toFixed(1) + 's)';
|
|
4287
|
+
ih += '</div>';
|
|
4288
|
+
if (ex.tool_calls) {
|
|
4289
|
+
ih += '<div style="margin-bottom:8px"><strong style="font-size:12px;color:#888">Tool Calls:</strong>';
|
|
4290
|
+
ih += '<pre class="we-skill-source-view" style="margin-top:4px;max-height:200px">' + esc(ex.tool_calls) + '</pre></div>';
|
|
4291
|
+
}
|
|
4292
|
+
if (ex.tool_results) {
|
|
4293
|
+
ih += '<div><strong style="font-size:12px;color:#888">Tool Results:</strong>';
|
|
4294
|
+
ih += '<pre class="we-skill-source-view" style="margin-top:4px;max-height:200px">' + esc(ex.tool_results) + '</pre></div>';
|
|
4295
|
+
}
|
|
4296
|
+
if (ex.error) {
|
|
4297
|
+
ih += '<div style="margin-top:8px"><strong style="font-size:12px;color:#e03131">Error:</strong>';
|
|
4298
|
+
ih += '<pre class="we-skill-source-view" style="margin-top:4px;color:#c97070">' + esc(ex.error) + '</pre></div>';
|
|
4299
|
+
}
|
|
4300
|
+
safeSetHtml(el, ih);
|
|
4301
|
+
}).catch(function() {
|
|
4302
|
+
var el = document.getElementById('we-skill-logs');
|
|
4303
|
+
if (el) el.textContent = 'Failed to load logs.';
|
|
4304
|
+
});
|
|
4305
|
+
return h;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
// ---- Skill CRUD ----
|
|
4309
|
+
|
|
4310
|
+
WE._deleteSkill = function(id, name) {
|
|
4311
|
+
if (!confirm('Delete skill "' + name + '"? This cannot be undone.')) return;
|
|
4312
|
+
apiDelete('/skills/' + id).then(function(result) {
|
|
4313
|
+
if (result.error) { if (typeof showToast === 'function') showToast('Error: ' + result.error, '#f00'); return; }
|
|
4314
|
+
if (typeof showToast === 'function') showToast('Deleted: ' + name, 'var(--accent)');
|
|
4315
|
+
_skillDetailOpen = null;
|
|
4316
|
+
WE.renderSkills();
|
|
4317
|
+
});
|
|
4318
|
+
};
|
|
4319
|
+
|
|
4320
|
+
WE._exportSkill = function(id) {
|
|
4321
|
+
api('/skills/' + id + '/export').then(function(result) {
|
|
4322
|
+
var bundle = result.data;
|
|
4323
|
+
if (!bundle) { if (typeof showToast === 'function') showToast('Export failed', '#f00'); return; }
|
|
4324
|
+
var blob = new Blob([JSON.stringify(bundle, null, 2)], { type: 'application/json' });
|
|
4325
|
+
var url = URL.createObjectURL(blob);
|
|
4326
|
+
var a = document.createElement('a');
|
|
4327
|
+
a.href = url; a.download = (bundle.name || 'skill') + '.json'; a.click();
|
|
4328
|
+
URL.revokeObjectURL(url);
|
|
4329
|
+
if (typeof showToast === 'function') showToast('Exported: ' + (bundle.name || ''), 'var(--accent)');
|
|
4330
|
+
}).catch(function(err) {
|
|
4331
|
+
if (typeof showToast === 'function') showToast('Export error: ' + (err.message || ''), '#f00');
|
|
4332
|
+
});
|
|
4333
|
+
};
|
|
4334
|
+
|
|
4335
|
+
WE._importSkillPrompt = function() {
|
|
4336
|
+
var input = document.createElement('input');
|
|
4337
|
+
input.type = 'file'; input.accept = '.json';
|
|
4338
|
+
input.onchange = function() {
|
|
4339
|
+
var file = input.files[0];
|
|
4340
|
+
if (!file) return;
|
|
4341
|
+
var reader = new FileReader();
|
|
4342
|
+
reader.onload = function(e) {
|
|
4343
|
+
try {
|
|
4344
|
+
var bundle = JSON.parse(e.target.result);
|
|
4345
|
+
apiPost('/skills/import', bundle).then(function(result) {
|
|
4346
|
+
if (result.error) { if (typeof showToast === 'function') showToast('Import failed: ' + result.error, '#f00'); return; }
|
|
4347
|
+
if (typeof showToast === 'function') showToast('Imported: ' + (result.data?.name || ''), 'var(--accent)');
|
|
4348
|
+
WE.renderSkills();
|
|
4349
|
+
});
|
|
4350
|
+
} catch (err) {
|
|
4351
|
+
if (typeof showToast === 'function') showToast('Invalid JSON file', '#f00');
|
|
4352
|
+
}
|
|
4353
|
+
};
|
|
4354
|
+
reader.readAsText(file);
|
|
4355
|
+
};
|
|
4356
|
+
input.click();
|
|
4357
|
+
};
|
|
4358
|
+
|
|
4359
|
+
// ---- Skill Editor ----
|
|
4360
|
+
|
|
4361
|
+
WE._openSkillEditor = function(skillId) {
|
|
4362
|
+
_skillEditorMode = skillId || 'create';
|
|
4363
|
+
var panel = document.getElementById('we-skill-detail');
|
|
4364
|
+
var backdrop = document.getElementById('we-skill-backdrop');
|
|
4365
|
+
if (!panel) return;
|
|
4366
|
+
|
|
4367
|
+
var isEdit = skillId && skillId !== 'create';
|
|
4368
|
+
var skill = isEdit ? _skillsData.find(function(s) { return s.id === skillId; }) : null;
|
|
4369
|
+
|
|
4370
|
+
var h = '<div class="we-skill-detail-header">';
|
|
4371
|
+
h += '<button class="we-skill-detail-close" onclick="WE._closeSkillDetail()">✕</button>';
|
|
4372
|
+
h += '<div class="we-skill-detail-title">' + (isEdit ? 'Edit Skill' : 'Create New Skill') + '</div>';
|
|
4373
|
+
h += '</div>';
|
|
4374
|
+
|
|
4375
|
+
h += '<div class="we-skill-detail-content">';
|
|
4376
|
+
|
|
4377
|
+
if (!isEdit) {
|
|
4378
|
+
// Template selection
|
|
4379
|
+
h += '<div class="walle-section-title">Start from a template</div>';
|
|
4380
|
+
h += '<div class="we-template-grid">';
|
|
4381
|
+
var templates = [
|
|
4382
|
+
{ name: 'Data Fetcher', desc: 'Agent that fetches data via tools', tpl: 'data-fetcher' },
|
|
4383
|
+
{ name: 'Script Runner', desc: 'Script skill with run.js', tpl: 'script-runner' },
|
|
4384
|
+
{ name: 'Periodic Checker', desc: 'Interval-triggered monitor', tpl: 'periodic-checker' },
|
|
4385
|
+
{ name: 'Manual Action', desc: 'One-shot manual trigger', tpl: 'manual-action' },
|
|
4386
|
+
];
|
|
4387
|
+
templates.forEach(function(t) {
|
|
4388
|
+
h += '<div class="we-template-card" onclick="WE._loadTemplate(\'' + t.tpl + '\')">';
|
|
4389
|
+
h += '<div class="we-template-name">' + esc(t.name) + '</div>';
|
|
4390
|
+
h += '<div class="we-template-desc">' + esc(t.desc) + '</div>';
|
|
4391
|
+
h += '</div>';
|
|
4392
|
+
});
|
|
4393
|
+
h += '</div>';
|
|
4394
|
+
h += '<div class="walle-section-title" style="margin-top:16px">Or start from scratch</div>';
|
|
3390
4395
|
}
|
|
4396
|
+
|
|
4397
|
+
// Form
|
|
4398
|
+
h += '<div class="we-skill-editor" id="we-skill-editor-form">';
|
|
4399
|
+
h += '<div class="we-config-field"><label class="we-config-label">Name</label>';
|
|
4400
|
+
h += '<input type="text" class="we-config-input" id="we-ed-name" value="' + esc(skill ? skill.name : '') + '" placeholder="my-skill-name"></div>';
|
|
4401
|
+
|
|
4402
|
+
h += '<div class="we-config-field"><label class="we-config-label">Description</label>';
|
|
4403
|
+
h += '<textarea class="we-config-input" id="we-ed-desc" rows="2" placeholder="What does this skill do?">' + esc(skill ? (skill.description || '') : '') + '</textarea></div>';
|
|
4404
|
+
|
|
4405
|
+
h += '<div class="we-skill-editor-row">';
|
|
4406
|
+
h += '<div class="we-config-field"><label class="we-config-label">Execution</label>';
|
|
4407
|
+
h += '<select class="we-config-select" id="we-ed-exec">';
|
|
4408
|
+
h += '<option value="agent"' + (skill && skill.execution === 'agent' ? ' selected' : '') + '>Agent</option>';
|
|
4409
|
+
h += '<option value="script"' + (skill && skill.execution === 'script' ? ' selected' : '') + '>Script</option>';
|
|
4410
|
+
h += '</select></div>';
|
|
4411
|
+
|
|
4412
|
+
h += '<div class="we-config-field"><label class="we-config-label">Trigger</label>';
|
|
4413
|
+
h += '<select class="we-config-select" id="we-ed-trigger">';
|
|
4414
|
+
h += '<option value="manual"' + (skill && skill.trigger_type === 'manual' ? ' selected' : '') + '>Manual</option>';
|
|
4415
|
+
h += '<option value="interval"' + (skill && skill.trigger_type === 'interval' ? ' selected' : '') + '>Interval</option>';
|
|
4416
|
+
h += '</select></div>';
|
|
4417
|
+
h += '</div>';
|
|
4418
|
+
|
|
4419
|
+
h += '<div class="we-config-field"><label class="we-config-label">Tags (comma-separated)</label>';
|
|
4420
|
+
h += '<input type="text" class="we-config-input" id="we-ed-tags" value="' + esc(skill && skill.tags ? skill.tags.join(', ') : '') + '" placeholder="data, sync"></div>';
|
|
4421
|
+
|
|
4422
|
+
h += '<div class="we-config-field"><label class="we-config-label">Instructions (Markdown)</label>';
|
|
4423
|
+
h += '<textarea class="we-skill-editor-textarea" id="we-ed-instructions" placeholder="# My Skill\n\nDescribe what the skill should do...">' + (skill ? esc(skill.prompt_template || '') : '') + '</textarea></div>';
|
|
4424
|
+
|
|
4425
|
+
// Raw SKILL.md toggle
|
|
4426
|
+
h += '<div style="margin-top:8px"><button class="walle-btn" onclick="WE._toggleRawEditor()">Toggle Raw SKILL.md</button></div>';
|
|
4427
|
+
h += '<div id="we-ed-raw-wrap" style="display:none"><div class="we-config-field"><label class="we-config-label">Raw SKILL.md</label>';
|
|
4428
|
+
h += '<textarea class="we-skill-editor-textarea" id="we-ed-raw" rows="15" placeholder="---\nname: my-skill\n..."></textarea></div></div>';
|
|
4429
|
+
|
|
4430
|
+
h += '<div style="display:flex;gap:8px;margin-top:12px">';
|
|
4431
|
+
h += '<button class="walle-btn primary" onclick="WE._saveSkillEditor()">' + (isEdit ? 'Save Changes' : 'Create Skill') + '</button>';
|
|
4432
|
+
h += '<button class="walle-btn" onclick="WE._closeSkillDetail()">Cancel</button>';
|
|
4433
|
+
h += '</div>';
|
|
4434
|
+
|
|
4435
|
+
h += '</div>'; // editor
|
|
4436
|
+
h += '</div>'; // content
|
|
4437
|
+
|
|
4438
|
+
safeSetHtml(panel, h);
|
|
4439
|
+
panel.classList.add('open');
|
|
4440
|
+
if (backdrop) backdrop.classList.add('open');
|
|
4441
|
+
};
|
|
4442
|
+
|
|
4443
|
+
WE._toggleRawEditor = function() {
|
|
4444
|
+
var wrap = document.getElementById('we-ed-raw-wrap');
|
|
4445
|
+
if (wrap) wrap.style.display = wrap.style.display === 'none' ? 'block' : 'none';
|
|
4446
|
+
};
|
|
4447
|
+
|
|
4448
|
+
WE._loadTemplate = function(tplName) {
|
|
4449
|
+
var templates = {
|
|
4450
|
+
'data-fetcher': { name: 'my-data-fetcher', desc: 'Fetches data from an external source and stores observations as memories', exec: 'agent', trigger: 'interval', tags: 'data, sync', instructions: '# Data Fetcher\n\nUse the available tools to fetch data from the configured source.\nParse the response and return a structured list of observations.' },
|
|
4451
|
+
'script-runner': { name: 'my-script', desc: 'Runs a custom Node.js script', exec: 'script', trigger: 'manual', tags: 'automation', instructions: '# Script Runner\n\nThis skill executes run.js in the skill directory.' },
|
|
4452
|
+
'periodic-checker': { name: 'my-checker', desc: 'Periodically checks a condition and reports changes', exec: 'agent', trigger: 'interval', tags: 'monitoring', instructions: '# Periodic Checker\n\nCheck the configured target at each interval.\nCompare current state with previous run.\nOnly report meaningful changes.' },
|
|
4453
|
+
'manual-action': { name: 'my-action', desc: 'A manually triggered one-shot action', exec: 'agent', trigger: 'manual', tags: 'utility', instructions: '# Manual Action\n\nWhen triggered:\n1. Gather context\n2. Execute the action\n3. Report the result' },
|
|
4454
|
+
};
|
|
4455
|
+
var t = templates[tplName];
|
|
4456
|
+
if (!t) return;
|
|
4457
|
+
var el = function(id) { return document.getElementById(id); };
|
|
4458
|
+
if (el('we-ed-name')) el('we-ed-name').value = t.name;
|
|
4459
|
+
if (el('we-ed-desc')) el('we-ed-desc').value = t.desc;
|
|
4460
|
+
if (el('we-ed-exec')) el('we-ed-exec').value = t.exec;
|
|
4461
|
+
if (el('we-ed-trigger')) el('we-ed-trigger').value = t.trigger;
|
|
4462
|
+
if (el('we-ed-tags')) el('we-ed-tags').value = t.tags;
|
|
4463
|
+
if (el('we-ed-instructions')) el('we-ed-instructions').value = t.instructions;
|
|
4464
|
+
};
|
|
4465
|
+
|
|
4466
|
+
WE._saveSkillEditor = function() {
|
|
4467
|
+
var rawWrap = document.getElementById('we-ed-raw-wrap');
|
|
4468
|
+
var rawMode = rawWrap && rawWrap.style.display !== 'none';
|
|
4469
|
+
var rawContent = document.getElementById('we-ed-raw');
|
|
4470
|
+
|
|
4471
|
+
if (rawMode && rawContent && rawContent.value.trim()) {
|
|
4472
|
+
// Save raw SKILL.md
|
|
4473
|
+
var name = (document.getElementById('we-ed-name') || {}).value || '';
|
|
4474
|
+
if (_skillEditorMode && _skillEditorMode !== 'create') {
|
|
4475
|
+
// Edit existing
|
|
4476
|
+
apiPut('/skills/' + _skillEditorMode + '/source', { content: rawContent.value }).then(function(result) {
|
|
4477
|
+
if (result.error) { if (typeof showToast === 'function') showToast('Error: ' + result.error, '#f00'); return; }
|
|
4478
|
+
if (typeof showToast === 'function') showToast('Skill updated', 'var(--accent)');
|
|
4479
|
+
WE._closeSkillDetail();
|
|
4480
|
+
WE.renderSkills();
|
|
4481
|
+
});
|
|
4482
|
+
} else {
|
|
4483
|
+
// Create new from raw
|
|
4484
|
+
apiPost('/skills/create-file', { name: name || 'unnamed', content: rawContent.value }).then(function(result) {
|
|
4485
|
+
if (result.error) { if (typeof showToast === 'function') showToast('Error: ' + result.error, '#f00'); return; }
|
|
4486
|
+
if (typeof showToast === 'function') showToast('Skill created', 'var(--accent)');
|
|
4487
|
+
WE._closeSkillDetail();
|
|
4488
|
+
WE.renderSkills();
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
// Form mode
|
|
4495
|
+
var name = (document.getElementById('we-ed-name') || {}).value || '';
|
|
4496
|
+
var desc = (document.getElementById('we-ed-desc') || {}).value || '';
|
|
4497
|
+
var exec = (document.getElementById('we-ed-exec') || {}).value || 'agent';
|
|
4498
|
+
var trigger = (document.getElementById('we-ed-trigger') || {}).value || 'manual';
|
|
4499
|
+
var tags = (document.getElementById('we-ed-tags') || {}).value || '';
|
|
4500
|
+
var instructions = (document.getElementById('we-ed-instructions') || {}).value || '';
|
|
4501
|
+
|
|
4502
|
+
if (!name.trim()) { if (typeof showToast === 'function') showToast('Name is required', '#f00'); return; }
|
|
4503
|
+
|
|
4504
|
+
var tagsArr = tags.split(',').map(function(t) { return t.trim(); }).filter(Boolean);
|
|
4505
|
+
|
|
4506
|
+
if (_skillEditorMode && _skillEditorMode !== 'create') {
|
|
4507
|
+
// Edit existing — update DB + source file
|
|
4508
|
+
apiPut('/skills/' + _skillEditorMode, {
|
|
4509
|
+
name: name, description: desc, trigger_type: trigger, prompt_template: instructions
|
|
4510
|
+
}).then(function() {
|
|
4511
|
+
if (typeof showToast === 'function') showToast('Skill updated', 'var(--accent)');
|
|
4512
|
+
WE._closeSkillDetail();
|
|
4513
|
+
WE.renderSkills();
|
|
4514
|
+
});
|
|
4515
|
+
} else {
|
|
4516
|
+
// Create new
|
|
4517
|
+
apiPost('/skills/create-file', {
|
|
4518
|
+
name: name, description: desc, execution: exec, trigger_type: trigger,
|
|
4519
|
+
tags: tagsArr, instructions: instructions
|
|
4520
|
+
}).then(function(result) {
|
|
4521
|
+
if (result.error) { if (typeof showToast === 'function') showToast('Error: ' + result.error, '#f00'); return; }
|
|
4522
|
+
if (typeof showToast === 'function') showToast('Skill created!', 'var(--accent)');
|
|
4523
|
+
WE._closeSkillDetail();
|
|
4524
|
+
WE.renderSkills();
|
|
4525
|
+
});
|
|
4526
|
+
}
|
|
4527
|
+
};
|
|
4528
|
+
|
|
4529
|
+
// Slack ingest progress loader (extracted to keep renderSkillsContent cleaner)
|
|
4530
|
+
function _loadSlackIngestProgress(slackSkill) {
|
|
4531
|
+
api('/slack-ingest/progress').then(function(result) {
|
|
4532
|
+
var card = document.getElementById('walle-slack-ingest-card');
|
|
4533
|
+
if (!card) return;
|
|
4534
|
+
var p = result.data || {};
|
|
4535
|
+
var phaseLabel = p.phase === 'done' ? 'Complete' : p.phase === 'history' ? 'Fetching messages' : p.phase === 'conversations' ? 'Waiting to start' : esc(p.phase || 'unknown');
|
|
4536
|
+
var pct = p.percent || 0;
|
|
4537
|
+
var ih = '<div class="walle-card-body">';
|
|
4538
|
+
ih += '<div style="margin-bottom:6px"><strong>Slack Ingestion — Phase:</strong> ' + esc(phaseLabel) + '</div>';
|
|
4539
|
+
ih += '<div style="margin-bottom:6px"><strong>Conversations:</strong> ' + (p.conversations_processed || 0) + ' / ' + (p.conversations_total || 0) + '</div>';
|
|
4540
|
+
ih += '<div style="margin-bottom:6px"><strong>Messages ingested:</strong> ' + (p.messages_ingested || 0) + '</div>';
|
|
4541
|
+
ih += '<div style="background:#333;border-radius:4px;height:18px;margin:8px 0;overflow:hidden">';
|
|
4542
|
+
ih += '<div style="background:var(--accent,#228be6);height:100%;width:' + pct + '%;transition:width 0.3s;border-radius:4px"></div></div>';
|
|
4543
|
+
ih += '<div style="font-size:11px;color:var(--fg-muted,#888)">' + pct + '% complete';
|
|
4544
|
+
if (p.started_at) ih += ' | Started: ' + esc(timeAgo(p.started_at));
|
|
4545
|
+
if (p.completed_at) ih += ' | Done: ' + esc(timeAgo(p.completed_at));
|
|
4546
|
+
ih += '</div>';
|
|
4547
|
+
ih += '<div style="margin-top:8px">';
|
|
4548
|
+
if (p.phase !== 'done' && !slackSkill.enabled) {
|
|
4549
|
+
ih += '<button class="walle-btn primary" onclick="WE._startSlackIngest()">Start Ingestion</button> ';
|
|
4550
|
+
}
|
|
4551
|
+
if (slackSkill.enabled && p.phase !== 'done') {
|
|
4552
|
+
ih += '<span style="color:#5c940d;font-size:12px;margin-right:8px">Running...</span>';
|
|
4553
|
+
}
|
|
4554
|
+
ih += '<button class="walle-btn" onclick="WE._resetSlackIngest()">Reset</button>';
|
|
4555
|
+
ih += '</div></div>';
|
|
4556
|
+
safeSetHtml(card, ih);
|
|
4557
|
+
}).catch(function() {
|
|
4558
|
+
var card = document.getElementById('walle-slack-ingest-card');
|
|
4559
|
+
if (card) card.textContent = 'Failed to load progress.';
|
|
4560
|
+
});
|
|
3391
4561
|
}
|
|
3392
4562
|
|
|
3393
4563
|
WE._startSlackIngest = function() {
|