This repository has been archived on 2026-07-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
tao.chen 7b40b23d33 feat(web): multi-terminal tabs per workspace
Frontend wiring for the new multi-shell backend. The bottom panel's
Terminal tab now has a tab strip: one tab per shell (showing
shellId[:8] + an X to close), plus a + button to spawn a new shell.
Tab state lives in workspace-ui-store (shellsByWorkspace +
activeShellIdByWorkspace). Auto-start creates the first shell if the
list is empty.

- web/src/lib/api/process.ts:
  - useShellStart returns ShellInfo on 201
  - useShellStop / useShellRestart take {shellId}
  - useShellStatus(workspaceId, shellId) requires both
  - useShellWebSocket(workspaceId, shellId, enabled) refactored:
    shellId change closes the current socket intentionally and opens
    a new one with ?shellId= appended
  - useShellResize takes {shellId, cols, rows}
  - new useShellList(workspaceId)
- web/src/lib/store/workspace-ui-store.ts: shellsByWorkspace +
  activeShellIdByWorkspace maps + setters (addShellToWorkspace,
  removeShellFromWorkspace, setActiveShell, setShellsForWorkspace)
- web/src/components/terminal/TerminalPanel.tsx: requires shellId
  prop; clears/resets terminal on shellId change (in addition to
  workspaceId); welcome banner shows
  '[connected to <workspaceId>/<shortShellId>]'
- web/src/components/layout/WorkspaceShell.tsx: new TerminalTabs
  component:
  - lists shells as tabs, + to add, x to close
  - useShellList hydrates the store on mount
  - auto-start guard via useRef so we don't double-fire
  - close-pending tracked per shellId via Set
  - renders the active <TerminalPanel workspaceId shellId>

Diff: 4 files. pnpm lint + pnpm build clean.

Conversation: B975103F-4528-493E-8F8A-91D20506631D
2026-07-06 15:48:12 +08:00
2026-07-02 19:53:00 +08:00
2026-07-02 15:07:54 +08:00

codespace

A Go backend skeleton for managing local workspaces and file operations with process orchestration.

V1 scope

  • Workspace creation, listing, lookup, and deletion backed by local directories.
  • File read/write/list/mkdir/remove/rename/stat through a FileSystem interface with path-escape protection.
  • Workspace root deletion protection — cannot remove the workspace root directory through the file API.
  • Configurable per-file write size limit — oversized writes are rejected early before hitting disk.
  • OpenCode process start/stop/restart/status per workspace (launched with opencode acp by default; raw stdio is still piped through the process WebSocket for backward compatibility).
  • ACP (Agent Client Protocol) client — minimal JSON-RPC over NDJSON against the running opencode process. Exposes initialize / session/new / session/prompt / session/cancel. Serves fs/read_text_file and fs/write_text_file from the workspace filesystem. The web UI surfaces this as a chat panel alongside the terminal.
  • HTTP API using Gin for routing with JSON responses.
  • Explicit http.Server composition with configurable timeouts and max header bytes.
  • Structured logging using Go standard library log/slog with JSON/text format and level control.

Safety

  • File paths are workspace-relative; absolute paths are rejected.
  • .. escape attempts are rejected before filesystem operations.
  • Deleting the workspace root through the file API is explicitly blocked.
  • File content is never logged; structured logging only includes metadata.
  • Workspace root paths are not exposed in API responses or logs.

Run

make run

The server listens on :8080 by default.

Configuration

configs/config.yaml:

server:
  addr: ":8080"
  readTimeout: "15s"
  writeTimeout: "15s"
  idleTimeout: "60s"
  maxHeaderBytes: 1048576
workspace:
  root: "./workspaces"
process:
  opencodeCommand: "opencode"
  args:
    - "acp"

shell:
  command: "bash"
  args:
    - "-i"

file:
  maxWriteBytes: 1048576
gin:
  mode: "release"
log:
  level: "info"
  format: "json"

Environment overrides

Variable Description Default
CODESPACE_ADDR Listen address :8080
CODESPACE_WORKSPACE_ROOT Workspace storage root ./workspaces
CODESPACE_OPENCODE_COMMAND OpenCode binary path opencode
CODESPACE_OPENCODE_ARGS Comma-separated extra args passed to OpenCode (sets process.args) acp
CODESPACE_SHELL_COMMAND Shell binary path bash
CODESPACE_SHELL_ARGS Comma-separated shell arguments -i
CODESPACE_FILE_MAX_WRITE_BYTES Max bytes per file write 1048576
CODESPACE_GIN_MODE Gin mode (debug, release, test) release
CODESPACE_READ_TIMEOUT HTTP read timeout 15s
CODESPACE_WRITE_TIMEOUT HTTP write timeout 15s
CODESPACE_IDLE_TIMEOUT HTTP idle timeout 60s
CODESPACE_MAX_HEADER_BYTES Max HTTP header bytes 1048576
CODESPACE_LOG_LEVEL Log level (debug, info, warn, error) info
CODESPACE_LOG_FORMAT Log format (json, text) json

