@generativereality/cctabs 0.1.1 → 0.1.2

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.
@@ -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.1.1",
4
+ "version": "0.1.2",
5
5
  "author": {
6
6
  "name": "generativereality",
7
7
  "url": "https://cctabs.com"
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { consola } from "consola";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
11
11
  //#region package.json
12
12
  var name = "@generativereality/cctabs";
13
- var version = "0.1.1";
13
+ var version = "0.1.2";
14
14
  var description = "Claude Code tab manager. Terminal tabs as the UI, no tmux.";
15
15
  var package_default = {
16
16
  name,
@@ -28,10 +28,10 @@ var package_default = {
28
28
  "build": "tsdown",
29
29
  "typecheck": "tsc --noEmit",
30
30
  "lint": "eslint src/",
31
- "check": "bun run typecheck && bun run build",
31
+ "check": "npm run typecheck && npm run build",
32
32
  "release": "bumpp && npm publish",
33
33
  "sync-plugin": "bash scripts/sync-plugin.sh",
34
- "prepack": "bash scripts/sync-plugin.sh --check && bun run build"
34
+ "prepack": "bash scripts/sync-plugin.sh --check && npm run build"
35
35
  },
36
36
  keywords: [
37
37
  "claude-code",
@@ -206,7 +206,9 @@ var WaveAdapter = class {
206
206
  const out = execFileSync("wsh", [
207
207
  "blocks",
208
208
  "list",
209
- "--json"
209
+ "--json",
210
+ "--timeout",
211
+ "15000"
210
212
  ], { encoding: "utf-8" });
211
213
  return JSON.parse(out);
212
214
  } catch {
@@ -222,6 +224,16 @@ var WaveAdapter = class {
222
224
  `-${lastN}`
223
225
  ], { encoding: "utf-8" }).stdout ?? "";
224
226
  }
227
+ /** Poll scrollback to confirm it really is empty (block has no live shell).
228
+ * A freshly-opened tab may briefly have empty scrollback before the prompt
229
+ * renders, so poll a few times before declaring the block dead. */
230
+ async confirmScrollbackEmpty(blockId, attempts = 3, intervalMs = 500) {
231
+ for (let i = 0; i < attempts; i++) {
232
+ if (this.scrollback(blockId, 10).trim()) return false;
233
+ if (i < attempts - 1) await sleep(intervalMs);
234
+ }
235
+ return true;
236
+ }
225
237
  /** Detect whether a Claude session is running in a terminal block */
226
238
  detectSessionStatus(blockId) {
227
239
  const tail = this.scrollback(blockId, 10);
@@ -261,7 +273,7 @@ var WaveAdapter = class {
261
273
  }
262
274
  return true;
263
275
  }
264
- async waitForNewBlock(beforeIds, timeoutMs = 5e3) {
276
+ async waitForNewBlock(beforeIds, timeoutMs = 15e3) {
265
277
  const deadline = Date.now() + timeoutMs;
266
278
  while (Date.now() < deadline) {
267
279
  await sleep(250);
@@ -343,9 +355,12 @@ var WaveAdapter = class {
343
355
  }
344
356
  resolveTab(query, tabsById, tabNames) {
345
357
  const q = query.toLowerCase();
346
- return [...tabsById.keys()].filter((tid) => {
358
+ const ids = [...tabsById.keys()];
359
+ const exact = ids.filter((tid) => (tabNames.get(tid) ?? "").toLowerCase() === q);
360
+ if (exact.length > 0) return exact;
361
+ return ids.filter((tid) => {
347
362
  const name = tabNames.get(tid) ?? "";
348
- return name.toLowerCase() === q || tid.startsWith(query) || name.toLowerCase().startsWith(q);
363
+ return tid.startsWith(query) || name.toLowerCase().startsWith(q);
349
364
  });
350
365
  }
351
366
  resolveBlock(query, blocks) {
@@ -353,10 +368,11 @@ var WaveAdapter = class {
353
368
  }
354
369
  resolveWorkspace(workspaces, query) {
355
370
  const q = query.toLowerCase();
356
- return workspaces.filter(({ workspacedata: wd }) => {
371
+ const exact = workspaces.filter(({ workspacedata: wd }) => (wd.name ?? "").toLowerCase() === q);
372
+ return (exact.length > 0 ? exact : workspaces.filter(({ workspacedata: wd }) => {
357
373
  const name = wd.name ?? "";
358
- return name.toLowerCase() === q || wd.oid.startsWith(query) || name.toLowerCase().startsWith(q);
359
- }).map((w) => ({
374
+ return wd.oid.startsWith(query) || name.toLowerCase().startsWith(q);
375
+ })).map((w) => ({
360
376
  data: w.workspacedata,
361
377
  windowId: w.windowid
362
378
  }));
@@ -649,9 +665,11 @@ const newCommand = define({
649
665
  });
650
666
  //#endregion
651
667
  //#region src/core/session.ts
652
- /** Convert an absolute path to Claude's project slug (/ and . → -) */
668
+ /** Convert an absolute path to Claude Code's project slug.
669
+ * Claude Code replaces any non-alphanumeric character (spaces, /, ., etc.) with '-'.
670
+ * Hyphens are preserved. Example: "/Users/me/Remember This" → "-Users-me-Remember-This". */
653
671
  function pathToProjectSlug(dir) {
654
- return resolve(dir).replace(/[/.]/g, "-");
672
+ return resolve(dir).replace(/[^A-Za-z0-9-]/g, "-");
655
673
  }
656
674
  /** Find the most recent .jsonl session file in a Claude project directory */
657
675
  function latestJsonlIn(projectDir) {
@@ -849,7 +867,8 @@ const resumeCommand = define({
849
867
  process.exit(1);
850
868
  }
851
869
  const tabId = matchingTabs[0];
852
- const termBlock = (tabsById.get(tabId) ?? []).find((b) => b.view === "term");
870
+ const blocks = tabsById.get(tabId) ?? [];
871
+ const termBlock = blocks.find((b) => b.view === "term");
853
872
  if (!termBlock) {
854
873
  consola.error(`No terminal block found in tab '${name}'`);
855
874
  process.exit(1);
@@ -860,7 +879,20 @@ const resumeCommand = define({
860
879
  consola.warn(`Claude is already running in tab "${name}" (${status}) — skipping resume`);
861
880
  process.exit(0);
862
881
  }
863
- if (status === "unknown") consola.warn(`Scrollback unavailable for tab "${name}" — cannot confirm shell is ready. Proceeding anyway.`);
882
+ if (status === "unknown") {
883
+ if (await adapter.confirmScrollbackEmpty(termBlock.blockid)) {
884
+ consola.info(`Tab "${name}" has no live shell (empty scrollback) — recreating`);
885
+ for (const b of blocks) adapter.deleteBlock(b.blockid);
886
+ adapter.closeSocket();
887
+ const newTabId = await openSession({
888
+ tabName: name,
889
+ dir,
890
+ claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}`
891
+ });
892
+ consola.success(`Tab "${name}" [${newTabId.slice(0, 8)}] → claude --resume ${sessionId.slice(0, 8)}… at ${dir} (recreated)`);
893
+ return;
894
+ }
895
+ }
864
896
  const extraFlags = loadConfig().claude.flags.join(" ");
865
897
  const cmd = `cd ${JSON.stringify(dir)} && claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(name)}\r`;
866
898
  await adapter.sendInput(termBlock.blockid, cmd);
@@ -1243,6 +1275,7 @@ const restoreCommand = define({
1243
1275
  consola.info(`Found ${toResume.length} tab(s) to restore:`);
1244
1276
  const extraFlags = loadConfig().claude.flags.join(" ");
1245
1277
  const results = [];
1278
+ const toRecreate = [];
1246
1279
  for (const tab of toResume) {
1247
1280
  const sessions = findSessionsByName(dir, tab.name);
1248
1281
  if (sessions.length === 0) {
@@ -1263,14 +1296,31 @@ const restoreCommand = define({
1263
1296
  }
1264
1297
  const sessionId = sessions[0].id;
1265
1298
  if (dryRun) {
1266
- consola.log(` ${tab.name} would resume session ${sessionId.slice(0, 8)}…`);
1299
+ const mode = tab.status === "unknown" ? "recreate" : "send";
1300
+ consola.log(` ${tab.name} → would ${mode} session ${sessionId.slice(0, 8)}…`);
1267
1301
  results.push({
1268
1302
  name: tab.name,
1269
1303
  result: `dry run: ${sessionId.slice(0, 8)}…`
1270
1304
  });
1271
1305
  continue;
1272
1306
  }
1273
- consola.log(` ${tab.name} resuming session ${sessionId.slice(0, 8)}…`);
1307
+ if (tab.status === "unknown") {
1308
+ if (await adapter.confirmScrollbackEmpty(tab.blockId)) {
1309
+ const blockIds = (tabsById.get(tab.tabId) ?? []).map((b) => b.blockid);
1310
+ toRecreate.push({
1311
+ name: tab.name,
1312
+ sessionId,
1313
+ blockIds,
1314
+ tabId: tab.tabId
1315
+ });
1316
+ results.push({
1317
+ name: tab.name,
1318
+ result: "queued for recreate"
1319
+ });
1320
+ continue;
1321
+ }
1322
+ }
1323
+ consola.log(` ${tab.name} → resuming session ${sessionId.slice(0, 8)}… (send)`);
1274
1324
  const cmd = `cd ${JSON.stringify(dir)} && claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(tab.name)}\r`;
1275
1325
  await adapter.sendInput(tab.blockId, cmd);
1276
1326
  await new Promise((r) => setTimeout(r, 500));
@@ -1293,7 +1343,23 @@ const restoreCommand = define({
1293
1343
  }
1294
1344
  }
1295
1345
  }
1296
- adapter.closeSocket();
1346
+ if (!dryRun && toRecreate.length) {
1347
+ for (const t of toRecreate) for (const bid of t.blockIds) adapter.deleteBlock(bid);
1348
+ adapter.closeSocket();
1349
+ consola.info(`Recreating ${toRecreate.length} dead tab(s)…`);
1350
+ for (const t of toRecreate) try {
1351
+ const newTabId = await openSession({
1352
+ tabName: t.name,
1353
+ dir,
1354
+ claudeCmd: `claude --resume ${t.sessionId} --name ${JSON.stringify(t.name)}`
1355
+ });
1356
+ const r = results.find((x) => x.name === t.name);
1357
+ r.result = `✔ recreated [${newTabId.slice(0, 8)}]`;
1358
+ } catch (err) {
1359
+ const r = results.find((x) => x.name === t.name);
1360
+ r.result = `✘ recreate failed: ${err.message}`;
1361
+ }
1362
+ } else adapter.closeSocket();
1297
1363
  console.log("\nRestore summary:");
1298
1364
  for (const r of results) console.log(` ${r.name}: ${r.result}`);
1299
1365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@generativereality/cctabs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Claude Code tab manager. Terminal tabs as the UI, no tmux.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,10 +16,10 @@
16
16
  "build": "tsdown",
17
17
  "typecheck": "tsc --noEmit",
18
18
  "lint": "eslint src/",
19
- "check": "bun run typecheck && bun run build",
19
+ "check": "npm run typecheck && npm run build",
20
20
  "release": "bumpp && npm publish",
21
21
  "sync-plugin": "bash scripts/sync-plugin.sh",
22
- "prepack": "bash scripts/sync-plugin.sh --check && bun run build"
22
+ "prepack": "bash scripts/sync-plugin.sh --check && npm run build"
23
23
  },
24
24
  "keywords": [
25
25
  "claude-code",
@@ -7,6 +7,20 @@ You are managing Claude Code sessions using the `cctabs` CLI.
7
7
 
8
8
  **Important:** "tabs" here means **terminal tabs** (e.g. Wave Terminal tabs), NOT browser tabs. Each terminal tab runs its own Claude Code session. This skill is for managing those terminal-based Claude Code sessions — not for browser automation.
9
9
 
10
+ ## Before you spawn anything: is cctabs the right tool?
11
+
12
+ cctabs is excellent for:
13
+ - **Multiple human-driven sessions** on unrelated projects (check on a deploy here, draft a blog post there, monitor a long-running task somewhere else).
14
+ - **Genuinely orthogonal parallel work** where each tab touches a disjoint file set (e.g. each tab writes to its own new directory, or each tab works on a different repo).
15
+ - **Long-running background sessions** that the user wants to check on later (builds, scrapes, benchmarks).
16
+
17
+ cctabs is the WRONG tool for:
18
+ - **Interconnected parallel work within one session.** If you're orchestrating and farming out subtasks that all modify the same evolving codebase, tabs hide each other's commits from each other. By the time they're done, you have three diverged branches that need manual merge, and any intervening change on `main`/`next` can make the merge structurally painful. **Use the Agent tool instead** — subagents share your filesystem and git state, commit in place, and surface their result back to you.
19
+ - **Sequential dependencies.** If B depends on A's commits landing, don't parallelize — run A to completion first, then B.
20
+ - **Work that touches the same files as the current orchestrator session.** Commits race, branches diverge, conflicts multiply.
21
+
22
+ A good test: *"If both tabs finish successfully, will merging their output be trivial?"* If yes, cctabs is fine. If no (or you can't tell), do it sequentially or use subagents.
23
+
10
24
  ## First: Ensure cctabs is available
11
25
 
12
26
  ```bash
@@ -27,6 +41,29 @@ Do not modify PATH or npm configuration beyond this.
27
41
 
28
42
  Each Claude Code session runs in its own **terminal tab**. `cctabs` lets you — and other Claude Code sessions — introspect and orchestrate the full session fleet.
29
43
 
44
+ ## When to Use Worktrees
45
+
46
+ **Use `--worktree` whenever a tab will edit code on a branch that differs from the main working tree.** This includes:
47
+ - Fixing CI on a PR (`cctabs new fix-1789 ~/Dev/myapp --worktree`)
48
+ - Working on a feature branch while the main checkout runs a dev server
49
+ - Any task where multiple tabs might checkout different branches
50
+
51
+ Without `--worktree`, all tabs share the same working directory. If two tabs checkout different branches, they stomp on each other's files — causing silent conflicts, lost changes, and broken dev servers.
52
+
53
+ **Rule of thumb:**
54
+ - **Read-only / docs / coordination** → no worktree needed (stays on current branch)
55
+ - **Editing code on a different branch** → always `--worktree`
56
+
57
+ ```bash
58
+ # ❌ WRONG — two tabs checking out different branches in the same directory
59
+ cctabs new fix-auth ~/Dev/myapp --prompt "checkout PR #101 and fix lint"
60
+ cctabs new fix-api ~/Dev/myapp --prompt "checkout PR #102 and fix tests"
61
+
62
+ # ✅ RIGHT — each gets its own isolated copy
63
+ cctabs new fix-auth ~/Dev/myapp --worktree --prompt "checkout PR #101 and fix lint"
64
+ cctabs new fix-api ~/Dev/myapp --worktree --prompt "checkout PR #102 and fix tests"
65
+ ```
66
+
30
67
  ## Quick Reference
31
68
 
32
69
  ```bash
@@ -104,7 +141,9 @@ The forked session shares full conversation history up to the fork point, then d
104
141
 
105
142
  ## Workflow: Spawning a Parallel Agent
106
143
 
107
- As a Claude Code session, you can spawn a sibling session to work on a parallel task:
144
+ **Before spawning, re-read "is cctabs the right tool?" above.** If the task is interconnected with your current work, use the Agent tool (subagents) instead — they share your filesystem and commits.
145
+
146
+ As a Claude Code session, you can spawn a sibling session for a **genuinely independent** parallel task:
108
147
 
109
148
  **Preferred: pass the initial task directly to `cctabs new`** using `--prompt` or `--file`. This polls internally until Claude's `❯` prompt appears before sending — no race condition:
110
149
 
@@ -125,6 +164,25 @@ cctabs send payments "yes\n" # quick replies
125
164
 
126
165
  **Do NOT call `cctabs send` immediately after `cctabs new`** — Claude is still starting up and the text will land as raw shell commands.
127
166
 
167
+ ### Spawning gotchas (hard-won)
168
+
169
+ 1. **Verify the worktree base immediately after spawn.** `--worktree` does not always branch from your current HEAD — if you have local un-pushed commits, the child session may branch from an older commit (whatever the remote tracking branch points at). Always check:
170
+ ```bash
171
+ cctabs new kid ~/Dev/myapp --worktree -p "..."
172
+ # Then in the ORCHESTRATOR tab:
173
+ git -C ~/Dev/myapp/.claude/worktrees/kid log --oneline -1
174
+ ```
175
+ If the base is not what you expected, abort and fix: either push your commits to the tracking branch first, or spawn without `--worktree` and let the subagent work on your branch directly.
176
+
177
+ 2. **Never instruct a subagent to "rebase your branch on main/next."** Subagents interpret this liberally. A common failure mode: the subagent does `git reset --hard <remote>` and throws away its own completed commits, trying to redo the work from scratch. Instead:
178
+ - Have the orchestrator handle rebases after the subagent is done.
179
+ - Or send a precise patch/diff rather than a verbal rebase instruction.
180
+ - Or tell the subagent explicitly: *"do not rebase, do not reset; make fixup commits on top of your existing branch."*
181
+
182
+ 3. **Subagents won't see each other's commits.** Each tab has its own working tree. If ws-A commits a schema, ws-B cannot consume it until you merge A → main → rebase B. This is a fundamental property, not a bug. Only parallelize when this limitation doesn't matter.
183
+
184
+ 4. **Don't delegate rebases or merges to subagents.** Those are orchestrator work. Subagents produce content; orchestrator integrates.
185
+
128
186
  ## Workflow: Monitoring Another Session
129
187
 
130
188
  ```bash
@@ -176,10 +234,36 @@ cctabs new feature ~/Dev/myapp --worktree
176
234
 
177
235
  **Why:** Manually created worktree dirs placed outside the repo confuse Claude Code's session tracking, project memory lookup (`.claude/` is in the main repo), and CLAUDE.md resolution. Claude Code's built-in worktree support keeps everything co-located under `.claude/worktrees/` and handles cleanup on session exit.
178
236
 
237
+ **Worktree base-commit caveat:** after spawning with `--worktree`, verify the branch base matches your expectation (see "Spawning gotchas" above). If your orchestrator has local commits that haven't been pushed, the worktree may branch from the stale remote tip instead of HEAD. This bites hardest when parallel tabs need to share schema/types your orchestrator has been working on — they won't see those changes if they branched before the commits landed upstream.
238
+
239
+ ## Handling `cctabs new` Timeout Errors
240
+
241
+ `cctabs new` may occasionally fail with "Timed out waiting for new terminal block". This does **NOT** mean you have too many tabs or that Wave Terminal has hit a limit.
242
+
243
+ **Possible causes** (root cause not yet confirmed):
244
+ - Wave Terminal may need to be in focus / foreground for tab creation to register
245
+ - The internal timeout may be slightly too short for the current system load
246
+ - Transient IPC timing issue between cctabs and Wave
247
+
248
+ **What to do:**
249
+ 1. **Retry the same command** — it often works on the second attempt
250
+ 2. If it fails again, wait a few seconds and retry once more
251
+ 3. If it keeps failing, ask the user to bring Wave Terminal to the foreground and try again
252
+
253
+ **What NOT to do:**
254
+ - ❌ Do NOT assume there is a "tab limit" — there isn't one
255
+ - ❌ Do NOT close other tabs to "make room" — this destroys the user's sessions
256
+ - ❌ Do NOT suggest the user has too many tabs open
257
+
179
258
  ## Workflow: Cleanup
180
259
 
260
+ **⚠️ NEVER close tabs without explicit user approval.** Each tab may contain an active session with important context, uncommitted work, or in-progress tasks. Closing a tab is destructive and irreversible.
261
+
262
+ **Always ask first:**
263
+ > "These tabs look idle: `old-feature`, `fix-1234`. Want me to close any of them?"
264
+
265
+ Only after the user confirms:
181
266
  ```bash
182
- cctabs sessions # find idle/terminal tabs
183
267
  cctabs close old-feature # close by name (prefix match)
184
268
  cctabs close e5f6a7b8 # close by block ID prefix
185
269
  ```
@@ -200,3 +284,14 @@ Name tabs after the **project or task**:
200
284
  - `cctabs new` and `cctabs resume` automatically pass `--name <tab-name>` to claude, syncing the session display name with the tab title
201
285
  - Configured `claude.flags` in `~/.config/cctabs/config.toml` are applied to every session
202
286
  - `cctabs send` resolves tab names to their terminal block automatically
287
+
288
+ ## Lesson: the common failure mode
289
+
290
+ A pattern that wastes the most tokens: an orchestrator spawns three tabs for "parallel workstreams" on the same feature, each tab diverges from the base and from each other, the orchestrator loses visibility into what each is doing, one tab misinterprets a course-correct and resets its own work, and finally the orchestrator spends hours hand-merging commits that don't apply cleanly against an intervening refactor.
291
+
292
+ The fix is upstream: before spawning, ask *"are these workstreams actually independent?"* If the answer is "mostly, but they share a common data model / schema / utility module" — they are **not** independent for cctabs purposes. Either:
293
+ - Do them sequentially in one tab (cheapest).
294
+ - Use the Agent tool for subtasks that share orchestrator state.
295
+ - Land the shared pieces first on `main`/`next`, push, then spawn tabs (each branches cleanly off the new tip and work is truly orthogonal from there).
296
+
297
+ Parallel tabs earn their keep when the work is genuinely orthogonal (separate repos, separate brand-new directories, independent features) and when you'd otherwise be idle waiting for one long-running task to finish.