Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ file is the human-readable summary of what changed and why.

## Unreleased

### TUI

- **A fresh install no longer defaults to "Big Pickle · OpenCode Zen"** — with
no `model` in the config and no saved model selection (e.g. right after
`codevhub install`, which stopped pinning a model in the same round as our
setup flow), the fallback picked the first provider in the list, and
upstream's hosted Zen provider always registers itself first. The fallback
now prefers the first deliberately-configured provider — the same "a real
provider is configured" test the startup gate and the footer indicator use —
so a gateway-only setup starts on a gateway model.

## 1.18.4-4

### TUI
Expand Down
1 change: 1 addition & 0 deletions DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ from the squash message first, or use a plain merge.
| `packages/tui/src/component/dialog-provider.tsx`, `packages/tui/src/component/dialog-model.tsx`, `packages/opencode/src/cli/cmd/providers.ts` | Filter `opencode`/`opencode-go` out of provider pickers and the `/model` dialog (model list + Favorites/Recent; the model dialog's opencode-first sort became a plain name sort) | Upstream's hosted offerings (Zen/Go) are detours for gateway users. | Keep our filters. |
| `packages/tui/src/component/dialog-provider.tsx` | The "Other" custom-provider option routes into the fork-only `runCustomProviderFlow` (component/dialog-custom-provider.tsx, new file) — id → API key → optional base URL → model discovery via `/v1/models` → `provider.<id>` block PATCHed into global codev.json (key stays in auth.json) — instead of upstream's credential-only `ApiMethod`; the local `promptCustomProviderID` helper moved into that flow (upstream's "only stores a credential" description no longer holds). `ApiMethod`'s `custom` prop is now unreferenced but left in place. | An empty base URL keeps upstream's credential-only behavior; with one, the provider is usable without hand-editing codev.json. The configure gate's "Custom provider" option shares the same flow. | Keep our `onSelect` + import and the helper deletion; take upstream's changes to `ApiMethod`/other methods (the flow only wraps the entry point). |
| `packages/tui/src/component/prompt/index.tsx` | Gated the prompt footer's model/provider indicator on `useConnected()` (the shared "a real provider is configured" predicate) | Don't advertise the free hosted Zen default (e.g. "Big Pickle · OpenCode Zen") in the footer while nothing is configured — same rationale and predicate as `app.tsx`'s startup gate and the provider/model filters. A configured gateway/real provider, or a deliberately-authenticated opencode/opencode-go, still shows its model. | Keep the extra `Show` condition; take upstream's surrounding changes. |
| `packages/tui/src/component/use-connected.tsx`, `packages/tui/src/context/local.tsx` | use-connected.tsx: the provider predicate is extracted into an exported `isConnectedProvider`; `useConnected` now delegates to it (same logic). local.tsx: `fallbackModel`'s last resort is `sync.data.provider.find(isConnectedProvider) ?? sync.data.provider[0]` instead of a blind `sync.data.provider[0]` (covered by fork-only `test/component/use-connected.test.ts`, not tracked here). | The hosted Zen provider self-registers (free models need no auth) and is inserted ahead of config-only providers, so with no config `model` pin and empty recents (a fresh `codevhub install` — the pin was dropped in PR #142 / hub PR #221) the TUI defaulted to "Big Pickle · OpenCode Zen" and would send messages to upstream's hosted gateway instead of the configured one. Same predicate as `app.tsx`'s startup gate and the footer indicator; when no provider qualifies the old first-provider behavior is kept. | Keep the predicate export and the `find` fallback; take upstream's surrounding changes to either file. If upstream reworks `fallbackModel`, re-apply the connected-first selection to its last-resort branch. |
| `packages/tui/src/component/prompt/index.tsx` | The busy-state footer arm (`status().type !== "idle"`) also renders `props.hint` after the `esc interrupt` text, gated on `status().type !== "retry"` | The hidden-sidebar hint (see the `routes/session/index.tsx` hint row) only rendered in the footer `Switch`'s idle fallback arm, so it vanished for the whole duration of a running turn — exactly when todos are being created. Retry keeps its `space-between` error row uncluttered. | Keep the `<Show when={status().type !== "retry"}>{props.hint}</Show>` line in the busy `Match` arm (additive); take upstream's surrounding changes. |
| `packages/tui/src/component/prompt/index.tsx` | Removed the editor open-file label from the footer's right-side group and lifted the state behind it out of `Prompt`: the local `dismissedEditorSelectionKey` signal and the `editorContext`/`editorPath`/`editorSelectionLabel`/`editorFileLabel`/`editorFileLabelDisplay`/`editorContextLabelState` memos are gone, replaced by `useEditorLabel()` (fork-only `src/context/editor-label.tsx`) — `selection()` feeds the `prompt.editor_context.clear` `enabled` predicate and the submit-time editor part, `state()` replaces `editor.labelState()` there, and `dismiss()` replaces the local `dismissEditorContext`. The pure helpers `hasEditorRangeSelection`/`getEditorRangeLabel` moved to that context module and are imported back for `formatEditorContext`. | The label now lives in the sidebar footer (see its row above). The dismissal state had to move with it: the sidebar must label the *effective* selection — the one this file actually attaches on submit — or it would advertise context the prompt has dismissed and won't send. | Keep the deletions, the `useEditorLabel` import + its three call sites, and the helper import from `../../context/editor-label`; take upstream's surrounding changes. If upstream reworks the editor-context label, apply the change to `editorContextFileLabel` in the fork-only sidebar module rather than restoring the footer render. |
| `packages/tui/src/routes/session/index.tsx`, `packages/tui/src/component/prompt/index.tsx` | Left accent border removed entirely: the user-message block's `border={["left"]}`/`borderColor` (agent accent)/`customBorderChars` props and the prompt input box's two bordered boxes (main box with its `bottomLeft: "╹"` char, plus the height-1 `╹` terminator row) are replaced with `paddingLeft={1}`; the now-unused `borderHighlight` memo and the `tint`/`SplitBorder` imports are removed from the prompt (the `highlight`/`agentMetaAlpha` memos stay — they still color the agent name label); the fork's in-scrollbox banner box gains `paddingLeft={1}` so the banner/tips left edge lines up with the prompt/message blocks' background edge (the removed border glyph used to occupy that aligned column) | No vertical accent line next to sent messages or the prompt — removed rather than recolored to the background because background-colored glyphs render oddly on some terminals (e.g. PowerShell). `paddingLeft={1}` keeps the same 1-column offset, so text alignment with other transcript blocks is unchanged. | Keep our `paddingLeft={1}` boxes and the `borderHighlight`/`tint`/`SplitBorder` removals; take upstream's surrounding changes. If upstream adds new accent-colored left borders to these blocks, replace them with `paddingLeft={1}` the same way. |
Expand Down
5 changes: 3 additions & 2 deletions packages/codev-gateway/src/config-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ const MODEL_MODALITIES: { attachment: boolean; modalities: { input: Modality[];
// then the state dir's model.json recents), so the wizard's one-time choice
// would silently undo every later /models switch. The configure flow seeds
// that choice into the recents instead, which the next switch displaces; the
// hub leads its models map with the chosen id for the same effect. A config
// written by an older hub still carries a pin — nothing here removes one.
// hub seeds the same recents file when it is empty (hub PR #229 — its map
// order never carried the choice). A config written by an older hub still
// carries a pin — nothing here removes one.
export function buildGatewayConfig(input: { apiKey: string; baseUrl: string; model: string; models?: string[] }) {
const ids = [...new Set([input.model, ...(input.models ?? [])])]
return {
Expand Down
14 changes: 8 additions & 6 deletions packages/tui/src/component/use-connected.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { createMemo } from "solid-js"
import { useSync } from "../context/sync"

// True unless the entry is the hosted Zen provider's free-tier auto-registration
// (its free models register without any auth). A deliberately-authenticated
// opencode still passes via its paid models.
export function isConnectedProvider(provider: { id: string; models: Record<string, { cost?: { input: number } }> }) {
return provider.id !== "opencode" || Object.values(provider.models).some((model) => model.cost?.input !== 0)
}

export function useConnected() {
const sync = useSync()
return createMemo(() =>
sync.data.provider.some(
(provider) =>
provider.id !== "opencode" || Object.values(provider.models).some((model) => model.cost?.input !== 0),
),
)
return createMemo(() => sync.data.provider.some(isConnectedProvider))
}
7 changes: 6 additions & 1 deletion packages/tui/src/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useTheme } from "./theme"
import { useToast } from "../ui/toast"
import { useRoute } from "./route"
import { usePermission } from "./permission"
import { isConnectedProvider } from "../component/use-connected"

export type LocalTheme = {
secondary: RGBA
Expand Down Expand Up @@ -221,7 +222,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}

const provider = sync.data.provider[0]
// The hosted Zen provider self-registers (free models need no auth) and
// lands first in the list, so a blind [0] would default a gateway-only
// setup to a Zen model. Prefer the first deliberately-configured
// provider — same predicate as the startup gate and prompt footer.
const provider = sync.data.provider.find(isConnectedProvider) ?? sync.data.provider[0]
if (!provider) return undefined
const defaultModel = sync.data.provider_default[provider.id]
const firstModel = Object.values(provider.models)[0]
Expand Down
30 changes: 30 additions & 0 deletions packages/tui/test/component/use-connected.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, test } from "bun:test"
import { isConnectedProvider } from "../../src/component/use-connected"

const zenFree = {
id: "opencode",
models: { "big-pickle": { cost: { input: 0 } } },
}
const gateway = {
id: "netgate",
models: { "MiniMax/MiniMax-M3": {} },
}

test("the free Zen auto-registration does not count as connected", () => {
expect(isConnectedProvider(zenFree)).toBe(false)
})

test("a deliberately-authenticated opencode counts via its paid models", () => {
expect(
isConnectedProvider({
id: "opencode",
models: { "big-pickle": { cost: { input: 0 } }, "heavy-pickle": { cost: { input: 3 } } },
}),
).toBe(true)
})

test("fallbackModel's last resort prefers a configured provider over the Zen registration", () => {
expect(isConnectedProvider(gateway)).toBe(true)
// Zen registers first in the provider list; find(isConnectedProvider) must skip it.
expect([zenFree, gateway].find(isConnectedProvider)).toBe(gateway)
})
Loading