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 fc4bed67f4 fix(web): cache empty shells array in TerminalTabs selector
useWorkspaceUiStore((s) => s.shellsByWorkspace[workspaceId] ?? [])
returned a fresh [] on every render when the workspace had no shells
yet, so useSyncExternalStore saw a new snapshot each render and
re-rendered, which ran the selector again, ad infinitum.

  "The result of getSnapshot should be cached to avoid an infinite
   loop"
  TerminalTabs @ WorkspaceShell.tsx:75

Fix: hoist a module-level EMPTY_SHELLS constant. The selector now
returns a stable reference for the empty case. The non-empty case
already returns the array stored in the Zustand store, whose
reference is set once per setShellsForWorkspace call.
2026-07-06 18:10:03 +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%