spacedriveapp / spacedriveapp/spacebot

OpenCode embed exposure: chat→workbench link, resizable+full-width worker columns

Open
#596 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
2.4k
Forks
367
PR merge metrics
No merged PRs in 30d

Description

Summary

The OpenCode embed surfaces in the dashboard don't expose their content well. Three related issues:

1. Chat popup → workbench thread is a dead end

When OpenCode is shown in the worker-detail panel (small inline view of an active OpenCode session inside the AgentWorkers page), there's no way to escape into the full workbench experience for the same thread. Good for quick glance, bad for actual work.

Proposed: add an "Open in Workbench" link next to the existing OpenCodeDirectLink in the worker-detail panel header. Routes to a new /workbench/$thread sub-route filtered to that single thread.

2. Workbench WorkerColumns aren't resizable

Each WorkerColumn is a fixed w-[560px]. With multiple workers running, the screen partitions arbitrarily and the user can't widen the column they're actively reading.

Proposed: add drag handles between columns using the existing Resizable primitive (@spacedrive/primitives/Resizable, wraps react-resizable-layout). Persist widths to localStorage by column position, not worker id, so the layout is stable across worker churn.

3. Workbench columns don't fill the available width

The container is flex-1 gap-[10px] overflow-x-auto (Workbench.tsx:179) and each column is flex-shrink-0 w-[560px]. With N×560 < viewport width there's wasted horizontal space at the right edge; with N×560 > viewport, the page scrolls horizontally instead of letting columns share remaining space.

Proposed: flex columns to fill 100% of the workbench pane, with a per-column min-width (~360px) so they only fall back to horizontal scroll when there are genuinely too many. User-set widths from #2 take precedence.

Why this matters

The workbench is the primary workspace for multi-worker sessions. Friction here pushes users back to the worker-detail popup (read-only-ish, less capable), undercutting the workbench's whole purpose. Each issue compounds the others — without (3) you don't have the room to want (2), and without (2) the popup-vs-workbench tradeoff in (1) tilts even more toward "stay in the popup."


Implementation plan

Files involved
Concern File Lines (approx)
Worker-detail panel rendering OpenCode (the "chat popup") interface/src/routes/AgentWorkers.tsx 395–545 (WorkerDetail), 471 (OpenCodeDirectLink), 633 (impl)
Workbench multi-column layout interface/src/routes/Workbench.tsx 162–192
Per-column wrapper + fixed width interface/src/components/workbench/WorkerColumn.tsx 18 (w-[560px] flex-shrink-0)
Workbench routing interface/src/router.tsx 106 (path: "/workbench")
Resizable primitive @spacedrive/primitives/src/Resizable.tsx already shipped, wraps react-resizable-layout
Decisions
  • Routing for filtered workbench: sub-route /workbench/$thread, using TanStack Router's native History integration (back/forward/deep-link/refresh all work out of the box).
  • Resize implementation: JS-based react-resizable-layout via the existing Resizable primitive — gives a thin draggable divider between columns, more polished than native CSS resize.
  • Width persistence: keyed by column position (spacebot-workbench-column-widths = Record<positionIndex, number>), so layout is stable across worker churn.
Issue 1 — Open-in-Workbench link

Routing change in interface/src/router.tsx:

// Add a child route under /workbench
{
  path: "/workbench/$thread",
  component: function WorkbenchThreadPage() {
    const { thread } = useParams({ from: "/workbench/$thread" });
    return <Workbench filterThread={thread} />;
  },
}

Workbench accepts an optional filterThread prop that narrows the visible columns to that session id. When set, the sidebar shows a "Showing 1 of N workers — clear filter" affordance that links back to /workbench.

The link itself in AgentWorkers.tsx ~471:

{hasOpenCodeEmbed && detail.opencode_session_id && (
  <Link
    to="/workbench/$thread"
    params={{ thread: detail.opencode_session_id }}
    className={cx(badgeVariants({variant: "outline", size: "sm"}), "w-fit")}
  >
    Open in Workbench →
  </Link>
)}

UX touch (non-blocking): when workbench loads filtered to one thread, scroll that column into view + flash the border briefly so the user knows they landed on the right one.

Issue 2 — Resizable columns

Convert WorkerColumn's wrapper to participate in a horizontal Resizable chain. The primitive's useResizable hook returns a position value driven by drag; we feed the resulting widths into a Map<positionIndex, number> and persist on debounce.

Sketch (Workbench.tsx):

const [widths, setWidths] = useColumnWidths(); // localStorage-backed hook

return (
  <div className="flex flex-1 gap-[10px] min-w-0">
    {workers.map((worker, i) => (
      <ResizableColumn
        key={worker.id}
        index={i}
        width={widths[i]}
        onResize={(w) => setWidths({ ...widths, [i]: w })}
        isLast={i === workers.length - 1}
      >
        <WorkerColumn worker={worker} />
      </ResizableColumn>
    ))}
  </div>
);

useColumnWidths: reads/writes localStorage["spacebot-workbench-column-widths"] as JSON. Debounce writes (~250ms) to avoid spamming localStorage during drag. No migration needed — fresh feature.

Issue 3 — Fill width by default

Workbench.tsx:179:

- <div className="flex flex-1 gap-[10px] overflow-x-auto">
+ <div className="flex flex-1 gap-[10px] min-w-0 overflow-x-auto">

(Keep overflow-x-auto — only kicks in when columns' min-widths sum to more than viewport.)

WorkerColumn.tsx:18:

- <div className="flex h-full w-[560px] flex-shrink-0 flex-col overflow-hidden rounded-2xl border border-app-line bg-app-box">
+ <div className="flex h-full min-w-[360px] flex-1 flex-col overflow-hidden rounded-2xl border border-app-line bg-app-box">

When #2's resizer assigns an explicit width to a column, it takes precedence (flex-[0_0_<px>]).

Order to ship
  1. Issue 3 first (~1 file changed substantially, ~2-line diff in another). Visible win, low risk.
  2. Issue 2 second. Builds on #3's flex layout — resizer flips columns from flex-1 to explicit widths.
  3. Issue 1 last. Touches router.tsx + Workbench.tsx + AgentWorkers.tsx. Adds the new sub-route and the filterThread prop.

Each can ship as its own PR or combined.

Out of scope

  • Whether the popup-vs-workbench routing should remember per-thread preferences (separate UX question)
  • Vertical resizing within a column (worker output vs prompt-input) — separate
  • Custom user-authored workbench layouts / saved arrangements — bigger feature

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Workbench.tsx and WorkerColumn.tsx to understand the current column layout, then inspect the Resizable primitive and router.tsx for the proposed integration points. Review AgentWorkers.tsx for the worker-detail link. Done means the workbench supports the documented filtered route, resizable persisted columns, and responsive minimum-width behavior across the three planned changes.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
developer-experience, frontend, web-dev
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.