pingdotgg / pingdotgg/t3code

[Bug]: manual machine selection permanently disables "Auto balance" for a logical project

Open
#12,688 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

accepted bug via-triage
Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Before submitting
  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.
Area

apps/web

Summary

Picking a machine by hand from the composer's Run on menu permanently disables automatic routing for that logical project. The "Auto balance" entry keeps rendering and stays visually identical to a working one, but clicking it is a no-op, and there is no in-app way to get back to automatic routing.

The state is persisted per logical project in localStorage (t3code:composer-drafts:v1), so it survives reloads and every subsequent new thread in that project.

Edited after filing: the original report blamed composer attachments. That is one way to hit the inert entry, but it is not the important one - the attachment case is recoverable by removing the attachment. The manual-selection case is not recoverable at all, and that is the real defect. Steps, workaround and suggested fix below are updated accordingly.

Steps to reproduce
  1. Have two or more environments in one logical project, with loadBalancingEnabled on.
  2. Open the Run on menu and pick a specific machine.
  3. Open the Run on menu again and click Auto balance. Nothing happens.
  4. Start a new thread in the same project. Still nothing - the draft is reused by logical project key.
Expected behavior

Selecting Auto balance after a manual pick returns the draft to automatic routing. Failing that, the entry communicates that it cannot - disabled, hidden, or a distinct label. The existing "Auto balance unavailable" / "Checking machines…" labels already establish that pattern.

Actual behavior

automaticEnvironment (apps/web/src/components/ChatView.tsx:2622) requires, among other terms:

draftThread?.environmentSelection !== "manual" &&
(!composerHasAttachments || Boolean(draftThread?.loadBalancedEnvironmentId)) &&
(!draftThread?.branch || draftThread.environmentSelection === "auto") &&
!draftThread?.worktreePath

Only two places write environmentSelection: "auto" back, and both are gated behind automaticEnvironment itself:

  • the load-balancing useEffect (ChatView.tsx:3850), gated on needsLoadBalancing, which is automaticEnvironment && !draftThread?.loadBalancedEnvironmentId;
  • onAutoEnvironment (ChatView.tsx:3877), whose only call sites are guarded by autoEnvironmentLabel, which is undefined unless automaticEnvironment holds.

Meanwhile onEnvironmentChange (ChatView.tsx:3912) writes environmentSelection: "manual" and does not clear branch. So a single manual pick clears the flag that both recovery paths require. It is a one-way transition.

The same closed loop applies to branch: with branch set and environmentSelection anything other than "auto", the (!draftThread?.branch || ...) term is false, and onAutoEnvironment - the only thing that would clear branch back to null - is unreachable for the same reason.

Because openOrReuseProjectDraftThread (ChatView.tsx:2494) resolves drafts through getDraftSessionByLogicalProjectKey, the dead state is scoped to the logical project and is inherited by every new thread there.

Observed directly in t3code:composer-drafts:v1 on a 5-environment install, two projects, loadBalancingEnabled: true:

logical project environmentSelection branch loadBalancedEnvironmentId Auto balance
github.com/<org>/a auto main set works
github.com/<org>/b manual main null dead
The entry gives no signal that it is dead

BranchToolbarEnvironmentSelector.tsx renders the item off onAutoEnvironment but guards the click on autoEnvironmentLabel:

{onAutoEnvironment && (
  <SelectItem
    value="auto"
    onClick={() => {
      if (autoEnvironmentLabel) onAutoEnvironment?.();   // L141 - no-op when undefined
    }}
  >
    <span className="inline-flex items-center gap-1.5">
      <ScaleIcon className="size-3" aria-hidden="true" />
      {autoEnvironmentLabel ?? "Auto balance"}            {/* L146 - looks enabled */}
    </span>
  </SelectItem>
)}

onAutoEnvironment is passed on a four-term check (ChatView.tsx:10207) that does not include any of the automaticEnvironment terms:

onAutoEnvironment={
  draftId && !envLocked && hasMultipleEnvironments && loadBalancingSettings.loadBalancingEnabled
    ? onAutoEnvironment
    : undefined
}

The ?? "Auto balance" fallback on L146 (and the identical one in environmentItems, L46) makes the dead state pixel-identical to the live one.

Two smaller things in the same file, same root cause:

  • The Select's own onValueChange (L95) calls onAutoEnvironment?.() without the autoEnvironmentLabel guard, so the two activation paths in one component disagree about whether the entry is live.
  • value={autoEnvironmentLabel ? "auto" : environmentId} (L93) means the entry can never show as selected in the inert state.
Impact

Minor bug or occasional failure

Version or commit

main @ 7445aa7 (also reproduces on the published npm build, t3 v0.0.42)

Environment

macOS 27.0 (Darwin), t3 v0.0.42, 5 linked environments

Workaround

There is no in-app recovery. The persisted draft has to be edited directly, from the renderer console:

const k = 't3code:composer-drafts:v1';
const s = JSON.parse(localStorage.getItem(k));
const tk = s.state.logicalProjectDraftThreadKeyByLogicalProjectKey['github.com/<org>/<repo>'];
Object.assign(s.state.draftThreadsByThreadKey[tk], {
  environmentSelection: 'auto', branch: null, worktreePath: null, loadBalancedEnvironmentId: null,
});
localStorage.setItem(k, JSON.stringify(s));
location.reload();
Possible fix

The rendering/click mismatch and the unrecoverable state are separable.

1. Make the transition two-way. onAutoEnvironment already sets exactly the right thing - { environmentSelection: "auto", loadBalancedEnvironmentId: null, branch: null, worktreePath: null }. It just has to be reachable. Gate the click on the four-term onAutoEnvironment condition rather than on autoEnvironmentLabel, i.e. drop the if (autoEnvironmentLabel) guard on L141 and let onAutoEnvironment decide for itself (it already early-returns on envLocked/!draftId and already toasts for the attachment case). That alone restores recovery.

2. Stop the entry from lying about its state. Either hide it when inert:

-      ...(onAutoEnvironment
+      ...(onAutoEnvironment && autoEnvironmentLabel
         ? [{ value: "auto", label: autoEnvironmentLabel ?? "Auto balance" }]
         : []),

or keep it clickable and give it a distinct label, matching the existing "Auto balance unavailable" treatment.

Note that (1) and the hide-it variant of (2) conflict - hiding the entry whenever autoEnvironmentLabel is undefined re-closes the loop and also makes the "Keep attachments on this machine" toast unreachable. Fixing (1) by dropping the L141 guard and handling (2) with disabled + a distinct label only for the genuinely unrecoverable states seems like the consistent combination, but it depends on what the toast is meant to be for. Happy to open a PR for whichever direction you prefer.

Contributor guide

Open the contributing guide

First steps

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

Research direction

Start in apps/web/src/components/ChatView.tsx at automaticEnvironment, onAutoEnvironment, onEnvironmentChange, and openOrReuseProjectDraftThread, then inspect BranchToolbarEnvironmentSelector.tsx. Reproduce the manual-selection flow with two environments and inspect the persisted draft in localStorage. Done means Auto balance can recover the draft and the selector no longer appears enabled when its action is unavailable.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.