{"version":3,"file":"index.cjs","names":[],"sources":["../../../../src/services/workflowTerminal/index.ts"],"sourcesContent":["import type { WorkflowTerminalCapabilitiesView } from '../../types/webWorkflowTerminal';\n\n/**\n * One run's terminal, shared by every surface watching it.\n *\n * WHY COALESCING LIVES HERE:\n * Reading a multiplexer's screen forks a CLI, measured at roughly 75ms a call.\n * Several readers on one run (a step tab, a second browser, the session's own\n * panel) would otherwise each fork on their own timer, and a read that runs\n * long would have the next one start on top of it, unbounded. One read per run\n * at a time, no faster than the interval, however many surfaces are watching.\n *\n * WHY A CONTROL LEASE:\n * A run's terminal takes keystrokes from anyone who can reach it, so two\n * readers typing into one nested agent is a real state, not a hypothetical.\n * The lease does not make it impossible, since the session's own panel can\n * still type; it makes it visible, and keeps two cockpit tabs from fighting.\n */\n\n/** The engine's terminal facade, as this service needs it. */\nexport interface TerminalPort<Record> {\n  capabilities(record: Record): WorkflowTerminalCapabilitiesView;\n  screen(record: Record, options: { lines?: number }): Promise<string[]>;\n  write(record: Record, data: string): Promise<void>;\n  resize(record: Record, columns: number, rows: number): Promise<boolean>;\n}\n\nexport interface WorkflowTerminalDeps<Record> {\n  terminal: TerminalPort<Record>;\n  /** Injected so the cadence and the lease clock are assertable. */\n  now: () => number;\n  /** Shortest gap between two reads of one run. */\n  refreshMs?: number;\n  /** How long a keyboard lease survives without being renewed. */\n  leaseMs?: number;\n}\n\n/** A read in flight, and what the last one returned. */\ninterface ScreenEntry {\n  /**\n   * When the last read finished, absent until one has.\n   *\n   * Absent rather than zero: zero is a real instant, and a service whose clock\n   * starts near it would answer the very first read from an empty cache and\n   * paint nothing until the interval had passed.\n   */\n  at?: number;\n  lines: string[];\n  inFlight?: Promise<string[]>;\n}\n\ninterface Lease {\n  token: string;\n  expiresAt: number;\n}\n\nconst DEFAULT_REFRESH_MS = 750;\nconst DEFAULT_LEASE_MS = 60_000;\nconst NOT_HELD = 'Another reader holds the keyboard for this run.';\n\nexport interface WorkflowTerminalService<Record> {\n  /** The run's recent screen, reusing a recent read rather than forking again. */\n  screen(identity: string, record: Record, lines: number): Promise<string[]>;\n  capabilities(record: Record): WorkflowTerminalCapabilitiesView;\n  /** Takes the keyboard, or reports who has it. Renews when the holder asks again. */\n  takeControl(identity: string, token: string): boolean;\n  releaseControl(identity: string, token: string): void;\n  /** Writes on behalf of a lease holder; throws when the lease is not theirs. */\n  write(identity: string, record: Record, token: string, data: string): Promise<void>;\n  /**\n   * Matches the run's terminal to the holder's viewport.\n   *\n   * Behind the same lease as writing: geometry is shared by everyone watching,\n   * so a passive reader must not reflow the screen under the person typing.\n   */\n  resize(identity: string, record: Record, token: string, columns: number, rows: number): Promise<boolean>;\n  /** Drops what is remembered for runs that can never change again. */\n  forget(live: ReadonlySet<string>): void;\n}\n\nexport function createWorkflowTerminalService<Record>(\n  deps: WorkflowTerminalDeps<Record>,\n): WorkflowTerminalService<Record> {\n  const refreshMs = deps.refreshMs ?? DEFAULT_REFRESH_MS;\n  const leaseMs = deps.leaseMs ?? DEFAULT_LEASE_MS;\n  const screens = new Map<string, ScreenEntry>();\n  const leases = new Map<string, Lease>();\n\n  const heldBy = (identity: string): Lease | undefined => {\n    const lease = leases.get(identity);\n    if (lease === undefined) return undefined;\n    if (lease.expiresAt > deps.now()) return lease;\n    leases.delete(identity);\n    return undefined;\n  };\n\n  return {\n    async screen(identity, record, lines) {\n      const entry = screens.get(identity) ?? { lines: [] };\n      screens.set(identity, entry);\n      if (entry.inFlight) return entry.lines;\n      if (entry.at !== undefined && deps.now() - entry.at < refreshMs) return entry.lines;\n      entry.inFlight = deps.terminal.screen(record, { lines });\n      try {\n        entry.lines = await entry.inFlight;\n      } finally {\n        // Stamped after the read, not before: the interval is a gap between\n        // reads, so a slow scrape must not immediately earn another one.\n        entry.at = deps.now();\n        entry.inFlight = undefined;\n      }\n      return entry.lines;\n    },\n    capabilities(record) {\n      return deps.terminal.capabilities(record);\n    },\n    takeControl(identity, token) {\n      const holder = heldBy(identity);\n      if (holder !== undefined && holder.token !== token) return false;\n      leases.set(identity, { token, expiresAt: deps.now() + leaseMs });\n      return true;\n    },\n    releaseControl(identity, token) {\n      if (heldBy(identity)?.token === token) leases.delete(identity);\n    },\n    async write(identity, record, token, data) {\n      const holder = heldBy(identity);\n      if (holder === undefined || holder.token !== token) throw new Error(NOT_HELD);\n      leases.set(identity, { token, expiresAt: deps.now() + leaseMs });\n      await deps.terminal.write(record, data);\n    },\n    async resize(identity, record, token, columns, rows) {\n      const holder = heldBy(identity);\n      if (holder === undefined || holder.token !== token) throw new Error(NOT_HELD);\n      return deps.terminal.resize(record, columns, rows);\n    },\n    forget(live) {\n      for (const identity of screens.keys()) {\n        if (!live.has(identity)) screens.delete(identity);\n      }\n      for (const identity of leases.keys()) {\n        if (!live.has(identity)) leases.delete(identity);\n      }\n    },\n  };\n}\n"],"mappings":";AAwDA,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,WAAW;AAsBjB,SAAgB,8BACd,MACiC;CACjC,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,0BAAU,IAAI,IAAyB;CAC7C,MAAM,yBAAS,IAAI,IAAmB;CAEtC,MAAM,UAAU,aAAwC;EACtD,MAAM,QAAQ,OAAO,IAAI,QAAQ;EACjC,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAChC,IAAI,MAAM,YAAY,KAAK,IAAI,GAAG,OAAO;EACzC,OAAO,OAAO,QAAQ;CAExB;CAEA,OAAO;EACL,MAAM,OAAO,UAAU,QAAQ,OAAO;GACpC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,KAAK,EAAE,OAAO,CAAC,EAAE;GACnD,QAAQ,IAAI,UAAU,KAAK;GAC3B,IAAI,MAAM,UAAU,OAAO,MAAM;GACjC,IAAI,MAAM,OAAO,KAAA,KAAa,KAAK,IAAI,IAAI,MAAM,KAAK,WAAW,OAAO,MAAM;GAC9E,MAAM,WAAW,KAAK,SAAS,OAAO,QAAQ,EAAE,MAAM,CAAC;GACvD,IAAI;IACF,MAAM,QAAQ,MAAM,MAAM;GAC5B,UAAU;IAGR,MAAM,KAAK,KAAK,IAAI;IACpB,MAAM,WAAW,KAAA;GACnB;GACA,OAAO,MAAM;EACf;EACA,aAAa,QAAQ;GACnB,OAAO,KAAK,SAAS,aAAa,MAAM;EAC1C;EACA,YAAY,UAAU,OAAO;GAC3B,MAAM,SAAS,OAAO,QAAQ;GAC9B,IAAI,WAAW,KAAA,KAAa,OAAO,UAAU,OAAO,OAAO;GAC3D,OAAO,IAAI,UAAU;IAAE;IAAO,WAAW,KAAK,IAAI,IAAI;GAAQ,CAAC;GAC/D,OAAO;EACT;EACA,eAAe,UAAU,OAAO;GAC9B,IAAI,OAAO,QAAQ,CAAC,EAAE,UAAU,OAAO,OAAO,OAAO,QAAQ;EAC/D;EACA,MAAM,MAAM,UAAU,QAAQ,OAAO,MAAM;GACzC,MAAM,SAAS,OAAO,QAAQ;GAC9B,IAAI,WAAW,KAAA,KAAa,OAAO,UAAU,OAAO,MAAM,IAAI,MAAM,QAAQ;GAC5E,OAAO,IAAI,UAAU;IAAE;IAAO,WAAW,KAAK,IAAI,IAAI;GAAQ,CAAC;GAC/D,MAAM,KAAK,SAAS,MAAM,QAAQ,IAAI;EACxC;EACA,MAAM,OAAO,UAAU,QAAQ,OAAO,SAAS,MAAM;GACnD,MAAM,SAAS,OAAO,QAAQ;GAC9B,IAAI,WAAW,KAAA,KAAa,OAAO,UAAU,OAAO,MAAM,IAAI,MAAM,QAAQ;GAC5E,OAAO,KAAK,SAAS,OAAO,QAAQ,SAAS,IAAI;EACnD;EACA,OAAO,MAAM;GACX,KAAK,MAAM,YAAY,QAAQ,KAAK,GAClC,IAAI,CAAC,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ;GAElD,KAAK,MAAM,YAAY,OAAO,KAAK,GACjC,IAAI,CAAC,KAAK,IAAI,QAAQ,GAAG,OAAO,OAAO,QAAQ;EAEnD;CACF;AACF"}