create-walle 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +5 -1
- package/template/claude-task-manager/public/css/walle.css +317 -0
- package/template/claude-task-manager/public/index.html +404 -101
- package/template/claude-task-manager/public/js/walle.js +1256 -86
- package/template/claude-task-manager/server.js +189 -14
- package/template/docs/site/api/README.md +146 -0
- package/template/docs/site/skills/README.md +99 -5
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +54 -0
- package/template/wall-e/api-walle.js +452 -3
- package/template/wall-e/brain.js +45 -1
- package/template/wall-e/channels/telegram-channel.js +96 -0
- package/template/wall-e/chat.js +61 -2
- package/template/wall-e/coding-context.js +252 -0
- package/template/wall-e/coding-orchestrator.js +625 -0
- package/template/wall-e/coding-review.js +189 -0
- package/template/wall-e/core-tasks.js +12 -3
- package/template/wall-e/deploy.sh +4 -4
- package/template/wall-e/fly.toml +2 -2
- package/template/wall-e/package.json +4 -1
- package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
- package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
- package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
- package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
- package/template/wall-e/skills/_templates/manual-action.md +19 -0
- package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
- package/template/wall-e/skills/_templates/script-runner.md +21 -0
- package/template/wall-e/skills/claude-code-reader.js +16 -4
- package/template/wall-e/skills/skill-executor.js +23 -1
- package/template/wall-e/skills/skill-validator.js +73 -0
- package/template/wall-e/tests/brain.test.js +3 -3
- package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
- package/template/wall-e/tests/coding-context.test.js +212 -0
- package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
- package/template/wall-e/tests/coding-review.test.js +141 -0
- package/template/claude-task-manager/package-lock.json +0 -1607
- package/template/claude-task-manager/tests/test-ai-search.js +0 -61
- package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
- package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
- package/template/claude-task-manager/tests/test-features-v2.js +0 -127
- package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
- package/template/claude-task-manager/tests/test-insights.js +0 -124
- package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
- package/template/claude-task-manager/tests/test-permissions.js +0 -122
- package/template/claude-task-manager/tests/test-pin.js +0 -51
- package/template/claude-task-manager/tests/test-prompts.js +0 -164
- package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
- package/template/claude-task-manager/tests/test-review.js +0 -104
- package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
- package/template/claude-task-manager/tests/test-send-final.js +0 -30
- package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
- package/template/claude-task-manager/tests/test-send-integration.js +0 -107
- package/template/claude-task-manager/tests/test-send-visual.js +0 -34
- package/template/claude-task-manager/tests/test-session-create.js +0 -147
- package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
- package/template/claude-task-manager/tests/test-url-hash.js +0 -68
- package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
- package/template/claude-task-manager/tests/test-ux-review.js +0 -130
- package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
- package/template/claude-task-manager/tests/test-zoom.js +0 -92
- package/template/claude-task-manager/tests/test-zoom2.js +0 -67
- package/template/docs/openclaw-vs-walle-comparison.md +0 -103
- package/template/docs/ux-improvement-plan.md +0 -84
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
- package/template/wall-e/package-lock.json +0 -533
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
|
@@ -1315,16 +1315,25 @@
|
|
|
1315
1315
|
height: var(--tab-height);
|
|
1316
1316
|
background: var(--bg);
|
|
1317
1317
|
border-bottom: 1px solid var(--border);
|
|
1318
|
-
overflow
|
|
1318
|
+
overflow: hidden;
|
|
1319
1319
|
flex-shrink: 0;
|
|
1320
1320
|
align-items: flex-end;
|
|
1321
1321
|
}
|
|
1322
|
-
#tabbar
|
|
1322
|
+
#tabbar-scroll {
|
|
1323
|
+
display: flex;
|
|
1324
|
+
flex: 1;
|
|
1325
|
+
min-width: 0;
|
|
1326
|
+
overflow-x: auto;
|
|
1327
|
+
align-items: flex-end;
|
|
1328
|
+
height: 100%;
|
|
1329
|
+
scroll-behavior: smooth;
|
|
1330
|
+
}
|
|
1331
|
+
#tabbar-scroll::-webkit-scrollbar { height: 0; }
|
|
1323
1332
|
.tab {
|
|
1324
1333
|
display: flex;
|
|
1325
1334
|
align-items: center;
|
|
1326
1335
|
gap: 6px;
|
|
1327
|
-
padding: 0
|
|
1336
|
+
padding: 0 10px;
|
|
1328
1337
|
height: 32px;
|
|
1329
1338
|
font-size: 12px;
|
|
1330
1339
|
color: var(--fg-dim);
|
|
@@ -1333,15 +1342,23 @@
|
|
|
1333
1342
|
border-bottom: none;
|
|
1334
1343
|
cursor: pointer;
|
|
1335
1344
|
white-space: nowrap;
|
|
1336
|
-
|
|
1345
|
+
min-width: 60px;
|
|
1346
|
+
max-width: 240px;
|
|
1347
|
+
flex-shrink: 1;
|
|
1337
1348
|
border-radius: 6px 6px 0 0;
|
|
1338
1349
|
transition: all 0.1s;
|
|
1339
1350
|
}
|
|
1351
|
+
.tab .tab-label {
|
|
1352
|
+
overflow: hidden;
|
|
1353
|
+
text-overflow: ellipsis;
|
|
1354
|
+
min-width: 0;
|
|
1355
|
+
}
|
|
1340
1356
|
.tab:hover { color: var(--fg); background: var(--bg-light); }
|
|
1341
1357
|
.tab.active {
|
|
1342
1358
|
color: var(--fg);
|
|
1343
1359
|
background: var(--bg-light);
|
|
1344
1360
|
border-color: var(--border);
|
|
1361
|
+
flex-shrink: 0;
|
|
1345
1362
|
}
|
|
1346
1363
|
.tab.waiting {
|
|
1347
1364
|
animation: tab-pulse 2s ease-in-out infinite;
|
|
@@ -1373,24 +1390,87 @@
|
|
|
1373
1390
|
flex-shrink: 0;
|
|
1374
1391
|
}
|
|
1375
1392
|
.tab .close-tab {
|
|
1376
|
-
font-size:
|
|
1393
|
+
font-size: 13px;
|
|
1394
|
+
line-height: 1;
|
|
1377
1395
|
opacity: 0;
|
|
1378
1396
|
cursor: pointer;
|
|
1379
|
-
padding:
|
|
1397
|
+
padding: 3px 5px;
|
|
1398
|
+
border-radius: 4px;
|
|
1399
|
+
flex-shrink: 0;
|
|
1400
|
+
transition: background 0.1s, opacity 0.1s;
|
|
1380
1401
|
}
|
|
1381
|
-
.tab:hover .close-tab { opacity: 0.
|
|
1382
|
-
.tab .close-tab:hover { opacity: 1; }
|
|
1402
|
+
.tab:hover .close-tab { opacity: 0.5; }
|
|
1403
|
+
.tab .close-tab:hover { opacity: 1; background: rgba(255,255,255,0.1); }
|
|
1383
1404
|
.tab-add {
|
|
1384
|
-
|
|
1385
|
-
height:
|
|
1405
|
+
width: 28px;
|
|
1406
|
+
height: 24px;
|
|
1407
|
+
margin-left: 10px;
|
|
1386
1408
|
display: flex;
|
|
1387
1409
|
align-items: center;
|
|
1410
|
+
justify-content: center;
|
|
1388
1411
|
color: var(--fg-dim);
|
|
1389
1412
|
cursor: pointer;
|
|
1390
1413
|
font-size: 16px;
|
|
1391
1414
|
flex-shrink: 0;
|
|
1415
|
+
border-radius: 6px;
|
|
1416
|
+
border: 1px solid var(--border);
|
|
1417
|
+
transition: all 0.15s;
|
|
1418
|
+
}
|
|
1419
|
+
.tab-add:hover { color: var(--fg); background: rgba(255,255,255,0.08); }
|
|
1420
|
+
.tab-overflow-btn {
|
|
1421
|
+
padding: 0 8px;
|
|
1422
|
+
height: 32px;
|
|
1423
|
+
display: none;
|
|
1424
|
+
align-items: center;
|
|
1425
|
+
color: var(--fg-dim);
|
|
1426
|
+
cursor: pointer;
|
|
1427
|
+
font-size: 11px;
|
|
1428
|
+
flex-shrink: 0;
|
|
1429
|
+
border-left: 1px solid var(--border);
|
|
1430
|
+
position: relative;
|
|
1431
|
+
}
|
|
1432
|
+
.tab-overflow-btn:hover { color: var(--fg); background: var(--bg-light); }
|
|
1433
|
+
.tab-overflow-btn.visible { display: flex; }
|
|
1434
|
+
.tab-overflow-menu {
|
|
1435
|
+
position: absolute;
|
|
1436
|
+
top: 100%;
|
|
1437
|
+
right: 0;
|
|
1438
|
+
z-index: 100;
|
|
1439
|
+
background: var(--bg-light);
|
|
1440
|
+
border: 1px solid var(--border);
|
|
1441
|
+
border-radius: 6px;
|
|
1442
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
|
1443
|
+
min-width: 220px;
|
|
1444
|
+
max-width: 340px;
|
|
1445
|
+
max-height: 400px;
|
|
1446
|
+
overflow-y: auto;
|
|
1447
|
+
padding: 4px 0;
|
|
1448
|
+
}
|
|
1449
|
+
.tab-overflow-item {
|
|
1450
|
+
display: flex;
|
|
1451
|
+
align-items: center;
|
|
1452
|
+
gap: 8px;
|
|
1453
|
+
padding: 6px 12px;
|
|
1454
|
+
font-size: 12px;
|
|
1455
|
+
color: var(--fg-dim);
|
|
1456
|
+
cursor: pointer;
|
|
1457
|
+
white-space: nowrap;
|
|
1458
|
+
overflow: hidden;
|
|
1459
|
+
text-overflow: ellipsis;
|
|
1460
|
+
}
|
|
1461
|
+
.tab-overflow-item:hover { background: rgba(255,255,255,0.06); color: var(--fg); }
|
|
1462
|
+
.tab-overflow-item.active { color: var(--accent); font-weight: 600; }
|
|
1463
|
+
.tab-overflow-item .overflow-dot {
|
|
1464
|
+
width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
|
|
1465
|
+
}
|
|
1466
|
+
.tab-overflow-item .overflow-label {
|
|
1467
|
+
overflow: hidden; text-overflow: ellipsis; flex: 1;
|
|
1392
1468
|
}
|
|
1393
|
-
.tab-
|
|
1469
|
+
.tab-overflow-item .overflow-close {
|
|
1470
|
+
opacity: 0; font-size: 14px; padding: 0 2px; flex-shrink: 0;
|
|
1471
|
+
}
|
|
1472
|
+
.tab-overflow-item:hover .overflow-close { opacity: 0.6; }
|
|
1473
|
+
.tab-overflow-item .overflow-close:hover { opacity: 1; }
|
|
1394
1474
|
|
|
1395
1475
|
/* Terminal container */
|
|
1396
1476
|
#terminal-area {
|
|
@@ -1405,13 +1485,14 @@
|
|
|
1405
1485
|
flex: 1;
|
|
1406
1486
|
display: none;
|
|
1407
1487
|
overflow: hidden;
|
|
1488
|
+
min-height: 0;
|
|
1408
1489
|
}
|
|
1409
1490
|
.term-container.active { display: flex; flex-direction: column; }
|
|
1410
|
-
.term-container .xterm { flex: 1; height: 0; }
|
|
1491
|
+
.term-container .xterm { flex: 1; height: 0; min-height: 0; overflow: hidden; }
|
|
1411
1492
|
.session-toolbar {
|
|
1412
1493
|
display: flex; align-items: center; gap: 6px; padding: 3px 8px;
|
|
1413
1494
|
background: rgba(255,255,255,0.03); border-bottom: 1px solid var(--border);
|
|
1414
|
-
flex-shrink: 0;
|
|
1495
|
+
flex-shrink: 0; position: relative; z-index: 1;
|
|
1415
1496
|
}
|
|
1416
1497
|
.session-toolbar-btn {
|
|
1417
1498
|
background: none; border: 1px solid transparent; color: var(--fg-dim);
|
|
@@ -2375,7 +2456,10 @@
|
|
|
2375
2456
|
<div id="sidebar-resize"></div>
|
|
2376
2457
|
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
|
2377
2458
|
<div id="tabbar">
|
|
2378
|
-
<
|
|
2459
|
+
<div id="tabbar-scroll">
|
|
2460
|
+
<span class="tab-add" onclick="showNewSessionModal()" title="New Tab">+</span>
|
|
2461
|
+
</div>
|
|
2462
|
+
<span class="tab-overflow-btn" id="tab-overflow-btn" onclick="toggleTabOverflow(event)" title="All tabs">▾ <span id="tab-overflow-count"></span></span>
|
|
2379
2463
|
</div>
|
|
2380
2464
|
<!-- Queue Status Bar -->
|
|
2381
2465
|
<div id="queue-bar" style="display:none;">
|
|
@@ -3438,7 +3522,11 @@ function createTerminal(id) {
|
|
|
3438
3522
|
term.onData((data) => {
|
|
3439
3523
|
send({ type: 'input', id, data });
|
|
3440
3524
|
clearWaitingState(id);
|
|
3441
|
-
// User typed something — scroll to bottom and re-enable follow mode
|
|
3525
|
+
// User typed something — scroll to bottom and re-enable follow mode.
|
|
3526
|
+
// Skip mouse escape sequences (\x1b[M..., \x1b[<...) — these are wheel/click
|
|
3527
|
+
// events forwarded by xterm when mouse tracking is enabled, not real user input.
|
|
3528
|
+
const isMouse = data.length >= 3 && data[0] === '\x1b' && data[1] === '[' && (data[2] === 'M' || data[2] === '<');
|
|
3529
|
+
if (isMouse) return;
|
|
3442
3530
|
const s = state.sessions.get(id);
|
|
3443
3531
|
if (s) { s.writer.followMode = true; s.term.scrollToBottom(); }
|
|
3444
3532
|
});
|
|
@@ -3468,6 +3556,7 @@ function createTerminal(id) {
|
|
|
3468
3556
|
// Momentum scroll clamping — macOS trackpad momentum can cause "rocket scroll"
|
|
3469
3557
|
// where the viewport flies past the content. Detect momentum events (rapid,
|
|
3470
3558
|
// decaying deltas) and clamp them after a threshold.
|
|
3559
|
+
// Also tracks _lastWheelAt for scroll-during-output detection.
|
|
3471
3560
|
const viewportEl = container.querySelector('.xterm-viewport');
|
|
3472
3561
|
if (viewportEl) {
|
|
3473
3562
|
let lastWheelTime = 0;
|
|
@@ -3477,6 +3566,11 @@ function createTerminal(id) {
|
|
|
3477
3566
|
const now = Date.now();
|
|
3478
3567
|
const elapsed = now - lastWheelTime;
|
|
3479
3568
|
lastWheelTime = now;
|
|
3569
|
+
writer._lastWheelAt = now;
|
|
3570
|
+
// Immediately disable auto-scroll when user scrolls up — don't rely on
|
|
3571
|
+
// the onScroll handler which can be suppressed or arrive too late after
|
|
3572
|
+
// xterm's internal auto-scroll resets the viewport to bottom.
|
|
3573
|
+
if (e.deltaY < 0) writer.followMode = false;
|
|
3480
3574
|
if (elapsed < 80 && Math.abs(e.deltaY) < Math.abs(lastDeltaY) * 0.9) {
|
|
3481
3575
|
momentumCount++;
|
|
3482
3576
|
} else if (elapsed > 120) {
|
|
@@ -3506,12 +3600,16 @@ function createTerminal(id) {
|
|
|
3506
3600
|
term.onScroll(() => {
|
|
3507
3601
|
const buf = term.buffer.active;
|
|
3508
3602
|
const atBottom = buf.viewportY >= buf.baseY;
|
|
3509
|
-
// During suppression (after fit/write), ignore
|
|
3510
|
-
//
|
|
3511
|
-
//
|
|
3512
|
-
//
|
|
3513
|
-
//
|
|
3514
|
-
|
|
3603
|
+
// During suppression (after fit/write), ignore programmatic scroll events
|
|
3604
|
+
// to prevent write()'s internal auto-scroll from flipping followMode.
|
|
3605
|
+
// BUT: if the user actively scrolled (wheel event within last 150ms),
|
|
3606
|
+
// let their scroll intent through — the wheel check in the write callback
|
|
3607
|
+
// will prevent scrollToBottom/scrollToLine from overriding them.
|
|
3608
|
+
const now = Date.now();
|
|
3609
|
+
if (now < writer._suppressScroll) {
|
|
3610
|
+
const recentWheel = writer._lastWheelAt && (now - writer._lastWheelAt < 150);
|
|
3611
|
+
if (!recentWheel) return;
|
|
3612
|
+
}
|
|
3515
3613
|
writer.followMode = atBottom;
|
|
3516
3614
|
});
|
|
3517
3615
|
|
|
@@ -3558,13 +3656,27 @@ async function _scanPromptLinesFromAPI(id, projectEntry) {
|
|
|
3558
3656
|
_scanPromptLinesFromTerminal(id);
|
|
3559
3657
|
return;
|
|
3560
3658
|
}
|
|
3561
|
-
// Match the review page: count all role:'user' messages
|
|
3659
|
+
// Match the review page: count all role:'user' messages, filtering out
|
|
3660
|
+
// injected system/skill context that isn't a real user prompt
|
|
3562
3661
|
const userMsgs = messages.filter(m => m.role === 'user');
|
|
3563
3662
|
const previews = [];
|
|
3564
3663
|
for (const msg of userMsgs) {
|
|
3565
|
-
const
|
|
3664
|
+
const text = msg.text;
|
|
3665
|
+
// Skip injected context: skill metadata, built-in commands, continuation summaries
|
|
3666
|
+
const firstLine = text.split('\n')[0].trim();
|
|
3566
3667
|
if (!firstLine || firstLine.length < 3) continue;
|
|
3567
|
-
|
|
3668
|
+
if (/^Base directory for this skill:/i.test(firstLine)) continue;
|
|
3669
|
+
if (/^<command-name>\/(model|exit|compact|clear|help|fast|config|memory|doctor|bug|init|review|cost|vim|resume|login|logout|permissions|status|mcp|diff|terminal-setup)<\/command-name>/.test(firstLine)) continue;
|
|
3670
|
+
if (/^This session is being continued from a previous conversation/i.test(firstLine)) continue;
|
|
3671
|
+
if (/^ARGUMENTS:\s/.test(firstLine)) continue;
|
|
3672
|
+
// Clean up command-message wrappers into readable format
|
|
3673
|
+
let cleaned = firstLine;
|
|
3674
|
+
if (/^<command-message>/.test(cleaned)) {
|
|
3675
|
+
cleaned = cleaned.replace(/<command-message>(.*?)<\/command-message>.*/, '/$1');
|
|
3676
|
+
}
|
|
3677
|
+
cleaned = cleaned.replace(/<[^>]+>/g, '').trim();
|
|
3678
|
+
if (!cleaned || cleaned.length < 2) continue;
|
|
3679
|
+
previews.push(cleaned.slice(0, 80));
|
|
3568
3680
|
}
|
|
3569
3681
|
_promptScanCache[id] = { ts: Date.now(), previews };
|
|
3570
3682
|
if (s) s._promptLinesResolved = false;
|
|
@@ -3745,6 +3857,11 @@ function activateTab(id) {
|
|
|
3745
3857
|
const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'backups'];
|
|
3746
3858
|
const isPanel = specialPanels.includes(id);
|
|
3747
3859
|
|
|
3860
|
+
// Clean up WALL-E background pollers when leaving WALL-E tab
|
|
3861
|
+
if (state.activeTab === 'walle' && id !== 'walle' && typeof WE !== 'undefined' && WE.pausePollers) {
|
|
3862
|
+
WE.pausePollers();
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3748
3865
|
// Hide all
|
|
3749
3866
|
for (const [sid, s] of state.sessions) {
|
|
3750
3867
|
s.container.classList.remove('active');
|
|
@@ -4246,7 +4363,9 @@ function onOutput(msg) {
|
|
|
4246
4363
|
s._waitingForInput = false;
|
|
4247
4364
|
// Only strip \e[3J (Erase Scrollback) — preserves scroll history.
|
|
4248
4365
|
// Do NOT strip \e[2J or \e[?1049h/l — needed for Claude Code's TUI.
|
|
4249
|
-
|
|
4366
|
+
// Skip regex if the escape sequence isn't present (common fast path).
|
|
4367
|
+
const raw = msg.data;
|
|
4368
|
+
const data = raw.indexOf('\x1b[3J') >= 0 ? raw.replace(/\x1b\[3J/g, '') : raw;
|
|
4250
4369
|
|
|
4251
4370
|
// followMode is maintained by the term.onScroll listener — no manual check needed here.
|
|
4252
4371
|
|
|
@@ -4262,10 +4381,20 @@ function onOutput(msg) {
|
|
|
4262
4381
|
const follow = s.writer.followMode;
|
|
4263
4382
|
s.writer.queue = '';
|
|
4264
4383
|
s.writer.scheduled = false;
|
|
4384
|
+
// Snapshot wheel timestamp before write — xterm processes writes async
|
|
4385
|
+
// (its own RAF), so the callback fires in a later frame. Between our
|
|
4386
|
+
// write() call and the callback, the user may scroll via wheel. If so,
|
|
4387
|
+
// we must NOT override their scroll with scrollToBottom/scrollToLine.
|
|
4388
|
+
const wheelBefore = s.writer._lastWheelAt || 0;
|
|
4265
4389
|
// Re-check active tab at render time (may have changed since queued)
|
|
4266
4390
|
if (state.activeTab === sid && follow) {
|
|
4391
|
+
s.writer._suppressScroll = Date.now() + 2000;
|
|
4267
4392
|
s.term.write(batch, () => {
|
|
4268
|
-
s.
|
|
4393
|
+
s.writer._suppressScroll = Date.now() + 100;
|
|
4394
|
+
// Only auto-scroll if user didn't wheel-scroll since write was queued
|
|
4395
|
+
if ((s.writer._lastWheelAt || 0) === wheelBefore) {
|
|
4396
|
+
s.term.scrollToBottom();
|
|
4397
|
+
}
|
|
4269
4398
|
});
|
|
4270
4399
|
} else {
|
|
4271
4400
|
const savedLine = s.term.buffer.active.viewportY;
|
|
@@ -4275,57 +4404,72 @@ function onOutput(msg) {
|
|
|
4275
4404
|
s.writer._suppressScroll = Date.now() + 2000;
|
|
4276
4405
|
s.term.write(batch, () => {
|
|
4277
4406
|
s.writer._suppressScroll = Date.now() + 100;
|
|
4278
|
-
|
|
4407
|
+
// Only restore position if user didn't wheel-scroll since write
|
|
4408
|
+
if ((s.writer._lastWheelAt || 0) === wheelBefore) {
|
|
4409
|
+
s.term.scrollToLine(savedLine);
|
|
4410
|
+
}
|
|
4279
4411
|
});
|
|
4280
4412
|
}
|
|
4281
|
-
});
|
|
4282
|
-
}
|
|
4283
4413
|
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4414
|
+
// --- Deferred housekeeping (once per frame, not per WS message) ---
|
|
4415
|
+
|
|
4416
|
+
// Idle timer: reset after output stops for 300ms (dimension drift auto-heal)
|
|
4417
|
+
clearTimeout(s._outputIdleTimer);
|
|
4418
|
+
s._outputIdleTimer = setTimeout(() => {
|
|
4419
|
+
if (state.activeTab === sid) {
|
|
4420
|
+
try {
|
|
4421
|
+
const dims = s.fitAddon.proposeDimensions();
|
|
4422
|
+
if (dims && (dims.cols !== s.term.cols || dims.rows !== s.term.rows)) {
|
|
4423
|
+
const buf = s.term.buffer.active;
|
|
4424
|
+
const wasAtBottom = buf.viewportY >= buf.baseY;
|
|
4425
|
+
const oldCols = s.term.cols;
|
|
4426
|
+
const savedLine2 = buf.viewportY;
|
|
4427
|
+
const savedAnchor = wasAtBottom ? null : _getScrollAnchor(s.term);
|
|
4428
|
+
s.writer._suppressScroll = Date.now() + 200;
|
|
4429
|
+
s.fitAddon.fit();
|
|
4430
|
+
send({ type: 'resize', id: sid, cols: s.term.cols, rows: s.term.rows });
|
|
4431
|
+
if (wasAtBottom) {
|
|
4432
|
+
s.term.scrollToBottom();
|
|
4433
|
+
} else if (s.term.cols !== oldCols) {
|
|
4434
|
+
_restoreScrollAnchor(s.term, savedAnchor);
|
|
4435
|
+
s._promptLinesResolved = false;
|
|
4436
|
+
} else {
|
|
4437
|
+
s.term.scrollToLine(savedLine2);
|
|
4438
|
+
}
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
} catch (_) { /* proposeDimensions may fail if container hidden */ }
|
|
4442
|
+
if (s.writer.followMode) {
|
|
4303
4443
|
s.term.scrollToBottom();
|
|
4304
|
-
} else if (s.term.cols !== oldCols) {
|
|
4305
|
-
_restoreScrollAnchor(s.term, savedAnchor);
|
|
4306
|
-
s._promptLinesResolved = false;
|
|
4307
|
-
} else {
|
|
4308
|
-
s.term.scrollToLine(savedLine);
|
|
4309
4444
|
}
|
|
4310
|
-
return; // fit handles scroll position
|
|
4311
4445
|
}
|
|
4312
|
-
}
|
|
4313
|
-
|
|
4314
|
-
|
|
4446
|
+
}, 300);
|
|
4447
|
+
|
|
4448
|
+
// Remove compact banner on new activity (once per frame, not per chunk)
|
|
4449
|
+
if (!s._bannerCleared) {
|
|
4450
|
+
const banner = s.container.querySelector('.compact-banner');
|
|
4451
|
+
if (banner) {
|
|
4452
|
+
banner.remove();
|
|
4453
|
+
s._bannerCleared = true;
|
|
4454
|
+
const ln = s.term.buffer.active.viewportY;
|
|
4455
|
+
const ab = s.writer.followMode;
|
|
4456
|
+
s.writer._suppressScroll = Date.now() + 200;
|
|
4457
|
+
s.fitAddon.fit();
|
|
4458
|
+
if (!ab) s.term.scrollToLine(ln);
|
|
4459
|
+
} else {
|
|
4460
|
+
s._bannerCleared = true; // no banner exists, skip future checks
|
|
4461
|
+
}
|
|
4315
4462
|
}
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
if (!isUserInput) {
|
|
4327
|
-
s.term.focus();
|
|
4328
|
-
}
|
|
4463
|
+
|
|
4464
|
+
// Keep focus on the active session's terminal
|
|
4465
|
+
if (state.activeTab === sid && document.activeElement !== s.term.textarea) {
|
|
4466
|
+
const ae = document.activeElement;
|
|
4467
|
+
const isUserInput = ae && (ae.tagName === 'TEXTAREA' || ae.tagName === 'INPUT' || ae.tagName === 'SELECT' || ae.isContentEditable);
|
|
4468
|
+
if (!isUserInput) {
|
|
4469
|
+
s.term.focus();
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
});
|
|
4329
4473
|
}
|
|
4330
4474
|
}
|
|
4331
4475
|
|
|
@@ -4412,16 +4556,17 @@ async function onSessionsList(msg) {
|
|
|
4412
4556
|
const saved = state._savedTabOrder;
|
|
4413
4557
|
delete state._savedTabOrder;
|
|
4414
4558
|
// Reorder: put saved IDs first (if they still exist), then any new ones
|
|
4415
|
-
|
|
4416
|
-
const
|
|
4559
|
+
// Include 'review' as a restorable tab position
|
|
4560
|
+
const restorableIds = state.tabOrder.filter(id => state.sessions.has(id) || id === 'review');
|
|
4561
|
+
const nonRestorableIds = state.tabOrder.filter(id => !state.sessions.has(id) && id !== 'review');
|
|
4417
4562
|
const ordered = [];
|
|
4418
4563
|
for (const id of saved) {
|
|
4419
|
-
if (
|
|
4564
|
+
if (restorableIds.includes(id)) ordered.push(id);
|
|
4420
4565
|
}
|
|
4421
|
-
for (const id of
|
|
4566
|
+
for (const id of restorableIds) {
|
|
4422
4567
|
if (!ordered.includes(id)) ordered.push(id);
|
|
4423
4568
|
}
|
|
4424
|
-
state.tabOrder = [...
|
|
4569
|
+
state.tabOrder = [...nonRestorableIds, ...ordered];
|
|
4425
4570
|
}
|
|
4426
4571
|
|
|
4427
4572
|
renderSessionList();
|
|
@@ -4554,6 +4699,19 @@ function recentItemDblClick(id, event) {
|
|
|
4554
4699
|
}, 10);
|
|
4555
4700
|
}
|
|
4556
4701
|
|
|
4702
|
+
// Shared helper: attach event listeners for inline rename inputs
|
|
4703
|
+
function setupRenameInput(input, currentText, finish) {
|
|
4704
|
+
// Stop events from bubbling to parent (e.g., activateTab → steal focus)
|
|
4705
|
+
for (const evt of ['click', 'mousedown', 'dblclick']) {
|
|
4706
|
+
input.addEventListener(evt, (e) => e.stopPropagation());
|
|
4707
|
+
}
|
|
4708
|
+
input.addEventListener('blur', finish);
|
|
4709
|
+
input.addEventListener('keydown', (e) => {
|
|
4710
|
+
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
|
|
4711
|
+
if (e.key === 'Escape') { input.value = currentText; input.blur(); }
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4557
4715
|
function startRenameSession(sessionId, labelEl) {
|
|
4558
4716
|
// Guard: already editing
|
|
4559
4717
|
if (labelEl.querySelector('input')) return;
|
|
@@ -4590,6 +4748,8 @@ function startRenameSession(sessionId, labelEl) {
|
|
|
4590
4748
|
const reviewTitleEl = document.getElementById('review-title');
|
|
4591
4749
|
if (reviewTitleEl) reviewTitleEl.textContent = newName;
|
|
4592
4750
|
}
|
|
4751
|
+
// Send rename via WebSocket too so the server updates in-memory session label
|
|
4752
|
+
send({ type: 'rename', id: sessionId, label: newName });
|
|
4593
4753
|
}
|
|
4594
4754
|
// Remove input before re-rendering so the input-guard doesn't block
|
|
4595
4755
|
input.remove();
|
|
@@ -4598,11 +4758,7 @@ function startRenameSession(sessionId, labelEl) {
|
|
|
4598
4758
|
renderTabs();
|
|
4599
4759
|
}
|
|
4600
4760
|
|
|
4601
|
-
input
|
|
4602
|
-
input.addEventListener('keydown', (e) => {
|
|
4603
|
-
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
|
|
4604
|
-
if (e.key === 'Escape') { input.value = currentText; input.blur(); }
|
|
4605
|
-
});
|
|
4761
|
+
setupRenameInput(input, currentText, finish);
|
|
4606
4762
|
}
|
|
4607
4763
|
|
|
4608
4764
|
function startRenameReviewTitle(titleEl) {
|
|
@@ -4644,11 +4800,7 @@ function startRenameReviewTitle(titleEl) {
|
|
|
4644
4800
|
}
|
|
4645
4801
|
}
|
|
4646
4802
|
|
|
4647
|
-
input
|
|
4648
|
-
input.addEventListener('keydown', (e) => {
|
|
4649
|
-
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
|
|
4650
|
-
if (e.key === 'Escape') { input.value = currentText; input.blur(); }
|
|
4651
|
-
});
|
|
4803
|
+
setupRenameInput(input, currentText, finish);
|
|
4652
4804
|
}
|
|
4653
4805
|
|
|
4654
4806
|
function startRenameRecentSession(sessionId, spanEl) {
|
|
@@ -4693,11 +4845,7 @@ function startRenameRecentSession(sessionId, spanEl) {
|
|
|
4693
4845
|
renderTabs();
|
|
4694
4846
|
}
|
|
4695
4847
|
|
|
4696
|
-
input
|
|
4697
|
-
input.addEventListener('keydown', (e) => {
|
|
4698
|
-
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
|
|
4699
|
-
if (e.key === 'Escape') { input.value = currentText; input.blur(); }
|
|
4700
|
-
});
|
|
4848
|
+
setupRenameInput(input, currentText, finish);
|
|
4701
4849
|
}
|
|
4702
4850
|
|
|
4703
4851
|
async function loadSessionPrompts(sessionId) {
|
|
@@ -4728,6 +4876,7 @@ function showCompactBannerIfStale(id, s) {
|
|
|
4728
4876
|
// Remove any existing banner first
|
|
4729
4877
|
const existing = s.container.querySelector('.compact-banner');
|
|
4730
4878
|
if (existing) existing.remove();
|
|
4879
|
+
s._bannerCleared = false; // reset so onOutput can detect new banners
|
|
4731
4880
|
|
|
4732
4881
|
if (_compactDismissed.has(id)) return;
|
|
4733
4882
|
const lastAct = s.meta?.lastActivity || s.meta?.createdAt || 0;
|
|
@@ -4778,13 +4927,13 @@ function dismissCompactBanner(id) {
|
|
|
4778
4927
|
}
|
|
4779
4928
|
|
|
4780
4929
|
function renderTabs() {
|
|
4781
|
-
const
|
|
4930
|
+
const scrollContainer = document.getElementById('tabbar-scroll');
|
|
4782
4931
|
// Skip re-render if user is actively renaming a tab
|
|
4783
|
-
if (
|
|
4784
|
-
const addBtn =
|
|
4932
|
+
if (scrollContainer.querySelector('input')) return;
|
|
4933
|
+
const addBtn = scrollContainer.querySelector('.tab-add');
|
|
4785
4934
|
|
|
4786
4935
|
// Remove old tabs
|
|
4787
|
-
|
|
4936
|
+
scrollContainer.querySelectorAll('.tab').forEach(t => t.remove());
|
|
4788
4937
|
|
|
4789
4938
|
for (const id of state.tabOrder) {
|
|
4790
4939
|
if (id === 'rules') {
|
|
@@ -4792,16 +4941,35 @@ function renderTabs() {
|
|
|
4792
4941
|
tab.className = `tab panel-tab ${state.activeTab === 'rules' ? 'active' : ''}`;
|
|
4793
4942
|
tab.innerHTML = `<span>📝 Rules</span><span class="close-tab" onclick="event.stopPropagation();closeSpecialTab('rules')">×</span>`;
|
|
4794
4943
|
tab.onclick = () => activateTab('rules');
|
|
4795
|
-
|
|
4944
|
+
scrollContainer.insertBefore(tab, addBtn);
|
|
4796
4945
|
continue;
|
|
4797
4946
|
}
|
|
4798
4947
|
if (id === 'review') {
|
|
4799
4948
|
const tab = document.createElement('div');
|
|
4800
4949
|
tab.className = `tab panel-tab ${state.activeTab === 'review' ? 'active' : ''}`;
|
|
4950
|
+
tab.dataset.sessionId = 'review';
|
|
4951
|
+
tab.draggable = true;
|
|
4801
4952
|
const title = document.getElementById('review-title').textContent;
|
|
4802
4953
|
tab.innerHTML = `<span>📋 ${escHtml(title)}</span><span class="close-tab" onclick="event.stopPropagation();closeSpecialTab('review')">×</span>`;
|
|
4803
4954
|
tab.onclick = () => activateTab('review');
|
|
4804
|
-
|
|
4955
|
+
tab.ondragstart = function(e) { _tabDragId = 'review'; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', 'review'); tab.style.opacity = '0.4'; };
|
|
4956
|
+
tab.ondragend = function() { tab.style.opacity = ''; };
|
|
4957
|
+
tab.ondragover = function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; tab.classList.add('tab-drag-over'); };
|
|
4958
|
+
tab.ondragleave = function() { tab.classList.remove('tab-drag-over'); };
|
|
4959
|
+
tab.ondrop = function(e) {
|
|
4960
|
+
e.preventDefault();
|
|
4961
|
+
tab.classList.remove('tab-drag-over');
|
|
4962
|
+
if (!_tabDragId || _tabDragId === 'review') return;
|
|
4963
|
+
const from = state.tabOrder.indexOf(_tabDragId);
|
|
4964
|
+
const to = state.tabOrder.indexOf('review');
|
|
4965
|
+
if (from === -1 || to === -1) return;
|
|
4966
|
+
state.tabOrder.splice(from, 1);
|
|
4967
|
+
state.tabOrder.splice(to, 0, _tabDragId);
|
|
4968
|
+
_tabDragId = null;
|
|
4969
|
+
saveTabOrder();
|
|
4970
|
+
renderTabs();
|
|
4971
|
+
};
|
|
4972
|
+
scrollContainer.insertBefore(tab, addBtn);
|
|
4805
4973
|
continue;
|
|
4806
4974
|
}
|
|
4807
4975
|
if (id === 'insights') {
|
|
@@ -4809,7 +4977,7 @@ function renderTabs() {
|
|
|
4809
4977
|
tab.className = `tab panel-tab ${state.activeTab === 'insights' ? 'active' : ''}`;
|
|
4810
4978
|
tab.innerHTML = `<span>📊 Insights</span><span class="close-tab" onclick="event.stopPropagation();closeSpecialTab('insights')">×</span>`;
|
|
4811
4979
|
tab.onclick = () => activateTab('insights');
|
|
4812
|
-
|
|
4980
|
+
scrollContainer.insertBefore(tab, addBtn);
|
|
4813
4981
|
continue;
|
|
4814
4982
|
}
|
|
4815
4983
|
if (id === 'permissions') {
|
|
@@ -4817,7 +4985,7 @@ function renderTabs() {
|
|
|
4817
4985
|
tab.className = `tab panel-tab ${state.activeTab === 'permissions' ? 'active' : ''}`;
|
|
4818
4986
|
tab.innerHTML = `<span>🔒 Permissions</span><span class="close-tab" onclick="event.stopPropagation();closeSpecialTab('permissions')">×</span>`;
|
|
4819
4987
|
tab.onclick = () => activateTab('permissions');
|
|
4820
|
-
|
|
4988
|
+
scrollContainer.insertBefore(tab, addBtn);
|
|
4821
4989
|
continue;
|
|
4822
4990
|
}
|
|
4823
4991
|
if (id === 'codereview') {
|
|
@@ -4865,15 +5033,109 @@ function renderTabs() {
|
|
|
4865
5033
|
saveTabOrder();
|
|
4866
5034
|
renderTabs();
|
|
4867
5035
|
};
|
|
4868
|
-
|
|
5036
|
+
scrollContainer.insertBefore(tab, addBtn);
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// Auto-scroll active tab into view + update overflow indicator
|
|
5040
|
+
requestAnimationFrame(() => {
|
|
5041
|
+
scrollActiveTabIntoView();
|
|
5042
|
+
updateTabOverflowBtn();
|
|
5043
|
+
});
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
// Scroll the active tab into view within the tab bar
|
|
5047
|
+
function scrollActiveTabIntoView() {
|
|
5048
|
+
const scrollContainer = document.getElementById('tabbar-scroll');
|
|
5049
|
+
const activeTab = scrollContainer.querySelector('.tab.active');
|
|
5050
|
+
if (!activeTab) return;
|
|
5051
|
+
const cRect = scrollContainer.getBoundingClientRect();
|
|
5052
|
+
const tRect = activeTab.getBoundingClientRect();
|
|
5053
|
+
if (tRect.left < cRect.left) {
|
|
5054
|
+
scrollContainer.scrollLeft += tRect.left - cRect.left - 8;
|
|
5055
|
+
} else if (tRect.right > cRect.right) {
|
|
5056
|
+
scrollContainer.scrollLeft += tRect.right - cRect.right + 8;
|
|
4869
5057
|
}
|
|
4870
5058
|
}
|
|
4871
5059
|
|
|
5060
|
+
// Show/hide the overflow dropdown button + count badge
|
|
5061
|
+
function updateTabOverflowBtn() {
|
|
5062
|
+
const scrollContainer = document.getElementById('tabbar-scroll');
|
|
5063
|
+
const btn = document.getElementById('tab-overflow-btn');
|
|
5064
|
+
const countEl = document.getElementById('tab-overflow-count');
|
|
5065
|
+
const tabs = scrollContainer.querySelectorAll('.tab');
|
|
5066
|
+
const isOverflowing = scrollContainer.scrollWidth > scrollContainer.clientWidth + 2;
|
|
5067
|
+
btn.classList.toggle('visible', isOverflowing);
|
|
5068
|
+
if (isOverflowing && countEl) {
|
|
5069
|
+
countEl.textContent = tabs.length;
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
// Toggle the tab overflow dropdown menu
|
|
5074
|
+
function toggleTabOverflow(e) {
|
|
5075
|
+
e.stopPropagation();
|
|
5076
|
+
const btn = document.getElementById('tab-overflow-btn');
|
|
5077
|
+
let menu = btn.querySelector('.tab-overflow-menu');
|
|
5078
|
+
if (menu) { menu.remove(); return; }
|
|
5079
|
+
|
|
5080
|
+
menu = document.createElement('div');
|
|
5081
|
+
menu.className = 'tab-overflow-menu';
|
|
5082
|
+
|
|
5083
|
+
for (const id of state.tabOrder) {
|
|
5084
|
+
// Skip tabs that aren't rendered
|
|
5085
|
+
if (id === 'codereview' || id === 'walle') continue;
|
|
5086
|
+
let label, dotColor, isActive, closeAction;
|
|
5087
|
+
const specialLabels = { rules: '📝 Rules', insights: '📊 Insights', permissions: '🔒 Permissions' };
|
|
5088
|
+
|
|
5089
|
+
if (specialLabels[id]) {
|
|
5090
|
+
label = specialLabels[id];
|
|
5091
|
+
dotColor = 'var(--accent)';
|
|
5092
|
+
isActive = state.activeTab === id;
|
|
5093
|
+
closeAction = `closeSpecialTab('${id}')`;
|
|
5094
|
+
} else if (id === 'review') {
|
|
5095
|
+
const title = document.getElementById('review-title').textContent;
|
|
5096
|
+
label = '📋 ' + title;
|
|
5097
|
+
dotColor = 'var(--accent)';
|
|
5098
|
+
isActive = state.activeTab === 'review';
|
|
5099
|
+
closeAction = "closeSpecialTab('review')";
|
|
5100
|
+
} else {
|
|
5101
|
+
const s = state.sessions.get(id);
|
|
5102
|
+
if (!s) continue;
|
|
5103
|
+
label = s.meta?.label || id.slice(0, 8);
|
|
5104
|
+
const st = getSessionStatus(s);
|
|
5105
|
+
dotColor = st.cls === 'running' ? 'var(--green)' : st.cls === 'waiting' ? 'var(--yellow)' : 'var(--fg-dim)';
|
|
5106
|
+
isActive = state.activeTab === id;
|
|
5107
|
+
closeAction = `killSession('${escHtml(id)}')`;
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
const item = document.createElement('div');
|
|
5111
|
+
item.className = 'tab-overflow-item' + (isActive ? ' active' : '');
|
|
5112
|
+
item.innerHTML = `<span class="overflow-dot" style="background:${dotColor}"></span><span class="overflow-label">${escHtml(label)}</span><span class="overflow-close" onclick="event.stopPropagation();${closeAction}">×</span>`;
|
|
5113
|
+
item.onclick = function() { menu.remove(); activateTab(id); };
|
|
5114
|
+
menu.appendChild(item);
|
|
5115
|
+
}
|
|
5116
|
+
|
|
5117
|
+
btn.appendChild(menu);
|
|
5118
|
+
|
|
5119
|
+
// Close on outside click
|
|
5120
|
+
function closeMenu(ev) {
|
|
5121
|
+
if (!btn.contains(ev.target)) { menu.remove(); document.removeEventListener('click', closeMenu); }
|
|
5122
|
+
}
|
|
5123
|
+
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5126
|
+
// Wheel-to-scroll on tab bar (vertical wheel → horizontal scroll)
|
|
5127
|
+
document.getElementById('tabbar-scroll').addEventListener('wheel', function(e) {
|
|
5128
|
+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
5129
|
+
e.preventDefault();
|
|
5130
|
+
this.scrollLeft += e.deltaY;
|
|
5131
|
+
}
|
|
5132
|
+
}, { passive: false });
|
|
5133
|
+
|
|
4872
5134
|
// --- Tab drag-and-drop reorder ---
|
|
4873
5135
|
let _tabDragId = null;
|
|
4874
5136
|
function saveTabOrder() {
|
|
4875
|
-
// Save
|
|
4876
|
-
const sessionOrder = state.tabOrder.filter(id => state.sessions.has(id));
|
|
5137
|
+
// Save session IDs and review tab (for position restore after restart)
|
|
5138
|
+
const sessionOrder = state.tabOrder.filter(id => state.sessions.has(id) || id === 'review');
|
|
4877
5139
|
savePref('tab_order', sessionOrder);
|
|
4878
5140
|
}
|
|
4879
5141
|
|
|
@@ -4995,6 +5257,11 @@ function showRulesPanel() {
|
|
|
4995
5257
|
|
|
4996
5258
|
function closeSpecialTab(tabId) {
|
|
4997
5259
|
state.tabOrder = state.tabOrder.filter(t => t !== tabId);
|
|
5260
|
+
if (tabId === 'review') {
|
|
5261
|
+
state.reviewingSessionId = null;
|
|
5262
|
+
savePref('reviewing_session', null);
|
|
5263
|
+
document.querySelectorAll('.recent-item.reviewing').forEach(el => el.classList.remove('reviewing'));
|
|
5264
|
+
}
|
|
4998
5265
|
if (state.activeTab === tabId) {
|
|
4999
5266
|
// Prefer switching to an active session, then fall back to sessions nav
|
|
5000
5267
|
const nextSession = state.tabOrder.filter(t => state.sessions.has(t)).pop();
|
|
@@ -5283,6 +5550,7 @@ function _restoreScrollAnchor(term, anchor) {
|
|
|
5283
5550
|
}
|
|
5284
5551
|
new ResizeObserver(fitActiveTerminal).observe(document.getElementById('terminal-area'));
|
|
5285
5552
|
window.addEventListener('resize', fitActiveTerminal);
|
|
5553
|
+
window.addEventListener('resize', updateTabOverflowBtn);
|
|
5286
5554
|
|
|
5287
5555
|
// Default cwd hint
|
|
5288
5556
|
const cwdInput = document.getElementById('ns-cwd');
|
|
@@ -5369,6 +5637,11 @@ async function loadPrefs() {
|
|
|
5369
5637
|
state._savedTabOrder = prefs.tab_order;
|
|
5370
5638
|
}
|
|
5371
5639
|
|
|
5640
|
+
// Restore reviewing session (reopen review tab after CTM restart)
|
|
5641
|
+
if (prefs.reviewing_session) {
|
|
5642
|
+
state._savedReviewingSession = prefs.reviewing_session;
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5372
5645
|
// Restore code review tree width
|
|
5373
5646
|
if (prefs.cr_tree_width) {
|
|
5374
5647
|
state._savedCrTreeWidth = prefs.cr_tree_width;
|
|
@@ -5481,6 +5754,27 @@ async function loadRecentSessions() {
|
|
|
5481
5754
|
}
|
|
5482
5755
|
}
|
|
5483
5756
|
|
|
5757
|
+
// Restore reviewing session from prefs (reopen after CTM restart)
|
|
5758
|
+
// Only if no hash override and no review already open
|
|
5759
|
+
if (state._savedReviewingSession && !state.reviewingSessionId && !location.hash.includes('review=')) {
|
|
5760
|
+
const savedId = state._savedReviewingSession;
|
|
5761
|
+
delete state._savedReviewingSession;
|
|
5762
|
+
const s = allRecentSessions.find(x => x.sessionId === savedId);
|
|
5763
|
+
if (s) {
|
|
5764
|
+
state.reviewingSessionId = savedId;
|
|
5765
|
+
// Add review to tab order (matching saved position) but don't activate it
|
|
5766
|
+
if (!state.tabOrder.includes('review')) state.tabOrder.push('review');
|
|
5767
|
+
// Load the review content in the background
|
|
5768
|
+
reviewSession(s.sessionId, s.projectEntry, sessionDisplayText(s), s);
|
|
5769
|
+
// If the saved active session was 'review', activate it; otherwise stay on current tab
|
|
5770
|
+
if (state._savedActiveSession !== 'review') {
|
|
5771
|
+
// reviewSession calls activateTab('review'), so switch back to the saved session
|
|
5772
|
+
const target = state._savedActiveSession || state.tabOrder.find(t => state.sessions.has(t));
|
|
5773
|
+
if (target) setTimeout(() => activateTab(target), 50);
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5484
5778
|
// Re-scan prompts for active sessions now that we have projectEntry data
|
|
5485
5779
|
for (const [id, s] of state.sessions) {
|
|
5486
5780
|
if (s.term) scanPromptLines(id);
|
|
@@ -5827,6 +6121,12 @@ async function deleteSession(sessionId, projectEntry) {
|
|
|
5827
6121
|
}
|
|
5828
6122
|
|
|
5829
6123
|
function openSessionReview(sessionId) {
|
|
6124
|
+
// If already reviewing this session, just switch to the tab without reloading
|
|
6125
|
+
if (state.reviewingSessionId === sessionId && state.tabOrder.includes('review')) {
|
|
6126
|
+
activateTab('review');
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
|
|
5830
6130
|
const s = allRecentSessions.find(x => x.sessionId === sessionId)
|
|
5831
6131
|
|| { sessionId, cwd: state.sessions.get(sessionId)?.cwd || '', projectEntry: '' };
|
|
5832
6132
|
state.reviewingSessionId = sessionId;
|
|
@@ -5970,6 +6270,9 @@ async function reviewSession(sessionId, projectEntry, title, sessionData) {
|
|
|
5970
6270
|
state.tabOrder.push('review');
|
|
5971
6271
|
}
|
|
5972
6272
|
|
|
6273
|
+
// Persist for restore after CTM restart
|
|
6274
|
+
savePref('reviewing_session', sessionId);
|
|
6275
|
+
|
|
5973
6276
|
// Update URL hash for deep linking (review= for session review)
|
|
5974
6277
|
history.replaceState(null, '', location.pathname + location.search + '#review=' + sessionId);
|
|
5975
6278
|
|