Files
NomadArch/tasks/done/005-session-picker-modal.md
Gemini AI 157449a9ad 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)
2025-12-23 13:03:48 +04:00

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:

  1. Create instance → Modal appears
  2. Shows session list if sessions exist
  3. Shows empty state if no sessions
  4. Click session → Modal closes, session activates
  5. Select agent, click Start → New session created
  6. Press Escape → Modal closes, instance removed
  7. Keyboard navigation works
  8. 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)