create-walle 0.7.1 → 0.8.0

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.
@@ -288,7 +288,7 @@ function stopQuiet(dir, port) {
288
288
  try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
289
289
  }
290
290
  // Kill CTM and Wall-E processes
291
- const wallePort = String(parseInt(port) + 1);
291
+ const wallePort = dir ? readWallePort(dir) : String(parseInt(port) + 1);
292
292
  for (const p of [port, wallePort]) {
293
293
  try {
294
294
  const pids = execFileSync('lsof', ['-ti', ':' + p], { encoding: 'utf8', timeout: 3000 }).trim().split('\n').filter(Boolean);
@@ -454,6 +454,15 @@ function readPort(dir) {
454
454
  return '3456';
455
455
  }
456
456
 
457
+ function readWallePort(dir) {
458
+ try {
459
+ const env = fs.readFileSync(path.join(dir, '.env'), 'utf8');
460
+ const m = env.match(/^WALL_E_PORT=(\d+)/m);
461
+ if (m) return m[1];
462
+ } catch {}
463
+ return String(parseInt(readPort(dir)) + 1);
464
+ }
465
+
457
466
  function detectName() {
458
467
  try { return execFileSync('git', ['config', 'user.name'], { encoding: 'utf8' }).trim(); } catch {}
459
468
  try { return execFileSync('id', ['-F'], { encoding: 'utf8' }).trim(); } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Wall-E — your personal digital twin. AI agent that learns from Slack, email & calendar. Includes dashboard, chat, and 7 bundled skills.",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # Start a dev instance of CTM + Wall-E on separate ports with a DB snapshot.
3
+ # Primary instance stays untouched. Ctrl+C to stop.
4
+ #
5
+ # Usage:
6
+ # bash bin/dev.sh # Start dev instance (snapshot DBs on first run)
7
+ # bash bin/dev.sh --refresh # Re-copy production DBs before starting
8
+ # bash bin/dev.sh --fresh # Start with empty DBs
9
+
10
+ set -e
11
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
12
+ DEV_DIR="${WALLE_DEV_DIR:-/tmp/walle-dev}"
13
+ DEV_CTM_PORT="${DEV_CTM_PORT:-4000}"
14
+ DEV_WALLE_PORT="${DEV_WALLE_PORT:-4001}"
15
+
16
+ # Source production .env to find DB paths
17
+ PROD_CTM_DIR="$HOME/.walle/data"
18
+ PROD_WALLE_DIR="$HOME/.walle/data"
19
+ if [[ -f "$ROOT/.env" ]]; then
20
+ eval "$(grep -E '^(CTM_DATA_DIR|WALL_E_DATA_DIR)=' "$ROOT/.env" 2>/dev/null)" || true
21
+ [[ -n "$CTM_DATA_DIR" ]] && PROD_CTM_DIR="$CTM_DATA_DIR"
22
+ [[ -n "$WALL_E_DATA_DIR" ]] && PROD_WALLE_DIR="$WALL_E_DATA_DIR"
23
+ fi
24
+
25
+ mkdir -p "$DEV_DIR"
26
+
27
+ # Handle flags
28
+ if [[ "$1" == "--fresh" ]]; then
29
+ echo "[dev] Starting with fresh (empty) databases"
30
+ rm -f "$DEV_DIR"/*.db "$DEV_DIR"/*.db-wal "$DEV_DIR"/*.db-shm
31
+ elif [[ "$1" == "--refresh" || ! -f "$DEV_DIR/task-manager.db" ]]; then
32
+ echo "[dev] Copying production databases to $DEV_DIR ..."
33
+ # Copy CTM DB
34
+ if [[ -f "$PROD_CTM_DIR/task-manager.db" ]]; then
35
+ cp "$PROD_CTM_DIR/task-manager.db" "$DEV_DIR/task-manager.db"
36
+ echo " CTM: $PROD_CTM_DIR/task-manager.db → $DEV_DIR/"
37
+ fi
38
+ # Copy Wall-E brain
39
+ if [[ -f "$PROD_WALLE_DIR/wall-e-brain.db" ]]; then
40
+ cp "$PROD_WALLE_DIR/wall-e-brain.db" "$DEV_DIR/wall-e-brain.db"
41
+ echo " Brain: $PROD_WALLE_DIR/wall-e-brain.db → $DEV_DIR/"
42
+ fi
43
+ # Clean WAL files (they're specific to the source process)
44
+ rm -f "$DEV_DIR"/*.db-wal "$DEV_DIR"/*.db-shm
45
+ fi
46
+
47
+ echo ""
48
+ echo " Dev CTM: http://localhost:$DEV_CTM_PORT"
49
+ echo " Dev Wall-E: port $DEV_WALLE_PORT"
50
+ echo " Dev data: $DEV_DIR"
51
+ echo " Source: $ROOT"
52
+ echo ""
53
+ echo " Ctrl+C to stop. Primary instance on :3456 is unaffected."
54
+ echo ""
55
+
56
+ # Load production .env vars (API keys, etc.) but override ports and data dirs
57
+ export CTM_PORT="$DEV_CTM_PORT"
58
+ export WALL_E_PORT="$DEV_WALLE_PORT"
59
+ export CTM_DATA_DIR="$DEV_DIR"
60
+ export WALL_E_DATA_DIR="$DEV_DIR"
61
+ export CTM_HOST="127.0.0.1"
62
+
63
+ # Source the rest of .env (API keys, owner name, etc.)
64
+ if [[ -f "$ROOT/.env" ]]; then
65
+ set -a
66
+ source <(grep -v '^#' "$ROOT/.env" | grep -vE '^(CTM_PORT|WALL_E_PORT|CTM_DATA_DIR|WALL_E_DATA_DIR|CTM_HOST)=' | grep '=')
67
+ set +a
68
+ fi
69
+
70
+ # Run in foreground so Ctrl+C stops it cleanly
71
+ cd "$ROOT"
72
+ exec node claude-task-manager/server.js
@@ -137,7 +137,7 @@
137
137
  font-size: 10px; font-weight: 800; text-transform: uppercase;
138
138
  letter-spacing: 1px; margin-bottom: 3px;
139
139
  }
140
- .walle-chat-msg-role.user { color: #5c7cfa; }
140
+ .walle-chat-msg-role.user { color: #94a3b8; text-transform: none; font-weight: 700; letter-spacing: 0.3px; }
141
141
  .walle-chat-msg-role.assistant { color: #51cf66; }
142
142
 
143
143
  /* User text — plain, tight */
@@ -555,10 +555,31 @@
555
555
  margin-bottom: 4px;
556
556
  }
557
557
  .we-task-card-title { font-size: 13px; font-weight: 600; color: var(--accent, #60a5fa); }
558
- .we-task-card-status { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
558
+ .we-task-card-status { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; display: inline-flex; align-items: center; gap: 5px; }
559
559
  .we-task-card-desc { font-size: 12px; color: #999; line-height: 1.4; margin-bottom: 4px; }
560
560
  .we-task-card-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
561
561
 
562
+ /* Status dots */
563
+ .we-status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
564
+ .we-status-dot--running { background: #228be6; animation: we-pulse 1.5s ease-in-out infinite; }
565
+ .we-status-dot--pending { background: #fab005; }
566
+ .we-status-dot--paused { background: #888; }
567
+ .we-status-dot--failed { background: #e03131; }
568
+ .we-status-dot--completed { background: #5c940d; }
569
+ .we-status-dot--cancelled { background: #666; }
570
+ @keyframes we-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
571
+
572
+ /* Task metadata details toggle */
573
+ .we-task-meta-toggle { color: #555; cursor: pointer; font-size: 10px; text-decoration: underline; text-underline-offset: 2px; user-select: none; }
574
+ .we-task-meta-toggle:hover { color: #888; }
575
+ .we-task-meta-extra { display: none; }
576
+ .we-task-meta.we-meta-expanded .we-task-meta-extra { display: contents; }
577
+
578
+ /* Chat date separator */
579
+ .we-chat-date-sep { text-align: center; color: var(--fg-dim, #666); font-size: 11px; padding: 12px 0 4px; position: relative; }
580
+ .we-chat-date-sep::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; border-top: 1px solid rgba(255,255,255,0.06); }
581
+ .we-chat-date-sep span { position: relative; background: var(--bg, #1a1b26); padding: 0 12px; }
582
+
562
583
  /* Task live logs */
563
584
  .we-task-log-panel {
564
585
  margin-top: 8px;
@@ -2226,6 +2226,9 @@
2226
2226
  <button class="svc-btn" onclick="svcAction('walle','restart')" id="svc-walle-restart" style="display:none">Restart</button>
2227
2227
  </div>
2228
2228
  </div>
2229
+ <div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--border);">
2230
+ <button class="svc-btn" onclick="restartAll()" style="width:100%;">Restart All</button>
2231
+ </div>
2229
2232
  </div>
2230
2233
  </div>
2231
2234
  </div>
@@ -2234,12 +2237,17 @@
2234
2237
  <nav class="topbar-nav" id="topbar-nav">
2235
2238
  <button class="nav-pill active" data-nav="sessions" onclick="navTo('sessions')" title="Terminal sessions">Sessions</button>
2236
2239
  <button class="nav-pill" data-nav="prompts" onclick="navTo('prompts')" title="Prompt Editor">Prompts</button>
2237
- <button class="nav-pill" data-nav="insights" onclick="navTo('insights')" title="Session analysis">Insights</button>
2238
- <button class="nav-pill" data-nav="permissions" onclick="navTo('permissions')" title="Tool permissions">Permissions</button>
2239
- <button class="nav-pill" data-nav="codereview" onclick="navTo('codereview')" title="Code Review">Review</button>
2240
2240
  <button class="nav-pill" data-nav="walle" onclick="navTo('walle')" title="WALL-E Agent">WALL-E</button>
2241
- <button class="nav-pill" data-nav="rules" onclick="navTo('rules')" title="Edit CLAUDE.md rules">Rules</button>
2242
- <button class="nav-pill" data-nav="backups" onclick="navTo('backups')" title="Database backups">Backups</button>
2241
+ <div style="position:relative;display:inline-block;" id="nav-more-wrap">
2242
+ <button class="nav-pill" onclick="toggleNavMore()" title="More pages">More <span style="font-size:10px;">&#9662;</span></button>
2243
+ <div id="nav-more-dropdown" style="display:none;position:absolute;top:100%;left:0;z-index:999;background:var(--bg-light);border:1px solid var(--border);border-radius:6px;padding:4px 0;min-width:140px;box-shadow:0 4px 12px rgba(0,0,0,0.3);">
2244
+ <button class="nav-pill" data-nav="insights" onclick="navTo('insights');closeNavMore()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Insights</button>
2245
+ <button class="nav-pill" data-nav="permissions" onclick="navTo('permissions');closeNavMore()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Permissions</button>
2246
+ <button class="nav-pill" data-nav="codereview" onclick="navTo('codereview');closeNavMore()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Review</button>
2247
+ <button class="nav-pill" data-nav="rules" onclick="navTo('rules');closeNavMore()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Rules</button>
2248
+ <button class="nav-pill" data-nav="backups" onclick="navTo('backups');closeNavMore()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Backups</button>
2249
+ </div>
2250
+ </div>
2243
2251
  <a href="/setup.html" class="nav-pill" title="Setup & Settings" style="text-decoration:none;margin-left:auto;font-size:16px;padding:4px 8px;">⚙</a>
2244
2252
  </nav>
2245
2253
  <div class="topbar-right">
@@ -2307,14 +2315,22 @@
2307
2315
  </div>
2308
2316
  <div id="terminal-area">
2309
2317
  <div id="welcome">
2310
- <h2>Welcome to Claude Task Manager</h2>
2311
- <p>Create a new terminal session to get started. You can run Claude Code, shell commands, or any terminal program.</p>
2312
- <div class="shortcut-grid">
2313
- <span>New session</span><kbd>Ctrl+Shift+N</kbd>
2314
- <span>Close tab</span><kbd>Ctrl+Shift+W</kbd>
2315
- <span>Next tab</span><kbd>Ctrl+Shift+]</kbd>
2316
- <span>Prev tab</span><kbd>Ctrl+Shift+[</kbd>
2317
- <span>Toggle sidebar</span><kbd>Ctrl+Shift+B</kbd>
2318
+ <h2 style="font-size:24px;margin-bottom:4px;">Welcome to CTM</h2>
2319
+ <p style="color:var(--fg-dim);margin-bottom:20px;">Manage Claude Code sessions, prompts, and your AI assistant Wall-E.</p>
2320
+ <button class="btn primary" onclick="createSession()" style="font-size:15px;padding:8px 24px;margin-bottom:28px;">New Claude Session</button>
2321
+ <div style="display:flex;gap:16px;max-width:680px;">
2322
+ <div onclick="navTo('sessions')" style="flex:1;padding:14px;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:8px;cursor:pointer;">
2323
+ <div style="font-weight:600;margin-bottom:4px;color:var(--fg);">Sessions</div>
2324
+ <div style="font-size:12px;color:var(--fg-dim);">Run and manage Claude Code terminal sessions</div>
2325
+ </div>
2326
+ <div onclick="navTo('prompts')" style="flex:1;padding:14px;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:8px;cursor:pointer;">
2327
+ <div style="font-weight:600;margin-bottom:4px;color:var(--fg);">Prompts</div>
2328
+ <div style="font-size:12px;color:var(--fg-dim);">Save, organize, and send prompts to Claude</div>
2329
+ </div>
2330
+ <div onclick="navTo('walle')" style="flex:1;padding:14px;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:8px;cursor:pointer;">
2331
+ <div style="font-weight:600;margin-bottom:4px;color:var(--fg);">WALL-E</div>
2332
+ <div style="font-size:12px;color:var(--fg-dim);">Your personal AI assistant — chat, tasks, and insights</div>
2333
+ </div>
2318
2334
  </div>
2319
2335
  </div>
2320
2336
  <div id="insights-panel">
@@ -2379,12 +2395,17 @@
2379
2395
  <div class="walle-subnav">
2380
2396
  <button class="walle-subnav-btn active" data-view="chat" onclick="WE.showView('chat')">Chat</button>
2381
2397
  <button class="walle-subnav-btn" data-view="tasks" onclick="WE.showView('tasks')">Tasks</button>
2382
- <button class="walle-subnav-btn" data-view="brain" onclick="WE.showView('brain')">Brain</button>
2383
- <button class="walle-subnav-btn" data-view="actions" onclick="WE.showView('actions')">Actions</button>
2384
2398
  <button class="walle-subnav-btn" data-view="skills" onclick="WE.showView('skills')">Skills</button>
2385
- <button class="walle-subnav-btn" data-view="timeline" onclick="WE.showView('timeline')">Timeline</button>
2386
- <button class="walle-subnav-btn" data-view="questions" onclick="WE.showView('questions')">Questions</button>
2387
- <button class="walle-subnav-btn" data-view="status" onclick="WE.showView('status')">Status</button>
2399
+ <div style="position:relative;display:inline-block;" id="we-more-wrap">
2400
+ <button class="walle-subnav-btn" onclick="WE.toggleMoreTabs()" id="we-more-btn">More <span style="font-size:10px;">&#9662;</span></button>
2401
+ <div id="we-more-dropdown" style="display:none;position:absolute;top:100%;left:0;z-index:999;background:var(--bg-light);border:1px solid var(--border);border-radius:6px;padding:4px 0;min-width:120px;box-shadow:0 4px 12px rgba(0,0,0,0.3);">
2402
+ <button class="walle-subnav-btn" data-view="brain" onclick="WE.showView('brain');WE.closeMoreTabs()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Brain</button>
2403
+ <button class="walle-subnav-btn" data-view="actions" onclick="WE.showView('actions');WE.closeMoreTabs()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Actions</button>
2404
+ <button class="walle-subnav-btn" data-view="timeline" onclick="WE.showView('timeline');WE.closeMoreTabs()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Timeline</button>
2405
+ <button class="walle-subnav-btn" data-view="questions" onclick="WE.showView('questions');WE.closeMoreTabs()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Questions</button>
2406
+ <button class="walle-subnav-btn" data-view="status" onclick="WE.showView('status');WE.closeMoreTabs()" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;">Status</button>
2407
+ </div>
2408
+ </div>
2388
2409
  </div>
2389
2410
  </div>
2390
2411
  <div id="walle-body" class="walle-body"></div>
@@ -2420,6 +2441,11 @@
2420
2441
  <div id="permissions-panel">
2421
2442
  <div class="perm-toolbar">
2422
2443
  <h3>Permission Manager</h3>
2444
+ <div id="perm-explainer" style="background:rgba(255,255,255,0.03);border-left:3px solid var(--accent);padding:8px 12px;font-size:12px;color:var(--fg-dim);margin:0 0 8px;border-radius:0 4px 4px 0;display:flex;align-items:center;gap:8px;">
2445
+ <span style="flex:1;">Permissions control what Claude Code can do automatically. Rules you approve here apply across all sessions.</span>
2446
+ <button onclick="this.parentElement.style.display='none';localStorage.setItem('perm_explainer_dismissed','1')" style="background:none;border:none;color:var(--fg-dim);cursor:pointer;font-size:14px;padding:2px 6px;">&times;</button>
2447
+ </div>
2448
+ <script>if(localStorage.getItem('perm_explainer_dismissed')==='1'){document.getElementById('perm-explainer').style.display='none';}</script>
2423
2449
  <select id="perm-scope-filter" onchange="renderPermissions()" style="background:var(--bg-lighter);color:var(--fg);border:1px solid var(--border);padding:5px 8px;border-radius:5px;font-size:12px;">
2424
2450
  <option value="__global__">All Projects (Global)</option>
2425
2451
  </select>
@@ -2493,11 +2519,16 @@
2493
2519
  <!-- Prompts Panel (merged from prompts.html) -->
2494
2520
  <div id="prompts-panel">
2495
2521
  <div class="prompts-topbar">
2496
- <button class="btn" onclick="PE.showView('chains')">Chains</button>
2497
- <button class="btn" onclick="PE.showView('templates')">Templates</button>
2498
- <button class="btn" onclick="PE.showView('patterns')">Patterns</button>
2499
- <button class="btn" onclick="PE.openHarvestModal()">Harvest</button>
2500
- <button class="btn" onclick="PE.toggleCopilotPanel()">Copilot</button>
2522
+ <div style="position:relative;display:inline-block;" id="pe-power-wrap">
2523
+ <button class="btn" onclick="(function(){ var dd=document.getElementById('pe-power-dd'); var open=dd.style.display==='none'; dd.style.display=open?'block':'none'; if(open){setTimeout(function(){document.addEventListener('click',function h(e){if(!document.getElementById('pe-power-wrap').contains(e.target)){dd.style.display='none';document.removeEventListener('click',h,true);}},true);},0);} })()">Power Tools <span style="font-size:10px;">&#9662;</span></button>
2524
+ <div id="pe-power-dd" style="display:none;position:absolute;top:100%;left:0;z-index:999;background:var(--bg-light);border:1px solid var(--border);border-radius:6px;padding:4px 0;min-width:130px;box-shadow:0 4px 12px rgba(0,0,0,0.3);">
2525
+ <button class="btn" onclick="PE.showView('chains');document.getElementById('pe-power-dd').style.display='none'" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;border:none;">Chains</button>
2526
+ <button class="btn" onclick="PE.showView('templates');document.getElementById('pe-power-dd').style.display='none'" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;border:none;">Templates</button>
2527
+ <button class="btn" onclick="PE.showView('patterns');document.getElementById('pe-power-dd').style.display='none'" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;border:none;">Patterns</button>
2528
+ <button class="btn" onclick="PE.openHarvestModal();document.getElementById('pe-power-dd').style.display='none'" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;border:none;">Harvest</button>
2529
+ <button class="btn" onclick="PE.toggleCopilotPanel();document.getElementById('pe-power-dd').style.display='none'" style="display:block;width:100%;text-align:left;border-radius:0;padding:6px 14px;border:none;">Copilot</button>
2530
+ </div>
2531
+ </div>
2501
2532
  </div>
2502
2533
  <div id="prompts-panel-inner">
2503
2534
  <div id="prompt-sidebar">
@@ -2793,7 +2824,7 @@
2793
2824
  </div>
2794
2825
  <!-- Queue Builder Panel (right side) -->
2795
2826
  <div id="queue-panel-resize" style="width:4px;cursor:col-resize;flex-shrink:0;background:var(--border);display:none;transition:background 0.15s;z-index:2;" onmousedown="startQueuePanelResize(event)" onmouseenter="this.style.background='var(--accent)'" onmouseleave="this.style.background=''"></div>
2796
- <div id="queue-panel" style="display:flex;width:320px;background:var(--bg-light);border-left:none;flex-direction:column;flex-shrink:0;overflow:hidden;">
2827
+ <div id="queue-panel" style="display:none;width:320px;background:var(--bg-light);border-left:none;flex-direction:column;flex-shrink:0;overflow:hidden;">
2797
2828
  <div style="padding:10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;">
2798
2829
  <span style="flex:1;font-weight:600;font-size:13px;">Queue Builder</span>
2799
2830
  <button class="btn" style="padding:2px 8px;font-size:11px;" onclick="toggleQueuePanel()">Close</button>
@@ -3022,7 +3053,7 @@ const state = window._ctmState = {
3022
3053
  sidebarManuallyHidden: false,
3023
3054
  rulesFiles: [],
3024
3055
  currentRule: null,
3025
- queuePanelOpen: true,
3056
+ queuePanelOpen: false,
3026
3057
  };
3027
3058
 
3028
3059
  // --- WebSocket ---
@@ -3168,6 +3199,22 @@ async function pollServiceStatus() {
3168
3199
  }
3169
3200
  }
3170
3201
 
3202
+ async function restartAll() {
3203
+ if (!confirm('Restart CTM server and Wall-E?')) return;
3204
+ hideAppMenu();
3205
+ // Restart Wall-E first (while CTM is still up to handle the API call)
3206
+ try {
3207
+ await fetch(`/api/restart/walle?token=${state.token}`, { method: 'POST' });
3208
+ toast('Wall-E restarting...', { type: 'info' });
3209
+ } catch {}
3210
+ // Brief pause to let Wall-E restart begin, then restart CTM
3211
+ await new Promise(r => setTimeout(r, 500));
3212
+ state._restarting = true;
3213
+ try {
3214
+ await fetch(`/api/restart/ctm?token=${state.token}`, { method: 'POST' });
3215
+ } catch { /* expected — server dying */ }
3216
+ }
3217
+
3171
3218
  async function svcAction(service, action) {
3172
3219
  const label = service === 'ctm' ? 'CTM server' : 'Wall-E';
3173
3220
  if (action === 'stop' && !confirm(`Stop ${label}?`)) return;
@@ -3262,39 +3309,26 @@ function createTerminal(id) {
3262
3309
  term.onData((data) => {
3263
3310
  send({ type: 'input', id, data });
3264
3311
  clearWaitingState(id);
3265
- // User typed something — they want to follow output again
3312
+ // User typed something — scroll to bottom so they see the response
3266
3313
  const s = state.sessions.get(id);
3267
- if (s) s.userScrolling = false;
3314
+ if (s) s.term.scrollToBottom();
3268
3315
  });
3269
3316
 
3270
3317
  term.onResize(({ cols, rows }) => {
3271
3318
  send({ type: 'resize', id, cols, rows });
3272
3319
  });
3273
3320
 
3274
- // Track user scroll: if user scrolls up, pause auto-scroll.
3275
- // If they scroll back to bottom, resume auto-scroll.
3276
- const viewport = container.querySelector('.xterm-viewport');
3277
- if (viewport) {
3278
- viewport.addEventListener('scroll', () => {
3279
- const s = state.sessions.get(id);
3280
- if (!s || s._programmaticScroll) return; // Ignore scroll events from our own scrollToBottom
3281
- const atBottom = viewport.scrollTop + viewport.clientHeight >= viewport.scrollHeight - 10;
3282
- s.userScrolling = !atBottom;
3283
- });
3284
- // Mouse wheel on the terminal should always work — reset stuck userScrolling
3285
- // if user scrolls to the bottom manually
3286
- viewport.addEventListener('wheel', () => {
3287
- const s = state.sessions.get(id);
3288
- if (!s || !s.userScrolling) return;
3289
- // Check after the wheel event is processed
3290
- requestAnimationFrame(() => {
3291
- const atBottom = viewport.scrollTop + viewport.clientHeight >= viewport.scrollHeight - 10;
3292
- if (atBottom) s.userScrolling = false;
3293
- });
3294
- });
3295
- }
3321
+ // Auto-scroll is handled in onOutput using xterm.js buffer API (viewportY vs baseY).
3322
+ // No manual tracking needed xterm.js buffer is the source of truth.
3323
+ //
3324
+ // Safety: clicking the terminal forces a fit() recalculation, fixing any scroll desync
3325
+ // that may occur after Claude Code's TUI exits or terminal state changes.
3326
+ container.addEventListener('click', () => {
3327
+ const s = state.sessions.get(id);
3328
+ if (s) s.fitAddon.fit();
3329
+ });
3296
3330
 
3297
- state.sessions.set(id, { term, fitAddon, container, userScrolling: false, needsFontRefresh: true });
3331
+ state.sessions.set(id, { term, fitAddon, container, needsFontRefresh: true });
3298
3332
  return { term, fitAddon, container };
3299
3333
  }
3300
3334
 
@@ -3434,7 +3468,6 @@ function updateTopbarContext(activeId) {
3434
3468
  if (activeId === 'prompts') {
3435
3469
  ctxBtns.innerHTML = `
3436
3470
  <button class="topbar-util-btn" onclick="PE.showView('conversations')" title="Browse prompt conversations">Conversations</button>
3437
- <button class="topbar-util-btn" onclick="PE.showView('backups')" title="Prompt backups" style="color:var(--green)">Backups</button>
3438
3471
  `;
3439
3472
  divider.style.display = '';
3440
3473
  newSessionBtn.innerHTML = '+ New Prompt';
@@ -3449,6 +3482,21 @@ function updateTopbarContext(activeId) {
3449
3482
 
3450
3483
  let _prevNav = null; // track previous nav section for Alt+Tab swap
3451
3484
 
3485
+ function toggleNavMore() {
3486
+ const dd = document.getElementById('nav-more-dropdown');
3487
+ dd.style.display = dd.style.display === 'none' ? 'block' : 'none';
3488
+ if (dd.style.display === 'block') {
3489
+ setTimeout(() => document.addEventListener('click', _closeNavMoreOutside, true), 0);
3490
+ }
3491
+ }
3492
+ function closeNavMore() {
3493
+ document.getElementById('nav-more-dropdown').style.display = 'none';
3494
+ document.removeEventListener('click', _closeNavMoreOutside, true);
3495
+ }
3496
+ function _closeNavMoreOutside(e) {
3497
+ if (!document.getElementById('nav-more-wrap').contains(e.target)) closeNavMore();
3498
+ }
3499
+
3452
3500
  function navTo(target, opts) {
3453
3501
  // Track previous nav for Alt+Tab toggle
3454
3502
  const currentNav = document.querySelector('.nav-pill.active')?.dataset?.nav || 'sessions';
@@ -3617,11 +3665,30 @@ function _copyablePath(label, p) {
3617
3665
  return row;
3618
3666
  }
3619
3667
 
3668
+ var _backupSelected = { ctm: new Set(), brain: new Set() };
3669
+
3620
3670
  function _appendBackupTable(container, title, backups, type, showRestore) {
3671
+ var selected = _backupSelected[type];
3672
+ // Header row with title and batch delete
3673
+ var header = document.createElement('div');
3674
+ header.style.cssText = 'display:flex;align-items:center;gap:8px;margin:20px 0 8px;';
3621
3675
  var h3 = document.createElement('h3');
3622
- h3.style.cssText = 'font-size:13px;color:var(--fg-dim);margin:20px 0 8px;';
3676
+ h3.style.cssText = 'font-size:13px;color:var(--fg-dim);flex:1;margin:0;';
3623
3677
  h3.textContent = title + ' (' + backups.length + ')';
3624
- container.appendChild(h3);
3678
+ header.appendChild(h3);
3679
+ if (selected.size > 0) {
3680
+ var countLabel = document.createElement('span');
3681
+ countLabel.style.cssText = 'font-size:11px;color:var(--fg-dim);';
3682
+ countLabel.textContent = selected.size + ' selected';
3683
+ header.appendChild(countLabel);
3684
+ var batchBtn = document.createElement('button');
3685
+ batchBtn.className = 'btn small';
3686
+ batchBtn.style.color = 'var(--red)';
3687
+ batchBtn.textContent = 'Delete selected';
3688
+ batchBtn.onclick = function() { _batchDeleteBackups(type); };
3689
+ header.appendChild(batchBtn);
3690
+ }
3691
+ container.appendChild(header);
3625
3692
  if (backups.length === 0) {
3626
3693
  var p = document.createElement('p'); p.style.cssText = 'font-size:12px;color:var(--fg-dim)'; p.textContent = 'No backups yet.';
3627
3694
  container.appendChild(p); return;
@@ -3630,10 +3697,27 @@ function _appendBackupTable(container, title, backups, type, showRestore) {
3630
3697
  table.style.cssText = 'width:100%;font-size:12px;border-collapse:collapse;';
3631
3698
  var thead = document.createElement('tr');
3632
3699
  thead.style.cssText = 'color:var(--fg-dim);text-align:left;';
3700
+ // Select-all checkbox
3701
+ var thCb = document.createElement('th'); thCb.style.cssText = 'padding:4px 8px;width:28px;';
3702
+ var selectAll = document.createElement('input'); selectAll.type = 'checkbox';
3703
+ selectAll.checked = backups.length > 0 && backups.slice(0, 20).every(function(b) { return selected.has(b.name); });
3704
+ selectAll.onchange = function() {
3705
+ backups.slice(0, 20).forEach(function(b) { if (selectAll.checked) selected.add(b.name); else selected.delete(b.name); });
3706
+ loadBackupsData();
3707
+ };
3708
+ thCb.appendChild(selectAll);
3709
+ thead.appendChild(thCb);
3633
3710
  ['Name','Size','Date',''].forEach(function(t) { var th = document.createElement('th'); th.style.padding = '4px 8px'; th.textContent = t; thead.appendChild(th); });
3634
3711
  table.appendChild(thead);
3635
3712
  backups.slice(0, 20).forEach(function(b) {
3636
- var tr = document.createElement('tr'); tr.style.borderTop = '1px solid var(--border)';
3713
+ var isChecked = selected.has(b.name);
3714
+ var tr = document.createElement('tr');
3715
+ tr.style.cssText = 'border-top:1px solid var(--border);' + (isChecked ? 'background:rgba(88,166,255,0.06);' : '');
3716
+ // Checkbox
3717
+ var tdCb = document.createElement('td'); tdCb.style.cssText = 'padding:6px 8px;width:28px;';
3718
+ var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = isChecked;
3719
+ cb.onchange = (function(name) { return function() { if (cb.checked) selected.add(name); else selected.delete(name); loadBackupsData(); }; })(b.name);
3720
+ tdCb.appendChild(cb);
3637
3721
  var td1 = document.createElement('td'); td1.style.cssText = 'padding:6px 8px;color:var(--fg)'; td1.textContent = b.name + (b.hasImages ? ' + images' : '');
3638
3722
  var td2 = document.createElement('td'); td2.style.cssText = 'padding:6px 8px;color:var(--fg-dim)'; td2.textContent = _fmtSize(b.size);
3639
3723
  var td3 = document.createElement('td'); td3.style.cssText = 'padding:6px 8px;color:var(--fg-dim)'; td3.textContent = _fmtDate(b.createdAt);
@@ -3647,12 +3731,29 @@ function _appendBackupTable(container, title, backups, type, showRestore) {
3647
3731
  var db = document.createElement('button'); db.className = 'btn small'; db.style.color = 'var(--red)'; db.textContent = 'Delete';
3648
3732
  db.onclick = (function(n) { return function() { _deleteBackup(type, n); }; })(b.name);
3649
3733
  td4.appendChild(db);
3650
- tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4);
3734
+ tr.appendChild(tdCb); tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4);
3651
3735
  table.appendChild(tr);
3652
3736
  });
3653
3737
  container.appendChild(table);
3654
3738
  }
3655
3739
 
3740
+ function _batchDeleteBackups(type) {
3741
+ var selected = _backupSelected[type];
3742
+ if (selected.size === 0) return;
3743
+ if (!confirm('Delete ' + selected.size + ' ' + (type === 'ctm' ? 'CTM' : 'Brain') + ' backup(s)?')) return;
3744
+ var token = window._ctmState ? window._ctmState.token : '';
3745
+ var promises = [];
3746
+ selected.forEach(function(name) {
3747
+ var url = type === 'ctm' ? '/api/backups/' + encodeURIComponent(name) + '?token=' + token : '/api/wall-e/backups/' + encodeURIComponent(name) + '?token=' + token;
3748
+ promises.push(fetch(url, { method: 'DELETE' }));
3749
+ });
3750
+ Promise.all(promises).then(function() {
3751
+ selected.clear();
3752
+ if (typeof showToast === 'function') showToast('Deleted ' + promises.length + ' backup(s)');
3753
+ loadBackupsData();
3754
+ });
3755
+ }
3756
+
3656
3757
  function _fmtSize(bytes) {
3657
3758
  if (!bytes) return '0 B';
3658
3759
  if (bytes < 1024) return bytes + ' B';
@@ -3726,19 +3827,28 @@ function onOutput(msg) {
3726
3827
  // Only strip \e[3J (Erase Scrollback) — preserves scroll history.
3727
3828
  // Do NOT strip \e[2J or \e[?1049h/l — needed for Claude Code's TUI.
3728
3829
  const data = msg.data.replace(/\x1b\[3J/g, '');
3729
- // Auto-scroll: use write callback to scrollToBottom AFTER content is rendered.
3730
- // term.write() in xterm.js 5.x is async scrolling before render causes a race
3731
- // where the scroll listener falsely sets userScrolling=true (content added below viewport).
3732
- if (state.activeTab === msg.id && !s.userScrolling) {
3733
- s._programmaticScroll = true;
3830
+ // Auto-scroll using xterm.js buffer API: if viewport is at the bottom of the
3831
+ // scrollback, keep it there. If user has scrolled up, don't interrupt.
3832
+ // buffer.active.viewportY === buffer.active.baseY means "at bottom".
3833
+ const buf = s.term.buffer.active;
3834
+ const wasAtBottom = buf.viewportY >= buf.baseY;
3835
+ if (state.activeTab === msg.id && wasAtBottom) {
3734
3836
  s.term.write(data, () => {
3735
3837
  s.term.scrollToBottom();
3736
- // Keep _programmaticScroll true briefly after scroll settles
3737
- requestAnimationFrame(() => { s._programmaticScroll = false; });
3738
3838
  });
3739
3839
  } else {
3740
3840
  s.term.write(data);
3741
3841
  }
3842
+ // After output stops for 500ms, force xterm to recalculate viewport dimensions.
3843
+ // This fixes a desync between xterm's internal buffer and the DOM viewport that
3844
+ // can occur when Claude Code's TUI exits (Ink unmount, alt screen buffer exit).
3845
+ // Without this, the viewport may become unscrollable after output finishes.
3846
+ clearTimeout(s._outputIdleTimer);
3847
+ s._outputIdleTimer = setTimeout(() => {
3848
+ if (state.activeTab === msg.id) {
3849
+ s.fitAddon.fit();
3850
+ }
3851
+ }, 500);
3742
3852
  // Remove compact banner on new activity
3743
3853
  const banner = s.container.querySelector('.compact-banner');
3744
3854
  if (banner) { banner.remove(); requestAnimationFrame(() => s.fitAddon.fit()); }
@@ -3760,7 +3870,7 @@ function onScrollback(msg) {
3760
3870
  // \e[3J = Erase Scrollback, \e[2J = Erase Display, \e[?1049h/l = Alt Screen Buffer
3761
3871
  // These are fine during live output but destroy content when replayed on refresh.
3762
3872
  msg.data = msg.data.replace(/\x1b\[3J/g, '').replace(/\x1b\[2J/g, '').replace(/\x1b\[\?1049[hl]/g, '');
3763
- s.userScrolling = false; // Reset scroll lock on fresh attach
3873
+ // Fresh attach scroll to bottom after scrollback is loaded (via write callback below)
3764
3874
  // Fit terminal to get correct local dimensions
3765
3875
  s.fitAddon.fit();
3766
3876
  const localCols = s.term.cols;
@@ -7348,6 +7458,27 @@ function handleHashRoute() {
7348
7458
  return;
7349
7459
  }
7350
7460
 
7461
+ // #walle/task/:id — open WALL-E tasks and scroll to specific task
7462
+ if (firstPart.startsWith('walle/task/')) {
7463
+ const taskId = firstPart.slice('walle/task/'.length);
7464
+ navTo('walle', { skipHash: true });
7465
+ setTimeout(() => {
7466
+ if (typeof WE !== 'undefined') {
7467
+ WE.showView('tasks');
7468
+ // Wait for tasks to render, then scroll to the task and expand output
7469
+ setTimeout(() => {
7470
+ const el = document.getElementById('task-' + taskId);
7471
+ if (el) {
7472
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
7473
+ const output = el.querySelector('.we-task-output-panel');
7474
+ if (output && !output.classList.contains('expanded')) output.classList.add('expanded');
7475
+ }
7476
+ }, 500);
7477
+ }
7478
+ }, 200);
7479
+ return;
7480
+ }
7481
+
7351
7482
  // #walle — open WALL-E panel
7352
7483
  if (isNav && firstPart === 'walle') {
7353
7484
  navTo('walle', { skipHash: true });