feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: show which thread owns a local dev-server port - #9036

Closed
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains
Closed

feat: show which thread owns a local dev-server port#9036
InM1nd wants to merge 82 commits into
pingdotgg:mainfrom
InM1nd:t3code/manage-local-development-domains

Conversation

@InM1nd

@InM1ndInM1nd commented Sep 1, 2026

Copy link
Copy Markdown

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 PortDiscovery scanner (no new scanning/detection logic added).

  • Server: a <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, upstream ProviderCommandReactor.ts file minimal (~13 added lines), and the scan itself is timeout-guarded so it can never delay or fail turn start.
  • Web: the existing local-server preview card now shows a small badge naming the owning thread, and a new /ports page (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.
  • Kill/Stop is explicitly out of scope — this is visibility only, not process control.

Test plan

  • apps/server/src/orchestration/activePortsPrompt.test.ts — prompt-block formatting, empty/populated/dedupe/dropped-title/timeout paths
  • apps/server/src/orchestration/activePortOwners.test.ts — pure filter for other-thread port ownership
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts + apps/server/integration/orchestrationEngine.integration.test.ts + relevant server.test.ts coverage — reactor wiring
  • apps/web/src/components/preview/portOwnerLabel.test.ts + PreviewLocalServerCard.test.tsx — ownership badge
  • apps/web/src/components/ports/PortsPage.test.tsx — new page, including host:port display, host-aware sort, and the reactive owner-label fallback
  • Manual live-browser verification of /ports navigation and footer collapse (screenshots not included in this PR)
  • Manual two-thread verification that the <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, a navigator.clipboard crash 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-jcode state, 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, getProjectActivity read API + RPC auth, and an MCP board_* toolkit (list/digest/upsert/handoff/status/archive, etc.) with board capability 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 inner jcodeProvider changes despite the same model slug, and turn dispatch accepts non-image file attachments via shared MIME allowlists.

Supporting changes include default keybindings for board.toggle and composer.attachImages, workspace-scope warning activities on tool paths outside the worktree, and widespread test harness stubs for getProjectActivity / 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

  • Adds a full Project Board system: contracts, decider, projections, MCP toolkit (board_list, board_digest, board_get_brief, board_upsert), right-panel UI with status cycling, archive/restore, handoffs, and a digest inserter via command palette
  • Introduces a new jcode provider driver with ACP session runtime, session daemon, MCP stdio-to-HTTP bridge, inner-provider model routing (Claude/OpenAI), and text generation support
  • Adds a /ports page 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 ownership
  • Groups sidebar threads by logical project with collapsible headers and per-project settled pagination; adds unified work modes (Build, Plan, Debug, Swarm Lite) replacing the old default/plan split
  • Includes companion pet overlay, Cursor dashboard usage tracking, workspace-scope mismatch warnings, thread rollover, new built-in themes, non-image file attachments, and Tandem branding
  • Risk: DEFAULT_PROVIDER_INTERACTION_MODE changes from default to build in orchestration.ts; ServerSettings.defaultThreadEnvMode changes from local to worktree in settings.ts; desktop appId changes to com.t3tools.tandem and state dir to ~/.t3-jcode in DesktopEnvironment.ts; migration Filter app runtime env vars from terminal spawn environment #44 adds board_items_json column to projection_projects

Macroscope summarized 9ad2eb6.

InM1ndand others added 30 commits August 10, 2026 21:12
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
InM1ndand others added 22 commits August 31, 2026 00:49
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

Copy link
Copy Markdown
Author

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.

@InM1ndInM1nd closed this Sep 1, 2026
@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

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:

FileDiff SizeEstimate
apps/server/src/provider/Layers/JcodeAdapter.ts60.14KB$3.01
apps/web/src/components/Sidebar.tsx39.05KB$1.95
apps/web/src/components/ProjectBoardPanel.tsx37.56KB$1.88

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +702 to +732
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +195 to +230
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +646 to +662
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 9ad2eb6. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@InM1nd