create-walle 0.9.0 → 0.9.3

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 (45) hide show
  1. package/README.md +35 -31
  2. package/package.json +3 -3
  3. package/template/CLAUDE.md +23 -1
  4. package/template/claude-task-manager/bin/restart-ctm.sh +3 -2
  5. package/template/claude-task-manager/db.js +38 -0
  6. package/template/claude-task-manager/public/css/walle.css +123 -0
  7. package/template/claude-task-manager/public/index.html +962 -69
  8. package/template/claude-task-manager/public/js/walle.js +374 -121
  9. package/template/claude-task-manager/public/prompts.html +84 -26
  10. package/template/claude-task-manager/public/walle-icon.svg +45 -0
  11. package/template/claude-task-manager/server.js +69 -4
  12. package/template/docs/openclaw-vs-walle-comparison.md +103 -0
  13. package/template/package.json +1 -1
  14. package/template/wall-e/agent.js +63 -3
  15. package/template/wall-e/api-walle.js +42 -0
  16. package/template/wall-e/brain.js +182 -5
  17. package/template/wall-e/channels/imessage-channel.js +4 -1
  18. package/template/wall-e/channels/slack-channel.js +3 -1
  19. package/template/wall-e/chat.js +106 -224
  20. package/template/wall-e/context/compactor.js +163 -0
  21. package/template/wall-e/context/context-builder.js +355 -0
  22. package/template/wall-e/context/state-snapshot.js +209 -0
  23. package/template/wall-e/context/token-counter.js +55 -0
  24. package/template/wall-e/context/topic-matcher.js +79 -0
  25. package/template/wall-e/core-tasks.js +24 -0
  26. package/template/wall-e/events/event-bus.js +23 -0
  27. package/template/wall-e/loops/ingest.js +4 -0
  28. package/template/wall-e/loops/initiative.js +316 -0
  29. package/template/wall-e/loops/tasks.js +55 -5
  30. package/template/wall-e/skills/_bundled/email-sync/run.js +3 -1
  31. package/template/wall-e/skills/_bundled/morning-briefing/run.js +41 -0
  32. package/template/wall-e/skills/_bundled/proactive-alerts/SKILL.md +20 -0
  33. package/template/wall-e/skills/_bundled/proactive-alerts/run.js +144 -0
  34. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +18 -0
  35. package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +4 -0
  36. package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +52 -0
  37. package/template/wall-e/skills/_bundled/slack-mentions/run.js +470 -0
  38. package/template/wall-e/skills/_bundled/weekly-reflection/SKILL.md +69 -0
  39. package/template/wall-e/tests/brain.test.js +4 -4
  40. package/template/wall-e/tests/compactor.test.js +323 -0
  41. package/template/wall-e/tests/context-builder.test.js +215 -0
  42. package/template/wall-e/tests/event-bus.test.js +74 -0
  43. package/template/wall-e/tests/initiative.test.js +354 -0
  44. package/template/wall-e/tests/proactive-alerts.test.js +140 -0
  45. package/template/wall-e/tests/session-persistence.test.js +335 -0
@@ -1016,7 +1016,7 @@ function renderChatUI() {
1016
1016
  if (i + 1 < chatHistory.length && chatHistory[i + 1].role === 'assistant') {
1017
1017
  i++;
1018
1018
  html += '<div class="walle-chat-msg assistant">';
1019
- html += '<div class="walle-chat-msg-role assistant">WALL-E</div>';
1019
+ html += '<div class="walle-chat-msg-role assistant"><img src="/walle-icon.svg" width="16" height="16" class="walle-avatar"> WALL-E</div>';
1020
1020
  html += '<div class="walle-chat-msg-text we-markdown">' + renderMarkdown(chatHistory[i].text) + '</div>';
1021
1021
  html += '</div>';
1022
1022
  }
@@ -1030,7 +1030,7 @@ function renderChatUI() {
1030
1030
  html += '<div class="we-turn-checkbox">' + (isSelSA ? '&#9745;' : '&#9744;') + '</div>';
1031
1031
  }
1032
1032
  html += '<div class="walle-chat-msg assistant">';
1033
- html += '<div class="walle-chat-msg-role assistant">WALL-E</div>';
1033
+ html += '<div class="walle-chat-msg-role assistant"><img src="/walle-icon.svg" width="16" height="16" class="walle-avatar"> WALL-E</div>';
1034
1034
  html += '<div class="walle-chat-msg-text we-markdown">' + renderMarkdown(msg.text) + '</div>';
1035
1035
  html += '</div>';
1036
1036
  html += '</div>';
@@ -1843,6 +1843,10 @@ function _highlightSearchTerms(html, query) {
1843
1843
  }
1844
1844
 
1845
1845
  // ---- Tasks View ----
1846
+ var _taskViewTab = 'active'; // active | inbox | automation | done | all
1847
+ var _expandedTasks = new Set(); // track which task cards are expanded
1848
+ var _collapsedGroups = new Set(); // track which groups user explicitly collapsed
1849
+ var _expandedGroups = new Set(); // track which groups user explicitly expanded
1846
1850
  var _taskFilter = { search: '', statusSet: new Set(), type: 'all' }; // empty statusSet = all
1847
1851
 
1848
1852
  WE.renderTasks = function() {
@@ -1874,8 +1878,22 @@ WE.renderTasks = function() {
1874
1878
  });
1875
1879
  };
1876
1880
 
