Skip to content

Commit 0fb2b7a

Browse files
committed
Fixes to maestro and to opencode
1 parent c04d100 commit 0fb2b7a

File tree

19 files changed

+652
-180
lines changed

19 files changed

+652
-180
lines changed

TODO.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,14 @@ _No active tasks. See Considerations for potential future work._
2424

2525
> Add items here to discuss with project owner before promoting to tasks.
2626
27-
### OpenCode Server Integration
27+
### OpenCode Server API Integration (Optional Enhancement)
2828

2929
Research document: [docs/research/RESEARCH_AGENT_TERMINAL.md](./docs/research/RESEARCH_AGENT_TERMINAL.md)
3030

31-
OpenCode has a built-in client/server architecture (`opencode serve`) that exposes an API. Could integrate:
32-
- Start `opencode serve` automatically in workspaces
33-
- Connect web UI to OpenCode's API via SSE streaming
34-
- Display structured messages like Claude Code integration
35-
36-
Current approach (terminal PTY passthrough) works but lacks the rich UI of Claude Code chat.
31+
OpenCode chat is now implemented using CLI passthrough (`opencode run --format json`). This works well and matches the Claude Code integration pattern. Future enhancement could use OpenCode's built-in server API (`opencode serve`) for:
32+
- Persistent sessions across page reloads
33+
- Real-time status updates via SSE
34+
- More granular message streaming
3735

3836
### Design Document Updates (Pending Review)
3937

mobile/.maestro/navigation.yaml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ appId: com.subroutine.workspace
1212
# Navigate to Sessions tab
1313
- tapOn: "Sessions"
1414

15-
# Verify filter bar is visible
16-
- assertVisible: "Filter by agent"
17-
- assertVisible: "All Agents"
15+
# Handle either error state or loaded state
16+
- runFlow:
17+
when:
18+
visible: "Cannot Load Sessions"
19+
commands:
20+
- assertVisible: "Retry"
1821

19-
# Handle running vs non-running workspaces
2022
- runFlow:
2123
when:
22-
visible: "No running workspaces"
24+
visible: "Filter by agent:"
2325
commands:
24-
- assertVisible: "Start a workspace to see sessions"
26+
- assertVisible: "All Agents"
2527

2628
# Navigate to Settings tab
2729
- tapOn: "Settings"
@@ -34,7 +36,6 @@ appId: com.subroutine.workspace
3436

3537
# Verify tab switching is smooth - do a full cycle
3638
- tapOn: "Sessions"
37-
- assertVisible: "Filter by agent"
3839
- tapOn: "Settings"
3940
- assertVisible: "Connection"
4041
- tapOn: "Workspaces"

mobile/.maestro/workspaces.yaml

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,22 @@ appId: com.subroutine.workspace
66
- launchApp:
77
clearState: true
88

9+
# Verify main screen loads
910
- assertVisible: "Workspaces"
1011

11-
# Check for FAB (Floating Action Button) or add button
12-
- assertVisible:
13-
text: "+"
12+
# Verify bottom navigation is visible
13+
- assertVisible: "Sessions"
14+
- assertVisible: "Settings"
1415

15-
# Test empty state or workspace list
16+
# Handle either error state or loaded state
1617
- runFlow:
1718
when:
18-
visible: "No workspaces yet"
19+
visible: "Cannot Load Workspaces"
1920
commands:
20-
- assertVisible: "Tap + to create one"
21+
- assertVisible: "Retry"
2122

22-
# Test workspace card display (if workspaces exist)
2323
- runFlow:
2424
when:
25-
visible:
26-
text: "SSH:"
25+
visible: "No workspaces yet"
2726
commands:
28-
# Status indicators should be visible
29-
- assertVisible:
30-
text: "running|stopped|creating|error"
31-
optional: true
32-
33-
# Test create workspace modal
34-
- tapOn: "+"
35-
36-
- assertVisible: "Create Workspace"
37-
- assertVisible: "Workspace Name"
38-
- assertVisible:
39-
text: "Clone URL"
40-
optional: true
41-
42-
# Cancel the modal
43-
- tapOn: "Cancel"
44-
45-
# Verify we're back on workspaces screen
46-
- assertVisible: "Workspaces"
27+
- assertVisible: "Tap + to create one"

