Skip to content

refactor: replace ecosystem if-chains with IEcosystemAdapter registry#641

Merged
rajbos merged 8 commits intomainfrom
rajbos/refactor-ecosystem-interface
Apr 22, 2026
Merged

refactor: replace ecosystem if-chains with IEcosystemAdapter registry#641
rajbos merged 8 commits intomainfrom
rajbos/refactor-ecosystem-interface

Conversation

@rajbos
Copy link
Copy Markdown
Owner

@rajbos rajbos commented Apr 21, 2026

Summary

Replaces the repeated ecosystem if-statement chains in extension.ts, usageAnalysis.ts, and cli/src/helpers.ts with a unified IEcosystemAdapter registry pattern.

Problem

Every method that touched session files had a chain like:

if (this.openCode.isOpenCodeSessionFile(f)) { return this.openCode.countOpenCodeInteractions(f); }
if (this.visualStudio.isVSSessionFile(f)) { ... }
if (this.crush.isCrushSessionFile(f)) { return this.crush.countCrushInteractions(f); }
// ... 4 more ecosystems

This existed in 8+ methods across 3 files. Adding a new ecosystem required updating all of them.

Solution

Adapter pattern — a common IEcosystemAdapter interface with 7 thin implementations:

interface IEcosystemAdapter {
  readonly id: string;
  readonly displayName: string;
  handles(sessionFile: string): boolean;
  stat(sessionFile: string): Promise<fs.Stats>;
  getTokens(sessionFile: string): Promise<{ tokens, thinkingTokens, actualTokens }>;
  countInteractions(sessionFile: string): Promise<number>;
  getModelUsage(sessionFile: string): Promise<ModelUsage>;
  getMeta(sessionFile: string): Promise<{ title, firstInteraction, lastInteraction, workspacePath? }>;
  getEditorRoot(sessionFile: string): string;
}

All 8 if-chain methods are replaced with:

const eco = this.findEcosystem(sessionFile);
if (eco) return eco.countInteractions(sessionFile);
// Copilot Chat fallthrough (unchanged)

Files Changed

New:

  • vscode-extension/src/ecosystemAdapter.tsIEcosystemAdapter interface
  • vscode-extension/src/adapters/ — 7 adapter implementations (OpenCode, Crush, Continue, ClaudeDesktop, ClaudeCode, VisualStudio, MistralVibe)

Modified:

  • vscode-extension/src/extension.ts — 8 if-chain methods replaced; findEcosystem() added
  • vscode-extension/src/usageAnalysis.ts — adapter dispatch in getModelUsageFromSession() with legacy fallback
  • cli/src/helpers.ts — adapter registry wired into statSessionFile(), processSessionFile(), and calculateUsageAnalysisStats()

Bugs Fixed Along the Way

  • Duplicate VS check in statSessionFile() and estimateTokensFromSession() (unreachable dead code)
  • Missing MistralVibe in isSpecialSession check and getSessionFileDetails()
  • Missing ClaudeCode in getSessionFileDetails()

What's NOT Changed

  • Copilot Chat fallthrough logic (still in-line — a future CopilotChatAdapter can wrap it)
  • analyzeSessionUsage() in usageAnalysis.ts (too complex, deferred to follow-up)
  • Existing DA classes are not modified — adapters are thin wrappers

Adding a New Ecosystem (Going Forward)

  1. Create src/adapters/newEditorAdapter.ts implementing IEcosystemAdapter
  2. Add it to the registry in the extension.ts constructor and cli/src/helpers.ts
  3. Done ✅ — no if-chain hunting required

rajbos and others added 8 commits April 21, 2026 20:30
- Add IEcosystemAdapter interface (ecosystemAdapter.ts)
- Add 7 adapter implementations in src/adapters/ (OpenCode, Crush, Continue, ClaudeDesktop, ClaudeCode, VisualStudio, MistralVibe)
- Replace 8 if-chain methods in extension.ts with findEcosystem() dispatch
- Replace getModelUsageFromSession() if-chain in usageAnalysis.ts with adapter dispatch + legacy fallback
- Replace statSessionFile() + processSessionFile() if-chains in cli/src/helpers.ts
- Wire ecosystems registry into calculateUsageAnalysisStats() in CLI
- Remove now-unused isCrushSessionFile() + isOpenCodeDbSession() helpers from CLI
- Fix duplicate VS check bugs in statSessionFile() and estimateTokensFromSession()
- Fix missing MistralVibe in isSpecialSession check and getSessionFileDetails()
- Fix missing ClaudeCode in getSessionFileDetails()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… pattern

