opencode-zellij 0.0.12 → 0.0.14

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/dist/index.mjs CHANGED
@@ -39,7 +39,7 @@ async function installedPackageMetadata(installRoot) {
39
39
  try {
40
40
  const content = await readFile(join(packageDir(installRoot), "package.json"), "utf8");
41
41
  const pkg = JSON.parse(content);
42
- if (isRecord$1(pkg)) return {
42
+ if (isRecord$2(pkg)) return {
43
43
  name: typeof pkg.name === "string" ? pkg.name : void 0,
44
44
  version: typeof pkg.version === "string" ? pkg.version : void 0,
45
45
  main: typeof pkg.main === "string" ? pkg.main : void 0
@@ -107,7 +107,7 @@ async function findInstallContext(importMetaUrl) {
107
107
  try {
108
108
  const content = await readFile(packageJsonPath, "utf8");
109
109
  const pkg = JSON.parse(content);
110
- if (isRecord$1(pkg) && pkg.name === "opencode-zellij" && typeof pkg.version === "string" && pkg.version.length > 0) {
110
+ if (isRecord$2(pkg) && pkg.name === "opencode-zellij" && typeof pkg.version === "string" && pkg.version.length > 0) {
111
111
  const installRoot = dirname(dirname(dir));
112
112
  if (existsSync(join(installRoot, "package.json"))) return {
113
113
  installRoot,
@@ -124,7 +124,7 @@ async function findInstallContext(importMetaUrl) {
124
124
  dir = parent;
125
125
  }
126
126
  }
127
- function isRecord$1(value) {
127
+ function isRecord$2(value) {
128
128
  return typeof value === "object" && value !== null;
129
129
  }
130
130
  function isAutoUpdatableSpec(spec) {
@@ -143,7 +143,7 @@ async function fetchLatestVersion(fetchImpl = globalThis.fetch) {
143
143
  return;
144
144
  }
145
145
  const data = await response.json();
146
- if (isRecord$1(data) && typeof data.latest === "string") return data.latest;
146
+ if (isRecord$2(data) && typeof data.latest === "string") return data.latest;
147
147
  debug("npm registry response missing latest tag");
148
148
  return;
149
149
  } catch (cause) {
@@ -584,7 +584,7 @@ function numericProperty(object, keys) {
584
584
  }
585
585
  }
586
586
  }
587
- function stringProperty$1(object, keys) {
587
+ function stringProperty$2(object, keys) {
588
588
  for (const key of keys) {
589
589
  const value = object[key];
590
590
  if (typeof value === "string") return value;
@@ -643,7 +643,7 @@ function parsePaneExists(listPanesJson, paneId) {
643
643
  function tabNameProperty(object, tabId) {
644
644
  if (tabId === void 0) return void 0;
645
645
  if (numericProperty(object, ["tab_id", "tabId"]) !== tabId) return void 0;
646
- const name = stringProperty$1(object, ["name", "title"]);
646
+ const name = stringProperty$2(object, ["name", "title"]);
647
647
  return typeof name === "string" ? name : void 0;
648
648
  }
649
649
  function findTabName(value, tabId) {
@@ -1913,62 +1913,18 @@ const zellijPtyWriteTool = tool({
1913
1913
  }
1914
1914
  });
1915
1915
  //#endregion
1916
- //#region src/zellij/tab-title-status-snapshot.ts
1917
- /**
1918
- * Events that should trigger a debounced refresh of the tab title base status
1919
- * via the `/session/status` snapshot API.
1920
- *
1921
- * Base state (running vs idle) is sourced from the snapshot rather than
1922
- * individual `session.idle` events because testing showed that both parent and
1923
- * child sessions report busy during subagent execution, and the parent remains
1924
- * busy even after the child completes. The snapshot gives a consistent,
1925
- * server-authoritative view. `needs-input` has no REST API, so it continues
1926
- * to be managed purely through events.
1927
- *
1928
- * The snapshot reconciliation is intentionally *not* optimistic for idle-like
1929
- * transitions: a lone parent/child idle event can be stale during subagent
1930
- * handoff. The debounce coalesces high-frequency event streams to avoid
1931
- * hammering the API on every individual status change.
1932
- */
1933
- function shouldRefreshTabTitleStatusSnapshot(event) {
1934
- switch (event.type) {
1935
- case "session.status":
1936
- case "session.idle":
1937
- case "session.error":
1938
- case "session.created":
1939
- case "session.deleted":
1940
- case "question.asked":
1941
- case "question.replied":
1942
- case "question.rejected":
1943
- case "permission.asked":
1944
- case "permission.replied":
1945
- case "permission.updated": return true;
1946
- default: return false;
1947
- }
1948
- }
1949
- /**
1950
- * Best-effort fetch of session statuses for a workspace.
1951
- *
1952
- * Only accepts object-keyed maps: `{ [sessionID]: SessionStatus }` directly,
1953
- * or the generated-client envelope `{ data: { [sessionID]: SessionStatus } }`.
1954
- *
1955
- * An empty map `{}` is a valid snapshot (all sessions ended / none tracked).
1956
- * Arrays are never accepted — they always return undefined.
1957
- * If any single status entry fails to parse, the entire snapshot is rejected
1958
- * (no partial apply) to avoid incorrectly clearing session states.
1959
- *
1960
- * Failures are swallowed and return undefined so the caller never throws.
1961
- */
1962
- async function fetchSessionStatusSnapshot(client, workspaceRoot) {
1916
+ //#region src/zellij/completion-notifications.ts
1917
+ /** Fetches the prompt-mode idle guard status; unrelated to tab title state. */
1918
+ async function fetchPromptIdleStatusSnapshot(client, workspaceRoot) {
1963
1919
  try {
1964
1920
  if (!client.session?.status) {
1965
- debug("fetchSessionStatusSnapshot: client.session.status not available");
1921
+ debug("fetchPromptIdleStatusSnapshot: client.session.status not available");
1966
1922
  return;
1967
1923
  }
1968
1924
  const result = await client.session.status({ query: { directory: workspaceRoot } });
1969
1925
  const payload = result && typeof result === "object" && "data" in result ? result.data : result;
1970
1926
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1971
- debug("fetchSessionStatusSnapshot received non-object payload");
1927
+ debug("fetchPromptIdleStatusSnapshot received non-object payload");
1972
1928
  return;
1973
1929
  }
1974
1930
  const entries = Object.entries(payload);
@@ -1977,14 +1933,14 @@ async function fetchSessionStatusSnapshot(client, workspaceRoot) {
1977
1933
  for (const [sessionID, status] of entries) {
1978
1934
  const parsed = parseSessionStatus(status);
1979
1935
  if (parsed === void 0) {
1980
- debug("fetchSessionStatusSnapshot received invalid status entry, rejecting entire snapshot");
1936
+ debug("fetchPromptIdleStatusSnapshot received invalid status entry, rejecting entire snapshot");
1981
1937
  return;
1982
1938
  }
1983
1939
  snapshot[sessionID] = parsed;
1984
1940
  }
1985
1941
  return snapshot;
1986
1942
  } catch (err) {
1987
- debug("fetchSessionStatusSnapshot failed", errorMessage(err));
1943
+ debug("fetchPromptIdleStatusSnapshot failed", errorMessage(err));
1988
1944
  return;
1989
1945
  }
1990
1946
  }
@@ -1999,74 +1955,6 @@ function parseSessionStatus(value) {
1999
1955
  next: typeof status.next === "number" ? status.next : 0
2000
1956
  };
2001
1957
  }
2002
- const DEFAULT_DEBOUNCE_MS = 1e3;
2003
- /**
2004
- * Encapsulates tab title session-status snapshot fetching with debounced refresh.
2005
- *
2006
- * This class replaces the nested `refreshTabTitleSnapshot` /
2007
- * `scheduleTabTitleSnapshotRefresh` functions that previously lived inside
2008
- * `createZellijPtyPlugin`. It manages its own timer so the plugin factory
2009
- * remains a simple composition root.
2010
- *
2011
- * Usage:
2012
- * ```
2013
- * const refresher = tabTitleManager
2014
- * ? new TabTitleStatusSnapshotRefresher({ client, workspaceRoot, manager: tabTitleManager })
2015
- * : undefined
2016
- *
2017
- * await refresher?.refreshNow() // initial snapshot
2018
- * refresher?.scheduleRefresh() // on relevant events
2019
- * refresher?.dispose() // on shutdown
2020
- * ```
2021
- */
2022
- var TabTitleStatusSnapshotRefresher = class {
2023
- client;
2024
- workspaceRoot;
2025
- manager;
2026
- debounceMs;
2027
- timer;
2028
- constructor(options) {
2029
- this.client = options.client;
2030
- this.workspaceRoot = options.workspaceRoot;
2031
- this.manager = options.manager;
2032
- this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2033
- }
2034
- /**
2035
- * Fetches and applies the snapshot immediately, cancelling any pending
2036
- * debounced refresh.
2037
- */
2038
- async refreshNow() {
2039
- this.clearTimer();
2040
- const snapshot = await fetchSessionStatusSnapshot(this.client, this.workspaceRoot);
2041
- if (snapshot !== void 0) this.manager.applySessionStatusSnapshot(snapshot);
2042
- }
2043
- /**
2044
- * Schedules a debounced snapshot refresh. Subsequent calls while a timer
2045
- * is pending coalesce into a single refresh.
2046
- */
2047
- scheduleRefresh() {
2048
- if (this.timer) return;
2049
- this.timer = setTimeout(() => {
2050
- this.timer = void 0;
2051
- this.refreshNow().catch((err) => debug("tab title snapshot refresh failed", errorMessage(err)));
2052
- }, this.debounceMs);
2053
- }
2054
- /**
2055
- * Clears any pending debounced refresh. Use this during shutdown so a
2056
- * pending timer does not fire after the manager has been destroyed.
2057
- */
2058
- dispose() {
2059
- this.clearTimer();
2060
- }
2061
- clearTimer() {
2062
- if (this.timer) {
2063
- clearTimeout(this.timer);
2064
- this.timer = void 0;
2065
- }
2066
- }
2067
- };
2068
- //#endregion
2069
- //#region src/zellij/completion-notifications.ts
2070
1958
  const completionTitle = "Zellij PTY session completed";
2071
1959
  const completionMessage = "A Zellij PTY session completed. Review the finished pane if needed.";
2072
1960
  const queuedNoticeHeader = "[OpenCode] Zellij PTY completion notice";
@@ -2236,7 +2124,7 @@ var SessionCompletionNotificationQueue = class {
2236
2124
  }
2237
2125
  const session = this.context.client.session;
2238
2126
  const prompt = session?.prompt ?? session?.promptAsync;
2239
- const statusSnapshot = await fetchSessionStatusSnapshot(this.context.client, this.context.workspaceRoot);
2127
+ const statusSnapshot = await fetchPromptIdleStatusSnapshot(this.context.client, this.context.workspaceRoot);
2240
2128
  const decision = evaluateCompletionPromptDecision({
2241
2129
  event: state.event,
2242
2130
  config: this.context.config,
@@ -2338,41 +2226,21 @@ function exitAfterCleanup(signal, code) {
2338
2226
  //#endregion
2339
2227
  //#region src/zellij/tab-title-events.ts
2340
2228
  const execFileAsync = promisify(execFile);
2341
- function isRecord(value) {
2229
+ function isRecord$1(value) {
2342
2230
  return typeof value === "object" && value !== null;
2343
2231
  }
2344
- function stringProperty(object, key) {
2232
+ function stringProperty$1(object, key) {
2345
2233
  const value = object[key];
2346
2234
  return typeof value === "string" ? value : void 0;
2347
2235
  }
2348
- function nestedStringProperty(object, key, nestedKey) {
2236
+ function nestedStringProperty$1(object, key, nestedKey) {
2349
2237
  const nested = object[key];
2350
- if (!isRecord(nested)) return void 0;
2351
- return stringProperty(nested, nestedKey);
2352
- }
2353
- function sessionStatusProperty(object) {
2354
- const status = object.status;
2355
- if (!isRecord(status)) return void 0;
2356
- if (status.type === "idle" || status.type === "busy") return { type: status.type };
2357
- if (status.type === "retry") return {
2358
- type: "retry",
2359
- attempt: typeof status.attempt === "number" ? status.attempt : 0,
2360
- message: typeof status.message === "string" ? status.message : "",
2361
- next: typeof status.next === "number" ? status.next : 0
2362
- };
2363
- }
2364
- function inputRequestID(object) {
2365
- return stringProperty(object, "id") ?? stringProperty(object, "requestID") ?? stringProperty(object, "permissionID");
2366
- }
2367
- function inputState(object) {
2368
- return (stringProperty(object, "status") ?? stringProperty(object, "state") ?? stringProperty(object, "type"))?.toLowerCase();
2238
+ if (!isRecord$1(nested)) return void 0;
2239
+ return stringProperty$1(nested, nestedKey);
2369
2240
  }
2370
- function isResolvedInputState(state) {
2371
- return state === "approved" || state === "denied" || state === "rejected" || state === "resolved" || state === "replied";
2372
- }
2373
- function deletedSessionID(event) {
2374
- if (!isRecord(event.properties)) return void 0;
2375
- return nestedStringProperty(event.properties, "info", "id") ?? stringProperty(event.properties, "sessionID");
2241
+ function deletedSessionID$1(event) {
2242
+ if (!isRecord$1(event.properties)) return void 0;
2243
+ return nestedStringProperty$1(event.properties, "info", "id") ?? stringProperty$1(event.properties, "sessionID");
2376
2244
  }
2377
2245
  async function readGitBranch(worktree) {
2378
2246
  return (await execFileAsync("git", [
@@ -2397,61 +2265,6 @@ async function getInitialBranch(worktree, readBranch = readGitBranch) {
2397
2265
  function shouldReadInitialBranch(zellij) {
2398
2266
  return Boolean(zellij);
2399
2267
  }
2400
- function handleTabTitleEvent(tabTitleManager, event) {
2401
- if (event.type === "server.instance.disposed" || event.type === "global.disposed") return tabTitleManager.destroy?.();
2402
- if (!isRecord(event.properties)) return;
2403
- const properties = event.properties;
2404
- switch (event.type) {
2405
- case "session.status": {
2406
- const sessionID = stringProperty(properties, "sessionID");
2407
- const status = sessionStatusProperty(properties);
2408
- if (sessionID && status && status.type !== "idle") tabTitleManager.updateSessionStatus(sessionID, status);
2409
- break;
2410
- }
2411
- case "session.idle": break;
2412
- case "session.error": break;
2413
- case "vcs.branch.updated":
2414
- tabTitleManager.setBranch(stringProperty(properties, "branch"));
2415
- break;
2416
- case "question.asked":
2417
- case "permission.asked": {
2418
- const id = inputRequestID(properties);
2419
- const sessionID = stringProperty(properties, "sessionID");
2420
- if (id && sessionID) {
2421
- tabTitleManager.markNeedsInput(id, sessionID);
2422
- tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
2423
- }
2424
- break;
2425
- }
2426
- case "permission.updated": {
2427
- const id = inputRequestID(properties);
2428
- const sessionID = stringProperty(properties, "sessionID");
2429
- const state = inputState(properties);
2430
- if (id && isResolvedInputState(state)) {
2431
- tabTitleManager.clearNeedsInput(id);
2432
- if (sessionID) tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
2433
- } else if (id && sessionID) {
2434
- tabTitleManager.markNeedsInput(id, sessionID);
2435
- tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
2436
- }
2437
- break;
2438
- }
2439
- case "question.replied":
2440
- case "question.rejected":
2441
- case "permission.replied": {
2442
- const id = inputRequestID(properties);
2443
- const sessionID = stringProperty(properties, "sessionID");
2444
- if (id) tabTitleManager.clearNeedsInput(id);
2445
- if (sessionID) tabTitleManager.updateSessionStatus(sessionID, { type: "busy" });
2446
- break;
2447
- }
2448
- case "session.deleted": {
2449
- const sessionID = deletedSessionID(event);
2450
- if (sessionID) tabTitleManager.removeSession(sessionID);
2451
- break;
2452
- }
2453
- }
2454
- }
2455
2268
  //#endregion
2456
2269
  //#region src/zellij/tab-title.ts
2457
2270
  const defaultTabTitleEmojis = {
@@ -2470,10 +2283,237 @@ function sanitizeTitle(title, maxLength = 90) {
2470
2283
  if (chars.length > maxLength) cleaned = `${chars.slice(0, maxLength - 1).join("")}…`;
2471
2284
  return cleaned;
2472
2285
  }
2473
- var TabTitleManager = class {
2474
- sessionStatuses = /* @__PURE__ */ new Map();
2475
- pendingInputs = /* @__PURE__ */ new Map();
2286
+ function isRecord(value) {
2287
+ return typeof value === "object" && value !== null;
2288
+ }
2289
+ function stringProperty(object, key) {
2290
+ const value = object[key];
2291
+ return typeof value === "string" ? value : void 0;
2292
+ }
2293
+ function nestedStringProperty(object, key, nestedKey) {
2294
+ const nested = object[key];
2295
+ if (!isRecord(nested)) return void 0;
2296
+ return stringProperty(nested, nestedKey);
2297
+ }
2298
+ function sessionStatusType(properties) {
2299
+ const status = properties.status;
2300
+ if (!isRecord(status)) return void 0;
2301
+ const type = status.type;
2302
+ if (type === "idle" || type === "busy") return type;
2303
+ if (type === "retry") return "retry";
2304
+ }
2305
+ function inputRequestID(properties) {
2306
+ return stringProperty(properties, "id") ?? stringProperty(properties, "requestID") ?? stringProperty(properties, "permissionID");
2307
+ }
2308
+ function inputState(properties) {
2309
+ return (stringProperty(properties, "status") ?? stringProperty(properties, "state") ?? stringProperty(properties, "type"))?.toLowerCase();
2310
+ }
2311
+ function isResolvedInputState(state) {
2312
+ return state === "approved" || state === "denied" || state === "rejected" || state === "resolved" || state === "replied";
2313
+ }
2314
+ function deletedSessionID(properties) {
2315
+ return nestedStringProperty(properties, "info", "id") ?? stringProperty(properties, "sessionID");
2316
+ }
2317
+ var TabTitleIdentityModel = class {
2318
+ ready;
2319
+ projectName;
2476
2320
  branchName;
2321
+ worktree;
2322
+ readBranch;
2323
+ refreshGeneration = 0;
2324
+ constructor(options) {
2325
+ this.projectName = options.projectName;
2326
+ this.worktree = options.worktree;
2327
+ this.readBranch = options.readBranch;
2328
+ this.ready = this.refreshBranch("initial");
2329
+ }
2330
+ async refreshBranch(_reason) {
2331
+ const generation = ++this.refreshGeneration;
2332
+ try {
2333
+ const result = await this.readBranch(this.worktree);
2334
+ if (generation !== this.refreshGeneration) return;
2335
+ const trimmed = result.trim() || void 0;
2336
+ this.branchName = trimmed;
2337
+ } catch (error) {
2338
+ if (generation !== this.refreshGeneration) return;
2339
+ debug("refreshBranch failed", errorMessage(error));
2340
+ }
2341
+ }
2342
+ handleEvent(event) {
2343
+ if (event.type === "vcs.branch.updated") return this.refreshBranch("vcs.branch.updated");
2344
+ }
2345
+ };
2346
+ var TabTitleActivityModel = class {
2347
+ status = "idle";
2348
+ worktreeDirectory;
2349
+ sessions = /* @__PURE__ */ new Map();
2350
+ scopedSessions = /* @__PURE__ */ new Set();
2351
+ runningSessions = /* @__PURE__ */ new Set();
2352
+ pendingInputs = /* @__PURE__ */ new Map();
2353
+ constructor(options) {
2354
+ this.worktreeDirectory = options.worktreeDirectory;
2355
+ }
2356
+ getSession(sessionID) {
2357
+ return this.scopedSessions.has(sessionID) ? this.sessions.get(sessionID) : void 0;
2358
+ }
2359
+ hasPendingInput(sessionID, requestID) {
2360
+ return this.pendingInputs.has(`${sessionID}:${requestID}`);
2361
+ }
2362
+ handleEvent(event) {
2363
+ if (!isRecord(event.properties)) return;
2364
+ const properties = event.properties;
2365
+ switch (event.type) {
2366
+ case "session.created":
2367
+ case "session.updated": {
2368
+ const info = properties.info;
2369
+ if (isRecord(info)) {
2370
+ const id = stringProperty(info, "id");
2371
+ if (id) this.storeSession(id, info);
2372
+ }
2373
+ break;
2374
+ }
2375
+ case "session.status": {
2376
+ const sessionID = stringProperty(properties, "sessionID");
2377
+ const statusType = sessionStatusType(properties);
2378
+ if (sessionID && statusType) {
2379
+ if (statusType === "idle") {
2380
+ if (this.runningSessions.has(sessionID)) {
2381
+ this.runningSessions.delete(sessionID);
2382
+ this.updateStatus();
2383
+ }
2384
+ } else if (statusType === "busy" || statusType === "retry") {
2385
+ if (this.scopedSessions.has(sessionID)) {
2386
+ this.runningSessions.add(sessionID);
2387
+ this.updateStatus();
2388
+ }
2389
+ }
2390
+ }
2391
+ break;
2392
+ }
2393
+ case "session.idle":
2394
+ case "session.error": {
2395
+ const sessionID = stringProperty(properties, "sessionID");
2396
+ if (sessionID && this.runningSessions.has(sessionID)) {
2397
+ this.runningSessions.delete(sessionID);
2398
+ this.updateStatus();
2399
+ }
2400
+ break;
2401
+ }
2402
+ case "question.asked":
2403
+ case "permission.asked": {
2404
+ const id = inputRequestID(properties);
2405
+ const sessionID = stringProperty(properties, "sessionID");
2406
+ if (id && sessionID && this.scopedSessions.has(sessionID)) {
2407
+ this.pendingInputs.set(`${sessionID}:${id}`, sessionID);
2408
+ this.runningSessions.add(sessionID);
2409
+ this.updateStatus();
2410
+ }
2411
+ break;
2412
+ }
2413
+ case "permission.updated": {
2414
+ const id = inputRequestID(properties);
2415
+ const sessionID = stringProperty(properties, "sessionID");
2416
+ const state = inputState(properties);
2417
+ if (id && isResolvedInputState(state)) {
2418
+ this.pendingInputs.delete(`${sessionID}:${id}`);
2419
+ if (sessionID && this.runningSessions.has(sessionID)) this.runningSessions.add(sessionID);
2420
+ this.updateStatus();
2421
+ } else if (id && sessionID && this.scopedSessions.has(sessionID)) {
2422
+ this.pendingInputs.set(`${sessionID}:${id}`, sessionID);
2423
+ this.runningSessions.add(sessionID);
2424
+ this.updateStatus();
2425
+ }
2426
+ break;
2427
+ }
2428
+ case "question.replied":
2429
+ case "question.rejected":
2430
+ case "permission.replied": {
2431
+ const id = inputRequestID(properties);
2432
+ const sessionID = stringProperty(properties, "sessionID");
2433
+ if (id) this.pendingInputs.delete(`${sessionID}:${id}`);
2434
+ if (sessionID && this.runningSessions.has(sessionID)) this.runningSessions.add(sessionID);
2435
+ this.updateStatus();
2436
+ break;
2437
+ }
2438
+ case "session.deleted": {
2439
+ const sessionID = deletedSessionID(properties);
2440
+ if (sessionID) {
2441
+ this.removeSessionAndDescendants(sessionID);
2442
+ this.updateStatus();
2443
+ }
2444
+ break;
2445
+ }
2446
+ }
2447
+ }
2448
+ storeSession(id, info) {
2449
+ const directory = stringProperty(info, "directory");
2450
+ const parentID = stringProperty(info, "parentID");
2451
+ this.sessions.set(id, {
2452
+ directory,
2453
+ parentID
2454
+ });
2455
+ const isDirectlyScoped = directory === this.worktreeDirectory;
2456
+ const isDescendantScoped = parentID ? this.scopedSessions.has(parentID) : false;
2457
+ if (this.scopedSessions.has(id) || isDirectlyScoped || isDescendantScoped) this.scopedSessions.add(id);
2458
+ }
2459
+ removeSessionAndDescendants(rootID) {
2460
+ const toRemove = /* @__PURE__ */ new Set();
2461
+ toRemove.add(rootID);
2462
+ let changed = true;
2463
+ while (changed) {
2464
+ changed = false;
2465
+ for (const [id, session] of this.sessions) if (!toRemove.has(id) && session.parentID && toRemove.has(session.parentID)) {
2466
+ toRemove.add(id);
2467
+ changed = true;
2468
+ }
2469
+ }
2470
+ for (const id of toRemove) {
2471
+ this.sessions.delete(id);
2472
+ this.scopedSessions.delete(id);
2473
+ this.runningSessions.delete(id);
2474
+ }
2475
+ for (const [key, sessionID] of [...this.pendingInputs.entries()]) if (toRemove.has(sessionID)) this.pendingInputs.delete(key);
2476
+ }
2477
+ updateStatus() {
2478
+ if (this.pendingInputs.size > 0) this.status = "needs-input";
2479
+ else if (this.runningSessions.size > 0) this.status = "running";
2480
+ else this.status = "idle";
2481
+ }
2482
+ };
2483
+ var TabTitleActor = class {
2484
+ ready;
2485
+ identity;
2486
+ activity;
2487
+ emojis;
2488
+ constructor(options) {
2489
+ this.identity = options.identity;
2490
+ this.activity = options.activity;
2491
+ this.emojis = {
2492
+ ...defaultTabTitleEmojis,
2493
+ ...options.emojis
2494
+ };
2495
+ this.ready = this.identity.ready;
2496
+ }
2497
+ get context() {
2498
+ return {
2499
+ projectName: this.identity.projectName,
2500
+ branchName: this.identity.branchName,
2501
+ status: this.activity.status
2502
+ };
2503
+ }
2504
+ get title() {
2505
+ return formatTabTitle({
2506
+ ...this.context,
2507
+ emojis: this.emojis
2508
+ });
2509
+ }
2510
+ async handleEvent(event) {
2511
+ this.activity.handleEvent(event);
2512
+ const identityResult = this.identity.handleEvent(event);
2513
+ if (identityResult instanceof Promise) await identityResult;
2514
+ }
2515
+ };
2516
+ var TabTitleManager = class {
2477
2517
  desiredTitle;
2478
2518
  lastSyncedTitle;
2479
2519
  debounceTimer;
@@ -2484,7 +2524,6 @@ var TabTitleManager = class {
2484
2524
  debounceMs;
2485
2525
  retryInitialMs;
2486
2526
  retryMaxMs;
2487
- projectName;
2488
2527
  cli;
2489
2528
  emojis;
2490
2529
  enabled;
@@ -2493,9 +2532,8 @@ var TabTitleManager = class {
2493
2532
  originalTabTitleLoaded = false;
2494
2533
  originalTabTitlePromise;
2495
2534
  destroyPromise;
2535
+ actor;
2496
2536
  constructor(options) {
2497
- this.projectName = options.projectName;
2498
- this.branchName = options.branchName?.trim() || void 0;
2499
2537
  this.cli = options.cli ?? new ZellijCli();
2500
2538
  this.emojis = {
2501
2539
  ...defaultTabTitleEmojis,
@@ -2504,80 +2542,12 @@ var TabTitleManager = class {
2504
2542
  this.debounceMs = options.debounceMs ?? 300;
2505
2543
  this.retryInitialMs = options.retryInitialMs ?? 250;
2506
2544
  this.retryMaxMs = options.retryMaxMs ?? 5e3;
2507
- this.enabled = Boolean(process.env.ZELLIJ);
2508
- }
2509
- setBranch(branch) {
2510
- const trimmed = branch?.trim() || void 0;
2511
- if (this.branchName === trimmed) return;
2512
- this.branchName = trimmed;
2513
- this.scheduleUpdate();
2514
- }
2515
- /**
2516
- * Applies a snapshot of session statuses from the server.
2517
- *
2518
- * This replaces the entire base status map. Sessions absent from the snapshot
2519
- * (e.g. because they ended) are removed so stale busy/idle entries do not
2520
- * persist. This is the authoritative source for the "running vs idle" base
2521
- * state; individual session.status events still perform optimistic updates
2522
- * for immediacy but the snapshot corrects drift.
2523
- *
2524
- * The `needs-input` overlay remains independent — it is managed purely by
2525
- * events (question/permission asked/replied/etc.) and always takes priority
2526
- * over the snapshot base when computing the displayed title.
2527
- */
2528
- applySessionStatusSnapshot(statuses) {
2529
- for (const sessionID of this.sessionStatuses.keys()) if (!(sessionID in statuses)) this.sessionStatuses.delete(sessionID);
2530
- for (const [sessionID, status] of Object.entries(statuses)) {
2531
- const activity = status.type === "idle" ? "idle" : "running";
2532
- if (this.sessionStatuses.get(sessionID) !== activity) this.sessionStatuses.set(sessionID, activity);
2533
- }
2534
- this.scheduleUpdate();
2535
- }
2536
- updateSessionStatus(sessionID, status) {
2537
- const activity = status.type === "idle" ? "idle" : "running";
2538
- if (this.sessionStatuses.get(sessionID) === activity) return;
2539
- this.sessionStatuses.set(sessionID, activity);
2540
- this.scheduleUpdate();
2541
- }
2542
- markSessionIdle(sessionID) {
2543
- this.updateSessionStatus(sessionID, { type: "idle" });
2544
- }
2545
- removeSession(sessionID) {
2546
- const hadSessionStatus = this.sessionStatuses.delete(sessionID);
2547
- let hadPendingInput = false;
2548
- for (const [id, pendingSessionID] of this.pendingInputs) if (pendingSessionID === sessionID) {
2549
- this.pendingInputs.delete(id);
2550
- hadPendingInput = true;
2551
- }
2552
- if (!hadSessionStatus && !hadPendingInput) return;
2553
- this.scheduleUpdate();
2554
- }
2555
- markNeedsInput(id, sessionID) {
2556
- if (this.pendingInputs.get(id) === sessionID) return;
2557
- this.pendingInputs.set(id, sessionID);
2558
- this.scheduleUpdate();
2559
- }
2560
- clearNeedsInput(id) {
2561
- if (!this.pendingInputs.delete(id)) return;
2562
- this.scheduleUpdate();
2563
- }
2564
- get isBusy() {
2565
- for (const activity of this.sessionStatuses.values()) if (activity === "running") return true;
2566
- return false;
2567
- }
2568
- get needsInput() {
2569
- return this.pendingInputs.size > 0;
2570
- }
2571
- get status() {
2572
- if (this.needsInput) return "needs-input";
2573
- if (this.isBusy) return "running";
2574
- return "idle";
2545
+ this.enabled = Boolean(process.env.ZELLIJ || process.env.ZELLIJ_SESSION_NAME);
2546
+ this.actor = options.actor;
2575
2547
  }
2576
2548
  buildTitle() {
2577
2549
  return sanitizeTitle(formatTabTitle({
2578
- projectName: this.projectName,
2579
- branchName: this.branchName,
2580
- status: this.status,
2550
+ ...this.actor.context,
2581
2551
  emojis: this.emojis
2582
2552
  }));
2583
2553
  }
@@ -2753,10 +2723,18 @@ function createZellijPtyPlugin(dependencies = {}) {
2753
2723
  registerShutdownCleanup();
2754
2724
  const workspaceRoot = getWorkspaceRoot(input);
2755
2725
  const projectName = getProjectName(workspaceRoot);
2756
- const branchName = config.tabTitle.enabled && shouldReadInitialBranch(process.env.ZELLIJ) ? await getInitialBranch(workspaceRoot) : void 0;
2757
- const tabTitleManager = config.tabTitle.enabled ? new TabTitleManager({
2726
+ const identityModel = config.tabTitle.enabled ? new TabTitleIdentityModel({
2758
2727
  projectName,
2759
- branchName,
2728
+ worktree: workspaceRoot,
2729
+ readBranch: async (worktree) => shouldReadInitialBranch(process.env.ZELLIJ || process.env.ZELLIJ_SESSION_NAME) ? await getInitialBranch(worktree) ?? "" : ""
2730
+ }) : void 0;
2731
+ const activityModel = config.tabTitle.enabled ? new TabTitleActivityModel({ worktreeDirectory: workspaceRoot }) : void 0;
2732
+ const actor = identityModel && activityModel ? new TabTitleActor({
2733
+ identity: identityModel,
2734
+ activity: activityModel
2735
+ }) : void 0;
2736
+ const tabTitleManager = config.tabTitle.enabled && actor ? new TabTitleManager({
2737
+ actor,
2760
2738
  debounceMs: config.tabTitle.debounceMs,
2761
2739
  emojis: {
2762
2740
  idle: config.tabTitle.emojiIdle,
@@ -2790,22 +2768,16 @@ function createZellijPtyPlugin(dependencies = {}) {
2790
2768
  }
2791
2769
  });
2792
2770
  subscriberManager.setLifecycleHooks(completionNotifications ? { onSessionTerminal: (event) => void completionNotifications.handleSessionTerminal(event).catch((error) => debug("completion notification lifecycle hook failed", errorMessage(error))) } : void 0);
2793
- const tabTitleSnapshotRefresher = tabTitleManager ? new TabTitleStatusSnapshotRefresher({
2794
- client,
2795
- workspaceRoot,
2796
- manager: tabTitleManager,
2797
- debounceMs: 1e3
2798
- }) : void 0;
2799
- tabTitleSnapshotRefresher?.refreshNow().catch((error) => debug("initial tab title snapshot refresh failed", errorMessage(error)));
2771
+ if (actor) await actor.ready;
2800
2772
  tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
2801
2773
  if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
2802
2774
  return {
2803
2775
  async event(input) {
2804
2776
  const event = input.event;
2805
- if (tabTitleManager) {
2806
- if (event.type === "server.instance.disposed" || event.type === "global.disposed") tabTitleSnapshotRefresher?.dispose();
2807
- await handleTabTitleEvent(tabTitleManager, event);
2808
- if (shouldRefreshTabTitleStatusSnapshot(event)) tabTitleSnapshotRefresher?.scheduleRefresh();
2777
+ if (actor && tabTitleManager) {
2778
+ await actor.handleEvent(event);
2779
+ if (event.type === "server.instance.disposed" || event.type === "global.disposed") await tabTitleManager.destroy();
2780
+ else tabTitleManager.scheduleUpdate();
2809
2781
  }
2810
2782
  if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
2811
2783
  completionNotifications?.clearAll();
@@ -2813,7 +2785,7 @@ function createZellijPtyPlugin(dependencies = {}) {
2813
2785
  subscriberManager.setLifecycleHooks(void 0);
2814
2786
  }
2815
2787
  if (event.type === "session.deleted") {
2816
- const sessionID = deletedSessionID(event);
2788
+ const sessionID = deletedSessionID$1(event);
2817
2789
  if (!sessionID) return;
2818
2790
  const sessions = sessionManager.listByOpenCodeSession(sessionID);
2819
2791
  for (const session of sessions) completionNotifications?.clearSession(session.id);