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.
Files changed (75) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/db.js +5 -1
  4. package/template/claude-task-manager/public/css/walle.css +317 -0
  5. package/template/claude-task-manager/public/index.html +404 -101
  6. package/template/claude-task-manager/public/js/walle.js +1256 -86
  7. package/template/claude-task-manager/server.js +189 -14
  8. package/template/docs/site/api/README.md +146 -0
  9. package/template/docs/site/skills/README.md +99 -5
  10. package/template/package.json +1 -1
  11. package/template/wall-e/agent.js +54 -0
  12. package/template/wall-e/api-walle.js +452 -3
  13. package/template/wall-e/brain.js +45 -1
  14. package/template/wall-e/channels/telegram-channel.js +96 -0
  15. package/template/wall-e/chat.js +61 -2
  16. package/template/wall-e/coding-context.js +252 -0
  17. package/template/wall-e/coding-orchestrator.js +625 -0
  18. package/template/wall-e/coding-review.js +189 -0
  19. package/template/wall-e/core-tasks.js +12 -3
  20. package/template/wall-e/deploy.sh +4 -4
  21. package/template/wall-e/fly.toml +2 -2
  22. package/template/wall-e/package.json +4 -1
  23. package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
  24. package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
  25. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
  26. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
  27. package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
  28. package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
  29. package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
  30. package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
  31. package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
  32. package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
  33. package/template/wall-e/skills/_templates/manual-action.md +19 -0
  34. package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
  35. package/template/wall-e/skills/_templates/script-runner.md +21 -0
  36. package/template/wall-e/skills/claude-code-reader.js +16 -4
  37. package/template/wall-e/skills/skill-executor.js +23 -1
  38. package/template/wall-e/skills/skill-validator.js +73 -0
  39. package/template/wall-e/tests/brain.test.js +3 -3
  40. package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
  41. package/template/wall-e/tests/coding-context.test.js +212 -0
  42. package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
  43. package/template/wall-e/tests/coding-review.test.js +141 -0
  44. package/template/claude-task-manager/package-lock.json +0 -1607
  45. package/template/claude-task-manager/tests/test-ai-search.js +0 -61
  46. package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
  47. package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
  48. package/template/claude-task-manager/tests/test-features-v2.js +0 -127
  49. package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
  50. package/template/claude-task-manager/tests/test-insights.js +0 -124
  51. package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
  52. package/template/claude-task-manager/tests/test-permissions.js +0 -122
  53. package/template/claude-task-manager/tests/test-pin.js +0 -51
  54. package/template/claude-task-manager/tests/test-prompts.js +0 -164
  55. package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
  56. package/template/claude-task-manager/tests/test-review.js +0 -104
  57. package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
  58. package/template/claude-task-manager/tests/test-send-final.js +0 -30
  59. package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
  60. package/template/claude-task-manager/tests/test-send-integration.js +0 -107
  61. package/template/claude-task-manager/tests/test-send-visual.js +0 -34
  62. package/template/claude-task-manager/tests/test-session-create.js +0 -147
  63. package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
  64. package/template/claude-task-manager/tests/test-url-hash.js +0 -68
  65. package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
  66. package/template/claude-task-manager/tests/test-ux-review.js +0 -130
  67. package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
  68. package/template/claude-task-manager/tests/test-zoom.js +0 -92
  69. package/template/claude-task-manager/tests/test-zoom2.js +0 -67
  70. package/template/docs/openclaw-vs-walle-comparison.md +0 -103
  71. package/template/docs/ux-improvement-plan.md +0 -84
  72. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
  73. package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
  74. package/template/wall-e/package-lock.json +0 -533
  75. 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
- var d = new Date(ts);
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
- renderStatusContent(status, stats);
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
- renderChatUI();
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">&#9998;</button>';
993
1183
  html += ' <button class="we-edit-btn we-delete-btn" onclick="WE._deleteMessage(' + i + ')" title="Delete this exchange">&#128465;</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">&lsaquo;</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">&rsaquo;</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