- Replace 6-ecosystem buildTurns if-chain in getSessionLogData() with
  adapter delegation (~450 lines -> ~20 lines)
- Fix ClaudeDesktopAdapter constructor call with required callbacks
- Replace openRawFile VS-specific handler with adapter getRawFileContent()
- Fix detectEditorSource to use findEcosystem() instead of isOpenCodeSessionFile()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ters

Add IDiscoverableEcosystem interface with discover() and getCandidatePaths()
methods. All 7 adapters implement both methods, moving ~200 lines of
ecosystem-specific discovery blocks from SessionDiscovery into the adapters.

- ecosystemAdapter.ts: add CandidatePath, DiscoveryResult, IDiscoverableEcosystem
- All 7 adapters: implement discover() + getCandidatePaths()
- sessionDiscovery.ts: replace 7 discovery blocks + 7 diagnostic blocks with
  adapter loops (~200 lines removed)
- extension.ts + cli/helpers.ts: wire ecosystems into SessionDiscovery deps

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… DA calls

Phase 4: Drop 7 individual DataAccess fields from SessionDiscoveryDeps.
SessionDiscovery now only needs ecosystems + log/warn/error + optional override.
Updated constructor calls in extension.ts and cli/src/helpers.ts.

Phase 5 (partial): Fix last 3 direct this.openCode.* calls in extension.ts.
- getEditorTypeFromPath(): use findEcosystem(p)?.id === 'opencode' instead of
  this.openCode.isOpenCodeSessionFile()
- Folder counting loop: use adapter.getEditorRoot() for all adapter-owned sessions
  (was only handling OpenCode; now handles Crush and other virtual-path sessions too)

Phase 3 tests: Add 24 unit tests covering adapter discovery:
- isDiscoverable() type guard (all 7 adapters + negative case)
- handles() path recognition for OpenCode/Continue/ClaudeCode/MistralVibe
- getCandidatePaths() structure validation for all adapters
- getEditorRoot() returns non-empty string for all adapters
- discover() returns valid DiscoveryResult shape (graceful empty dirs)
- getCandidatePaths/discover consistency invariant

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove 7 individual DataAccess fields from UsageAnalysisDeps. All usage
analysis routing now goes through the IAnalyzableEcosystem interface
implemented by all 7 ecosystem adapters.

Changes:
- ecosystemAdapter.ts: add UsageAnalysisAdapterContext, IAnalyzableEcosystem
  interface and isAnalyzable() type guard
- usageAnalysis.ts: slim UsageAnalysisDeps from 13 to 5 fields, replace
  7 DA if-blocks in analyzeSessionUsage() with adapter dispatch, remove
  legacy DA fallback from getModelUsageFromSession(), export
  readClaudeCodeEventsForAnalysis, applyModelTierClassification, and
  createEmptySessionUsageAnalysis() factory
- All 7 adapters: implement IAnalyzableEcosystem with analyzeUsage()
- openCodeAdapter.ts + crushAdapter.ts: add getSyncData() for backend sync
- extension.ts: slim usageAnalysisDeps getter to 5 fields
- cli/src/helpers.ts: slim calculateUsageAnalysisStats deps to 5 fields
- usageAnalysis.test.ts: update makeMockDeps to adapter-based mock

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ClaudeDesktopCoworkDataAccess.getCoworkBaseDir() intentionally returns ''
on non-Windows platforms (the feature is Windows-only). The test
'getEditorRoot: all adapters return non-empty string' was failing on Linux
CI because it called getEditorRoot() on every adapter without regard for
platform availability. Skip the claudedesktop adapter check when not on
win32.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rajbos rajbos merged commit 1f136ff into main Apr 22, 2026
18 checks passed
@rajbos rajbos deleted the rajbos/refactor-ecosystem-interface branch April 22, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant