Skip to content

Commit 7d465e5

Browse files
authored
fix: stabilize opencode model selection (#132)
1 parent f5d81b4 commit 7d465e5

16 files changed

Lines changed: 136 additions & 107 deletions

File tree

.github/workflows/docs.yml

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +0,0 @@
1-
name: Deploy Docs
2-
3-
on:
4-
push:
5-
branches:
6-
- main
7-
paths:
8-
- 'docs/**'
9-
- '.github/workflows/docs.yml'
10-
workflow_dispatch:
11-
12-
permissions:
13-
contents: read
14-
pages: write
15-
id-token: write
16-
17-
concurrency:
18-
group: pages
19-
cancel-in-progress: true
20-
21-
jobs:
22-
build:
23-
runs-on: ubuntu-latest
24-
steps:
25-
- uses: actions/checkout@v4
26-
27-
- uses: oven-sh/setup-bun@v2
28-
with:
29-
bun-version: latest
30-
31-
- name: Install dependencies
32-
working-directory: docs
33-
run: bun install --frozen-lockfile
34-
35-
- name: Build
36-
working-directory: docs
37-
run: bun run build
38-
39-
- name: Upload artifact
40-
uses: actions/upload-pages-artifact@v3
41-
with:
42-
path: docs/build
43-
44-
deploy:
45-
environment:
46-
name: github-pages
47-
url: ${{ steps.deployment.outputs.page_url }}
48-
runs-on: ubuntu-latest
49-
needs: build
50-
steps:
51-
- name: Deploy to GitHub Pages
52-
id: deployment
53-
uses: actions/deploy-pages@v4

mobile/src/lib/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface ModelInfo {
8383
id: string;
8484
name: string;
8585
description?: string;
86+
provider?: string;
8687
}
8788

8889
export type AgentType = 'claude-code' | 'opencode' | 'codex';

mobile/src/screens/HomeScreen.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { RepoSelector } from '../components/RepoSelector'
2626

2727
const DELETE_ACTION_WIDTH = 80
2828

29-
function DeleteAction({
29+
function HomeDeleteAction({
3030
drag,
3131
onPress,
3232
color,
@@ -285,7 +285,7 @@ export function HomeScreen() {
285285
friction={2}
286286
rightThreshold={40}
287287
renderRightActions={(_prog, drag, swipeable) => (
288-
<DeleteAction
288+
<HomeDeleteAction
289289
drag={drag}
290290
onPress={() => confirmDeleteWorkspace(item, swipeable.close)}
291291
color={colors.error}

mobile/src/screens/SessionChatScreen.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ export function SessionChatScreen({ route, navigation }: any) {
825825
if (availableModels.length === 0) return
826826
if (isStreaming) return
827827

828-
const options = [...availableModels.map(m => m.name), 'Cancel']
828+
const options = [...availableModels.map(m => (m.provider ? `${m.provider}/${m.name}` : m.name)), 'Cancel']
829829
ActionSheetIOS.showActionSheetWithOptions(
830830
{
831831
options,
@@ -840,9 +840,11 @@ export function SessionChatScreen({ route, navigation }: any) {
840840
if (wsRef.current?.readyState === WebSocket.OPEN) {
841841
wsRef.current.send(JSON.stringify({ type: 'set_model', model: newModel }))
842842
}
843+
const selectedInfo = availableModels[buttonIndex]
844+
const label = selectedInfo.provider ? `${selectedInfo.provider}/${selectedInfo.name}` : selectedInfo.name
843845
setMessages(prev => [...prev, {
844846
role: 'system',
845-
content: `Switching to model: ${availableModels[buttonIndex].name}`,
847+
content: `Switching to model: ${label}`,
846848
id: `msg-model-${Date.now()}`,
847849
}])
848850
}
@@ -851,7 +853,11 @@ export function SessionChatScreen({ route, navigation }: any) {
851853
)
852854
}
853855

854-
const selectedModelName = availableModels.find(m => m.id === selectedModel)?.name || 'Model'
856+
const selectedModelName = (() => {
857+
const info = availableModels.find(m => m.id === selectedModel)
858+
if (!info) return 'Model'
859+
return info.provider ? `${info.provider}/${info.name}` : info.name
860+
})()
855861
const canChangeModel = !isStreaming
856862

857863
const agentLabels: Record<AgentType, string> = {

mobile/src/screens/SettingsScreen.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,12 @@ function ModelPicker({
103103
}) {
104104
const { colors } = useTheme();
105105
const selectedModelInfo = models.find((m) => m.id === selectedModel);
106+
const selectedModelLabel = selectedModelInfo
107+
? (selectedModelInfo.provider ? `${selectedModelInfo.provider}/${selectedModelInfo.name}` : selectedModelInfo.name)
108+
: undefined;
106109

107110
const showPicker = () => {
108-
const options = [...models.map((m) => m.name), 'Cancel'];
111+
const options = [...models.map((m) => (m.provider ? `${m.provider}/${m.name}` : m.name)), 'Cancel'];
109112
ActionSheetIOS.showActionSheetWithOptions(
110113
{
111114
options,
@@ -124,7 +127,7 @@ function ModelPicker({
124127
<View style={styles.row}>
125128
<Text style={[styles.label, { color: colors.textMuted }]}>{label}</Text>
126129
<TouchableOpacity style={[styles.modelPicker, { backgroundColor: colors.surfaceSecondary }]} onPress={showPicker}>
127-
<Text style={[styles.modelPickerText, { color: colors.text }]}>{selectedModelInfo?.name || 'Select Model'}</Text>
130+
<Text style={[styles.modelPickerText, { color: colors.text }]}>{selectedModelLabel || 'Select Model'}</Text>
128131
<Text style={[styles.modelPickerChevron, { color: colors.textMuted }]}></Text>
129132
</TouchableOpacity>
130133
</View>

mobile/src/screens/WorkspaceDetailScreen.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type DateGroup = 'Today' | 'Yesterday' | 'This Week' | 'Older'
2424

2525
const DELETE_ACTION_WIDTH = 80
2626

27-
function DeleteAction({
27+
function WorkspaceDetailDeleteAction({
2828
drag,
2929
onPress,
3030
color,
@@ -416,7 +416,7 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
416416
friction={2}
417417
rightThreshold={40}
418418
renderRightActions={(_prog, drag, swipeable) => (
419-
<DeleteAction
419+
<WorkspaceDetailDeleteAction
420420
drag={drag}
421421
onPress={() => confirmDeleteSession(item.session, swipeable.close)}
422422
color={colors.error}

src/agent/router.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import path from 'path';
66
import type { AgentConfig } from '../shared/types';
77
import { HOST_WORKSPACE_NAME } from '../shared/client-types';
88
import { AnyWorkspaceNameSchema, UserWorkspaceNameSchema } from '../shared/workspace-name';
9+
import type { ModelInfo } from '../models/cache';
910
import { getDockerVersion, execInContainer, getContainerName, type ExecResult } from '../docker';
1011
import { createWorkerClient } from '../worker/client';
1112
import type { WorkspaceManager } from '../workspace/manager';
@@ -37,6 +38,7 @@ import {
3738
discoverClaudeCodeModels,
3839
discoverHostOpencodeModels,
3940
discoverContainerOpencodeModels,
41+
shouldUseCachedOpencodeModels,
4042
} from '../models/discovery';
4143
import { deleteOpencodeSession } from '../sessions/agents/opencode-storage';
4244
import { SessionIndex } from '../worker/session-index';
@@ -540,6 +542,7 @@ export function createRouter(ctx: RouterContext) {
540542
const newConfig = { ...currentConfig, agents: input };
541543
ctx.config.set(newConfig);
542544
await saveAgentConfig(newConfig, ctx.configDir);
545+
await ctx.modelCache.clearCache();
543546
ctx.triggerAutoSync();
544547
return input;
545548
});
@@ -1435,12 +1438,13 @@ export function createRouter(ctx: RouterContext) {
14351438
return { models };
14361439
}
14371440

1441+
const prefersWorkspaceModels = !!config.agents?.opencode?.zen_token;
14381442
const cached = await ctx.modelCache.getOpencodeModels();
1439-
if (cached) {
1443+
if (shouldUseCachedOpencodeModels(cached, prefersWorkspaceModels, input.workspaceName)) {
14401444
return { models: cached };
14411445
}
14421446

1443-
let models;
1447+
let models: ModelInfo[] = [];
14441448
if (input.workspaceName === HOST_WORKSPACE_NAME) {
14451449
if (!config.allowHostAccess) {
14461450
throw new ORPCError('PRECONDITION_FAILED', { message: 'Host access is disabled' });
@@ -1457,14 +1461,27 @@ export function createRouter(ctx: RouterContext) {
14571461
const containerName = `workspace-${input.workspaceName}`;
14581462
models = await discoverContainerOpencodeModels(containerName, execInContainer);
14591463
} else {
1460-
models = await discoverHostOpencodeModels();
1461-
if (models.length === 0) {
1464+
const prefersWorkspaceModels = !!config.agents?.opencode?.zen_token;
1465+
if (prefersWorkspaceModels) {
14621466
const allWorkspaces = await ctx.workspaces.list();
14631467
const runningWorkspace = allWorkspaces.find((w) => w.status === 'running');
14641468
if (runningWorkspace) {
14651469
const containerName = `workspace-${runningWorkspace.name}`;
14661470
models = await discoverContainerOpencodeModels(containerName, execInContainer);
14671471
}
1472+
if (models.length === 0) {
1473+
models = await discoverHostOpencodeModels();
1474+
}
1475+
} else {
1476+
models = await discoverHostOpencodeModels();
1477+
if (models.length === 0) {
1478+
const allWorkspaces = await ctx.workspaces.list();
1479+
const runningWorkspace = allWorkspaces.find((w) => w.status === 'running');
1480+
if (runningWorkspace) {
1481+
const containerName = `workspace-${runningWorkspace.name}`;
1482+
models = await discoverContainerOpencodeModels(containerName, execInContainer);
1483+
}
1484+
}
14681485
}
14691486
}
14701487

src/index.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ async function promptForAgent(): Promise<string> {
204204
return agentHost;
205205
}
206206

207-
async function getClient(timeoutMs?: number) {
207+
async function createClient(timeoutMs?: number) {
208208
const agent = await getAgentWithFallback();
209209
return createApiClient(agent, undefined, timeoutMs);
210210
}
@@ -237,7 +237,7 @@ program
237237
.description('List all workspaces')
238238
.action(async () => {
239239
try {
240-
const client = await getClient();
240+
const client = await createClient();
241241
const workspaces = await client.listWorkspaces();
242242

243243
if (workspaces.length === 0) {
@@ -273,7 +273,7 @@ program
273273
.option('--clone <url>', 'Git repository URL to clone (when creating)')
274274
.action(async (name, options) => {
275275
try {
276-
const client = await getClient();
276+
const client = await createClient();
277277
console.log(`Starting workspace '${name}'...`);
278278

279279
let workspace;
@@ -316,7 +316,7 @@ program
316316
.description('Stop a running workspace')
317317
.action(async (name) => {
318318
try {
319-
const client = await getClient();
319+
const client = await createClient();
320320
console.log(`Stopping workspace '${name}'...`);
321321

322322
const workspace = await client.stopWorkspace(name);
@@ -333,7 +333,7 @@ program
333333
.description('Delete a workspace')
334334
.action(async (name) => {
335335
try {
336-
const client = await getClient();
336+
const client = await createClient();
337337
console.log(`Deleting workspace '${name}'...`);
338338

339339
await client.deleteWorkspace(name);
@@ -349,7 +349,7 @@ program
349349
.description('Clone an existing workspace')
350350
.action(async (source, cloneName) => {
351351
try {
352-
const client = await getClient();
352+
const client = await createClient();
353353
console.log(`Cloning workspace '${source}' to '${cloneName}'...`);
354354
console.log('This may take a while for large workspaces.');
355355

@@ -368,7 +368,7 @@ program
368368
.description('Show workspace or agent info')
369369
.action(async (name) => {
370370
try {
371-
const client = await getClient();
371+
const client = await createClient();
372372

373373
if (name) {
374374
const workspace = await client.getWorkspace(name);
@@ -405,7 +405,7 @@ program
405405
.option('-n, --tail <lines>', 'Number of lines to show', '100')
406406
.action(async (name, options) => {
407407
try {
408-
const client = await getClient();
408+
const client = await createClient();
409409
const logs = await client.getLogs(name, parseInt(options.tail, 10));
410410
console.log(logs);
411411
} catch (err) {
@@ -419,7 +419,7 @@ program
419419
.option('-a, --all', 'Sync all running workspaces')
420420
.action(async (name, options) => {
421421
try {
422-
const client = await getClient(5 * 60 * 1000);
422+
const client = await createClient(5 * 60 * 1000);
423423

424424
if (options.all) {
425425
console.log('Syncing all running workspaces...');
@@ -462,7 +462,7 @@ program
462462
.action(async (name) => {
463463
try {
464464
const agentHost = await getAgentWithFallback();
465-
const client = await getClient();
465+
const client = await createClient();
466466

467467
const workspace = await client.getWorkspace(name);
468468
if (workspace.status !== 'running') {
@@ -508,7 +508,7 @@ program
508508
.action(async (name, ports: string[]) => {
509509
try {
510510
const agentHost = await getAgentWithFallback();
511-
const client = await getClient();
511+
const client = await createClient();
512512

513513
const workspace = await client.getWorkspace(name);
514514
if (workspace.status !== 'running') {
@@ -602,7 +602,7 @@ program
602602
.description('Configure port mappings for a workspace (e.g. 3000, 8080:3000)')
603603
.action(async (name, ports: string[]) => {
604604
try {
605-
const client = await getClient();
605+
const client = await createClient();
606606

607607
const workspace = await client.getWorkspace(name);
608608
if (!workspace) {
@@ -793,7 +793,7 @@ sshCmd
793793
.description('Show current SSH configuration')
794794
.action(async () => {
795795
try {
796-
const client = await getClient();
796+
const client = await createClient();
797797
const ssh = await client.getSSHSettings();
798798

799799
console.log('');
@@ -844,7 +844,7 @@ sshCmd
844844
.description('Toggle auto-authorization of host keys (on/off)')
845845
.action(async (toggle?: string) => {
846846
try {
847-
const client = await getClient();
847+
const client = await createClient();
848848
const settings = await client.getSSHSettings();
849849

850850
if (!toggle) {
@@ -871,7 +871,7 @@ sshCmd
871871
.option('-w, --workspace <name>', 'Apply to specific workspace only')
872872
.action(async (keyPath: string, options: { workspace?: string }) => {
873873
try {
874-
const client = await getClient();
874+
const client = await createClient();
875875
const settings = await client.getSSHSettings();
876876

877877
const normalizedPath = keyPath.replace(/\.pub$/, '');
@@ -907,7 +907,7 @@ sshCmd
907907
.option('-w, --workspace <name>', 'Apply to specific workspace only')
908908
.action(async (keyPath: string, options: { workspace?: string }) => {
909909
try {
910-
const client = await getClient();
910+
const client = await createClient();
911911
const settings = await client.getSSHSettings();
912912

913913
if (options.workspace) {
@@ -947,7 +947,7 @@ sshCmd
947947
options: { workspace?: string; copy?: boolean; authorize?: boolean }
948948
) => {
949949
try {
950-
const client = await getClient();
950+
const client = await createClient();
951951
const settings = await client.getSSHSettings();
952952

953953
const normalizedPath = keyPath.replace(/\.pub$/, '');
@@ -1254,7 +1254,7 @@ function handleError(err: unknown): never {
12541254
console.error(`Error: ${JSON.stringify(err)}`);
12551255
}
12561256
} else if (err !== undefined && err !== null) {
1257-
console.error(`Error: ${String(err)}`);
1257+
console.error('Error:', err);
12581258
} else {
12591259
console.error('An unknown error occurred');
12601260
}

0 commit comments

Comments
 (0)