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)
6.5 KiB
6.5 KiB
Task 005: Session Picker Modal
Goal
Create the session picker modal that appears when an instance starts, allowing users to resume an existing session or create a new one.
Prerequisites
- Task 004 completed (SDK integration, session fetching)
- Understanding of modal/dialog patterns
- Kobalte UI primitives knowledge
Acceptance Criteria
- Modal appears after instance becomes ready
- Displays list of existing sessions
- Shows session metadata (title, timestamp)
- Allows creating new session with agent selection
- Can close modal (cancels instance creation)
- Keyboard navigation works (up/down, enter)
- Properly styled and accessible
- Loading states during fetch
Steps
1. Create Session Picker Component
src/components/session-picker.tsx:
Props:
interface SessionPickerProps {
instanceId: string
open: boolean
onClose: () => void
onSessionSelect: (sessionId: string) => void
onNewSession: (agent: string) => void
}
Structure:
- Modal backdrop (semi-transparent overlay)
- Modal dialog (centered card)
- Header: "OpenCode • {folder}"
- Section 1: Resume session list
- Separator: "or"
- Section 2: Create new session
- Footer: Cancel button
2. Use Kobalte Dialog
Implementation approach:
import { Dialog } from '@kobalte/core'
<Dialog.Root open={props.open} onOpenChange={(open) => !open && props.onClose()}>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
{/* Modal content */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
Styling:
- Overlay: Dark background, 50% opacity
- Content: White card, max-width 500px, centered
- Padding: 24px
- Border radius: 8px
- Shadow: Large elevation
3. Create Session List Section
Resume Section:
- Header: "Resume a session:"
- List of sessions (max 10 recent)
- Each item shows:
- Title (truncated at 50 chars)
- Relative timestamp ("2h ago")
- Hover state
- Active selection state
Session Item Component:
interface SessionItemProps {
session: Session
selected: boolean
onClick: () => void
}
Empty state:
- Show when no sessions exist
- Text: "No previous sessions"
- Muted styling
Scrollable:
- If >5 sessions, add scroll
- Max height: 300px
4. Create New Session Section
Structure:
- Header: "Start new session:"
- Agent selector dropdown
- "Start" button
Agent Selector:
- Dropdown using Kobalte Select
- Shows agent name
- Grouped by category if applicable
- Default: "Build" agent
Start Button:
- Primary button style
- Click triggers onNewSession callback
- Disabled while creating
5. Add Loading States
While fetching sessions:
- Show skeleton list (3-4 placeholder items)
- Shimmer animation
While fetching agents:
- Agent dropdown shows "Loading..."
- Disabled state
While creating session:
- Start button shows spinner
- Disabled state
- Text changes to "Creating..."
6. Wire Up to Instance Store
Show modal after instance ready:
src/stores/ui.ts additions:
interface UIStore {
sessionPickerInstance: string | null
}
function showSessionPicker(instanceId: string) {
sessionPickerInstance = instanceId
}
function hideSessionPicker() {
sessionPickerInstance = null
}
src/stores/instances.ts updates:
async function createInstance(folder: string) {
// ... spawn and connect ...
// Show session picker
showSessionPicker(id)
return id
}
7. Handle Session Selection
Resume session:
function handleSessionSelect(sessionId: string) {
setActiveSession(instanceId, sessionId)
hideSessionPicker()
// Will trigger session display in Task 006
}
Create new session:
async function handleNewSession(agent: string) {
try {
const session = await createSession(instanceId, agent)
setActiveSession(instanceId, session.id)
hideSessionPicker()
} catch (error) {
// Show error toast (Task 010)
console.error("Failed to create session:", error)
}
}
8. Handle Cancel
Close modal:
function handleClose() {
// Remove instance since user cancelled
await removeInstance(instanceId)
hideSessionPicker()
}
Confirmation if needed:
- If server already started, ask "Stop server?"
- Otherwise, just close
9. Add Keyboard Navigation
Keyboard shortcuts:
- Up/Down: Navigate session list
- Enter: Select highlighted session
- Escape: Close modal (cancel)
- Tab: Cycle through sections
Implement focus management:
- Auto-focus first session on open
- Trap focus within modal
- Restore focus on close
10. Add Accessibility
ARIA attributes:
role="dialog"aria-labelledby="dialog-title"aria-describedby="dialog-description"aria-modal="true"
Screen reader support:
- Announce "X sessions available"
- Announce selection changes
- Clear focus indicators
11. Style Refinements
Light/Dark mode:
- Test in both themes
- Ensure contrast meets WCAG AA
- Use CSS variables for colors
Responsive:
- Works at minimum window size
- Mobile-friendly (future web version)
- Scales text appropriately
Animations:
- Fade in backdrop (200ms)
- Scale in content (200ms)
- Smooth transitions on hover
12. Update App Component
src/App.tsx:
Render session picker:
<Show when={ui.sessionPickerInstance}>
{(instanceId) => (
<SessionPicker
instanceId={instanceId()}
open={true}
onClose={() => ui.hideSessionPicker()}
onSessionSelect={(id) => handleSessionSelect(instanceId(), id)}
onNewSession={(agent) => handleNewSession(instanceId(), agent)}
/>
)}
</Show>
Testing Checklist
Manual Tests:
- Create instance → Modal appears
- Shows session list if sessions exist
- Shows empty state if no sessions
- Click session → Modal closes, session activates
- Select agent, click Start → New session created
- Press Escape → Modal closes, instance removed
- Keyboard navigation works
- Screen reader announces content
Edge Cases:
- No sessions + no agents (error state)
- Very long session titles (truncate)
- Many sessions (scroll works)
- Create session fails (error shown)
- Slow network (loading states)
Dependencies
- Blocks: Task 006 (needs active session)
- Blocked by: Task 004 (needs session data)
Estimated Time
3-4 hours
Notes
- Keep modal simple and focused
- Clear call-to-action
- Don't overwhelm with options
- Loading states crucial for UX
- Consider adding search if >20 sessions (future)