@gitkraken/core-gitlens 0.10.4 → 0.10.6

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/git/providers/branches.d.ts +4 -1
  3. package/dist/git/providers/refs.d.ts +7 -0
  4. package/dist/git-cli/exec/git.js +1 -1
  5. package/dist/git-cli/providers/branches.d.ts +10 -1
  6. package/dist/git-cli/providers/branches.js +31 -6
  7. package/dist/git-cli/providers/branches.js.map +1 -1
  8. package/dist/git-cli/providers/config.js +2 -2
  9. package/dist/git-cli/providers/pausedOperations.js +2 -2
  10. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  11. package/dist/git-cli/providers/refs.d.ts +12 -0
  12. package/dist/git-cli/providers/refs.js +18 -2
  13. package/dist/git-cli/providers/refs.js.map +1 -1
  14. package/dist/git-cli/providers/staging.js +1 -1
  15. package/dist/ipc/discovery.js +1 -1
  16. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  17. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  18. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  19. package/dist/plus/git-github/api/github.js +1 -1
  20. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +2 -1
  21. package/dist/plus/integrations/authentication/configuredIntegrationService.js +11 -6
  22. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  23. package/dist/plus/integrations/authentication/models.d.ts +11 -0
  24. package/dist/plus/integrations/authentication/models.js.map +1 -1
  25. package/dist/plus/integrations/constants.d.ts +4 -0
  26. package/dist/plus/integrations/constants.js.map +1 -1
  27. package/dist/plus/integrations/integrationService.d.ts +6 -0
  28. package/dist/plus/integrations/integrationService.js +32 -3
  29. package/dist/plus/integrations/integrationService.js.map +1 -1
  30. package/dist/plus/integrations/models/integration.d.ts +6 -1
  31. package/dist/plus/integrations/models/integration.js +7 -0
  32. package/dist/plus/integrations/models/integration.js.map +1 -1
  33. package/dist/plus/integrations/providers/azure/azure.js +1 -1
  34. package/dist/plus/integrations/providers/github.js +1 -1
  35. package/dist/plus/integrations/providers/linear.d.ts +9 -1
  36. package/dist/plus/integrations/providers/linear.js +34 -11
  37. package/dist/plus/integrations/providers/linear.js.map +1 -1
  38. package/dist/utils/env/node/exec.js +2 -2
  39. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"claudeCodeTranscript.js","names":["readdir"],"sources":["../../../../../plus/agents/src/providers/claudeCodeTranscript.ts"],"sourcesContent":["import { createReadStream } from 'fs';\nimport { open, readdir, readFile, stat } from 'fs/promises';\nimport { homedir } from 'os';\nimport { basename, dirname, join } from 'path';\nimport { createInterface } from 'readline';\n\nexport interface TranscriptTitles {\n\tcustom?: string;\n\tai?: string;\n\tagent?: string;\n}\n\n/** A transcript file discovered by {@link ClaudeCodeTranscriptReader.listSessions}, before its\n * contents are read — everything here comes from `readdir` + `stat`. */\nexport interface TranscriptSessionEntry {\n\treadonly sessionId: string;\n\treadonly path: string;\n\treadonly lastActivityMs: number;\n\treadonly size: number;\n}\n\n/** A {@link TranscriptSessionEntry} enriched with the fields recovered from the file's head/tail. */\nexport interface TranscriptSessionSummary extends TranscriptSessionEntry {\n\treadonly titles: TranscriptTitles;\n\treadonly lastPrompt?: string;\n}\n\nexport interface TranscriptSessionListing {\n\treadonly sessions: TranscriptSessionSummary[];\n\t/** Every transcript in the directory, not just the summarized slice — drives \"Showing N of M\". */\n\treadonly total: number;\n}\n\n/** A transcript found outside the queried cwd's project directory, plus the directory Claude\n * actually uses to resolve it for `--resume`. */\nexport interface ResumableTranscriptSessionSummary extends TranscriptSessionSummary {\n\treadonly cwd: string;\n}\n\nexport interface ResumableTranscriptSessionListing {\n\treadonly sessions: ResumableTranscriptSessionSummary[];\n\treadonly total: number;\n}\n\nexport interface EndedTranscriptDetails {\n\ttitles: TranscriptTitles;\n\tfirstPrompt?: string;\n\tlastPrompt?: string;\n}\n\ninterface CacheEntry {\n\tpath: string;\n\tmtimeMs: number;\n\tsize: number;\n\tnextOffset: number;\n\ttitles: TranscriptTitles;\n}\n\ninterface ListingCacheEntry {\n\tentries: TranscriptSessionEntry[];\n\tresolvedAt: number;\n}\n\ninterface NameCacheEntry {\n\tnames: string[];\n\tresolvedAt: number;\n}\n\ninterface SummaryCacheEntry {\n\tpath: string;\n\tmtimeMs: number;\n\tsize: number;\n\tsummary: TranscriptSessionSummary;\n}\n\ninterface TitleEntry {\n\ttype: string;\n\tsessionId?: string;\n\tcustomTitle?: string;\n\taiTitle?: string;\n\tagentName?: string;\n\tlastPrompt?: string;\n}\n\n/** Bytes read from each end of a transcript when summarizing. Titles and prompts cluster at both\n * extremes, and files run to tens of MB, so reading whole files is not an option. */\nconst summaryWindowSize = 64 * 1024;\n/** Listings are cheap (readdir + stat) but re-run per panel/sheet open; a short TTL absorbs bursts.\n * Time-based rather than dir-mtime-based because appends move file mtimes without touching the dir. */\nconst listingCacheTtlMs = 10 * 1000;\n/** Summaries are keyed by session, and a busy project has hundreds — unlike the per-live-session\n * title cache, this needs a ceiling. Sized to hold a whole junk-heavy store: the listing scan runs\n * to exhaustion, and each entry is tiny (a path plus a few short strings), so a cap below the\n * store's size would evict-and-re-read the junk tail on every listing instead of paying its\n * windowed reads once. */\nconst summaryCacheLimit = 1000;\nconst defaultListLimit = 50;\n\n/**\n * Reads Claude Code transcript JSONL files at `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`\n * to surface `custom-title`, `ai-title`, and `agent-name` entries as fallback session names.\n *\n * Maintains a per-session cache keyed by mtime + last-read byte offset, so repeated calls only\n * read the appended tail. Last occurrence per type wins.\n *\n * `resolve` is async, so a `forget` (or a newer `resolve`) for the same session can land between\n * its `await`s. A per-session generation counter is stamped at entry and re-checked before the\n * final cache write, so a stale `resolve` can't resurrect a forgotten entry or clobber a newer\n * resolve's result.\n */\nexport class ClaudeCodeTranscriptReader {\n\tprivate readonly _cache = new Map<string, CacheEntry>();\n\tprivate readonly _generations = new Map<string, number>();\n\tprivate readonly _listings = new Map<string, ListingCacheEntry>();\n\t/** Transcript file names per project directory — the readdir behind both the plain listing and\n\t * the by-id scan, which sweeps every project directory. */\n\tprivate readonly _names = new Map<string, NameCacheEntry>();\n\t/** Project directories under {@link getProjectsRoot}, re-scanned by every by-id recovery. */\n\tprivate _projectDirs: { dirs: string[]; resolvedAt: number } | undefined;\n\t/** Insertion-ordered LRU — re-inserted on hit, oldest key evicted past {@link summaryCacheLimit}. */\n\tprivate readonly _summaries = new Map<string, SummaryCacheEntry>();\n\t/** Resolved project path per transcript path, bounded like {@link _summaries}. */\n\tprivate readonly _transcriptCwds = new Map<string, string>();\n\tprivate _nextGen = 0;\n\t/** Bumped by {@link invalidateListings}; async listing scans capture it at entry and skip their\n\t * cache write when it moved, so a stale in-flight result can't re-seed a just-cleared cache. */\n\tprivate _listingGen = 0;\n\n\t/** Drops the directory-listing caches (entries, names, project dirs) so the next listing re-reads\n\t * the store. Called when a session ends or is removed: its transcript may have appeared inside\n\t * the TTL window, and a cached listing would hide the new file from the very fetch meant to\n\t * surface it. Summary and cwd caches are keyed by file identity and stay valid, so they're kept.\n\t * The generation bump makes any in-flight listing skip its own cache write — otherwise a scan\n\t * started before the invalidation would re-seed the caches with pre-end results, freshly\n\t * timestamped (same discipline as `resolve`'s per-session generations). */\n\tinvalidateListings(): void {\n\t\tthis._listingGen++;\n\t\tthis._listings.clear();\n\t\tthis._names.clear();\n\t\tthis._projectDirs = undefined;\n\t}\n\n\tasync resolve(sessionId: string, cwd: string | undefined): Promise<TranscriptTitles | undefined> {\n\t\tconst gen = ++this._nextGen;\n\t\tthis._generations.set(sessionId, gen);\n\n\t\tconst cached = this._cache.get(sessionId);\n\t\tconst path = cached?.path ?? (await this.locateTranscript(sessionId, cwd));\n\t\tif (path == null) return undefined;\n\n\t\tlet stats;\n\t\ttry {\n\t\t\tstats = await stat(path);\n\t\t} catch {\n\t\t\t// File disappeared; drop any stale cache entry so a future call retries discovery.\n\t\t\t// Skip the delete if a newer resolve/forget claimed the slot — their state is fresher.\n\t\t\tif (this._generations.get(sessionId) === gen) {\n\t\t\t\tthis._cache.delete(sessionId);\n\t\t\t\tthis._generations.delete(sessionId);\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (stats.mtimeMs === cached?.mtimeMs && stats.size === cached?.size) {\n\t\t\treturn cached.titles;\n\t\t}\n\n\t\t// Truncation or rewrite: stat shrank below our read cursor or mtime moved backward → full re-scan.\n\t\tconst startFromScratch = cached == null || stats.size < cached.nextOffset || stats.mtimeMs < cached.mtimeMs;\n\t\tconst startOffset = startFromScratch ? 0 : cached.nextOffset;\n\t\tconst baseTitles: TranscriptTitles = startFromScratch ? {} : { ...cached.titles };\n\n\t\tconst { titles, consumedEnd } =\n\t\t\tstats.size > startOffset\n\t\t\t\t? await this.readRange(path, startOffset, stats.size, sessionId, baseTitles)\n\t\t\t\t: { titles: baseTitles, consumedEnd: startOffset };\n\n\t\t// If `forget` ran or a newer `resolve` started while we were awaiting, skip the cache write\n\t\t// — the entry we'd be writing reflects stale state.\n\t\tif (this._generations.get(sessionId) !== gen) return titles;\n\n\t\tconst entry: CacheEntry = {\n\t\t\tpath: path,\n\t\t\tmtimeMs: stats.mtimeMs,\n\t\t\tsize: stats.size,\n\t\t\tnextOffset: consumedEnd,\n\t\t\ttitles: titles,\n\t\t};\n\t\tthis._cache.set(sessionId, entry);\n\t\treturn titles;\n\t}\n\n\tforget(sessionId: string): void {\n\t\tthis._cache.delete(sessionId);\n\t\tthis._generations.delete(sessionId);\n\t}\n\n\t/**\n\t * One-shot read for an *ended* (terminal, static) session: extracts the transcript titles plus\n\t * the first and last user prompts. Unlike {@link resolve}, it bypasses the tail cache — a finished\n\t * transcript never grows, so it's read once on demand (when an ended row is opened) and the\n\t * result applied to the session. `last-prompt` entries carry the prompt as of that point; the first\n\t * one bearing a value is the session's opening prompt, the last its most recent. Returns `undefined`\n\t * when no transcript is found (archived/purged) — the caller keeps whatever durable-store fields it\n\t * already has.\n\t *\n\t * Streams the file line-by-line rather than buffering it whole: a finished transcript can run to\n\t * tens of MB, but this keeps only one line resident at a time. A *full* scan (not a head/tail\n\t * window) is required for correctness — when the first turn is large the opening `last-prompt` sits\n\t * past any fixed head window, and a windowed read would then mistake a later prompt for the first,\n\t * poisoning the derived session name.\n\t */\n\tasync resolveEndedDetails(sessionId: string, cwd: string | undefined): Promise<EndedTranscriptDetails | undefined> {\n\t\tconst path = await this.locateTranscript(sessionId, cwd);\n\t\tif (path == null) return undefined;\n\n\t\tlet stats;\n\t\ttry {\n\t\t\tstats = await stat(path);\n\t\t} catch (ex) {\n\t\t\t// A transcript that vanished is terminal; an I/O or permission failure must propagate so\n\t\t\t// the caller retries rather than caching this session as permanently unresolvable.\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn undefined;\n\t\t}\n\t\tif (stats.size === 0) return undefined;\n\n\t\tconst titles: TranscriptTitles = {};\n\t\tlet firstPrompt: string | undefined;\n\t\tlet lastPrompt: string | undefined;\n\n\t\tconst rl = createInterface({ input: createReadStream(path, { encoding: 'utf8' }), crlfDelay: Infinity });\n\t\ttry {\n\t\t\tfor await (const line of rl) {\n\t\t\t\tapplyTitleLine(line, sessionId, titles);\n\t\t\t\tif (!line.includes('last-prompt')) continue;\n\n\t\t\t\tconst entry = parseSummaryLine(line, sessionId);\n\t\t\t\tif (entry?.type === 'last-prompt' && entry.lastPrompt != null && entry.lastPrompt.length > 0) {\n\t\t\t\t\tfirstPrompt ??= entry.lastPrompt;\n\t\t\t\t\tlastPrompt = entry.lastPrompt;\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\trl.close();\n\t\t}\n\n\t\treturn { titles: titles, firstPrompt: firstPrompt, lastPrompt: lastPrompt };\n\t}\n\n\t/**\n\t * Lists the transcripts of sessions whose working directory is `cwd`, most-recently-active first,\n\t * summarizing until `limit` summarizable transcripts are found — not just the first `limit` on\n\t * disk, since junk transcripts (dropped below) would otherwise starve the result.\n\t * `excludeSessionIds` is skipped before `limit` applies, and excluded entries are also dropped\n\t * from `total` — they're already shown elsewhere (e.g. as live sessions), so a caller's \"N of M\"\n\t * count must not double-count them.\n\t *\n\t * Claude homes a transcript under the directory encoding the cwd from which the session started.\n\t * That directory is authoritative for where `claude --resume <id>` can find the transcript, but\n\t * Claude does not reliably move it when the session later changes worktrees. Callers that have a\n\t * durable session-to-worktree association can recover those moved sessions with\n\t * {@link listSessionsByIds}; this method intentionally remains the cheap legacy directory listing.\n\t *\n\t * Discovery (readdir + stat) covers the whole directory and is cheap; summarizing is not, so it's\n\t * capped. Entries whose summary yields neither a title nor a prompt are dropped — those are aborted\n\t * or empty transcripts with nothing to show or search on.\n\t */\n\tasync listSessions(\n\t\tcwd: string,\n\t\toptions?: { limit?: number; excludeSessionIds?: ReadonlySet<string> },\n\t): Promise<TranscriptSessionListing> {\n\t\tconst dir = await this.resolveProjectDir(cwd);\n\t\tif (dir == null) return { sessions: [], total: 0 };\n\n\t\tconst entries = await this.listEntries(dir);\n\t\tif (entries.length === 0) return { sessions: [], total: 0 };\n\n\t\tconst limit = options?.limit ?? defaultListLimit;\n\t\tconst exclude = options?.excludeSessionIds;\n\t\t// Zero-byte transcripts can never summarize — skip the read entirely and keep them out of\n\t\t// `total`, which otherwise counts sessions no `limit` could ever surface.\n\t\tconst candidates = entries.filter(e => e.size > 0 && exclude?.has(e.sessionId) !== true);\n\t\tconst { sessions, exhausted } = await this.collectSessions(candidates, limit, async entry => {\n\t\t\tconst summary = await this.resolveSummary(entry);\n\t\t\treturn summary != null && hasSummaryContent(summary) ? summary : undefined;\n\t\t});\n\n\t\t// An exhausted scan proved the exact valid count; otherwise unscanned candidates keep\n\t\t// `total` an upper bound so paging knows more MAY exist.\n\t\treturn { sessions: sessions, total: exhausted ? sessions.length : candidates.length };\n\t}\n\n\t/**\n\t * Finds a known set of session ids across every Claude project directory except `excludeCwd`'s.\n\t * This is the recovery path for durable CLI records whose worktree changed after Claude chose the\n\t * transcript's project directory. The caller supplies the association; transcript contents alone\n\t * are deliberately not used to guess which current worktree owns an arbitrary old session.\n\t *\n\t * Each result carries the project path encoded by the directory that actually holds the transcript,\n\t * so resuming it does not repeat discovery from the wrong worktree. Duplicate ids are counted once,\n\t * with the newest transcript winning.\n\t */\n\tasync listSessionsByIds(\n\t\tsessionIds: ReadonlySet<string>,\n\t\toptions?: { limit?: number; excludeCwd?: string },\n\t): Promise<ResumableTranscriptSessionListing> {\n\t\tif (sessionIds.size === 0) return { sessions: [], total: 0 };\n\n\t\tconst excludeDir = await this.resolveProjectDir(options?.excludeCwd);\n\t\t// Same zero-byte drop as `listSessions` — see the rationale there.\n\t\tconst entries = (await this.listEntriesByIds(sessionIds, excludeDir)).filter(e => e.size > 0);\n\t\tif (entries.length === 0) return { sessions: [], total: 0 };\n\n\t\tconst limit = options?.limit ?? defaultListLimit;\n\t\tconst { sessions, exhausted } = await this.collectSessions(entries, limit, async entry => {\n\t\t\tconst summary = await this.resolveSummary(entry);\n\t\t\tif (summary == null || !hasSummaryContent(summary)) return undefined;\n\n\t\t\tconst cwd = await this.resolveTranscriptCwd(entry);\n\t\t\treturn cwd != null ? { ...summary, cwd: cwd } : undefined;\n\t\t});\n\n\t\t// Exhausted scans prove the exact count — see `listSessions`.\n\t\treturn { sessions: sessions, total: exhausted ? sessions.length : entries.length };\n\t}\n\n\t/** Discovers every transcript in `dir`, newest first. The directory also holds one sibling\n\t * subdirectory per session (`<uuid>/subagents`, `<uuid>/tool-results`), so entries must be filtered\n\t * to files — a busy project has roughly as many subdirectories as transcripts. */\n\tprivate async listEntries(dir: string): Promise<TranscriptSessionEntry[]> {\n\t\tconst cached = this._listings.get(dir);\n\t\tif (cached != null && Date.now() - cached.resolvedAt < listingCacheTtlMs) return cached.entries;\n\n\t\tconst gen = this._listingGen;\n\t\tlet names: string[];\n\t\ttry {\n\t\t\tnames = await this.listTranscriptNames(dir);\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst entries = (await this.statEntries(dir, names)).sort((a, b) => b.lastActivityMs - a.lastActivityMs);\n\n\t\tif (gen === this._listingGen) {\n\t\t\tthis._listings.set(dir, { entries: entries, resolvedAt: Date.now() });\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/** Stats `names` in `dir`, dropping the ones that failed — a transcript can vanish (or be\n\t * archived) between the readdir and the stat. Unsorted; callers order as they need.\n\t * Protected (like {@link readSummary}) so tests can interleave against the listing flow. */\n\tprotected async statEntries(dir: string, names: string[]): Promise<TranscriptSessionEntry[]> {\n\t\tconst settled = await Promise.allSettled(\n\t\t\tnames.map(async (name): Promise<TranscriptSessionEntry> => {\n\t\t\t\tconst path = join(dir, name);\n\t\t\t\tconst stats = await stat(path);\n\t\t\t\treturn {\n\t\t\t\t\tsessionId: basename(name, '.jsonl'),\n\t\t\t\t\tpath: path,\n\t\t\t\t\tlastActivityMs: stats.mtimeMs,\n\t\t\t\t\tsize: stats.size,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\n\t\treturn settled.filter(r => r.status === 'fulfilled').map(r => r.value);\n\t}\n\n\t/** The `.jsonl` file names in `dir`, cached for {@link listingCacheTtlMs}. Failures propagate\n\t * uncached: an unreadable directory means different things to different callers, and the next\n\t * call must be free to get a real answer. */\n\tprivate async listTranscriptNames(dir: string): Promise<string[]> {\n\t\tconst cached = this._names.get(dir);\n\t\tif (cached != null && Date.now() - cached.resolvedAt < listingCacheTtlMs) return cached.names;\n\n\t\tconst gen = this._listingGen;\n\t\tconst dirents = await readdir(dir, { withFileTypes: true });\n\t\tconst names = dirents.filter(d => d.isFile() && d.name.endsWith('.jsonl')).map(d => d.name);\n\t\tif (gen === this._listingGen) {\n\t\t\tthis._names.set(dir, { names: names, resolvedAt: Date.now() });\n\t\t}\n\n\t\treturn names;\n\t}\n\n\t/** Every project directory under the projects root, cached for {@link listingCacheTtlMs} — a\n\t * by-id recovery sweeps all of them, once per queried worktree. */\n\tprivate async listProjectDirs(): Promise<string[]> {\n\t\tconst cached = this._projectDirs;\n\t\tif (cached != null && Date.now() - cached.resolvedAt < listingCacheTtlMs) return cached.dirs;\n\n\t\tconst gen = this._listingGen;\n\t\tconst root = this.getProjectsRoot();\n\t\tconst dirents = await readdir(root, { withFileTypes: true });\n\t\tconst dirs = dirents.filter(d => d.isDirectory()).map(d => join(root, d.name));\n\t\tif (gen === this._listingGen) {\n\t\t\tthis._projectDirs = { dirs: dirs, resolvedAt: Date.now() };\n\t\t}\n\n\t\treturn dirs;\n\t}\n\n\t/** Scans project directory names once, then stats only requested transcript ids. */\n\tprivate async listEntriesByIds(\n\t\tsessionIds: ReadonlySet<string>,\n\t\texcludeDir: string | undefined,\n\t): Promise<TranscriptSessionEntry[]> {\n\t\tlet idsToFind = sessionIds;\n\t\tif (excludeDir != null) {\n\t\t\ttry {\n\t\t\t\tconst names = await this.listTranscriptNames(excludeDir);\n\t\t\t\tconst idsInExcludedDir = new Set(names.map(name => basename(name, '.jsonl')));\n\t\t\t\tidsToFind = new Set([...sessionIds].filter(id => !idsInExcludedDir.has(id)));\n\t\t\t\tif (idsToFind.size === 0) return [];\n\t\t\t} catch {\n\t\t\t\t// If the exact directory can't be inspected, don't risk returning a duplicate from a\n\t\t\t\t// stale transcript copy elsewhere. The ordinary listing still owns the exact path.\n\t\t\t\treturn [];\n\t\t\t}\n\t\t}\n\n\t\tlet dirs: string[];\n\t\ttry {\n\t\t\tdirs = await this.listProjectDirs();\n\t\t} catch (ex) {\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn [];\n\t\t}\n\n\t\tconst settledDirs = await Promise.allSettled(\n\t\t\tdirs.map(async dir => {\n\t\t\t\tif (dir === excludeDir) return [];\n\n\t\t\t\tconst names = (await this.listTranscriptNames(dir)).filter(name =>\n\t\t\t\t\tidsToFind.has(basename(name, '.jsonl')),\n\t\t\t\t);\n\t\t\t\treturn this.statEntries(dir, names);\n\t\t\t}),\n\t\t);\n\n\t\tconst bySessionId = new Map<string, TranscriptSessionEntry>();\n\t\tfor (const settledDir of settledDirs) {\n\t\t\tif (settledDir.status !== 'fulfilled') continue;\n\n\t\t\tfor (const entry of settledDir.value) {\n\t\t\t\tconst existing = bySessionId.get(entry.sessionId);\n\t\t\t\tif (existing == null || entry.lastActivityMs > existing.lastActivityMs) {\n\t\t\t\t\tbySessionId.set(entry.sessionId, entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn [...bySessionId.values()].sort((a, b) => b.lastActivityMs - a.lastActivityMs);\n\t}\n\n\t/** `exhausted` reports whether every candidate was scanned — an exhausted scan has proven the\n\t * exact valid count, so callers can report it instead of an upper bound that would keep a\n\t * \"Show More\" affordance alive with nothing left to show. */\n\tprivate async collectSessions<T>(\n\t\tcandidates: readonly TranscriptSessionEntry[],\n\t\tlimit: number,\n\t\tresolve: (entry: TranscriptSessionEntry) => Promise<T | undefined>,\n\t): Promise<{ sessions: T[]; exhausted: boolean }> {\n\t\tconst sessions: T[] = [];\n\t\tconst pushResolved = (settled: PromiseSettledResult<T | undefined>[]): void => {\n\t\t\tfor (const result of settled) {\n\t\t\t\tif (result.status !== 'fulfilled' || result.value == null) continue;\n\n\t\t\t\tsessions.push(result.value);\n\t\t\t}\n\t\t};\n\n\t\t// limit <= 0 means \"no ceiling\" — resolve every candidate.\n\t\tif (limit <= 0) {\n\t\t\tpushResolved(await Promise.allSettled(candidates.map(resolve)));\n\t\t\treturn { sessions: sessions, exhausted: true };\n\t\t}\n\n\t\t// Scans until `limit` results or the candidates run out — a fixed scan ceiling would let a run\n\t\t// of junk transcripts hide every older real session behind it (and the picker then claims there\n\t\t// are none at all). The cost stays bounded: callers pre-drop zero-byte candidates read-free, and\n\t\t// a junk summary is windowed, tiny, and LRU-cached, so the sweep is paid in syscalls, once.\n\t\tlet cursor = 0;\n\t\twhile (sessions.length < limit && cursor < candidates.length) {\n\t\t\t// Batched by remaining need so a hit-rich head stops the scan early instead of resolving\n\t\t\t// every candidate up front.\n\t\t\tconst batch = candidates.slice(cursor, cursor + (limit - sessions.length));\n\t\t\tcursor += batch.length;\n\t\t\tpushResolved(await Promise.allSettled(batch.map(resolve)));\n\t\t}\n\n\t\treturn { sessions: sessions, exhausted: cursor >= candidates.length };\n\t}\n\n\t/** Summarizes one transcript, reusing the cached result while its mtime and size are unchanged. */\n\tprivate async resolveSummary(entry: TranscriptSessionEntry): Promise<TranscriptSessionSummary | undefined> {\n\t\tconst cached = this._summaries.get(entry.sessionId);\n\t\tif (\n\t\t\tcached != null &&\n\t\t\tcached.path === entry.path &&\n\t\t\tcached.mtimeMs === entry.lastActivityMs &&\n\t\t\tcached.size === entry.size\n\t\t) {\n\t\t\t// Refresh recency for the LRU.\n\t\t\tthis._summaries.delete(entry.sessionId);\n\t\t\tthis._summaries.set(entry.sessionId, cached);\n\t\t\treturn cached.summary;\n\t\t}\n\n\t\tlet summary: TranscriptSessionSummary;\n\t\ttry {\n\t\t\tsummary = await this.readSummary(entry);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis._summaries.set(entry.sessionId, {\n\t\t\tpath: entry.path,\n\t\t\tmtimeMs: entry.lastActivityMs,\n\t\t\tsize: entry.size,\n\t\t\tsummary: summary,\n\t\t});\n\t\tif (this._summaries.size > summaryCacheLimit) {\n\t\t\tconst oldest = this._summaries.keys().next();\n\t\t\tif (!oldest.done) {\n\t\t\t\tthis._summaries.delete(oldest.value);\n\t\t\t}\n\t\t}\n\t\treturn summary;\n\t}\n\n\t/** Recovers the project path represented by the transcript's containing directory. A resolved\n\t * answer is cached: it's a line-scan of the whole transcript, and the directory holding a given\n\t * path never changes. A miss isn't cached — a `cwd` line can still land on append. */\n\tprivate async resolveTranscriptCwd(entry: TranscriptSessionEntry): Promise<string | undefined> {\n\t\tconst cached = this._transcriptCwds.get(entry.path);\n\t\tif (cached != null) return cached;\n\n\t\tconst projectDir = dirname(entry.path);\n\t\tconst projectDirName = basename(projectDir);\n\t\tconst matchesProjectDir = (cwd: string): boolean =>\n\t\t\tencodeProjectDirName(cwd).toLowerCase() === projectDirName.toLowerCase();\n\n\t\ttry {\n\t\t\tconst index = JSON.parse(await readFile(join(projectDir, 'sessions-index.json'), 'utf8')) as {\n\t\t\t\tentries?: { sessionId?: unknown; projectPath?: unknown }[];\n\t\t\t};\n\t\t\tif (Array.isArray(index.entries)) {\n\t\t\t\tconst indexed = index.entries.find(item => item?.sessionId === entry.sessionId);\n\t\t\t\tif (typeof indexed?.projectPath === 'string' && matchesProjectDir(indexed.projectPath)) {\n\t\t\t\t\tthis.rememberTranscriptCwd(entry.path, indexed.projectPath);\n\t\t\t\t\treturn indexed.projectPath;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// The index is optional and can lag or be malformed. The transcript is authoritative below.\n\t\t}\n\n\t\tconst rl = createInterface({ input: createReadStream(entry.path, { encoding: 'utf8' }), crlfDelay: Infinity });\n\t\ttry {\n\t\t\tfor await (const line of rl) {\n\t\t\t\tif (!line.includes('\"cwd\"')) continue;\n\n\t\t\t\tlet parsed: { cwd?: unknown; sessionId?: unknown };\n\t\t\t\ttry {\n\t\t\t\t\tparsed = JSON.parse(line) as { cwd?: unknown; sessionId?: unknown };\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (parsed.sessionId != null && parsed.sessionId !== entry.sessionId) continue;\n\n\t\t\t\tif (typeof parsed.cwd === 'string' && matchesProjectDir(parsed.cwd)) {\n\t\t\t\t\tthis.rememberTranscriptCwd(entry.path, parsed.cwd);\n\t\t\t\t\treturn parsed.cwd;\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\trl.close();\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\tprivate rememberTranscriptCwd(path: string, cwd: string): void {\n\t\tthis._transcriptCwds.set(path, cwd);\n\t\tif (this._transcriptCwds.size > summaryCacheLimit) {\n\t\t\tconst oldest = this._transcriptCwds.keys().next();\n\t\t\tif (!oldest.done) {\n\t\t\t\tthis._transcriptCwds.delete(oldest.value);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Reads a summary from the first and last {@link summaryWindowSize} bytes of the transcript.\n\t *\n\t * Deliberately does not reuse `resolve` — that scans from the last-read offset and buffers the whole\n\t * range, which for a cold multi-MB transcript means loading the entire file. Titles are written early\n\t * and re-written as they're refined, while prompts land at the tail, so the two windows recover both\n\t * at a fraction of the bytes. The tail is parsed second so the newest value wins.\n\t */\n\tprotected async readSummary(entry: TranscriptSessionEntry): Promise<TranscriptSessionSummary> {\n\t\tconst titles: TranscriptTitles = {};\n\t\tlet lastPrompt: string | undefined;\n\n\t\tconst apply = (buffer: Buffer, dropPartialFirstLine: boolean): void => {\n\t\t\tconst text = buffer.toString('utf8');\n\t\t\tconst lines = text.split('\\n');\n\t\t\t// A window starting mid-file almost always opens mid-line; that fragment can't be parsed.\n\t\t\tif (dropPartialFirstLine) {\n\t\t\t\tlines.shift();\n\t\t\t}\n\t\t\tfor (const line of lines) {\n\t\t\t\tconst parsed = parseSummaryLine(line, entry.sessionId);\n\t\t\t\tif (parsed == null) continue;\n\n\t\t\t\tapplyTitleEntry(parsed, titles);\n\t\t\t\tif (parsed.lastPrompt != null && parsed.lastPrompt.length > 0) {\n\t\t\t\t\tlastPrompt = parsed.lastPrompt;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tconst handle = await open(entry.path, 'r');\n\t\ttry {\n\t\t\tif (entry.size <= summaryWindowSize * 2) {\n\t\t\t\tconst whole = Buffer.alloc(entry.size);\n\t\t\t\tawait handle.read(whole, 0, entry.size, 0);\n\t\t\t\tapply(whole, false);\n\t\t\t} else {\n\t\t\t\tconst head = Buffer.alloc(summaryWindowSize);\n\t\t\t\tawait handle.read(head, 0, summaryWindowSize, 0);\n\t\t\t\tapply(head, false);\n\n\t\t\t\tconst tail = Buffer.alloc(summaryWindowSize);\n\t\t\t\tawait handle.read(tail, 0, summaryWindowSize, entry.size - summaryWindowSize);\n\t\t\t\tapply(tail, true);\n\t\t\t}\n\t\t} finally {\n\t\t\tawait handle.close();\n\t\t}\n\n\t\treturn {\n\t\t\t...entry,\n\t\t\ttitles: titles,\n\t\t\tlastPrompt: lastPrompt,\n\t\t};\n\t}\n\n\tprotected getProjectsRoot(): string {\n\t\treturn join(homedir(), '.claude', 'projects');\n\t}\n\n\t/** Resolves the `~/.claude/projects` directory holding the sessions whose working directory is\n\t * `cwd`, or `undefined` when none exists. Falls back to a case-insensitive name match because on\n\t * Windows our\n\t * paths carry a lower-cased drive letter (`normalizePath`) while Claude encodes the OS-native\n\t * `C:\\...` — so the exact name never matches by case. */\n\tprotected async resolveProjectDir(cwd: string | undefined): Promise<string | undefined> {\n\t\tif (cwd == null || cwd.length === 0) return undefined;\n\n\t\tconst root = this.getProjectsRoot();\n\t\tconst encoded = encodeProjectDirName(cwd);\n\n\t\tconst exact = join(root, encoded);\n\t\tif (await directoryExists(exact)) return exact;\n\n\t\tlet dirs: string[];\n\t\ttry {\n\t\t\tdirs = await readdir(root);\n\t\t} catch (ex) {\n\t\t\t// Only a missing projects root means \"no transcript\". A transient failure propagates so the\n\t\t\t// caller retries instead of caching a permanent miss.\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst lowered = encoded.toLowerCase();\n\t\tconst match = dirs.find(d => d !== encoded && d.toLowerCase() === lowered);\n\t\treturn match != null ? join(root, match) : undefined;\n\t}\n\n\tprotected async locateTranscript(sessionId: string, cwd: string | undefined): Promise<string | undefined> {\n\t\tconst fileName = `${sessionId}.jsonl`;\n\n\t\tconst dir = await this.resolveProjectDir(cwd);\n\t\tif (dir != null) {\n\t\t\tconst candidate = join(dir, fileName);\n\t\t\tif (await fileExists(candidate)) return candidate;\n\t\t}\n\n\t\t// The recorded cwd drifts whenever the agent `cd`s, so it can encode to a directory the session\n\t\t// doesn't actually live in — scan every project for the file as a last resort.\n\t\tconst root = this.getProjectsRoot();\n\t\tlet dirs: string[];\n\t\ttry {\n\t\t\tdirs = await readdir(root);\n\t\t} catch (ex) {\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst candidates = await Promise.all(\n\t\t\tdirs.map(async d => {\n\t\t\t\tconst candidate = join(root, d, fileName);\n\t\t\t\treturn (await fileExists(candidate)) ? candidate : undefined;\n\t\t\t}),\n\t\t);\n\t\treturn candidates.find(c => c != null);\n\t}\n\n\t/**\n\t * Reads bytes `[start, end)` from `path` and applies any title entries it finds to a copy of\n\t * `baseTitles`. Returns the merged titles and the byte position immediately after the last\n\t * newline observed — partial trailing lines (writer hasn't flushed `\\n` yet) are intentionally\n\t * left unconsumed so they're re-read on the next pass.\n\t */\n\tprotected async readRange(\n\t\tpath: string,\n\t\tstart: number,\n\t\tend: number,\n\t\tsessionId: string,\n\t\tbaseTitles: TranscriptTitles,\n\t): Promise<{ titles: TranscriptTitles; consumedEnd: number }> {\n\t\tconst buffer = await readSlice(path, start, end);\n\t\tconst titles: TranscriptTitles = { ...baseTitles };\n\n\t\tlet lastNewlineEnd = start;\n\t\tlet lineStart = 0;\n\t\tfor (let i = 0; i < buffer.length; i++) {\n\t\t\tif (buffer[i] !== 0x0a) continue;\n\n\t\t\t// Slice the line (trim a trailing \\r for CRLF files), then advance the cursor past the \\n\n\t\t\t// regardless of whether the line yielded anything — we've fully observed those bytes.\n\t\t\tconst lineEnd = i > lineStart && buffer[i - 1] === 0x0d ? i - 1 : i;\n\t\t\tif (lineEnd > lineStart) {\n\t\t\t\tconst line = buffer.toString('utf8', lineStart, lineEnd);\n\t\t\t\tapplyTitleLine(line, sessionId, titles);\n\t\t\t}\n\t\t\tlineStart = i + 1;\n\t\t\tlastNewlineEnd = start + i + 1;\n\t\t}\n\n\t\treturn { titles: titles, consumedEnd: lastNewlineEnd };\n\t}\n}\n\nfunction applyTitleLine(line: string, sessionId: string, titles: TranscriptTitles): void {\n\tif (!isLikelyTitleLine(line)) return;\n\n\tlet entry: TitleEntry;\n\ttry {\n\t\tentry = JSON.parse(line) as TitleEntry;\n\t} catch {\n\t\treturn;\n\t}\n\tif (entry.sessionId != null && entry.sessionId !== sessionId) return;\n\n\tapplyTitleEntry(entry, titles);\n}\n\n/** Parses one transcript line for {@link ClaudeCodeTranscriptReader.readSummary}, rejecting entries\n * that belong to another session. Can't use the `-title` prefilter that {@link isLikelyTitleLine}\n * applies: `last-prompt` matches neither of its markers. */\nfunction parseSummaryLine(line: string, sessionId: string): TitleEntry | undefined {\n\tif (line.length === 0 || !line.includes('\"type\"')) return undefined;\n\n\tlet entry: TitleEntry;\n\ttry {\n\t\tentry = JSON.parse(line) as TitleEntry;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (entry.sessionId != null && entry.sessionId !== sessionId) return undefined;\n\n\treturn entry;\n}\n\nfunction applyTitleEntry(entry: TitleEntry, titles: TranscriptTitles): void {\n\tswitch (entry.type) {\n\t\tcase 'custom-title':\n\t\t\tif (typeof entry.customTitle === 'string' && entry.customTitle.length > 0) {\n\t\t\t\ttitles.custom = entry.customTitle;\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 'ai-title':\n\t\t\tif (typeof entry.aiTitle === 'string' && entry.aiTitle.length > 0) {\n\t\t\t\ttitles.ai = entry.aiTitle;\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 'agent-name':\n\t\t\tif (typeof entry.agentName === 'string' && entry.agentName.length > 0) {\n\t\t\t\ttitles.agent = entry.agentName;\n\t\t\t}\n\t\t\tbreak;\n\t}\n}\n\n/** Encodes a working directory into its `~/.claude/projects` directory name. Claude replaces every\n * non-alphanumeric character — not just separators — preserving runs (`/home/e/.claude` →\n * `-home-e--claude`) and case. Separators-only would leave the dot in our own worktree convention\n * (`<repo>.worktrees/<name>`) intact and compute a directory that never exists. */\nexport function encodeProjectDirName(cwd: string): string {\n\treturn cwd.replace(/[^a-zA-Z0-9]/g, '-');\n}\n\nfunction isLikelyTitleLine(line: string): boolean {\n\tif (!line.includes('\"type\"')) return false;\n\treturn line.includes('-title') || line.includes('agent-name');\n}\n\n/** A transcript with no title and no prompt is an aborted or empty session — nothing to name it by,\n * nothing to search it on. */\nfunction hasSummaryContent(summary: TranscriptSessionSummary): boolean {\n\tconst { custom, ai, agent } = summary.titles;\n\treturn custom != null || ai != null || agent != null || summary.lastPrompt != null;\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n\ttry {\n\t\tconst stats = await stat(path);\n\t\treturn stats.isFile();\n\t} catch (ex) {\n\t\t// See {@link isMissingEntry}: only a genuine absence is an answer.\n\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\treturn false;\n\t}\n}\n\nasync function directoryExists(path: string): Promise<boolean> {\n\ttry {\n\t\tconst stats = await stat(path);\n\t\treturn stats.isDirectory();\n\t} catch (ex) {\n\t\t// \"Not there\" is an answer; anything else (EACCES, EIO, a mount hiccup) is a transient\n\t\t// failure the caller must be able to tell apart — see {@link isMissingEntry}.\n\t\tif (isMissingEntry(ex)) return false;\n\n\t\tthrow ex;\n\t}\n}\n\n/** True for the \"path simply isn't there\" errno codes, as opposed to a transient I/O or permission\n * failure. Callers cache a genuine absence as terminal, so a transient failure must not look like\n * one — it would pin a session to its fallback name until the window reloads. */\nfunction isMissingEntry(ex: unknown): boolean {\n\tconst code = (ex as NodeJS.ErrnoException | undefined)?.code;\n\treturn code === 'ENOENT' || code === 'ENOTDIR';\n}\n\nfunction readSlice(path: string, start: number, end: number): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst chunks: Buffer[] = [];\n\t\tconst stream = createReadStream(path, { start: start, end: end - 1 });\n\t\tstream.on('data', chunk => chunks.push(chunk as Buffer));\n\t\tstream.on('end', () => resolve(Buffer.concat(chunks)));\n\t\tstream.on('error', reject);\n\t});\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"claudeCodeTranscript.js","names":[],"sources":["../../../../../plus/agents/src/providers/claudeCodeTranscript.ts"],"sourcesContent":["import { createReadStream } from 'fs';\nimport { open, readdir, readFile, stat } from 'fs/promises';\nimport { homedir } from 'os';\nimport { basename, dirname, join } from 'path';\nimport { createInterface } from 'readline';\n\nexport interface TranscriptTitles {\n\tcustom?: string;\n\tai?: string;\n\tagent?: string;\n}\n\n/** A transcript file discovered by {@link ClaudeCodeTranscriptReader.listSessions}, before its\n * contents are read — everything here comes from `readdir` + `stat`. */\nexport interface TranscriptSessionEntry {\n\treadonly sessionId: string;\n\treadonly path: string;\n\treadonly lastActivityMs: number;\n\treadonly size: number;\n}\n\n/** A {@link TranscriptSessionEntry} enriched with the fields recovered from the file's head/tail. */\nexport interface TranscriptSessionSummary extends TranscriptSessionEntry {\n\treadonly titles: TranscriptTitles;\n\treadonly lastPrompt?: string;\n}\n\nexport interface TranscriptSessionListing {\n\treadonly sessions: TranscriptSessionSummary[];\n\t/** Every transcript in the directory, not just the summarized slice — drives \"Showing N of M\". */\n\treadonly total: number;\n}\n\n/** A transcript found outside the queried cwd's project directory, plus the directory Claude\n * actually uses to resolve it for `--resume`. */\nexport interface ResumableTranscriptSessionSummary extends TranscriptSessionSummary {\n\treadonly cwd: string;\n}\n\nexport interface ResumableTranscriptSessionListing {\n\treadonly sessions: ResumableTranscriptSessionSummary[];\n\treadonly total: number;\n}\n\nexport interface EndedTranscriptDetails {\n\ttitles: TranscriptTitles;\n\tfirstPrompt?: string;\n\tlastPrompt?: string;\n}\n\ninterface CacheEntry {\n\tpath: string;\n\tmtimeMs: number;\n\tsize: number;\n\tnextOffset: number;\n\ttitles: TranscriptTitles;\n}\n\ninterface ListingCacheEntry {\n\tentries: TranscriptSessionEntry[];\n\tresolvedAt: number;\n}\n\ninterface NameCacheEntry {\n\tnames: string[];\n\tresolvedAt: number;\n}\n\ninterface SummaryCacheEntry {\n\tpath: string;\n\tmtimeMs: number;\n\tsize: number;\n\tsummary: TranscriptSessionSummary;\n}\n\ninterface TitleEntry {\n\ttype: string;\n\tsessionId?: string;\n\tcustomTitle?: string;\n\taiTitle?: string;\n\tagentName?: string;\n\tlastPrompt?: string;\n}\n\n/** Bytes read from each end of a transcript when summarizing. Titles and prompts cluster at both\n * extremes, and files run to tens of MB, so reading whole files is not an option. */\nconst summaryWindowSize = 64 * 1024;\n/** Listings are cheap (readdir + stat) but re-run per panel/sheet open; a short TTL absorbs bursts.\n * Time-based rather than dir-mtime-based because appends move file mtimes without touching the dir. */\nconst listingCacheTtlMs = 10 * 1000;\n/** Summaries are keyed by session, and a busy project has hundreds — unlike the per-live-session\n * title cache, this needs a ceiling. Sized to hold a whole junk-heavy store: the listing scan runs\n * to exhaustion, and each entry is tiny (a path plus a few short strings), so a cap below the\n * store's size would evict-and-re-read the junk tail on every listing instead of paying its\n * windowed reads once. */\nconst summaryCacheLimit = 1000;\nconst defaultListLimit = 50;\n\n/**\n * Reads Claude Code transcript JSONL files at `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`\n * to surface `custom-title`, `ai-title`, and `agent-name` entries as fallback session names.\n *\n * Maintains a per-session cache keyed by mtime + last-read byte offset, so repeated calls only\n * read the appended tail. Last occurrence per type wins.\n *\n * `resolve` is async, so a `forget` (or a newer `resolve`) for the same session can land between\n * its `await`s. A per-session generation counter is stamped at entry and re-checked before the\n * final cache write, so a stale `resolve` can't resurrect a forgotten entry or clobber a newer\n * resolve's result.\n */\nexport class ClaudeCodeTranscriptReader {\n\tprivate readonly _cache = new Map<string, CacheEntry>();\n\tprivate readonly _generations = new Map<string, number>();\n\tprivate readonly _listings = new Map<string, ListingCacheEntry>();\n\t/** Transcript file names per project directory — the readdir behind both the plain listing and\n\t * the by-id scan, which sweeps every project directory. */\n\tprivate readonly _names = new Map<string, NameCacheEntry>();\n\t/** Project directories under {@link getProjectsRoot}, re-scanned by every by-id recovery. */\n\tprivate _projectDirs: { dirs: string[]; resolvedAt: number } | undefined;\n\t/** Insertion-ordered LRU — re-inserted on hit, oldest key evicted past {@link summaryCacheLimit}. */\n\tprivate readonly _summaries = new Map<string, SummaryCacheEntry>();\n\t/** Resolved project path per transcript path, bounded like {@link _summaries}. */\n\tprivate readonly _transcriptCwds = new Map<string, string>();\n\tprivate _nextGen = 0;\n\t/** Bumped by {@link invalidateListings}; async listing scans capture it at entry and skip their\n\t * cache write when it moved, so a stale in-flight result can't re-seed a just-cleared cache. */\n\tprivate _listingGen = 0;\n\n\t/** Drops the directory-listing caches (entries, names, project dirs) so the next listing re-reads\n\t * the store. Called when a session ends or is removed: its transcript may have appeared inside\n\t * the TTL window, and a cached listing would hide the new file from the very fetch meant to\n\t * surface it. Summary and cwd caches are keyed by file identity and stay valid, so they're kept.\n\t * The generation bump makes any in-flight listing skip its own cache write — otherwise a scan\n\t * started before the invalidation would re-seed the caches with pre-end results, freshly\n\t * timestamped (same discipline as `resolve`'s per-session generations). */\n\tinvalidateListings(): void {\n\t\tthis._listingGen++;\n\t\tthis._listings.clear();\n\t\tthis._names.clear();\n\t\tthis._projectDirs = undefined;\n\t}\n\n\tasync resolve(sessionId: string, cwd: string | undefined): Promise<TranscriptTitles | undefined> {\n\t\tconst gen = ++this._nextGen;\n\t\tthis._generations.set(sessionId, gen);\n\n\t\tconst cached = this._cache.get(sessionId);\n\t\tconst path = cached?.path ?? (await this.locateTranscript(sessionId, cwd));\n\t\tif (path == null) return undefined;\n\n\t\tlet stats;\n\t\ttry {\n\t\t\tstats = await stat(path);\n\t\t} catch {\n\t\t\t// File disappeared; drop any stale cache entry so a future call retries discovery.\n\t\t\t// Skip the delete if a newer resolve/forget claimed the slot — their state is fresher.\n\t\t\tif (this._generations.get(sessionId) === gen) {\n\t\t\t\tthis._cache.delete(sessionId);\n\t\t\t\tthis._generations.delete(sessionId);\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (stats.mtimeMs === cached?.mtimeMs && stats.size === cached?.size) {\n\t\t\treturn cached.titles;\n\t\t}\n\n\t\t// Truncation or rewrite: stat shrank below our read cursor or mtime moved backward → full re-scan.\n\t\tconst startFromScratch = cached == null || stats.size < cached.nextOffset || stats.mtimeMs < cached.mtimeMs;\n\t\tconst startOffset = startFromScratch ? 0 : cached.nextOffset;\n\t\tconst baseTitles: TranscriptTitles = startFromScratch ? {} : { ...cached.titles };\n\n\t\tconst { titles, consumedEnd } =\n\t\t\tstats.size > startOffset\n\t\t\t\t? await this.readRange(path, startOffset, stats.size, sessionId, baseTitles)\n\t\t\t\t: { titles: baseTitles, consumedEnd: startOffset };\n\n\t\t// If `forget` ran or a newer `resolve` started while we were awaiting, skip the cache write\n\t\t// — the entry we'd be writing reflects stale state.\n\t\tif (this._generations.get(sessionId) !== gen) return titles;\n\n\t\tconst entry: CacheEntry = {\n\t\t\tpath: path,\n\t\t\tmtimeMs: stats.mtimeMs,\n\t\t\tsize: stats.size,\n\t\t\tnextOffset: consumedEnd,\n\t\t\ttitles: titles,\n\t\t};\n\t\tthis._cache.set(sessionId, entry);\n\t\treturn titles;\n\t}\n\n\tforget(sessionId: string): void {\n\t\tthis._cache.delete(sessionId);\n\t\tthis._generations.delete(sessionId);\n\t}\n\n\t/**\n\t * One-shot read for an *ended* (terminal, static) session: extracts the transcript titles plus\n\t * the first and last user prompts. Unlike {@link resolve}, it bypasses the tail cache — a finished\n\t * transcript never grows, so it's read once on demand (when an ended row is opened) and the\n\t * result applied to the session. `last-prompt` entries carry the prompt as of that point; the first\n\t * one bearing a value is the session's opening prompt, the last its most recent. Returns `undefined`\n\t * when no transcript is found (archived/purged) — the caller keeps whatever durable-store fields it\n\t * already has.\n\t *\n\t * Streams the file line-by-line rather than buffering it whole: a finished transcript can run to\n\t * tens of MB, but this keeps only one line resident at a time. A *full* scan (not a head/tail\n\t * window) is required for correctness — when the first turn is large the opening `last-prompt` sits\n\t * past any fixed head window, and a windowed read would then mistake a later prompt for the first,\n\t * poisoning the derived session name.\n\t */\n\tasync resolveEndedDetails(sessionId: string, cwd: string | undefined): Promise<EndedTranscriptDetails | undefined> {\n\t\tconst path = await this.locateTranscript(sessionId, cwd);\n\t\tif (path == null) return undefined;\n\n\t\tlet stats;\n\t\ttry {\n\t\t\tstats = await stat(path);\n\t\t} catch (ex) {\n\t\t\t// A transcript that vanished is terminal; an I/O or permission failure must propagate so\n\t\t\t// the caller retries rather than caching this session as permanently unresolvable.\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn undefined;\n\t\t}\n\t\tif (stats.size === 0) return undefined;\n\n\t\tconst titles: TranscriptTitles = {};\n\t\tlet firstPrompt: string | undefined;\n\t\tlet lastPrompt: string | undefined;\n\n\t\tconst rl = createInterface({ input: createReadStream(path, { encoding: 'utf8' }), crlfDelay: Infinity });\n\t\ttry {\n\t\t\tfor await (const line of rl) {\n\t\t\t\tapplyTitleLine(line, sessionId, titles);\n\t\t\t\tif (!line.includes('last-prompt')) continue;\n\n\t\t\t\tconst entry = parseSummaryLine(line, sessionId);\n\t\t\t\tif (entry?.type === 'last-prompt' && entry.lastPrompt != null && entry.lastPrompt.length > 0) {\n\t\t\t\t\tfirstPrompt ??= entry.lastPrompt;\n\t\t\t\t\tlastPrompt = entry.lastPrompt;\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\trl.close();\n\t\t}\n\n\t\treturn { titles: titles, firstPrompt: firstPrompt, lastPrompt: lastPrompt };\n\t}\n\n\t/**\n\t * Lists the transcripts of sessions whose working directory is `cwd`, most-recently-active first,\n\t * summarizing until `limit` summarizable transcripts are found — not just the first `limit` on\n\t * disk, since junk transcripts (dropped below) would otherwise starve the result.\n\t * `excludeSessionIds` is skipped before `limit` applies, and excluded entries are also dropped\n\t * from `total` — they're already shown elsewhere (e.g. as live sessions), so a caller's \"N of M\"\n\t * count must not double-count them.\n\t *\n\t * Claude homes a transcript under the directory encoding the cwd from which the session started.\n\t * That directory is authoritative for where `claude --resume <id>` can find the transcript, but\n\t * Claude does not reliably move it when the session later changes worktrees. Callers that have a\n\t * durable session-to-worktree association can recover those moved sessions with\n\t * {@link listSessionsByIds}; this method intentionally remains the cheap legacy directory listing.\n\t *\n\t * Discovery (readdir + stat) covers the whole directory and is cheap; summarizing is not, so it's\n\t * capped. Entries whose summary yields neither a title nor a prompt are dropped — those are aborted\n\t * or empty transcripts with nothing to show or search on.\n\t */\n\tasync listSessions(\n\t\tcwd: string,\n\t\toptions?: { limit?: number; excludeSessionIds?: ReadonlySet<string> },\n\t): Promise<TranscriptSessionListing> {\n\t\tconst dir = await this.resolveProjectDir(cwd);\n\t\tif (dir == null) return { sessions: [], total: 0 };\n\n\t\tconst entries = await this.listEntries(dir);\n\t\tif (entries.length === 0) return { sessions: [], total: 0 };\n\n\t\tconst limit = options?.limit ?? defaultListLimit;\n\t\tconst exclude = options?.excludeSessionIds;\n\t\t// Zero-byte transcripts can never summarize — skip the read entirely and keep them out of\n\t\t// `total`, which otherwise counts sessions no `limit` could ever surface.\n\t\tconst candidates = entries.filter(e => e.size > 0 && exclude?.has(e.sessionId) !== true);\n\t\tconst { sessions, exhausted } = await this.collectSessions(candidates, limit, async entry => {\n\t\t\tconst summary = await this.resolveSummary(entry);\n\t\t\treturn summary != null && hasSummaryContent(summary) ? summary : undefined;\n\t\t});\n\n\t\t// An exhausted scan proved the exact valid count; otherwise unscanned candidates keep\n\t\t// `total` an upper bound so paging knows more MAY exist.\n\t\treturn { sessions: sessions, total: exhausted ? sessions.length : candidates.length };\n\t}\n\n\t/**\n\t * Finds a known set of session ids across every Claude project directory except `excludeCwd`'s.\n\t * This is the recovery path for durable CLI records whose worktree changed after Claude chose the\n\t * transcript's project directory. The caller supplies the association; transcript contents alone\n\t * are deliberately not used to guess which current worktree owns an arbitrary old session.\n\t *\n\t * Each result carries the project path encoded by the directory that actually holds the transcript,\n\t * so resuming it does not repeat discovery from the wrong worktree. Duplicate ids are counted once,\n\t * with the newest transcript winning.\n\t */\n\tasync listSessionsByIds(\n\t\tsessionIds: ReadonlySet<string>,\n\t\toptions?: { limit?: number; excludeCwd?: string },\n\t): Promise<ResumableTranscriptSessionListing> {\n\t\tif (sessionIds.size === 0) return { sessions: [], total: 0 };\n\n\t\tconst excludeDir = await this.resolveProjectDir(options?.excludeCwd);\n\t\t// Same zero-byte drop as `listSessions` — see the rationale there.\n\t\tconst entries = (await this.listEntriesByIds(sessionIds, excludeDir)).filter(e => e.size > 0);\n\t\tif (entries.length === 0) return { sessions: [], total: 0 };\n\n\t\tconst limit = options?.limit ?? defaultListLimit;\n\t\tconst { sessions, exhausted } = await this.collectSessions(entries, limit, async entry => {\n\t\t\tconst summary = await this.resolveSummary(entry);\n\t\t\tif (summary == null || !hasSummaryContent(summary)) return undefined;\n\n\t\t\tconst cwd = await this.resolveTranscriptCwd(entry);\n\t\t\treturn cwd != null ? { ...summary, cwd: cwd } : undefined;\n\t\t});\n\n\t\t// Exhausted scans prove the exact count — see `listSessions`.\n\t\treturn { sessions: sessions, total: exhausted ? sessions.length : entries.length };\n\t}\n\n\t/** Discovers every transcript in `dir`, newest first. The directory also holds one sibling\n\t * subdirectory per session (`<uuid>/subagents`, `<uuid>/tool-results`), so entries must be filtered\n\t * to files — a busy project has roughly as many subdirectories as transcripts. */\n\tprivate async listEntries(dir: string): Promise<TranscriptSessionEntry[]> {\n\t\tconst cached = this._listings.get(dir);\n\t\tif (cached != null && Date.now() - cached.resolvedAt < listingCacheTtlMs) return cached.entries;\n\n\t\tconst gen = this._listingGen;\n\t\tlet names: string[];\n\t\ttry {\n\t\t\tnames = await this.listTranscriptNames(dir);\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst entries = (await this.statEntries(dir, names)).sort((a, b) => b.lastActivityMs - a.lastActivityMs);\n\n\t\tif (gen === this._listingGen) {\n\t\t\tthis._listings.set(dir, { entries: entries, resolvedAt: Date.now() });\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/** Stats `names` in `dir`, dropping the ones that failed — a transcript can vanish (or be\n\t * archived) between the readdir and the stat. Unsorted; callers order as they need.\n\t * Protected (like {@link readSummary}) so tests can interleave against the listing flow. */\n\tprotected async statEntries(dir: string, names: string[]): Promise<TranscriptSessionEntry[]> {\n\t\tconst settled = await Promise.allSettled(\n\t\t\tnames.map(async (name): Promise<TranscriptSessionEntry> => {\n\t\t\t\tconst path = join(dir, name);\n\t\t\t\tconst stats = await stat(path);\n\t\t\t\treturn {\n\t\t\t\t\tsessionId: basename(name, '.jsonl'),\n\t\t\t\t\tpath: path,\n\t\t\t\t\tlastActivityMs: stats.mtimeMs,\n\t\t\t\t\tsize: stats.size,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\n\t\treturn settled.filter(r => r.status === 'fulfilled').map(r => r.value);\n\t}\n\n\t/** The `.jsonl` file names in `dir`, cached for {@link listingCacheTtlMs}. Failures propagate\n\t * uncached: an unreadable directory means different things to different callers, and the next\n\t * call must be free to get a real answer. */\n\tprivate async listTranscriptNames(dir: string): Promise<string[]> {\n\t\tconst cached = this._names.get(dir);\n\t\tif (cached != null && Date.now() - cached.resolvedAt < listingCacheTtlMs) return cached.names;\n\n\t\tconst gen = this._listingGen;\n\t\tconst dirents = await readdir(dir, { withFileTypes: true });\n\t\tconst names = dirents.filter(d => d.isFile() && d.name.endsWith('.jsonl')).map(d => d.name);\n\t\tif (gen === this._listingGen) {\n\t\t\tthis._names.set(dir, { names: names, resolvedAt: Date.now() });\n\t\t}\n\n\t\treturn names;\n\t}\n\n\t/** Every project directory under the projects root, cached for {@link listingCacheTtlMs} — a\n\t * by-id recovery sweeps all of them, once per queried worktree. */\n\tprivate async listProjectDirs(): Promise<string[]> {\n\t\tconst cached = this._projectDirs;\n\t\tif (cached != null && Date.now() - cached.resolvedAt < listingCacheTtlMs) return cached.dirs;\n\n\t\tconst gen = this._listingGen;\n\t\tconst root = this.getProjectsRoot();\n\t\tconst dirents = await readdir(root, { withFileTypes: true });\n\t\tconst dirs = dirents.filter(d => d.isDirectory()).map(d => join(root, d.name));\n\t\tif (gen === this._listingGen) {\n\t\t\tthis._projectDirs = { dirs: dirs, resolvedAt: Date.now() };\n\t\t}\n\n\t\treturn dirs;\n\t}\n\n\t/** Scans project directory names once, then stats only requested transcript ids. */\n\tprivate async listEntriesByIds(\n\t\tsessionIds: ReadonlySet<string>,\n\t\texcludeDir: string | undefined,\n\t): Promise<TranscriptSessionEntry[]> {\n\t\tlet idsToFind = sessionIds;\n\t\tif (excludeDir != null) {\n\t\t\ttry {\n\t\t\t\tconst names = await this.listTranscriptNames(excludeDir);\n\t\t\t\tconst idsInExcludedDir = new Set(names.map(name => basename(name, '.jsonl')));\n\t\t\t\tidsToFind = new Set([...sessionIds].filter(id => !idsInExcludedDir.has(id)));\n\t\t\t\tif (idsToFind.size === 0) return [];\n\t\t\t} catch {\n\t\t\t\t// If the exact directory can't be inspected, don't risk returning a duplicate from a\n\t\t\t\t// stale transcript copy elsewhere. The ordinary listing still owns the exact path.\n\t\t\t\treturn [];\n\t\t\t}\n\t\t}\n\n\t\tlet dirs: string[];\n\t\ttry {\n\t\t\tdirs = await this.listProjectDirs();\n\t\t} catch (ex) {\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn [];\n\t\t}\n\n\t\tconst settledDirs = await Promise.allSettled(\n\t\t\tdirs.map(async dir => {\n\t\t\t\tif (dir === excludeDir) return [];\n\n\t\t\t\tconst names = (await this.listTranscriptNames(dir)).filter(name =>\n\t\t\t\t\tidsToFind.has(basename(name, '.jsonl')),\n\t\t\t\t);\n\t\t\t\treturn this.statEntries(dir, names);\n\t\t\t}),\n\t\t);\n\n\t\tconst bySessionId = new Map<string, TranscriptSessionEntry>();\n\t\tfor (const settledDir of settledDirs) {\n\t\t\tif (settledDir.status !== 'fulfilled') continue;\n\n\t\t\tfor (const entry of settledDir.value) {\n\t\t\t\tconst existing = bySessionId.get(entry.sessionId);\n\t\t\t\tif (existing == null || entry.lastActivityMs > existing.lastActivityMs) {\n\t\t\t\t\tbySessionId.set(entry.sessionId, entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn [...bySessionId.values()].sort((a, b) => b.lastActivityMs - a.lastActivityMs);\n\t}\n\n\t/** `exhausted` reports whether every candidate was scanned — an exhausted scan has proven the\n\t * exact valid count, so callers can report it instead of an upper bound that would keep a\n\t * \"Show More\" affordance alive with nothing left to show. */\n\tprivate async collectSessions<T>(\n\t\tcandidates: readonly TranscriptSessionEntry[],\n\t\tlimit: number,\n\t\tresolve: (entry: TranscriptSessionEntry) => Promise<T | undefined>,\n\t): Promise<{ sessions: T[]; exhausted: boolean }> {\n\t\tconst sessions: T[] = [];\n\t\tconst pushResolved = (settled: PromiseSettledResult<T | undefined>[]): void => {\n\t\t\tfor (const result of settled) {\n\t\t\t\tif (result.status !== 'fulfilled' || result.value == null) continue;\n\n\t\t\t\tsessions.push(result.value);\n\t\t\t}\n\t\t};\n\n\t\t// limit <= 0 means \"no ceiling\" — resolve every candidate.\n\t\tif (limit <= 0) {\n\t\t\tpushResolved(await Promise.allSettled(candidates.map(resolve)));\n\t\t\treturn { sessions: sessions, exhausted: true };\n\t\t}\n\n\t\t// Scans until `limit` results or the candidates run out — a fixed scan ceiling would let a run\n\t\t// of junk transcripts hide every older real session behind it (and the picker then claims there\n\t\t// are none at all). The cost stays bounded: callers pre-drop zero-byte candidates read-free, and\n\t\t// a junk summary is windowed, tiny, and LRU-cached, so the sweep is paid in syscalls, once.\n\t\tlet cursor = 0;\n\t\twhile (sessions.length < limit && cursor < candidates.length) {\n\t\t\t// Batched by remaining need so a hit-rich head stops the scan early instead of resolving\n\t\t\t// every candidate up front.\n\t\t\tconst batch = candidates.slice(cursor, cursor + (limit - sessions.length));\n\t\t\tcursor += batch.length;\n\t\t\tpushResolved(await Promise.allSettled(batch.map(resolve)));\n\t\t}\n\n\t\treturn { sessions: sessions, exhausted: cursor >= candidates.length };\n\t}\n\n\t/** Summarizes one transcript, reusing the cached result while its mtime and size are unchanged. */\n\tprivate async resolveSummary(entry: TranscriptSessionEntry): Promise<TranscriptSessionSummary | undefined> {\n\t\tconst cached = this._summaries.get(entry.sessionId);\n\t\tif (\n\t\t\tcached != null &&\n\t\t\tcached.path === entry.path &&\n\t\t\tcached.mtimeMs === entry.lastActivityMs &&\n\t\t\tcached.size === entry.size\n\t\t) {\n\t\t\t// Refresh recency for the LRU.\n\t\t\tthis._summaries.delete(entry.sessionId);\n\t\t\tthis._summaries.set(entry.sessionId, cached);\n\t\t\treturn cached.summary;\n\t\t}\n\n\t\tlet summary: TranscriptSessionSummary;\n\t\ttry {\n\t\t\tsummary = await this.readSummary(entry);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tthis._summaries.set(entry.sessionId, {\n\t\t\tpath: entry.path,\n\t\t\tmtimeMs: entry.lastActivityMs,\n\t\t\tsize: entry.size,\n\t\t\tsummary: summary,\n\t\t});\n\t\tif (this._summaries.size > summaryCacheLimit) {\n\t\t\tconst oldest = this._summaries.keys().next();\n\t\t\tif (!oldest.done) {\n\t\t\t\tthis._summaries.delete(oldest.value);\n\t\t\t}\n\t\t}\n\t\treturn summary;\n\t}\n\n\t/** Recovers the project path represented by the transcript's containing directory. A resolved\n\t * answer is cached: it's a line-scan of the whole transcript, and the directory holding a given\n\t * path never changes. A miss isn't cached — a `cwd` line can still land on append. */\n\tprivate async resolveTranscriptCwd(entry: TranscriptSessionEntry): Promise<string | undefined> {\n\t\tconst cached = this._transcriptCwds.get(entry.path);\n\t\tif (cached != null) return cached;\n\n\t\tconst projectDir = dirname(entry.path);\n\t\tconst projectDirName = basename(projectDir);\n\t\tconst matchesProjectDir = (cwd: string): boolean =>\n\t\t\tencodeProjectDirName(cwd).toLowerCase() === projectDirName.toLowerCase();\n\n\t\ttry {\n\t\t\tconst index = JSON.parse(await readFile(join(projectDir, 'sessions-index.json'), 'utf8')) as {\n\t\t\t\tentries?: { sessionId?: unknown; projectPath?: unknown }[];\n\t\t\t};\n\t\t\tif (Array.isArray(index.entries)) {\n\t\t\t\tconst indexed = index.entries.find(item => item?.sessionId === entry.sessionId);\n\t\t\t\tif (typeof indexed?.projectPath === 'string' && matchesProjectDir(indexed.projectPath)) {\n\t\t\t\t\tthis.rememberTranscriptCwd(entry.path, indexed.projectPath);\n\t\t\t\t\treturn indexed.projectPath;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// The index is optional and can lag or be malformed. The transcript is authoritative below.\n\t\t}\n\n\t\tconst rl = createInterface({ input: createReadStream(entry.path, { encoding: 'utf8' }), crlfDelay: Infinity });\n\t\ttry {\n\t\t\tfor await (const line of rl) {\n\t\t\t\tif (!line.includes('\"cwd\"')) continue;\n\n\t\t\t\tlet parsed: { cwd?: unknown; sessionId?: unknown };\n\t\t\t\ttry {\n\t\t\t\t\tparsed = JSON.parse(line) as { cwd?: unknown; sessionId?: unknown };\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (parsed.sessionId != null && parsed.sessionId !== entry.sessionId) continue;\n\n\t\t\t\tif (typeof parsed.cwd === 'string' && matchesProjectDir(parsed.cwd)) {\n\t\t\t\t\tthis.rememberTranscriptCwd(entry.path, parsed.cwd);\n\t\t\t\t\treturn parsed.cwd;\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\trl.close();\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\tprivate rememberTranscriptCwd(path: string, cwd: string): void {\n\t\tthis._transcriptCwds.set(path, cwd);\n\t\tif (this._transcriptCwds.size > summaryCacheLimit) {\n\t\t\tconst oldest = this._transcriptCwds.keys().next();\n\t\t\tif (!oldest.done) {\n\t\t\t\tthis._transcriptCwds.delete(oldest.value);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Reads a summary from the first and last {@link summaryWindowSize} bytes of the transcript.\n\t *\n\t * Deliberately does not reuse `resolve` — that scans from the last-read offset and buffers the whole\n\t * range, which for a cold multi-MB transcript means loading the entire file. Titles are written early\n\t * and re-written as they're refined, while prompts land at the tail, so the two windows recover both\n\t * at a fraction of the bytes. The tail is parsed second so the newest value wins.\n\t */\n\tprotected async readSummary(entry: TranscriptSessionEntry): Promise<TranscriptSessionSummary> {\n\t\tconst titles: TranscriptTitles = {};\n\t\tlet lastPrompt: string | undefined;\n\n\t\tconst apply = (buffer: Buffer, dropPartialFirstLine: boolean): void => {\n\t\t\tconst text = buffer.toString('utf8');\n\t\t\tconst lines = text.split('\\n');\n\t\t\t// A window starting mid-file almost always opens mid-line; that fragment can't be parsed.\n\t\t\tif (dropPartialFirstLine) {\n\t\t\t\tlines.shift();\n\t\t\t}\n\t\t\tfor (const line of lines) {\n\t\t\t\tconst parsed = parseSummaryLine(line, entry.sessionId);\n\t\t\t\tif (parsed == null) continue;\n\n\t\t\t\tapplyTitleEntry(parsed, titles);\n\t\t\t\tif (parsed.lastPrompt != null && parsed.lastPrompt.length > 0) {\n\t\t\t\t\tlastPrompt = parsed.lastPrompt;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tconst handle = await open(entry.path, 'r');\n\t\ttry {\n\t\t\tif (entry.size <= summaryWindowSize * 2) {\n\t\t\t\tconst whole = Buffer.alloc(entry.size);\n\t\t\t\tawait handle.read(whole, 0, entry.size, 0);\n\t\t\t\tapply(whole, false);\n\t\t\t} else {\n\t\t\t\tconst head = Buffer.alloc(summaryWindowSize);\n\t\t\t\tawait handle.read(head, 0, summaryWindowSize, 0);\n\t\t\t\tapply(head, false);\n\n\t\t\t\tconst tail = Buffer.alloc(summaryWindowSize);\n\t\t\t\tawait handle.read(tail, 0, summaryWindowSize, entry.size - summaryWindowSize);\n\t\t\t\tapply(tail, true);\n\t\t\t}\n\t\t} finally {\n\t\t\tawait handle.close();\n\t\t}\n\n\t\treturn {\n\t\t\t...entry,\n\t\t\ttitles: titles,\n\t\t\tlastPrompt: lastPrompt,\n\t\t};\n\t}\n\n\tprotected getProjectsRoot(): string {\n\t\treturn join(homedir(), '.claude', 'projects');\n\t}\n\n\t/** Resolves the `~/.claude/projects` directory holding the sessions whose working directory is\n\t * `cwd`, or `undefined` when none exists. Falls back to a case-insensitive name match because on\n\t * Windows our\n\t * paths carry a lower-cased drive letter (`normalizePath`) while Claude encodes the OS-native\n\t * `C:\\...` — so the exact name never matches by case. */\n\tprotected async resolveProjectDir(cwd: string | undefined): Promise<string | undefined> {\n\t\tif (cwd == null || cwd.length === 0) return undefined;\n\n\t\tconst root = this.getProjectsRoot();\n\t\tconst encoded = encodeProjectDirName(cwd);\n\n\t\tconst exact = join(root, encoded);\n\t\tif (await directoryExists(exact)) return exact;\n\n\t\tlet dirs: string[];\n\t\ttry {\n\t\t\tdirs = await readdir(root);\n\t\t} catch (ex) {\n\t\t\t// Only a missing projects root means \"no transcript\". A transient failure propagates so the\n\t\t\t// caller retries instead of caching a permanent miss.\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst lowered = encoded.toLowerCase();\n\t\tconst match = dirs.find(d => d !== encoded && d.toLowerCase() === lowered);\n\t\treturn match != null ? join(root, match) : undefined;\n\t}\n\n\tprotected async locateTranscript(sessionId: string, cwd: string | undefined): Promise<string | undefined> {\n\t\tconst fileName = `${sessionId}.jsonl`;\n\n\t\tconst dir = await this.resolveProjectDir(cwd);\n\t\tif (dir != null) {\n\t\t\tconst candidate = join(dir, fileName);\n\t\t\tif (await fileExists(candidate)) return candidate;\n\t\t}\n\n\t\t// The recorded cwd drifts whenever the agent `cd`s, so it can encode to a directory the session\n\t\t// doesn't actually live in — scan every project for the file as a last resort.\n\t\tconst root = this.getProjectsRoot();\n\t\tlet dirs: string[];\n\t\ttry {\n\t\t\tdirs = await readdir(root);\n\t\t} catch (ex) {\n\t\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst candidates = await Promise.all(\n\t\t\tdirs.map(async d => {\n\t\t\t\tconst candidate = join(root, d, fileName);\n\t\t\t\treturn (await fileExists(candidate)) ? candidate : undefined;\n\t\t\t}),\n\t\t);\n\t\treturn candidates.find(c => c != null);\n\t}\n\n\t/**\n\t * Reads bytes `[start, end)` from `path` and applies any title entries it finds to a copy of\n\t * `baseTitles`. Returns the merged titles and the byte position immediately after the last\n\t * newline observed — partial trailing lines (writer hasn't flushed `\\n` yet) are intentionally\n\t * left unconsumed so they're re-read on the next pass.\n\t */\n\tprotected async readRange(\n\t\tpath: string,\n\t\tstart: number,\n\t\tend: number,\n\t\tsessionId: string,\n\t\tbaseTitles: TranscriptTitles,\n\t): Promise<{ titles: TranscriptTitles; consumedEnd: number }> {\n\t\tconst buffer = await readSlice(path, start, end);\n\t\tconst titles: TranscriptTitles = { ...baseTitles };\n\n\t\tlet lastNewlineEnd = start;\n\t\tlet lineStart = 0;\n\t\tfor (let i = 0; i < buffer.length; i++) {\n\t\t\tif (buffer[i] !== 0x0a) continue;\n\n\t\t\t// Slice the line (trim a trailing \\r for CRLF files), then advance the cursor past the \\n\n\t\t\t// regardless of whether the line yielded anything — we've fully observed those bytes.\n\t\t\tconst lineEnd = i > lineStart && buffer[i - 1] === 0x0d ? i - 1 : i;\n\t\t\tif (lineEnd > lineStart) {\n\t\t\t\tconst line = buffer.toString('utf8', lineStart, lineEnd);\n\t\t\t\tapplyTitleLine(line, sessionId, titles);\n\t\t\t}\n\t\t\tlineStart = i + 1;\n\t\t\tlastNewlineEnd = start + i + 1;\n\t\t}\n\n\t\treturn { titles: titles, consumedEnd: lastNewlineEnd };\n\t}\n}\n\nfunction applyTitleLine(line: string, sessionId: string, titles: TranscriptTitles): void {\n\tif (!isLikelyTitleLine(line)) return;\n\n\tlet entry: TitleEntry;\n\ttry {\n\t\tentry = JSON.parse(line) as TitleEntry;\n\t} catch {\n\t\treturn;\n\t}\n\tif (entry.sessionId != null && entry.sessionId !== sessionId) return;\n\n\tapplyTitleEntry(entry, titles);\n}\n\n/** Parses one transcript line for {@link ClaudeCodeTranscriptReader.readSummary}, rejecting entries\n * that belong to another session. Can't use the `-title` prefilter that {@link isLikelyTitleLine}\n * applies: `last-prompt` matches neither of its markers. */\nfunction parseSummaryLine(line: string, sessionId: string): TitleEntry | undefined {\n\tif (line.length === 0 || !line.includes('\"type\"')) return undefined;\n\n\tlet entry: TitleEntry;\n\ttry {\n\t\tentry = JSON.parse(line) as TitleEntry;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (entry.sessionId != null && entry.sessionId !== sessionId) return undefined;\n\n\treturn entry;\n}\n\nfunction applyTitleEntry(entry: TitleEntry, titles: TranscriptTitles): void {\n\tswitch (entry.type) {\n\t\tcase 'custom-title':\n\t\t\tif (typeof entry.customTitle === 'string' && entry.customTitle.length > 0) {\n\t\t\t\ttitles.custom = entry.customTitle;\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 'ai-title':\n\t\t\tif (typeof entry.aiTitle === 'string' && entry.aiTitle.length > 0) {\n\t\t\t\ttitles.ai = entry.aiTitle;\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 'agent-name':\n\t\t\tif (typeof entry.agentName === 'string' && entry.agentName.length > 0) {\n\t\t\t\ttitles.agent = entry.agentName;\n\t\t\t}\n\t\t\tbreak;\n\t}\n}\n\n/** Encodes a working directory into its `~/.claude/projects` directory name. Claude replaces every\n * non-alphanumeric character — not just separators — preserving runs (`/home/e/.claude` →\n * `-home-e--claude`) and case. Separators-only would leave the dot in our own worktree convention\n * (`<repo>.worktrees/<name>`) intact and compute a directory that never exists. */\nexport function encodeProjectDirName(cwd: string): string {\n\treturn cwd.replace(/[^a-zA-Z0-9]/g, '-');\n}\n\nfunction isLikelyTitleLine(line: string): boolean {\n\tif (!line.includes('\"type\"')) return false;\n\treturn line.includes('-title') || line.includes('agent-name');\n}\n\n/** A transcript with no title and no prompt is an aborted or empty session — nothing to name it by,\n * nothing to search it on. */\nfunction hasSummaryContent(summary: TranscriptSessionSummary): boolean {\n\tconst { custom, ai, agent } = summary.titles;\n\treturn custom != null || ai != null || agent != null || summary.lastPrompt != null;\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n\ttry {\n\t\tconst stats = await stat(path);\n\t\treturn stats.isFile();\n\t} catch (ex) {\n\t\t// See {@link isMissingEntry}: only a genuine absence is an answer.\n\t\tif (!isMissingEntry(ex)) throw ex;\n\n\t\treturn false;\n\t}\n}\n\nasync function directoryExists(path: string): Promise<boolean> {\n\ttry {\n\t\tconst stats = await stat(path);\n\t\treturn stats.isDirectory();\n\t} catch (ex) {\n\t\t// \"Not there\" is an answer; anything else (EACCES, EIO, a mount hiccup) is a transient\n\t\t// failure the caller must be able to tell apart — see {@link isMissingEntry}.\n\t\tif (isMissingEntry(ex)) return false;\n\n\t\tthrow ex;\n\t}\n}\n\n/** True for the \"path simply isn't there\" errno codes, as opposed to a transient I/O or permission\n * failure. Callers cache a genuine absence as terminal, so a transient failure must not look like\n * one — it would pin a session to its fallback name until the window reloads. */\nfunction isMissingEntry(ex: unknown): boolean {\n\tconst code = (ex as NodeJS.ErrnoException | undefined)?.code;\n\treturn code === 'ENOENT' || code === 'ENOTDIR';\n}\n\nfunction readSlice(path: string, start: number, end: number): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst chunks: Buffer[] = [];\n\t\tconst stream = createReadStream(path, { start: start, end: end - 1 });\n\t\tstream.on('data', chunk => chunks.push(chunk as Buffer));\n\t\tstream.on('end', () => resolve(Buffer.concat(chunks)));\n\t\tstream.on('error', reject);\n\t});\n}\n"],"mappings":";;;;;;;;AAsFA,MAAM,oBAAoB;;;AAG1B,MAAM,oBAAoB;;;;;;AAM1B,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;;;;;;;;;;;;;AAczB,IAAa,6BAAb,MAAwC;CACvC,yBAA0B,IAAI,IAAwB;CACtD,+BAAgC,IAAI,IAAoB;CACxD,4BAA6B,IAAI,IAA+B;;;CAGhE,yBAA0B,IAAI,IAA4B;;CAE1D;;CAEA,6BAA8B,IAAI,IAA+B;;CAEjE,kCAAmC,IAAI,IAAoB;CAC3D,WAAmB;;;CAGnB,cAAsB;;;;;;;;CAStB,qBAA2B;EAC1B,KAAK;EACL,KAAK,UAAU,MAAM;EACrB,KAAK,OAAO,MAAM;EAClB,KAAK,eAAe,KAAA;CACrB;CAEA,MAAM,QAAQ,WAAmB,KAAgE;EAChG,MAAM,MAAM,EAAE,KAAK;EACnB,KAAK,aAAa,IAAI,WAAW,GAAG;EAEpC,MAAM,SAAS,KAAK,OAAO,IAAI,SAAS;EACxC,MAAM,OAAO,QAAQ,QAAS,MAAM,KAAK,iBAAiB,WAAW,GAAG;EACxE,IAAI,QAAQ,MAAM,OAAO,KAAA;EAEzB,IAAI;EACJ,IAAI;GACH,QAAQ,MAAM,KAAK,IAAI;EACxB,QAAQ;GAGP,IAAI,KAAK,aAAa,IAAI,SAAS,MAAM,KAAK;IAC7C,KAAK,OAAO,OAAO,SAAS;IAC5B,KAAK,aAAa,OAAO,SAAS;GACnC;GACA;EACD;EAEA,IAAI,MAAM,YAAY,QAAQ,WAAW,MAAM,SAAS,QAAQ,MAC/D,OAAO,OAAO;EAIf,MAAM,mBAAmB,UAAU,QAAQ,MAAM,OAAO,OAAO,cAAc,MAAM,UAAU,OAAO;EACpG,MAAM,cAAc,mBAAmB,IAAI,OAAO;EAClD,MAAM,aAA+B,mBAAmB,CAAC,IAAI,EAAE,GAAG,OAAO,OAAO;EAEhF,MAAM,EAAE,QAAQ,gBACf,MAAM,OAAO,cACV,MAAM,KAAK,UAAU,MAAM,aAAa,MAAM,MAAM,WAAW,UAAU,IACzE;GAAE,QAAQ;GAAY,aAAa;EAAY;EAInD,IAAI,KAAK,aAAa,IAAI,SAAS,MAAM,KAAK,OAAO;EAErD,MAAM,QAAoB;GACnB;GACN,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,YAAY;GACJ;EACT;EACA,KAAK,OAAO,IAAI,WAAW,KAAK;EAChC,OAAO;CACR;CAEA,OAAO,WAAyB;EAC/B,KAAK,OAAO,OAAO,SAAS;EAC5B,KAAK,aAAa,OAAO,SAAS;CACnC;;;;;;;;;;;;;;;;CAiBA,MAAM,oBAAoB,WAAmB,KAAsE;EAClH,MAAM,OAAO,MAAM,KAAK,iBAAiB,WAAW,GAAG;EACvD,IAAI,QAAQ,MAAM,OAAO,KAAA;EAEzB,IAAI;EACJ,IAAI;GACH,QAAQ,MAAM,KAAK,IAAI;EACxB,SAAS,IAAI;GAGZ,IAAI,CAAC,eAAe,EAAE,GAAG,MAAM;GAE/B;EACD;EACA,IAAI,MAAM,SAAS,GAAG,OAAO,KAAA;EAE7B,MAAM,SAA2B,CAAC;EAClC,IAAI;EACJ,IAAI;EAEJ,MAAM,KAAK,gBAAgB;GAAE,OAAO,iBAAiB,MAAM,EAAE,UAAU,OAAO,CAAC;GAAG,WAAW;EAAS,CAAC;EACvG,IAAI;GACH,WAAW,MAAM,QAAQ,IAAI;IAC5B,eAAe,MAAM,WAAW,MAAM;IACtC,IAAI,CAAC,KAAK,SAAS,aAAa,GAAG;IAEnC,MAAM,QAAQ,iBAAiB,MAAM,SAAS;IAC9C,IAAI,OAAO,SAAS,iBAAiB,MAAM,cAAc,QAAQ,MAAM,WAAW,SAAS,GAAG;KAC7F,gBAAgB,MAAM;KACtB,aAAa,MAAM;IACpB;GACD;EACD,UAAU;GACT,GAAG,MAAM;EACV;EAEA,OAAO;GAAU;GAAqB;GAAyB;EAAW;CAC3E;;;;;;;;;;;;;;;;;;;CAoBA,MAAM,aACL,KACA,SACoC;EACpC,MAAM,MAAM,MAAM,KAAK,kBAAkB,GAAG;EAC5C,IAAI,OAAO,MAAM,OAAO;GAAE,UAAU,CAAC;GAAG,OAAO;EAAE;EAEjD,MAAM,UAAU,MAAM,KAAK,YAAY,GAAG;EAC1C,IAAI,QAAQ,WAAW,GAAG,OAAO;GAAE,UAAU,CAAC;GAAG,OAAO;EAAE;EAE1D,MAAM,QAAQ,SAAS,SAAS;EAChC,MAAM,UAAU,SAAS;EAGzB,MAAM,aAAa,QAAQ,QAAO,MAAK,EAAE,OAAO,KAAK,SAAS,IAAI,EAAE,SAAS,MAAM,IAAI;EACvF,MAAM,EAAE,UAAU,cAAc,MAAM,KAAK,gBAAgB,YAAY,OAAO,OAAM,UAAS;GAC5F,MAAM,UAAU,MAAM,KAAK,eAAe,KAAK;GAC/C,OAAO,WAAW,QAAQ,kBAAkB,OAAO,IAAI,UAAU,KAAA;EAClE,CAAC;EAID,OAAO;GAAY;GAAU,OAAO,YAAY,SAAS,SAAS,WAAW;EAAO;CACrF;;;;;;;;;;;CAYA,MAAM,kBACL,YACA,SAC6C;EAC7C,IAAI,WAAW,SAAS,GAAG,OAAO;GAAE,UAAU,CAAC;GAAG,OAAO;EAAE;EAE3D,MAAM,aAAa,MAAM,KAAK,kBAAkB,SAAS,UAAU;EAEnE,MAAM,WAAW,MAAM,KAAK,iBAAiB,YAAY,UAAU,EAAA,CAAG,QAAO,MAAK,EAAE,OAAO,CAAC;EAC5F,IAAI,QAAQ,WAAW,GAAG,OAAO;GAAE,UAAU,CAAC;GAAG,OAAO;EAAE;EAE1D,MAAM,QAAQ,SAAS,SAAS;EAChC,MAAM,EAAE,UAAU,cAAc,MAAM,KAAK,gBAAgB,SAAS,OAAO,OAAM,UAAS;GACzF,MAAM,UAAU,MAAM,KAAK,eAAe,KAAK;GAC/C,IAAI,WAAW,QAAQ,CAAC,kBAAkB,OAAO,GAAG,OAAO,KAAA;GAE3D,MAAM,MAAM,MAAM,KAAK,qBAAqB,KAAK;GACjD,OAAO,OAAO,OAAO;IAAE,GAAG;IAAc;GAAI,IAAI,KAAA;EACjD,CAAC;EAGD,OAAO;GAAY;GAAU,OAAO,YAAY,SAAS,SAAS,QAAQ;EAAO;CAClF;;;;CAKA,MAAc,YAAY,KAAgD;EACzE,MAAM,SAAS,KAAK,UAAU,IAAI,GAAG;EACrC,IAAI,UAAU,QAAQ,KAAK,IAAI,IAAI,OAAO,aAAa,mBAAmB,OAAO,OAAO;EAExF,MAAM,MAAM,KAAK;EACjB,IAAI;EACJ,IAAI;GACH,QAAQ,MAAM,KAAK,oBAAoB,GAAG;EAC3C,QAAQ;GACP,OAAO,CAAC;EACT;EAEA,MAAM,WAAW,MAAM,KAAK,YAAY,KAAK,KAAK,EAAA,CAAG,MAAM,GAAG,MAAM,EAAE,iBAAiB,EAAE,cAAc;EAEvG,IAAI,QAAQ,KAAK,aAChB,KAAK,UAAU,IAAI,KAAK;GAAW;GAAS,YAAY,KAAK,IAAI;EAAE,CAAC;EAGrE,OAAO;CACR;;;;CAKA,MAAgB,YAAY,KAAa,OAAoD;EAc5F,QAAO,MAbe,QAAQ,WAC7B,MAAM,IAAI,OAAO,SAA0C;GAC1D,MAAM,OAAO,KAAK,KAAK,IAAI;GAC3B,MAAM,QAAQ,MAAM,KAAK,IAAI;GAC7B,OAAO;IACN,WAAW,SAAS,MAAM,QAAQ;IAC5B;IACN,gBAAgB,MAAM;IACtB,MAAM,MAAM;GACb;EACD,CAAC,CACF,EAAA,CAEe,QAAO,MAAK,EAAE,WAAW,WAAW,CAAC,CAAC,KAAI,MAAK,EAAE,KAAK;CACtE;;;;CAKA,MAAc,oBAAoB,KAAgC;EACjE,MAAM,SAAS,KAAK,OAAO,IAAI,GAAG;EAClC,IAAI,UAAU,QAAQ,KAAK,IAAI,IAAI,OAAO,aAAa,mBAAmB,OAAO,OAAO;EAExF,MAAM,MAAM,KAAK;EAEjB,MAAM,SAAQ,MADQ,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC,EAAA,CACpC,QAAO,MAAK,EAAE,OAAO,KAAK,EAAE,KAAK,SAAS,QAAQ,CAAC,CAAC,CAAC,KAAI,MAAK,EAAE,IAAI;EAC1F,IAAI,QAAQ,KAAK,aAChB,KAAK,OAAO,IAAI,KAAK;GAAS;GAAO,YAAY,KAAK,IAAI;EAAE,CAAC;EAG9D,OAAO;CACR;;;CAIA,MAAc,kBAAqC;EAClD,MAAM,SAAS,KAAK;EACpB,IAAI,UAAU,QAAQ,KAAK,IAAI,IAAI,OAAO,aAAa,mBAAmB,OAAO,OAAO;EAExF,MAAM,MAAM,KAAK;EACjB,MAAM,OAAO,KAAK,gBAAgB;EAElC,MAAM,QAAO,MADS,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC,EAAA,CACtC,QAAO,MAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAI,MAAK,KAAK,MAAM,EAAE,IAAI,CAAC;EAC7E,IAAI,QAAQ,KAAK,aAChB,KAAK,eAAe;GAAQ;GAAM,YAAY,KAAK,IAAI;EAAE;EAG1D,OAAO;CACR;;CAGA,MAAc,iBACb,YACA,YACoC;EACpC,IAAI,YAAY;EAChB,IAAI,cAAc,MACjB,IAAI;GACH,MAAM,QAAQ,MAAM,KAAK,oBAAoB,UAAU;GACvD,MAAM,mBAAmB,IAAI,IAAI,MAAM,KAAI,SAAQ,SAAS,MAAM,QAAQ,CAAC,CAAC;GAC5E,YAAY,IAAI,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC,QAAO,OAAM,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;GAC3E,IAAI,UAAU,SAAS,GAAG,OAAO,CAAC;EACnC,QAAQ;GAGP,OAAO,CAAC;EACT;EAGD,IAAI;EACJ,IAAI;GACH,OAAO,MAAM,KAAK,gBAAgB;EACnC,SAAS,IAAI;GACZ,IAAI,CAAC,eAAe,EAAE,GAAG,MAAM;GAE/B,OAAO,CAAC;EACT;EAEA,MAAM,cAAc,MAAM,QAAQ,WACjC,KAAK,IAAI,OAAM,QAAO;GACrB,IAAI,QAAQ,YAAY,OAAO,CAAC;GAEhC,MAAM,SAAS,MAAM,KAAK,oBAAoB,GAAG,EAAA,CAAG,QAAO,SAC1D,UAAU,IAAI,SAAS,MAAM,QAAQ,CAAC,CACvC;GACA,OAAO,KAAK,YAAY,KAAK,KAAK;EACnC,CAAC,CACF;EAEA,MAAM,8BAAc,IAAI,IAAoC;EAC5D,KAAK,MAAM,cAAc,aAAa;GACrC,IAAI,WAAW,WAAW,aAAa;GAEvC,KAAK,MAAM,SAAS,WAAW,OAAO;IACrC,MAAM,WAAW,YAAY,IAAI,MAAM,SAAS;IAChD,IAAI,YAAY,QAAQ,MAAM,iBAAiB,SAAS,gBACvD,YAAY,IAAI,MAAM,WAAW,KAAK;GAExC;EACD;EAEA,OAAO,CAAC,GAAG,YAAY,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,iBAAiB,EAAE,cAAc;CACpF;;;;CAKA,MAAc,gBACb,YACA,OACA,SACiD;EACjD,MAAM,WAAgB,CAAC;EACvB,MAAM,gBAAgB,YAAyD;GAC9E,KAAK,MAAM,UAAU,SAAS;IAC7B,IAAI,OAAO,WAAW,eAAe,OAAO,SAAS,MAAM;IAE3D,SAAS,KAAK,OAAO,KAAK;GAC3B;EACD;EAGA,IAAI,SAAS,GAAG;GACf,aAAa,MAAM,QAAQ,WAAW,WAAW,IAAI,OAAO,CAAC,CAAC;GAC9D,OAAO;IAAY;IAAU,WAAW;GAAK;EAC9C;EAMA,IAAI,SAAS;EACb,OAAO,SAAS,SAAS,SAAS,SAAS,WAAW,QAAQ;GAG7D,MAAM,QAAQ,WAAW,MAAM,QAAQ,UAAU,QAAQ,SAAS,OAAO;GACzE,UAAU,MAAM;GAChB,aAAa,MAAM,QAAQ,WAAW,MAAM,IAAI,OAAO,CAAC,CAAC;EAC1D;EAEA,OAAO;GAAY;GAAU,WAAW,UAAU,WAAW;EAAO;CACrE;;CAGA,MAAc,eAAe,OAA8E;EAC1G,MAAM,SAAS,KAAK,WAAW,IAAI,MAAM,SAAS;EAClD,IACC,UAAU,QACV,OAAO,SAAS,MAAM,QACtB,OAAO,YAAY,MAAM,kBACzB,OAAO,SAAS,MAAM,MACrB;GAED,KAAK,WAAW,OAAO,MAAM,SAAS;GACtC,KAAK,WAAW,IAAI,MAAM,WAAW,MAAM;GAC3C,OAAO,OAAO;EACf;EAEA,IAAI;EACJ,IAAI;GACH,UAAU,MAAM,KAAK,YAAY,KAAK;EACvC,QAAQ;GACP;EACD;EAEA,KAAK,WAAW,IAAI,MAAM,WAAW;GACpC,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,MAAM,MAAM;GACH;EACV,CAAC;EACD,IAAI,KAAK,WAAW,OAAO,mBAAmB;GAC7C,MAAM,SAAS,KAAK,WAAW,KAAK,CAAC,CAAC,KAAK;GAC3C,IAAI,CAAC,OAAO,MACX,KAAK,WAAW,OAAO,OAAO,KAAK;EAErC;EACA,OAAO;CACR;;;;CAKA,MAAc,qBAAqB,OAA4D;EAC9F,MAAM,SAAS,KAAK,gBAAgB,IAAI,MAAM,IAAI;EAClD,IAAI,UAAU,MAAM,OAAO;EAE3B,MAAM,aAAa,QAAQ,MAAM,IAAI;EACrC,MAAM,iBAAiB,SAAS,UAAU;EAC1C,MAAM,qBAAqB,QAC1B,qBAAqB,GAAG,CAAC,CAAC,YAAY,MAAM,eAAe,YAAY;EAExE,IAAI;GACH,MAAM,QAAQ,KAAK,MAAM,MAAM,SAAS,KAAK,YAAY,qBAAqB,GAAG,MAAM,CAAC;GAGxF,IAAI,MAAM,QAAQ,MAAM,OAAO,GAAG;IACjC,MAAM,UAAU,MAAM,QAAQ,MAAK,SAAQ,MAAM,cAAc,MAAM,SAAS;IAC9E,IAAI,OAAO,SAAS,gBAAgB,YAAY,kBAAkB,QAAQ,WAAW,GAAG;KACvF,KAAK,sBAAsB,MAAM,MAAM,QAAQ,WAAW;KAC1D,OAAO,QAAQ;IAChB;GACD;EACD,QAAQ,CAER;EAEA,MAAM,KAAK,gBAAgB;GAAE,OAAO,iBAAiB,MAAM,MAAM,EAAE,UAAU,OAAO,CAAC;GAAG,WAAW;EAAS,CAAC;EAC7G,IAAI;GACH,WAAW,MAAM,QAAQ,IAAI;IAC5B,IAAI,CAAC,KAAK,SAAS,SAAO,GAAG;IAE7B,IAAI;IACJ,IAAI;KACH,SAAS,KAAK,MAAM,IAAI;IACzB,QAAQ;KACP;IACD;IACA,IAAI,OAAO,aAAa,QAAQ,OAAO,cAAc,MAAM,WAAW;IAEtE,IAAI,OAAO,OAAO,QAAQ,YAAY,kBAAkB,OAAO,GAAG,GAAG;KACpE,KAAK,sBAAsB,MAAM,MAAM,OAAO,GAAG;KACjD,OAAO,OAAO;IACf;GACD;EACD,UAAU;GACT,GAAG,MAAM;EACV;CAGD;CAEA,sBAA8B,MAAc,KAAmB;EAC9D,KAAK,gBAAgB,IAAI,MAAM,GAAG;EAClC,IAAI,KAAK,gBAAgB,OAAO,mBAAmB;GAClD,MAAM,SAAS,KAAK,gBAAgB,KAAK,CAAC,CAAC,KAAK;GAChD,IAAI,CAAC,OAAO,MACX,KAAK,gBAAgB,OAAO,OAAO,KAAK;EAE1C;CACD;;;;;;;;;CAUA,MAAgB,YAAY,OAAkE;EAC7F,MAAM,SAA2B,CAAC;EAClC,IAAI;EAEJ,MAAM,SAAS,QAAgB,yBAAwC;GAEtE,MAAM,QADO,OAAO,SAAS,MACZ,CAAC,CAAC,MAAM,IAAI;GAE7B,IAAI,sBACH,MAAM,MAAM;GAEb,KAAK,MAAM,QAAQ,OAAO;IACzB,MAAM,SAAS,iBAAiB,MAAM,MAAM,SAAS;IACrD,IAAI,UAAU,MAAM;IAEpB,gBAAgB,QAAQ,MAAM;IAC9B,IAAI,OAAO,cAAc,QAAQ,OAAO,WAAW,SAAS,GAC3D,aAAa,OAAO;GAEtB;EACD;EAEA,MAAM,SAAS,MAAM,KAAK,MAAM,MAAM,GAAG;EACzC,IAAI;GACH,IAAI,MAAM,QAAQ,oBAAoB,GAAG;IACxC,MAAM,QAAQ,OAAO,MAAM,MAAM,IAAI;IACrC,MAAM,OAAO,KAAK,OAAO,GAAG,MAAM,MAAM,CAAC;IACzC,MAAM,OAAO,KAAK;GACnB,OAAO;IACN,MAAM,OAAO,OAAO,MAAM,iBAAiB;IAC3C,MAAM,OAAO,KAAK,MAAM,GAAG,mBAAmB,CAAC;IAC/C,MAAM,MAAM,KAAK;IAEjB,MAAM,OAAO,OAAO,MAAM,iBAAiB;IAC3C,MAAM,OAAO,KAAK,MAAM,GAAG,mBAAmB,MAAM,OAAO,iBAAiB;IAC5E,MAAM,MAAM,IAAI;GACjB;EACD,UAAU;GACT,MAAM,OAAO,MAAM;EACpB;EAEA,OAAO;GACN,GAAG;GACK;GACI;EACb;CACD;CAEA,kBAAoC;EACnC,OAAO,KAAK,QAAQ,GAAG,WAAW,UAAU;CAC7C;;;;;;CAOA,MAAgB,kBAAkB,KAAsD;EACvF,IAAI,OAAO,QAAQ,IAAI,WAAW,GAAG,OAAO,KAAA;EAE5C,MAAM,OAAO,KAAK,gBAAgB;EAClC,MAAM,UAAU,qBAAqB,GAAG;EAExC,MAAM,QAAQ,KAAK,MAAM,OAAO;EAChC,IAAI,MAAM,gBAAgB,KAAK,GAAG,OAAO;EAEzC,IAAI;EACJ,IAAI;GACH,OAAO,MAAM,QAAQ,IAAI;EAC1B,SAAS,IAAI;GAGZ,IAAI,CAAC,eAAe,EAAE,GAAG,MAAM;GAE/B;EACD;EAEA,MAAM,UAAU,QAAQ,YAAY;EACpC,MAAM,QAAQ,KAAK,MAAK,MAAK,MAAM,WAAW,EAAE,YAAY,MAAM,OAAO;EACzE,OAAO,SAAS,OAAO,KAAK,MAAM,KAAK,IAAI,KAAA;CAC5C;CAEA,MAAgB,iBAAiB,WAAmB,KAAsD;EACzG,MAAM,WAAW,GAAG,UAAU;EAE9B,MAAM,MAAM,MAAM,KAAK,kBAAkB,GAAG;EAC5C,IAAI,OAAO,MAAM;GAChB,MAAM,YAAY,KAAK,KAAK,QAAQ;GACpC,IAAI,MAAM,WAAW,SAAS,GAAG,OAAO;EACzC;EAIA,MAAM,OAAO,KAAK,gBAAgB;EAClC,IAAI;EACJ,IAAI;GACH,OAAO,MAAM,QAAQ,IAAI;EAC1B,SAAS,IAAI;GACZ,IAAI,CAAC,eAAe,EAAE,GAAG,MAAM;GAE/B;EACD;EAQA,QAAO,MANkB,QAAQ,IAChC,KAAK,IAAI,OAAM,MAAK;GACnB,MAAM,YAAY,KAAK,MAAM,GAAG,QAAQ;GACxC,OAAQ,MAAM,WAAW,SAAS,IAAK,YAAY,KAAA;EACpD,CAAC,CACF,EAAA,CACkB,MAAK,MAAK,KAAK,IAAI;CACtC;;;;;;;CAQA,MAAgB,UACf,MACA,OACA,KACA,WACA,YAC6D;EAC7D,MAAM,SAAS,MAAM,UAAU,MAAM,OAAO,GAAG;EAC/C,MAAM,SAA2B,EAAE,GAAG,WAAW;EAEjD,IAAI,iBAAiB;EACrB,IAAI,YAAY;EAChB,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;GACvC,IAAI,OAAO,OAAO,IAAM;GAIxB,MAAM,UAAU,IAAI,aAAa,OAAO,IAAI,OAAO,KAAO,IAAI,IAAI;GAClE,IAAI,UAAU,WAEb,eADa,OAAO,SAAS,QAAQ,WAAW,OAC9B,GAAG,WAAW,MAAM;GAEvC,YAAY,IAAI;GAChB,iBAAiB,QAAQ,IAAI;EAC9B;EAEA,OAAO;GAAU;GAAQ,aAAa;EAAe;CACtD;AACD;AAEA,SAAS,eAAe,MAAc,WAAmB,QAAgC;CACxF,IAAI,CAAC,kBAAkB,IAAI,GAAG;CAE9B,IAAI;CACJ,IAAI;EACH,QAAQ,KAAK,MAAM,IAAI;CACxB,QAAQ;EACP;CACD;CACA,IAAI,MAAM,aAAa,QAAQ,MAAM,cAAc,WAAW;CAE9D,gBAAgB,OAAO,MAAM;AAC9B;;;;AAKA,SAAS,iBAAiB,MAAc,WAA2C;CAClF,IAAI,KAAK,WAAW,KAAK,CAAC,KAAK,SAAS,UAAQ,GAAG,OAAO,KAAA;CAE1D,IAAI;CACJ,IAAI;EACH,QAAQ,KAAK,MAAM,IAAI;CACxB,QAAQ;EACP;CACD;CACA,IAAI,MAAM,aAAa,QAAQ,MAAM,cAAc,WAAW,OAAO,KAAA;CAErE,OAAO;AACR;AAEA,SAAS,gBAAgB,OAAmB,QAAgC;CAC3E,QAAQ,MAAM,MAAd;EACC,KAAK;GACJ,IAAI,OAAO,MAAM,gBAAgB,YAAY,MAAM,YAAY,SAAS,GACvE,OAAO,SAAS,MAAM;GAEvB;EACD,KAAK;GACJ,IAAI,OAAO,MAAM,YAAY,YAAY,MAAM,QAAQ,SAAS,GAC/D,OAAO,KAAK,MAAM;GAEnB;EACD,KAAK,cACJ,IAAI,OAAO,MAAM,cAAc,YAAY,MAAM,UAAU,SAAS,GACnE,OAAO,QAAQ,MAAM;CAGxB;AACD;;;;;AAMA,SAAgB,qBAAqB,KAAqB;CACzD,OAAO,IAAI,QAAQ,iBAAiB,GAAG;AACxC;AAEA,SAAS,kBAAkB,MAAuB;CACjD,IAAI,CAAC,KAAK,SAAS,UAAQ,GAAG,OAAO;CACrC,OAAO,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,YAAY;AAC7D;;;AAIA,SAAS,kBAAkB,SAA4C;CACtE,MAAM,EAAE,QAAQ,IAAI,UAAU,QAAQ;CACtC,OAAO,UAAU,QAAQ,MAAM,QAAQ,SAAS,QAAQ,QAAQ,cAAc;AAC/E;AAEA,eAAe,WAAW,MAAgC;CACzD,IAAI;EAEH,QAAO,MADa,KAAK,IAAI,EAAA,CAChB,OAAO;CACrB,SAAS,IAAI;EAEZ,IAAI,CAAC,eAAe,EAAE,GAAG,MAAM;EAE/B,OAAO;CACR;AACD;AAEA,eAAe,gBAAgB,MAAgC;CAC9D,IAAI;EAEH,QAAO,MADa,KAAK,IAAI,EAAA,CAChB,YAAY;CAC1B,SAAS,IAAI;EAGZ,IAAI,eAAe,EAAE,GAAG,OAAO;EAE/B,MAAM;CACP;AACD;;;;AAKA,SAAS,eAAe,IAAsB;CAC7C,MAAM,OAAQ,IAA0C;CACxD,OAAO,SAAS,YAAY,SAAS;AACtC;AAEA,SAAS,UAAU,MAAc,OAAe,KAA8B;CAC7E,OAAO,IAAI,SAAS,SAAS,WAAW;EACvC,MAAM,SAAmB,CAAC;EAC1B,MAAM,SAAS,iBAAiB,MAAM;GAAS;GAAO,KAAK,MAAM;EAAE,CAAC;EACpE,OAAO,GAAG,SAAQ,UAAS,OAAO,KAAK,KAAe,CAAC;EACvD,OAAO,GAAG,aAAa,QAAQ,OAAO,OAAO,MAAM,CAAC,CAAC;EACrD,OAAO,GAAG,SAAS,MAAM;CAC1B,CAAC;AACF"}
@@ -1,8 +1,8 @@
1
1
  import { CancellationError, isCancellationError } from "../../../utils/cancellation.js";
2
2
  import { Logger as _logger, getLoggableName } from "../../../utils/logger.js";
3
3
  import { maybeStartScopedLogger } from "../../../utils/logger.scoped.js";
4
- import _usingCtx from "../../../_virtual/@oxc-project+runtime/helpers/esm/usingCtx.js";
5
4
  import { uuid } from "../../../utils/crypto.js";
5
+ import _usingCtx from "../../../_virtual/@oxc-project+runtime/helpers/esm/usingCtx.js";
6
6
  import { AIError } from "../errors.js";
7
7
  import { getActionName, getReducedMaxInputTokens, getValidatedTemperature } from "../utils/ai.utils.js";
8
8
  import { isResponseFormatRejected, rememberResponseFormatRejection } from "./responseFormatCache.js";
@@ -17,8 +17,8 @@ import { defaultIssueSort } from "../../../git/models/issue.js";
17
17
  import "../../../git/models/pullRequest.js";
18
18
  import { effectiveIssueSort, getIssueComparator } from "../../../git/utils/issue.utils.js";
19
19
  import { getPullRequestComparator } from "../../../git/utils/pullRequest.utils.js";
20
- import { githubSearchResultLimit } from "./config.js";
21
20
  import { fromGitHubIssue, fromGitHubIssueOrPullRequestState, fromGitHubPullRequest, fromGitHubPullRequestLite } from "../models.js";
21
+ import { githubSearchResultLimit } from "./config.js";
22
22
  import { gitHubIssueSearchRelationships, toGitHubIssueSearchQualifiers, toGitHubIssueSearchScopeQualifiers, toGitHubIssueSearchSlideQualifier, toGitHubIssueSortQualifier } from "./issueSearchQuery.js";
23
23
  import { toGitHubPullRequestSearchFacets } from "./pullRequestSearchQuery.js";
24
24
  import * as l10n from "@vscode/l10n";
@@ -1,6 +1,6 @@
1
1
  import { Disposable } from "../../../utils/disposable.js";
2
2
  import { Event } from "../../../utils/event.js";
3
- import { ConfiguredIntegrationDescriptor, ProviderAuthenticationSession } from "./models.js";
3
+ import { ConfiguredIntegrationDescriptor, ConnectionResource, ProviderAuthenticationSession } from "./models.js";
4
4
  import { IntegrationIds } from "../constants.js";
5
5
  import { IntegrationAuthenticationSessionDescriptor } from "./integrationAuthenticationProvider.js";
6
6
  import { IntegrationServiceContext } from "../context.js";
@@ -51,6 +51,7 @@ export declare class ConfiguredIntegrationService implements Disposable {
51
51
  */
52
52
  storeSession(id: IntegrationIds, session: ProviderAuthenticationSession, options?: {
53
53
  signOutMark?: number;
54
+ resource?: ConnectionResource;
54
55
  }): Promise<boolean>;
55
56
  getStoredSession(id: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): Promise<ProviderAuthenticationSession | undefined>;
56
57
  /**
@@ -80,16 +80,19 @@ var ConfiguredIntegrationService = class {
80
80
  const primary = descriptor.primary ?? existing?.primary ?? !primaryScope.some((d) => d.primary);
81
81
  const type = descriptor.type ?? existing?.type;
82
82
  const accountName = descriptor.accountName ?? existing?.accountName;
83
+ const resource = descriptor.resourceId != null ? descriptor : existing;
83
84
  const normalized = {
84
85
  ...descriptor,
85
86
  primary,
86
87
  type,
87
- accountName
88
+ accountName,
89
+ resourceName: resource?.resourceName,
90
+ resourceId: resource?.resourceId
88
91
  };
89
92
  let changed;
90
93
  if (existing != null) {
91
- if (existing.domain === normalized.domain && sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) && existing.expiresAt === normalized.expiresAt && existing.scopes === normalized.scopes && (existing.primary ?? false) === (normalized.primary ?? false) && existing.type === normalized.type && existing.accountName === normalized.accountName) return;
92
- changed = existing.domain !== normalized.domain || !sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) || existing.scopes !== normalized.scopes || (existing.primary ?? false) !== (normalized.primary ?? false) || existing.type !== normalized.type || existing.accountName !== normalized.accountName;
94
+ if (existing.domain === normalized.domain && sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) && existing.expiresAt === normalized.expiresAt && existing.scopes === normalized.scopes && (existing.primary ?? false) === (normalized.primary ?? false) && existing.type === normalized.type && existing.accountName === normalized.accountName && existing.resourceName === normalized.resourceName && existing.resourceId === normalized.resourceId) return;
95
+ changed = existing.domain !== normalized.domain || !sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) || existing.scopes !== normalized.scopes || (existing.primary ?? false) !== (normalized.primary ?? false) || existing.type !== normalized.type || existing.accountName !== normalized.accountName || existing.resourceName !== normalized.resourceName || existing.resourceId !== normalized.resourceId;
93
96
  descriptors.splice(descriptors.indexOf(existing), 1);
94
97
  } else changed = true;
95
98
  descriptors.push(normalized);
@@ -158,7 +161,7 @@ var ConfiguredIntegrationService = class {
158
161
  storeSession(id, session, options) {
159
162
  return this.withLock(id, async () => {
160
163
  if (options?.signOutMark != null && this.isSignedOutSince(id, session, options.signOutMark)) return false;
161
- await this.writeSecret(id, session);
164
+ await this.writeSecret(id, session, options?.resource);
162
165
  return true;
163
166
  });
164
167
  }
@@ -245,7 +248,7 @@ var ConfiguredIntegrationService = class {
245
248
  if (domain != null) return;
246
249
  await this.deleteSecrets(id, providersMetadata[id]?.domain || id, cloud);
247
250
  }
248
- async writeSecret(id, session) {
251
+ async writeSecret(id, session, resource) {
249
252
  await this.ctx.storage.storeSecret(this.getSecretKey(id, session.id, session.cloud ?? false), JSON.stringify(session));
250
253
  await this.addOrUpdateConfigured({
251
254
  id: session.id,
@@ -256,7 +259,9 @@ var ConfiguredIntegrationService = class {
256
259
  scopes: session.scopes.join(","),
257
260
  cloud: session.cloud ?? false,
258
261
  type: session.type,
259
- accountName: session.account?.label || void 0
262
+ accountName: session.account?.label || void 0,
263
+ resourceName: resource?.name,
264
+ resourceId: resource?.id
260
265
  });
261
266
  }
262
267
  async readSecret(id, sessionId, cloud = false) {
@@ -1 +1 @@
1
- {"version":3,"file":"configuredIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/configuredIntegrationService.ts"],"sourcesContent":["import { debounce } from '@gitlens/utils/debounce.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { flatten } from '@gitlens/utils/iterable.js';\nimport type {\n\tIntegrationIds,\n\tStoredConfiguredIntegrationDescriptor,\n\tStoredIntegrationConfigurations,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { providersMetadata } from '../providers/models.js';\nimport {\n\tareDomainsOnSameHost,\n\tbaseUrlFromDomain,\n\thostFromDomain,\n\tsameConfiguredBaseUrl,\n} from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider.js';\nimport type {\n\tCloudIntegrationAuthType,\n\tConfiguredIntegrationDescriptor,\n\tProviderAuthenticationSession,\n} from './models.js';\n\ninterface StoredSession {\n\tid: string;\n\taccessToken: string;\n\taccount?: { label?: string; displayName?: string; id: string };\n\tscopes: string[];\n\tcloud?: boolean;\n\ttype: CloudIntegrationAuthType | undefined;\n\texpiresAt?: string;\n\tdomain?: string;\n\t/** See {@link ProviderAuthenticationSession.baseUrl}: the configured address, path included. */\n\tbaseUrl?: string;\n\tprotocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\tappKey?: string;\n}\n\nexport interface ConfiguredIntegrationsChangeEvent {\n\treadonly added: readonly IntegrationIds[];\n\treadonly removed: readonly IntegrationIds[];\n}\n\nexport class ConfiguredIntegrationService implements Disposable {\n\tprivate readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();\n\tprivate storeConfiguredQueue: Promise<void> = Promise.resolve();\n\t/**\n\t * One provider's secret and descriptor mutations run one at a time, so a sign-out never interleaves with a\n\t * token being stored: storing writes the secret and then registers its descriptor, and a delete in between would\n\t * miss a connection it cannot see yet, or be undone by the pending write (#5948).\n\t */\n\tprivate readonly _locks = new Map<IntegrationIds, Promise<void>>();\n\t/** Monotonic across providers; see {@link getSignOutMark}. */\n\tprivate _signOutEpoch = 0;\n\t/** The epoch of the latest sign-out per scope: a provider, a self-managed host, or a single connection. */\n\tprivate readonly _signOuts = new Map<string, number>();\n\tget onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tprivate _configured?: Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>;\n\tprivate get configured(): Map<IntegrationIds, ConfiguredIntegrationDescriptor[]> {\n\t\tif (this._configured == null) {\n\t\t\tthis._configured = new Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>();\n\n\t\t\tconst storedConfigured = this.ctx.storage.get('integrations:configured');\n\t\t\tfor (const [id, configured] of Object.entries(storedConfigured ?? {}) as [\n\t\t\t\tIntegrationIds,\n\t\t\t\tStoredConfiguredIntegrationDescriptor[],\n\t\t\t][]) {\n\t\t\t\tif (configured == null) continue;\n\n\t\t\t\tconst descriptors = configured.map(d => ({\n\t\t\t\t\t...d,\n\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? getStoredBaseUrl(d.baseUrl, d.domain) : d.baseUrl,\n\t\t\t\t\tdomain: this.normalizeConfiguredDomain(id, d.domain),\n\t\t\t\t\t// Backfill a stable connection id for pre-multi-account stored data: the domain for\n\t\t\t\t\t// self-managed hosts, or the provider's canonical domain for cloud (which is the legacy\n\t\t\t\t\t// secret-key session id), so existing secrets keep resolving with zero migration. Fall back\n\t\t\t\t\t// to the integration id (never empty) when no domain is known — self-managed providers carry\n\t\t\t\t\t// `domain: ''` in metadata, and an empty id would collide across connections and produce\n\t\t\t\t\t// ambiguous secret keys (`...|`).\n\t\t\t\t\tid: d.id || d.domain || providersMetadata[id]?.domain || id,\n\t\t\t\t\texpiresAt: d.expiresAt ? new Date(d.expiresAt) : undefined,\n\t\t\t\t}));\n\t\t\t\tthis._configured.set(id, descriptors);\n\t\t\t}\n\t\t}\n\n\t\treturn this._configured;\n\t}\n\n\t// Synchronous: reads the in-memory configured map (lazily hydrated from storage). No async work —\n\t// the old async variant only existed to weave in the built-in VS Code GitHub session, which is gone.\n\tgetConfigured(\n\t\tid?: IntegrationIds,\n\t\toptions?: { cloud?: boolean; domain?: string },\n\t): ConfiguredIntegrationDescriptor[] {\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\n\t\tconst configured =\n\t\t\tid != null\n\t\t\t\t? this.configured.get(id)\n\t\t\t\t: [...flatten<ConfiguredIntegrationDescriptor>(this.configured.values())];\n\t\tif (!configured?.length) return descriptors;\n\n\t\tif (options?.domain != null || options?.cloud != null) {\n\t\t\tfor (const descriptor of configured) {\n\t\t\t\tif (\n\t\t\t\t\t(options?.domain != null &&\n\t\t\t\t\t\t!this.domainsMatch(id ?? descriptor.integrationId, descriptor.domain, options.domain)) ||\n\t\t\t\t\t(options?.cloud === true && !descriptor.cloud) ||\n\t\t\t\t\t(options?.cloud === false && descriptor.cloud)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdescriptors.push(descriptor);\n\t\t\t}\n\t\t} else {\n\t\t\tdescriptors.push(...configured);\n\t\t}\n\n\t\treturn descriptors;\n\t}\n\n\tprivate async storeConfigured(): Promise<void> {\n\t\t// We need to convert the map to a record to store\n\t\tconst configured: StoredIntegrationConfigurations = {} as unknown as StoredIntegrationConfigurations;\n\t\tfor (const [id, descriptors] of this.configured) {\n\t\t\tconfigured[id] = descriptors.map(d => ({\n\t\t\t\t...d,\n\t\t\t\texpiresAt: d.expiresAt\n\t\t\t\t\t? d.expiresAt instanceof Date\n\t\t\t\t\t\t? d.expiresAt.toISOString()\n\t\t\t\t\t\t: d.expiresAt\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\t\t}\n\n\t\tconst pending = this.storeConfiguredQueue.then(() =>\n\t\t\tthis.ctx.storage.store('integrations:configured', configured),\n\t\t);\n\t\tthis.storeConfiguredQueue = pending.catch(() => {});\n\t\tawait pending;\n\t}\n\n\tprivate async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {\n\t\tdescriptor = {\n\t\t\t...descriptor,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(descriptor.integrationId)\n\t\t\t\t? getStoredBaseUrl(descriptor.baseUrl, descriptor.domain)\n\t\t\t\t: descriptor.baseUrl,\n\t\t\tdomain: this.normalizeConfiguredDomain(descriptor.integrationId, descriptor.domain),\n\t\t};\n\t\tconst descriptors = this.configured.get(descriptor.integrationId) ?? [];\n\t\t// Key connections by their stable id (+ cloud, to preserve legacy local/cloud coexistence) so\n\t\t// multiple accounts on the same provider+domain no longer overwrite each other.\n\t\tconst existing = descriptors.find(d => d.id === descriptor.id && d.cloud === descriptor.cloud);\n\n\t\t// The first connection for a provider/domain is primary by default; an explicit flag (or the\n\t\t// existing value) always wins. Self-managed providers can span multiple hosts under the same\n\t\t// integration id, so each host needs its own primary.\n\t\tconst primaryScope = this.getPrimaryScope(descriptor.integrationId, descriptors, descriptor.domain);\n\t\tconst primary = descriptor.primary ?? existing?.primary ?? !primaryScope.some(d => d.primary);\n\t\t// Preserve a previously-resolved type/accountName when the incoming write doesn't carry them (e.g.\n\t\t// the model re-stores the primary session with an empty account, after reconcile enriched it).\n\t\tconst type = descriptor.type ?? existing?.type;\n\t\tconst accountName = descriptor.accountName ?? existing?.accountName;\n\t\tconst normalized: ConfiguredIntegrationDescriptor = {\n\t\t\t...descriptor,\n\t\t\tprimary: primary,\n\t\t\ttype: type,\n\t\t\taccountName: accountName,\n\t\t};\n\n\t\tlet changed: boolean;\n\t\tif (existing != null) {\n\t\t\t// `baseUrl` is compared alongside `domain`, not folded into it: `domain` is normalized to a bare\n\t\t\t// host, so a connection re-pointed to another context path on the SAME host is identical by domain\n\t\t\t// and would otherwise be treated as unchanged — leaving the stored descriptor, and every warm\n\t\t\t// integration listening for the change, on the old address.\n\t\t\tif (\n\t\t\t\texisting.domain === normalized.domain &&\n\t\t\t\tsameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) &&\n\t\t\t\texisting.expiresAt === normalized.expiresAt &&\n\t\t\t\texisting.scopes === normalized.scopes &&\n\t\t\t\t(existing.primary ?? false) === (normalized.primary ?? false) &&\n\t\t\t\texisting.type === normalized.type &&\n\t\t\t\texisting.accountName === normalized.accountName\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only fire the change event on address/scopes/primary/type/accountName changes (ignore expiresAt churn)\n\t\t\tchanged =\n\t\t\t\texisting.domain !== normalized.domain ||\n\t\t\t\t!sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) ||\n\t\t\t\texisting.scopes !== normalized.scopes ||\n\t\t\t\t(existing.primary ?? false) !== (normalized.primary ?? false) ||\n\t\t\t\texisting.type !== normalized.type ||\n\t\t\t\texisting.accountName !== normalized.accountName;\n\n\t\t\t// remove the existing descriptor from the array\n\t\t\tdescriptors.splice(descriptors.indexOf(existing), 1);\n\t\t} else {\n\t\t\tchanged = true;\n\t\t}\n\n\t\tdescriptors.push(normalized);\n\t\tthis.configured.set(descriptor.integrationId, descriptors);\n\n\t\tif (changed) {\n\t\t\tthis.fireChange(descriptor.integrationId);\n\t\t}\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate async removeConfigured(\n\t\tid: IntegrationIds,\n\t\toptions: { connectionId: string; cloud: boolean | undefined },\n\t): Promise<void> {\n\t\tconst existing = this.configured.get(id);\n\t\tif (existing == null || existing.length === 0) return;\n\n\t\tconst removedPrimaryDomains = new Set<string | undefined>();\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\t\tfor (const d of existing) {\n\t\t\tif (\n\t\t\t\td.id === options.connectionId &&\n\t\t\t\t(options?.cloud == null ||\n\t\t\t\t\t(options?.cloud === true && d.cloud === true) ||\n\t\t\t\t\t(options?.cloud === false && d.cloud === false))\n\t\t\t) {\n\t\t\t\tif (d.primary) {\n\t\t\t\t\tremovedPrimaryDomains.add(d.domain);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tdescriptors.push(d);\n\t\t}\n\n\t\tif (descriptors.length === existing.length) return; // nothing matched\n\n\t\t// Promote a secondary to primary when the removed connection was the primary and others remain\n\t\t// in the same primary scope, so the provider/host stays in a well-defined connected state.\n\t\tfor (const domain of removedPrimaryDomains) {\n\t\t\tif (descriptors.some(d => this.isInPrimaryScope(id, d, domain) && d.primary)) continue;\n\n\t\t\tconst index = descriptors.findIndex(d => this.isInPrimaryScope(id, d, domain));\n\t\t\tif (index !== -1) {\n\t\t\t\tdescriptors[index] = { ...descriptors[index], primary: true };\n\t\t\t}\n\t\t}\n\n\t\tthis.configured.set(id, descriptors);\n\t\tthis.fireChange(undefined, id);\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate withLock<T>(id: IntegrationIds, fn: () => Promise<T>): Promise<T> {\n\t\tconst run = (this._locks.get(id) ?? Promise.resolve()).then(fn);\n\t\tconst settled = run.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\tthis._locks.set(id, settled);\n\t\tvoid settled.then(() => {\n\t\t\tif (this._locks.get(id) === settled) {\n\t\t\t\tthis._locks.delete(id);\n\t\t\t}\n\t\t});\n\t\treturn run;\n\t}\n\n\t/**\n\t * Taken before a token is read or fetched, and handed to {@link storeSession}: a sign-out that lands after it\n\t * (a disconnect, a reauthentication, the connection being removed) refuses the store, so a token fetched across\n\t * it is not written back and reconnected on the next resolution (gitkraken/kepler#3546).\n\t */\n\tgetSignOutMark(): number {\n\t\treturn this._signOutEpoch;\n\t}\n\n\tprivate markSignOut(scope: string): void {\n\t\tthis._signOuts.set(scope, ++this._signOutEpoch);\n\t}\n\n\tprivate getHostScope(id: IntegrationIds, domain: string): string {\n\t\treturn `${id}|host:${hostFromDomain(domain) ?? domain}`;\n\t}\n\n\tprivate isSignedOutSince(id: IntegrationIds, session: ProviderAuthenticationSession, mark: number): boolean {\n\t\tconst since = (scope: string) => (this._signOuts.get(scope) ?? 0) > mark;\n\t\tif (since(id)) return true;\n\t\tif (isSelfManagedHostIntegrationId(id) && session.domain && since(this.getHostScope(id, session.domain))) {\n\t\t\treturn true;\n\t\t}\n\t\treturn since(`${id}|connection:${session.id}`);\n\t}\n\n\t/**\n\t * Stores the session's secret and descriptor together. With a `signOutMark`, nothing is stored, and `false` is\n\t * returned, when a sign-out covering the session landed after the mark was taken.\n\t */\n\tstoreSession(\n\t\tid: IntegrationIds,\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions?: { signOutMark?: number },\n\t): Promise<boolean> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tif (options?.signOutMark != null && this.isSignedOutSince(id, session, options.signOutMark)) return false;\n\n\t\t\tawait this.writeSecret(id, session);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tgetStoredSession(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\t// Under the lock too: reading can repair storage (a legacy local-key cloud session, a missing descriptor).\n\t\treturn this.withLock(id, async () => {\n\t\t\tconst sessionId = this.resolveConnectionId(id, descriptor);\n\t\t\tlet session = descriptor.cloud === true ? undefined : await this.readSecret(id, sessionId, false);\n\n\t\t\tlet cloudIfMissing = false;\n\t\t\tif (session != null) {\n\t\t\t\t// Check the `expiresAt` field\n\t\t\t\t// If it has an expiresAt property and the key is the old type, then it's a cloud session,\n\t\t\t\t// so delete it from the local key and\n\t\t\t\t// store with the \"cloud\" type key, and then use that one.\n\t\t\t\t// Otherwise it's a local session under the local key, so just return it.\n\t\t\t\tif (session.expiresAt != null) {\n\t\t\t\t\tcloudIfMissing = true;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.deleteSecrets(id, session.id);\n\t\t\t\t\t\tawait this.writeSecret(id, session);\n\t\t\t\t\t} catch {}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If no local session we try to restore a session with the cloud key\n\t\t\tif (session == null && descriptor.cloud !== false) {\n\t\t\t\tcloudIfMissing = true;\n\t\t\t\tsession = await this.readSecret(id, sessionId, true);\n\t\t\t}\n\n\t\t\treturn convertStoredSessionToSession(session, descriptor, cloudIfMissing);\n\t\t});\n\t}\n\n\t/**\n\t * Deletes one connection's stored session. `signOut` is for a delete that signs the integration out (a forced new\n\t * session): it also refuses the store of any token of the provider, or of the descriptor's self-managed host,\n\t * fetched before it (see {@link getSignOutMark}). A cleanup of a replaced token leaves it unset.\n\t */\n\tdeleteStoredSessions(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean; signOut?: boolean },\n\t): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tif (options?.signOut) {\n\t\t\t\tthis.markSignOut(\n\t\t\t\t\tisSelfManagedHostIntegrationId(id) && descriptor.domain\n\t\t\t\t\t\t? this.getHostScope(id, descriptor.domain)\n\t\t\t\t\t\t: id,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.deleteSecrets(id, this.resolveConnectionId(id, descriptor), cloud, options);\n\t\t});\n\t}\n\n\t/** Signs out of every stored session of the provider, or of one self-managed host when `domain` is given. */\n\tdeleteAllStoredSessions(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tthis.markSignOut(isSelfManagedHostIntegrationId(id) && domain != null ? this.getHostScope(id, domain) : id);\n\t\t\tawait this.deleteAllSecrets(id, cloud, domain);\n\t\t});\n\t}\n\n\t/**\n\t * Reclaims all stored configuration + secrets for the given (typically retired) integration ids.\n\t * Used to clean up state left behind when an id is removed from the model — e.g. the local\n\t * self-managed `github-enterprise`/`gitlab-self-hosted` ids dropped when integrations went cloud-only.\n\t * Domains are read from the stored config (secrets are keyed by domain); ids with no stored config\n\t * are a no-op.\n\t */\n\tasync purgeStoredConfiguration(ids: readonly string[]): Promise<void> {\n\t\tconst stored =\n\t\t\tthis.ctx.storage.get<Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>>(\n\t\t\t\t'integrations:configured',\n\t\t\t);\n\t\tif (stored == null) return;\n\n\t\tlet changed = false;\n\t\tfor (const id of ids) {\n\t\t\tif (!(id in stored)) continue;\n\n\t\t\tfor (const descriptor of stored[id] ?? []) {\n\t\t\t\t// Mirror the hydration backfill so the secret keys we delete match what was written: fall back\n\t\t\t\t// through domain, the provider's canonical domain, then the integration id (never empty). A\n\t\t\t\t// legacy cloud descriptor stored neither id nor domain, so an empty connection id here would\n\t\t\t\t// miss the canonical-domain secret (e.g. `integration.auth.cloud:github|github.com`) and orphan it.\n\t\t\t\tconst connectionId =\n\t\t\t\t\tdescriptor.id || descriptor.domain || providersMetadata[id as IntegrationIds]?.domain || id;\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tif (descriptor.domain) {\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(`connected:${id}:${descriptor.domain}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchanged = true;\n\t\t}\n\t\tif (!changed) return;\n\n\t\tconst remaining = Object.fromEntries(Object.entries(stored).filter(([key]) => !ids.includes(key)));\n\t\tawait this.ctx.storage.store('integrations:configured', remaining as StoredIntegrationConfigurations);\n\t}\n\n\tprivate async deleteSecrets(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tif (cloud == null || cloud === false) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (cloud == null || cloud === true) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (options?.preserveConfigured) return;\n\n\t\tawait this.removeConfigured(id, { connectionId: connectionId, cloud: cloud });\n\t}\n\n\tprivate async deleteAllSecrets(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\t// Delete every connection's secret (multi-account): secrets are keyed per connection id, so a\n\t\t// single canonical-domain delete would orphan secondary tokens. When a domain is given (self-managed\n\t\t// disconnect of one host), scope to that host so other hosts under the same provider id survive.\n\t\tconst descriptors = this.configured.get(id);\n\t\tconst connectionIds = [\n\t\t\t...new Set(\n\t\t\t\t(domain != null ? descriptors?.filter(c => this.domainsMatch(id, c.domain, domain)) : descriptors)?.map(\n\t\t\t\t\tc => c.id,\n\t\t\t\t) ?? [],\n\t\t\t),\n\t\t];\n\t\tif (connectionIds.length) {\n\t\t\tfor (const connectionId of connectionIds) {\n\t\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// A domain-scoped clear that matched nothing means this host has no stored connections, so it must be\n\t\t// a no-op. The canonical-domain fallback below would target a bogus key (self-managed canonical domain\n\t\t// is empty), so skip it.\n\t\tif (domain != null) return;\n\n\t\t// No configured connections (e.g. an orphaned secret with no descriptor): fall back to the\n\t\t// provider's canonical domain, which is the legacy session id for cloud providers. Mirror the\n\t\t// hydration backfill's `|| id` guard so a self-managed provider (empty canonical domain) doesn't\n\t\t// target an ambiguous empty connection id.\n\t\tawait this.deleteSecrets(id, providersMetadata[id]?.domain || id, cloud);\n\t}\n\n\tprivate async writeSecret(\n\t\tid: IntegrationIds,\n\t\tsession: ProviderAuthenticationSession | StoredSession,\n\t): Promise<void> {\n\t\tawait this.ctx.storage.storeSecret(\n\t\t\tthis.getSecretKey(id, session.id, session.cloud ?? false),\n\t\t\tJSON.stringify(session),\n\t\t);\n\n\t\tawait this.addOrUpdateConfigured({\n\t\t\tid: session.id,\n\t\t\tintegrationId: id,\n\t\t\tdomain: isSelfManagedHostIntegrationId(id) ? session.domain : undefined,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? session.baseUrl : undefined,\n\t\t\texpiresAt: session.expiresAt,\n\t\t\tscopes: session.scopes.join(','),\n\t\t\tcloud: session.cloud ?? false,\n\t\t\ttype: session.type,\n\t\t\taccountName: session.account?.label || undefined,\n\t\t});\n\t}\n\n\tprivate async readSecret(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): Promise<StoredSession | undefined> {\n\t\tlet storedSession: StoredSession | undefined;\n\t\ttry {\n\t\t\tconst sessionJSON = await this.ctx.storage.getSecret(this.getSecretKey(id, sessionId, cloud));\n\t\t\tif (sessionJSON) {\n\t\t\t\tstoredSession = JSON.parse(sessionJSON);\n\t\t\t\tif (storedSession != null) {\n\t\t\t\t\tconst configured = this.configured.get(id);\n\t\t\t\t\tconst connectionId = storedSession.id ?? sessionId;\n\t\t\t\t\tconst sessionCloud = storedSession.cloud ?? cloud;\n\t\t\t\t\tconst domain = isSelfManagedHostIntegrationId(id)\n\t\t\t\t\t\t? (storedSession.domain ?? storedSession.id)\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (\n\t\t\t\t\t\tconfigured == null ||\n\t\t\t\t\t\tconfigured.length === 0 ||\n\t\t\t\t\t\t!configured.some(\n\t\t\t\t\t\t\tc => c.id === connectionId && c.integrationId === id && c.cloud === sessionCloud,\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tawait this.addOrUpdateConfigured({\n\t\t\t\t\t\t\tid: connectionId,\n\t\t\t\t\t\t\tintegrationId: id,\n\t\t\t\t\t\t\tdomain: domain,\n\t\t\t\t\t\t\t// Rebuilt from the secret, so it must carry the address the session does; omitting it\n\t\t\t\t\t\t\t// leaves the descriptor claiming no context path and makes reconcile read every\n\t\t\t\t\t\t\t// routine check-in as a re-pointed connection.\n\t\t\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? storedSession.baseUrl : undefined,\n\t\t\t\t\t\t\texpiresAt: storedSession.expiresAt,\n\t\t\t\t\t\t\tscopes: storedSession.scopes.join(','),\n\t\t\t\t\t\t\tcloud: sessionCloud,\n\t\t\t\t\t\t\ttype: storedSession.type,\n\t\t\t\t\t\t\taccountName: storedSession.account?.label || undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (_ex) {\n\t\t\ttry {\n\t\t\t\tawait this.deleteSecrets(id, sessionId, cloud);\n\t\t\t} catch {}\n\t\t}\n\t\treturn storedSession;\n\t}\n\n\tprivate getSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): `integration.auth:${IntegrationIds}|${string}` | `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn cloud ? this.getCloudSecretKey(id, sessionId) : this.getLocalSecretKey(id, sessionId);\n\t}\n\n\tprivate getLocalSecretKey(id: IntegrationIds, sessionId: string): `integration.auth:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth:${id}|${sessionId}`;\n\t}\n\n\tprivate getCloudSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t): `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth.cloud:${id}|${sessionId}`;\n\t}\n\n\tprivate getPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptors: ConfiguredIntegrationDescriptor[],\n\t\tdomain: string | undefined,\n\t): ConfiguredIntegrationDescriptor[] {\n\t\treturn isSelfManagedHostIntegrationId(id)\n\t\t\t? descriptors.filter(d => this.isInPrimaryScope(id, d, domain))\n\t\t\t: descriptors;\n\t}\n\n\tprivate isInPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptor: ConfiguredIntegrationDescriptor,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, descriptor.domain, domain);\n\t}\n\n\t/**\n\t * Resolves the connection id used as the secret-key session id for an auth descriptor.\n\t * - If the descriptor targets a specific connection (`connectionId`), that wins.\n\t * - Otherwise returns the primary connection's id for the provider (matching the descriptor's domain\n\t * for self-managed hosts; any connection for cloud hosts, where the stored domain is undefined).\n\t * - Falls back to the descriptor domain when nothing is configured yet, preserving legacy\n\t * (pre-multi-account) reads with zero secret migration.\n\t */\n\tresolveConnectionId(id: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string {\n\t\t// An empty connectionId is not a real target — treat it as \"no target\" so it resolves the primary\n\t\t// rather than keying a secret under an empty id.\n\t\tif (descriptor.connectionId) return descriptor.connectionId;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? descriptor.domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, domain, descriptor.cloud);\n\t\treturn (candidates?.find(c => c.primary) ?? candidates?.[0])?.id ?? descriptor.domain;\n\t}\n\n\t/**\n\t * Returns the id of the configured connection an unscoped descriptor resolves to (the primary for the\n\t * descriptor's domain, matching {@link resolveConnectionId}'s selection), or `undefined` when nothing is\n\t * configured for it. Unlike {@link resolveConnectionId} this only returns a genuine per-connection cloud\n\t * token id (never a local/PAT or domain-derived legacy id), so callers can safely use it to scope a cloud\n\t * token fetch to one host of a multi-host self-managed provider, or fall through to the provider-global\n\t * primary endpoint on `undefined`.\n\t */\n\t/**\n\t * The address already stored for a self-managed connection — see {@link ConfiguredIntegrationDescriptor.baseUrl}.\n\t *\n\t * Exists so a session rebuilt from a backend response that omits the domain can keep the address the\n\t * connection was configured with, rather than silently falling back to the bare host and losing a context\n\t * path the user never changed. Looked up by connection id first, then by host for an unscoped fetch.\n\t */\n\t/**\n\t * Whether `connectionId` names a cloud connection configured for `domain`'s host.\n\t *\n\t * The caller-facing question is \"is this id evidence that a token belongs to this host\". A connection id\n\t * can arrive from a caller rather than from our own configuration, so it is only evidence once it matches\n\t * a descriptor we stored for that host — see the host guard in `CloudIntegrationAuthenticationProvider`.\n\t */\n\tisConnectionConfiguredForHost(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string | undefined,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\tif (connectionId == null) return false;\n\n\t\tconst connection = this.getConfigured(id, { cloud: true }).find(c => c.id === connectionId);\n\t\tif (connection == null) return false;\n\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, connection.domain, domain);\n\t}\n\n\tgetConfiguredBaseUrl(id: IntegrationIds, connectionId: string | undefined, domain?: string): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return undefined;\n\n\t\tconst configured = this.getConfigured(id, { cloud: true });\n\t\tconst byId = connectionId != null ? configured.find(c => c.id === connectionId) : undefined;\n\t\tif (byId != null) return byId.baseUrl;\n\n\t\treturn configured.find(c => this.domainsMatch(id, c.domain, domain))?.baseUrl;\n\t}\n\n\tgetConfiguredConnectionId(id: IntegrationIds, domain: string | undefined, cloud?: boolean): string | undefined {\n\t\tconst scoped = isSelfManagedHostIntegrationId(id) ? domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, scoped, cloud);\n\t\tconst connection = candidates?.find(c => c.primary) ?? candidates?.[0];\n\t\tif (connection == null) return undefined;\n\n\t\t// Only return a real per-connection cloud token id. Reject a local (PAT) descriptor (surfaced by\n\t\t// scopeConnectionCandidates' non-cloud fallback) and a domain-derived legacy id (the descriptor's own\n\t\t// domain or the provider's canonical domain — see ConfiguredIntegrationDescriptor.id), either of which\n\t\t// would target `v1/provider-tokens/tokens/{domain}` (rejected as a non-uuid). Callers then fall through\n\t\t// to the provider-scoped primary endpoint.\n\t\tif (!connection.cloud) return undefined;\n\n\t\tconst canonicalDomain = providersMetadata[id]?.domain;\n\t\tif (connection.id === connection.domain || (canonicalDomain && connection.id === canonicalDomain)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn connection.id;\n\t}\n\n\t/**\n\t * Selects configured descriptors for a provider matching `domain`, further narrowed to the `cloud`\n\t * variant when specified. A connection id can have both a local (PAT) and cloud descriptor whose ids\n\t * differ (e.g. a legacy local descriptor keyed by domain alongside a real cloud token id); scoping by\n\t * `cloud` keeps a cloud-scoped read/delete from resolving the local id (and vice versa). Falls back to\n\t * the domain-only set when no variant matches, so legacy single-variant resolution is unchanged.\n\t */\n\tprivate scopeConnectionCandidates(\n\t\tid: IntegrationIds,\n\t\tdomain: string | undefined,\n\t\tcloud: boolean | undefined,\n\t): ConfiguredIntegrationDescriptor[] | undefined {\n\t\tconst candidates = this.configured.get(id)?.filter(c => this.domainsMatch(id, c.domain, domain));\n\t\tif (cloud == null || candidates == null) return candidates;\n\n\t\tconst scoped = candidates.filter(c => c.cloud === cloud);\n\t\treturn scoped.length ? scoped : candidates;\n\t}\n\n\t/**\n\t * Marks the given connection as the primary/default for the provider and clears the flag on its\n\t * siblings. Persists immediately instead of relying on a session re-store to carry the primary flag.\n\t */\n\tsetPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\treturn this.withLock(id, () => this.setPrimaryConnectionCore(id, connectionId));\n\t}\n\n\tprivate async setPrimaryConnectionCore(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\tconst descriptors = this.configured.get(id);\n\t\tif (descriptors == null || descriptors.length === 0) return;\n\t\tif (!descriptors.some(d => d.id === connectionId)) return;\n\n\t\tconst target =\n\t\t\tdescriptors.find(d => d.id === connectionId && d.cloud) ?? descriptors.find(d => d.id === connectionId);\n\t\tif (target == null) return;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? target.domain : undefined;\n\t\t// A connection id can have both a local (PAT) and cloud descriptor. Mark the primary on a single\n\t\t// canonical variant (prefer cloud, since multi-account primaries are cloud-driven) within the\n\t\t// provider/host scope, so other self-managed hosts keep their own default connection.\n\t\tconst primaryIsCloud = descriptors.some(\n\t\t\td => d.id === connectionId && d.cloud && this.isInPrimaryScope(id, d, domain),\n\t\t);\n\n\t\tlet changed = false;\n\t\tconst updated = descriptors.map(d => {\n\t\t\tif (!this.isInPrimaryScope(id, d, domain)) return d;\n\n\t\t\tconst primary = d.id === connectionId && (d.cloud ?? false) === primaryIsCloud;\n\t\t\tif ((d.primary ?? false) === primary) return d;\n\n\t\t\tchanged = true;\n\t\t\treturn { ...d, primary: primary };\n\t\t});\n\t\tif (!changed) return;\n\n\t\tthis.configured.set(id, updated);\n\t\tthis.fireChange(id);\n\t\tawait this.storeConfigured();\n\t}\n\n\t/**\n\t * Removes a single connection (its secret + configured descriptor). If it was the primary and other\n\t * connections remain, a secondary is promoted (see {@link removeConfigured}). Unlike\n\t * {@link deleteAllStoredSessions}, this only affects the targeted connection. Pass `cloud` to scope\n\t * the removal to the cloud/local variant when a local PAT and a cloud session share a connection id.\n\t */\n\tdeleteConnection(id: IntegrationIds, connectionId: string, cloud?: boolean): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\t// The connection is gone, so a token of it fetched before must not be stored back.\n\t\t\tthis.markSignOut(`${id}|connection:${connectionId}`);\n\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t});\n\t}\n\n\tprivate _addedIds = new Set<IntegrationIds>();\n\tprivate _removedIds = new Set<IntegrationIds>();\n\tprivate _fireChangeDebounced?: () => void;\n\n\tprivate normalizeConfiguredDomain(id: IntegrationIds, domain: string | undefined): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return domain;\n\n\t\treturn hostFromDomain(domain) ?? domain;\n\t}\n\n\tprivate domainsMatch(id: IntegrationIds, first: string | undefined, second: string | undefined): boolean {\n\t\tif (first === second) return true;\n\t\tif (!isSelfManagedHostIntegrationId(id)) return false;\n\t\tif (first == null || second == null) return false;\n\n\t\treturn areDomainsOnSameHost(first, second);\n\t}\n\n\tprivate fireChange(added?: IntegrationIds, removed?: IntegrationIds) {\n\t\tthis._fireChangeDebounced ??= debounce(() => {\n\t\t\tconst added = [...this._addedIds];\n\t\t\tthis._addedIds.clear();\n\t\t\tconst removed = [...this._removedIds];\n\t\t\tthis._removedIds.clear();\n\n\t\t\tthis._onDidChange.fire({ added: added, removed: removed });\n\t\t}, 250);\n\n\t\tif (added != null) {\n\t\t\tthis._addedIds.add(added);\n\t\t}\n\t\tif (removed != null) {\n\t\t\tthis._removedIds.add(removed);\n\t\t}\n\t\tthis._fireChangeDebounced();\n\t}\n}\n\nfunction convertStoredSessionToSession(\n\tstoredSession: StoredSession | undefined,\n\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\tcloudIfMissing: boolean,\n): ProviderAuthenticationSession | undefined {\n\tif (storedSession == null) return undefined;\n\n\treturn {\n\t\tid: storedSession.id,\n\t\taccessToken: storedSession.accessToken,\n\t\taccount: {\n\t\t\tid: storedSession.account?.id ?? '',\n\t\t\tlabel: storedSession.account?.label ?? '',\n\t\t},\n\t\tscopes: storedSession.scopes,\n\t\tcloud: storedSession.cloud ?? cloudIfMissing,\n\t\texpiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : undefined,\n\t\tdomain: storedSession.domain ?? descriptor.domain,\n\t\tbaseUrl: getStoredBaseUrl(storedSession.baseUrl, storedSession.domain, storedSession.protocol),\n\t\tprotocol: storedSession.protocol,\n\t\ttype: storedSession.type,\n\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello); without\n\t\t// this a rehydrated session silently loses the key and every read no-ops.\n\t\tappKey: storedSession.appKey,\n\t};\n}\n\nfunction getStoredBaseUrl(\n\tbaseUrl: string | undefined,\n\tdomain: string | undefined,\n\tprotocol?: string,\n): string | undefined {\n\tif (baseUrl != null) return baseUrl;\n\n\tconst legacyUrl = baseUrlFromDomain(domain, protocol);\n\treturn legacyUrl != null && new URL(legacyUrl).pathname !== '/' ? legacyUrl : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"configuredIntegrationService.js","names":[],"sources":["../../../../../plus/integrations/src/authentication/configuredIntegrationService.ts"],"sourcesContent":["import { debounce } from '@gitlens/utils/debounce.js';\nimport type { Disposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { flatten } from '@gitlens/utils/iterable.js';\nimport type {\n\tIntegrationIds,\n\tStoredConfiguredIntegrationDescriptor,\n\tStoredIntegrationConfigurations,\n} from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport { providersMetadata } from '../providers/models.js';\nimport {\n\tareDomainsOnSameHost,\n\tbaseUrlFromDomain,\n\thostFromDomain,\n\tsameConfiguredBaseUrl,\n} from '../utils/domain.utils.js';\nimport { isSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider.js';\nimport type {\n\tCloudIntegrationAuthType,\n\tConfiguredIntegrationDescriptor,\n\tConnectionResource,\n\tProviderAuthenticationSession,\n} from './models.js';\n\ninterface StoredSession {\n\tid: string;\n\taccessToken: string;\n\taccount?: { label?: string; displayName?: string; id: string };\n\tscopes: string[];\n\tcloud?: boolean;\n\ttype: CloudIntegrationAuthType | undefined;\n\texpiresAt?: string;\n\tdomain?: string;\n\t/** See {@link ProviderAuthenticationSession.baseUrl}: the configured address, path included. */\n\tbaseUrl?: string;\n\tprotocol?: string;\n\t/** The provider app key paired with the token for providers whose client needs one (e.g. Trello). */\n\tappKey?: string;\n}\n\nexport interface ConfiguredIntegrationsChangeEvent {\n\treadonly added: readonly IntegrationIds[];\n\treadonly removed: readonly IntegrationIds[];\n}\n\nexport class ConfiguredIntegrationService implements Disposable {\n\tprivate readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();\n\tprivate storeConfiguredQueue: Promise<void> = Promise.resolve();\n\t/**\n\t * One provider's secret and descriptor mutations run one at a time, so a sign-out never interleaves with a\n\t * token being stored: storing writes the secret and then registers its descriptor, and a delete in between would\n\t * miss a connection it cannot see yet, or be undone by the pending write (#5948).\n\t */\n\tprivate readonly _locks = new Map<IntegrationIds, Promise<void>>();\n\t/** Monotonic across providers; see {@link getSignOutMark}. */\n\tprivate _signOutEpoch = 0;\n\t/** The epoch of the latest sign-out per scope: a provider, a self-managed host, or a single connection. */\n\tprivate readonly _signOuts = new Map<string, number>();\n\tget onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {\n\t\treturn this._onDidChange.event;\n\t}\n\n\tconstructor(private readonly ctx: IntegrationServiceContext) {}\n\n\tdispose(): void {\n\t\tthis._onDidChange.dispose();\n\t}\n\n\tprivate _configured?: Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>;\n\tprivate get configured(): Map<IntegrationIds, ConfiguredIntegrationDescriptor[]> {\n\t\tif (this._configured == null) {\n\t\t\tthis._configured = new Map<IntegrationIds, ConfiguredIntegrationDescriptor[]>();\n\n\t\t\tconst storedConfigured = this.ctx.storage.get('integrations:configured');\n\t\t\tfor (const [id, configured] of Object.entries(storedConfigured ?? {}) as [\n\t\t\t\tIntegrationIds,\n\t\t\t\tStoredConfiguredIntegrationDescriptor[],\n\t\t\t][]) {\n\t\t\t\tif (configured == null) continue;\n\n\t\t\t\tconst descriptors = configured.map(d => ({\n\t\t\t\t\t...d,\n\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? getStoredBaseUrl(d.baseUrl, d.domain) : d.baseUrl,\n\t\t\t\t\tdomain: this.normalizeConfiguredDomain(id, d.domain),\n\t\t\t\t\t// Backfill a stable connection id for pre-multi-account stored data: the domain for\n\t\t\t\t\t// self-managed hosts, or the provider's canonical domain for cloud (which is the legacy\n\t\t\t\t\t// secret-key session id), so existing secrets keep resolving with zero migration. Fall back\n\t\t\t\t\t// to the integration id (never empty) when no domain is known — self-managed providers carry\n\t\t\t\t\t// `domain: ''` in metadata, and an empty id would collide across connections and produce\n\t\t\t\t\t// ambiguous secret keys (`...|`).\n\t\t\t\t\tid: d.id || d.domain || providersMetadata[id]?.domain || id,\n\t\t\t\t\texpiresAt: d.expiresAt ? new Date(d.expiresAt) : undefined,\n\t\t\t\t}));\n\t\t\t\tthis._configured.set(id, descriptors);\n\t\t\t}\n\t\t}\n\n\t\treturn this._configured;\n\t}\n\n\t// Synchronous: reads the in-memory configured map (lazily hydrated from storage). No async work —\n\t// the old async variant only existed to weave in the built-in VS Code GitHub session, which is gone.\n\tgetConfigured(\n\t\tid?: IntegrationIds,\n\t\toptions?: { cloud?: boolean; domain?: string },\n\t): ConfiguredIntegrationDescriptor[] {\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\n\t\tconst configured =\n\t\t\tid != null\n\t\t\t\t? this.configured.get(id)\n\t\t\t\t: [...flatten<ConfiguredIntegrationDescriptor>(this.configured.values())];\n\t\tif (!configured?.length) return descriptors;\n\n\t\tif (options?.domain != null || options?.cloud != null) {\n\t\t\tfor (const descriptor of configured) {\n\t\t\t\tif (\n\t\t\t\t\t(options?.domain != null &&\n\t\t\t\t\t\t!this.domainsMatch(id ?? descriptor.integrationId, descriptor.domain, options.domain)) ||\n\t\t\t\t\t(options?.cloud === true && !descriptor.cloud) ||\n\t\t\t\t\t(options?.cloud === false && descriptor.cloud)\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdescriptors.push(descriptor);\n\t\t\t}\n\t\t} else {\n\t\t\tdescriptors.push(...configured);\n\t\t}\n\n\t\treturn descriptors;\n\t}\n\n\tprivate async storeConfigured(): Promise<void> {\n\t\t// We need to convert the map to a record to store\n\t\tconst configured: StoredIntegrationConfigurations = {} as unknown as StoredIntegrationConfigurations;\n\t\tfor (const [id, descriptors] of this.configured) {\n\t\t\tconfigured[id] = descriptors.map(d => ({\n\t\t\t\t...d,\n\t\t\t\texpiresAt: d.expiresAt\n\t\t\t\t\t? d.expiresAt instanceof Date\n\t\t\t\t\t\t? d.expiresAt.toISOString()\n\t\t\t\t\t\t: d.expiresAt\n\t\t\t\t\t: undefined,\n\t\t\t}));\n\t\t}\n\n\t\tconst pending = this.storeConfiguredQueue.then(() =>\n\t\t\tthis.ctx.storage.store('integrations:configured', configured),\n\t\t);\n\t\tthis.storeConfiguredQueue = pending.catch(() => {});\n\t\tawait pending;\n\t}\n\n\tprivate async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {\n\t\tdescriptor = {\n\t\t\t...descriptor,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(descriptor.integrationId)\n\t\t\t\t? getStoredBaseUrl(descriptor.baseUrl, descriptor.domain)\n\t\t\t\t: descriptor.baseUrl,\n\t\t\tdomain: this.normalizeConfiguredDomain(descriptor.integrationId, descriptor.domain),\n\t\t};\n\t\tconst descriptors = this.configured.get(descriptor.integrationId) ?? [];\n\t\t// Key connections by their stable id (+ cloud, to preserve legacy local/cloud coexistence) so\n\t\t// multiple accounts on the same provider+domain no longer overwrite each other.\n\t\tconst existing = descriptors.find(d => d.id === descriptor.id && d.cloud === descriptor.cloud);\n\n\t\t// The first connection for a provider/domain is primary by default; an explicit flag (or the\n\t\t// existing value) always wins. Self-managed providers can span multiple hosts under the same\n\t\t// integration id, so each host needs its own primary.\n\t\tconst primaryScope = this.getPrimaryScope(descriptor.integrationId, descriptors, descriptor.domain);\n\t\tconst primary = descriptor.primary ?? existing?.primary ?? !primaryScope.some(d => d.primary);\n\t\t// Preserve a previously-resolved type/accountName when the incoming write doesn't carry them (e.g.\n\t\t// the model re-stores the primary session with an empty account, after reconcile enriched it).\n\t\tconst type = descriptor.type ?? existing?.type;\n\t\tconst accountName = descriptor.accountName ?? existing?.accountName;\n\t\t// A write without a workspace (a failed lookup, or a session re-store) keeps the stored pair whole.\n\t\tconst resource = descriptor.resourceId != null ? descriptor : existing;\n\t\tconst normalized: ConfiguredIntegrationDescriptor = {\n\t\t\t...descriptor,\n\t\t\tprimary: primary,\n\t\t\ttype: type,\n\t\t\taccountName: accountName,\n\t\t\tresourceName: resource?.resourceName,\n\t\t\tresourceId: resource?.resourceId,\n\t\t};\n\n\t\tlet changed: boolean;\n\t\tif (existing != null) {\n\t\t\t// `baseUrl` is compared alongside `domain`, not folded into it: `domain` is normalized to a bare\n\t\t\t// host, so a connection re-pointed to another context path on the SAME host is identical by domain\n\t\t\t// and would otherwise be treated as unchanged — leaving the stored descriptor, and every warm\n\t\t\t// integration listening for the change, on the old address.\n\t\t\tif (\n\t\t\t\texisting.domain === normalized.domain &&\n\t\t\t\tsameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) &&\n\t\t\t\texisting.expiresAt === normalized.expiresAt &&\n\t\t\t\texisting.scopes === normalized.scopes &&\n\t\t\t\t(existing.primary ?? false) === (normalized.primary ?? false) &&\n\t\t\t\texisting.type === normalized.type &&\n\t\t\t\texisting.accountName === normalized.accountName &&\n\t\t\t\texisting.resourceName === normalized.resourceName &&\n\t\t\t\texisting.resourceId === normalized.resourceId\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only fire the change event on address/scopes/primary/type/accountName changes (ignore expiresAt churn)\n\t\t\tchanged =\n\t\t\t\texisting.domain !== normalized.domain ||\n\t\t\t\t!sameConfiguredBaseUrl(existing.baseUrl, normalized.baseUrl) ||\n\t\t\t\texisting.scopes !== normalized.scopes ||\n\t\t\t\t(existing.primary ?? false) !== (normalized.primary ?? false) ||\n\t\t\t\texisting.type !== normalized.type ||\n\t\t\t\texisting.accountName !== normalized.accountName ||\n\t\t\t\texisting.resourceName !== normalized.resourceName ||\n\t\t\t\texisting.resourceId !== normalized.resourceId;\n\n\t\t\t// remove the existing descriptor from the array\n\t\t\tdescriptors.splice(descriptors.indexOf(existing), 1);\n\t\t} else {\n\t\t\tchanged = true;\n\t\t}\n\n\t\tdescriptors.push(normalized);\n\t\tthis.configured.set(descriptor.integrationId, descriptors);\n\n\t\tif (changed) {\n\t\t\tthis.fireChange(descriptor.integrationId);\n\t\t}\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate async removeConfigured(\n\t\tid: IntegrationIds,\n\t\toptions: { connectionId: string; cloud: boolean | undefined },\n\t): Promise<void> {\n\t\tconst existing = this.configured.get(id);\n\t\tif (existing == null || existing.length === 0) return;\n\n\t\tconst removedPrimaryDomains = new Set<string | undefined>();\n\t\tconst descriptors: ConfiguredIntegrationDescriptor[] = [];\n\t\tfor (const d of existing) {\n\t\t\tif (\n\t\t\t\td.id === options.connectionId &&\n\t\t\t\t(options?.cloud == null ||\n\t\t\t\t\t(options?.cloud === true && d.cloud === true) ||\n\t\t\t\t\t(options?.cloud === false && d.cloud === false))\n\t\t\t) {\n\t\t\t\tif (d.primary) {\n\t\t\t\t\tremovedPrimaryDomains.add(d.domain);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tdescriptors.push(d);\n\t\t}\n\n\t\tif (descriptors.length === existing.length) return; // nothing matched\n\n\t\t// Promote a secondary to primary when the removed connection was the primary and others remain\n\t\t// in the same primary scope, so the provider/host stays in a well-defined connected state.\n\t\tfor (const domain of removedPrimaryDomains) {\n\t\t\tif (descriptors.some(d => this.isInPrimaryScope(id, d, domain) && d.primary)) continue;\n\n\t\t\tconst index = descriptors.findIndex(d => this.isInPrimaryScope(id, d, domain));\n\t\t\tif (index !== -1) {\n\t\t\t\tdescriptors[index] = { ...descriptors[index], primary: true };\n\t\t\t}\n\t\t}\n\n\t\tthis.configured.set(id, descriptors);\n\t\tthis.fireChange(undefined, id);\n\t\tawait this.storeConfigured();\n\t}\n\n\tprivate withLock<T>(id: IntegrationIds, fn: () => Promise<T>): Promise<T> {\n\t\tconst run = (this._locks.get(id) ?? Promise.resolve()).then(fn);\n\t\tconst settled = run.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\tthis._locks.set(id, settled);\n\t\tvoid settled.then(() => {\n\t\t\tif (this._locks.get(id) === settled) {\n\t\t\t\tthis._locks.delete(id);\n\t\t\t}\n\t\t});\n\t\treturn run;\n\t}\n\n\t/**\n\t * Taken before a token is read or fetched, and handed to {@link storeSession}: a sign-out that lands after it\n\t * (a disconnect, a reauthentication, the connection being removed) refuses the store, so a token fetched across\n\t * it is not written back and reconnected on the next resolution (gitkraken/kepler#3546).\n\t */\n\tgetSignOutMark(): number {\n\t\treturn this._signOutEpoch;\n\t}\n\n\tprivate markSignOut(scope: string): void {\n\t\tthis._signOuts.set(scope, ++this._signOutEpoch);\n\t}\n\n\tprivate getHostScope(id: IntegrationIds, domain: string): string {\n\t\treturn `${id}|host:${hostFromDomain(domain) ?? domain}`;\n\t}\n\n\tprivate isSignedOutSince(id: IntegrationIds, session: ProviderAuthenticationSession, mark: number): boolean {\n\t\tconst since = (scope: string) => (this._signOuts.get(scope) ?? 0) > mark;\n\t\tif (since(id)) return true;\n\t\tif (isSelfManagedHostIntegrationId(id) && session.domain && since(this.getHostScope(id, session.domain))) {\n\t\t\treturn true;\n\t\t}\n\t\treturn since(`${id}|connection:${session.id}`);\n\t}\n\n\t/**\n\t * Stores the session's secret and descriptor together. With a `signOutMark`, nothing is stored, and `false` is\n\t * returned, when a sign-out covering the session landed after the mark was taken.\n\t */\n\tstoreSession(\n\t\tid: IntegrationIds,\n\t\tsession: ProviderAuthenticationSession,\n\t\toptions?: { signOutMark?: number; resource?: ConnectionResource },\n\t): Promise<boolean> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tif (options?.signOutMark != null && this.isSignedOutSince(id, session, options.signOutMark)) return false;\n\n\t\t\tawait this.writeSecret(id, session, options?.resource);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tgetStoredSession(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t): Promise<ProviderAuthenticationSession | undefined> {\n\t\t// Under the lock too: reading can repair storage (a legacy local-key cloud session, a missing descriptor).\n\t\treturn this.withLock(id, async () => {\n\t\t\tconst sessionId = this.resolveConnectionId(id, descriptor);\n\t\t\tlet session = descriptor.cloud === true ? undefined : await this.readSecret(id, sessionId, false);\n\n\t\t\tlet cloudIfMissing = false;\n\t\t\tif (session != null) {\n\t\t\t\t// Check the `expiresAt` field\n\t\t\t\t// If it has an expiresAt property and the key is the old type, then it's a cloud session,\n\t\t\t\t// so delete it from the local key and\n\t\t\t\t// store with the \"cloud\" type key, and then use that one.\n\t\t\t\t// Otherwise it's a local session under the local key, so just return it.\n\t\t\t\tif (session.expiresAt != null) {\n\t\t\t\t\tcloudIfMissing = true;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait this.deleteSecrets(id, session.id);\n\t\t\t\t\t\tawait this.writeSecret(id, session);\n\t\t\t\t\t} catch {}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If no local session we try to restore a session with the cloud key\n\t\t\tif (session == null && descriptor.cloud !== false) {\n\t\t\t\tcloudIfMissing = true;\n\t\t\t\tsession = await this.readSecret(id, sessionId, true);\n\t\t\t}\n\n\t\t\treturn convertStoredSessionToSession(session, descriptor, cloudIfMissing);\n\t\t});\n\t}\n\n\t/**\n\t * Deletes one connection's stored session. `signOut` is for a delete that signs the integration out (a forced new\n\t * session): it also refuses the store of any token of the provider, or of the descriptor's self-managed host,\n\t * fetched before it (see {@link getSignOutMark}). A cleanup of a replaced token leaves it unset.\n\t */\n\tdeleteStoredSessions(\n\t\tid: IntegrationIds,\n\t\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean; signOut?: boolean },\n\t): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tif (options?.signOut) {\n\t\t\t\tthis.markSignOut(\n\t\t\t\t\tisSelfManagedHostIntegrationId(id) && descriptor.domain\n\t\t\t\t\t\t? this.getHostScope(id, descriptor.domain)\n\t\t\t\t\t\t: id,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.deleteSecrets(id, this.resolveConnectionId(id, descriptor), cloud, options);\n\t\t});\n\t}\n\n\t/** Signs out of every stored session of the provider, or of one self-managed host when `domain` is given. */\n\tdeleteAllStoredSessions(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\tthis.markSignOut(isSelfManagedHostIntegrationId(id) && domain != null ? this.getHostScope(id, domain) : id);\n\t\t\tawait this.deleteAllSecrets(id, cloud, domain);\n\t\t});\n\t}\n\n\t/**\n\t * Reclaims all stored configuration + secrets for the given (typically retired) integration ids.\n\t * Used to clean up state left behind when an id is removed from the model — e.g. the local\n\t * self-managed `github-enterprise`/`gitlab-self-hosted` ids dropped when integrations went cloud-only.\n\t * Domains are read from the stored config (secrets are keyed by domain); ids with no stored config\n\t * are a no-op.\n\t */\n\tasync purgeStoredConfiguration(ids: readonly string[]): Promise<void> {\n\t\tconst stored =\n\t\t\tthis.ctx.storage.get<Record<string, StoredConfiguredIntegrationDescriptor[] | undefined>>(\n\t\t\t\t'integrations:configured',\n\t\t\t);\n\t\tif (stored == null) return;\n\n\t\tlet changed = false;\n\t\tfor (const id of ids) {\n\t\t\tif (!(id in stored)) continue;\n\n\t\t\tfor (const descriptor of stored[id] ?? []) {\n\t\t\t\t// Mirror the hydration backfill so the secret keys we delete match what was written: fall back\n\t\t\t\t// through domain, the provider's canonical domain, then the integration id (never empty). A\n\t\t\t\t// legacy cloud descriptor stored neither id nor domain, so an empty connection id here would\n\t\t\t\t// miss the canonical-domain secret (e.g. `integration.auth.cloud:github|github.com`) and orphan it.\n\t\t\t\tconst connectionId =\n\t\t\t\t\tdescriptor.id || descriptor.domain || providersMetadata[id as IntegrationIds]?.domain || id;\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id as IntegrationIds, connectionId));\n\t\t\t\tif (descriptor.domain) {\n\t\t\t\t\tawait this.ctx.storage.deleteWorkspace(`connected:${id}:${descriptor.domain}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchanged = true;\n\t\t}\n\t\tif (!changed) return;\n\n\t\tconst remaining = Object.fromEntries(Object.entries(stored).filter(([key]) => !ids.includes(key)));\n\t\tawait this.ctx.storage.store('integrations:configured', remaining as StoredIntegrationConfigurations);\n\t}\n\n\tprivate async deleteSecrets(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string,\n\t\tcloud?: boolean,\n\t\toptions?: { preserveConfigured?: boolean },\n\t): Promise<void> {\n\t\tif (cloud == null || cloud === false) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getLocalSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (cloud == null || cloud === true) {\n\t\t\tawait this.ctx.storage.deleteSecret(this.getCloudSecretKey(id, connectionId));\n\t\t}\n\n\t\tif (options?.preserveConfigured) return;\n\n\t\tawait this.removeConfigured(id, { connectionId: connectionId, cloud: cloud });\n\t}\n\n\tprivate async deleteAllSecrets(id: IntegrationIds, cloud?: boolean, domain?: string): Promise<void> {\n\t\t// Delete every connection's secret (multi-account): secrets are keyed per connection id, so a\n\t\t// single canonical-domain delete would orphan secondary tokens. When a domain is given (self-managed\n\t\t// disconnect of one host), scope to that host so other hosts under the same provider id survive.\n\t\tconst descriptors = this.configured.get(id);\n\t\tconst connectionIds = [\n\t\t\t...new Set(\n\t\t\t\t(domain != null ? descriptors?.filter(c => this.domainsMatch(id, c.domain, domain)) : descriptors)?.map(\n\t\t\t\t\tc => c.id,\n\t\t\t\t) ?? [],\n\t\t\t),\n\t\t];\n\t\tif (connectionIds.length) {\n\t\t\tfor (const connectionId of connectionIds) {\n\t\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// A domain-scoped clear that matched nothing means this host has no stored connections, so it must be\n\t\t// a no-op. The canonical-domain fallback below would target a bogus key (self-managed canonical domain\n\t\t// is empty), so skip it.\n\t\tif (domain != null) return;\n\n\t\t// No configured connections (e.g. an orphaned secret with no descriptor): fall back to the\n\t\t// provider's canonical domain, which is the legacy session id for cloud providers. Mirror the\n\t\t// hydration backfill's `|| id` guard so a self-managed provider (empty canonical domain) doesn't\n\t\t// target an ambiguous empty connection id.\n\t\tawait this.deleteSecrets(id, providersMetadata[id]?.domain || id, cloud);\n\t}\n\n\tprivate async writeSecret(\n\t\tid: IntegrationIds,\n\t\tsession: ProviderAuthenticationSession | StoredSession,\n\t\tresource?: ConnectionResource,\n\t): Promise<void> {\n\t\tawait this.ctx.storage.storeSecret(\n\t\t\tthis.getSecretKey(id, session.id, session.cloud ?? false),\n\t\t\tJSON.stringify(session),\n\t\t);\n\n\t\tawait this.addOrUpdateConfigured({\n\t\t\tid: session.id,\n\t\t\tintegrationId: id,\n\t\t\tdomain: isSelfManagedHostIntegrationId(id) ? session.domain : undefined,\n\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? session.baseUrl : undefined,\n\t\t\texpiresAt: session.expiresAt,\n\t\t\tscopes: session.scopes.join(','),\n\t\t\tcloud: session.cloud ?? false,\n\t\t\ttype: session.type,\n\t\t\taccountName: session.account?.label || undefined,\n\t\t\tresourceName: resource?.name,\n\t\t\tresourceId: resource?.id,\n\t\t});\n\t}\n\n\tprivate async readSecret(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): Promise<StoredSession | undefined> {\n\t\tlet storedSession: StoredSession | undefined;\n\t\ttry {\n\t\t\tconst sessionJSON = await this.ctx.storage.getSecret(this.getSecretKey(id, sessionId, cloud));\n\t\t\tif (sessionJSON) {\n\t\t\t\tstoredSession = JSON.parse(sessionJSON);\n\t\t\t\tif (storedSession != null) {\n\t\t\t\t\tconst configured = this.configured.get(id);\n\t\t\t\t\tconst connectionId = storedSession.id ?? sessionId;\n\t\t\t\t\tconst sessionCloud = storedSession.cloud ?? cloud;\n\t\t\t\t\tconst domain = isSelfManagedHostIntegrationId(id)\n\t\t\t\t\t\t? (storedSession.domain ?? storedSession.id)\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (\n\t\t\t\t\t\tconfigured == null ||\n\t\t\t\t\t\tconfigured.length === 0 ||\n\t\t\t\t\t\t!configured.some(\n\t\t\t\t\t\t\tc => c.id === connectionId && c.integrationId === id && c.cloud === sessionCloud,\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tawait this.addOrUpdateConfigured({\n\t\t\t\t\t\t\tid: connectionId,\n\t\t\t\t\t\t\tintegrationId: id,\n\t\t\t\t\t\t\tdomain: domain,\n\t\t\t\t\t\t\t// Rebuilt from the secret, so it must carry the address the session does; omitting it\n\t\t\t\t\t\t\t// leaves the descriptor claiming no context path and makes reconcile read every\n\t\t\t\t\t\t\t// routine check-in as a re-pointed connection.\n\t\t\t\t\t\t\tbaseUrl: isSelfManagedHostIntegrationId(id) ? storedSession.baseUrl : undefined,\n\t\t\t\t\t\t\texpiresAt: storedSession.expiresAt,\n\t\t\t\t\t\t\tscopes: storedSession.scopes.join(','),\n\t\t\t\t\t\t\tcloud: sessionCloud,\n\t\t\t\t\t\t\ttype: storedSession.type,\n\t\t\t\t\t\t\taccountName: storedSession.account?.label || undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (_ex) {\n\t\t\ttry {\n\t\t\t\tawait this.deleteSecrets(id, sessionId, cloud);\n\t\t\t} catch {}\n\t\t}\n\t\treturn storedSession;\n\t}\n\n\tprivate getSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t\tcloud: boolean = false,\n\t): `integration.auth:${IntegrationIds}|${string}` | `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn cloud ? this.getCloudSecretKey(id, sessionId) : this.getLocalSecretKey(id, sessionId);\n\t}\n\n\tprivate getLocalSecretKey(id: IntegrationIds, sessionId: string): `integration.auth:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth:${id}|${sessionId}`;\n\t}\n\n\tprivate getCloudSecretKey(\n\t\tid: IntegrationIds,\n\t\tsessionId: string,\n\t): `integration.auth.cloud:${IntegrationIds}|${string}` {\n\t\treturn `integration.auth.cloud:${id}|${sessionId}`;\n\t}\n\n\tprivate getPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptors: ConfiguredIntegrationDescriptor[],\n\t\tdomain: string | undefined,\n\t): ConfiguredIntegrationDescriptor[] {\n\t\treturn isSelfManagedHostIntegrationId(id)\n\t\t\t? descriptors.filter(d => this.isInPrimaryScope(id, d, domain))\n\t\t\t: descriptors;\n\t}\n\n\tprivate isInPrimaryScope(\n\t\tid: IntegrationIds,\n\t\tdescriptor: ConfiguredIntegrationDescriptor,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, descriptor.domain, domain);\n\t}\n\n\t/**\n\t * Resolves the connection id used as the secret-key session id for an auth descriptor.\n\t * - If the descriptor targets a specific connection (`connectionId`), that wins.\n\t * - Otherwise returns the primary connection's id for the provider (matching the descriptor's domain\n\t * for self-managed hosts; any connection for cloud hosts, where the stored domain is undefined).\n\t * - Falls back to the descriptor domain when nothing is configured yet, preserving legacy\n\t * (pre-multi-account) reads with zero secret migration.\n\t */\n\tresolveConnectionId(id: IntegrationIds, descriptor: IntegrationAuthenticationSessionDescriptor): string {\n\t\t// An empty connectionId is not a real target — treat it as \"no target\" so it resolves the primary\n\t\t// rather than keying a secret under an empty id.\n\t\tif (descriptor.connectionId) return descriptor.connectionId;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? descriptor.domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, domain, descriptor.cloud);\n\t\treturn (candidates?.find(c => c.primary) ?? candidates?.[0])?.id ?? descriptor.domain;\n\t}\n\n\t/**\n\t * Returns the id of the configured connection an unscoped descriptor resolves to (the primary for the\n\t * descriptor's domain, matching {@link resolveConnectionId}'s selection), or `undefined` when nothing is\n\t * configured for it. Unlike {@link resolveConnectionId} this only returns a genuine per-connection cloud\n\t * token id (never a local/PAT or domain-derived legacy id), so callers can safely use it to scope a cloud\n\t * token fetch to one host of a multi-host self-managed provider, or fall through to the provider-global\n\t * primary endpoint on `undefined`.\n\t */\n\t/**\n\t * The address already stored for a self-managed connection — see {@link ConfiguredIntegrationDescriptor.baseUrl}.\n\t *\n\t * Exists so a session rebuilt from a backend response that omits the domain can keep the address the\n\t * connection was configured with, rather than silently falling back to the bare host and losing a context\n\t * path the user never changed. Looked up by connection id first, then by host for an unscoped fetch.\n\t */\n\t/**\n\t * Whether `connectionId` names a cloud connection configured for `domain`'s host.\n\t *\n\t * The caller-facing question is \"is this id evidence that a token belongs to this host\". A connection id\n\t * can arrive from a caller rather than from our own configuration, so it is only evidence once it matches\n\t * a descriptor we stored for that host — see the host guard in `CloudIntegrationAuthenticationProvider`.\n\t */\n\tisConnectionConfiguredForHost(\n\t\tid: IntegrationIds,\n\t\tconnectionId: string | undefined,\n\t\tdomain: string | undefined,\n\t): boolean {\n\t\tif (connectionId == null) return false;\n\n\t\tconst connection = this.getConfigured(id, { cloud: true }).find(c => c.id === connectionId);\n\t\tif (connection == null) return false;\n\n\t\treturn !isSelfManagedHostIntegrationId(id) || this.domainsMatch(id, connection.domain, domain);\n\t}\n\n\tgetConfiguredBaseUrl(id: IntegrationIds, connectionId: string | undefined, domain?: string): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return undefined;\n\n\t\tconst configured = this.getConfigured(id, { cloud: true });\n\t\tconst byId = connectionId != null ? configured.find(c => c.id === connectionId) : undefined;\n\t\tif (byId != null) return byId.baseUrl;\n\n\t\treturn configured.find(c => this.domainsMatch(id, c.domain, domain))?.baseUrl;\n\t}\n\n\tgetConfiguredConnectionId(id: IntegrationIds, domain: string | undefined, cloud?: boolean): string | undefined {\n\t\tconst scoped = isSelfManagedHostIntegrationId(id) ? domain : undefined;\n\t\tconst candidates = this.scopeConnectionCandidates(id, scoped, cloud);\n\t\tconst connection = candidates?.find(c => c.primary) ?? candidates?.[0];\n\t\tif (connection == null) return undefined;\n\n\t\t// Only return a real per-connection cloud token id. Reject a local (PAT) descriptor (surfaced by\n\t\t// scopeConnectionCandidates' non-cloud fallback) and a domain-derived legacy id (the descriptor's own\n\t\t// domain or the provider's canonical domain — see ConfiguredIntegrationDescriptor.id), either of which\n\t\t// would target `v1/provider-tokens/tokens/{domain}` (rejected as a non-uuid). Callers then fall through\n\t\t// to the provider-scoped primary endpoint.\n\t\tif (!connection.cloud) return undefined;\n\n\t\tconst canonicalDomain = providersMetadata[id]?.domain;\n\t\tif (connection.id === connection.domain || (canonicalDomain && connection.id === canonicalDomain)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn connection.id;\n\t}\n\n\t/**\n\t * Selects configured descriptors for a provider matching `domain`, further narrowed to the `cloud`\n\t * variant when specified. A connection id can have both a local (PAT) and cloud descriptor whose ids\n\t * differ (e.g. a legacy local descriptor keyed by domain alongside a real cloud token id); scoping by\n\t * `cloud` keeps a cloud-scoped read/delete from resolving the local id (and vice versa). Falls back to\n\t * the domain-only set when no variant matches, so legacy single-variant resolution is unchanged.\n\t */\n\tprivate scopeConnectionCandidates(\n\t\tid: IntegrationIds,\n\t\tdomain: string | undefined,\n\t\tcloud: boolean | undefined,\n\t): ConfiguredIntegrationDescriptor[] | undefined {\n\t\tconst candidates = this.configured.get(id)?.filter(c => this.domainsMatch(id, c.domain, domain));\n\t\tif (cloud == null || candidates == null) return candidates;\n\n\t\tconst scoped = candidates.filter(c => c.cloud === cloud);\n\t\treturn scoped.length ? scoped : candidates;\n\t}\n\n\t/**\n\t * Marks the given connection as the primary/default for the provider and clears the flag on its\n\t * siblings. Persists immediately instead of relying on a session re-store to carry the primary flag.\n\t */\n\tsetPrimaryConnection(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\treturn this.withLock(id, () => this.setPrimaryConnectionCore(id, connectionId));\n\t}\n\n\tprivate async setPrimaryConnectionCore(id: IntegrationIds, connectionId: string): Promise<void> {\n\t\tconst descriptors = this.configured.get(id);\n\t\tif (descriptors == null || descriptors.length === 0) return;\n\t\tif (!descriptors.some(d => d.id === connectionId)) return;\n\n\t\tconst target =\n\t\t\tdescriptors.find(d => d.id === connectionId && d.cloud) ?? descriptors.find(d => d.id === connectionId);\n\t\tif (target == null) return;\n\n\t\tconst domain = isSelfManagedHostIntegrationId(id) ? target.domain : undefined;\n\t\t// A connection id can have both a local (PAT) and cloud descriptor. Mark the primary on a single\n\t\t// canonical variant (prefer cloud, since multi-account primaries are cloud-driven) within the\n\t\t// provider/host scope, so other self-managed hosts keep their own default connection.\n\t\tconst primaryIsCloud = descriptors.some(\n\t\t\td => d.id === connectionId && d.cloud && this.isInPrimaryScope(id, d, domain),\n\t\t);\n\n\t\tlet changed = false;\n\t\tconst updated = descriptors.map(d => {\n\t\t\tif (!this.isInPrimaryScope(id, d, domain)) return d;\n\n\t\t\tconst primary = d.id === connectionId && (d.cloud ?? false) === primaryIsCloud;\n\t\t\tif ((d.primary ?? false) === primary) return d;\n\n\t\t\tchanged = true;\n\t\t\treturn { ...d, primary: primary };\n\t\t});\n\t\tif (!changed) return;\n\n\t\tthis.configured.set(id, updated);\n\t\tthis.fireChange(id);\n\t\tawait this.storeConfigured();\n\t}\n\n\t/**\n\t * Removes a single connection (its secret + configured descriptor). If it was the primary and other\n\t * connections remain, a secondary is promoted (see {@link removeConfigured}). Unlike\n\t * {@link deleteAllStoredSessions}, this only affects the targeted connection. Pass `cloud` to scope\n\t * the removal to the cloud/local variant when a local PAT and a cloud session share a connection id.\n\t */\n\tdeleteConnection(id: IntegrationIds, connectionId: string, cloud?: boolean): Promise<void> {\n\t\treturn this.withLock(id, async () => {\n\t\t\t// The connection is gone, so a token of it fetched before must not be stored back.\n\t\t\tthis.markSignOut(`${id}|connection:${connectionId}`);\n\t\t\tawait this.deleteSecrets(id, connectionId, cloud);\n\t\t});\n\t}\n\n\tprivate _addedIds = new Set<IntegrationIds>();\n\tprivate _removedIds = new Set<IntegrationIds>();\n\tprivate _fireChangeDebounced?: () => void;\n\n\tprivate normalizeConfiguredDomain(id: IntegrationIds, domain: string | undefined): string | undefined {\n\t\tif (!isSelfManagedHostIntegrationId(id)) return domain;\n\n\t\treturn hostFromDomain(domain) ?? domain;\n\t}\n\n\tprivate domainsMatch(id: IntegrationIds, first: string | undefined, second: string | undefined): boolean {\n\t\tif (first === second) return true;\n\t\tif (!isSelfManagedHostIntegrationId(id)) return false;\n\t\tif (first == null || second == null) return false;\n\n\t\treturn areDomainsOnSameHost(first, second);\n\t}\n\n\tprivate fireChange(added?: IntegrationIds, removed?: IntegrationIds) {\n\t\tthis._fireChangeDebounced ??= debounce(() => {\n\t\t\tconst added = [...this._addedIds];\n\t\t\tthis._addedIds.clear();\n\t\t\tconst removed = [...this._removedIds];\n\t\t\tthis._removedIds.clear();\n\n\t\t\tthis._onDidChange.fire({ added: added, removed: removed });\n\t\t}, 250);\n\n\t\tif (added != null) {\n\t\t\tthis._addedIds.add(added);\n\t\t}\n\t\tif (removed != null) {\n\t\t\tthis._removedIds.add(removed);\n\t\t}\n\t\tthis._fireChangeDebounced();\n\t}\n}\n\nfunction convertStoredSessionToSession(\n\tstoredSession: StoredSession | undefined,\n\tdescriptor: IntegrationAuthenticationSessionDescriptor,\n\tcloudIfMissing: boolean,\n): ProviderAuthenticationSession | undefined {\n\tif (storedSession == null) return undefined;\n\n\treturn {\n\t\tid: storedSession.id,\n\t\taccessToken: storedSession.accessToken,\n\t\taccount: {\n\t\t\tid: storedSession.account?.id ?? '',\n\t\t\tlabel: storedSession.account?.label ?? '',\n\t\t},\n\t\tscopes: storedSession.scopes,\n\t\tcloud: storedSession.cloud ?? cloudIfMissing,\n\t\texpiresAt: storedSession.expiresAt ? new Date(storedSession.expiresAt) : undefined,\n\t\tdomain: storedSession.domain ?? descriptor.domain,\n\t\tbaseUrl: getStoredBaseUrl(storedSession.baseUrl, storedSession.domain, storedSession.protocol),\n\t\tprotocol: storedSession.protocol,\n\t\ttype: storedSession.type,\n\t\t// Carried for providers whose client needs an app key alongside the token (e.g. Trello); without\n\t\t// this a rehydrated session silently loses the key and every read no-ops.\n\t\tappKey: storedSession.appKey,\n\t};\n}\n\nfunction getStoredBaseUrl(\n\tbaseUrl: string | undefined,\n\tdomain: string | undefined,\n\tprotocol?: string,\n): string | undefined {\n\tif (baseUrl != null) return baseUrl;\n\n\tconst legacyUrl = baseUrlFromDomain(domain, protocol);\n\treturn legacyUrl != null && new URL(legacyUrl).pathname !== '/' ? legacyUrl : undefined;\n}\n"],"mappings":"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"}
@@ -83,6 +83,12 @@ export interface ConfiguredIntegrationDescriptor {
83
83
  * provider API (the token backend doesn't expose it); absent until resolved or if the lookup fails.
84
84
  */
85
85
  readonly accountName?: string;
86
+ /**
87
+ * Workspace the connection's token was granted for (Linear), separate from the person. Resolved with the
88
+ * connection's own token and kept by connection id; both are set together, or neither is.
89
+ */
90
+ readonly resourceName?: string;
91
+ readonly resourceId?: string;
86
92
  readonly cloud: boolean;
87
93
  readonly integrationId: IntegrationIds;
88
94
  readonly scopes: string;
@@ -112,6 +118,11 @@ export interface CloudIntegrationAuthenticationSession {
112
118
  export interface CloudIntegrationAuthorization {
113
119
  url: string;
114
120
  }
121
+ /** Workspace a connection's token was granted for; only ever a complete pair. */
122
+ export interface ConnectionResource {
123
+ readonly id: string;
124
+ readonly name: string;
125
+ }
115
126
  export interface CloudIntegrationConnection {
116
127
  type: CloudIntegrationAuthType;
117
128
  provider: CloudIntegrationType;