-
Notifications
You must be signed in to change notification settings - Fork 3
Add model favorites #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@copilot fix type errors |
|
@kcrommett I've opened a new pull request, #28, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThe pull request introduces a favorites feature that allows users to cycle through their preferred models using new keybinds. Changes include new UI commands, expanded configuration options across TypeScript and SDK packages, enhanced state management for persistence, refactored dialog model with ordering logic, and component updates to support keyboard events for favorites. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant App as App
participant Context as local.tsx<br/>(State)
participant Dialog as dialog-model.tsx<br/>(UI)
participant Disk as model.json
User->>App: Press Ctrl+F (cycle favorite keybind)
App->>Context: cycleFavorite(1)
Context->>Context: Select next favorite model
Context->>Context: Update current & recent
Context->>Disk: save() → persist {recent, favorite}
Disk-->>Context: Persisted
Context-->>Dialog: Notify state change
Dialog->>Dialog: Recompute orderedFavorites<br/>& option ordering
Dialog-->>User: Render updated model list
Note over Dialog: Favorites appear first<br/>with ★ indicator
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (1)
23-178: Solid refactoring with clear separation of concerns.The refactored option construction properly separates favorites, recents, and provider models with appropriate deduplication. The star indicator (★) correctly shows favorite status across all option types.
The favorite checking logic is repeated in multiple places (lines 81, 127-129). Consider extracting to a helper function:
+ const isFavorite = (item: { providerID: string; modelID: string }) => + favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID) + const recentOptions = !query ? orderedRecents.flatMap((item) => { const provider = sync.data.provider.find((x) => x.id === item.providerID) if (!provider) return [] const model = provider.models[item.modelID] if (!model) return [] - const favorite = favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID) + const favorite = isFavorite(item) return [
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
packages/sdk/js/src/gen/types.gen.tsis excluded by!**/gen/**
📒 Files selected for processing (8)
packages/opencode/src/cli/cmd/tui/app.tsx(1 hunks)packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx(4 hunks)packages/opencode/src/cli/cmd/tui/context/local.tsx(4 hunks)packages/opencode/src/config/config.ts(1 hunks)packages/sdk/go/config.go(2 hunks)packages/sdk/python/src/opencode_ai/models/keybinds_config.py(6 hunks)packages/ui/src/components/select-dialog.tsx(2 hunks)packages/web/src/content/docs/keybinds.mdx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (1)
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx (1)
filter(149-151)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: update
🔇 Additional comments (12)
packages/opencode/src/config/config.ts (1)
420-421: LGTM!The new keybind entries follow the established pattern and are appropriately defaulted to "none" for opt-in functionality.
packages/sdk/python/src/opencode_ai/models/keybinds_config.py (1)
46-47: LGTM!The Python SDK changes correctly implement serialization and deserialization for the new keybind fields, following the established pattern.
Also applies to: 100-101, 183-186, 288-291, 396-399, 469-470
packages/sdk/go/config.go (1)
1938-1941: LGTM!The Go SDK changes correctly add the new keybind fields with proper naming conventions and JSON metadata.
Also applies to: 2015-2016
packages/web/src/content/docs/keybinds.mdx (1)
41-42: LGTM!Documentation correctly reflects the new keybind configuration options with appropriate defaults.
packages/opencode/src/cli/cmd/tui/app.tsx (1)
251-268: LGTM!The favorite cycling commands follow the established pattern for recent model cycling and correctly integrate with the local context API.
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (1)
191-197: LGTM!The ctrl+f keybind correctly integrates with the toggleFavorite API and passes the selected model option.
packages/opencode/src/cli/cmd/tui/context/local.tsx (4)
129-137: Good refactoring with the shared save() helper.Centralizing persistence logic improves maintainability and ensures favorites and recents are always saved together.
202-204: LGTM!Simple getter following the established pattern for accessing favorites.
227-253: Solid implementation with good UX considerations.The cycleFavorite method correctly handles edge cases (no favorites, wrapping), validates models, maintains recent history, and persists state.
273-292: LGTM!The toggleFavorite method correctly validates, toggles, and persists favorite status.
packages/ui/src/components/select-dialog.tsx (2)
14-14: LGTM: Clean API extension for key event forwarding.The addition of the optional
onKeyEventprop enables consumers to observe keyboard interactions and the currently selected item, supporting features like favorite toggling viactrl+fas described in the PR objectives.
62-76: ****The review comment conflates two different components and makes an incorrect verification request. The original comment assumes
onKeyEventcallback (fromSelectDialogin the ui package) is used for favorites cycling in dialog-model.tsx, but it is not.Actual implementation: dialog-model.tsx uses
DialogSelect(a different component in the TUI package), notSelectDialog. Favorites cycling withctrl+fis handled through DialogSelect's keybind system (line 192), not through SelectDialog'sonKeyEventcallback. TheonKeyEventcallback is defined in SelectDialog but is completely unused in the codebase.The refactor at select-dialog.tsx (lines 62-76) is valid — it correctly moves the
onKeyEventcall before internal handling. However, this change has no impact on favorites cycling since that feature never used this callback. The requested verification for favorites cycling is therefore not applicable to this code change.Likely an incorrect or invalid review comment.
| const next = favorites[index] | ||
| if (!next) return | ||
| setModelStore("model", agent.current().name, { ...next }) | ||
| const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a delimiter in uniqueBy key generation.
The uniqueBy predicate concatenates providerID and modelID without a delimiter. While unlikely, this could cause collisions (e.g., {providerID: "ab", modelID: "cdef"} and {providerID: "abc", modelID: "def"} both produce "abcdef").
Apply this diff to use a delimiter:
- const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID)
+ const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)Also update line 266:
- const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
+ const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)Also applies to: 266-266
🤖 Prompt for AI Agents
In packages/opencode/src/cli/cmd/tui/context/local.tsx around lines 249 and 266,
the uniqueBy key concatenates providerID and modelID without a separator which
can cause collisions; update both occurrences to include a clear delimiter (for
example '|' or '::') when building the key (e.g., use
`${x.providerID}|${x.modelID}`) so the combined key is unambiguous and preserves
uniqueness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/console/app/package.json (1)
1-11: Moveversionfield to root level of package.json.The
versionfield is incorrectly nested inside thescriptsobject. In package.json,versionmust be a top-level field alongsidename,type,scripts, etc. This malformation will cause npm/bun tooling to not recognize the package version, breaking versioning and publishing workflows.Apply this diff to correct the structure:
{ "name": "@opencode-ai/console-app", "type": "module", + "version": "1.0.86", "scripts": { "typecheck": "tsgo --noEmit", "dev": "vinxi dev --host 0.0.0.0", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "1.0.86" },
🧹 Nitpick comments (1)
packages/opencode/src/tool/bash.ts (1)
185-193: Constrain$SHELLusage to non-Windows to avoid cross-platform shell incompatibility.The change from
shell: truetoshell: process.env.SHELL ?? trueintroduces valid cross-platform and shell compatibility risks:
- On Windows:
process.env.SHELLmay be set to a Unix-style path (e.g., by Git Bash or WSL tooling), but Node.jsspawnwill attempt to execute that path directly, which fails because Windows does not recognize Unix paths and those executables don't exist in the Windows filesystem.- On non-POSIX shells: If the agent generates bash-specific syntax and the user's
$SHELLis fish, csh, or another non-POSIX shell, the command will fail when executed by that shell instead of/bin/sh.Recommended fix: Constrain to non-Windows and fall back to Node's default on Windows:
- const proc = spawn(params.command, { - shell: process.env.SHELL ?? true, // $SHELL if set, otherwise default shell + const proc = spawn(params.command, { + shell: process.platform === "win32" ? true : process.env.SHELL ?? true, cwd: Instance.directory,If non-POSIX shell support is not intended, also consider restricting
$SHELLto known POSIX shells (bash, sh, ksh, zsh) to avoid incompatibility with the agent's generated commands.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (18)
package.json(1 hunks)packages/console/app/package.json(1 hunks)packages/console/core/package.json(1 hunks)packages/console/function/package.json(1 hunks)packages/console/mail/package.json(1 hunks)packages/desktop/package.json(1 hunks)packages/extensions/zed/extension.toml(2 hunks)packages/function/package.json(1 hunks)packages/opencode/package.json(1 hunks)packages/opencode/src/tool/bash.ts(1 hunks)packages/plugin/package.json(1 hunks)packages/sdk/js/package.json(1 hunks)packages/slack/package.json(1 hunks)packages/ui/package.json(1 hunks)packages/util/package.json(1 hunks)packages/web/package.json(1 hunks)packages/web/src/content/docs/zen.mdx(1 hunks)sdks/vscode/package.json(1 hunks)
✅ Files skipped from review due to trivial changes (9)
- packages/web/package.json
- packages/console/mail/package.json
- sdks/vscode/package.json
- packages/desktop/package.json
- packages/function/package.json
- packages/console/core/package.json
- packages/ui/package.json
- packages/console/function/package.json
- packages/util/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: update
- GitHub Check: update
🔇 Additional comments (7)
packages/extensions/zed/extension.toml (1)
4-4: Version bump applied consistently across all platform targets.The version has been updated from 1.0.85 to 1.0.86, and all archive URLs for the five platform targets (darwin-aarch64, darwin-x86_64, linux-aarch64, linux-x86_64, windows-x86_64) have been synchronized correctly. The GitHub release URL pattern is correct and consistent.
Also applies to: 14-14, 19-19, 24-24, 29-29, 34-34
package.json (1)
7-7: Verify Bun 1.3.3 compatibility with the project.The packageManager has been bumped from
bun@1.3.2tobun@1.3.3. Confirm this patch update doesn't introduce any breaking changes or incompatibilities with the new favorites feature or other toolchain configurations.packages/slack/package.json (1)
3-3: Version bump aligns with repository-wide versioning.Coordinated version bump across packages to 1.0.86.
packages/plugin/package.json (1)
4-4: Version bump aligns with repository-wide versioning.Coordinated version bump across packages to 1.0.86.
packages/sdk/js/package.json (1)
4-4: Version bump aligns with repository-wide versioning.Coordinated version bump across packages to 1.0.86.
packages/opencode/package.json (1)
3-3: Version bump aligns with repository-wide versioning.Coordinated version bump to 1.0.86 with correct root-level placement.
packages/web/src/content/docs/zen.mdx (1)
136-139: Verify GPT 5.x pricing reflects actual backend pricing.The pricing table for GPT 5.x models has been updated with reduced rates across input (~14% reduction), output (~15%), and cached reads. Confirm these prices match the actual backend pricing configuration and that no other documentation or config files need corresponding updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/ui/src/components/message-nav.tsx (1)
67-73: Tooltip placement + nested MessageNav override make sense; minor prop cleanup possibleUsing
placement="left-start"for the compact tooltip is fine and keeps the API usage intact. Overridingclasson the nested<MessageNav>to""is a good way to prevent outer layout/visibility classes (e.g., breakpoint-basedhidden/margins) from affecting the tooltip content.If you want to tighten this up, you could instead avoid passing
classat all to the nestedMessageNav(e.g., by cloningpropswithclassremoved before spreading), which is a bit clearer than using an empty string but functionally equivalent here.packages/enterprise/src/routes/share/[sessionID].tsx (2)
187-252:widememo and MessageNav layout logic are coherent and match intentDeriving
widefromdiffs().length === 0and then using it to drive padding (px-21vspx-6), MessageNav positioning (right-[90%]vsright-full), and size (normalvscompact) makes the layout logic much easier to follow and keeps the “no diffs” vs “has diffs” states clearly separated.Behavior-wise, this preserves:
- A wider content area when there are no diffs.
- A more compact nav when the diff panel is present.
- Separate compact (
xl:hidden) and larger (xl:block) nav instances with breakpoint-specific positioning.I don’t see any functional regressions; this is a nice cleanup.
267-297: Explicit diffs gating looks good; consider making the mobile label dynamicGating both the desktop diff panel and the mobile Tabs on
diffs().length > 0is clearer and avoids truthiness edge cases. The overall control flow for small screens (Tabswhen there are diffs, plainturns()otherwise) is straightforward and correct.One small improvement you might consider while you’re here: the “5 Files Changed” label in the mobile
Tabs.Triggeris still hard-coded. Usingdiffs().length(or a precomputedfileCountmemo) would keep the label accurate when the number of changed files varies.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
nix/hashes.json(1 hunks)packages/desktop/src/pages/session.tsx(1 hunks)packages/enterprise/src/routes/share/[sessionID].tsx(3 hunks)packages/opencode/package.json(1 hunks)packages/sdk/js/src/client.ts(1 hunks)packages/ui/src/components/message-nav.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- nix/hashes.json
🧰 Additional context used
📓 Path-based instructions (7)
packages/opencode/**/package.json
📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)
Install dependencies using
bun install
Files:
packages/opencode/package.json
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,jsx,ts,tsx}: DO NOT do unnecessary destructuring of variables
DO NOT useelsestatements unless necessary
DO NOT usetry/catchif it can be avoided
AVOIDtry/catchwhere possible
AVOIDletstatements
PREFER single word variable names where possible
Use as many Bun APIs as possible like Bun.file()
Files:
packages/sdk/js/src/client.tspackages/ui/src/components/message-nav.tsxpackages/desktop/src/pages/session.tsxpackages/enterprise/src/routes/share/[sessionID].tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
AVOID using
anytype
Files:
packages/sdk/js/src/client.tspackages/ui/src/components/message-nav.tsxpackages/desktop/src/pages/session.tsxpackages/enterprise/src/routes/share/[sessionID].tsx
packages/desktop/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (packages/desktop/AGENTS.md)
packages/desktop/**/*.{ts,tsx,js,jsx}: Use@/alias for imports from src/ directory (e.g.,import Button from "@/ui/button")
Use function declarations and splitProps for SolidJS component props
Use camelCase for variable and function names
Files:
packages/desktop/src/pages/session.tsx
packages/desktop/**/*.{ts,tsx}
📄 CodeRabbit inference engine (packages/desktop/AGENTS.md)
Define interfaces for component props and avoid using
anytype
Files:
packages/desktop/src/pages/session.tsx
packages/desktop/**/*.{ts,tsx,css}
📄 CodeRabbit inference engine (packages/desktop/AGENTS.md)
Use TailwindCSS with custom CSS variables theme system for styling
Files:
packages/desktop/src/pages/session.tsx
packages/desktop/**/*.{ts,tsx,js,jsx,css}
📄 CodeRabbit inference engine (packages/desktop/AGENTS.md)
Use snake_case for file names
Files:
packages/desktop/src/pages/session.tsx
🧠 Learnings (6)
📚 Learning: 2025-11-24T22:47:41.129Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:41.129Z
Learning: Applies to packages/opencode/**/*.{ts,tsx} : Use relative imports for local modules with named imports preferred
Applied to files:
packages/opencode/package.json
📚 Learning: 2025-11-24T22:47:41.129Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:41.129Z
Learning: Applies to packages/opencode/**/*.{ts,tsx} : Organize code using namespace-based structure (e.g., `Tool.define()`, `Session.create()`)
Applied to files:
packages/opencode/package.jsonpackages/enterprise/src/routes/share/[sessionID].tsx
📚 Learning: 2025-11-24T22:47:41.129Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:41.129Z
Learning: Applies to packages/opencode/**/*.{ts,tsx} : Pass `sessionID` in tool context and use `App.provide()` for dependency injection
Applied to files:
packages/opencode/package.jsonpackages/enterprise/src/routes/share/[sessionID].tsx
📚 Learning: 2025-11-24T22:47:41.129Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:41.129Z
Learning: Go TUI communicates with TypeScript server via stainless SDK; when modifying server endpoints in `packages/opencode/src/server/server.ts`, request user to generate a new client SDK before proceeding with client-side changes
Applied to files:
packages/sdk/js/src/client.ts
📚 Learning: 2025-11-24T22:47:28.227Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: packages/desktop/AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:28.227Z
Learning: Applies to packages/desktop/**/*.{ts,tsx,js,jsx} : Use function declarations and splitProps for SolidJS component props
Applied to files:
packages/desktop/src/pages/session.tsxpackages/enterprise/src/routes/share/[sessionID].tsx
📚 Learning: 2025-11-24T22:47:28.227Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: packages/desktop/AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:28.227Z
Learning: Applies to packages/desktop/**/({ui,components,pages})/**/*.{ts,tsx,js,jsx} : Use PascalCase for component names
Applied to files:
packages/desktop/src/pages/session.tsx
🧬 Code graph analysis (3)
packages/sdk/js/src/client.ts (2)
packages/console/app/src/config.ts (1)
config(4-29)packages/opencode/src/share/share.ts (1)
URL(68-70)
packages/ui/src/components/message-nav.tsx (1)
packages/ui/src/components/tooltip.tsx (1)
Tooltip(11-50)
packages/enterprise/src/routes/share/[sessionID].tsx (1)
packages/ui/src/components/message-nav.tsx (1)
MessageNav(7-81)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: update
- GitHub Check: update
🔇 Additional comments (2)
packages/desktop/src/pages/session.tsx (1)
351-355: 6xl breakpoint display change for MessageNav looks consistentSwitching the large-screen class from
@6xl:blockto@6xl:flexaligns the nav with the surrounding flex-based layout and should improve alignment without affecting behavior at smaller breakpoints. I don’t see any functional or layout regressions based on this file alone.packages/opencode/package.json (1)
65-66: Dependency versions verified and properly locked.Both @opentui/core@0.1.50 and @opentui/solid@0.1.50 are valid versions released on npm (2025-11-24) and correctly reflected in bun.lock with all platform-specific binaries. The patch version bump is compatible with existing codebase usage (useKeyboard, useRenderer, useTerminalDimensions, etc.) and satisfies opentui-spinner's peer dependency constraint (^0.1.49).
Note: SelectDialog with onKeyEvent prop was not found in the current codebase, so this may reference planned functionality or a different change.
packages/sdk/js/src/client.ts
Outdated
| const baseUrl = new URL(config.baseUrl) | ||
| if (baseUrl.username || baseUrl.password) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify URL validation upstream or document throwing behavior.
The new URL() constructor will throw a TypeError if config.baseUrl is malformed. Ensure that callers validate the URL format before passing it to this function, or document that this function may throw on invalid input.
Run the following script to check if there's upstream validation:
🏁 Script executed:
#!/bin/bash
# Description: Find all calls to createOpencodeClient and check for URL validation
# Find all calls to createOpencodeClient
rg -nP -A5 -B5 'createOpencodeClient\s*\(' --type=ts --type=jsLength of output: 8241
Looking at the output, I found a critical issue: on line 250 of packages/opencode/src/cli/cmd/run.ts, the baseUrl is set to args.attach which comes from CLI arguments—this appears to be user input that may not be validated. Let me verify if this is validated upstream.<!-- [scratchpad_start]
[task review_file_1/1 review_segment_2/3 btoa_verification]
Verify btoa() Unicode limitation is a real issue.
[observations]
Web search confirms btoa() limitation with Unicode:
- btoa() only accepts Latin-1 characters (0-255 code point range)
- Throws "InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range"
- Affects emoji, accented characters, non-Latin scripts
- MDN, Stack Overflow, and multiple sources confirm this
[analysis] - btoa(username:password) with Unicode characters WILL throw
- Example: username with emoji or accented chars will cause failure
- This is a real security/usability issue that should be flagged
[done]
btoa() limitation confirmed as major issue
[scratchpad_start] -->
Now I need to check the actual code around line 28-29 to see the full context of the btoa() usage:
🏁 Script executed:
#!/bin/bash
# Get the full context around lines 28-38 in packages/sdk/js/src/client.ts
head -40 packages/sdk/js/src/client.ts | tail -20Length of output: 513
Now I need to check if there's validation of the attach CLI argument:
Based on my verification, I have found two issues that need addressing:
Fix btoa() to support Unicode credentials.
The code uses btoa() which will throw a "Character Out Of Range" exception if a character's code point exceeds 0xff. For use cases where you need to encode arbitrary Unicode text, it is necessary to first convert the string to its constituent bytes in UTF-8, and then encode the bytes.
Example failure case: If a URL contains credentials with Unicode characters (emoji, accented characters, non-Latin scripts), the btoa() call at line 32 will throw an uncaught error.
Recommended fix: Use the TextEncoder API to first encode the Unicode string into a sequence of UTF-8 bytes, then convert those bytes into a Base64 string.
// Replace line 32 with:
const utf8Bytes = new TextEncoder().encode(`${baseUrl.username}:${baseUrl.password}`)
const binaryString = String.fromCharCode.apply(null, Array.from(utf8Bytes))
Authorization: `Basic ${btoa(binaryString)}`🤖 Prompt for AI Agents
packages/sdk/js/src/client.ts around lines 28-32: the code uses btoa on
`${baseUrl.username}:${baseUrl.password}` which throws for Unicode characters;
replace that call by encoding the credential string to UTF-8 bytes with
TextEncoder, convert the resulting Uint8Array to a binary string (e.g. by
mapping bytes to String.fromCharCode), then call btoa on that binary string and
set Authorization to `Basic ${...}`; ensure this path only runs when
username/password are present and preserve existing header behavior.
packages/sdk/js/src/client.ts
Outdated
| if (baseUrl.username || baseUrl.password) { | ||
| config.headers = { | ||
| ...config.headers, | ||
| Authorization: `Basic ${btoa(`${baseUrl.username}:${baseUrl.password}`)}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace btoa() with Unicode-safe encoding.
btoa() only supports Latin1 characters and will throw InvalidCharacterError if the username or password contains Unicode characters (e.g., emoji, accented letters, non-Latin scripts). Users with valid Unicode passwords will encounter runtime errors.
Apply this diff to use Unicode-safe encoding:
- Authorization: `Basic ${btoa(`${baseUrl.username}:${baseUrl.password}`)}`,
+ Authorization: `Basic ${Buffer.from(`${baseUrl.username}:${baseUrl.password}`, 'utf-8').toString('base64')}`,Note: If Buffer is not available in the target environment, use:
const credentials = `${baseUrl.username}:${baseUrl.password}`
const encoded = typeof Buffer !== 'undefined'
? Buffer.from(credentials, 'utf-8').toString('base64')
: btoa(unescape(encodeURIComponent(credentials)))🤖 Prompt for AI Agents
In packages/sdk/js/src/client.ts around line 32, the Authorization header
currently uses btoa which fails for Unicode credentials; replace it by first
building credentials = `${baseUrl.username}:${baseUrl.password}` and then
base64-encoding using Buffer when available (typeof Buffer !== 'undefined' ?
Buffer.from(credentials, 'utf-8').toString('base64') :
btoa(unescape(encodeURIComponent(credentials)))) and use that encoded string in
the Authorization header; ensure the typeof Buffer check is used so environments
without Buffer fall back to the Unicode-safe btoa wrapper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (2)
23-54: LGTM! Well-structured ordering and deduplication logic.The state management helpers correctly prioritize the current model and prevent duplicates between favorites and recents lists. The logic is clear and correct.
Optional: Consider shorter variable names per coding guidelines.
The coding guidelines suggest preferring single-word variable names where possible. Variables like
orderedRecents,currentIsFavorite, andorderedRecentListcould be shortened (e.g.,ordered,isFav,recentList), though the current names are more descriptive.
89-120: LGTM! Recent options correctly constructed.The recent options logic mirrors the favorites implementation and correctly excludes items already in favorites.
Optional: Extract shared option-building logic.
The construction logic for
favoriteOptions(lines 55-87) andrecentOptions(lines 89-120) is nearly identical. Consider extracting a helper function to reduce duplication:const buildOptionsFromList = ( list: typeof favorites, category: string, showStar = false ) => { return list.flatMap((item) => { const provider = sync.data.provider.find((x) => x.id === item.providerID) if (!provider) return [] const model = provider.models[item.modelID] if (!model) return [] return [{ value: { providerID: provider.id, modelID: model.id }, title: model.name ?? item.modelID, description: `${provider.name}${showStar ? " ★" : ""}`, category, disabled: provider.id === "opencode" && model.id.includes("-nano"), footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, onSelect: () => { dialog.clear() local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true }) } }] }) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx(6 hunks)packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,jsx,ts,tsx}: DO NOT do unnecessary destructuring of variables
DO NOT useelsestatements unless necessary
DO NOT usetry/catchif it can be avoided
AVOIDtry/catchwhere possible
AVOIDletstatements
PREFER single word variable names where possible
Use as many Bun APIs as possible like Bun.file()
Files:
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
AVOID using
anytype
Files:
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
packages/opencode/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)
Use Bun with TypeScript ESM modules as the runtime environment
Files:
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
packages/opencode/**/*.{ts,tsx}
📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)
packages/opencode/**/*.{ts,tsx}: Use relative imports for local modules with named imports preferred
Use Zod schemas for validation
Use TypeScript interfaces for defining data structures
Use camelCase for variable and function names
Use PascalCase for class and namespace names
Use Result patterns for error handling; avoid throwing exceptions in tools
Organize code using namespace-based structure (e.g.,Tool.define(),Session.create())
PasssessionIDin tool context and useApp.provide()for dependency injection
Validate all inputs with Zod schemas
UseLog.create({ service: "name" })pattern for logging
UseStoragenamespace for data persistence
Files:
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsxpackages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
🧬 Code graph analysis (1)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (1)
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx (1)
filter(149-151)
🔇 Additional comments (4)
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx (1)
240-240: LGTM! Improved spacing in action bar.The increased gap improves visual separation between action items in the bottom bar.
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (3)
4-8: LGTM! Import changes align with new favorites feature.The additional remeda utilities and Keybind import support the new favorites and recents logic.
55-87: LGTM! Favorites section correctly constructed.The favorite options are built with proper filtering, star indicators, and category labeling. The
onSelecthandler correctly updates state and marks as recent.
122-191: LGTM! Provider options correctly filter duplicates and show favorites.The concatenation of
favoriteOptions,recentOptions, and provider options with filtering ensures no duplicates appear in the list. The star indicators and category assignments are consistent throughout.
| { | ||
| keybind: Keybind.parse("ctrl+f")[0], | ||
| title: "Favorite", | ||
| onTrigger: (option) => { | ||
| local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify type safety of option.value.
The type assertion option.value as { providerID: string; modelID: string } could be unsafe if the option's value doesn't match the expected shape.
The DialogSelect component is generic (DialogSelect<T>), and while all options in this dialog appear to have the correct shape, the type assertion bypasses TypeScript's type checking. Consider one of these approaches:
Option 1: Type the DialogSelect with a specific type
- const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
+ type ModelOption = { providerID: string; modelID: string }
+ const [ref, setRef] = createSignal<DialogSelectRef<ModelOption>>()Then update the DialogSelect usage:
- return (
- <DialogSelect
+ return (
+ <DialogSelect<ModelOption>
keybind={[
...
{
keybind: Keybind.parse("ctrl+f")[0],
title: "Favorite",
- onTrigger: (option) => {
- local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
+ onTrigger: (option) => {
+ local.model.toggleFavorite(option.value)
},
},Option 2: Add runtime validation
{
keybind: Keybind.parse("ctrl+f")[0],
title: "Favorite",
onTrigger: (option) => {
+ const value = option.value as any
+ if (typeof value?.providerID === 'string' && typeof value?.modelID === 'string') {
- local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
+ local.model.toggleFavorite(value)
+ }
},
},🤖 Prompt for AI Agents
In packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx around lines 204
to 210, the code unsafely asserts option.value as { providerID: string; modelID:
string }; instead, make the DialogSelect usage type-safe by specifying the
generic type parameter (e.g., DialogSelect<{ providerID: string; modelID: string
}>) so option.value is correctly typed and remove the assertion, or if you
prefer runtime safety, validate option.value before use (check that providerID
and modelID are strings) and handle the unexpected shape (log/error/no-op)
rather than casting.
…rite recents, instead of counting the favorites against the recents if a model is both
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/opencode/src/cli/cmd/tui/context/local.tsx (1)
250-250: Add delimiter in uniqueBy key generation.The uniqueBy predicate concatenates
providerIDandmodelIDwithout a delimiter, which can cause collisions (e.g.,{providerID: "ab", modelID: "cdef"}and{providerID: "abc", modelID: "def"}both produce"abcdef").Apply this diff to use a delimiter at line 250:
- const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID) + const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)And at line 267:
- const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID) + const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)Also applies to: 267-267
🧹 Nitpick comments (2)
packages/opencode/src/cli/cmd/tui/context/local.tsx (2)
228-254: Consider extracting shared recent-update logic.The logic at lines 249-253 (updating model, adding to recent, and saving) duplicates the same logic in the
set()method (lines 265-270). This violates DRY.You could refactor by:
- Extracting the recent-update logic into a shared helper function
- Or having
cycleFavoritecallset(next, { recent: true })after line 248Example refactor (option 2):
const next = favorites[index] if (!next) return - setModelStore("model", agent.current().name, { ...next }) - const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID) - if (uniq.length > 10) uniq.pop() - setModelStore("recent", uniq) - save() + this.set(next, { recent: true })Note: This would add a redundant
isModelValidcheck, sincecycleFavoritealready filters for valid models at line 229. If performance is a concern, option 1 (extracting a helper) would be cleaner.
130-138: Consider awaitingBun.writeor documenting fire-and-forget behavior.The
save()function callsBun.write()without awaiting the returned Promise. This means:
- Write errors will be silently ignored
- The function returns before persistence completes
- Rapid successive calls could theoretically interleave (though the event loop will serialize them)
If fire-and-forget is intentional (given the guidelines to avoid
try/catch), this is acceptable. Otherwise, consider:- function save() { - Bun.write( + async function save() { + await Bun.write( file, JSON.stringify({ recent: modelStore.recent, favorite: modelStore.favorite, }), ) }Then update call sites (lines 253, 270, 291) to await or handle the Promise.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/opencode/src/cli/cmd/tui/context/local.tsx(4 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,jsx,ts,tsx}: DO NOT do unnecessary destructuring of variables
DO NOT useelsestatements unless necessary
DO NOT usetry/catchif it can be avoided
AVOIDtry/catchwhere possible
AVOIDletstatements
PREFER single word variable names where possible
Use as many Bun APIs as possible like Bun.file()
Files:
packages/opencode/src/cli/cmd/tui/context/local.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
AVOID using
anytype
Files:
packages/opencode/src/cli/cmd/tui/context/local.tsx
packages/opencode/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)
Use Bun with TypeScript ESM modules as the runtime environment
Files:
packages/opencode/src/cli/cmd/tui/context/local.tsx
packages/opencode/**/*.{ts,tsx}
📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)
packages/opencode/**/*.{ts,tsx}: Use relative imports for local modules with named imports preferred
Use Zod schemas for validation
Use TypeScript interfaces for defining data structures
Use camelCase for variable and function names
Use PascalCase for class and namespace names
Use Result patterns for error handling; avoid throwing exceptions in tools
Organize code using namespace-based structure (e.g.,Tool.define(),Session.create())
PasssessionIDin tool context and useApp.provide()for dependency injection
Validate all inputs with Zod schemas
UseLog.create({ service: "name" })pattern for logging
UseStoragenamespace for data persistence
Files:
packages/opencode/src/cli/cmd/tui/context/local.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-24T22:47:41.129Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:41.129Z
Learning: Applies to packages/opencode/**/*.{ts,tsx} : Use `Storage` namespace for data persistence
Applied to files:
packages/opencode/src/cli/cmd/tui/context/local.tsx
c6e3cbc to
13b2839
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (1)
204-210: Avoid unsafe cast foroption.value; typeDialogSelectinstead.
onTriggercurrently does:onTrigger: (option) => { local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) }This bypasses type safety. Since
DialogSelectis generic, prefer giving it a concrete value type (e.g.,ModelOption = { providerID: string; modelID: string }) and usingDialogSelect<ModelOption>plusDialogSelectRef<ModelOption>, sooption.valueis correctly typed and you can drop the assertion. This keeps the handler safer and aligns with the existing option construction.Based on learnings, prefer strong typing over casts in TSX.
🧹 Nitpick comments (1)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (1)
22-121: Consider de‑duping only against displayed recents to avoid “hidden” models.In the provider‑model pipeline, the final
filterchecks membership againstorderedRecents:const inRecents = orderedRecents.some( (item) => item.providerID === value.providerID && item.modelID === value.modelID, ) if (inRecents) return falseBut the “Recent” section actually renders only
orderedRecentList(top 5, favorites excluded). As a result, models that are inrecentsbut outside that top 5 disappear from the default dialog view (neither in “Recent” nor in the provider list) and are only reachable once the user types a query.If you just want to de‑duplicate against visibly rendered recents, consider checking against
orderedRecentList(orrecentList) instead oforderedRecents, so older recents still appear under their provider when no query is active.Also applies to: 135-174
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
packages/sdk/js/src/gen/types.gen.tsis excluded by!**/gen/**
📒 Files selected for processing (2)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx(6 hunks)packages/opencode/src/config/config.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,jsx,ts,tsx}: DO NOT do unnecessary destructuring of variables
DO NOT useelsestatements unless necessary
DO NOT usetry/catchif it can be avoided
AVOIDtry/catchwhere possible
AVOIDletstatements
PREFER single word variable names where possible
Use as many Bun APIs as possible like Bun.file()
Files:
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsxpackages/opencode/src/config/config.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
AVOID using
anytype
Files:
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsxpackages/opencode/src/config/config.ts
packages/opencode/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)
Use Bun with TypeScript ESM modules as the runtime environment
Files:
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsxpackages/opencode/src/config/config.ts
packages/opencode/**/*.{ts,tsx}
📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)
packages/opencode/**/*.{ts,tsx}: Use relative imports for local modules with named imports preferred
Use Zod schemas for validation
Use TypeScript interfaces for defining data structures
Use camelCase for variable and function names
Use PascalCase for class and namespace names
Use Result patterns for error handling; avoid throwing exceptions in tools
Organize code using namespace-based structure (e.g.,Tool.define(),Session.create())
PasssessionIDin tool context and useApp.provide()for dependency injection
Validate all inputs with Zod schemas
UseLog.create({ service: "name" })pattern for logging
UseStoragenamespace for data persistence
Files:
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsxpackages/opencode/src/config/config.ts
🧠 Learnings (1)
📚 Learning: 2025-11-24T22:47:17.581Z
Learnt from: CR
Repo: kcrommett/opencode PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T22:47:17.581Z
Learning: Applies to **/*.{ts,tsx} : AVOID using `any` type
Applied to files:
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
🧬 Code graph analysis (1)
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx (1)
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx (1)
filter(149-151)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: update
🔇 Additional comments (1)
packages/opencode/src/config/config.ts (1)
431-432: New favorite‑cycle keybinds align with existing schema pattern.The added
model_cycle_favoriteandmodel_cycle_favorite_reverseentries follow the existing keybind conventions (optional string,"none"default, clear descriptions) and should integrate cleanly with the new TUI commands and SDK updates.
020ee56 to
83c47e0
Compare
Note
Introduce favorite models with persistence, star indicators, favorite cycling keybinds, and in-dialog toggle; update SDK types and docs accordingly.
model_cycle_favorite,model_cycle_favorite_reverseinpackages/opencode/src/cli/cmd/tui/app.tsx.DialogModel(packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx): show separate “Favorites” and “Recent” sections, star-mark favorites, prioritize current recent, and addctrl+fto toggle favorite.packages/opencode/src/cli/cmd/tui/context/local.tsxwithfavorite[]; load/save tostate/model.json; addfavorite(),toggleFavorite(), andcycleFavorite(); persist via sharedsave().model_cycle_favoriteandmodel_cycle_favorite_reverseto schema with default"none"(packages/opencode/src/config/config.ts).model_cycle_favorite*) in generated types.packages/ui/src/components/select-dialog.tsx: addonKeyEventto pass key events and selected item to consumers.packages/web/src/content/docs/keybinds.mdx).Written by Cursor Bugbot for commit fd4d38b. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.