Skip to content

refactor(opencode): deduplicate shared code, add Zod schemas, fix recursive dialog, add logging#166

Open
aaronjo-Telnyx wants to merge 11 commits into
mainfrom
refactor/opencode-plugin-quality
Open

refactor(opencode): deduplicate shared code, add Zod schemas, fix recursive dialog, add logging#166
aaronjo-Telnyx wants to merge 11 commits into
mainfrom
refactor/opencode-plugin-quality

Conversation

@aaronjo-Telnyx
Copy link
Copy Markdown
Contributor

Summary

Refactor of the @telnyx/opencode plugin for code quality improvements identified from comparison with the opencode-tree plugin.

Changes

1. Deduplicate Server/TUI Shared Code (HIGHEST PRIORITY)

  • Extracted all shared logic to src/shared.ts:
    • isObject(), isTelnyxHostedModel() type helpers
    • storedApiKey(), apiKey() auth reading
    • normalizeModel(), modelConfigEntry() model normalization
    • fetchModels(), fetchAllHostedModelIDs(), fetchHostedModels() API calls
    • providerModels() for building the provider config map
  • Both index.ts and tui.tsx now import from the shared module
  • Zero duplication between the two entry points

2. Add Zod Schemas for Config

  • ModelsConfigFileSchema validates telnyx-models.json format
  • TelnyxCredentialSchema validates auth.json credential format
  • safeParse used in loadEnabledModels() and storedApiKey()
  • Parse errors are logged before falling back to defaults
  • Added zod to dependencies

3. Add Logging in Catch Blocks

  • Replaced all empty catch {} blocks with console.error("[telnyx]", ...) logging
  • Includes operation context (what failed, what was the error)
  • Applied across shared.ts and models-config.ts

4. Fix Recursive openManager Dialog Pattern

  • Replaced recursive await openManager(api) with a local renderDialog() function that re-renders in-place
  • Each model toggle now calls renderDialog() instead of re-entering openManager()
  • Avoids unbounded stack growth

5. Inject IO Dependencies for Testability

  • Dependencies interface with readFile and fetchModels injectable functions
  • Default implementations use real fs/fetch
  • replaceDependencies() for test injection

Verification

  • npm run typecheck — passes
  • npm run build — passes, dist/ output correct
  • node --test test/models-config.test.ts — 3/3 tests pass

What NOT Changed

  • Plugin external behavior/API unchanged — pure refactor
  • Test harness (test/run.ts) untouched
  • Atomic file write pattern preserved
  • chat.params hook preserved
  • All existing exports and plugin hooks work identically

Aaron J and others added 5 commits April 26, 2026 10:41
…ursive dialog, add logging

Comparison with opencode-tree plugin revealed several quality issues:

1. Deduplicate server/TUI shared code (HIGHEST)
   - Extract isObject(), isTelnyxHostedModel(), storedApiKey(),
     apiKey(), normalizeModel(), fetchModels(), fetchAllHostedModelIDs(),
     fetchHostedModels(), providerModels() to src/shared.ts
   - Both index.ts and tui.tsx now import from shared module
   - Zero duplication between the two entry points

2. Add Zod schemas for config validation
   - ModelsConfigFileSchema validates telnyx-models.json format
   - TelnyxCredentialSchema validates auth.json credential format
   - safeParse used in loadEnabledModels() and storedApiKey()
   - Parse errors logged before falling back to defaults
   - Added zod to dependencies

3. Add logging in all catch blocks
   - Replaced empty catch blocks with console.error('[telnyx]', ...)
   - Includes operation context for debugging
   - Applied across shared.ts and models-config.ts

4. Fix recursive openManager dialog pattern
   - Replaced recursive await openManager(api) with local
     renderDialog() function that re-renders in-place
   - Each model toggle now calls renderDialog() instead of
     re-entering openManager(), avoiding stack growth

5. Inject IO dependencies for testability
   - Dependencies interface with readFile and fetchModels
   - Default implementations use real fs/fetch
   - replaceDependencies() for test injection
   - Shared fetch logic consolidated into deps.fetchModels
…er, unit tests

