@bakapiano/ccsm 0.22.7 → 0.22.8
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/CLAUDE.md +44 -63
- package/README.md +11 -14
- package/lib/cliActivity.js +11 -114
- package/lib/codexSeed.js +4 -61
- package/lib/config.js +62 -64
- package/lib/persistedSessions.js +68 -28
- package/lib/winPath.js +1 -1
- package/package.json +1 -1
- package/public/css/widgets.css +1 -379
- package/public/js/api.js +5 -27
- package/public/js/components/EntityFormModal.js +2 -2
- package/public/js/pages/ConfigurePage.js +37 -35
- package/public/js/pages/LaunchPage.js +8 -26
- package/public/js/pages/SessionsPage.js +1 -1
- package/public/js/util.js +1 -1
- package/server.js +110 -192
- package/lib/localCliSessions.js +0 -519
- package/public/js/components/AdoptModal.js +0 -261
package/public/css/widgets.css
CHANGED
|
@@ -260,28 +260,6 @@
|
|
|
260
260
|
text-align: center;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
.launch-import-link {
|
|
264
|
-
display: inline-flex;
|
|
265
|
-
align-items: center;
|
|
266
|
-
background: transparent;
|
|
267
|
-
border: none;
|
|
268
|
-
font: inherit;
|
|
269
|
-
font-size: 12px;
|
|
270
|
-
color: var(--ink-faint);
|
|
271
|
-
cursor: pointer;
|
|
272
|
-
padding: 4px 8px;
|
|
273
|
-
border-radius: 6px;
|
|
274
|
-
transition: color 120ms ease;
|
|
275
|
-
}
|
|
276
|
-
.launch-import-link:hover { color: var(--ink-mid); }
|
|
277
|
-
.launch-import-arrow {
|
|
278
|
-
display: inline-flex;
|
|
279
|
-
align-items: center;
|
|
280
|
-
margin-left: 6px;
|
|
281
|
-
transition: transform 180ms cubic-bezier(.4, 0, .2, 1);
|
|
282
|
-
}
|
|
283
|
-
.launch-import-link:hover .launch-import-arrow { transform: translateX(3px); }
|
|
284
|
-
|
|
285
263
|
/* === Launch toolbar · A · Pill toolbar =========================== */
|
|
286
264
|
|
|
287
265
|
.launch-toolbar {
|
|
@@ -1080,8 +1058,7 @@
|
|
|
1080
1058
|
.popover-panel {
|
|
1081
1059
|
position: fixed;
|
|
1082
1060
|
/* Above the modal backdrop (200) so pickers opened from inside a modal
|
|
1083
|
-
|
|
1084
|
-
Still below toasts (1200). */
|
|
1061
|
+
float on top, not behind it. Still below toasts (1200). */
|
|
1085
1062
|
z-index: 250;
|
|
1086
1063
|
background: var(--bg-elev);
|
|
1087
1064
|
border: 1px solid var(--border);
|
|
@@ -1296,361 +1273,6 @@
|
|
|
1296
1273
|
margin-top: 4px;
|
|
1297
1274
|
}
|
|
1298
1275
|
|
|
1299
|
-
/* --- Adopt (import existing CLI session) modal --------------------- */
|
|
1300
|
-
/* Fills the (flush, scrolling) modal body: a pinned head (tabs + tools)
|
|
1301
|
-
and a scrolling list below it; pagination lives in the modal footer. */
|
|
1302
|
-
.adopt { display: flex; flex-direction: column; min-height: 360px; }
|
|
1303
|
-
|
|
1304
|
-
.adopt-head {
|
|
1305
|
-
position: sticky;
|
|
1306
|
-
top: 0;
|
|
1307
|
-
z-index: 2;
|
|
1308
|
-
display: flex;
|
|
1309
|
-
flex-direction: column;
|
|
1310
|
-
gap: 12px;
|
|
1311
|
-
padding: 14px 18px 12px;
|
|
1312
|
-
background: var(--bg-elev);
|
|
1313
|
-
border-bottom: 1px solid var(--border-soft);
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
/* Underline tab bar (was solid pills) — calmer, matches the app. */
|
|
1317
|
-
.adopt-tabs {
|
|
1318
|
-
display: flex;
|
|
1319
|
-
align-items: center;
|
|
1320
|
-
gap: 2px;
|
|
1321
|
-
}
|
|
1322
|
-
.adopt-tab {
|
|
1323
|
-
appearance: none;
|
|
1324
|
-
display: inline-flex;
|
|
1325
|
-
align-items: center;
|
|
1326
|
-
gap: 7px;
|
|
1327
|
-
padding: 6px 11px 8px;
|
|
1328
|
-
background: transparent;
|
|
1329
|
-
border: 0;
|
|
1330
|
-
border-bottom: 2px solid transparent;
|
|
1331
|
-
font: inherit;
|
|
1332
|
-
font-size: 13px;
|
|
1333
|
-
font-weight: 500;
|
|
1334
|
-
color: var(--ink-muted);
|
|
1335
|
-
cursor: pointer;
|
|
1336
|
-
transition: color .12s ease, border-color .12s ease;
|
|
1337
|
-
}
|
|
1338
|
-
.adopt-tab:hover { color: var(--ink); }
|
|
1339
|
-
.adopt-tab.is-active {
|
|
1340
|
-
color: var(--ink);
|
|
1341
|
-
border-bottom-color: var(--ink);
|
|
1342
|
-
}
|
|
1343
|
-
.adopt-tab-icon { display: inline-flex; width: 16px; height: 16px; }
|
|
1344
|
-
.adopt-tab-icon svg, .adopt-tab-icon img { width: 100%; height: 100%; }
|
|
1345
|
-
.adopt-tab-count {
|
|
1346
|
-
display: inline-flex;
|
|
1347
|
-
align-items: center;
|
|
1348
|
-
justify-content: center;
|
|
1349
|
-
min-width: 17px;
|
|
1350
|
-
height: 16px;
|
|
1351
|
-
padding: 0 5px;
|
|
1352
|
-
border-radius: 999px;
|
|
1353
|
-
background: var(--ui-bg);
|
|
1354
|
-
color: var(--ink-mid);
|
|
1355
|
-
font-size: 10.5px;
|
|
1356
|
-
font-weight: 600;
|
|
1357
|
-
font-variant-numeric: tabular-nums;
|
|
1358
|
-
}
|
|
1359
|
-
.adopt-rescan {
|
|
1360
|
-
appearance: none;
|
|
1361
|
-
margin-left: auto;
|
|
1362
|
-
display: inline-flex;
|
|
1363
|
-
align-items: center;
|
|
1364
|
-
justify-content: center;
|
|
1365
|
-
width: 28px;
|
|
1366
|
-
height: 28px;
|
|
1367
|
-
padding: 0;
|
|
1368
|
-
border: 1px solid var(--border);
|
|
1369
|
-
background: var(--bg-elev);
|
|
1370
|
-
color: var(--ink-muted);
|
|
1371
|
-
border-radius: 8px;
|
|
1372
|
-
cursor: pointer;
|
|
1373
|
-
transition: color .12s ease, border-color .12s ease, transform .3s ease;
|
|
1374
|
-
}
|
|
1375
|
-
.adopt-rescan:hover { color: var(--ink); border-color: var(--ink-faint); transform: rotate(90deg); }
|
|
1376
|
-
.adopt-rescan svg { width: 14px; height: 14px; }
|
|
1377
|
-
|
|
1378
|
-
/* Tools row: CLI picker pill + search input */
|
|
1379
|
-
.adopt-tools {
|
|
1380
|
-
display: flex;
|
|
1381
|
-
align-items: center;
|
|
1382
|
-
gap: 10px;
|
|
1383
|
-
flex-wrap: wrap;
|
|
1384
|
-
}
|
|
1385
|
-
.adopt-cli-pill {
|
|
1386
|
-
appearance: none;
|
|
1387
|
-
display: inline-flex;
|
|
1388
|
-
align-items: center;
|
|
1389
|
-
gap: 6px;
|
|
1390
|
-
padding: 5px 8px 5px 10px;
|
|
1391
|
-
height: 30px;
|
|
1392
|
-
border: 1px solid var(--border);
|
|
1393
|
-
border-radius: 8px;
|
|
1394
|
-
background: var(--bg-elev);
|
|
1395
|
-
color: var(--ink);
|
|
1396
|
-
font: inherit;
|
|
1397
|
-
font-size: 12px;
|
|
1398
|
-
cursor: pointer;
|
|
1399
|
-
transition: border-color .12s ease, background .12s ease;
|
|
1400
|
-
}
|
|
1401
|
-
.adopt-cli-pill:hover { border-color: var(--ink-faint); background: var(--bg); }
|
|
1402
|
-
.adopt-cli-pill.is-open { border-color: var(--ink); }
|
|
1403
|
-
.adopt-cli-pill-prefix { color: var(--ink-muted); font-size: 11.5px; }
|
|
1404
|
-
.adopt-cli-pill-icon {
|
|
1405
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
1406
|
-
width: 14px; height: 14px;
|
|
1407
|
-
}
|
|
1408
|
-
.adopt-cli-pill-icon svg { width: 100%; height: 100%; }
|
|
1409
|
-
.adopt-cli-pill-name { font-weight: 500; }
|
|
1410
|
-
.adopt-cli-pill > svg { color: var(--ink-faint); width: 12px; height: 12px; }
|
|
1411
|
-
|
|
1412
|
-
.adopt-search {
|
|
1413
|
-
position: relative;
|
|
1414
|
-
flex: 1 1 200px;
|
|
1415
|
-
min-width: 200px;
|
|
1416
|
-
}
|
|
1417
|
-
.adopt-search-icon {
|
|
1418
|
-
position: absolute;
|
|
1419
|
-
left: 9px;
|
|
1420
|
-
top: 50%;
|
|
1421
|
-
transform: translateY(-50%);
|
|
1422
|
-
display: inline-flex;
|
|
1423
|
-
color: var(--ink-faint);
|
|
1424
|
-
pointer-events: none;
|
|
1425
|
-
}
|
|
1426
|
-
.adopt-search-icon svg { width: 13px; height: 13px; }
|
|
1427
|
-
.adopt-search-input {
|
|
1428
|
-
appearance: none;
|
|
1429
|
-
width: 100%;
|
|
1430
|
-
height: 30px;
|
|
1431
|
-
padding: 0 28px 0 28px;
|
|
1432
|
-
border: 1px solid var(--border);
|
|
1433
|
-
border-radius: 8px;
|
|
1434
|
-
background: var(--bg-elev);
|
|
1435
|
-
color: var(--ink);
|
|
1436
|
-
font: inherit;
|
|
1437
|
-
font-size: 12px;
|
|
1438
|
-
outline: none;
|
|
1439
|
-
transition: border-color .12s ease;
|
|
1440
|
-
}
|
|
1441
|
-
.adopt-search-input:focus { border-color: var(--ink-faint); }
|
|
1442
|
-
.adopt-search-input:disabled { color: var(--ink-faint); }
|
|
1443
|
-
.adopt-search-clear {
|
|
1444
|
-
appearance: none;
|
|
1445
|
-
position: absolute;
|
|
1446
|
-
right: 6px;
|
|
1447
|
-
top: 50%;
|
|
1448
|
-
transform: translateY(-50%);
|
|
1449
|
-
border: 0;
|
|
1450
|
-
background: transparent;
|
|
1451
|
-
color: var(--ink-faint);
|
|
1452
|
-
padding: 4px;
|
|
1453
|
-
border-radius: 6px;
|
|
1454
|
-
cursor: pointer;
|
|
1455
|
-
display: inline-flex;
|
|
1456
|
-
}
|
|
1457
|
-
.adopt-search-clear:hover { color: var(--ink); background: var(--bg); }
|
|
1458
|
-
.adopt-search-clear svg { width: 11px; height: 11px; }
|
|
1459
|
-
|
|
1460
|
-
/* The list scrolls inside the modal body now (not its own overflow box);
|
|
1461
|
-
this is just the padded wrapper. */
|
|
1462
|
-
.adopt-list {
|
|
1463
|
-
padding: 12px 18px 16px;
|
|
1464
|
-
}
|
|
1465
|
-
.adopt-empty {
|
|
1466
|
-
padding: 48px 12px;
|
|
1467
|
-
text-align: center;
|
|
1468
|
-
color: var(--ink-muted);
|
|
1469
|
-
font-size: 12.5px;
|
|
1470
|
-
display: flex;
|
|
1471
|
-
flex-direction: column;
|
|
1472
|
-
align-items: center;
|
|
1473
|
-
gap: 10px;
|
|
1474
|
-
}
|
|
1475
|
-
.adopt-error { color: var(--danger); }
|
|
1476
|
-
.adopt-empty-mark {
|
|
1477
|
-
font-size: 24px;
|
|
1478
|
-
color: var(--ink-faint);
|
|
1479
|
-
width: 44px;
|
|
1480
|
-
height: 44px;
|
|
1481
|
-
border-radius: 50%;
|
|
1482
|
-
background: var(--bg);
|
|
1483
|
-
display: inline-flex;
|
|
1484
|
-
align-items: center;
|
|
1485
|
-
justify-content: center;
|
|
1486
|
-
border: 1px dashed var(--border);
|
|
1487
|
-
}
|
|
1488
|
-
.adopt-empty-spinner {
|
|
1489
|
-
width: 14px; height: 14px; border-radius: 50%;
|
|
1490
|
-
border: 1.6px solid var(--border);
|
|
1491
|
-
border-top-color: var(--ink-faint);
|
|
1492
|
-
animation: adopt-spin 0.8s linear infinite;
|
|
1493
|
-
display: inline-block;
|
|
1494
|
-
vertical-align: -2px;
|
|
1495
|
-
margin-right: 6px;
|
|
1496
|
-
}
|
|
1497
|
-
@keyframes adopt-spin { to { transform: rotate(360deg); } }
|
|
1498
|
-
|
|
1499
|
-
.adopt-rows {
|
|
1500
|
-
list-style: none;
|
|
1501
|
-
margin: 0;
|
|
1502
|
-
padding: 0;
|
|
1503
|
-
display: flex;
|
|
1504
|
-
flex-direction: column;
|
|
1505
|
-
gap: 6px;
|
|
1506
|
-
}
|
|
1507
|
-
.adopt-row {
|
|
1508
|
-
display: flex;
|
|
1509
|
-
align-items: center;
|
|
1510
|
-
gap: 11px;
|
|
1511
|
-
padding: 10px 12px;
|
|
1512
|
-
border: 1px solid var(--border);
|
|
1513
|
-
background: var(--bg-elev);
|
|
1514
|
-
border-radius: 10px;
|
|
1515
|
-
transition: border-color .12s ease, background .12s ease, transform .12s ease;
|
|
1516
|
-
}
|
|
1517
|
-
.adopt-row-icon {
|
|
1518
|
-
position: relative;
|
|
1519
|
-
flex: 0 0 auto;
|
|
1520
|
-
display: inline-flex;
|
|
1521
|
-
align-items: center;
|
|
1522
|
-
justify-content: center;
|
|
1523
|
-
width: 30px;
|
|
1524
|
-
height: 30px;
|
|
1525
|
-
border-radius: 8px;
|
|
1526
|
-
background: var(--bg);
|
|
1527
|
-
border: 1px solid var(--border-soft);
|
|
1528
|
-
}
|
|
1529
|
-
.adopt-row-icon svg, .adopt-row-icon img { width: 17px; height: 17px; }
|
|
1530
|
-
.adopt-row:hover {
|
|
1531
|
-
border-color: var(--ink-faint);
|
|
1532
|
-
transform: translateY(-1px);
|
|
1533
|
-
}
|
|
1534
|
-
.adopt-row.is-adopted {
|
|
1535
|
-
opacity: 0.6;
|
|
1536
|
-
background: var(--bg);
|
|
1537
|
-
}
|
|
1538
|
-
.adopt-row.is-adopted:hover { transform: none; border-color: var(--border); }
|
|
1539
|
-
/* A session a cli process currently has open. Quiet treatment: the row
|
|
1540
|
-
itself stays neutral (many rows can be live at once — a wall of green
|
|
1541
|
-
slabs is noise) and the "alive" signal rides on the icon as a small
|
|
1542
|
-
green presence dot, the way a chat avatar shows online. The icon tile
|
|
1543
|
-
picks up a faint green tint + green glyph so the eye still lands. */
|
|
1544
|
-
.adopt-row.is-active .adopt-row-icon {
|
|
1545
|
-
border-color: rgba(74, 138, 74, 0.4);
|
|
1546
|
-
background: rgba(74, 138, 74, 0.1);
|
|
1547
|
-
color: var(--green);
|
|
1548
|
-
}
|
|
1549
|
-
/* Static dot — punched out of the card with a bg-elev ring so it reads as
|
|
1550
|
-
a badge sitting on the icon corner. */
|
|
1551
|
-
.adopt-row.is-active .adopt-row-icon::after {
|
|
1552
|
-
content: "";
|
|
1553
|
-
position: absolute;
|
|
1554
|
-
right: -3px;
|
|
1555
|
-
bottom: -3px;
|
|
1556
|
-
width: 10px;
|
|
1557
|
-
height: 10px;
|
|
1558
|
-
border-radius: 50%;
|
|
1559
|
-
background: var(--green);
|
|
1560
|
-
border: 2px solid var(--bg-elev);
|
|
1561
|
-
}
|
|
1562
|
-
/* Soft presence pulse — a ring expanding off the dot. Opacity-animated on
|
|
1563
|
-
its own layer so it stays GPU-cheap even with a dozen live rows (the old
|
|
1564
|
-
box-shadow pulse pegged the paint thread). */
|
|
1565
|
-
.adopt-row.is-active .adopt-row-icon::before {
|
|
1566
|
-
content: "";
|
|
1567
|
-
position: absolute;
|
|
1568
|
-
right: -1px;
|
|
1569
|
-
bottom: -1px;
|
|
1570
|
-
width: 6px;
|
|
1571
|
-
height: 6px;
|
|
1572
|
-
border-radius: 50%;
|
|
1573
|
-
border: 1px solid var(--green);
|
|
1574
|
-
opacity: 0;
|
|
1575
|
-
animation: adopt-live-pulse 1.9s ease-in-out infinite;
|
|
1576
|
-
pointer-events: none;
|
|
1577
|
-
z-index: 1;
|
|
1578
|
-
}
|
|
1579
|
-
@keyframes adopt-live-pulse {
|
|
1580
|
-
0% { opacity: 0.55; transform: scale(0.8); }
|
|
1581
|
-
70%, 100% { opacity: 0; transform: scale(2.6); }
|
|
1582
|
-
}
|
|
1583
|
-
.adopt-row-main { flex: 1 1 auto; min-width: 0; }
|
|
1584
|
-
.adopt-row-title {
|
|
1585
|
-
font-size: 13px;
|
|
1586
|
-
color: var(--ink);
|
|
1587
|
-
font-weight: 500;
|
|
1588
|
-
overflow: hidden;
|
|
1589
|
-
text-overflow: ellipsis;
|
|
1590
|
-
white-space: nowrap;
|
|
1591
|
-
line-height: 1.35;
|
|
1592
|
-
display: flex;
|
|
1593
|
-
align-items: center;
|
|
1594
|
-
gap: 8px;
|
|
1595
|
-
}
|
|
1596
|
-
.adopt-row-untitled { color: var(--ink-faint); font-weight: 400; font-style: italic; }
|
|
1597
|
-
/* Minimal caption next to the title — the dot carries the signal, this
|
|
1598
|
-
just names it. No pill, no border, no ring. */
|
|
1599
|
-
.adopt-row-live {
|
|
1600
|
-
flex: 0 0 auto;
|
|
1601
|
-
font-size: 10px;
|
|
1602
|
-
font-weight: 600;
|
|
1603
|
-
letter-spacing: 0.05em;
|
|
1604
|
-
text-transform: uppercase;
|
|
1605
|
-
color: var(--green);
|
|
1606
|
-
cursor: default;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
/* Footer pagination (rendered into .modal-foot): ‹ Prev · X–Y of Z · Next ›.
|
|
1610
|
-
The flex:1 info pushes the two buttons to the edges and centres the count. */
|
|
1611
|
-
.adopt-pager-info {
|
|
1612
|
-
flex: 1;
|
|
1613
|
-
text-align: center;
|
|
1614
|
-
font-size: 12px;
|
|
1615
|
-
color: var(--ink-muted);
|
|
1616
|
-
font-variant-numeric: tabular-nums;
|
|
1617
|
-
}
|
|
1618
|
-
.adopt-pager-btn { min-width: 78px; gap: 4px; }
|
|
1619
|
-
.adopt-pager-btn svg { width: 13px; height: 13px; }
|
|
1620
|
-
.adopt-row-meta {
|
|
1621
|
-
display: flex;
|
|
1622
|
-
align-items: center;
|
|
1623
|
-
font-size: 11px;
|
|
1624
|
-
color: var(--ink-muted);
|
|
1625
|
-
margin-top: 4px;
|
|
1626
|
-
white-space: nowrap;
|
|
1627
|
-
overflow: hidden;
|
|
1628
|
-
}
|
|
1629
|
-
.adopt-row-path {
|
|
1630
|
-
min-width: 0;
|
|
1631
|
-
overflow: hidden;
|
|
1632
|
-
text-overflow: ellipsis;
|
|
1633
|
-
white-space: nowrap;
|
|
1634
|
-
flex: 0 1 auto;
|
|
1635
|
-
}
|
|
1636
|
-
.adopt-row-dot { margin: 0 8px; color: var(--ink-faint); }
|
|
1637
|
-
.adopt-row-id { color: var(--ink-faint); }
|
|
1638
|
-
.adopt-row-actions {
|
|
1639
|
-
display: flex;
|
|
1640
|
-
align-items: center;
|
|
1641
|
-
gap: 6px;
|
|
1642
|
-
flex: 0 0 auto;
|
|
1643
|
-
}
|
|
1644
|
-
.adopt-row-btn { padding: 6px 14px; font-size: 12px; }
|
|
1645
|
-
.adopt-row-badge {
|
|
1646
|
-
font-size: 11px;
|
|
1647
|
-
color: var(--ink-muted);
|
|
1648
|
-
padding: 5px 12px;
|
|
1649
|
-
border-radius: 999px;
|
|
1650
|
-
background: var(--bg);
|
|
1651
|
-
border: 1px solid var(--border);
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
1276
|
/* ── Remote page ──────────────────────────────────────────────────
|
|
1655
1277
|
Uses the existing .settings-scroll + Section + .config-grid + .field
|
|
1656
1278
|
+ .chip system from ConfigurePage. Only adds the bits that don't
|
package/public/js/api.js
CHANGED
|
@@ -100,6 +100,8 @@ export async function updateCli(id, patch) {
|
|
|
100
100
|
clis: (cfg.clis || []).map((c) => c.id === id ? {
|
|
101
101
|
...c, ...patch,
|
|
102
102
|
args: toArr(patch.args, c.args),
|
|
103
|
+
resumeLatestArgs: toArr(patch.resumeLatestArgs, c.resumeLatestArgs),
|
|
104
|
+
resumePickerArgs: toArr(patch.resumePickerArgs, c.resumePickerArgs),
|
|
103
105
|
shell: ['direct', 'pwsh', 'cmd'].includes(patch.shell ?? c.shell) ? (patch.shell ?? c.shell) : 'direct',
|
|
104
106
|
} : c),
|
|
105
107
|
};
|
|
@@ -158,7 +160,7 @@ export async function setDefaultCli(id) {
|
|
|
158
160
|
|
|
159
161
|
// Add a new CLI to config.clis and return its id. Generates a fresh id
|
|
160
162
|
// from the command name + an integer suffix when collisions exist.
|
|
161
|
-
export async function createCli({ name, command, args, shell, type }) {
|
|
163
|
+
export async function createCli({ name, command, args, resumeLatestArgs, resumePickerArgs, shell, type }) {
|
|
162
164
|
const cfg = S.config.value || (await api('GET', '/api/config'));
|
|
163
165
|
const base = (name || command || 'cli').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'cli';
|
|
164
166
|
let id = base, n = 1;
|
|
@@ -171,6 +173,8 @@ export async function createCli({ name, command, args, shell, type }) {
|
|
|
171
173
|
name: (name || command || id).trim(),
|
|
172
174
|
command: (command || '').trim(),
|
|
173
175
|
args: toArr(args),
|
|
176
|
+
resumeLatestArgs: toArr(resumeLatestArgs),
|
|
177
|
+
resumePickerArgs: toArr(resumePickerArgs),
|
|
174
178
|
shell: ['direct', 'pwsh', 'cmd'].includes(shell) ? shell : 'direct',
|
|
175
179
|
type: ['claude', 'codex', 'copilot', 'other'].includes(type) ? type : 'other',
|
|
176
180
|
}],
|
|
@@ -340,32 +344,6 @@ export async function refreshAll() {
|
|
|
340
344
|
S.lastRefreshAt.value = Date.now();
|
|
341
345
|
}
|
|
342
346
|
|
|
343
|
-
// List existing CLI sessions discovered on disk for a given cli type.
|
|
344
|
-
// Paginated: page 0 returns all currently-active sessions + the first
|
|
345
|
-
// `limit` non-active (sorted mtime desc). Subsequent pages return the
|
|
346
|
-
// next slice of non-active sessions.
|
|
347
|
-
// Returns { sessions, totalActive, totalNonActive, total, offset, limit, hasMore }.
|
|
348
|
-
export async function listLocalCliSessions(cliType, { offset = 0, limit = 30 } = {}) {
|
|
349
|
-
const qs = `offset=${offset}&limit=${limit}`;
|
|
350
|
-
const r = await api('GET', `/api/cli-sessions/${cliType}?${qs}`);
|
|
351
|
-
return {
|
|
352
|
-
sessions: r.sessions || [],
|
|
353
|
-
totalActive: r.totalActive ?? 0,
|
|
354
|
-
totalNonActive: r.totalNonActive ?? 0,
|
|
355
|
-
total: r.total ?? (r.sessions?.length || 0),
|
|
356
|
-
offset: r.offset ?? offset,
|
|
357
|
-
limit: r.limit ?? limit,
|
|
358
|
-
hasMore: !!r.hasMore,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Adopt an existing upstream CLI session into ccsm. Returns the created
|
|
363
|
-
// (or existing) persistedSessions record.
|
|
364
|
-
export async function adoptSession({ cliId, cliSessionId, cwd, title, folderId }) {
|
|
365
|
-
const r = await api('POST', '/api/sessions/adopt', { cliId, cliSessionId, cwd, title, folderId });
|
|
366
|
-
return r;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
347
|
export async function restartBackend() {
|
|
370
348
|
return api('POST', '/api/restart');
|
|
371
349
|
}
|
|
@@ -29,9 +29,9 @@ export function EntityFormModal({
|
|
|
29
29
|
// A field is read-only if its key is in the static `readOnlyKeys`
|
|
30
30
|
// prop OR its own `readOnly` predicate (called with the current
|
|
31
31
|
// draft) returns true. The predicate lets a field react to other
|
|
32
|
-
// fields' values — e.g. lock
|
|
32
|
+
// fields' values — e.g. lock known CLI resume args once a `type`
|
|
33
33
|
// is picked, since those args are an integration contract with the
|
|
34
|
-
// upstream CLI, not a
|
|
34
|
+
// upstream CLI, not a regular launch arg.
|
|
35
35
|
const isReadOnly = (field) => {
|
|
36
36
|
if (readOnlyKeys.includes(field.key)) return true;
|
|
37
37
|
if (typeof field.readOnly === 'function') {
|
|
@@ -27,7 +27,7 @@ import { useDragSort } from '../components/useDragSort.js';
|
|
|
27
27
|
import { IconPlus, IconPencil, IconClose, IconTerminal, IconFolder, IconBranch, IconRefresh, IconChevronUp, IconChevronDown, IconForCliType, IconClaudeColor, IconCodexColor, IconCopilotColor, IconSun, IconMoon, IconMonitor } from '../icons.js';
|
|
28
28
|
import { parseArgs, formatArgs } from '../util.js';
|
|
29
29
|
|
|
30
|
-
// Tokenize the
|
|
30
|
+
// Tokenize the free-form args fields into string[] before they hit
|
|
31
31
|
// the backend. Form values arrive as strings (text inputs) — backend
|
|
32
32
|
// stores arrays. parseArgs handles shell-style quoting so users can type
|
|
33
33
|
// `-Model "claude-opus-4-8"` or `-Path 'C:\some dir\bin'` and get sane
|
|
@@ -37,18 +37,18 @@ function tokenizeCliArgs(v) {
|
|
|
37
37
|
return {
|
|
38
38
|
...v,
|
|
39
39
|
args: tok(v.args),
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
resumeLatestArgs: tok(v.resumeLatestArgs),
|
|
41
|
+
resumePickerArgs: tok(v.resumePickerArgs),
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Type → smart defaults. Choosing a type in the form auto-fills
|
|
45
|
+
// Type → smart defaults. Choosing a type in the form auto-fills resume args
|
|
46
46
|
// (and command if blank) so users don't need to remember the per-CLI flag.
|
|
47
47
|
const CLI_TYPE_DEFAULTS = {
|
|
48
|
-
claude: { command: 'claude',
|
|
49
|
-
codex: { command: 'codex',
|
|
50
|
-
copilot: { command: 'copilot',
|
|
51
|
-
other: {
|
|
48
|
+
claude: { command: 'claude', resumeLatestArgs: '--continue', resumePickerArgs: '--resume' },
|
|
49
|
+
codex: { command: 'codex', resumeLatestArgs: 'resume --last', resumePickerArgs: 'resume' },
|
|
50
|
+
copilot: { command: 'copilot', resumeLatestArgs: '--continue', resumePickerArgs: '--resume' },
|
|
51
|
+
other: { resumeLatestArgs: '', resumePickerArgs: '' },
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
function cliFieldsFor({ creating } = {}) {
|
|
@@ -60,8 +60,8 @@ function cliFieldsFor({ creating } = {}) {
|
|
|
60
60
|
{ value: 'other', label: 'Other', icon: html`<${IconTerminal} />` },
|
|
61
61
|
],
|
|
62
62
|
// Type-change side effects. For known types we force the
|
|
63
|
-
//
|
|
64
|
-
//
|
|
63
|
+
// folder-level resume args to the canonical template — those
|
|
64
|
+
// fields are locked anyway so
|
|
65
65
|
// there's no value in leaving stale strings around. For
|
|
66
66
|
// type='other' we leave existing args alone so the user can
|
|
67
67
|
// keep editing them. Name + command are only prefilled when
|
|
@@ -71,8 +71,8 @@ function cliFieldsFor({ creating } = {}) {
|
|
|
71
71
|
if (!d) return null;
|
|
72
72
|
const patch = {};
|
|
73
73
|
if (v !== 'other') {
|
|
74
|
-
patch.
|
|
75
|
-
patch.
|
|
74
|
+
patch.resumeLatestArgs = d.resumeLatestArgs;
|
|
75
|
+
patch.resumePickerArgs = d.resumePickerArgs;
|
|
76
76
|
}
|
|
77
77
|
if (creating) {
|
|
78
78
|
if (!next.command || !next.command.trim()) patch.command = d.command || '';
|
|
@@ -90,20 +90,16 @@ function cliFieldsFor({ creating } = {}) {
|
|
|
90
90
|
{ key: 'command', label: 'Command', mono: true, placeholder: 'claude / codex / ...', required: true },
|
|
91
91
|
{ key: 'args', label: 'Args', mono: true, placeholder: '',
|
|
92
92
|
hint: 'Used on every launch. Shell-style quoting: -Model "claude-opus-4-8" or -Path \'C:\\some dir\\bin\'.' },
|
|
93
|
-
{ key: '
|
|
94
|
-
// Lock for known types — those args are an integration contract
|
|
95
|
-
// with the upstream CLI, not a user knob. Only Type=Other allows
|
|
96
|
-
// a custom value (for hand-rolled CLIs ccsm doesn't ship a
|
|
97
|
-
// template for).
|
|
93
|
+
{ key: 'resumeLatestArgs', label: 'Resume latest args', mono: true, placeholder: '--continue',
|
|
98
94
|
readOnly: (d) => d.type && d.type !== 'other',
|
|
99
95
|
hint: (d) => d.type && d.type !== 'other'
|
|
100
96
|
? `Locked to the canonical flags for ${d.type}. Change Type to "Other" to override.`
|
|
101
|
-
: '
|
|
102
|
-
{ key: '
|
|
97
|
+
: 'Used when Resume behavior is set to latest.' },
|
|
98
|
+
{ key: 'resumePickerArgs', label: 'Resume picker args', mono: true, placeholder: '--resume',
|
|
103
99
|
readOnly: (d) => d.type && d.type !== 'other',
|
|
104
100
|
hint: (d) => d.type && d.type !== 'other'
|
|
105
101
|
? `Locked to the canonical flags for ${d.type}. Change Type to "Other" to override.`
|
|
106
|
-
: 'Used
|
|
102
|
+
: 'Used when Resume behavior is set to picker.' },
|
|
107
103
|
{ key: 'shell', label: 'Shell', type: 'select', default: 'direct', options: [
|
|
108
104
|
{ value: 'direct', label: 'direct (real .exe / .cmd)' },
|
|
109
105
|
{ value: 'pwsh', label: 'pwsh (PowerShell aliases & functions)' },
|
|
@@ -157,7 +153,7 @@ export function ConfigurePage() {
|
|
|
157
153
|
setGeneral({
|
|
158
154
|
workDir: cfg.workDir,
|
|
159
155
|
editor: cfg.editor,
|
|
160
|
-
|
|
156
|
+
resumeMode: cfg.resumeMode === 'picker' ? 'picker' : 'latest',
|
|
161
157
|
});
|
|
162
158
|
}
|
|
163
159
|
}, [cfg]);
|
|
@@ -172,7 +168,7 @@ export function ConfigurePage() {
|
|
|
172
168
|
...cfg,
|
|
173
169
|
workDir: (merged.workDir || '').trim(),
|
|
174
170
|
editor: (merged.editor || '').trim(),
|
|
175
|
-
|
|
171
|
+
resumeMode: merged.resumeMode === 'picker' ? 'picker' : 'latest',
|
|
176
172
|
});
|
|
177
173
|
config.value = saved;
|
|
178
174
|
setToast('saved');
|
|
@@ -204,6 +200,21 @@ export function ConfigurePage() {
|
|
|
204
200
|
<span class="label">Backend</span>
|
|
205
201
|
<${RestartButton} />
|
|
206
202
|
</div>
|
|
203
|
+
<div class="field">
|
|
204
|
+
<span class="label">Resume behavior</span>
|
|
205
|
+
<div class="seg" role="group" aria-label="Resume behavior">
|
|
206
|
+
${[
|
|
207
|
+
{ id: 'latest', label: 'Resume latest' },
|
|
208
|
+
{ id: 'picker', label: 'Resume picker' },
|
|
209
|
+
].map((o) => html`
|
|
210
|
+
<button key=${o.id} type="button"
|
|
211
|
+
class=${`seg-btn${general.resumeMode === o.id ? ' is-active' : ''}`}
|
|
212
|
+
aria-pressed=${general.resumeMode === o.id}
|
|
213
|
+
onClick=${() => saveGeneral({ resumeMode: o.id })}>
|
|
214
|
+
<span>${o.label}</span>
|
|
215
|
+
</button>`)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
207
218
|
<label class="field">
|
|
208
219
|
<span class="label">Editor</span>
|
|
209
220
|
<input type="text" class="mono" value=${general.editor || ''}
|
|
@@ -214,7 +225,7 @@ export function ConfigurePage() {
|
|
|
214
225
|
</div>
|
|
215
226
|
</${Section}>
|
|
216
227
|
|
|
217
|
-
<${Section} title="CLIs" meta=${html`Built-in entries (<code>claude</code>, <code>codex</code>) auto-probe your PATH.`}>
|
|
228
|
+
<${Section} title="CLIs" meta=${html`Built-in entries (<code>claude</code>, <code>codex</code>, <code>copilot</code>) auto-probe your PATH.`}>
|
|
218
229
|
<${EntityList}
|
|
219
230
|
kind="cli"
|
|
220
231
|
addLabel="Add CLI"
|
|
@@ -308,14 +319,6 @@ export function ConfigurePage() {
|
|
|
308
319
|
<input type="text" value=${general.workDir}
|
|
309
320
|
onChange=${(e) => saveGeneral({ workDir: e.target.value })} />
|
|
310
321
|
</label>
|
|
311
|
-
<label class="field toggle">
|
|
312
|
-
<input type="checkbox" checked=${!!general.reserveWorkspacesForStoppedSessions}
|
|
313
|
-
onChange=${(e) => saveGeneral({ reserveWorkspacesForStoppedSessions: e.target.checked })} />
|
|
314
|
-
<span class="toggle-text">
|
|
315
|
-
<span class="label">Reserve stopped sessions</span>
|
|
316
|
-
<span class="hint">Stopped sessions keep their workspace marked in use until the session is deleted.</span>
|
|
317
|
-
</span>
|
|
318
|
-
</label>
|
|
319
322
|
</div>
|
|
320
323
|
<${WorkspaceList} />
|
|
321
324
|
</${Section}>
|
|
@@ -342,8 +345,8 @@ export function ConfigurePage() {
|
|
|
342
345
|
initial=${{
|
|
343
346
|
...edit.payload,
|
|
344
347
|
args: formatArgs(edit.payload.args),
|
|
345
|
-
|
|
346
|
-
|
|
348
|
+
resumeLatestArgs: formatArgs(edit.payload.resumeLatestArgs),
|
|
349
|
+
resumePickerArgs: formatArgs(edit.payload.resumePickerArgs),
|
|
347
350
|
}}
|
|
348
351
|
onClose=${close}
|
|
349
352
|
onTest=${(v) => testCli({ command: v.command, shell: v.shell, type: v.type })}
|
|
@@ -434,8 +437,7 @@ function EntityList({ items, onAdd, onEdit, onDelete, onActivate, emptyHint, dnd
|
|
|
434
437
|
// ── Workspace list ───────────────────────────────────────────────────
|
|
435
438
|
function WorkspaceList() {
|
|
436
439
|
const ws = workspaces.value || [];
|
|
437
|
-
const
|
|
438
|
-
const inUseBy = reserveStopped ? 'session' : 'running session';
|
|
440
|
+
const inUseBy = 'session';
|
|
439
441
|
if (ws.length === 0) {
|
|
440
442
|
return html`<div class="entity-empty">No workspaces yet — they're created automatically on launch.</div>`;
|
|
441
443
|
}
|