opencode-zellij 0.0.11 → 0.0.13

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
@@ -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,
@@ -2405,11 +2293,20 @@ function handleTabTitleEvent(tabTitleManager, event) {
2405
2293
  case "session.status": {
2406
2294
  const sessionID = stringProperty(properties, "sessionID");
2407
2295
  const status = sessionStatusProperty(properties);
2408
- if (sessionID && status && status.type !== "idle") tabTitleManager.updateSessionStatus(sessionID, status);
2296
+ if (sessionID && status) if (status.type === "idle") tabTitleManager.markSessionIdle(sessionID);
2297
+ else tabTitleManager.updateSessionStatus(sessionID, status);
2298
+ break;
2299
+ }
2300
+ case "session.idle": {
2301
+ const sessionID = stringProperty(properties, "sessionID");
2302
+ if (sessionID) tabTitleManager.markSessionIdle(sessionID);
2303
+ break;
2304
+ }
2305
+ case "session.error": {
2306
+ const sessionID = stringProperty(properties, "sessionID");
2307
+ if (sessionID) tabTitleManager.markSessionIdle(sessionID);
2409
2308
  break;
2410
2309
  }
2411
- case "session.idle": break;
2412
- case "session.error": break;
2413
2310
  case "vcs.branch.updated":
2414
2311
  tabTitleManager.setBranch(stringProperty(properties, "branch"));
2415
2312
  break;
@@ -2471,7 +2368,7 @@ function sanitizeTitle(title, maxLength = 90) {
2471
2368
  return cleaned;
2472
2369
  }
2473
2370
  var TabTitleManager = class {
2474
- sessionStatuses = /* @__PURE__ */ new Map();
2371
+ runningSessions = /* @__PURE__ */ new Set();
2475
2372
  pendingInputs = /* @__PURE__ */ new Map();
2476
2373
  branchName;
2477
2374
  desiredTitle;
@@ -2512,44 +2409,30 @@ var TabTitleManager = class {
2512
2409
  this.branchName = trimmed;
2513
2410
  this.scheduleUpdate();
2514
2411
  }
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
2412
  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();
2413
+ const isRunning = status.type === "busy" || status.type === "retry";
2414
+ const wasRunning = this.runningSessions.has(sessionID);
2415
+ if (isRunning) {
2416
+ if (!wasRunning) {
2417
+ this.runningSessions.add(sessionID);
2418
+ this.scheduleUpdate();
2419
+ }
2420
+ } else if (wasRunning) {
2421
+ this.runningSessions.delete(sessionID);
2422
+ this.scheduleUpdate();
2423
+ }
2541
2424
  }
2542
2425
  markSessionIdle(sessionID) {
2543
- this.updateSessionStatus(sessionID, { type: "idle" });
2426
+ if (this.runningSessions.delete(sessionID)) this.scheduleUpdate();
2544
2427
  }
2545
2428
  removeSession(sessionID) {
2546
- const hadSessionStatus = this.sessionStatuses.delete(sessionID);
2429
+ const hadRunning = this.runningSessions.delete(sessionID);
2547
2430
  let hadPendingInput = false;
2548
2431
  for (const [id, pendingSessionID] of this.pendingInputs) if (pendingSessionID === sessionID) {
2549
2432
  this.pendingInputs.delete(id);
2550
2433
  hadPendingInput = true;
2551
2434
  }
2552
- if (!hadSessionStatus && !hadPendingInput) return;
2435
+ if (!hadRunning && !hadPendingInput) return;
2553
2436
  this.scheduleUpdate();
2554
2437
  }
2555
2438
  markNeedsInput(id, sessionID) {
@@ -2562,8 +2445,7 @@ var TabTitleManager = class {
2562
2445
  this.scheduleUpdate();
2563
2446
  }
2564
2447
  get isBusy() {
2565
- for (const activity of this.sessionStatuses.values()) if (activity === "running") return true;
2566
- return false;
2448
+ return this.runningSessions.size > 0;
2567
2449
  }
2568
2450
  get needsInput() {
2569
2451
  return this.pendingInputs.size > 0;
@@ -2790,23 +2672,12 @@ function createZellijPtyPlugin(dependencies = {}) {
2790
2672
  }
2791
2673
  });
2792
2674
  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
- await tabTitleSnapshotRefresher?.refreshNow();
2800
2675
  tabTitleManager?.renderImmediate().catch((error) => debug("initial tab title render failed", errorMessage(error)));
2801
2676
  if (config.autoUpdate) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
2802
2677
  return {
2803
2678
  async event(input) {
2804
2679
  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();
2809
- }
2680
+ if (tabTitleManager) await handleTabTitleEvent(tabTitleManager, event);
2810
2681
  if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
2811
2682
  completionNotifications?.clearAll();
2812
2683
  completionNotifications?.dispose();