@generativereality/cctabs 0.4.3 → 0.4.5
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-plugin/plugin.json +1 -1
- package/dist/index.js +137 -27
- package/package.json +1 -1
- package/skills/cctabs/SKILL.md +12 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctabs",
|
|
3
3
|
"description": "Claude Code tab manager. Terminal tabs as the UI, no tmux. Claude can orchestrate its own sibling sessions.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "generativereality",
|
|
7
7
|
"url": "https://cctabs.com"
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { consola } from "consola";
|
|
|
11
11
|
import * as p from "@clack/prompts";
|
|
12
12
|
//#region package.json
|
|
13
13
|
var name = "@generativereality/cctabs";
|
|
14
|
-
var version = "0.4.
|
|
14
|
+
var version = "0.4.5";
|
|
15
15
|
var description = "Claude Code tab manager. Terminal tabs as the UI, no tmux.";
|
|
16
16
|
var package_default = {
|
|
17
17
|
name,
|
|
@@ -709,7 +709,8 @@ var TabbyAdapter = class {
|
|
|
709
709
|
cwd: opts.cwd,
|
|
710
710
|
title: opts.title,
|
|
711
711
|
command: opts.command,
|
|
712
|
-
args: opts.args
|
|
712
|
+
args: opts.args,
|
|
713
|
+
afterActive: opts.afterActive ?? false
|
|
713
714
|
}))?.uuid;
|
|
714
715
|
if (!uuid) throw new Error("Tabby plugin did not return a tab uuid");
|
|
715
716
|
return {
|
|
@@ -717,6 +718,10 @@ var TabbyAdapter = class {
|
|
|
717
718
|
tabId: uuid
|
|
718
719
|
};
|
|
719
720
|
}
|
|
721
|
+
async reorderTabs(order) {
|
|
722
|
+
this.ensureHealthy();
|
|
723
|
+
await this.http("POST", "/api/tabs/reorder", { order });
|
|
724
|
+
}
|
|
720
725
|
async sendInput(blockId, text) {
|
|
721
726
|
return this.http("POST", `/api/tabs/${blockId}/send`, { data: text });
|
|
722
727
|
}
|
|
@@ -1263,24 +1268,26 @@ function ensureConfigExists() {
|
|
|
1263
1268
|
return CONFIG_PATH;
|
|
1264
1269
|
}
|
|
1265
1270
|
//#endregion
|
|
1271
|
+
//#region src/core/shell.ts
|
|
1272
|
+
/**
|
|
1273
|
+
* POSIX single-quote escape one argv token. Several commands join the
|
|
1274
|
+
* configured `claude.flags` into a raw shell string and send it as terminal
|
|
1275
|
+
* input, so any value with shell metacharacters must be quoted or the shell
|
|
1276
|
+
* mangles it before `claude` sees it — e.g. a `--model opus[1m]` flag
|
|
1277
|
+
* glob-expands under zsh ("no matches found: opus[1m]") and the launch
|
|
1278
|
+
* silently falls back to the default model. Single quotes are inert in every
|
|
1279
|
+
* POSIX shell; embedded single quotes are closed, escaped, and reopened ('\'').
|
|
1280
|
+
*/
|
|
1281
|
+
function shellQuoteArg(arg) {
|
|
1282
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
1283
|
+
}
|
|
1284
|
+
//#endregion
|
|
1266
1285
|
//#region src/core/open-session.ts
|
|
1267
1286
|
function shellQuoteEnv$1(env) {
|
|
1268
1287
|
const entries = Object.entries(env);
|
|
1269
1288
|
if (!entries.length) return "";
|
|
1270
1289
|
return entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") + " ";
|
|
1271
1290
|
}
|
|
1272
|
-
/**
|
|
1273
|
-
* POSIX single-quote escape one argv token. The configured `claude.flags` are
|
|
1274
|
-
* joined into a raw shell string and sent as terminal input, so any value with
|
|
1275
|
-
* shell metacharacters must be quoted or the shell mangles it before `claude`
|
|
1276
|
-
* sees it — e.g. a `--model opus[1m]` flag glob-expands under zsh ("no matches
|
|
1277
|
-
* found: opus[1m]") and the launch silently falls back to the default model.
|
|
1278
|
-
* Single quotes are inert in every POSIX shell; embedded single quotes are
|
|
1279
|
-
* closed, escaped, and reopened ('\'').
|
|
1280
|
-
*/
|
|
1281
|
-
function shellQuoteArg(arg) {
|
|
1282
|
-
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
1283
|
-
}
|
|
1284
1291
|
/** Poll scrollback until a pattern is visible, then return. Rejects on timeout. */
|
|
1285
1292
|
async function waitForScrollbackMatch(adapter, blockId, pattern, label, timeoutMs, pollInterval = 1e3) {
|
|
1286
1293
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1327,6 +1334,12 @@ async function sendInitialPrompt(adapter, blockId, initialPromptFile) {
|
|
|
1327
1334
|
adapter.closeSocket();
|
|
1328
1335
|
throw new Error("Claude prompt (❯) never appeared — not sending initial prompt. Check that claude started successfully.");
|
|
1329
1336
|
}
|
|
1337
|
+
if (/trustthisfolder|Yes,?Itrustthis|Isthisaproject/i.test(adapter.scrollback(blockId, 40).replace(/\s+/g, ""))) for (let attempt = 0; attempt < 18; attempt++) {
|
|
1338
|
+
const screen = adapter.scrollback(blockId, 14).replace(/\s+/g, "");
|
|
1339
|
+
if (/automode|foragents|Try["'“]/i.test(screen)) break;
|
|
1340
|
+
await adapter.sendInput(blockId, "\r");
|
|
1341
|
+
await sleep(800);
|
|
1342
|
+
}
|
|
1330
1343
|
const prompt = readFileSync(initialPromptFile, "utf-8").trimEnd();
|
|
1331
1344
|
const sentinel = prompt.replace(/\s+/g, "").slice(0, 24);
|
|
1332
1345
|
const landed = () => {
|
|
@@ -1360,9 +1373,57 @@ async function sendInitialPrompt(adapter, blockId, initialPromptFile) {
|
|
|
1360
1373
|
}
|
|
1361
1374
|
consola.warn("Could not confirm the initial prompt was submitted — switch to the tab and press Enter to send it.");
|
|
1362
1375
|
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Auto-advance Claude's "resume" picker that appears when `claude --resume <id>`
|
|
1378
|
+
* reattaches a large/old session:
|
|
1379
|
+
*
|
|
1380
|
+
* ❯ 1. Resume from summary (recommended)
|
|
1381
|
+
* 2. Resume full session as-is
|
|
1382
|
+
* 3. Don't ask me again
|
|
1383
|
+
*
|
|
1384
|
+
* It blocks the tab until you choose, which is why a plain `restore` leaves
|
|
1385
|
+
* such tabs stuck. cctabs always wants the FULL session — the whole point of
|
|
1386
|
+
* restore is to bring the conversation back intact, not a lossy summary — so we
|
|
1387
|
+
* select option 2.
|
|
1388
|
+
*
|
|
1389
|
+
* Like the trust dialog, the picker has a brief not-ready window as it paints,
|
|
1390
|
+
* so we poll for it to appear, settle, then navigate. The default highlight is
|
|
1391
|
+
* option 1, so we move the cursor DOWN exactly once to reach option 2. We send
|
|
1392
|
+
* ↓ only once on purpose: spamming it across retries could land on option 3
|
|
1393
|
+
* ("Don't ask me again"), which permanently changes the user's config. The
|
|
1394
|
+
* confirm is the part we retry — re-pressing Enter on the same row is safe, and
|
|
1395
|
+
* if the single ↓ was ever dropped the worst case is a (still-usable) summary
|
|
1396
|
+
* resume, never option 3.
|
|
1397
|
+
*/
|
|
1398
|
+
async function confirmResumePicker(adapter, blockId) {
|
|
1399
|
+
const stripped = (n) => adapter.scrollback(blockId, n).replace(/\s+/g, "");
|
|
1400
|
+
const pickerVisible = (n) => {
|
|
1401
|
+
const c = stripped(n);
|
|
1402
|
+
return /Resumefromsummary/i.test(c) && /Resumefullsession/i.test(c);
|
|
1403
|
+
};
|
|
1404
|
+
let appeared = false;
|
|
1405
|
+
for (let i = 0; i < 25; i++) {
|
|
1406
|
+
if (pickerVisible(30)) {
|
|
1407
|
+
appeared = true;
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
await sleep(1e3);
|
|
1411
|
+
}
|
|
1412
|
+
if (!appeared) return;
|
|
1413
|
+
await sleep(1200);
|
|
1414
|
+
await adapter.sendInput(blockId, "\x1B[B");
|
|
1415
|
+
await sleep(250);
|
|
1416
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
1417
|
+
await adapter.sendInput(blockId, "\r");
|
|
1418
|
+
await sleep(900);
|
|
1419
|
+
if (!pickerVisible(8)) return;
|
|
1420
|
+
}
|
|
1421
|
+
consola.warn("Could not confirm the resume picker was dismissed — switch to the tab and pick \"Resume full session as-is\".");
|
|
1422
|
+
}
|
|
1363
1423
|
async function openSession(opts) {
|
|
1364
|
-
const { tabName, claudeCmd, workspaceQuery, initialPromptFile, envVars, modelOverride } = opts;
|
|
1424
|
+
const { tabName, claudeCmd, workspaceQuery, initialPromptFile, envVars, modelOverride, afterActive } = opts;
|
|
1365
1425
|
const tailDelayMs = opts.tailDelayMs ?? 2e3;
|
|
1426
|
+
const isResume = /--resume\b/.test(claudeCmd);
|
|
1366
1427
|
const dir = resolve(opts.dir.replace(/^~/, homedir()));
|
|
1367
1428
|
if (!existsSync(dir)) throw new Error(`Directory does not exist: ${dir}`);
|
|
1368
1429
|
const config = loadConfig();
|
|
@@ -1382,7 +1443,7 @@ async function openSession(opts) {
|
|
|
1382
1443
|
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
1383
1444
|
const claudeCore = `claude${extraFlags ? " " + extraFlags : ""} ${claudeCmd.replace(/^claude\s*/, "")}${namePart}${modelPart}`.replace(/\s+/g, " ").trim();
|
|
1384
1445
|
const shell = process.env.SHELL ?? "/bin/zsh";
|
|
1385
|
-
const launch = `${envPrefix}exec ${
|
|
1446
|
+
const launch = `${envPrefix}${claudeCore}; exec ${shell} -l -i`;
|
|
1386
1447
|
const { blockId, tabId } = await adapter.openTabDirect({
|
|
1387
1448
|
cwd: dir,
|
|
1388
1449
|
title: tabName,
|
|
@@ -1392,9 +1453,14 @@ async function openSession(opts) {
|
|
|
1392
1453
|
"-i",
|
|
1393
1454
|
"-c",
|
|
1394
1455
|
launch
|
|
1395
|
-
]
|
|
1456
|
+
],
|
|
1457
|
+
afterActive
|
|
1396
1458
|
});
|
|
1397
1459
|
mark("openTabDirect");
|
|
1460
|
+
if (isResume) {
|
|
1461
|
+
await confirmResumePicker(adapter, blockId);
|
|
1462
|
+
mark("resumePicker");
|
|
1463
|
+
}
|
|
1398
1464
|
if (initialPromptFile) await sendInitialPrompt(adapter, blockId, initialPromptFile);
|
|
1399
1465
|
adapter.closeSocket();
|
|
1400
1466
|
return tabId;
|
|
@@ -1431,6 +1497,10 @@ async function openSession(opts) {
|
|
|
1431
1497
|
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
1432
1498
|
const cmd = `cd ${JSON.stringify(dir)} && ${envPrefix}claude${extraFlags ? " " + extraFlags : ""} ${claudeCmd.replace(/^claude\s*/, "")}${namePart}${modelPart}\r`;
|
|
1433
1499
|
await adapter.sendInput(blockId, cmd);
|
|
1500
|
+
if (isResume) {
|
|
1501
|
+
await confirmResumePicker(adapter, blockId);
|
|
1502
|
+
mark("resumePicker");
|
|
1503
|
+
}
|
|
1434
1504
|
if (initialPromptFile) await sendInitialPrompt(adapter, blockId, initialPromptFile);
|
|
1435
1505
|
if (tailDelayMs > 0) await new Promise((r) => setTimeout(r, tailDelayMs));
|
|
1436
1506
|
mark("tail");
|
|
@@ -1721,7 +1791,8 @@ const newCommand = define({
|
|
|
1721
1791
|
workspaceQuery: workspace,
|
|
1722
1792
|
initialPromptFile,
|
|
1723
1793
|
envVars,
|
|
1724
|
-
modelOverride: resolvedModel
|
|
1794
|
+
modelOverride: resolvedModel,
|
|
1795
|
+
afterActive: true
|
|
1725
1796
|
});
|
|
1726
1797
|
const wt = useWorktree ? ` (worktree: .claude/worktrees/${name})` : "";
|
|
1727
1798
|
const be = backendName ? ` [backend: ${backendName}${resolvedModel ? ` → ${resolvedModel}` : ""}]` : "";
|
|
@@ -1863,13 +1934,14 @@ const resumeCommand = define({
|
|
|
1863
1934
|
dir,
|
|
1864
1935
|
claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}`,
|
|
1865
1936
|
envVars,
|
|
1866
|
-
modelOverride: resolvedModel
|
|
1937
|
+
modelOverride: resolvedModel,
|
|
1938
|
+
afterActive: true
|
|
1867
1939
|
});
|
|
1868
1940
|
consola.success(`Tab "${name}" [${newTabId.slice(0, 8)}] → claude --resume ${sessionId.slice(0, 8)}… at ${dir} (recreated)`);
|
|
1869
1941
|
return;
|
|
1870
1942
|
}
|
|
1871
1943
|
}
|
|
1872
|
-
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
1944
|
+
const extraFlags = loadConfig().claude.flags.map(shellQuoteArg).join(" ");
|
|
1873
1945
|
const envPrefix = envVars ? shellQuoteEnv(envVars) : "";
|
|
1874
1946
|
const modelPart = resolvedModel ? ` --model ${JSON.stringify(resolvedModel)}` : "";
|
|
1875
1947
|
const cmd = `cd ${JSON.stringify(dir)} && ${envPrefix}claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(name)}${modelPart}\r`;
|
|
@@ -1894,7 +1966,8 @@ const resumeCommand = define({
|
|
|
1894
1966
|
dir,
|
|
1895
1967
|
claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}`,
|
|
1896
1968
|
envVars,
|
|
1897
|
-
modelOverride: resolvedModel
|
|
1969
|
+
modelOverride: resolvedModel,
|
|
1970
|
+
afterActive: true
|
|
1898
1971
|
});
|
|
1899
1972
|
consola.success(`Tab "${name}" [${tabId.slice(0, 8)}] → claude --resume ${sessionId.slice(0, 8)}… at ${dir} (new tab)`);
|
|
1900
1973
|
} else {
|
|
@@ -1979,7 +2052,8 @@ const forkCommand = define({
|
|
|
1979
2052
|
const newTabId = await openSession({
|
|
1980
2053
|
tabName: newName,
|
|
1981
2054
|
dir: openDir,
|
|
1982
|
-
claudeCmd: `claude --resume ${sessionId} --fork-session
|
|
2055
|
+
claudeCmd: `claude --resume ${sessionId} --fork-session`,
|
|
2056
|
+
afterActive: true
|
|
1983
2057
|
});
|
|
1984
2058
|
consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}]`);
|
|
1985
2059
|
consola.info(`session: ${sessionId}`);
|
|
@@ -2240,6 +2314,15 @@ const configCommand = define({
|
|
|
2240
2314
|
});
|
|
2241
2315
|
//#endregion
|
|
2242
2316
|
//#region src/commands/restore.ts
|
|
2317
|
+
/**
|
|
2318
|
+
* Settle after each direct-spawn (Tabby) recreate, before creating the next
|
|
2319
|
+
* tab. A freshly created tab spawns its PTY only once it becomes the active
|
|
2320
|
+
* tab, and each new tab steals activation from the previous one — so without
|
|
2321
|
+
* this gap only the last-created tab actually launches Claude. One second is
|
|
2322
|
+
* comfortably longer than a PTY fork + shell exec, while keeping a full restore
|
|
2323
|
+
* snappy.
|
|
2324
|
+
*/
|
|
2325
|
+
const SPAWN_SETTLE_MS = 1e3;
|
|
2243
2326
|
function readStdinSync() {
|
|
2244
2327
|
if (process.stdin.isTTY) return "";
|
|
2245
2328
|
try {
|
|
@@ -2347,7 +2430,7 @@ async function runManifestMode(manifestPath, createMissing, dryRun) {
|
|
|
2347
2430
|
const wsTabIds = currentWsData ? new Set(currentWsData.workspacedata.tabids) : new Set(tabsById.keys());
|
|
2348
2431
|
const results = [];
|
|
2349
2432
|
const toSpawn = [];
|
|
2350
|
-
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
2433
|
+
const extraFlags = loadConfig().claude.flags.map(shellQuoteArg).join(" ");
|
|
2351
2434
|
for (const entry of entries) {
|
|
2352
2435
|
let resolvedSessionId = entry.session_id;
|
|
2353
2436
|
if (entry.session_id) {
|
|
@@ -2486,7 +2569,9 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2486
2569
|
const { tabsById, workspaces, tabNames } = await adapter.getAllData();
|
|
2487
2570
|
const currentTab = adapter.currentTabId();
|
|
2488
2571
|
const tabs = [];
|
|
2572
|
+
const originalOrder = [];
|
|
2489
2573
|
for (const wsp of workspaces) for (const tabId of wsp.workspacedata.tabids) {
|
|
2574
|
+
originalOrder.push(tabId);
|
|
2490
2575
|
if (tabId === currentTab) continue;
|
|
2491
2576
|
const blocks = (tabsById.get(tabId) ?? []).filter((b) => b.view === "term");
|
|
2492
2577
|
if (!blocks.length) continue;
|
|
@@ -2508,7 +2593,7 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2508
2593
|
return;
|
|
2509
2594
|
}
|
|
2510
2595
|
consola.info(`Found ${toResume.length} tab(s) to restore:`);
|
|
2511
|
-
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
2596
|
+
const extraFlags = loadConfig().claude.flags.map(shellQuoteArg).join(" ");
|
|
2512
2597
|
const results = [];
|
|
2513
2598
|
const toRecreate = [];
|
|
2514
2599
|
const resolved = [];
|
|
@@ -2570,8 +2655,20 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2570
2655
|
await Promise.all(unknownTabs.map(async (r) => {
|
|
2571
2656
|
emptyById.set(r.tab.tabId, await adapter.confirmScrollbackEmpty(r.tab.blockId));
|
|
2572
2657
|
}));
|
|
2658
|
+
const claimedNames = /* @__PURE__ */ new Set();
|
|
2573
2659
|
for (const r of resolved) {
|
|
2574
2660
|
const { tab, sessionId, sessionDir } = r;
|
|
2661
|
+
if (claimedNames.has(tab.name)) {
|
|
2662
|
+
const blockIds = (tabsById.get(tab.tabId) ?? []).map((b) => b.blockid);
|
|
2663
|
+
for (const bid of blockIds) adapter.deleteBlock(bid);
|
|
2664
|
+
consola.log(` ${tab.name} — duplicate dead tab, closing (already restoring one)`);
|
|
2665
|
+
results.push({
|
|
2666
|
+
name: tab.name,
|
|
2667
|
+
result: "duplicate dead tab — closed"
|
|
2668
|
+
});
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
claimedNames.add(tab.name);
|
|
2575
2672
|
if (tab.status === "unknown" && emptyById.get(tab.tabId)) {
|
|
2576
2673
|
const blockIds = (tabsById.get(tab.tabId) ?? []).map((b) => b.blockid);
|
|
2577
2674
|
toRecreate.push({
|
|
@@ -2615,6 +2712,7 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2615
2712
|
for (const t of toRecreate) for (const bid of t.blockIds) adapter.deleteBlock(bid);
|
|
2616
2713
|
adapter.closeSocket();
|
|
2617
2714
|
consola.info(`Recreating ${toRecreate.length} dead tab(s)…`);
|
|
2715
|
+
const recreatedIds = /* @__PURE__ */ new Map();
|
|
2618
2716
|
const recreateOne = async (t) => {
|
|
2619
2717
|
try {
|
|
2620
2718
|
const newTabId = await openSession({
|
|
@@ -2623,6 +2721,7 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2623
2721
|
claudeCmd: `claude --resume ${t.sessionId} --name ${JSON.stringify(t.name)}`,
|
|
2624
2722
|
tailDelayMs: 500
|
|
2625
2723
|
});
|
|
2724
|
+
recreatedIds.set(t.tabId, newTabId);
|
|
2626
2725
|
const r = results.find((x) => x.name === t.name);
|
|
2627
2726
|
r.result = `✔ recreated [${newTabId.slice(0, 8)}]`;
|
|
2628
2727
|
} catch (err) {
|
|
@@ -2630,8 +2729,19 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2630
2729
|
r.result = `✘ recreate failed: ${err.message}`;
|
|
2631
2730
|
}
|
|
2632
2731
|
};
|
|
2633
|
-
|
|
2634
|
-
|
|
2732
|
+
const usesDirectSpawn = typeof adapter.openTabDirect === "function";
|
|
2733
|
+
for (const t of toRecreate) {
|
|
2734
|
+
await recreateOne(t);
|
|
2735
|
+
if (usesDirectSpawn) await new Promise((r) => setTimeout(r, SPAWN_SETTLE_MS));
|
|
2736
|
+
}
|
|
2737
|
+
if (recreatedIds.size && typeof adapter.reorderTabs === "function") {
|
|
2738
|
+
const desiredOrder = originalOrder.map((id) => recreatedIds.get(id) ?? id);
|
|
2739
|
+
try {
|
|
2740
|
+
await adapter.reorderTabs(desiredOrder);
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
consola.warn(`Could not restore tab order: ${err.message}`);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2635
2745
|
} else adapter.closeSocket();
|
|
2636
2746
|
console.log("\nRestore summary:");
|
|
2637
2747
|
for (const r of results) console.log(` ${r.name}: ${r.result}`);
|
|
@@ -3372,7 +3482,7 @@ const importCommand = define({
|
|
|
3372
3482
|
mkdirSync(targetProjectDir, { recursive: true });
|
|
3373
3483
|
copyFileSync(srcJsonl, targetJsonl);
|
|
3374
3484
|
const config = loadConfig();
|
|
3375
|
-
const claudeCmd = `claude${config.claude.flags.length ? " " + config.claude.flags.join(" ") : ""} --resume ${entry.sessionId} --name ${JSON.stringify(entry.name)}`;
|
|
3485
|
+
const claudeCmd = `claude${config.claude.flags.length ? " " + config.claude.flags.map(shellQuoteArg).join(" ") : ""} --resume ${entry.sessionId} --name ${JSON.stringify(entry.name)}`;
|
|
3376
3486
|
try {
|
|
3377
3487
|
await openSession({
|
|
3378
3488
|
tabName: entry.name,
|
package/package.json
CHANGED
package/skills/cctabs/SKILL.md
CHANGED
|
@@ -281,6 +281,18 @@ cctabs restore ~/Dev/myapp # restrict the search to one project dir
|
|
|
281
281
|
|
|
282
282
|
If a session was started in a different `cwd` than the tab's current directory (common after `cd`-ing inside the tab), the global search still finds it via the recorded session metadata — no need to guess the right dir.
|
|
283
283
|
|
|
284
|
+
### The "Resume from summary / full session" picker
|
|
285
|
+
|
|
286
|
+
When `claude --resume` reattaches a large or old session, Claude first shows a blocking picker:
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
❯ 1. Resume from summary (recommended)
|
|
290
|
+
2. Resume full session as-is
|
|
291
|
+
3. Don't ask me again
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Always pick option 2, "Resume full session as-is."** The point of `restore` is to bring the conversation back intact — resuming from a summary discards the live context you're restoring for. `restore` auto-advances this picker for you (it moves down once to option 2 and confirms), so you normally never see it. If you ever do drive it manually (e.g. sending keys to a tab), send **↓ then Enter** — never the bare Enter that would accept the summary, and never option 3, which permanently silences the prompt in that session's config.
|
|
295
|
+
|
|
284
296
|
## Workflow: Moving sessions across machines
|
|
285
297
|
|
|
286
298
|
Use `export` + `import` to migrate a tab (or a whole workspace) — and its underlying Claude conversation — from one machine to another, e.g. when switching laptops or sharing a debug session with a teammate.
|