Uh oh!
There was an error while loading. Please reload this page.
feat: show which thread owns a local dev-server port - #9036
Conversation
Ship jcode as an ACP provider with shared project board todos, a web Board panel, and a stdio MCP bridge so jcode can use board_* tools without ACP mcpServers. Co-authored-by: Cursor <cursoragent@cursor.com>
…n linking Extends chat attachments beyond images, adds a pet UI over the project board, and links board turns from chat threads. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feat(jcode): add jcode provider, project board, and stdio MCP bridge
feat(chat): add file attachments, project board pet UI, and board turn linking
The stdio bridge never sent the mcp-protocol-version header, so the server rejected notifications/initialized with a bodyless 400. That threw out of initialize() and killed the bridge at startup, leaving jcode with no t3-code toolkit at all (board_* included). Now the negotiated protocol version is sent on every post-initialize request, and a rejected notification only warns instead of tearing down a working session. Model: Claude via Jcode
Provider limit cards only rendered providers that answered, so Claude/ Cursor/OpenCode silently vanished whenever only Codex reported. Always render all four schema providers with a "No limit data" placeholder, add a sidebar popover for a quick check without leaving the thread, mark claude-sonnet-5 as the default Claude model (catalog order was picking claude-fable-5), and prefer Cursor for a brand-new thread with no configured default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…default model Cursor's rate-limit API (api2.cursor.sh/.../GetCurrentPeriodUsage) now rejects the WorkOS session cookie we built from the local OAuth token and only accepts that token as a Bearer credential. Confirmed against the live account before and after: 401 unauthenticated -> 200 with real usage percentages. Also: shrink the sidebar provider-limits popover (drop the repeated environment label when there's only one, shorten reset timestamps), and add a "Default model for all projects" control in Settings so switching every project's default model doesn't require editing each one by hand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Agents can set cwd to a sibling clone while the UI still shows the selected worktree. Tell the provider the bound checkout, surface a warning in the thread, and render that warning as an aligned row with the agent icon. Co-authored-by: Cursor <cursoragent@cursor.com>
feat(server): warn when the agent leaves the thread worktree
ACP agents already send usage_update and prompt usage, but those events were dropped. Map them into thread.token-usage.updated so the chat chip and context meter work for those providers too. Co-authored-by: Cursor <cursoragent@cursor.com>
fix(server): show Cursor, Grok, and Jcode token usage in chat
…pe composer work-mode UI Board: - Add a search box to the project board panel (title/notes/area, ignores the legacy "[tag]" title prefix) so a card is findable once the board has more than a screenful of items. - Fix a real bug found while investigating a reported "create succeeds, immediate read says not found": the SQL-backed projection pipeline never had cases for the five project.board-item-* events, so board items only ever lived in the raw event log and the in-memory engine model, never in the `projection_projects` row that MCP board tools and the UI both read. Reproduced live with board_upsert + board_get_brief before fixing. Composer: - Removed the native binary Build/Plan toggle, which rendered alongside the fork's own Build/Plan/Debug/Swarm menu in the wide composer layout (same two settings, two controls). Gated the fork's Work mode section behind the same provider-capability check the native toggle used, so it no longer offers modes for providers that don't support them. Also: Agent Control lets you archive (not delete) an errored thread straight from the "Needs you" list, and the sidebar limits popover refreshes on open instead of trusting a stale 60s cache. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
server.host isn't always "localhost" (configured-URL probing sets it to the probed hostname), so two servers sharing a port on different hosts rendered as identical rows. Display host:port and break port sort ties on host.
The <t3_active_ports> block ran an unbounded lsof-plus-HTTP-probe scan on every turn start, and resolved one thread title per port, so a thread with three dev servers cost three sequential lookups on the critical path. Extracted the whole scan/dedupe/resolve/format sequence into buildActivePortsTurnInputPrefix in activePortsPrompt.ts, where the scan is bounded by a 2s timeout and degrades to no block on any failure, and owning threads are deduplicated before title resolution. That leaves the upstream reactor with a single call plus a small closure, and makes the populated path directly unit-testable -- it had never executed in CI, since every reactor-level test stubs scan() to return nothing. Model: claude-opus-5 via T3 Code Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three defects on /ports. The owner cell read thread refs through a one-shot snapshot, so a cold deep-link that rendered before refs bootstrapped was stuck on "Another thread" forever; it now reads them reactively. Open used a bare window.open on the raw URL, which points at the viewer's localhost rather than the environment's on a remote connection; it now resolves the URL and goes through the shell bridge. Copy URL called navigator.clipboard directly, which is undefined -- and therefore throws -- over plain HTTP to a LAN IP; it now uses the guarded useCopyToClipboard hook. Model: claude-opus-5 via T3 Code Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InM1nd
commented
Sep 1, 2026
Opened by mistake against the wrong repo (gh defaulted to the upstream parent instead of my fork). Closing immediately, no review needed. Sorry for the noise. |
Macroscope skipped reviewing this pull request. Per-review cost limit exceeded (workspace setting). This review would cost an estimated $36.07, which exceeds your per-review limit of $10.00. The top 3 files driving up this estimate:
Tip To get this pull request reviewed, you can:
|
There was a problem hiding this comment.
Findings are confined to the new ProjectBoardPanel.tsx, which is the only place in apps/web/src that reconstructs core primitives (<select>, action buttons, micro icon buttons) instead of composing them. The other new surfaces in this PR (ProjectActivityPanel, PortsPage, ProviderLimits*, TandemDraftWorkspacePicker, pet overlay) compose Button/Select/Empty/ScrollArea correctly.
Posted via Macroscope — UI Consistency
| <button | ||
| type="button" | ||
| disabled={!itemDraft.title.trim()} | ||
| onClick={() => void saveBrief()} | ||
| className="cursor-pointer rounded bg-foreground px-2 py-1 text-xs font-medium text-background disabled:cursor-not-allowed disabled:opacity-40" | ||
| > | ||
| Save | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => setItemDraft(createBoardItemDraft(detailItem))} | ||
| className="cursor-pointer rounded px-2 py-1 text-xs font-medium hover:bg-accent" | ||
| > | ||
| Cancel | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => | ||
| void (detailItem.archivedAt ? onRestore(detailItem) : onArchive(detailItem)) | ||
| } | ||
| className="cursor-pointer rounded px-2 py-1 text-xs font-medium hover:bg-accent" | ||
| > | ||
| {detailItem.archivedAt ? "Restore" : "Archive"} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => void onDelete(detailItem)} | ||
| className="cursor-pointer rounded px-2 py-1 text-xs font-medium text-destructive hover:bg-destructive/10" | ||
| > | ||
| Delete | ||
| </button> |
There was a problem hiding this comment.
This action row hand-rolls four buttons that map onto existing Button variants — Save is variant="default", Cancel/Archive are variant="ghost", Delete is variant="destructive-outline" — at roughly size="xs". None of them carry a focus-visible ring, so the whole detail editor is unusable by keyboard without a visible focus indicator, and Save's disabled:opacity-40 diverges from the primitive's disabled:opacity-64 while re-adding disabled:cursor-not-allowed that Button handles via disabled:pointer-events-none. bg-foreground/text-background also bypasses the primary token pair the rest of the app uses for a confirm action.
Consider composing Button (add it to the imports):
- <button- type="button"- disabled={!itemDraft.title.trim()}- onClick={() => void saveBrief()}- className="cursor-pointer rounded bg-foreground px-2 py-1 text-xs font-medium text-background disabled:cursor-not-allowed disabled:opacity-40"- >- Save- </button>- <button- type="button"- onClick={() => setItemDraft(createBoardItemDraft(detailItem))}- className="cursor-pointer rounded px-2 py-1 text-xs font-medium hover:bg-accent"- >- Cancel- </button>- <button- type="button"- onClick={() =>- void (detailItem.archivedAt ? onRestore(detailItem) : onArchive(detailItem))- }- className="cursor-pointer rounded px-2 py-1 text-xs font-medium hover:bg-accent"- >- {detailItem.archivedAt ? "Restore" : "Archive"}- </button>- <button- type="button"- onClick={() => void onDelete(detailItem)}- className="cursor-pointer rounded px-2 py-1 text-xs font-medium text-destructive hover:bg-destructive/10"- >- Delete- </button>+ <Button size="xs" disabled={!itemDraft.title.trim()} onClick={() => void saveBrief()}>+ Save+ </Button>+ <Button+ size="xs"+ variant="ghost"+ onClick={() => setItemDraft(createBoardItemDraft(detailItem))}+ >+ Cancel+ </Button>+ <Button+ size="xs"+ variant="ghost"+ onClick={() =>+ void (detailItem.archivedAt ? onRestore(detailItem) : onArchive(detailItem))+ }+ >+ {detailItem.archivedAt ? "Restore" : "Archive"}+ </Button>+ <Button size="xs" variant="destructive-outline" onClick={() => void onDelete(detailItem)}>+ Delete+ </Button>Posted via Macroscope — UI Consistency
| <button | ||
| type="button" | ||
| onClick={() => onImplement(item)} | ||
| className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground" | ||
| aria-label={`Implement "${item.title}" in a new thread`} | ||
| > | ||
| <Play className="size-3" /> | ||
| </button> | ||
| } | ||
| /> | ||
| <TooltipPopup side="top">Implement in new thread</TooltipPopup> | ||
| </Tooltip> | ||
| ) : null} | ||
| <button | ||
| type="button" | ||
| onClick={(event) => { | ||
| event.stopPropagation(); | ||
| if (item.archivedAt) onRestore(item); | ||
| else onArchive(item); | ||
| }} | ||
| className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground" | ||
| aria-label={`${item.archivedAt ? "Restore" : "Archive"} "${item.title}"`} | ||
| > | ||
| {item.archivedAt ? <RotateCcw className="size-3" /> : <Archive className="size-3" />} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={(event) => { | ||
| event.stopPropagation(); | ||
| onDelete(item); | ||
| }} | ||
| className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground" | ||
| aria-label={`Delete "${item.title}"`} | ||
| > | ||
| <X className="size-3" /> | ||
| </button> |
There was a problem hiding this comment.
These three row actions repeat a class string that exactly reconstructs Button size="icon-micro" variant="ghost-muted" (size-5, rounded-sm, muted foreground → foreground on hover) but drop the primitive's focus-visible:ring-2 focus-visible:ring-ring and its pointer-coarse 44px hit target. The container already reveals this group on group-focus-within, so keyboard users can reach the buttons but get no visible focus indicator on the focused one, and on touch the 20px targets are below the primitive's coarse-pointer minimum.
Composing Button keeps the contextual layout at the call site while restoring focus and hit-target behaviour (add Button to the ~/components/ui/button imports):
- <button- type="button"- onClick={(event) => {- event.stopPropagation();- onDelete(item);- }}- className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground"- aria-label={`Delete "${item.title}"`}- >- <X className="size-3" />- </button>+ <Button+ size="icon-micro"+ variant="ghost-muted"+ onClick={(event) => {+ event.stopPropagation();+ onDelete(item);+ }}+ aria-label={`Delete "${item.title}"`}+ >+ <X />+ </Button>The archive/restore button and the tooltip-triggered implement button (render={<Button size="icon-micro" variant="ghost-muted" … />}) take the same treatment.
Posted via Macroscope — UI Consistency
| <select | ||
| value={itemDraft.status} | ||
| onChange={(event) => | ||
| setItemDraft({ | ||
| ...itemDraft, | ||
| status: event.target.value as ProjectBoardItem["status"], | ||
| }) | ||
| } | ||
| className="h-8 w-full rounded-md border border-input bg-transparent px-2 text-sm" | ||
| aria-label="Task status" | ||
| > | ||
| {PROJECT_BOARD_STATUS_ORDER.map((status) => ( | ||
| <option key={status} value={status}> | ||
| {projectBoardStatusLabel(status)} | ||
| </option> | ||
| ))} | ||
| </select> |
There was a problem hiding this comment.
This is the only raw <select> in apps/web/src; every other picker composes the Select primitive (see PortsPage.tsx in this same PR). Reconstructing the trigger by hand loses the primitive's focus-visible ring, data-disabled handling, coarse-pointer hit target, and dark-mode surface treatment, and it renders a native OS dropdown next to Base UI popups everywhere else in this panel.
Consider composing the primitive (add Select, SelectItem, SelectPopup, SelectTrigger, SelectValue to the ~/components/ui/select import):
- <select- value={itemDraft.status}- onChange={(event) =>- setItemDraft({- ...itemDraft,- status: event.target.value as ProjectBoardItem["status"],- })- }- className="h-8 w-full rounded-md border border-input bg-transparent px-2 text-sm"- aria-label="Task status"- >- {PROJECT_BOARD_STATUS_ORDER.map((status) => (- <option key={status} value={status}>- {projectBoardStatusLabel(status)}- </option>- ))}- </select>+ <Select+ value={itemDraft.status}+ onValueChange={(value) =>+ setItemDraft({ ...itemDraft, status: value as ProjectBoardItem["status"] })+ }+ >+ <SelectTrigger size="sm" aria-label="Task status">+ <SelectValue>{projectBoardStatusLabel(itemDraft.status)}</SelectValue>+ </SelectTrigger>+ <SelectPopup>+ {PROJECT_BOARD_STATUS_ORDER.map((status) => (+ <SelectItem key={status} value={status}>+ {projectBoardStatusLabel(status)}+ </SelectItem>+ ))}+ </SelectPopup>+ </Select>Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.
| }} | ||
| > | ||
| Open | ||
| </Button> |
There was a problem hiding this comment.
Ports Open hits the wrong machine
Medium Severity
The /ports Open action only calls readLocalApi()?.shell.openExternal on the resolved URL. In the browser client that API is missing so Open does nothing. For a remote environment reached over a local tunnel, resolveDiscoveredServerUrl keeps a loopback host, so Electron opens the client's port instead of the environment's, and a failed resolve falls back to the raw localhost URL.
Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This change is a large cross-cutting product integration that alters defaults, adds new provider/MCP, persistence, usage, and UI behavior, and introduces static-analysis suppressions. The new ports page also has an unresolved issue where Open may do nothing in browsers or target the client machine for remote environments. Not approved because:
Review your spending limits in Billing settings, or comment |