1881
+ function _matchesViewTab(t) {
1882
+ if (_taskViewTab === 'all') return true;
1883
+ if (_taskViewTab === 'active') return t.status === 'running' || t.status === 'pending' || t.status === 'failed';
1884
+ if (_taskViewTab === 'inbox') return t.source === 'slack';
1885
+ if (_taskViewTab === 'automation') return t.type === 'recurring' && t.source !== 'slack';
1886
+ if (_taskViewTab === 'done') return t.status === 'completed' || t.status === 'cancelled';
1887
+ return true;
1888
+ }
1889
+
1890
+ function _countInbox(tasks) {
1891
+ return tasks.filter(function(t) { return t.source === 'slack' && t.status !== 'completed' && t.status !== 'cancelled' && t.status !== 'dismissed'; }).length;
1892
+ }
1893
+
1877
1894
  function filterTasks(tasks) {
1878
1895
  return tasks.filter(function(t) {
1896
+ if (!_matchesViewTab(t)) return false;
1879
1897
  if (_taskFilter.statusSet.size > 0 && !_taskFilter.statusSet.has(t.status)) return false;
1880
1898
  if (_taskFilter.type === 'recurring' && t.type !== 'recurring') return false;
1881
1899
  if (_taskFilter.type === 'once' && t.type === 'recurring') return false;
@@ -1926,6 +1944,32 @@ WE._reconnectSlack = function() {
1926
1944
  });
1927
1945
  };
1928
1946
 
1947
+ WE._switchTaskView = function(tab) {
1948
+ _taskViewTab = tab;
1949
+ _taskFilter.statusSet.clear();
1950
+ _collapsedGroups.clear();
1951
+ _expandedGroups.clear();
1952
+ if (cache.allTasks) _renderTasksDirect(filterTasks(cache.allTasks));
1953
+ };
1954
+
1955
+ WE._toggleTaskExpand = function(id) {
1956
+ if (_expandedTasks.has(id)) _expandedTasks.delete(id);
1957
+ else _expandedTasks.add(id);
1958
+ _snapshotGroupState();
1959
+ if (cache.allTasks) _renderTasksDirect(filterTasks(cache.allTasks));
1960
+ };
1961
+
1962
+ // Capture current open/closed state of all <details> groups before re-render
1963
+ function _snapshotGroupState() {
1964
+ var groups = document.querySelectorAll('.we-task-group[data-group-key]');
1965
+ groups.forEach(function(el) {
1966
+ var key = el.getAttribute('data-group-key');
1967
+ if (!key) return;
1968
+ if (el.open) { _expandedGroups.add(key); _collapsedGroups.delete(key); }
1969
+ else { _collapsedGroups.add(key); _expandedGroups.delete(key); }
1970
+ });
1971
+ }
1972
+
1929
1973
  WE._clearAllFilters = function() {
1930
1974
  _taskFilter.statusSet.clear();
1931
1975
  _taskFilter.search = '';
@@ -1995,42 +2039,62 @@ function _renderTasksContentInner(tasks) {
1995
2039
  var body = document.getElementById('walle-body');
1996
2040
  if (!body) return;
1997
2041
 
1998
- var totalCount = cache.allTasks ? cache.allTasks.length : tasks.length;
1999
- // ── Toolbar: search + filters + group toggle + new button ──
2000
- var html = '<div class="we-task-toolbar">';
2001
- html += '<input class="we-chat-search-input" id="we-task-search" placeholder="Search tasks..." value="' + esc(_taskFilter.search) + '" oninput="WE._onTaskFilter()" style="flex:1">';
2002
- html += '<select class="walle-filter-select" id="we-task-type-filter" onchange="WE._onTaskFilter()">';
2003
- html += '<option value="all"' + (_taskFilter.type === 'all' ? ' selected' : '') + '>All types</option>';
2004
- html += '<option value="recurring"' + (_taskFilter.type === 'recurring' ? ' selected' : '') + '>Recurring</option>';
2005
- html += '<option value="once"' + (_taskFilter.type === 'once' ? ' selected' : '') + '>One-time</option>';
2006
- html += '<option value="script"' + (_taskFilter.type === 'script' ? ' selected' : '') + '>Script</option>';
2007
- html += '<option value="ai"' + (_taskFilter.type === 'ai' ? ' selected' : '') + '>AI</option>';
2008
- html += '</select>';
2009
- // Group-by toggle
2010
- html += '<select class="walle-filter-select" id="we-task-group" onchange="WE._onTaskFilter()">';
2011
- html += '<option value="status"' + ((_taskFilter.group || 'status') === 'status' ? ' selected' : '') + '>By status</option>';
2012
- html += '<option value="type"' + (_taskFilter.group === 'type' ? ' selected' : '') + '>By type</option>';
2013
- html += '</select>';
2014
- html += '<button class="walle-btn primary" onclick="WE._newTaskForm()">+ New</button>';
2042
+ var allTasks = cache.allTasks || tasks;
2043
+ var inboxCount = _countInbox(allTasks);
2044
+
2045
+ // ── View Tabs ──
2046
+ var html = '<div class="we-view-tabs">';
2047
+ var tabs = [
2048
+ { id: 'active', label: 'Active' },
2049
+ { id: 'inbox', label: 'Inbox', badge: inboxCount },
2050
+ { id: 'automation', label: 'Automation' },
2051
+ { id: 'done', label: 'Done' },
2052
+ { id: 'all', label: 'All' }
2053
+ ];
2054
+ tabs.forEach(function(tab) {
2055
+ var cls = 'we-view-tab' + (_taskViewTab === tab.id ? ' active' : '');
2056
+ html += '<button class="' + cls + '" onclick="WE._switchTaskView(\'' + tab.id + '\')">' + tab.label;
2057
+ if (tab.badge) html += '<span class="we-view-tab-badge">' + tab.badge + '</span>';
2058
+ html += '</button>';
2059
+ });
2060
+ html += '<button class="walle-btn primary" style="margin-left:auto" onclick="WE._newTaskForm()">+ New</button>';
2015
2061
  html += '</div>';
2016
2062
 
2017
- // Clickable status filter pills
2018
- var counts = { running: 0, pending: 0, paused: 0, completed: 0, failed: 0 };
2019
- (cache.allTasks || tasks).forEach(function(t) { if (counts[t.status] !== undefined) counts[t.status]++; });
2020
- var statusLabels = { running: 'running', pending: 'pending', paused: 'paused', completed: 'done', failed: 'failed' };
2021
- html += '<div class="we-task-summary">';
2022
- ['running', 'pending', 'paused', 'completed', 'failed'].forEach(function(s) {
2023
- var active = _taskFilter.statusSet.has(s);
2024
- var cls = 'we-task-summary-item ' + s + (active ? ' active' : '');
2025
- html += '<span class="' + cls + '" onclick="WE._toggleStatusFilter(\'' + s + '\')">' + counts[s] + ' ' + statusLabels[s] + '</span>';
2026
- });
2027
- var hasAnyFilter = _taskFilter.statusSet.size > 0 || _taskFilter.search || _taskFilter.type !== 'all';
2028
- if (hasAnyFilter) html += '<span class="we-task-clear-btn" onclick="WE._clearAllFilters()" title="Clear all filters">&times;</span>';
2029
- if (tasks.length < totalCount) html += '<span style="color:#666;margin-left:4px">' + tasks.length + '/' + totalCount + '</span>';
2030
- html += '</div>';
2063
+ // ── Toolbar: search + filters (only in active/all/done views) ──
2064
+ if (_taskViewTab !== 'automation') {
2065
+ html += '<div class="we-task-toolbar">';
2066
+ html += '<input class="we-chat-search-input" id="we-task-search" placeholder="Search tasks..." value="' + esc(_taskFilter.search) + '" oninput="WE._onTaskFilter()" style="flex:1">';
2067
+ if (_taskViewTab === 'all') {
2068
+ html += '<select class="walle-filter-select" id="we-task-type-filter" onchange="WE._onTaskFilter()">';
2069
+ html += '<option value="all"' + (_taskFilter.type === 'all' ? ' selected' : '') + '>All types</option>';
2070
+ html += '<option value="recurring"' + (_taskFilter.type === 'recurring' ? ' selected' : '') + '>Recurring</option>';
2071
+ html += '<option value="once"' + (_taskFilter.type === 'once' ? ' selected' : '') + '>One-time</option>';
2072
+ html += '<option value="script"' + (_taskFilter.type === 'script' ? ' selected' : '') + '>Script</option>';
2073
+ html += '<option value="ai"' + (_taskFilter.type === 'ai' ? ' selected' : '') + '>AI</option>';
2074
+ html += '</select>';
2075
+ }
2076
+ html += '</div>';
2077
+ }
2031
2078
 
2032
- // Slack auth banner show if any task has a Slack auth error
2033
- var hasSlackAuthError = (cache.allTasks || tasks).some(function(t) {
2079
+ // ── Context pills (for active/all views) ──
2080
+ if (_taskViewTab === 'active' || _taskViewTab === 'all') {
2081
+ var counts = { running: 0, pending: 0, paused: 0, completed: 0, failed: 0 };
2082
+ allTasks.forEach(function(t) { if (counts[t.status] !== undefined) counts[t.status]++; });
2083
+ var statusLabels = { running: 'running', pending: 'pending', paused: 'paused', completed: 'done', failed: 'failed' };
2084
+ var visibleStatuses = _taskViewTab === 'active' ? ['running', 'pending', 'failed'] : ['running', 'pending', 'paused', 'completed', 'failed'];
2085
+ html += '<div class="we-task-summary">';
2086
+ visibleStatuses.forEach(function(s) {
2087
+ var active = _taskFilter.statusSet.has(s);
2088
+ var cls = 'we-task-summary-item ' + s + (active ? ' active' : '');
2089
+ html += '<span class="' + cls + '" onclick="WE._toggleStatusFilter(\'' + s + '\')">' + counts[s] + ' ' + statusLabels[s] + '</span>';
2090
+ });
2091
+ var hasAnyFilter = _taskFilter.statusSet.size > 0 || _taskFilter.search || _taskFilter.type !== 'all';
2092
+ if (hasAnyFilter) html += '<span class="we-task-clear-btn" onclick="WE._clearAllFilters()" title="Clear all filters">&times;</span>';
2093
+ html += '</div>';
2094
+ }
2095
+
2096
+ // Slack auth banner
2097
+ var hasSlackAuthError = allTasks.some(function(t) {
2034
2098
  return t.error && (t.error.includes('Slack token expired') || t.error.includes('invalid_auth'));
2035
2099
  });
2036
2100
  if (hasSlackAuthError) {
@@ -2044,60 +2108,157 @@ function _renderTasksContentInner(tasks) {
2044
2108
  html += '<div id="we-new-task-form" style="display:none"></div>';
2045
2109
 
2046
2110
  if (tasks.length === 0) {
2047
- html += '<div class="walle-empty" style="padding:20px">No tasks match your filters.</div>';
2111
+ var emptyMsg = _taskViewTab === 'inbox' ? 'No Slack requests yet. Mention @walle in Slack to assign tasks.' : 'No tasks match your filters.';
2112
+ html += '<div class="walle-empty" style="padding:20px">' + emptyMsg + '</div>';
2048
2113
  safeSetHtml(body, html);
2049
2114
  return;
2050
2115
  }
2051
2116
 
2052
- // ── Grouping ──
2053
- var groupBy = _taskFilter.group || 'status';
2054
- var groupEl = document.getElementById('we-task-group');
2055
- if (groupEl) groupBy = groupEl.value;
2056
-
2057
- // Expanded by default: running, pending, failed. Collapsed: paused, completed.
2058
- var expandByDefault = { running: true, pending: true, failed: true, paused: false, completed: false, recurring: true, once: true };
2059
-
2060
- function taskGroup(label, items, color, key) {
2061
- var open = _taskFilter.search ? ' open' : (expandByDefault[key] !== false ? ' open' : '');
2062
- var h = '<details class="we-task-group"' + open + '>';
2063
- h += '<summary class="we-task-group-header" style="color:' + (color || '#aaa') + '">' + label + ' (' + items.length + ')</summary>';
2064
- items.forEach(function(t) { h += renderTaskCard(t); });
2065
- h += '</details>';
2066
- return h;
2117
+ // ── Automation table view ──
2118
+ if (_taskViewTab === 'automation') {
2119
+ html += _renderAutomationTable(tasks);
2120
+ safeSetHtml(body, html);
2121
+ // Still poll running tasks
2122
+ var runningTasks = tasks.filter(function(t) { return t.status === 'running'; });
2123
+ var runningIds = {};
2124
+ runningTasks.forEach(function(t) { runningIds[t.id] = true; _pollTaskLogs(t.id); });
2125
+ Object.keys(_logPollers).forEach(function(id) { if (!runningIds[id]) _stopLogPoller(id); });
2126
+ if (runningTasks.length > 0) _ensureRunningTimer();
2127
+ _setupTaskRefreshPoller(tasks, runningTasks);
2128
+ return;
2067
2129
  }
2068
2130
 
2069
- if (groupBy === 'type') {
2070
- var recurring = tasks.filter(function(t) { return t.type === 'recurring'; });
2071
- var oneTime = tasks.filter(function(t) { return t.type !== 'recurring'; });
2072
- if (recurring.length > 0) html += taskGroup('Recurring', recurring, '#60a5fa', 'recurring');
2073
- if (oneTime.length > 0) html += taskGroup('One-time', oneTime, '#aaa', 'once');
2074
- } else {
2075
- var statusOrder = ['running', 'pending', 'paused', 'failed', 'completed'];
2131
+ // ── Inbox view — group by lifecycle ──
2132
+ if (_taskViewTab === 'inbox') {
2133
+ var inboxNew = tasks.filter(function(t) { return t.status === 'pending'; });
2134
+ var inboxProgress = tasks.filter(function(t) { return t.status === 'running'; });
2135
+ var inboxFailed = tasks.filter(function(t) { return t.status === 'failed'; });
2136
+ var inboxActiveThread = tasks.filter(function(t) { return (t.status === 'completed' || t.status === 'cancelled') && t.slack_thread && t.slack_thread.active; });
2137
+ var activeThreadIds = {};
2138
+ inboxActiveThread.forEach(function(t) { activeThreadIds[t.id] = true; });
2139
+ var inboxDone = tasks.filter(function(t) { return (t.status === 'completed' || t.status === 'cancelled') && !activeThreadIds[t.id]; });
2140
+ if (inboxActiveThread.length > 0) html += _taskGroup('Active Conversations', inboxActiveThread, '#4ade80', 'active-thread');
2141
+ if (inboxProgress.length > 0) html += _taskGroup('In Progress', inboxProgress, '#228be6', 'running');
2142
+ if (inboxNew.length > 0) html += _taskGroup('New', inboxNew, '#fab005', 'pending');
2143
+ if (inboxFailed.length > 0) html += _taskGroup('Failed', inboxFailed, '#e03131', 'failed');
2144
+ if (inboxDone.length > 0) html += _taskGroup('Done', inboxDone, '#5c940d', 'completed', true);
2145
+ }
2146
+ // ── Active / Done / All views — group by status ──
2147
+ else {
2148
+ var statusOrder = _taskViewTab === 'done'
2149
+ ? ['completed', 'cancelled']
2150
+ : ['running', 'pending', 'paused', 'failed', 'completed'];
2076
2151
  var groups = {};
2077
2152
  statusOrder.forEach(function(s) { groups[s] = []; });
2078
2153
  tasks.forEach(function(t) { (groups[t.status] || groups.pending).push(t); });
2079
-
2080
- var colors = { running: '#228be6', pending: '#fab005', paused: '#888', completed: '#5c940d', failed: '#e03131' };
2154
+ var colors = { running: '#228be6', pending: '#fab005', paused: '#888', completed: '#5c940d', failed: '#e03131', cancelled: '#666' };
2081
2155
  statusOrder.forEach(function(status) {
2082
2156
  var g = groups[status];
2083
2157
  if (g.length === 0) return;
2084
- html += taskGroup(status.charAt(0).toUpperCase() + status.slice(1), g, colors[status], status);
2158
+ var collapsed = (status === 'paused' || status === 'completed' || status === 'cancelled');
2159
+ html += _taskGroup(status.charAt(0).toUpperCase() + status.slice(1), g, colors[status], status, collapsed);
2085
2160
  });
2086
2161
  }
2087
2162
 
2088
2163
  safeSetHtml(body, html);
2089
2164
 
2090
- // Poll logs for running tasks (without re-rendering the page)
2165
+ // Poll logs for running tasks
2091
2166
  var runningTasks = tasks.filter(function(t) { return t.status === 'running'; });
2092
2167
  var runningIds = {};
2093
2168
  runningTasks.forEach(function(t) { runningIds[t.id] = true; _pollTaskLogs(t.id); });
2094
- // Stop pollers for tasks that are no longer running
2095
2169
  Object.keys(_logPollers).forEach(function(id) { if (!runningIds[id]) _stopLogPoller(id); });
2096
-
2097
- // Start/stop live elapsed-time ticking for running tasks
2098
2170
  if (runningTasks.length > 0) _ensureRunningTimer();
2171
+ _setupTaskRefreshPoller(tasks, runningTasks);
2172
+ }
2173
+
2174
+ function _taskGroup(label, items, color, key, defaultCollapsed) {
2175
+ // User explicit state takes priority, then search forces open, then default
2176
+ var isOpen;
2177
+ if (_taskFilter.search) {
2178
+ isOpen = true;
2179
+ } else if (_expandedGroups.has(key)) {
2180
+ isOpen = true;
2181
+ } else if (_collapsedGroups.has(key)) {
2182
+ isOpen = false;
2183
+ } else {
2184
+ isOpen = !defaultCollapsed;
2185
+ }
2186
+ var h = '<details class="we-task-group"' + (isOpen ? ' open' : '') + ' data-group-key="' + esc(key) + '">';
2187
+ h += '<summary class="we-task-group-header" style="color:' + (color || '#aaa') + '">' + label + ' (' + items.length + ')</summary>';
2188
+ items.forEach(function(t) { h += renderTaskCard(t); });
2189
+ h += '</details>';
2190
+ return h;
2191
+ }
2192
+
2193
+
2194
+ function _renderAutomationTable(tasks) {
2195
+ var h = '';
2196
+ // Health summary
2197
+ var active = tasks.filter(function(t) { return t.status === 'pending' || t.status === 'running'; }).length;
2198
+ var paused = tasks.filter(function(t) { return t.status === 'paused'; }).length;
2199
+ var failed = tasks.filter(function(t) { return t.status === 'failed'; }).length;
2200
+ h += '<div class="we-auto-health">';
2201
+ h += '<span class="we-auto-health-item ok">' + active + ' active</span>';
2202
+ if (paused > 0) h += '<span class="we-auto-health-item paused">' + paused + ' paused</span>';
2203
+ if (failed > 0) h += '<span class="we-auto-health-item failed">' + failed + ' failed</span>';
2204
+ h += '</div>';
2205
+
2206
+ h += '<table class="we-auto-table"><thead><tr>';
2207
+ h += '<th>Task</th><th>Schedule</th><th>Last Run</th><th>Status</th><th></th>';
2208
+ h += '</tr></thead><tbody>';
2209
+ tasks.forEach(function(t) {
2210
+ var statusColors = { running: '#228be6', pending: '#5c940d', completed: '#5c940d', failed: '#e03131', paused: '#888', cancelled: '#666' };
2211
+ var statusIcons = { running: '\u25C9', pending: '\u25CF', completed: '\u2713', failed: '\u2717', paused: '\u23F8', cancelled: '\u2014' };
2212
+ var color = statusColors[t.status] || '#888';
2213
+ var icon = statusIcons[t.status] || '';
2214
+ var lastRun = t.last_run_at ? timeAgo(t.last_run_at) : 'never';
2215
+ var isExpanded = _expandedTasks.has(t.id);
2216
+ h += '<tr class="we-auto-row' + (isExpanded ? ' expanded' : '') + '" onclick="WE._toggleTaskExpand(\'' + esc(t.id) + '\')">';
2217
+ h += '<td class="we-auto-title">' + esc(t.title);
2218
+ if (t.skill) h += ' <span class="walle-tag" style="background:#1a3a1a;color:#4ade80;font-size:9px">' + esc(t.skill) + '</span>';
2219
+ h += '</td>';
2220
+ h += '<td class="we-auto-sched">' + esc(t.schedule || (t.type === 'once' ? 'one-time' : '-')) + '</td>';
2221
+ h += '<td class="we-auto-last">' + esc(lastRun) + '</td>';
2222
+ h += '<td style="color:' + color + '">' + icon + ' ' + esc(t.status) + '</td>';
2223
+ h += '<td class="we-auto-actions">';
2224
+ if (t.status === 'running') h += '<button class="we-auto-btn" onclick="event.stopPropagation();WE._stopTask(\'' + esc(t.id) + '\')">Stop</button>';
2225
+ else h += '<button class="we-auto-btn" onclick="event.stopPropagation();WE._runTaskNow(\'' + esc(t.id) + '\',this)">Run</button>';
2226
+ h += '</td>';
2227
+ h += '</tr>';
2228
+ if (isExpanded) {
2229
+ h += '<tr class="we-auto-detail"><td colspan="5">' + _renderExpandedCardContent(t) + '</td></tr>';
2230
+ }
2231
+ });
2232
+ h += '</tbody></table>';
2233
+ return h;
2234
+ }
2235
+
2236
+ function _renderExpandedCardContent(t) {
2237
+ var h = '';
2238
+ if (t.description) h += '<div class="we-task-card-desc">' + esc(t.description) + '</div>';
2239
+ var taskItems = _getBriefingItemsForTask(t);
2240
+ if (taskItems.length > 0) h += renderBriefingItemsTable(taskItems, t);
2241
+ if (t.status === 'running') {
2242
+ h += '<div class="we-task-log-panel" id="we-task-log-' + esc(t.id) + '">';
2243
+ h += '<div class="we-task-log-header"><span class="we-task-log-pulse"></span> Live output</div>';
2244
+ h += '<pre class="we-task-log-content">Waiting for output...</pre>';
2245
+ h += '</div>';
2246
+ } else if (t.result || t.error) {
2247
+ h += _renderTaskResultPanel(t);
2248
+ }
2249
+ h += '<div class="we-task-card-actions" style="margin-top:8px">';
2250
+ if (t.status !== 'running') h += '<button class="walle-btn primary" onclick="WE._runTaskNow(\'' + esc(t.id) + '\',this)">Run Now</button>';
2251
+ if (t.type === 'recurring') {
2252
+ if (t.status === 'paused') h += '<button class="walle-btn" onclick="WE._pauseTask(\'' + esc(t.id) + '\',0)">Resume</button>';
2253
+ else if (t.status !== 'running') h += '<button class="walle-btn" onclick="WE._pauseTask(\'' + esc(t.id) + '\',1)">Pause</button>';
2254
+ }
2255
+ h += '<button class="walle-btn" onclick="WE._editTask(\'' + esc(t.id) + '\')">Edit</button>';
2256
+ h += '</div>';
2257
+ h += '<div id="we-edit-task-' + esc(t.id) + '" style="display:none"></div>';
2258
+ return h;
2259
+ }
2099
2260
 
2100
- // Light status check — only full re-render when a task changes status
2261
+ function _setupTaskRefreshPoller(tasks, runningTasks) {
2101
2262
  if (runningTasks.length > 0 && !_taskRefreshTimer) {
2102
2263
  _taskRefreshTimer = setInterval(function() {
2103
2264
  api('/tasks?limit=100').then(function(data) {
@@ -2109,6 +2270,7 @@ function _renderTasksContentInner(tasks) {
2109
2270
  if (displayed && displayed.status !== nt.status) changed = true;
2110
2271
  });
2111
2272
  if (changed) {
2273
+ _snapshotGroupState();
2112
2274
  Object.keys(_logPollers).forEach(_stopLogPoller);
2113
2275
  _renderTasksDirect(filterTasks(newTasks));
2114
2276
  }
@@ -2413,15 +2575,25 @@ WE._copyTaskLink = function(taskId) {
2413
2575
  });
2414
2576
  };
2415
2577
 
2578
+ function _sourceIcon(t) {
2579
+ var src = t.source || 'system';
2580
+ if (src === 'slack') return '<span class="we-src-icon slack" title="From Slack">slack</span>';
2581
+ if (src === 'chat') return '<span class="we-src-icon chat" title="From Chat">chat</span>';
2582
+ if (src === 'initiative') return '<span class="we-src-icon init" title="Auto-created">auto</span>';
2583
+ return '';
2584
+ }
2585
+
2416
2586
  function renderTaskCard(t) {
2417
2587
  var q = _taskFilter.search || '';
2418
2588
  var statusColors = { running: '#228be6', pending: '#fab005', completed: '#5c940d', failed: '#e03131', paused: '#888', cancelled: '#666' };
2419
2589
  var borderColor = statusColors[t.status] || '#333';
2590
+ var isRunning = t.status === 'running';
2591
+ var isExpanded = _expandedTasks.has(t.id) || isRunning;
2420
2592
 
2421
- var html = '<div class="we-task-card" id="task-' + esc(t.id) + '" style="border-left-color:' + borderColor + '">';
2593
+ var html = '<div class="we-task-card' + (isExpanded ? ' expanded' : ' compact') + '" id="task-' + esc(t.id) + '" style="border-left-color:' + borderColor + '">';
2422
2594
 
2423
- // Row 1: title + badges + status
2424
- html += '<div class="we-task-card-header">';
2595
+ // Row 1: title + badges + status (always visible)
2596
+ html += '<div class="we-task-card-header" onclick="WE._toggleTaskExpand(\'' + esc(t.id) + '\')" style="cursor:pointer">';
2425
2597
  html += '<span class="we-task-card-title">' + _hl(t.title, q);
2426
2598
  if (t.priority !== 'normal') html += ' <span class="walle-tag ' + esc(t.priority) + '">' + esc(t.priority) + '</span>';
2427
2599
  if (t.execution === 'script') html += ' <span class="walle-tag" style="background:#333;color:#aaa">script</span>';
@@ -2431,26 +2603,63 @@ function renderTaskCard(t) {
2431
2603
  html += '<span class="we-task-card-status" style="color:' + borderColor + '"><span class="we-status-dot we-status-dot--' + esc(t.status) + '"></span>' + esc(t.status) + '</span>';
2432
2604
  html += '</div>';
2433
2605
 
2434
- // Row 2: description
2435
- if (t.description) html += '<div class="we-task-card-desc">' + _hl(t.description, q) + '</div>';
2436
-
2437
- // Row 3: metadata chips — primary (schedule + last run) always visible
2438
- html += '<div class="we-task-meta">';
2606
+ // Row 2: compact metadata line (always visible)
2607
+ html += '<div class="we-task-meta-compact">';
2608
+ html += _sourceIcon(t);
2439
2609
  if (t.type === 'recurring' && t.schedule) html += '<span>\uD83D\uDD01 ' + esc(t.schedule) + '</span>';
2440
- if (t.last_run_at) html += '<span>\u23F1 Last: ' + esc(timeAgo(t.last_run_at)) + '</span>';
2441
- if (t.status === 'running' && t.started_at) {
2610
+ if (t.last_run_at) html += '<span>Last: ' + esc(timeAgo(t.last_run_at)) + '</span>';
2611
+ if (isRunning && t.started_at) {
2442
2612
  var startMs = new Date(t.started_at + (t.started_at.includes('Z') ? '' : 'Z')).getTime();
2443
2613
  var elapsedSec = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
2444
2614
  var elStr = elapsedSec < 60 ? elapsedSec + 's' : Math.floor(elapsedSec / 60) + 'm ' + (elapsedSec % 60) + 's';
2445
- html += '<span class="we-task-running-timer" data-start-ms="' + startMs + '" style="color:#228be6">\u23F3 Running: ' + elStr + '</span>';
2615
+ html += '<span class="we-task-running-timer" data-start-ms="' + startMs + '" style="color:#228be6">\u23F3 ' + elStr + '</span>';
2616
+ }
2617
+ if (t.run_count > 0 && !isExpanded) html += '<span>' + t.run_count + ' runs</span>';
2618
+ // Slack source_ref link (rewrite to enterprise domain if needed)
2619
+ 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>';
2622
+ }
2623
+ // Compact inline actions
2624
+ if (!isExpanded) {
2625
+ html += '<span class="we-compact-actions">';
2626
+ if (isRunning) html += '<button class="we-compact-btn stop" onclick="event.stopPropagation();WE._stopTask(\'' + esc(t.id) + '\')" title="Stop">\u25A0</button>';
2627
+ else html += '<button class="we-compact-btn run" onclick="event.stopPropagation();WE._runTaskNow(\'' + esc(t.id) + '\',this)" title="Run">\u25B6</button>';
2628
+ html += '<button class="we-compact-btn more" onclick="event.stopPropagation();WE._taskMenu(\'' + esc(t.id) + '\',this)" title="More actions">\u22EE</button>';
2629
+ html += '</span>';
2446
2630
  }
2447
- // Secondary metadata — collapsed behind "details" toggle
2448
- var _hasExtra = (t.run_count > 0) || (t.last_duration_ms && t.last_duration_ms > 0 && t.status !== 'running') || (t.next_run_at && (t.status === 'pending' || t.status === 'paused'));
2449
- if (_hasExtra) {
2450
- html += '<span class="we-task-meta-toggle" onclick="this.parentElement.classList.toggle(\'we-meta-expanded\');event.stopPropagation()">details</span>';
2451
- html += '<span class="we-task-meta-extra">';
2631
+ html += '</div>';
2632
+
2633
+ // ── Expanded content (only if expanded or running) ──
2634
+ if (isExpanded) {
2635
+ // Slack thread info panel
2636
+ if (t.source === 'slack') {
2637
+ html += '<div class="we-slack-info">';
2638
+ // Clean description: strip the ---\n**IMPORTANT** instructions block
2639
+ var cleanDesc = (t.description || '').replace(/\n---\n\*\*IMPORTANT\*\*[\s\S]*$/, '').replace(/^(Question from .+? in Slack:|Slack (?:task|mention) from .+?:)\n\n/, '').trim();
2640
+ if (cleanDesc) html += '<div class="we-task-card-desc">' + _hl(cleanDesc, q) + '</div>';
2641
+ // Thread watch status
2642
+ if (t.slack_thread) {
2643
+ var isActive = t.slack_thread.active;
2644
+ var expiresAt = new Date(t.slack_thread.expires_at);
2645
+ var msLeft = expiresAt - Date.now();
2646
+ var timeLeft = msLeft > 0 ? (msLeft > 3600000 ? Math.floor(msLeft / 3600000) + 'h ' + Math.floor((msLeft % 3600000) / 60000) + 'm' : Math.floor(msLeft / 60000) + 'm') : '';
2647
+ html += '<div class="we-slack-thread-status">';
2648
+ html += isActive
2649
+ ? '<span class="we-thread-badge active">\uD83D\uDFE2 Thread active</span><span class="we-thread-expires">Reply in Slack to continue \u00B7 expires in ' + timeLeft + '</span>'
2650
+ : '<span class="we-thread-badge expired">\u26AA Thread expired</span>';
2651
+ html += '</div>';
2652
+ }
2653
+ html += '</div>';
2654
+ } else {
2655
+ // Non-slack description
2656
+ if (t.description) html += '<div class="we-task-card-desc">' + _hl(t.description, q) + '</div>';
2657
+ }
2658
+
2659
+ // Extra metadata
2660
+ html += '<div class="we-task-meta">';
2452
2661
  if (t.run_count > 0) html += '<span>\u25B6 ' + t.run_count + ' runs</span>';
2453
- if (t.last_duration_ms && t.last_duration_ms > 0 && t.status !== 'running') {
2662
+ if (t.last_duration_ms && t.last_duration_ms > 0 && !isRunning) {
2454
2663
  var durSec = Math.round(t.last_duration_ms / 1000);
2455
2664
  var durStr = durSec < 60 ? durSec + 's' : Math.floor(durSec / 60) + 'm ' + (durSec % 60) + 's';
2456
2665
  html += '<span>\u23F1 Took: ' + durStr + '</span>';
@@ -2468,48 +2677,42 @@ function renderTaskCard(t) {
2468
2677
  }
2469
2678
  }
2470
2679
  }
2471
- html += '</span>';
2472
- }
2473
- html += '</div>';
2474
-
2475
- // Row 4: briefing items (if any), live logs (running), or result (completed/failed)
2476
- var taskItems = _getBriefingItemsForTask(t);
2477
- if (taskItems.length > 0) {
2478
- html += renderBriefingItemsTable(taskItems, t);
2479
- }
2480
- if (t.status === 'running') {
2481
- html += '<div class="we-task-log-panel" id="we-task-log-' + esc(t.id) + '">';
2482
- html += '<div class="we-task-log-header"><span class="we-task-log-pulse"></span> Live output</div>';
2483
- html += '<pre class="we-task-log-content">Waiting for output...</pre>';
2484
2680
  html += '</div>';
2485
- } else if (t.result || t.error) {
2486
- html += _renderTaskResultPanel(t);
2487
- }
2488
2681
 
2489
- // Row 5: actions
2490
- html += '<div class="we-task-card-actions">';
2491
- if (t.status === 'running') {
2492
- html += '<button class="walle-btn" style="color:#e03131;border-color:#e03131" onclick="WE._stopTask(\'' + esc(t.id) + '\')">Stop</button>';
2493
- } else {
2494
- html += '<button class="walle-btn primary" id="we-run-btn-' + esc(t.id) + '" onclick="WE._runTaskNow(\'' + esc(t.id) + '\',this)">Run Now</button>';
2495
- }
2496
- // Pause/Resume for recurring
2497
- if (t.type === 'recurring') {
2498
- if (t.status === 'paused') html += '<button class="walle-btn" onclick="WE._pauseTask(\'' + esc(t.id) + '\',0)">Resume</button>';
2499
- else if (t.status !== 'running') html += '<button class="walle-btn" onclick="WE._pauseTask(\'' + esc(t.id) + '\',1)">Pause</button>';
2500
- }
2501
- // Cancel for pending one-time
2502
- if (t.status === 'pending' && t.type !== 'recurring') {
2503
- html += '<button class="walle-btn" onclick="WE._cancelTask(\'' + esc(t.id) + '\')">Cancel</button>';
2504
- }
2505
- // Remove for terminal states
2506
- if (t.status === 'completed' || t.status === 'failed' || t.status === 'cancelled') {
2507
- html += '<button class="walle-btn" onclick="WE._deleteTaskUI(\'' + esc(t.id) + '\')">Remove</button>';
2682
+ // Briefing items
2683
+ var taskItems = _getBriefingItemsForTask(t);
2684
+ if (taskItems.length > 0) html += renderBriefingItemsTable(taskItems, t);
2685
+
2686
+ // Live logs or result
2687
+ if (isRunning) {
2688
+ html += '<div class="we-task-log-panel" id="we-task-log-' + esc(t.id) + '">';
2689
+ html += '<div class="we-task-log-header"><span class="we-task-log-pulse"></span> Live output</div>';
2690
+ html += '<pre class="we-task-log-content">Waiting for output...</pre>';
2691
+ html += '</div>';
2692
+ } else if (t.result || t.error) {
2693
+ html += _renderTaskResultPanel(t);
2694
+ }
2695
+
2696
+ // Full action row
2697
+ html += '<div class="we-task-card-actions">';
2698
+ if (isRunning) {
2699
+ html += '<button class="walle-btn" style="color:#e03131;border-color:#e03131" onclick="WE._stopTask(\'' + esc(t.id) + '\')">Stop</button>';
2700
+ } else {
2701
+ html += '<button class="walle-btn primary" id="we-run-btn-' + esc(t.id) + '" onclick="WE._runTaskNow(\'' + esc(t.id) + '\',this)">Run Now</button>';
2702
+ }
2703
+ if (t.status === 'paused') {
2704
+ html += '<button class="walle-btn" onclick="WE._pauseTask(\'' + esc(t.id) + '\',0)">Resume</button>';
2705
+ } else if (!isRunning && t.status === 'pending') {
2706
+ html += '<button class="walle-btn" onclick="WE._pauseTask(\'' + esc(t.id) + '\',1)">Pause</button>';
2707
+ }
2708
+ if (!isRunning) {
2709
+ html += '<button class="walle-btn" style="color:#e03131;border-color:#e03131" onclick="WE._deleteTaskUI(\'' + esc(t.id) + '\')">Delete</button>';
2710
+ }
2711
+ html += '<button class="walle-btn" onclick="WE._editTask(\'' + esc(t.id) + '\')">Edit</button>';
2712
+ html += '</div>';
2713
+ html += '<div id="we-edit-task-' + esc(t.id) + '" style="display:none"></div>';
2508
2714
  }
2509
- html += '<button class="walle-btn" onclick="WE._editTask(\'' + esc(t.id) + '\')">Edit</button>';
2510
- html += '</div>';
2511
2715
 
2512
- html += '<div id="we-edit-task-' + esc(t.id) + '" style="display:none"></div>';
2513
2716
  html += '</div>';
2514
2717
  return html;
2515
2718
  }
@@ -2800,6 +3003,9 @@ WE._cancelTask = function(id) {
2800
3003
  };
2801
3004
 
2802
3005
  WE._deleteTaskUI = function(id) {
3006
+ var t = (cache.allTasks || []).find(function(x) { return x.id === id; });
3007
+ var title = t ? t.title : id.slice(0, 8);
3008
+ if (!confirm('Delete task "' + title + '"?\n\nThis cannot be undone.')) return;
2803
3009
  var token = window._ctmState?.token || '';
2804
3010
  fetch(WALLE_BASE + '/api/wall-e/tasks/' + id + '?token=' + token, { method: 'DELETE' }).then(function() { WE.renderTasks(); });
2805
3011
  };
@@ -2808,6 +3014,53 @@ WE._retryTask = function(id) {
2808
3014
  apiPut('/tasks/' + id, { status: 'pending', error: null, result: null }).then(function() { WE.renderTasks(); });
2809
3015
  };
2810
3016
 
3017
+ WE._taskMenu = function(id, btn) {
3018
+ // Close any existing menu
3019
+ var old = document.querySelector('.we-task-menu');
3020
+ if (old) old.remove();
3021
+ var t = (cache.allTasks || []).find(function(x) { return x.id === id; });
3022
+ if (!t) return;
3023
+
3024
+ var menu = document.createElement('div');
3025
+ menu.className = 'we-task-menu';
3026
+ var items = [];
3027
+
3028
+ // Context-appropriate actions
3029
+ if (t.status === 'running') {
3030
+ items.push({ label: '\u25A0 Stop', fn: function() { WE._stopTask(id); } });
3031
+ } else {
3032
+ items.push({ label: '\u25B6 Run Now', fn: function() { WE._runTaskNow(id); } });
3033
+ }
3034
+ if (t.status === 'paused') {
3035
+ items.push({ label: '\u25B6 Resume', fn: function() { WE._pauseTask(id, 0); } });
3036
+ } else if (t.status !== 'running') {
3037
+ items.push({ label: '\u23F8 Pause', fn: function() { WE._pauseTask(id, 1); } });
3038
+ }
3039
+ items.push({ label: '\u270E Edit', fn: function() { WE._toggleTaskExpand(id); setTimeout(function() { WE._editTask(id); }, 100); } });
3040
+ items.push({ label: '\u2716 Delete', fn: function() { WE._deleteTaskUI(id); }, danger: true });
3041
+
3042
+ items.forEach(function(item) {
3043
+ var row = document.createElement('div');
3044
+ row.className = 'we-task-menu-item' + (item.danger ? ' danger' : '');
3045
+ row.textContent = item.label;
3046
+ row.onclick = function(e) { e.stopPropagation(); menu.remove(); item.fn(); };
3047
+ menu.appendChild(row);
3048
+ });
3049
+
3050
+ // Position relative to button
3051
+ var rect = btn.getBoundingClientRect();
3052
+ menu.style.position = 'fixed';
3053
+ menu.style.top = (rect.bottom + 4) + 'px';
3054
+ menu.style.left = (rect.right - 140) + 'px';
3055
+ document.body.appendChild(menu);
3056
+
3057
+ // Close on outside click
3058
+ setTimeout(function() {
3059
+ function close(e) { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', close); } }
3060
+ document.addEventListener('click', close);
3061
+ }, 0);
3062
+ };
3063
+
2811
3064
  WE._editTask = function(id) {
2812
3065
  var el = document.getElementById('we-edit-task-' + id);
2813
3066
  if (!el) return;