restore: recover deleted documentation, CI/CD, and infrastructure files
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)
This commit is contained in:
504
tasks/done/004-sdk-integration.md
Normal file
504
tasks/done/004-sdk-integration.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# 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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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):**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
1. Create instance → Sessions fetched automatically
|
||||
2. Console shows session list
|
||||
3. Create new session → Appears in list
|
||||
4. Delete session → Removed from list
|
||||
5. Network fails → Error message shown
|
||||
6. 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
|
||||
Reference in New Issue
Block a user