linkshell-cli 0.5.24 → 0.5.26

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,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, join, relative } from "node:path";
4
4
  import {
@@ -1944,12 +1944,83 @@ export class AgentWorkspaceProxy {
1944
1944
  }
1945
1945
 
1946
1946
  stop(): void {
1947
+ for (const watcher of this.fsWatchers) {
1948
+ try { watcher.close(); } catch { /* already closed */ }
1949
+ }
1950
+ this.fsWatchers = [];
1951
+ if (this.fsSyncTimer) {
1952
+ clearTimeout(this.fsSyncTimer);
1953
+ this.fsSyncTimer = undefined;
1954
+ }
1947
1955
  for (const client of this.clients.values()) {
1948
1956
  client.stop();
1949
1957
  }
1950
1958
  this.clients.clear();
1951
1959
  }
1952
1960
 
1961
+ // ── External-session live refresh ────────────────────────────────────
1962
+ // The console requests the session list only once, at connect. Without this,
1963
+ // a session another `claude`/`codex` process starts (or whose status changes)
1964
+ // after connect never shows up — the tree is frozen at connect time. Watch the
1965
+ // on-disk session stores and re-sync + push when they change, so externally-
1966
+ // driven sessions surface live (this is what CodeIsland does). Event-driven:
1967
+ // zero cost while idle, throttled to at most one re-scan per window during a
1968
+ // burst of writes (an active turn appends constantly).
1969
+ private fsWatchers: ReturnType<typeof watch>[] = [];
1970
+ private fsSyncTimer: ReturnType<typeof setTimeout> | undefined;
1971
+ private fsSyncRunning = false;
1972
+ private static readonly FS_SYNC_THROTTLE_MS = 1500;
1973
+
1974
+ private startSessionWatchers(): void {
1975
+ if (this.fsWatchers.length > 0) return; // already watching
1976
+ const roots = [
1977
+ join(homedir(), ".claude", "projects"),
1978
+ join(homedir(), ".codex", "sessions"),
1979
+ ];
1980
+ for (const root of roots) {
1981
+ if (!existsSync(root)) continue;
1982
+ try {
1983
+ // recursive is supported on macOS/Windows; on Linux this throws, and we
1984
+ // simply don't get live updates (the connect-time scan still works).
1985
+ const watcher = watch(root, { recursive: true, persistent: false }, () => this.scheduleSessionSync());
1986
+ watcher.on("error", () => { /* transient watch error — ignore */ });
1987
+ this.fsWatchers.push(watcher);
1988
+ } catch {
1989
+ // recursive watch unavailable on this platform — skip silently.
1990
+ }
1991
+ }
1992
+ }
1993
+
1994
+ private scheduleSessionSync(): void {
1995
+ if (this.fsSyncTimer) return; // a sync is already scheduled in this window
1996
+ this.fsSyncTimer = setTimeout(() => {
1997
+ this.fsSyncTimer = undefined;
1998
+ void this.syncAndPushSessions();
1999
+ }, AgentWorkspaceProxy.FS_SYNC_THROTTLE_MS);
2000
+ }
2001
+
2002
+ private async syncAndPushSessions(): Promise<void> {
2003
+ if (this.fsSyncRunning) return; // don't overlap with an in-flight sync
2004
+ this.fsSyncRunning = true;
2005
+ try {
2006
+ await this.syncProviderSessions();
2007
+ // Push the conversation list (statuses) — lighter than a full snapshot and
2008
+ // exactly what refreshes the tree. The web handles an unsolicited
2009
+ // conversation.list.result the same as a requested one.
2010
+ this.input.send(createEnvelope({
2011
+ type: "agent.v2.conversation.list.result",
2012
+ sessionId: this.input.sessionId,
2013
+ payload: { conversations: [...this.conversations.values()].filter((c) => !c.archived) },
2014
+ }));
2015
+ } catch (error) {
2016
+ if (this.input.verbose) {
2017
+ process.stderr.write(`[agent:v2] fs-watch sync failed: ${error instanceof Error ? error.message : String(error)}\n`);
2018
+ }
2019
+ } finally {
2020
+ this.fsSyncRunning = false;
2021
+ }
2022
+ }
2023
+
1953
2024
  private clientForProvider(provider: AgentProvider): AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined {
1954
2025
  return this.clients.get(provider);
1955
2026
  }
@@ -1996,6 +2067,9 @@ export class AgentWorkspaceProxy {
1996
2067
  this.status = "idle";
1997
2068
  this.error = undefined;
1998
2069
  this.sendCapabilities();
2070
+ // Start watching the on-disk session stores so externally-started sessions
2071
+ // and status changes surface live, not just at connect time.
2072
+ this.startSessionWatchers();
1999
2073
  }
2000
2074
 
2001
2075
  private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined> {