@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.
@@ -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
- — e.g. "Import as" in the adopt modal — float on top, not behind it.
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 newSessionIdArgs once a known `type`
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 user knob.
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 three free-form args fields into string[] before they hit
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
- resumeIdArgs: tok(v.resumeIdArgs),
41
- newSessionIdArgs: tok(v.newSessionIdArgs),
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 resumeArgs
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', resumeIdArgs: '--resume <id>', newSessionIdArgs: '--session-id <id>' },
49
- codex: { command: 'codex', resumeIdArgs: 'resume <id>', newSessionIdArgs: 'resume <id>' },
50
- copilot: { command: 'copilot', resumeIdArgs: '--session-id <id>', newSessionIdArgs: '--session-id <id>' },
51
- other: { resumeIdArgs: '', newSessionIdArgs: '' },
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
- // integration args (newSessionIdArgs / resumeIdArgs) to the
64
- // canonical template — those fields are locked anyway so
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.resumeIdArgs = d.resumeIdArgs;
75
- patch.newSessionIdArgs = d.newSessionIdArgs;
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: 'newSessionIdArgs', label: 'New session id args', mono: true, placeholder: '--session-id <id>',
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
- : 'ccsm pre-generates a UUID and substitutes it for <id> on first launch — the upstream CLI session id is known immediately.' },
102
- { key: 'resumeIdArgs', label: 'Resume by id args', mono: true, placeholder: '--resume <id>',
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 on every resume. Substitutes <id> with the captured session UUID.' },
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
- reserveWorkspacesForStoppedSessions: !!cfg.reserveWorkspacesForStoppedSessions,
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
- reserveWorkspacesForStoppedSessions: !!merged.reserveWorkspacesForStoppedSessions,
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
- resumeIdArgs: formatArgs(edit.payload.resumeIdArgs),
346
- newSessionIdArgs: formatArgs(edit.payload.newSessionIdArgs),
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 reserveStopped = !!config.value?.reserveWorkspacesForStoppedSessions;
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
  }