Logging

codespace uses the Go standard library log/slog for application logs. All runtime logging uses slog; fmt, log.Printf, and the Gin default logger are not used.

API

Workspaces

Method Path Description
GET /api/workspaces List all workspaces
POST /api/workspaces Create a workspace ({"id": "name"})
GET /api/workspaces/:id Get workspace by ID
DELETE /api/workspaces/:id Delete workspace (stops running process first)

Files

Method Path Description
GET /api/workspaces/:id/files?path=... List directory entries
GET /api/workspaces/:id/files/read?path=... Read file content
GET /api/workspaces/:id/files/stat?path=... Get file metadata (name, path, isDir, size, modTime)
PUT /api/workspaces/:id/files/write Write file ({"path": "...", "content": "..."})
POST /api/workspaces/:id/files/mkdir Create directory ({"path": "..."})
DELETE /api/workspaces/:id/files?path=... Remove file or directory recursively
POST /api/workspaces/:id/files/rename Rename file or directory ({"oldPath": "...", "newPath": "..."})

Process

Method Path Description
POST /api/workspaces/:id/process/start Start OpenCode process for workspace
POST /api/workspaces/:id/process/stop Stop OpenCode process
POST /api/workspaces/:id/process/restart Restart OpenCode process
GET /api/workspaces/:id/process/status Get process running status and PID
GET /api/workspaces/:id/process/ws WebSocket: stream process stdout/stderr, send stdin (text frames, raw bytes)

Shell

Method Path Description
POST /api/workspaces/:id/shell/start Start interactive shell for workspace
POST /api/workspaces/:id/shell/stop Stop interactive shell
POST /api/workspaces/:id/shell/restart Restart interactive shell
GET /api/workspaces/:id/shell/status Get shell running status and PID
GET /api/workspaces/:id/shell/ws WebSocket: stream shell stdout/stderr, send stdin (text frames, raw bytes)

ACP (Agent Client Protocol)

Sits on top of the OpenCode process started above. The ACP service starts the process on the first prompt if it isn't already running, performs the initialize + session/new handshake, and accumulates the agent's responses until the session/prompt reply arrives.

Method Path Description
GET /api/workspaces/:id/acp/status Observational status: {workspaceId, ready, sessionId, running, pid, error}. Does not start the process.
GET /api/workspaces/:id/acp/history Accumulated conversation: {sessionId, messages: [{role, text, time}]}, oldest first. In-memory only — cleared when the process restarts.
POST /api/workspaces/:id/acp/prompt Body {"content": "..."}. Response {"sessionId, stopReason, text}. Starts the process + session on demand. Synchronous (waits up to 5 min).
POST /api/workspaces/:id/acp/cancel Best-effort interrupt of an in-flight prompt.

terminal/* and other unsupported agent-initiated requests return JSON-RPC Method not found (-32601).

Health

Method Path Description
GET /healthz Server health check

Examples

Health check

curl -s http://localhost:8080/healthz

Create a workspace

curl -s -X POST http://localhost:8080/api/workspaces \
  -H 'Content-Type: application/json' \
  -d '{"id":"user1"}'

List workspaces

curl -s http://localhost:8080/api/workspaces

Write a file

curl -s -X PUT http://localhost:8080/api/workspaces/user1/files/write \
  -H 'Content-Type: application/json' \
  -d '{"path":"main.go","content":"package main\n"}'

Read a file

curl -s 'http://localhost:8080/api/workspaces/user1/files/read?path=main.go'

Get file stat

curl -s 'http://localhost:8080/api/workspaces/user1/files/stat?path=main.go'

Send a prompt to the agent

curl -s -X POST http://localhost:8080/api/workspaces/user1/acp/prompt \
  -H 'Content-Type: application/json' \
  -d '{"content":"list the files in this workspace"}'

Test

make test
S
Description
No description provided
Readme
359 KiB
Languages
Go 52.7%
TypeScript 46%
CSS 1%
JavaScript 0.2%