Summary
Parallel agent threads in the same environment often run dev servers on ports without knowing another thread already owns one, leading to killed/reused ports and confusion. This makes port ownership visible to both the coding agent and the human, on top of the existing
PortDiscoveryscanner (no new scanning/detection logic added).<t3_active_ports>block is prepended to turn input (same mechanism as the existing<t3_workspace_scope>block), listing dev-server ports owned by other threads in the environment. Omitted entirely when no other thread owns a port. The scan/dedupe/resolve/format sequence lives in a small, directly-testable Effect helper (activePortsPrompt.ts) to keep the change to the shared, upstreamProviderCommandReactor.tsfile minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start./portspage (reachable from the sidebar footer, next to Settings/Usage) lists every discovered port for an environment with its owner, an Open action, and a Copy URL action.Test plan
apps/server/src/orchestration/activePortsPrompt.test.ts— prompt-block formatting, empty/populated/dedupe/dropped-title/timeout pathsapps/server/src/orchestration/activePortOwners.test.ts— pure filter for other-thread port ownershipapps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+apps/server/integration/orchestrationEngine.integration.test.ts+ relevantserver.test.tscoverage — reactor wiringapps/web/src/components/preview/portOwnerLabel.test.ts+PreviewLocalServerCard.test.tsx— ownership badgeapps/web/src/components/ports/PortsPage.test.tsx— new page, including host:port display, host-aware sort, and the reactive owner-label fallback/portsnavigation and footer collapse (screenshots not included in this PR)<t3_active_ports>block actually reaches a second thread's turn input (only exercised in a live two-thread session, not CI)Every task went through an individual spec+quality review, and the whole branch went through a final review that caught and fixed 5 cross-task issues (turn-start latency risk, missing wiring test coverage, a stale-label bug on
/ports, anavigator.clipboardcrash risk on insecure-context clients plus wrong-machine URL opening on remote connections, and the fork-isolation line budget on the upstream reactor file).Model: Sonnet 5; harness: Claude Code (subagent-driven development — implementer + reviewer subagents per task, plus a final whole-branch review).
Note
Medium Risk
Touches orchestration turn input, MCP tool dispatch, and attachment validation—user-visible agent behavior and data paths—but changes are largely fork-owned modules with defensive fallbacks on port scanning.
Overview
This PR is a broad jcode fork slice: it rebrands/isolates the desktop app (Tandem,
~/.t3-jcodestate, separate userData dirs), documents fork wiring rules in AGENTS.md, and extends mobile/web-adjacent surfaces (nested jcode provider icons, Cursor in usage charts).Project board is wired end-to-end: new orchestration commands/events and SQL projection for
boardItems,getProjectActivityread API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) withboardcapability on MCP sessions. jcode turns get optional board prompt blocks; the reactor also prepends workspace scope and a timeout-bounded<t3_active_ports>block listing dev-server ports owned by other threads.jcode ↔ T3 MCP integration adds an NDJSON stdio bridge that proxies tool RPCs to Streamable HTTP
/mcp(with protocol-version headers and JSON/SSE parsing). Provider reactor behavior now restarts jcode sessions when the innerjcodeProviderchanges despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.Supporting changes include default keybindings for
board.toggleandcomposer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs forgetProjectActivity/PortDiscovery.Reviewed by Cursor Bugbot for commit 9ad2eb6. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add project board, Jcode provider, and port ownership display
board_list,board_digest,board_get_brief,board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palettejcodeprovider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support/portspage listing discovered dev servers per environment with thread ownership labels, open/copy URL actions, and reactive owner links; local server preview cards and turn-input prefixes also annotate port ownershipdefault/plansplitDEFAULT_PROVIDER_INTERACTION_MODEchanges fromdefaulttobuildin orchestration.ts;ServerSettings.defaultThreadEnvModechanges fromlocaltoworktreein settings.ts; desktopappIdchanges tocom.t3tools.tandemand state dir to~/.t3-jcodein DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 addsboard_items_jsoncolumn toprojection_projectsMacroscope summarized 9ad2eb6.