spacedriveapp / spacedriveapp/spacebot
OpenCode embed exposure: chat→workbench link, resizable+full-width worker columns
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-layoutvia the existingResizableprimitive — gives a thin draggable divider between columns, more polished than native CSSresize. - 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
- Issue 3 first (~1 file changed substantially, ~2-line diff in another). Visible win, low risk.
- Issue 2 second. Builds on #3's flex layout — resizer flips columns from
flex-1to explicit widths. - Issue 1 last. Touches
router.tsx+Workbench.tsx+AgentWorkers.tsx. Adds the new sub-route and thefilterThreadprop.
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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