fix(cli): use ASCII box/separator chars in CI and respect NO_COLOR in output-renderer#4310
Closed
PeterPonyu wants to merge 25 commits into
Closed
fix(cli): use ASCII box/separator chars in CI and respect NO_COLOR in output-renderer#4310PeterPonyu wants to merge 25 commits into
PeterPonyu wants to merge 25 commits into
Conversation
eb25d29 to
2bfad49
Compare
3044dd3 to
d3875e8
Compare
Contributor
Author
|
recheck |
Lays the groundwork for surfacing each role's active model + provider so users (and team leaders) can see what's actually answering. - New `display` config block: show_models_on_session_start, show_models_on_fallback, auto_pick, auto_pick_budget. Threaded through mergeConfigs in plugin-config.ts. - New src/features/roles-models/ module with role enumeration (14 known roles from AgentOverridesSchema), per-session state (overrides + budget cap), view-builder, and markdown-panel renderer. Wiring for /show-models, /pick, /auto-pick commands and the pick_model agent tool follows in subsequent commits. JSONC --persist writer and runtime fallback-state integration deferred to follow-up PRs. bun run typecheck: pass. New tests: 17/17. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lights the feature up end-to-end from the user's perspective. - New built-in command definitions (show-models, pick, auto-pick) added to the Zod enum, BuiltinCommandName union, and command definitions map. - New command-handler.ts in roles-models/ dispatches the three commands, parses arguments (incl. --variant and --persist), and pushes the rendered panel or confirmation as a text part. - /pick records a session-scoped override; --persist surfaces a deferred notice (JSONC writer coming in a follow-up). - /auto-pick toggles a per-session runtime override that wins over the config default; resolveAutoPick(sessionID, config) gives the merged view used by the panel. - command-execute-before factory now accepts pluginConfig and routes roles-models commands through the new handler. bun run typecheck: pass. Touched tests (display schema, roles-models, builtin-commands, command-execute-before): 29/29 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets agents (and team leaders) request a model swap for a role mid- session. Mirrors the user-facing /pick command, but callable from inside an agent loop. - Registered only when display.auto_pick is true OR /auto-pick on has been issued in this session (resolveAutoPick handles the merge). - Validates the requested model exists in the role's declared chain (primary + fallback_models). Unknown roles get the known-roles list back as an error. - Each role has a per-session swap budget (display.auto_pick_budget, default 2). Exhaustion returns a clear error without mutating state. - Tool registration follows the hashline_edit pattern: conditional toolsRecord spread into allTools, factory routed through ToolRegistryFactories so it can be mocked in tests. bun run typecheck: pass. New + existing roles-models tests: 33/33 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When display.show_models_on_session_start is true, the roles-models panel is injected into the user's first message of each session. Gated by the config flag, idempotent per session via an internal SESSION_PANEL_SHOWN set. Plays nicely with the existing firstMessageVariantGate in chat-message.ts. show_models_on_fallback is intentionally not wired in this commit — it needs a runtime-fallback event subscription that's better scoped to its own change. The config field is already in the schema so the follow-up won't change the public API. bun run typecheck: pass. roles-models + pick-model suites: 38/38 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bun build bundles cleanly but the declaration-emit step in the full build pipeline surfaced two strictness issues that tsc --noEmit did not. Bundle artifacts (dist/index.js, dist/cli/index.js) are correct, but anyone running the full build script (which includes --emitDeclarationOnly + JSON-schema regen) hit exit 1. - discover.ts: fallback_models is a Zod union of string, string[], FallbackModelObject[], and the mixed array. The old map() assumed shape code-yeongyu#3. Normalize all four through normalizeFallbackEntry so the declared chain is consistent regardless of how a user wrote it. - pick-model/index.ts: annotate createPickModelTool with ToolDefinition so the inferred return type doesn't reach into the @opencode-ai/plugin internal zod copy (TS2742). - Regenerated assets/oh-my-opencode.schema.json to include the new display block in the JSON schema (auto-generated by build:schema). bun run build: pass. Touched-area tests: 38/38 still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bug: /pick and the pick_model tool recorded a per-session override
in state.ts, but no hook ever read that map to actually substitute the
model. The slash command confirmation said "✓ /pick applied" yet
subsequent LLM turns kept running on whatever opencode had picked
originally — verified in a live opencode run.
Root cause: the override map was wired only into the panel renderer
(view.ts → buildViews). The chat.message hook, which is where
output.message.model is set, never consulted it.
Fix:
- New active-model.ts exposes resolveOverrideModel(sessionID, role)
which reads the override map and parses "provider/model" into
{providerID, modelID}. Splits on the first slash so namespaced model
ids like "vercel/openai/gpt-5.5" survive.
- chat-message.ts now calls resolveOverrideModel right after the
existing getStoredMainSessionModel block (so /pick wins over the
recovered session model). The result feeds output.message.model and
flows through the existing setSessionModel persistence path, so the
override sticks across subsequent turns in the same session.
Tests:
- 9 new tests in active-model.test.ts cover parseProviderModel edge
cases (missing slash, leading/trailing slash, namespaced model ids)
and resolveOverrideModel session/role scoping.
- 3 new integration tests in chat-message.test.ts assert that an
override set via setOverride results in output.message.model being
the parsed pair, that the pick wins over a stored session model,
and that picks for other roles don't leak.
Out of scope (deferred):
- Variant overrides — output.message.model only carries
{providerID, modelID}. Variant lives in chat.params (reasoning_effort
etc.); a parallel block there is a small follow-up.
bun run build: pass. Touched-area tests: 68/68 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the /pick override path. The prior commit made model swaps take effect via chat.message; this one extends the same treatment to variant. Without it, /pick sisyphus anthropic/claude-opus-4-7 --variant=max swapped the model correctly but left reasoning effort / thinking config driven by the original variant. How it works: - chat-params.ts reads getRolePick(sessionID, agentKey) at the top of the handler. - If the override carries a variant, it overwrites normalizedInput.message.variant (and the underlying rawMessage) before resolveCompatibleModelSettings runs. - The existing pipeline then translates the picked variant into output.options.reasoningEffort / output.options.thinking / temperature etc. based on the target model's capabilities. Precedence: pick.variant > input.message.variant > config default. If the pick was issued without a variant, the user's TUI selection is left untouched — both behaviors are covered by tests. bun run build: pass. chat-params + chat-message + roles-models suites: 77/77 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion
Live opencode test surfaced a schema error: when maybeAutoPrintPanel
pushed a bare {type:"text", text:"..."} into output.parts from the
chat.message hook, opencode 1.14.49's session.prompt.createUserMessage
rejected the request with "Missing key at [id]/[sessionID]/[messageID]"
and the message never saved. The unit tests passed because the test
harness builds the same loose shape, but the real opencode runtime
validates against the full TextPart schema:
{ id: string, sessionID: string, messageID: string, type: "text", text: string }
Fix:
- maybeAutoPrintPanel now takes messageID alongside sessionID and
constructs the part with id: prt_<uuid>, sessionID, messageID. The
callsite in chat-message.ts passes input.messageID (already in
opencode's hook contract; the plugin's narrower ChatMessageInput
type just didn't expose it — added now).
- If messageID is undefined (test mocks, edge cases) the function
skips injection silently — we can't construct a valid part without
it and we'd rather no-op than crash.
- Tests updated for the new signature and a new assertion verifies
the part carries id/sessionID/messageID with the expected shape.
Note: /show-models, /pick, /auto-pick are unaffected — they flow
through command.execute.before, which is a different output path that
doesn't hit the same schema check (confirmed live).
bun run build: pass. Touched-area tests: 85/85 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ut.agent is absent cubic-dev-ai flagged that resolveOverrideModel was passed undefined whenever input.agent was missing, silently skipping an active /pick override for that turn. Identified by cubic. The pattern matters because opencode occasionally invokes chat.message without input.agent populated (compaction retries, model-fallback re-fires). Fix: when input.agent is absent, look up the session's primary agent via getSessionAgent(sessionID) and resolve the override against that. The session-agent map is first-write-wins, so it stably represents the agent the user originally selected for the session. Tests: new chat-message integration test exercises the agent-absent path; existing 28 chat-message tests still pass (29/29 total). bun run build: pass. bun run typecheck: pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…VariantGate Live qa-tester captured a reproducible miss: on opencode reconnects to existing sessions (no session.created emitted to the plugin), the firstMessageVariantGate's `pending` set stays empty, so `shouldOverride` returns false, so the previous code path skipped `maybeAutoPrintPanel` entirely. The panel only fired for truly-fresh sessions, which is a narrower contract than the feature intends. maybeAutoPrintPanel already has its own per-session idempotency via SESSION_PANEL_SHOWN. Pulling it out of the isFirstMessage gate lets it fire on the first chat.message turn of EVERY session within the plugin lifetime — including reconnects — while the SESSION_PANEL_SHOWN check prevents duplicate injection within the same session. Tests: 3 new chat-message integration tests: - panel fires when shouldOverride returns false (reconnect case) - two consecutive chat.message calls inject exactly one panel (idempotent) - show_models_on_session_start=false still gates injection out (regression guard) bun run build: pass. bun run typecheck: pass. Touched-area tests: 73/73 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When chat.message fires without an explicit input.agent, the override resolution previously fell through to the session's stored primary agent, which could pick the wrong model if the /pick override was set against a different agent earlier in the session. Now we skip the override entirely in this case and let opencode resolve the model normally. Addresses cubic-dev-ai's first-pass review on PR code-yeongyu#4002 (severity 5/10, confidence 7/10). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ic write (Path A) Replaces the Path B stub (reject --persist with code-yeongyu#4011 error) with the real implementation, addressing the maintainer's design guidance in code-yeongyu#4011 (comment). Reuse anchors honoured: 1. JSONC mutation uses `jsonc-parser modify + applyEdits` — same pattern as `src/shared/migrate-legacy-plugin-entry.ts`. Comments, trailing commas, and existing formatting are preserved. 2. Atomic write via `writeFileAtomically` (write to <path>.tmp, then `renameSync` — POSIX-atomic on Linux/macOS APFS/HFS+). Windows EPERM fallback handled inside `writeFileAtomically`. 3. Path resolution routes through `getOmoConfigPath()` from `src/cli/config-manager/config-context.ts` — the same resolver used by `writeOmoConfig` and `loadPluginConfig`. No hardcoded platform paths. 4. Legacy `oh-my-opencode.json[c]` migration fallback is transparent: `getOmoConfigPath()` already calls `detectPluginConfigFile`, which resolves whichever file exists (canonical or legacy-migrated). 5. In-memory `pluginConfig` is mutated after the write so the current session sees the new model immediately. The next `loadPluginConfig` call reads the updated file from disk. On failure (permission denied, parent dir not creatable, etc.) a structured error is returned including the resolved path and underlying errno. No silent fallback to session-only. Tests added (8 scenarios, all tmpdir-based, no writes to real ~/.config): - Writes agents.<role>.model to config with empty-object file - Writes both model and variant with --variant=X - Preserves existing comments in .jsonc fixture - Overwrites existing agents.<role>.model - Creates agents block when missing - Returns structured error on write failure - Verifies .tmp file is not left behind (atomic rename confirmed) - Mutates in-memory config so session sees new value immediately Thank you to @code-yeongyu for the precise reuse anchors in code-yeongyu#4011 — this implementation follows them line-by-line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the TUI plugin module that registers a "Roles · Models" section in the host sidebar_content slot. The TUI module is fully decoupled from the server module and reads only api.state.config and session messages. - src/tui/index.tsx: TUI entry point, registers sidebar_content slot - src/tui/sidebar/roles-models-section.tsx: SolidJS sidebar component - src/tui/sidebar/derive-row.ts: Pure data derivation for RoleRow - src/tui/sidebar/use-session-role-activity.ts: Reactive session hook - src/tui/sidebar/index.test.ts: 14 tests (all passing) - script/build-tui.ts: Build using @opentui/solid/bun-plugin - package.json: ./tui export, build:tui script, bump deps - tsconfig.json: jsx preserve + jsxImportSource for TSX Fixes code-yeongyu#4303
…model format for --persist
… output-renderer Terminal emulators in CI (GitHub Actions and generic CI) do not render box-drawing Unicode characters reliably, producing mojibake in logs. format-verbose.ts and install-validators.ts now detect CI/GITHUB_ACTIONS at module load time and substitute ASCII equivalents (-, |, +) for the Unicode line/box chars. output-renderer.ts: colorizeWithProfileColor now skips truecolor ANSI escape sequences when NO_COLOR, CI, GITHUB_ACTIONS, or TERM=dumb is set, preventing raw \e[38;2;... codes from leaking into non-color terminals. output-renderer.test.ts (new): verifies that NO_COLOR suppresses truecolor escapes from renderAgentHeader. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt markup corruption
c77c639 to
bf39d62
Compare
Contributor
Author
|
Closing in favor of scope-isolated replacements. This branch accidentally stacked 25 commits across 4 distinct scopes:
#4455 carries only the title-scoped changes here (94 net additions across 6 files, all CLI). The 97ae452 "restore decomposed Unicode in CJK coverage test" commit was dropped because the renderer normalizes to NFC and the test would assert against decomposed bytes — that landed in error during the stacking. Apologies for the noise. |
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split off from #4305 to keep the change focused.
Summary
src/cli/run/output-renderer.ts: suppress truecolor SGR escapes whenNO_COLOR,CI,GITHUB_ACTIONS, orTERM=dumbis set.src/cli/install-validators.tsandsrc/cli/doctor/format-verbose.ts: switch box/separator drawing characters to ASCII fallbacks when the terminal cannot render Unicode reliably (CI logs, dumb terminals).NO_COLORsuppression inrenderAgentHeaderand ASCII fallback in installer/doctor output.Why split
PR #4305 grew to 156 files spanning team-mode, runtime-fallback, TUI, and several unrelated fixes. This commit (
e9894191) is independent and reviewable on its own.Verification
output-renderer.test.tsstill pass alongside the newNO_COLORtest.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Summary by cubic
Adds a roles·models feature to view and switch each role’s active model, with live application and an optional TUI sidebar. CLI output now uses ASCII in CI and skips truecolor when color is disabled or terminals can’t render it.
New Features
displayconfig:show_models_on_session_start,show_models_on_fallback,auto_pick,auto_pick_budget./show-models,/pick <role> <provider/model> [--variant] [--persist],/auto-pick on|off.pick_modelwith per-role session budget; validates against the role’s primary/fallback chain.output.message.modeland chat params (variant); panel can auto-print on session start./pick --persistwrites to config via JSONC edit + atomic write../tuiexport registers a “Roles · Models” sidebar; built viascript/build-tui.ts. Optional peers@opentui/core,@opentui/solid; addssolid-js.Bug Fixes
doctorand ASCII+/-/|boxes in installer whenCI/GITHUB_ACTIONS; suppress 24-bit color whenNO_COLOR,CI,GITHUB_ACTIONS, orTERM=dumb.Written for commit bf39d62. Summary will update on new commits. Review in cubic