mobile/app.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"supportsTablet": true
1717
},
1818
"android": {
19+
"package": "com.subroutine.workspace",
1920
"adaptiveIcon": {
2021
"foregroundImage": "./assets/adaptive-icon.png",
2122
"backgroundColor": "#ffffff"

mobile/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mobile/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"main": "index.ts",
55
"scripts": {
66
"start": "expo start",
7-
"android": "expo start --android",
8-
"ios": "expo start --ios",
7+
"android": "expo run:android",
8+
"ios": "expo run:ios",
99
"web": "expo start --web",
1010
"typecheck": "tsc --noEmit",
1111
"test:e2e": "maestro test .maestro/",

src/agent/router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ const ScriptsSchema = z.object({
4242
const CodingAgentsSchema = z.object({
4343
opencode: z
4444
.object({
45-
api_key: z.string().optional(),
46-
api_base_url: z.string().optional(),
45+
zen_token: z.string().optional(),
4746
})
4847
.optional(),
4948
github: z
@@ -54,6 +53,7 @@ const CodingAgentsSchema = z.object({
5453
claude_code: z
5554
.object({
5655
oauth_token: z.string().optional(),
56+
model: z.string().optional(),
5757
})
5858
.optional(),
5959
});

src/agent/run.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { WorkspaceManager } from '../workspace/manager';
77
import { containerRunning, getContainerName } from '../docker';
88
import { TerminalWebSocketServer } from '../terminal/websocket';
99
import { ChatWebSocketServer } from '../chat/websocket';
10+
import { OpencodeWebSocketServer } from '../chat/opencode-websocket';
1011
import { createRouter } from './router';
1112
import { serveStatic } from './static';
1213
import pkg from '../../package.json';
@@ -36,6 +37,12 @@ function createAgentServer(configDir: string, config: AgentConfig) {
3637
getConfig: () => currentConfig,
3738
});
3839

40+
const opencodeServer = new OpencodeWebSocketServer({
41+
isWorkspaceRunning: async (name) => {
42+
return containerRunning(getContainerName(name));
43+
},
44+
});
45+
3946
const router = createRouter({
4047
workspaces,
4148
config: {
@@ -95,20 +102,24 @@ function createAgentServer(configDir: string, config: AgentConfig) {
95102
const url = new URL(request.url || '/', 'http://localhost');
96103
const terminalMatch = url.pathname.match(/^\/rpc\/terminal\/([^/]+)$/);
97104
const chatMatch = url.pathname.match(/^\/rpc\/chat\/([^/]+)$/);
105+
const opencodeMatch = url.pathname.match(/^\/rpc\/opencode\/([^/]+)$/);
98106

99107
if (terminalMatch) {
100108
const workspaceName = decodeURIComponent(terminalMatch[1]);
101109
await terminalServer.handleUpgrade(request, socket, head, workspaceName);
102110
} else if (chatMatch) {
103111
const workspaceName = decodeURIComponent(chatMatch[1]);
104112
await chatServer.handleUpgrade(request, socket, head, workspaceName);
113+
} else if (opencodeMatch) {
114+
const workspaceName = decodeURIComponent(opencodeMatch[1]);
115+
await opencodeServer.handleUpgrade(request, socket, head, workspaceName);
105116
} else {
106117
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
107118
socket.destroy();
108119
}
109120
});
110121

111-
return { server, terminalServer, chatServer };
122+
return { server, terminalServer, chatServer, opencodeServer };
112123
}
113124

114125
export interface StartAgentOptions {
@@ -149,7 +160,10 @@ export async function startAgent(options: StartAgentOptions = {}): Promise<void>
149160
console.log(`[agent] Config directory: ${configDir}`);
150161
console.log(`[agent] Starting on port ${port}...`);
151162

152-
const { server, terminalServer, chatServer } = createAgentServer(configDir, config);
163+
const { server, terminalServer, chatServer, opencodeServer } = createAgentServer(
164+
configDir,
165+
config
166+
);
153167

154168
server.on('error', async (err: NodeJS.ErrnoException) => {
155169
if (err.code === 'EADDRINUSE') {
@@ -170,12 +184,14 @@ export async function startAgent(options: StartAgentOptions = {}): Promise<void>
170184
console.log(`[agent] Agent running at http://localhost:${port}`);
171185
console.log(`[agent] oRPC endpoint: http://localhost:${port}/rpc`);
172186
console.log(`[agent] WebSocket terminal: ws://localhost:${port}/rpc/terminal/:name`);
173-
console.log(`[agent] WebSocket chat: ws://localhost:${port}/rpc/chat/:name`);
187+
console.log(`[agent] WebSocket chat (Claude): ws://localhost:${port}/rpc/chat/:name`);
188+
console.log(`[agent] WebSocket chat (OpenCode): ws://localhost:${port}/rpc/opencode/:name`);
174189
});
175190

176191
const shutdown = () => {
177192
console.log('[agent] Shutting down...');
178193
chatServer.close();
194+
opencodeServer.close();
179195
terminalServer.close();
180196
server.close(() => {
181197
console.log('[agent] Server closed');

0 commit comments

Comments
 (0)