@generativereality/cctabs 0.4.2 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +97 -34
- package/package.json +1 -1
|
@@ -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.4",
|
|
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.4";
|
|
15
15
|
var description = "Claude Code tab manager. Terminal tabs as the UI, no tmux.";
|
|
16
16
|
var package_default = {
|
|
17
17
|
name,
|
|
@@ -335,7 +335,7 @@ var WaveAdapter = class {
|
|
|
335
335
|
async confirmScrollbackEmpty(blockId, attempts = 3, intervalMs = 500) {
|
|
336
336
|
for (let i = 0; i < attempts; i++) {
|
|
337
337
|
if (this.scrollback(blockId, 10).trim()) return false;
|
|
338
|
-
if (i < attempts - 1) await sleep$
|
|
338
|
+
if (i < attempts - 1) await sleep$2(intervalMs);
|
|
339
339
|
}
|
|
340
340
|
return true;
|
|
341
341
|
}
|
|
@@ -365,7 +365,7 @@ var WaveAdapter = class {
|
|
|
365
365
|
async newTab(focusWindowId) {
|
|
366
366
|
if (focusWindowId) {
|
|
367
367
|
await this.focusWindow(focusWindowId);
|
|
368
|
-
await sleep$
|
|
368
|
+
await sleep$2(300);
|
|
369
369
|
}
|
|
370
370
|
const r = spawnSync("osascript", ["-e", [
|
|
371
371
|
"tell application \"Wave\" to activate",
|
|
@@ -381,7 +381,7 @@ var WaveAdapter = class {
|
|
|
381
381
|
async waitForNewBlock(beforeIds, timeoutMs = 15e3) {
|
|
382
382
|
const deadline = Date.now() + timeoutMs;
|
|
383
383
|
while (Date.now() < deadline) {
|
|
384
|
-
await sleep$
|
|
384
|
+
await sleep$2(250);
|
|
385
385
|
for (const b of this.blocksList()) if (b.view === "term" && !beforeIds.has(b.blockid)) return {
|
|
386
386
|
blockId: b.blockid,
|
|
387
387
|
tabId: b.tabid
|
|
@@ -509,7 +509,7 @@ var WaveAdapter = class {
|
|
|
509
509
|
}));
|
|
510
510
|
}
|
|
511
511
|
};
|
|
512
|
-
function sleep$
|
|
512
|
+
function sleep$2(ms) {
|
|
513
513
|
return new Promise((r) => setTimeout(r, ms));
|
|
514
514
|
}
|
|
515
515
|
//#endregion
|
|
@@ -636,7 +636,7 @@ var TabbyAdapter = class {
|
|
|
636
636
|
async confirmScrollbackEmpty(blockId, attempts = 3, intervalMs = 500) {
|
|
637
637
|
for (let i = 0; i < attempts; i++) {
|
|
638
638
|
if (this.scrollback(blockId, 10).trim()) return false;
|
|
639
|
-
if (i < attempts - 1) await sleep(intervalMs);
|
|
639
|
+
if (i < attempts - 1) await sleep$1(intervalMs);
|
|
640
640
|
}
|
|
641
641
|
return true;
|
|
642
642
|
}
|
|
@@ -685,7 +685,7 @@ var TabbyAdapter = class {
|
|
|
685
685
|
async waitForNewBlock(beforeIds, timeoutMs = 15e3) {
|
|
686
686
|
const deadline = Date.now() + timeoutMs;
|
|
687
687
|
while (Date.now() < deadline) {
|
|
688
|
-
await sleep(250);
|
|
688
|
+
await sleep$1(250);
|
|
689
689
|
for (const b of this.blocksList()) if (!beforeIds.has(b.blockid)) return {
|
|
690
690
|
blockId: b.blockid,
|
|
691
691
|
tabId: b.tabid
|
|
@@ -816,7 +816,7 @@ var TabbyAdapter = class {
|
|
|
816
816
|
}
|
|
817
817
|
}
|
|
818
818
|
};
|
|
819
|
-
function sleep(ms) {
|
|
819
|
+
function sleep$1(ms) {
|
|
820
820
|
return new Promise((r) => setTimeout(r, ms));
|
|
821
821
|
}
|
|
822
822
|
/**
|
|
@@ -1263,6 +1263,20 @@ function ensureConfigExists() {
|
|
|
1263
1263
|
return CONFIG_PATH;
|
|
1264
1264
|
}
|
|
1265
1265
|
//#endregion
|
|
1266
|
+
//#region src/core/shell.ts
|
|
1267
|
+
/**
|
|
1268
|
+
* POSIX single-quote escape one argv token. Several commands join the
|
|
1269
|
+
* configured `claude.flags` into a raw shell string and send it as terminal
|
|
1270
|
+
* input, so any value with shell metacharacters must be quoted or the shell
|
|
1271
|
+
* mangles it before `claude` sees it — e.g. a `--model opus[1m]` flag
|
|
1272
|
+
* glob-expands under zsh ("no matches found: opus[1m]") and the launch
|
|
1273
|
+
* silently falls back to the default model. Single quotes are inert in every
|
|
1274
|
+
* POSIX shell; embedded single quotes are closed, escaped, and reopened ('\'').
|
|
1275
|
+
*/
|
|
1276
|
+
function shellQuoteArg(arg) {
|
|
1277
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
1278
|
+
}
|
|
1279
|
+
//#endregion
|
|
1266
1280
|
//#region src/core/open-session.ts
|
|
1267
1281
|
function shellQuoteEnv$1(env) {
|
|
1268
1282
|
const entries = Object.entries(env);
|
|
@@ -1282,21 +1296,31 @@ async function waitForScrollbackMatch(adapter, blockId, pattern, label, timeoutM
|
|
|
1282
1296
|
}
|
|
1283
1297
|
throw new Error(`Timed out waiting for ${label}`);
|
|
1284
1298
|
}
|
|
1299
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1285
1300
|
/**
|
|
1286
1301
|
* Wait for Claude's input prompt, then send the initial task and reliably
|
|
1287
1302
|
* submit it.
|
|
1288
1303
|
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1304
|
+
* The naive "send text, then send \r" is unreliable for two distinct reasons,
|
|
1305
|
+
* each handled by its own verify-and-retry stage:
|
|
1306
|
+
*
|
|
1307
|
+
* 1. The paste can be *dropped*. The welcome-screen placeholder (`❯ Try "…"`)
|
|
1308
|
+
* renders several seconds before the input handler is fully attached, so
|
|
1309
|
+
* input sent the instant `❯` appears lands in a not-ready terminal and is
|
|
1310
|
+
* silently lost — the box stays empty and a later Enter does nothing.
|
|
1311
|
+
* Fix: after pasting, confirm the text actually landed in the box and
|
|
1312
|
+
* re-paste if not (clearing first so a retry never duplicates).
|
|
1313
|
+
*
|
|
1314
|
+
* 2. The Enter can be *swallowed*. A burst of "text then \r" is seen as one
|
|
1315
|
+
* bracketed paste and the \r is absorbed as a newline rather than a
|
|
1316
|
+
* submit. Fix: wrap the prompt in explicit bracketed-paste markers so the
|
|
1317
|
+
* Enter lands outside the paste, then confirm a turn actually started
|
|
1318
|
+
* (spinner / "esc to interrupt" appear only while Claude is processing)
|
|
1319
|
+
* and re-send Enter a few times if not.
|
|
1320
|
+
*
|
|
1321
|
+
* Landed-detection handles both render shapes observed empirically: a short
|
|
1322
|
+
* prompt is echoed inline (so a distinctive prompt substring appears), while a
|
|
1323
|
+
* long / multi-line prompt collapses to a "[Pasted text #N +M lines]" chip.
|
|
1300
1324
|
*/
|
|
1301
1325
|
async function sendInitialPrompt(adapter, blockId, initialPromptFile) {
|
|
1302
1326
|
try {
|
|
@@ -1305,17 +1329,44 @@ async function sendInitialPrompt(adapter, blockId, initialPromptFile) {
|
|
|
1305
1329
|
adapter.closeSocket();
|
|
1306
1330
|
throw new Error("Claude prompt (❯) never appeared — not sending initial prompt. Check that claude started successfully.");
|
|
1307
1331
|
}
|
|
1332
|
+
if (/trustthisfolder|Yes,?Itrustthis|Isthisaproject/i.test(adapter.scrollback(blockId, 40).replace(/\s+/g, ""))) for (let attempt = 0; attempt < 18; attempt++) {
|
|
1333
|
+
const screen = adapter.scrollback(blockId, 14).replace(/\s+/g, "");
|
|
1334
|
+
if (/automode|foragents|Try["'“]/i.test(screen)) break;
|
|
1335
|
+
await adapter.sendInput(blockId, "\r");
|
|
1336
|
+
await sleep(800);
|
|
1337
|
+
}
|
|
1308
1338
|
const prompt = readFileSync(initialPromptFile, "utf-8").trimEnd();
|
|
1309
|
-
|
|
1339
|
+
const sentinel = prompt.replace(/\s+/g, "").slice(0, 24);
|
|
1340
|
+
const landed = () => {
|
|
1341
|
+
const c = adapter.scrollback(blockId, 60).replace(/\s+/g, "");
|
|
1342
|
+
return sentinel.length >= 4 && c.includes(sentinel) || c.includes("[Pastedtext");
|
|
1343
|
+
};
|
|
1344
|
+
let inBox = false;
|
|
1345
|
+
for (let attempt = 0; attempt < 3 && !inBox; attempt++) {
|
|
1346
|
+
if (attempt > 0) {
|
|
1347
|
+
await adapter.sendInput(blockId, "");
|
|
1348
|
+
await sleep(200);
|
|
1349
|
+
}
|
|
1350
|
+
await adapter.sendInput(blockId, `\x1b[200~${prompt}\x1b[201~`);
|
|
1351
|
+
for (let i = 0; i < 8; i++) {
|
|
1352
|
+
await sleep(300);
|
|
1353
|
+
if (landed()) {
|
|
1354
|
+
inBox = true;
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!inBox) {
|
|
1360
|
+
consola.warn("Initial prompt may not have landed in the input box — switch to the tab and press Enter (re-type if the box is empty).");
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1310
1363
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1311
|
-
await new Promise((r) => setTimeout(r, attempt === 0 ? 300 : 500));
|
|
1312
1364
|
await adapter.sendInput(blockId, "\r");
|
|
1313
|
-
await
|
|
1365
|
+
await sleep(500);
|
|
1314
1366
|
const tail = adapter.scrollback(blockId, 40);
|
|
1315
|
-
|
|
1316
|
-
if (/[✻✽✶✳✢]/.test(tail) || /esctointerrupt/i.test(compact)) return;
|
|
1367
|
+
if (/[✻✽✶✳✢]/.test(tail) || /esctointerrupt/i.test(tail.replace(/\s+/g, ""))) return;
|
|
1317
1368
|
}
|
|
1318
|
-
consola.warn("Could not confirm the initial prompt was submitted —
|
|
1369
|
+
consola.warn("Could not confirm the initial prompt was submitted — switch to the tab and press Enter to send it.");
|
|
1319
1370
|
}
|
|
1320
1371
|
async function openSession(opts) {
|
|
1321
1372
|
const { tabName, claudeCmd, workspaceQuery, initialPromptFile, envVars, modelOverride } = opts;
|
|
@@ -1333,13 +1384,13 @@ async function openSession(opts) {
|
|
|
1333
1384
|
tPhase = now;
|
|
1334
1385
|
};
|
|
1335
1386
|
if (adapter.openTabDirect) {
|
|
1336
|
-
const extraFlags = config.claude.flags.join(" ");
|
|
1387
|
+
const extraFlags = config.claude.flags.map(shellQuoteArg).join(" ");
|
|
1337
1388
|
const namePart = claudeCmd.includes("--resume") ? "" : ` --name ${JSON.stringify(tabName)}`;
|
|
1338
1389
|
const modelPart = modelOverride ? ` --model ${JSON.stringify(modelOverride)}` : "";
|
|
1339
1390
|
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
1340
1391
|
const claudeCore = `claude${extraFlags ? " " + extraFlags : ""} ${claudeCmd.replace(/^claude\s*/, "")}${namePart}${modelPart}`.replace(/\s+/g, " ").trim();
|
|
1341
1392
|
const shell = process.env.SHELL ?? "/bin/zsh";
|
|
1342
|
-
const launch = `${envPrefix}exec ${
|
|
1393
|
+
const launch = `${envPrefix}${claudeCore}; exec ${shell} -l -i`;
|
|
1343
1394
|
const { blockId, tabId } = await adapter.openTabDirect({
|
|
1344
1395
|
cwd: dir,
|
|
1345
1396
|
title: tabName,
|
|
@@ -1382,7 +1433,7 @@ async function openSession(opts) {
|
|
|
1382
1433
|
throw new Error("Shell prompt never appeared in new tab — aborting. Check your shell profile (e.g. nvm default alias).");
|
|
1383
1434
|
}
|
|
1384
1435
|
mark("shellPrompt");
|
|
1385
|
-
const extraFlags = config.claude.flags.join(" ");
|
|
1436
|
+
const extraFlags = config.claude.flags.map(shellQuoteArg).join(" ");
|
|
1386
1437
|
const namePart = claudeCmd.includes("--resume") ? "" : ` --name ${JSON.stringify(tabName)}`;
|
|
1387
1438
|
const modelPart = modelOverride ? ` --model ${JSON.stringify(modelOverride)}` : "";
|
|
1388
1439
|
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
@@ -1826,7 +1877,7 @@ const resumeCommand = define({
|
|
|
1826
1877
|
return;
|
|
1827
1878
|
}
|
|
1828
1879
|
}
|
|
1829
|
-
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
1880
|
+
const extraFlags = loadConfig().claude.flags.map(shellQuoteArg).join(" ");
|
|
1830
1881
|
const envPrefix = envVars ? shellQuoteEnv(envVars) : "";
|
|
1831
1882
|
const modelPart = resolvedModel ? ` --model ${JSON.stringify(resolvedModel)}` : "";
|
|
1832
1883
|
const cmd = `cd ${JSON.stringify(dir)} && ${envPrefix}claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(name)}${modelPart}\r`;
|
|
@@ -2197,6 +2248,15 @@ const configCommand = define({
|
|
|
2197
2248
|
});
|
|
2198
2249
|
//#endregion
|
|
2199
2250
|
//#region src/commands/restore.ts
|
|
2251
|
+
/**
|
|
2252
|
+
* Settle after each direct-spawn (Tabby) recreate, before creating the next
|
|
2253
|
+
* tab. A freshly created tab spawns its PTY only once it becomes the active
|
|
2254
|
+
* tab, and each new tab steals activation from the previous one — so without
|
|
2255
|
+
* this gap only the last-created tab actually launches Claude. One second is
|
|
2256
|
+
* comfortably longer than a PTY fork + shell exec, while keeping a full restore
|
|
2257
|
+
* snappy.
|
|
2258
|
+
*/
|
|
2259
|
+
const SPAWN_SETTLE_MS = 1e3;
|
|
2200
2260
|
function readStdinSync() {
|
|
2201
2261
|
if (process.stdin.isTTY) return "";
|
|
2202
2262
|
try {
|
|
@@ -2304,7 +2364,7 @@ async function runManifestMode(manifestPath, createMissing, dryRun) {
|
|
|
2304
2364
|
const wsTabIds = currentWsData ? new Set(currentWsData.workspacedata.tabids) : new Set(tabsById.keys());
|
|
2305
2365
|
const results = [];
|
|
2306
2366
|
const toSpawn = [];
|
|
2307
|
-
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
2367
|
+
const extraFlags = loadConfig().claude.flags.map(shellQuoteArg).join(" ");
|
|
2308
2368
|
for (const entry of entries) {
|
|
2309
2369
|
let resolvedSessionId = entry.session_id;
|
|
2310
2370
|
if (entry.session_id) {
|
|
@@ -2465,7 +2525,7 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2465
2525
|
return;
|
|
2466
2526
|
}
|
|
2467
2527
|
consola.info(`Found ${toResume.length} tab(s) to restore:`);
|
|
2468
|
-
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
2528
|
+
const extraFlags = loadConfig().claude.flags.map(shellQuoteArg).join(" ");
|
|
2469
2529
|
const results = [];
|
|
2470
2530
|
const toRecreate = [];
|
|
2471
2531
|
const resolved = [];
|
|
@@ -2587,8 +2647,11 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2587
2647
|
r.result = `✘ recreate failed: ${err.message}`;
|
|
2588
2648
|
}
|
|
2589
2649
|
};
|
|
2590
|
-
|
|
2591
|
-
|
|
2650
|
+
const usesDirectSpawn = typeof adapter.openTabDirect === "function";
|
|
2651
|
+
for (const t of toRecreate) {
|
|
2652
|
+
await recreateOne(t);
|
|
2653
|
+
if (usesDirectSpawn) await new Promise((r) => setTimeout(r, SPAWN_SETTLE_MS));
|
|
2654
|
+
}
|
|
2592
2655
|
} else adapter.closeSocket();
|
|
2593
2656
|
console.log("\nRestore summary:");
|
|
2594
2657
|
for (const r of results) console.log(` ${r.name}: ${r.result}`);
|
|
@@ -3329,7 +3392,7 @@ const importCommand = define({
|
|
|
3329
3392
|
mkdirSync(targetProjectDir, { recursive: true });
|
|
3330
3393
|
copyFileSync(srcJsonl, targetJsonl);
|
|
3331
3394
|
const config = loadConfig();
|
|
3332
|
-
const claudeCmd = `claude${config.claude.flags.length ? " " + config.claude.flags.join(" ") : ""} --resume ${entry.sessionId} --name ${JSON.stringify(entry.name)}`;
|
|
3395
|
+
const claudeCmd = `claude${config.claude.flags.length ? " " + config.claude.flags.map(shellQuoteArg).join(" ") : ""} --resume ${entry.sessionId} --name ${JSON.stringify(entry.name)}`;
|
|
3333
3396
|
try {
|
|
3334
3397
|
await openSession({
|
|
3335
3398
|
tabName: entry.name,
|