Restored from origin/main (b4663fb): - .github/ workflows and issue templates - .gitignore (proper exclusions) - .opencode/agent/web_developer.md - AGENTS.md, BUILD.md, PROGRESS.md - dev-docs/ (9 architecture/implementation docs) - docs/screenshots/ (4 UI screenshots) - images/ (CodeNomad icons) - package-lock.json (dependency lockfile) - tasks/ (25+ project task files) Also restored original source files that were modified: - packages/ui/src/App.tsx - packages/ui/src/lib/logger.ts - packages/ui/src/stores/instances.ts - packages/server/src/server/routes/workspaces.ts - packages/server/src/workspaces/manager.ts - packages/server/src/workspaces/runtime.ts - packages/server/package.json Kept new additions: - Install-*.bat/.sh (enhanced installers) - Launch-*.bat/.sh (new launchers) - README.md (SEO optimized with GLM 4.7)
10 KiB
10 KiB
Task 004: SDK Client Integration & Session Management
Goal
Integrate the OpenCode SDK to communicate with running servers, fetch session lists, and manage session lifecycle.
Prerequisites
- Task 003 completed (server spawning works)
- OpenCode SDK package available
- Understanding of HTTP/REST APIs
- Understanding of SolidJS reactivity
Acceptance Criteria
- SDK client created per instance
- Can fetch session list from server
- Can create new session
- Can get session details
- Can delete session
- Client lifecycle tied to instance lifecycle
- Error handling for network failures
- Proper TypeScript types throughout
Steps
1. Create SDK Manager Module
src/lib/sdk-manager.ts:
Purpose:
- Manage SDK client instances
- One client per server (per port)
- Create, retrieve, destroy clients
Interface:
interface SDKManager {
createClient(port: number): OpenCodeClient
getClient(port: number): OpenCodeClient | null
destroyClient(port: number): void
destroyAll(): void
}
Implementation details:
- Store clients in Map<port, client>
- Create client with base URL:
http://localhost:${port} - Handle client creation errors
- Clean up on destroy
2. Update Instance Store
src/stores/instances.ts additions:
Add client to Instance:
interface Instance {
// ... existing fields
client: OpenCodeClient | null
}
Update createInstance:
- After server spawns successfully
- Create SDK client for that port
- Store in instance.client
- Handle client creation errors
Update removeInstance:
- Destroy SDK client before removing
- Call sdkManager.destroyClient(port)
3. Create Session Store
src/stores/sessions.ts:
State structure:
interface Session {
id: string
instanceId: string
title: string
parentId: string | null
agent: string
model: {
providerId: string
modelId: string
}
time: {
created: number
updated: number
}
}
interface SessionStore {
// Sessions grouped by instance
sessions: Map<string, Map<string, Session>>
// Active session per instance
activeSessionId: Map<string, string>
}
Core actions:
// Fetch all sessions for an instance
async function fetchSessions(instanceId: string): Promise<void>
// Create new session
async function createSession(instanceId: string, agent: string): Promise<Session>
// Delete session
async function deleteSession(instanceId: string, sessionId: string): Promise<void>
// Set active session
function setActiveSession(instanceId: string, sessionId: string): void
// Get active session
function getActiveSession(instanceId: string): Session | null
// Get all sessions for instance
function getSessions(instanceId: string): Session[]
4. Implement Session Fetching
fetchSessions implementation:
async function fetchSessions(instanceId: string) {
const instance = instances.get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
try {
const response = await instance.client.session.list()
// Convert API response to Session objects
const sessionMap = new Map<string, Session>()
for (const apiSession of response.data) {
sessionMap.set(apiSession.id, {
id: apiSession.id,
instanceId,
title: apiSession.title || "Untitled",
parentId: apiSession.parentId || null,
agent: "", // Will be populated from messages
model: { providerId: "", modelId: "" },
time: {
created: apiSession.time.created,
updated: apiSession.time.updated,
},
})
}
sessions.set(instanceId, sessionMap)
} catch (error) {
console.error("Failed to fetch sessions:", error)
throw error
}
}
5. Implement Session Creation
createSession implementation:
async function createSession(instanceId: string, agent: string): Promise<Session> {
const instance = instances.get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
try {
const response = await instance.client.session.create({
// OpenCode API might need specific params
})
const session: Session = {
id: response.data.id,
instanceId,
title: "New Session",
parentId: null,
agent,
model: { providerId: "", modelId: "" },
time: {
created: Date.now(),
updated: Date.now(),
},
}
// Add to store
const instanceSessions = sessions.get(instanceId) || new Map()
instanceSessions.set(session.id, session)
sessions.set(instanceId, instanceSessions)
return session
} catch (error) {
console.error("Failed to create session:", error)
throw error
}
}
6. Implement Session Deletion
deleteSession implementation:
async function deleteSession(instanceId: string, sessionId: string): Promise<void> {
const instance = instances.get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
try {
await instance.client.session.delete({ path: { id: sessionId } })
// Remove from store
const instanceSessions = sessions.get(instanceId)
if (instanceSessions) {
instanceSessions.delete(sessionId)
}
// Clear active if it was active
if (activeSessionId.get(instanceId) === sessionId) {
activeSessionId.delete(instanceId)
}
} catch (error) {
console.error("Failed to delete session:", error)
throw error
}
}
7. Implement Agent & Model Fetching
Fetch available agents:
interface Agent {
name: string
description: string
mode: string
}
async function fetchAgents(instanceId: string): Promise<Agent[]> {
const instance = instances.get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
try {
const response = await instance.client.agent.list()
return response.data.filter((agent) => agent.mode !== "subagent")
} catch (error) {
console.error("Failed to fetch agents:", error)
return []
}
}
Fetch available models:
interface Provider {
id: string
name: string
models: Model[]
}
interface Model {
id: string
name: string
providerId: string
}
async function fetchProviders(instanceId: string): Promise<Provider[]> {
const instance = instances.get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
try {
const response = await instance.client.config.providers()
return response.data.providers.map((provider) => ({
id: provider.id,
name: provider.name,
models: Object.entries(provider.models).map(([id, model]) => ({
id,
name: model.name,
providerId: provider.id,
})),
}))
} catch (error) {
console.error("Failed to fetch providers:", error)
return []
}
}
8. Add Error Handling
Network error handling:
function handleSDKError(error: any): string {
if (error.code === "ECONNREFUSED") {
return "Cannot connect to server. Is it running?"
}
if (error.code === "ETIMEDOUT") {
return "Request timed out. Please try again."
}
if (error.response?.status === 404) {
return "Resource not found"
}
if (error.response?.status === 500) {
return "Server error. Check logs."
}
return error.message || "Unknown error occurred"
}
Retry logic (for transient failures):
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3, delay = 1000): Promise<T> {
let lastError
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error
if (i < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
throw lastError
}
9. Add Loading States
Track loading states:
interface LoadingState {
fetchingSessions: Map<string, boolean>
creatingSession: Map<string, boolean>
deletingSession: Map<string, Set<string>>
}
const loading: LoadingState = {
fetchingSessions: new Map(),
creatingSession: new Map(),
deletingSession: new Map(),
}
Use in actions:
async function fetchSessions(instanceId: string) {
loading.fetchingSessions.set(instanceId, true)
try {
// ... fetch logic
} finally {
loading.fetchingSessions.set(instanceId, false)
}
}
10. Wire Up to Instance Creation
src/stores/instances.ts updates:
After server ready:
async function createInstance(folder: string) {
// ... spawn server ...
// Create SDK client
const client = sdkManager.createClient(port)
// Update instance
instances.set(id, {
...instances.get(id)!,
port,
pid,
client,
status: "ready",
})
// Fetch initial data
try {
await fetchSessions(id)
await fetchAgents(id)
await fetchProviders(id)
} catch (error) {
console.error("Failed to fetch initial data:", error)
// Don't fail instance creation, just log
}
return id
}
11. Add Type Safety
src/types/session.ts:
export interface Session {
id: string
instanceId: string
title: string
parentId: string | null
agent: string
model: {
providerId: string
modelId: string
}
time: {
created: number
updated: number
}
}
export interface Agent {
name: string
description: string
mode: string
}
export interface Provider {
id: string
name: string
models: Model[]
}
export interface Model {
id: string
name: string
providerId: string
}
Testing Checklist
Manual Tests:
- Create instance → Sessions fetched automatically
- Console shows session list
- Create new session → Appears in list
- Delete session → Removed from list
- Network fails → Error message shown
- Server not running → Graceful error
Error Cases:
- Server not responding (ECONNREFUSED)
- Request timeout
- 404 on session endpoint
- 500 server error
- Invalid session ID
Edge Cases:
- No sessions exist (empty list)
- Many sessions (100+)
- Session with very long title
- Parent-child session relationships
Dependencies
- Blocks: Task 005 (needs session data)
- Blocked by: Task 003 (needs running server)
Estimated Time
3-4 hours
Notes
- Keep SDK calls isolated in store actions
- All SDK calls should have error handling
- Consider caching to reduce API calls
- Log all API calls for debugging
- Handle slow connections gracefully