- chatHistory = chatHistory.slice(0, idx);
1529
- chatHistory.push({ role: 'user', text: newText });
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
- // Resume all paused/failed Slack tasks and clear their errors
1923
- var updates = [];
1924
- (cache.allTasks || []).forEach(function(t) {
1925
- var isSlackTask = t.script && t.script.includes('slack');
1926
- var needsResume = isSlackTask && (t.status === 'paused' || t.status === 'failed');
1927
- var needsClearError = isSlackTask && t.error && (t.error.includes('Slack token expired') || t.error.includes('invalid_auth'));
1928
- if (needsResume || needsClearError) {
1929
- updates.push(apiPut('/tasks/' + t.id, { status: 'pending', error: null, result: null }));
1930
- }
1931
- });
1932
- // Wait for all updates to complete, then refresh
1933
- Promise.all(updates).then(function() {
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 hasSlackAuthError = allTasks.some(function(t) {
2098
- return t.error && (t.error.includes('Slack token expired') || t.error.includes('invalid_auth'));
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 (hasSlackAuthError) {
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 (rewrite to enterprise domain if needed)
2933
+ // Slack source_ref link
2619
2934
  if (t.source === 'slack' && t.source_ref) {
2620
- var slackUrl = t.source_ref.replace(/^(https:\/\/\w+)\.slack\.com/, '$1.enterprise.slack.com');
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
- var skills = results[0].data || [];
3280
- var suggestions = results[1].data || {};
3281
- if (!Array.isArray(skills)) skills = [];
3282
- renderSkillsContent(skills, suggestions);
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
- // Active skills
3304
- html += '<div class="walle-section-title">Learned Skills (' + skills.length + ')</div>';
3305
- if (skills.length === 0) {
3306
- html += '<div class="walle-empty">No skills yet. WALL-E will create bootstrap skills on next daemon restart.</div>';
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
- skills.forEach(function(s) {
3309
- var rate = (s.success_count + s.failure_count) > 0
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 Discovered</div>';
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 += '<div class="walle-card"><div class="walle-card-title">Claude Code Skills (' + claudeSkills.length + ')</div><div class="walle-card-body">';
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
- // Load Slack ingest progress if the card exists
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
- api('/slack-ingest/progress').then(function(result) {
3361
- var card = document.getElementById('walle-slack-ingest-card');
3362
- if (!card) return;
3363
- var p = result.data || {};
3364
- var phaseLabel = p.phase === 'done' ? 'Complete' : p.phase === 'history' ? 'Fetching messages' : p.phase === 'conversations' ? 'Waiting to start' : esc(p.phase || 'unknown');
3365
- var pct = p.percent || 0;
3366
- var ih = '<div class="walle-card-body">';
3367
- ih += '<div style="margin-bottom:6px"><strong>Phase:</strong> ' + esc(phaseLabel) + '</div>';
3368
- ih += '<div style="margin-bottom:6px"><strong>Conversations:</strong> ' + (p.conversations_processed || 0) + ' / ' + (p.conversations_total || 0) + '</div>';
3369
- ih += '<div style="margin-bottom:6px"><strong>Messages ingested:</strong> ' + (p.messages_ingested || 0) + '</div>';
3370
- ih += '<div style="background:#333;border-radius:4px;height:18px;margin:8px 0;overflow:hidden">';
3371
- ih += '<div style="background:var(--accent,#228be6);height:100%;width:' + pct + '%;transition:width 0.3s;border-radius:4px"></div></div>';
3372
- ih += '<div style="font-size:11px;color:var(--fg-muted,#888)">' + pct + '% complete';
3373
- if (p.started_at) ih += ' | Started: ' + esc(timeAgo(p.started_at));
3374
- if (p.completed_at) ih += ' | Done: ' + esc(timeAgo(p.completed_at));
3375
- ih += '</div>';
3376
- ih += '<div style="margin-top:8px">';
3377
- if (p.phase !== 'done' && !slackSkill.enabled) {
3378
- ih += '<button class="walle-btn primary" onclick="WE._startSlackIngest()">Start Ingestion</button> ';
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
- if (slackSkill.enabled && p.phase !== 'done') {
3381
- ih += '<span style="color:#5c940d;font-size:12px;margin-right:8px">Running...</span>';
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
- ih += '<button class="walle-btn" onclick="WE._resetSlackIngest()">Reset</button>';
3384
- ih += '</div></div>';
3385
- safeSetHtml(card, ih);
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 card = document.getElementById('walle-slack-ingest-card');
3388
- if (card) card.innerHTML = '<div class="walle-card-body">Failed to load progress.</div>';
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() {