@generativereality/cctabs 0.4.1 → 0.4.3
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 +81 -37
- 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.3",
|
|
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.3";
|
|
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
|
/**
|
|
@@ -1269,6 +1269,18 @@ function shellQuoteEnv$1(env) {
|
|
|
1269
1269
|
if (!entries.length) return "";
|
|
1270
1270
|
return entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") + " ";
|
|
1271
1271
|
}
|
|
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
|
+
}
|
|
1272
1284
|
/** Poll scrollback until a pattern is visible, then return. Rejects on timeout. */
|
|
1273
1285
|
async function waitForScrollbackMatch(adapter, blockId, pattern, label, timeoutMs, pollInterval = 1e3) {
|
|
1274
1286
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1282,21 +1294,31 @@ async function waitForScrollbackMatch(adapter, blockId, pattern, label, timeoutM
|
|
|
1282
1294
|
}
|
|
1283
1295
|
throw new Error(`Timed out waiting for ${label}`);
|
|
1284
1296
|
}
|
|
1297
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1285
1298
|
/**
|
|
1286
1299
|
* Wait for Claude's input prompt, then send the initial task and reliably
|
|
1287
1300
|
* submit it.
|
|
1288
1301
|
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1302
|
+
* The naive "send text, then send \r" is unreliable for two distinct reasons,
|
|
1303
|
+
* each handled by its own verify-and-retry stage:
|
|
1304
|
+
*
|
|
1305
|
+
* 1. The paste can be *dropped*. The welcome-screen placeholder (`❯ Try "…"`)
|
|
1306
|
+
* renders several seconds before the input handler is fully attached, so
|
|
1307
|
+
* input sent the instant `❯` appears lands in a not-ready terminal and is
|
|
1308
|
+
* silently lost — the box stays empty and a later Enter does nothing.
|
|
1309
|
+
* Fix: after pasting, confirm the text actually landed in the box and
|
|
1310
|
+
* re-paste if not (clearing first so a retry never duplicates).
|
|
1311
|
+
*
|
|
1312
|
+
* 2. The Enter can be *swallowed*. A burst of "text then \r" is seen as one
|
|
1313
|
+
* bracketed paste and the \r is absorbed as a newline rather than a
|
|
1314
|
+
* submit. Fix: wrap the prompt in explicit bracketed-paste markers so the
|
|
1315
|
+
* Enter lands outside the paste, then confirm a turn actually started
|
|
1316
|
+
* (spinner / "esc to interrupt" appear only while Claude is processing)
|
|
1317
|
+
* and re-send Enter a few times if not.
|
|
1318
|
+
*
|
|
1319
|
+
* Landed-detection handles both render shapes observed empirically: a short
|
|
1320
|
+
* prompt is echoed inline (so a distinctive prompt substring appears), while a
|
|
1321
|
+
* long / multi-line prompt collapses to a "[Pasted text #N +M lines]" chip.
|
|
1300
1322
|
*/
|
|
1301
1323
|
async function sendInitialPrompt(adapter, blockId, initialPromptFile) {
|
|
1302
1324
|
try {
|
|
@@ -1306,16 +1328,37 @@ async function sendInitialPrompt(adapter, blockId, initialPromptFile) {
|
|
|
1306
1328
|
throw new Error("Claude prompt (❯) never appeared — not sending initial prompt. Check that claude started successfully.");
|
|
1307
1329
|
}
|
|
1308
1330
|
const prompt = readFileSync(initialPromptFile, "utf-8").trimEnd();
|
|
1309
|
-
|
|
1331
|
+
const sentinel = prompt.replace(/\s+/g, "").slice(0, 24);
|
|
1332
|
+
const landed = () => {
|
|
1333
|
+
const c = adapter.scrollback(blockId, 60).replace(/\s+/g, "");
|
|
1334
|
+
return sentinel.length >= 4 && c.includes(sentinel) || c.includes("[Pastedtext");
|
|
1335
|
+
};
|
|
1336
|
+
let inBox = false;
|
|
1337
|
+
for (let attempt = 0; attempt < 3 && !inBox; attempt++) {
|
|
1338
|
+
if (attempt > 0) {
|
|
1339
|
+
await adapter.sendInput(blockId, "");
|
|
1340
|
+
await sleep(200);
|
|
1341
|
+
}
|
|
1342
|
+
await adapter.sendInput(blockId, `\x1b[200~${prompt}\x1b[201~`);
|
|
1343
|
+
for (let i = 0; i < 8; i++) {
|
|
1344
|
+
await sleep(300);
|
|
1345
|
+
if (landed()) {
|
|
1346
|
+
inBox = true;
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (!inBox) {
|
|
1352
|
+
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).");
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1310
1355
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1311
|
-
await new Promise((r) => setTimeout(r, attempt === 0 ? 300 : 500));
|
|
1312
1356
|
await adapter.sendInput(blockId, "\r");
|
|
1313
|
-
await
|
|
1357
|
+
await sleep(500);
|
|
1314
1358
|
const tail = adapter.scrollback(blockId, 40);
|
|
1315
|
-
|
|
1316
|
-
if (/[✻✽✶✳✢]/.test(tail) || /esctointerrupt/i.test(compact)) return;
|
|
1359
|
+
if (/[✻✽✶✳✢]/.test(tail) || /esctointerrupt/i.test(tail.replace(/\s+/g, ""))) return;
|
|
1317
1360
|
}
|
|
1318
|
-
consola.warn("Could not confirm the initial prompt was submitted —
|
|
1361
|
+
consola.warn("Could not confirm the initial prompt was submitted — switch to the tab and press Enter to send it.");
|
|
1319
1362
|
}
|
|
1320
1363
|
async function openSession(opts) {
|
|
1321
1364
|
const { tabName, claudeCmd, workspaceQuery, initialPromptFile, envVars, modelOverride } = opts;
|
|
@@ -1333,7 +1376,7 @@ async function openSession(opts) {
|
|
|
1333
1376
|
tPhase = now;
|
|
1334
1377
|
};
|
|
1335
1378
|
if (adapter.openTabDirect) {
|
|
1336
|
-
const extraFlags = config.claude.flags.join(" ");
|
|
1379
|
+
const extraFlags = config.claude.flags.map(shellQuoteArg).join(" ");
|
|
1337
1380
|
const namePart = claudeCmd.includes("--resume") ? "" : ` --name ${JSON.stringify(tabName)}`;
|
|
1338
1381
|
const modelPart = modelOverride ? ` --model ${JSON.stringify(modelOverride)}` : "";
|
|
1339
1382
|
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
@@ -1346,6 +1389,7 @@ async function openSession(opts) {
|
|
|
1346
1389
|
command: shell,
|
|
1347
1390
|
args: [
|
|
1348
1391
|
"-l",
|
|
1392
|
+
"-i",
|
|
1349
1393
|
"-c",
|
|
1350
1394
|
launch
|
|
1351
1395
|
]
|
|
@@ -1381,7 +1425,7 @@ async function openSession(opts) {
|
|
|
1381
1425
|
throw new Error("Shell prompt never appeared in new tab — aborting. Check your shell profile (e.g. nvm default alias).");
|
|
1382
1426
|
}
|
|
1383
1427
|
mark("shellPrompt");
|
|
1384
|
-
const extraFlags = config.claude.flags.join(" ");
|
|
1428
|
+
const extraFlags = config.claude.flags.map(shellQuoteArg).join(" ");
|
|
1385
1429
|
const namePart = claudeCmd.includes("--resume") ? "" : ` --name ${JSON.stringify(tabName)}`;
|
|
1386
1430
|
const modelPart = modelOverride ? ` --model ${JSON.stringify(modelOverride)}` : "";
|
|
1387
1431
|
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
@@ -2698,18 +2742,18 @@ function checkTabbyPlugin() {
|
|
|
2698
2742
|
}
|
|
2699
2743
|
/**
|
|
2700
2744
|
* Probe whether `node` is findable in a freshly spawned shell — the canonical
|
|
2701
|
-
* symptom of the macOS
|
|
2702
|
-
* node'` simulates the same login
|
|
2703
|
-
* that cctabs
|
|
2704
|
-
* tabs will also fail to find Node, every plugin MCP
|
|
2705
|
-
* will ENOENT, and the cctabs CLI itself becomes
|
|
2706
|
-
* tabs (chicken-and-egg). The
|
|
2707
|
-
*
|
|
2708
|
-
* workaround for users on older versions or non-Tabby terminals.
|
|
2745
|
+
* symptom of the macOS PATH-sourcing bug. Spawning `zsh -l -i -c 'command -v
|
|
2746
|
+
* node'` simulates the same login + interactive shell init (/etc/zprofile →
|
|
2747
|
+
* path_helper, then ~/.zshrc) that cctabs uses when it opens new Tabby tabs.
|
|
2748
|
+
* If this fails, brand-new tabs will also fail to find Node, every plugin MCP
|
|
2749
|
+
* that shells out to npx will ENOENT, and the cctabs CLI itself becomes
|
|
2750
|
+
* unusable from inside those tabs (chicken-and-egg). The flags must match
|
|
2751
|
+
* open-session.ts to keep the doctor honest.
|
|
2709
2752
|
*/
|
|
2710
2753
|
function checkSpawnedShellPath() {
|
|
2711
2754
|
const r = spawnSync("zsh", [
|
|
2712
2755
|
"-l",
|
|
2756
|
+
"-i",
|
|
2713
2757
|
"-c",
|
|
2714
2758
|
"command -v node"
|
|
2715
2759
|
], {
|
|
@@ -2724,8 +2768,8 @@ function checkSpawnedShellPath() {
|
|
|
2724
2768
|
return {
|
|
2725
2769
|
name: "Spawned shell PATH",
|
|
2726
2770
|
status: "warn",
|
|
2727
|
-
detail: r.error?.message ?? r.stderr?.trim() ?? "node not found in a login zsh",
|
|
2728
|
-
hint: "A login zsh cannot find `node`. Either node is not installed, or PATH is broken.
|
|
2771
|
+
detail: r.error?.message ?? r.stderr?.trim() ?? "node not found in a login+interactive zsh",
|
|
2772
|
+
hint: "A login+interactive zsh cannot find `node`. Either node is not installed, or PATH is broken. cctabs spawns tabs with `zsh -l -i -c` so both ~/.zprofile and ~/.zshrc are sourced — if your PATH-extending logic lives elsewhere (e.g. a sourced file that bails on non-interactive), move the `export PATH=...` lines into ~/.zshenv as a belt-and-braces fix."
|
|
2729
2773
|
};
|
|
2730
2774
|
}
|
|
2731
2775
|
function checkWaveDb() {
|