1. Switch models-config.ts to async file I/O (fs/promises)
   - loadEnabledModels, persistEnabledModels, persistDefaultModelsConfigIfMissing
     are now async and use readFile/writeFile/mkdir/rename from node:fs/promises
   - All callers in index.ts, tui.tsx, shared.ts updated to await
   - shared.ts Dependencies.readFile now returns Promise<string>
   - storedApiKey() and apiKey() are now async

2. Validate-on-write in persistEnabledModels
   - Runs ModelsConfigFileSchema.parse(payload) before writing to disk
   - Guarantees we can never write something we can't read back

3. Extract buildProviderConfig() helper to shared.ts
   - Eliminates duplicated provider config construction in index.ts and tui.tsx
   - Handles optional apiKey (omitted when key is undefined)

4. Add unit tests for shared.ts and models-config.ts
   - test/shared.test.ts: isObject, isTelnyxHostedModel, normalizeModel,
     modelConfigEntry, buildProviderConfig, storedApiKey (via DI)
   - test/models-config.test.ts: defaultModelsConfig, loadEnabledModels
     (valid, missing, invalid JSON, wrong schema, wrong version, empty strings),
     persistEnabledModels (write, dedup, empty, atomic, schema validation)
   - All 39 tests pass
@aisling404
Copy link
Copy Markdown
Collaborator

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9bbc81f06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/opencode/src/models-config.ts Outdated
Comment on lines +77 to +79
if (!parsed.success) {
console.error("[telnyx] invalid models config file, falling back to defaults:", parsed.error)
return [...DEFAULT_ENABLED_MODELS]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve valid model IDs when config is partially invalid

Falling back to DEFAULT_ENABLED_MODELS for any schema mismatch means a single malformed entry in enabledModels (for example from manual edits or a stale file) discards all otherwise valid saved selections. Before this refactor, invalid entries were filtered out while valid strings were retained; now users can unexpectedly lose their curated allowlist and silently switch back to defaults. Consider salvaging valid IDs from enabledModels instead of resetting the entire config when only part of the payload is invalid.

Useful? React with 👍 / 👎.

Comment thread plugins/opencode/src/shared.ts Outdated
Comment on lines +80 to +82
} catch (error) {
console.error("[telnyx] failed to read auth file:", error)
return undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Skip error logging for missing auth file

When no Telnyx credential has been saved yet, reading auth.json commonly fails with ENOENT, but this path now logs an error every time storedApiKey() runs. That makes normal first-run behavior look like a failure and can flood logs during startup or /telnyx usage. Treat missing-file errors as expected (return undefined quietly) and reserve console.error for unexpected read/parse failures.

Useful? React with 👍 / 👎.

@aaronjo-Telnyx
Copy link
Copy Markdown
Contributor Author

Both issues flagged by @codex have been addressed in the current branch:

  • P2 (models-config.ts): The fallback-to-defaults regression is fixed. loadEnabledModels now attempts to salvage valid individual entries from a malformed config, only falling back to DEFAULT_ENABLED_MODELS when zero valid entries remain.

  • P3 (shared.ts): The noisy console.error on ENOENT is fixed. storedApiKey() now silently returns undefined for ENOENT (expected on first run) and only logs on unexpected errors.

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

… silence ENOENT on first run

- models-config.ts: when Zod validation fails, attempt to salvage valid
  string entries from enabledModels rather than falling back to defaults
  entirely. Only fall back to DEFAULT_ENABLED_MODELS if zero valid entries
  can be salvaged.
- shared.ts: storedApiKey() now returns undefined silently for ENOENT
  (auth.json missing = normal first-run state). Only console.error on
  unexpected failures.

Addresses P2 and P3 from Codex review.
@aaronjo-Telnyx
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Aaron J and others added 2 commits April 28, 2026 09:34
… silence ENOENT in storedApiKey

loadEnabledModels now validates each model ID individually when the
overall schema fails, keeping valid entries rather than replacing the
entire config with defaults. storedApiKey silently returns undefined
on ENOENT (normal first-run) and only logs unexpected errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aaronjo-Telnyx
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@aaronjo-Telnyx
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants