Skip to content

Conversation

@bborn
Copy link
Owner

@bborn bborn commented Jan 14, 2026

Summary

  • Adds complete web-based UI for taskyou with per-user isolated Fly Sprite VMs
  • Each user gets their own VM with SQLite DB - host never sees user task data
  • React + TypeScript + TailwindCSS + Shadcn/ui frontend with full TUI feature parity

Architecture

┌─────────────────┐     ┌────────────────────────────┐
│   Browser       │────▶│     taskweb (Host)         │
└─────────────────┘     │  - OAuth (Google/GitHub)   │
                        │  - Session management      │
                        │  - Sprite orchestration    │
                        └────────────┬───────────────┘
                                     │ Sprites API
              ┌──────────────────────┼──────────────┐
              ▼                      ▼              ▼
       ┌────────────┐        ┌────────────┐  ┌────────────┐
       │  Sprite A  │        │  Sprite B  │  │  Sprite C  │
       │  webapi    │        │  webapi    │  │  webapi    │
       │  SQLite    │        │  SQLite    │  │  SQLite    │
       └────────────┘        └────────────┘  └────────────┘

Backend Components

  • cmd/taskweb/: Host web service (OAuth, sprites orchestration)
  • cmd/taskweb-dev/: Local development server (bypasses OAuth)
  • internal/hostdb/: Host database (users, OAuth accounts, sprites, sessions)
  • internal/auth/: OAuth providers (Google, GitHub) and session management
  • internal/sprite/: Fly Sprites SDK integration
  • internal/webserver/: Host HTTP server
  • internal/webapi/: HTTP API running inside each sprite

Frontend Features

  • 4-column Kanban board (Backlog, In Progress, Blocked, Done)
  • Task cards with status badges, project tags, action buttons
  • Task detail panel with live execution logs
  • Settings page (theme, defaults, project management)
  • OAuth login page (Google, GitHub)

Test plan

  • Run cd web && npm install && npm run dev - frontend starts on :5173
  • Run make dev-web - API server starts on :8081
  • Open http://localhost:5173 - dashboard loads with tasks
  • Click task card - detail panel opens
  • Click Settings gear - settings page loads
  • Verify projects list displays correctly

🤖 Generated with Claude Code

bborn and others added 4 commits January 14, 2026 06:09
This PR adds a complete web-based UI for taskyou with the following architecture:

## Architecture
- **Host (taskweb)**: Handles OAuth authentication, session management, and Fly Sprites orchestration
- **Sprite (webapi)**: HTTP API that runs inside each user's Fly Sprite VM
- **Frontend (web)**: React + TypeScript + TailwindCSS + Shadcn/ui

Each user gets their own isolated Fly Sprite VM with SQLite DB - the host never sees user task data.

## Backend Components
- `cmd/taskweb/main.go`: Host web service entry point
- `cmd/taskweb-dev/main.go`: Local development server (bypasses OAuth)
- `internal/hostdb/`: Host database for users, OAuth accounts, sprites, sessions
- `internal/auth/`: OAuth providers (Google, GitHub) and session management
- `internal/sprite/`: Fly Sprites SDK integration for VM management
- `internal/webserver/`: Host HTTP server with OAuth endpoints
- `internal/webapi/`: HTTP/WebSocket API that runs inside sprites

## Frontend Components
- Kanban board with 4 columns (Backlog, In Progress, Blocked, Done)
- Task cards with status badges, project tags, action buttons
- Task detail panel with live execution logs
- Settings page with theme, defaults, and project management
- New task dialog
- OAuth login page

## Development
Run locally with:
```bash
cd web && npm install && npm run dev  # Frontend on :5173
make dev-web                           # API server on :8081
```

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused getSessionFromContext function
- Remove unused parseJSON function
- Remove unused getProviderFromPath function
- Remove unnecessary f.Stat() call

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add framer-motion for smooth animations throughout
- Create Command Palette (Cmd+K) for quick task search and navigation
- Enhance TaskCard with status animations, glow effects, and better visual hierarchy
- Redesign TaskBoard with staggered animations and improved layout
- Build comprehensive TaskDetail panel with:
  - Inline task editing
  - Retry with feedback dialog
  - Expandable execution logs with syntax highlighting
  - Status badges and metadata display
- Improve Login page with split layout and feature highlights
- Redesign Settings page with card-based sections and animations
- Add global keyboard shortcuts (N: new task, R: refresh, S: settings)
- Implement status-based color system with CSS variables
- Add glassmorphism, glow effects, and gradient accents
- Create smooth scrollbars and better responsive design

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CORS middleware now uses actual request Origin header for dev mode
- Add Hijack() method to responseWriter for WebSocket support
- Add fallback status icon for unknown task statuses in CommandPalette

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@openhands-ai
Copy link

openhands-ai bot commented Jan 20, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • CI

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #160 at branch `task/348-build-a-web-based-ui-for-taskyou`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

@bborn
Copy link
Owner Author

bborn commented Jan 26, 2026

Terminal Streaming for Executor Panes

To let users see and interact with the executor in the browser, we should use ttyd - a C-based terminal-over-websocket server.

Why ttyd?

  • Single binary (~5MB memory footprint) - ideal for Sprites
  • Built on libwebsockets + xterm.js - very low latency
  • Battle-tested (8+ years, widely deployed)
  • Supports read-only mode, SSL, and shared sessions

Implementation Approach

1. Add ttyd to each Sprite

In the Sprite Dockerfile or startup script:

# Install ttyd (or include binary in image)
apt-get install -y ttyd

# Start ttyd attached to the executor tmux session
ttyd -W -p 7681 tmux attach -t executor &

Flags:

  • -W = writable (interactive). Omit for read-only viewing
  • -p 7681 = port to serve on

2. Expose via webapi or directly

Option A: Proxy through the existing webapi in each sprite
Option B: Expose port 7681 directly from the sprite and connect from frontend

3. Frontend Integration

Either embed as iframe:

<iframe src={`https://${spriteHost}:7681`} />

Or connect xterm.js directly to ttyd's websocket for more control:

import { Terminal } from 'xterm';
import { AttachAddon } from 'xterm-addon-attach';

const term = new Terminal();
const ws = new WebSocket(`wss://${spriteHost}:7681/ws`);
term.loadAddon(new AttachAddon(ws));
term.open(containerRef.current);

Resources

bborn and others added 2 commits January 26, 2026 16:36
- Add terminal API endpoints (GET/DELETE /tasks/{id}/terminal)
- Create ttyd manager to spawn terminals attached to task tmux sessions
- Add TaskTerminal component using xterm.js for browser-based terminal
- Integrate live terminal section in TaskDetail for running tasks
- Update golangci-lint config to v2 format and suppress style warnings
- Fix WebSocket hijacker support in logging middleware

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Merge golangci.yml configs, keeping comments and exclusions
- Add UpdateTaskSchedule method to db package for webapi compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
bborn added a commit that referenced this pull request Jan 27, 2026
Review the new Sprites exec API and document how it can improve
the existing sprites integration in PR #103 and PR #160.

Key findings:
- Exec sessions replace tmux entirely (persistent, reconnectable)
- Filesystem API replaces shell-based file operations
- Services API replaces nohup+polling for long-running processes
- Network Policy API enables security restrictions
- Port notifications enable dev server URL forwarding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@bborn
Copy link
Owner Author

bborn commented Jan 27, 2026

Sprites Exec API — opportunities for this PR

I reviewed the new Sprites exec API (docs.sprites.dev/api/dev-latest/exec/) and the sprites-go SDK source. Several features would improve this PR's sprite management and proxy layers. Here's a detailed breakdown.


1. Use Services API instead of nohup + polling

Current approach (sprite/manager.go:100-130):

// Start taskd with nohup in background
m.client.RunCommandCombined(ctx, spriteName,
    "sh", []string{"-c", "nohup /usr/local/bin/taskd --web-api --addr :8080 > /var/log/taskd.log 2>&1 &"})

// Poll for readiness (up to 30 seconds)
for i := 0; i < 30; i++ {
    output, err := m.client.RunCommand(ctx, spriteName,
        "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080/health")
    if string(output) == "200" {
        return nil
    }
    time.Sleep(time.Second)
}

With Services API:

// Create a managed service
service, _ := client.CreateService(ctx, spriteName, &sprites.ServiceConfig{
    Name:    "webapi",
    Command: "/usr/local/bin/taskd --web-api --addr :8080",
})

// Start it (auto-restarts on crash)
client.StartService(ctx, spriteName, service.ID)

// Stream logs
stream, _ := client.GetServiceLogs(ctx, spriteName, service.ID)
stream.ProcessAll(func(event *sprites.ServiceLogEvent) error {
    // Monitor startup, errors, etc.
    return nil
})

Benefits:

  • Auto-restart on crash (no manual recovery)
  • Proper log management via GET /v1/services/{id}/logs
  • Clean start/stop lifecycle (POST /start, /stop)
  • No polling for readiness — services have state
  • No curl dependency for health checks

2. Replace RunCommand status checks with real sprite status

Current approach (sprite/client.go:100-115):

func (c *Client) GetSpriteStatus(ctx context.Context, name string) (string, error) {
    // Run "echo ok" to check if sprite is accessible
    sprite := c.client.Sprite(name)
    cmd := sprite.CommandContext(ctx, "echo", "ok")
    _, err := cmd.Output()
    if err != nil {
        return "stopped", nil
    }
    return "running", nil
}

The Sprites API has a proper GET /v1/sprites/{id} endpoint that returns status, region, and metadata. The SDK's client.GetSprite(ctx, name) returns a Sprite with a Status field. PR #103 already uses this — this PR should too:

sprite, err := client.GetSprite(ctx, name)
if err != nil {
    return "not_found", nil
}
return sprite.Status, nil // "running", "suspended", "stopped", etc.

3. Use Filesystem API for sprite initialization

Current approach runs shell commands for file operations:

m.client.RunCommandCombined(ctx, spriteName, "mkdir", []string{"-p", "/data"})

With Filesystem API:

fs := sprite.Filesystem()
fs.MkdirAll("/data", 0755)

// Write config files directly
fs.WriteFile("/etc/taskd/config.json", configJSON, 0644)

No shell escaping needed, proper error handling, and file permissions set atomically.


4. Replace WebSocket proxy placeholder with exec attach

Current approach (sprite/proxy.go:82-125) has a placeholder:

func dialWebSocket(ctx context.Context, url string) (io.ReadWriteCloser, error) {
    return nil, fmt.Errorf("websocket not implemented - use gorilla/websocket")
}

For terminal access in the web UI, the exec API's WebSocket attach is the right tool:

// Terminal WebSocket handler in the web API
func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) {
    // Create an exec session with TTY
    cmd := sprite.CommandContext(ctx, "bash")
    cmd.SetTTY(true)
    cmd.SetTTYSize(cols, rows)

    // Connect stdin/stdout to the WebSocket
    stdin, _ := cmd.StdinPipe()
    stdout, _ := cmd.StdoutPipe()
    cmd.Start()

    // Bidirectional proxy between browser WebSocket and exec session
    // The exec API handles resize messages natively:
    // {"type": "resize", "cols": 180, "rows": 50}
}

The internal/webapi/terminal.go already has terminal handling — the exec API's TTY support with native resize messages maps directly to xterm.js in the browser.


5. Network Policy for user isolation

Each user's sprite should have restricted network access:

// Set per-user network policy
client.SetPolicy(ctx, spriteName, &sprites.Policy{
    AllowedDomains: []string{
        "github.com",
        "api.anthropic.com",
        // User's configured domains
    },
})

This is important for the multi-user web UI — prevents one user's sprite from accessing another's infrastructure.


6. Port notifications for the web UI

When a user's Claude starts a dev server inside their sprite, the exec API sends PortNotificationMessage with proxy URLs:

cmd.TextMessageHandler = func(data []byte) {
    var notification sprites.PortNotificationMessage
    json.Unmarshal(data, &notification)
    if notification.Type == "port_opened" {
        // Send to browser via WebSocket
        sendToClient(userID, map[string]interface{}{
            "type":     "port_opened",
            "port":     notification.Port,
            "proxyURL": notification.ProxyURL,
        })
    }
}

The web UI could show a "Preview" button that opens the proxied URL — a great UX for web development tasks.


7. Use exec sessions instead of RunCommand for the web API

The web API (internal/webapi/) proxies task operations to the sprite. For task execution, native exec sessions would give:

  • Session persistence if the host web server restarts
  • Real-time stdout/stderr streaming to the browser
  • Proper process lifecycle (kill with signal, wait for exit)

Instead of running commands fire-and-forget, create exec sessions and track them by ID.


SDK Note

The sprites-go SDK already supports all of these: Client.ListSessions(), Client.CreateService(), Client.ListServices(), Sprite.Filesystem(), Cmd.TextMessageHandler, Cmd.SetTTY(), and Client.SetPolicy(). No upgrade needed.

Full analysis with code examples: #286

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants