Commit Graph
49 Commits
Author SHA1 Message Date
Jinwoo Hong 8f15f217a2 Preserve user-set workspace names across branch changes (#17448)
* fix(worktrees): preserve user workspace names across branch changes

* test(worktrees): cover pinned rename metadata

* fix(workspaces): address display-name review edge cases

* fix(workspaces): keep automatic names fresh across refreshes

* fix(workspaces): preserve legacy CLI labels

* fix(workspaces): preserve display-name provenance across hosts

* fix(workspaces): honor legacy display-name provenance

* fix(workspaces): fence display-name refresh races

* fix(workspaces): accept peer renames from provenance-less hosts

The old-host preserve fence kept a pinned local label on every refresh,
which also suppressed a legitimate rename another client persisted
through the same host until app restart. Narrow it to labels the host
re-derived itself (branch short name, or path basename when detached);
any other changed label in a mode-less response is explicit meta a peer
wrote there. Stale prior-label responses stay covered by the downstream
staleness fence, in-flight writes by the pending fence.

* refactor(workspaces): unify display-name pin derivation

Three call sites (renderer optimistic update, local IPC updateMeta
handler, remote worktree.set handler) each restated the same formula;
a future edit to one would silently skew provenance between paths.
2026-08-31 19:08:05 -04:00
a651e81843 refactor(agents): remove dead hook IPC and derive shared agent defaults (#16089)
* refactor(agent-hooks): drop the unused per-agent hook status IPC surface

No renderer, CLI, or mobile caller invoked window.api.agentHooks.*Status; main
already reads install status through MANAGED_AGENT_HOOK_STATUS_READERS. The
14 handlers had also drifted (kimiStatus existed in main/preload but not in
AgentHooksApi or the web stub).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(tui-agent-config): default launchCmd and expectedProcess to detectCmd

32 of 36 entries repeated the binary name three times. Entries are now
authored in a source form where both default to detectCmd and resolved once
at module load, so TUI_AGENT_CONFIG keeps its exact shape for consumers
(verified equal to the previous table).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(mobile): derive the agent order, labels, and picker from src/shared

The mobile mirror (and its regex-over-desktop-source parity test) predates
mobile importing runtime values from src/shared, which it now does in a dozen
modules. Only the favicon-domain map stays mobile-local because desktop's lives
in the renderer catalog next to bundled ?url imports. The parity test now
imports the real registries and also checks label parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(web): align preload surface after hook IPC removal

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-30 19:02:14 -07:00
Neil 81b4d88bd6 refactor(mobile): split tasks route into focused modules (#17438)
* refactor(mobile): split tasks route into focused modules

* fix(mobile): repair tasks refactor module boundaries

* chore(mobile): document intentional render resets

* fix(mobile): remove stale lint suppressions from tasks split

* test(mobile): keep parity checks stable with doctor suppressions

* test(mobile): follow tasks module split

* test(mobile): follow project routing module split
2026-08-30 14:10:36 -07:00
Brennan Benson 8cf7c6926e fix(mobile): create-worktree sheet dies after picking a source and loses the picked PR (#16917)
* fix(mobile): keep the create form on screen through drawer swaps and survive reconnects

The create-worktree flow could reach a state where the shared modal host was
mounted with no sheet in it: a full-screen transparent window that swallows
every tap with no way out. Frame analysis of the reported recording and a live
simulator repro both land on the same state - the form sheet laid out at the
right frame with progress=1, backdrop painting, sheet not painted.

- Keep the form sheet mounted through every drawer transition, so the host
  Modal is never on screen without a sheet, and drop the render-read pin ref.
- Re-assert a pinned sheet's enter transform when it takes the window back
  from a fill picker; nothing re-applied it before.
- Key the form session on hostId, not on the RpcClient object: useHostClient
  swaps that object on every reconnect, which silently remounted the form and
  threw away the picked source.
- Run the pasted-item lookup concurrently with the provider fan-out instead of
  after it (measured 2631ms -> 1480ms for a typed PR number).

* fix(mobile): remount the sheet view on window hand-back so a rebuilt native view repaints

On-device confirmation showed the committed hand-back re-assert never
reaches the native view: progress already sits at 1 and translateY at 0,
so withTiming produces no style delta, and the dead screen stayed
reachable (1/25 on the committed build; 1/9 with a sub-pixel value
nudge, which lands on the stale native binding when the view was rebuilt
with a new tag). Remounting the sheet's Animated.View on an epoch keyed
to the hand-back mounts a fresh native view with the style computed from
the current shared values - progress is already 1, so it paints in place
with no visible animation. 0 dead in 50 attempts on the remount build
under the same churn condition that reproduced the dead screen on base.

LANE-REPORT.md carries the full confirmation evidence and limits.

* chore: drop the stray lane report from the repo root

It is a working artifact, not source, and the root directory guard blocks
any new top-level entry.

* test(mobile): assert the sheet subtree rebuild directly, not through a test-only prop

The hand-back test proved the remount by reading an epoch-keyed nativeID that
existed only for it — production markup shaped by a test, and an assertion a
future refactor could satisfy without rebuilding anything. Count mounts of the
sheet's content instead, which is the property the fix actually depends on, and
drop the nativeID.

Also stop typing test renderers as 'ReactTestRenderer | null'. The static
analysis job installs no mobile/node_modules, so that type is unresolvable
there and the union trips no-redundant-type-constituents on every added line.

The hand-back re-assert is not dead code as the old comment implied: the drawer
swap hands back at 166ms, before the 180ms enter animation ends.

* fix(mobile): keep the create form when a render is thrown away

The session key was built from counters mutated during render. A blurred screen
suspends this subtree (react-native-screens freezes via react-freeze), so React
runs the component and then discards that render — but the counter bumps
survive it. The next committed render then produced a new key and remounted the
form, throwing away the picked source for a host switch or a close that never
committed.

Hold the open epoch in state, which React discards with the render that set it,
and put the host in the key directly instead of counting host changes.
2026-08-28 15:45:54 -07:00
Brennan Benson 81ae98e10d fix(mobile): honor host worktree create retention (#16342)
* fix(mobile): honor host worktree create retention

* fix(mobile): cover malformed worktree retention policy

* fix(mobile): fail closed on malformed retention policy

* fix(mobile): fail closed on missing dedupe ttl
2026-08-27 16:02:38 -07:00
Brennan Benson 30bf2647fc fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it (#15472)
* fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it

A socket close or response timeout rejects an in-flight worktree.create as
delivery-unknown: the frame reached the wire, so the host may already have
built the worktree. The client only replayed connection-migration cutovers,
so every other ambiguity surfaced as a create failure for a create that may
well have succeeded. Replay on the same clientMutationId — which the host
already dedupes — after waiting for the transport to come back.

* fix(mobile): bound the ambiguous worktree.create replay by the host's dedupe window

The replay was bounded only by a retry count, but what makes a replay reconcile
instead of building a second worktree is wall clock: the host drops a settled
create's dedupe record 60s after it resolves, and past that the replay is just a
fresh create that the host's suffix loop happily duplicates — for a folder
workspace, into a second workspace with the very same name and no collision
check at all.

Two paths ran past that window:

- The request-timeout path. A silently dropped response frame leaves the socket
  alive, so nothing rejects until WORKTREE_CREATE_TIMEOUT_MS — ten minutes, with
  no bound at all on when the host actually resolved. This was previously the
  path that replayed *soonest*, short-circuiting the reconnect wait because the
  transport still looked healthy. Invert it: every path that reports a real drop
  has already left 'connected' by the time the rejection surfaces, so still being
  'connected' identifies the timeout and is now refused.
- The reported-drop path. Worst-case detection is a full liveness idle period
  plus the missed-probe budget before the client even learns the socket is dead,
  and the old 20s wait on top of that overran the record. Derive the wait from
  the watchdog constants and the TTL instead of hardcoding it, and anchor a
  single deadline at the first ambiguity so a second wait gets the remainder
  rather than restarting.

The TTL now has one definition shared by both processes, so the client asserts
its budget against the host's real window instead of a copied literal.

* fix(mobile): end the reconnect wait on a revoked pairing, and pin the wait's behavior

waitForRpcClientReconnected resolves only on 'connected' or the timeout, but an
'auth-failed' client never reaches 'connected' — so a create interrupted by a
revoked pairing sat out the full wait before surfacing the error it already had.
Treat auth-failed as a terminal answer on both the fast path and the listener.

The helper also shipped with no tests of its own: its already-connected fast path,
its timeout path, and the synchronous-notification-during-subscribe teardown were
only ever exercised indirectly through the retry suite, and neither RpcClient
implementation notifies synchronously, so that branch had no coverage at all. Add
a direct suite covering all of them, asserting listener and timer teardown rather
than just the resolved value.

Also give the fake-timer tests an explicit timeout. advanceTimersByTimeAsync
yields through real macrotasks between ticks while vitest's own budget runs on
real time, so on a loaded runner the default 5s is reachable — observed once as a
spurious timeout in this suite.

* fix(mobile): bound the ambiguous replay in wall clock, not timer time

The replay window was derived from the liveness watchdog's own budget
(idle + missed probes x probe timeout). That is a bound on how long the
watchdog takes to *fire*, not on how much wall clock passed. iOS and
Android suspend JS timers while the app is backgrounded, so across a
background cycle the socket dies silently and the pending create rejects
delivery-unknown minutes later with the timer-derived ceiling still
reading ~44s. The replay then lands well past the host's 60s dedupe
record and the suffix loop builds a SECOND worktree - for a folder
workspace, one with the very same name and no collision check at all.

Anchor the deadline on the watchdog's lastInboundAt instead: a wall-clock
stamp of a frame that really arrived, so it stays honest across a
suspension. Fall back to the send time when the transport can't vouch for
one (relay sessions run with idleProbeMs: null), which errs toward
refusing the replay.

Also restore the delivery-unknown discrimination test that the
still-connected guard had made vacuous, pin the still-connected guard
itself against a live inbound stamp, and pin the deadline against being
re-read from a fresher replacement session.
2026-08-19 18:10:39 -07:00
Brennan Benson ab9d1a29a9 fix(worktree): never reissue a generated workspace name (#14350)
* fix(worktree): never reissue a generated workspace name

Generated workspace names were deduped only against currently-live
worktrees, so deleting a workspace returned its name to the pool. A later
workspace could draw the same name, land on the same directory path, and
inherit the previous occupant's agent conversation history — coding-agent
CLIs key their prompt history and transcripts by cwd.

Names are now retired permanently per repo. The registry is written in
main with the name Git actually used (the create loop can advance past a
requested name on collision), and seeded once per run from workspace
directories and surviving agent transcript buckets so already-spent names
are excluded from the start. Suggestions degrade to -2, -3 variants
instead of recycling, and those variants retire too.

User-typed names are untouched: retirement filters suggestions only.

* fix(mobile): honor retired workspace names, on one shared implementation

Mobile hand-duplicated the desktop name-suggestion algorithm and deduped
only against live workspaces, so a phone could still be offered a name
whose deleted workspace left agent conversation state behind at that path.

Both platforms now call one shared selector in src/shared, so the two can
no longer drift. The host publishes retired names as an optional field on
the existing worktree.list response, and mobile fetches them per selected
repo while the create sheet is open — mirroring the desktop hook.

Mobile never calls worktree.list for its catalog (it uses worktree.ps,
which carries rows only), so this is a targeted request rather than a
change to the catalog or its cache. Hosts predating the field omit it and
mobile falls back to live-only dedupe, which is the pre-change behavior.

* fix(worktree): close retirement consistency gaps

* test(worktree): cover retirement runtime contracts

* fix(worktree): retire generated collision names

* fix(worktree): enforce retired names at creation

* refactor(ai-vault): extract the Claude project-dir encoder

The bucket-name encoder and its scope-boundary check were private to the
session scanner, so a second consumer had to reimplement them — and got the
per-character encoding wrong. Move both to a shared module with direct tests.

* fix(worktree): make the retirement seed scan actually match buckets

The bucket encoder collapsed runs of non-alphanumerics while the real one
emits a dash per character, so every dot-path bucket missed and the Windows
default workspace root (C:\...) matched nothing at all. Reuse the shared
encoder and its boundary check, which also stops a repo absorbing a sibling
whose path merely shares its prefix.

Also:
- Derive the workspace leaf by stripping the known encoded parent instead of
  guessing from trailing dash segments, which retired the parent directory's
  name whenever a workspace was named numerically.
- Reuse isAutoGeneratedCreatureBranchName so the -10 and -100 tiers retire.
- Drop the .codex/sessions root: Codex keeps the cwd inside the transcript
  rather than in a directory name, so the scan could only ever see a year
  folder. Reading transcript contents is not a trade this feature justifies,
  so the gap is documented instead.
- Honor CLAUDE_CONFIG_DIR, which relocates the bucket root.
- Delete the unused retirableLeafName export.

Tests write buckets with the real per-character encoding against a fake home,
covering POSIX, dot-directory, Windows drive and WSL UNC roots; all three
platform cases fail against the previous encoder.

* fix(worktree): retire only generated names, keyed by cwd namespace

Two problems in the host-side registry.

Retirement fired for every create, including names the user typed. The
creature pool contains ordinary words — orca, runner, sole, molly, oscar — so
typing a retired 'nautilus' silently produced directory and branch
'nautilus-2' and burned the name for good. Creates now carry an explicit
nameWasGenerated flag; both the skip and the retire are gated on it, and it
defaults to false so CLI and automation callers are unaffected.

The registry was keyed by repo id, but both readers already discarded the id
and unioned by the cwd collision key, because the collision this prevents is
on the path. Keying by that namespace directly fixes several things at once:
entries no longer orphan when a repo is removed, remove/re-add no longer loses
every retirement for an unchanged path, the missing removeProject prune is
moot, and the backfill promise no longer merges into only the first repo id it
saw. The feature is unreleased, so no migration is needed.

Also:
- Memoize the collision key. It runs computeWorktreePath, which for a WSL repo
  is a blocking execFileSync('wsl.exe') whose failure path is uncached, and
  the previous code recomputed it once per repo on every create and every
  listRetiredNames call.
- Drop retiredNamesByRepo from the worktree list result. It had no readers and
  leaked onto 'orca worktree list --json', and its awaited backfill sat on CLI
  selector resolution. The dedicated listRetiredNames RPC keeps its consumers.
- Make the three RuntimeStore methods required. RuntimeStore is file-private
  with two constructors, so the 'older embedders' the optionality protected do
  not exist, and the optional chain silently returned no retirements.
- Revert the unrelated forceDeleteBranch rewrite, and make room under the
  file's line budget by extracting the create-args mapping instead.

* fix(worktree): send name provenance and stop gating Create on the fetch

Desktop and mobile now mark a create as generated-name only when the user
typed nothing and the composer fell back to the suggestion, so the host knows
which names it may retire.

Remove the retired-names loading gate from every create path. The host already
skips retired candidates before doing any git work, so the client gate bought
nothing while it could disable Create for the length of a full mobile
reconnect ladder (the wait had no timeout) and blank the desktop button
between queued creates. The suggestion still waits; the button never does.

Also make the web client call worktree.listRetiredNames instead of hardcoding
an empty list — the method is registered and mobile-allowlisted, so the
comment claiming no wire call existed was wrong — and filter the mobile
response to strings so a malformed row cannot throw during normalization.

* fix(worktree): key retirement by repo id and prune it with the repo

Reverts the collision-key storage key. It was a function of workspaceDir,
nestWorkspaces, worktreeBasePath and repo.path, so toggling any one of those
orphaned every retirement for every affected repo at once — trading a rare
churn (remove/re-add) for a common one. The read path already unions by cwd
namespace at query time, so cross-repo sharing never depended on the storage
key.

Instead, address the growth and orphaning directly:
- Drop the registry in removeProject, and in removeProjectForHost once the last
  host's copy of the repo id is gone, alongside the sparse-preset deletes that
  already follow this convention.
- Bound each repo's registry. The cap sits far above the 552-name pool because
  evicting inside it would reissue a name whose agent state is still on disk;
  only -2/-3 tier accumulation can ever reach it.
- Carry retirements through profile transfer, re-keyed to the destination repo
  id and dropped from the source, mirroring sparsePresetsByRepo.

Separately, fix the backfill merge: the scan promise is cached per cwd
namespace, but it closed over the first repo id that triggered it, so a second
repo in the same namespace received nothing. The scan stays shared; the merge
moves out of the cached promise and runs for whichever repo asked.

Local repos re-seed on re-add through that backfill. SSH repos do not — the
scan cannot see the execution host — which is now stated in the module.

* docs(worktree): spell out why the retirement bound sits above the pool

Names the trap directly: the neighbouring 50/200 bounds cap histories, so
lowering this one to match them would silently start reissuing names whose
agent state is still on disk. Also states that oldest-first eviction is a
deliberate least-bad choice rather than a neutral one.

* fix(worktree): send name provenance from the web runtime client

This client hand-enumerates worktree.create params, so the new optional field
was silently dropped and typecheck could not see it. On web and paired-desktop
the host therefore never received it: generated names were never retired, and
the host-side skip that backstops a stale suggestion was disabled too. The same
client does fetch retired names for suggestions, so it was filtering against a
registry nothing ever wrote to.

The test asserts both directions, and fails without the fix.

* fix(worktree): retire names that took more than one collision suffix

isAutoGeneratedCreatureBranchName strips exactly one trailing -N, which is
right for auto-rename eligibility but wrong here. Once the pool is spent the
suggester emits nautilus-2, and a collision on that yields nautilus-2-3 —
which a single strip leaves as nautilus-2, not a pool name, so retirement
no-opped at exactly the tier where every base name is already gone. Strip
repeated suffixes locally rather than moving the auto-rename predicate.

* perf(worktree): keep the retirement backfill off the blocking WSL probe

The backfill runs on composer repo-select, not just at create time, and it
derived the probe path synchronously — which for a WSL repo with a mirrored
workspace dir reaches getWslHome and its blocking execFileSync('wsl.exe').
A stopped distro froze the main process for up to 5s on composer open.

Adds an async twin of computeWorktreePath and uses it for the probe. Resolving
the home there also warms the shared cache, so later sync callers are free.

Also stops memoizing the collision key when the WSL home is still unresolved:
only the success path is cached upstream, so caching the fallback namespace
would strand the repo there for the rest of the session.

* fix(worktree): hold retired names across a refresh instead of blanking

refreshKey changes on every workspace-list mutation, so create-multiple
refetches after each create and the hook returned an empty list until the
refetch landed — precisely the window in which resetForNextCreate clears the
name field and a fresh suggestion is drawn. Keep the previous answer while
revalidating and reset only when the repo changes; a failed refresh keeps what
was already loaded rather than un-retiring everything.

Also makes the returned array referentially stable, so the suggestion memo
downstream stops rerunning on every refetch.

* refactor(worktree): put the retired-name cache rules on one implementation

The desktop and mobile hooks that fetch retired names had already drifted
four ways. The transports genuinely differ (IPC vs RPC), but the caching
rules must not, and mobile's copy reset to [] on any error -- which
un-retires every name for the rest of the sheet session, the one outcome
retirement exists to prevent.

Moves the rules into src/shared/worktree/retired-name-cache: response
normalization, the never-leak-across-repos rule, and the hold-previous-on-
failure rule. Pure, no React, because src/shared is on the main process's
import graph. Each platform keeps its own transport and effect.

Mobile moves up to desktop's behavior: it now holds the previous answer
through a failed refresh, and refetches when the workspace list changes
instead of never refetching after mount.

Also drops the unused `loading` return. Neither platform consumed it; its
only consumer was the Create-button gate reviewed out earlier, and removing
it makes that regression unexpressible.

* fix(worktree): import shared types from their real modules

Main dropped the src/shared/types barrel, so the retirement module's import
resolved locally but not against the PR's merge base.

* refactor(worktree): bound the retirement registry by tier compaction, not eviction

Retirement is a correctness guarantee — a spent name's directory may still hold
agent conversation state keyed by that cwd — so the 2000-entry cap was the wrong
shape: reaching it handed a name back. At the owner's measured rate (~6.6 pool
names retired per day in one repo) the cap was ~9 months out.

Names come from a fixed 552-entry pool and the suggester only reaches tier N+1
once every tier-N name is taken, so a completed tier is exactly a set that no
longer needs listing. A row is now a watermark plus the names above it: reads
answer at-or-below the watermark with no lookup, and compaction drops the 552
entries the watermark now covers. Bounded at one pool per repo forever, with no
eviction and nothing un-retired.

Tiers can complete out of order (a create-time collision can spend `nautilus-2`
while tier 1 is open), so compaction loops and higher-tier names simply wait.

The RPC result carries the watermark beside the names as a new field; a client
predating it reads the names only and under-retires the compacted tiers, which
degrades to the pre-retirement behavior rather than breaking.

* fix(worktree): preserve generated name retirement across failures
2026-08-14 22:18:36 -07:00
Brennan Benson 266b5ae8f5 fix(mobile): match desktop project and run target picker (#14457)
* fix(mobile): disambiguate repository locations

* fix(mobile): preserve explicit repository ownership

* test(mobile): use explicit renderer type

* refactor(mobile): match desktop project targets
2026-08-14 10:22:55 -07:00
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00
Neil 583ab1601b refactor(shared): group worktree, github, and linear modules into folders (#14437)
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.

Move each domain into its own folder and drop the now-redundant prefix:

    src/shared/github-pr-types.ts    -> src/shared/github/pull-request-types.ts
    src/shared/worktree-id.ts        -> src/shared/worktree/id.ts
    src/shared/linear-links.ts       -> src/shared/linear/links.ts

This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.

Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.

Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.

Two things `tsc` cannot catch, handled explicitly:

- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
  entry is REPOINTED to the new path rather than pruned. Pruning would drop the
  bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
  (`mobile/node_modules` is empty). Instead every relative specifier in the repo
  was resolved against the filesystem: 174 unresolved before this change and 174
  after — identical, so nothing broke in mobile either.

The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
2026-08-13 20:44:16 -07:00
Neil 9796f7dc5f refactor(mobile): use shared GitHub Project sorting (#13459) 2026-08-10 20:41:34 -07:00
Jinwoo Hong 427f178a75 fix(mobile): load the repo list in GitHub Project mode (#12972) 2026-08-09 14:55:31 -07:00
EvgeniiandBrennan Benson 63af0bdd24 feat(agents): add Prime Agent as a supported TUI agent with session history (#12935)
* feat(agents): add Prime Agent as a supported TUI agent with session history

Wire Prime Intellect's prime-agent CLI (a Pi fork) into the desktop and
mobile agent catalogs following the Trae registration pattern, and into
the Agent Session History browser following the OMP pattern:

- types.ts, tui-agent-config.ts: register 'prime-agent' with argv prompt
  injection behind a `--` separator (its own help documents `--` as
  "treat all following arguments as messages"; without it, prompts
  starting with `help`/`agents`/`-…` dispatch as subcommands or
  flags), plus csi-u Shift+Enter encoding matching the Pi TUI it embeds.
- agent-kind.ts, telemetry-events.ts, agent-status-types.ts,
  agent-type-label.ts, tui-agent-display-names.ts,
  tui-agent-selection.ts, skills-cli-agent-keys.ts: standard per-agent
  registrations.
- agent-headless-command.ts: `-p/--print` one-shot runs share the
  print-mode matcher with Claude/Trae so they are not mistaken for live
  interactive panes.
- agent-process-recognition: the npm shim launches a generic bundled
  cli.js, so only the exact package path is an authoritative identity
  (same as Pi and cursor-agent). The three per-agent regex branches are
  now one table in agent-node-entrypoint-identities.ts — the module was
  at its max-lines budget and a table makes the next agent one entry.
- AI Vault: sessions are Pi's message-graph JSONL under
  ~/.prime/agent/sessions (override PRIME_AGENT_CODING_AGENT_DIR —
  Prime Agent brands Pi's env contract instead of sharing
  PI_CODING_AGENT_DIR); parsed by the shared message-graph parser with
  incremental append-resume; discovered locally, in WSL homes, and over
  remote SSH; resumes by absolute transcript path
  (`prime-agent --resume <path>`) like OMP, with session-id fallback.
- skill-discovery-sources.ts: ~/.prime/agent/skills home source.
- Catalog, i18n (en/es/ja/ko/zh), mobile registries, and a bundled
  64x64 favicon (required by mobile's offline-icon invariant).

Scanner-test fixtures for OMP and Prime Agent move into
session-scanner-test-fixtures.ts and the incremental fixture into its
own module, keeping every touched file inside its max-lines budget
without ratchet bumps.

* fix(ai-vault): map custom Prime Agent roots to their sessions child

PRIME_AGENT_CODING_AGENT_DIR is consumed verbatim by the CLI as its agent
config dir, with transcripts always in <agentDir>/sessions — unlike
PI_CODING_AGENT_DIR's <home>/agent/sessions shape the shared normalizer
models. A custom root with a non-special basename (or a `.prime` leaf)
was therefore scanned as-is instead of its sessions child. Dedicated
normalizePrimeAgentSessionsDir appends `sessions` to every configured
root, taking only an explicit `.../sessions` path as-is; the shared
Pi/OMP normalizer drops the `.prime` widening it no longer needs.

Raised in review on #12935.

* fix(ai-vault): guard degenerate Prime Agent roots and cover the remote source

normalizePrimeAgentSessionsDir stripped a filesystem-root value ('/' or '//')
to '', which then joined into the relative root 'sessions' and would walk the
main-process cwd. session-scanner-roots.ts already carries this guard for the
OMP variant; apply the same fallback here.

The remote SSH source had no test: deleting jsonlSource('prime-agent', ...)
left the suite green, unlike the local path which is pinned by the
AI_VAULT_AGENTS exhaustiveness assertion in session-scanner.test.ts. Add a
case that fixes the .prime/agent/sessions root segments, the .jsonl
extension, and parser routing.

Raised in review on #12935.

* fix(ai-vault): honor Prime Agent's sessions-root env and non-interactive modes

Verified against upstream PrimeIntellect-ai/prime-agent source rather than
inferred from the CLI's help text.

config.ts getSessionsDir() reads PRIME_AGENT_SESSION_DIR (and its legacy
PRIME_AGENT_CODING_AGENT_SESSION_DIR alias) ahead of the agent dir and uses it
verbatim; setting either left the vault silently empty. It also appends
`sessions` to the agent dir unconditionally, with no basename escape hatch, so
PRIME_AGENT_CODING_AGENT_DIR=/data/sessions writes to /data/sessions/sessions
while Orca scanned /data/sessions. getAgentDir() and the session-dir override
both run through expandTildePath, so a `~` value set outside a shell resolves.

cli/args.ts also spells the non-interactive runs `--mode json|rpc|acp|daemon`,
which the shared print-mode matcher does not know, so those panes were counted
as live interactive agents and the paste-submit path would write user text into
a JSON-RPC/ACP stream. Match upstream exactly: only the space-separated form,
since `--mode=json` is not parsed by the CLI and does start the TUI.

Raised in review on #12935.

* fix(ai-vault): keep Prime Agent roots absolute and remote segments posix

Two holes in the previous commit.

The degenerate-root guard only rejected pure-separator values, so a relative
env value still resolved against the main-process cwd:
PRIME_AGENT_CODING_AGENT_DIR='.' scanned '<cwd>/sessions' and, worse,
PRIME_AGENT_SESSION_DIR='.' scanned the cwd itself. Require an absolute path in
both branches and fall back to the default otherwise.

remotePrimeAgentSessionsSegments() built its segments with the local-platform
join, so on a Windows client scanning a posix SSH host it produced
'\.prime\agent\sessions' and split('/') collapsed it to one bogus segment —
remote discovery would have found nothing. Remote roots are posix regardless of
client platform, so keep them literal. Pi and OMP are unaffected: their
normalizer returns a '.../sessions' input unchanged and never joins.

Raised in review on #12935.

* test(ai-vault): pin Windows drive roots to the Prime Agent default fallback

'C:\' and 'C:/' strip to the drive-relative 'C:', which isAbsolute
rejects on every platform — assert they land in the default fallback so
a looser truthiness check can't reintroduce a 'C:sessions' scan root.

Raised in review on #12935.

* test(ai-vault): pin the drive-relative root form and state what the posix runner can assert

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-09 14:50:02 -07:00
Wooseong KimandJinwoo-H e68831f32c fix(github-project): index fork upstream slugs for project row matching (#12822)
* fix(github-project): index fork upstream slugs for project row matching

Project cards often reference the public upstream repo while the open
clone's origin is a personal fork. Map the parent slug to the same Repo
so selected-repo filters no longer hide every board row.

Preserves origin-based getRepoSlug identity for non-project callers.

Fixes #12647

* fix(github-project): match project rows against fork upstream slugs

Resolve the referenced call to a nonexistent `resolveRepoUpstreamSlug` and
match the persisted `repo.upstream` parent instead of issuing an extra
`github.repoUpstream` RPC per repo on every index build — that lookup shells
out to `gh repo view` for non-forks, so it would have gated the Projects tab
on N network calls. `repo.upstream` is already resolved at repo-add time and
backfilled at startup, so the fix costs no IPC.

Origin matches take precedence over upstream ones so an open clone of the
upstream repo itself is never made ambiguous by someone's fork of it.

Also covers the two surfaces the origin-only match broke alongside the desktop
table: mobile's project row matcher and the store-slice row-mutation routing.

* fix(github-project): scope fork upstream matching by host and selection

Round-1 review fixes on top of the upstream-slug index:

- Apply origin-over-upstream precedence among *selected* repos instead of
  globally. An open-but-unselected clone of the upstream repo was shadowing the
  selected fork, so #12647 still reproduced for anyone holding both — and repo
  selection collapses to one repo per project key, which is exactly that case.
- Scope a fork's upstream identity key to the fork's own origin host.
  Persistence strips upstream.host, so GHES forks never matched their own rows
  and a GHES fork's parent could bind a same-named github.com row.

* fix(github-project): skip the fork alias when its own origin is unresolved

Round-2 review fix. `githubHostFromIdentityKey` cannot tell "origin resolved to
github.com" from "origin did not resolve" — both yield no host. A GHES fork
whose slug resolution had failed (auth lapse, unreachable runtime) therefore
landed in the github.com namespace, so an unrelated public Project row matched
it and Start work opened the wrong clone on the wrong server.

Require a resolved origin before indexing the upstream alias: it is the only
host evidence there is, and a repo with an unresolved origin was already absent
from the origin index, so nothing is lost that origin matching had.

* fix(repos): persist the fork upstream host instead of dropping it

`sanitizeRepoUpstream` kept only `{owner, repo}`, so a fork's parent lost the
server it lives on every time the record round-tripped through disk.

That forced the Project row matcher to re-infer the host from `origin`. The
inference is right for an API-resolved fork parent — `getRepoUpstream` stamps
`origin.host` there precisely because "a fork parent lives on the same server as
the fork". It is wrong for the other branch: a local `upstream` remote carries
its own host, so a github.com clone with a GHES `upstream` remote was indexed
into the github.com namespace, where an unrelated same-owner/name public repo
could claim it and Start work would open the wrong clone.

Keeping the host removes the guess. Absent stays absent, so records written
before this hydrate unchanged and the origin-derived fallback still covers them.
Also fixes the avatar for rehydrated GHES forks, which resolved against
github.com for the same reason.

* docs(github-project): correct upstream host fallback comment

Persistence now keeps non-empty upstream.host; originIdentityKey remains
the host fallback for older records without one (CodeRabbit nit).

* fix(github-project): own slug-index retry timer cleanup

Move the failure-retry setTimeout into its own effect so cleanup always
clears it. Scheduling from the async buildIndex then-handler failed the
react-doctor effect-needs-cleanup gate in static analysis.

* test(github-project): guard the slug-index retry timer, fix the mobile twin comment

Two follow-ups on 52298d82 and 2f89c20d:

- Cover the retry timer both ways: a failed resolution still re-resolves after
  the TTL and recovers the match, and the pending timer is gone after unmount.
  The second fails if the timer moves back into the async then-handler, so the
  property is guarded by more than the lint rule.
- The mobile matcher's comment made the same stale "persistence strips
  upstream.host" claim that 2f89c20d fixed on the renderer side.

* test(github-project): unmount slug-index hooks so React cannot flush after teardown

CI shard `tests node 24 6/16` failed with 10 unhandled
`ReferenceError: window is not defined` traced to this file. The tests mounted
hooks without unmounting, so React scheduler work flushed after the DOM
environment was disposed. All assertions passed; the shard failed on the
unhandled errors alone.

`cleanup()` after each test unmounts the trees. Does not reproduce locally in
isolation — it needs CI's worker pooling and file ordering.

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 18:43:28 -07:00
Brennan Benson 885afb55a9 fix(mobile): open host editor from root navigation (#12766)
* fix(mobile): open host editor from root navigation

* test(mobile): update task navigation router contract
2026-08-05 13:32:58 -07:00
Kaynan Sampaio de Camargoandkaynan 7948e46db8 fix(mobile): open the Resume workspace through a mounted host stack (#12001)
* fix(mobile): open the Resume workspace through a mounted host stack

Tapping Resume on Home landed on a blank host screen instead of the
session. A cold push straight into the nested /h/[hostId] navigator
resolves to the host index route without the dynamic id, so
HostProtocolGate mounts with hostId undefined and never connects.

Home already worked around this for the host editor (#11635) and Tasks
(#11853) by mounting /h/[hostId] first and replacing it once the stack
is committed. Extract that mechanism into host-stack-navigation so
Resume uses the same transition instead of a direct push.

The previous Resume fix (#11876) swapped the manual href for a typed
dynamic href, but expo-router's encodeParam already applies
encodeURIComponent to dynamic segments, so it resolved to the same URL
the manual string produced and left the cold-navigator path unchanged.

Claude-Session: https://claude.ai/code/session_01RMoaxp7MLg2ydP28KFLX7B

* fix(mobile): harden the host-stack transition after bot review

- match a host route committed as the encoded segment it was pushed as,
  so an id containing `/`, `#`, or `%` still triggers the REPLACE
- share one pending transition across the Home entry points; per-hook
  refs let a Tasks tap and a Resume tap arm two pushes that could not
  cancel each other
- assert the source markers before slicing in the Resume wiring test

Claude-Session: https://claude.ai/code/session_01RMoaxp7MLg2ydP28KFLX7B

* test(mobile): lock the host-stack transition state machine

Assert the replace waits for the host mount (zero dispatches before it,
exactly one after), and cover cancel/retarget — the paths the shared
pending transition relies on.

* test(mobile): model listener removal in the navigation harness

A no-op unsubscribe let setState keep calling a canceled listener, so the
teardown assertions only exercised the active guard. Dropping the
unsubscribe call from dispose now fails the suite.

---------

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
2026-08-04 16:06:37 -07:00
Brennan Benson 4c03cdff72 fix(mobile): mount host before opening tasks (#11853) 2026-07-31 20:46:51 -07:00
NeilandOrca fdb58695e9 [P1] fix(checks): stop skipped and manual checks reporting as failures (#11700)
* fix(checks): stop skipped and manual checks reporting as failures

Route every check-classification surface through one shared helper so
desktop renderer, desktop main and mobile agree on the same verdict.

- GitLab `manual` jobs and pipelines are neutral again, not action_required/failure
- `skipped` counts as passed everywhere, including mobile
- a neutral check no longer demotes a summary that has passing checks

* fix(checks): move the check-classification parity test into the renderer project

The parity table lived in src/shared but imported a renderer module, and both
config/tsconfig.node.json and config/tsconfig.cli.json are composite projects
that include src/shared without that renderer path, so `pnpm typecheck` failed
with TS6307 on two of its three projects. Only the web project spans both trees.

Co-authored-by: Orca <help@stably.ai>

* fix(checks): stop the Tasks-grid pill contradicting its own verdict

The checks pill's label, tone and icon all read one ProviderCheckSummary, but
getChecksLabel short-circuited on the raw `neutral` counter while the tone and
icon key off `state`. After the classification fix a PR with 19 success + 1
neutral renders an emerald CheckCircle2 pill that reads "1 unresolved", and
mobile's own label (which keys off `state`) reads "19/20 passed" for the same
summary.

Move the label into src/shared/provider-check-summary.ts so desktop and mobile
cannot fork it again, and key it off `state`.

Also covers deriveWorkItemCheckSummary, the desktop-main producer of the summary
that reaches the Tasks grid and the relay-paired mobile client. It was rewritten
here with no test at all; the parity table stands in derivePRCheckStatusFromRollup,
which is a different normalizer. The new main-process test drives getWorkItem with
a real statusCheckRollup fixture, pinning the StatusContext `state` fallback that
would otherwise be deletable with the whole suite still green.

Co-authored-by: Orca <help@stably.ai>

* fix(gitlab): route the pipeline job-array rollup through the shared check classifier

The array path in derivePipelineStatus kept its own copy of the rollup rules, so
manual-only read green and one unrecognized job status demoted a passing pipeline
to neutral — both disagreeing with every other check surface.

Also retry the packaged-CLI smoke temp cleanup on Windows: the copied Orca.exe can
still be locked by AV/indexers after every assertion passed, failing the package job.

Co-authored-by: Orca <help@stably.ai>

* fix(gitlab): stop the skipped pipeline string diverging from the Checks tab

- classifyPipelineString now counts a skipped pipeline as passing, matching
  the per-check classifier; canceled stays neutral and is pinned as an
  explicit, sign-off-pending divergence.
- Pin the production string path (head_pipeline.status) in the parity table
  and note that the job-array branch has no production caller yet.
- Count skipped checks in the Checks panel's passing header so it agrees
  with the checks pill.
- Correct the packaged-CLI smoke retry comment: the EBUSY is the smoke's own
  just-exited Electron process, not AV/indexers.

Co-authored-by: Orca <help@stably.ai>

* fix(checks): finish cross-surface check parity and back out the skipped MR-card flip

Review follow-ups on the check-classification PR.

- PullRequestPage and GitHubItemDialog kept private copies of getCheckCounts /
  getChecksSummaryLabel that still counted only `success` as passing, so a
  2-success/3-skipped PR read "2 passing · 3 skipped" there and "5 passing" in
  the sidebar. Both copies move to pr-check-counts.ts, which routes the passing
  bucket through classifyCheckOutcome; action_required keeps its own amber
  bucket. The summary icon now keys off passing count, so an all-neutral PR
  stops painting a green tick above "0 of N checks passing".
- The sidebar checks header and triage strip still called
  `{status: completed, conclusion: null}` pending, contradicting the grey
  "Unresolved checks" pill. Both now read summarizeProviderChecks and render an
  unresolved chip/strip instead of an amber spinner that can never resolve.
- classifyPipelineString('skipped') is reverted to neutral. That flip painted
  MR cards green for pipelines that never ran, on the only GitLab path with
  production callers, and contradicted the same function's deferral of
  `canceled`. Both tone changes stay deferred, pinned by one test.
- classifyPipelineString('manual') resolves to pending rather than neutral: a
  blocked pipeline is outstanding, and neutral let the worktree card fall
  through to its emerald `open` default while GitLab still refuses the merge.
- TaskPage's checks pill helpers move to task-page-checks-pill.ts so the
  "1 unresolved on a green pill" fix is actually pinned by a test.
- smoke-packaged-cli no longer lets an EBUSY cleanup replace the real failure.

* fix(checks): stop completed unknown checks from spinning

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-31 04:58:15 -07:00
KyuJoo HanandOrcaWin 6f3845baa4 fix(checks): rank successful checks above skipped and neutral (#11337)
* fix(checks): rank successful checks above skipped and neutral

Checks were ordered with `skipped` (4) and `neutral` (3) ahead of
`success` (5), so a PR with a long tail of skipped jobs pushed every
passing check below the fold — you scroll past a wall of "Skipped" to
find out whether anything actually ran.

Rank the no-signal conclusions last (`success` 3, `neutral` 4, `skipped`
5) and pull the order out of its three duplicated copies
(checks-panel-content, PullRequestPage, GitHubItemDialog) into
`src/shared/pr-check-severity-order.ts`. Unknown conclusions now sink to
the bottom instead of silently ranking as `neutral`.

* fix(checks): look up check ranks through a Map, not an object literal

An object-literal rank table resolves `constructor`, `toString`, and
`__proto__` off Object.prototype, so those keys returned a function
instead of falling through to UNKNOWN_CHECK_RANK — the comparator then
subtracted functions, went NaN, and left the list in arbitrary order.
Conclusions come from provider payloads, so keep the lookup on a Map and
cover prototype property names in the test.

* test(checks): cover provider-neutral ordering states

* fix(checks): preserve actionable provider states

* fix(checks): preserve unresolved provider rollups

* fix(checks): keep unknown GitLab rollups neutral

* fix: preserve neutral review check summaries

* fix: complete provider-neutral check ordering remediation

* fix: use provider-neutral mobile review status input

* fix: hydrate GitLab mobile review status

* fix: type mobile GitLab review hydration

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-30 13:29:41 -07:00
2d23217166 feat: add Trae CLI as a supported TUI agent (#10763)
* feat: [AI-GEN] add Trae CLI as a supported TUI agent

Closes #10579.

Wire trae-cli into the desktop and mobile agent catalogs following the
same integration pattern as other CLI agents (e.g. Ante, Devin):

- src/shared/types.ts, tui-agent-config.ts: register 'trae' with
  detectCmdAliases (traecli/trae-agent) and argv prompt injection,
  matching trae-cli's `trae-cli [prompt]` contract. The CLI's own
  third documented alias `ta` is intentionally excluded — too generic
  a 2-letter name to use as a PATH-existence detection signal without
  false-positiving on unrelated tools.
- src/shared/trae-headless-command.ts: recognize `--print`/`-p` and
  `--output-format json|stream-json` as one-shot headless invocations
  (same shape as claude-headless-command.ts) so they aren't mistaken
  for a live interactive session.
- agent-kind.ts, telemetry-events.ts, agent-status-types.ts,
  agent-type-label.ts, tui-agent-display-names.ts,
  tui-agent-permissions.ts (YOLO via trae-cli's own --yolo flag),
  tui-agent-selection.ts: standard per-agent registrations.
- agent-catalog.tsx, agent-favicon-assets.ts,
  mobile/src/tasks/mobile-tui-agents.ts,
  mobile/src/components/mobile-agent-icon-assets.ts: catalog entries
  and bundled favicon (fetched from docs.trae.cn, required by mobile's
  offline-icon invariant test).
- i18n: add the "Trae" label to all five locale catalogs (en/es/ja/ko/zh).
- Tests: agent-process-recognition, agent-status, tui-agent-startup.

Verified with `pnpm typecheck` (desktop + mobile), the relevant vitest
suites (869 tests across 12 files, all green), oxlint (clean), and a
real end-to-end launch of the actual trae-cli binary through Orca's
pty.spawn IPC path (confirmed via the OS process table).

* fix: [AI-GEN] point Trae catalog entry at the real CLI quick-start doc

docs.trae.cn/cli (what the installed CLI's own --help text prints as
its "User manual" link) soft-404s — the docs site restructured and the
working page is docs.trae.cn/cli_get-started-with-trae-cli (confirmed
by HTTP fetch: real page title "TRAE CLI 快速开始" vs the old path's
"404 - 页面不存在"). Addresses CodeRabbit's homepageUrl review comment.

* fix: [AI-GEN] detect Trae on traecli, not the ambiguous trae-cli name

Per @AmethystLiang's review: the open-source bytedance/trae-agent
project (MIT, ~12k stars) registers its own console script as
`trae-cli` (pyproject.toml: `trae-cli = "trae_agent.cli:main"`), an
entirely unrelated CLI with a different contract (`trae-cli run
"task"`, `-p` short for `--provider`). Detecting on bare `trae-cli`
would false-positive on that project's installs and break launch for
anyone who has it instead of the actual TRAE CN CLI.

- tui-agent-config.ts: detectCmd/launchCmd/expectedProcess -> `traecli`
  (TRAE CN's own installer symlinks this alias too, but the other
  project does not ship it). Dropped the `trae-agent` alias entirely —
  it's the colliding project's literal repo name, the highest
  false-positive string available.
- agent-catalog.tsx: cmd -> `traecli` to match; faviconDomain ->
  `www.trae.cn` (bare `trae.cn` 404s on Google's favicon service;
  `www.trae.cn` is the product-root domain that actually resolves).
- mobile-tui-agents.ts: faviconDomain -> `www.trae.cn` to match.
- Tests updated: agent-process-recognition now asserts `trae-cli` and
  `trae-agent` are NOT recognized as Trae (regression guard against
  reintroducing the collision); tui-agent-startup updated for the new
  launch command.

promptInjectionMode stays `argv` and the headless-command file stays
as-is — both verified against the real TRAE CN CLI's actual --help
output (pasted in the PR review thread), not assumptions.

* refactor: [AI-GEN] share one print-mode headless matcher across agents

trae-headless-command.ts was a rename-only fork of claude-headless-command.ts,
and ante-headless-command.ts carried a third copy of optionName. Collapse both
print-mode files into print-mode-headless-command.ts, dispatch from a
Partial<Record<TuiAgent, ...>> table instead of an if-chain, and compress the
Trae comments to the repo's one-line style.

* fix: [AI-GEN] terminate Trae flag parsing before the positional prompt

`traecli` is a Cobra CLI with subcommands, so an argv prompt starting with
`help`, `config`, `-…` was dispatched as a subcommand or flag instead of being
run as the task. Add `argvPromptSeparator: '--'` (same reason Grok has it), and
stop the shared print-mode headless matcher at `--` so a prompt that reads like
`--print` no longer drops the pane out of agent recognition.

* docs: [AI-GEN] name both Trae CLIs explicitly in the detect-name comment

Co-authored-by: Orca <help@stably.ai>

* docs: [AI-GEN] drop the vendor tag from the Trae union comment

Co-authored-by: Orca <help@stably.ai>

* fix: [AI-GEN] guard the nullable startup plan in the Trae separator test

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: 陈泽榜 <chenzebang@jianzhikeji.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-28 11:18:49 -07:00
Kaynan Sampaio de Camargoandkaynan 69d05b6e24 fix(mobile): resolve permission args when a new session launches an agent (#8469)
The New Workspace flow built a bare launch command client-side and sent it as
`startupCommand`, so the host ran it verbatim and never applied the default
launch args. The first Claude session therefore started in manual mode, while
opening another Claude via the "+" tab (which sends the agent id and lets the
host resolve args) started with `--dangerously-skip-permissions`.

Send `startupAgent` from every client-built create path (blank, reuse-branch,
and new-branch) so the host resolves the launch command, args, env, and
host-shell quoting through the same path the "+" new-tab and CLI use. The
work-item path already delegated via `startupDraft`. Custom `agentDefaultArgs`
are now honored on all paths.

Adds a shared `agentLaunchCreateFields` helper and removes the now-unused
client-side command map, which had also drifted from the canonical launch
commands for continue, hermes, command-code, kiro, and mistral-vibe.

Claude-Session: https://claude.ai/code/session_014iufZnQwPD2obYuvdahjaE

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
2026-07-24 00:29:19 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
JinjingandOrca 3708c4f6ce fix(mobile): report interrupted native chat sends as delivery-unknown, not failed (#10021)
* fix(mobile): report interrupted native chat sends as delivery-unknown, not failed

A terminal.send interrupted mid-flight showed a definite "Message not sent"
even when the desktop may have already delivered the text. Three paths were
misclassified as definite failures:

- Logical relay/direct cutover: migrateTo rejects in-flight requests with
  LogicalClientCutoverError, which mapped to 'rejected'. Now maps to 'unknown'
  (held unconfirmed + transcript-echo verification; never retried since
  terminal.send is non-idempotent).
- Suspend/close of a half-open session: the stable logical client blanket-
  rejected in-flight pendings with plain 'Client suspended'/'Client closed',
  preempting the physical layer's delivery-unknown marking. It now lets the
  physical close settle them, so post-write failures stay marked and pre-write
  failures stay definite.
- Relay path: mobile-relay-rpc-session never marked delivery ambiguity at all
  (timeout, close, link failure). Post-write rejections are now marked;
  pending entries only exist after the frame reached the authenticated link.

Permission, ask-answer, and cancel-Escape surfaces now show "unconfirmed —
check chat before retrying" instead of a definite "not sent" on ambiguous
outcomes (still not-accepted, never retried). Also consolidates a private
copy of isLogicalClientCutoverError in worktree-create-retry.

Co-authored-by: Orca <help@stably.ai>

* chore(skills): regenerate skill-bundle manifest artifacts

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 19:28:21 -07:00
Brennan Benson 6e6b7d8195 fix(mobile): retry session capability probe so tab-row actions survive relay cutover (#9794)
* fix(mobile): retry session capability probe so tab-row actions survive relay cutover

The session screen learned host capabilities (quick commands, browser
screencast, agent history, query-reply input) from a single status.get
fired when the screen connected. Over relay, a relay-to-direct transport
cutover rejects every in-flight request while connState stays
'connected', and a request timeout does the same — so one transient
failure latched the capability flags false (or left them null on an
ok:false reply) and the quick-commands tab-row button stayed hidden
until the screen was remounted.

Replace the one-shot probe with startRuntimeCapabilityProbe: retry
promptly after a cutover (the replacement transport is already
authenticated) and with capped exponential backoff on other failures,
until a probe lands or the effect is cleaned up. Also export the
cutover-error predicate from stable-logical-rpc-client and reuse it in
worktree-create-capability instead of a local copy.

* fix(mobile): reset runtime gates before capability reprobe
2026-07-21 17:55:02 -07:00
971b167548 fix(github): load PR diffs for Enterprise remotes (#8932)
* fix(github): load PR diffs for Enterprise remotes

* fix(github): encode PR content paths by segment

* Fix PR review actions failing on GitHub Enterprise remotes

- Threads GitHub host identity (not just owner/repo) through the client,
  work-item-details, issues, and RPC layers so gh commands target the
  correct Enterprise server instead of silently falling back to github.com
- Adds a shared github-api-repository helper to resolve/host-qualify repo
  identity consistently across REST, GraphQL, and CLI shorthand calls
- Scopes the gh rate-limit breaker and singleton rate-limit snapshot by
  host/runtime so a github.com block or probe can't affect GHES or WSL
- Coalesces concurrent host-auth probes and paginates PR file fetching
  beyond 100 results
- Propagates `host` through renderer PR caches, checks-panel keys, and
  preload IPC types so Enterprise and github.com data never collide

* Route gh host qualification through runner options instead of argv sniff

Move GHES/GH_HOST resolution from parsing --hostname/--repo out of gh argv to an explicit options.host passed through ghExecFileAsync, since SSH-backed repos spawn gh with no cwd and argv sniffing couldn't reliably detect the target host. The runner now injects --hostname and qualifies --repo/-R at spawn time from options.host, and rate-limit scoping/guards use the same explicit host instead of inferring it. Also adds a shared githubRepoIdentityKey helper to keep cache/store keys consistent with the new host-aware repository identity.

* Fix gh CLI GHES host pinning and rate-limit scope leaks

- Pin `--host` on every gh call site so a process-level GH_HOST can't
  silently redirect requests, and qualify `-R`/`-R=` repo shorthand
  alongside the existing `--repo=` handling.
- Check the target scope for an active rate-limit block before each
  WSL/native or host fallback retry, not just on the initial attempt,
  so a blocked scope can't be hit again through a fallback path.
- Compute idempotency once per call instead of re-deriving it after
  fallback reassigns args.

* Fix GitHub Enterprise host identity loss across PR/work-item paths

- Thread `host` through mobile PR RPC params, IPC work-item lookups, and
  RPC schemas so GHES identity survives the renderer/mobile/main boundary
  instead of silently falling back to a same-named github.com repo.
- Qualify `--repo`/`-R` args for github.com too (not just GHES), since
  gh resolves bare shorthand against a process-level GH_HOST that can
  redirect pinned github.com commands.
- Cache `getOriginGitHubApiRepository` to avoid a per-call uncached
  `git remote get-url` round trip on connection-backed repos.
- Add a local-fork fallback in `getWorkItemDetails` so PRs living on a
  base repo (not visible via the origin slug) still resolve via cwd.
- Centralize the github.com-vs-GHES host predicate in
  `isDefaultGitHubHost` so cache keys, quota scoping, and identity
  checks can't drift out of sync.

* Make repository identity host-aware across all GitHub surfaces

Generalize the auth-gated enterprise resolver to any remote and build a
cached hosted-identity family (origin/issue/candidates/source) on top of
it, then migrate every github.com-only consumer: Tasks listing/counting,
branch-to-PR discovery, push targets, fork upstream, issue operations,
Projects, web links, avatars, and PR-link facts. Scope the rate-limit
breaker probe per runtime:host and classify WSL UNC cwds correctly.

Co-authored-by: Orca <help@stably.ai>

* Fix expected slug to include host field in GitHub PR link test

Updates the smart-source paste-intent test fixture to match the
repository slug shape that now carries a `host` field, keeping GHES
host identity intact through the paste-intent parsing path.

* Surface per-host gh auth state for GitHub Enterprise

diagnoseGhAuth accepts the host a surface needs credentials for, scopes
the account/scope diagnosis to that host, and reports whether gh has any
login there; GhAuthErrorHelp renders host-qualified login/refresh
commands so an unauthenticated GHES host stops masquerading as a
github.com scope problem. Also fixes the mobile paste-intent expectation
for host-carrying parsed links.

Co-authored-by: Orca <help@stably.ai>

* Bound GHES identity caches and preserve non-default ports in host identity

Cap the origin-repo and host-auth caches like ownerRepoCache; keep ports
from remote/link URLs so GHES on a non-default port is a distinct
identity; make positional github.com slugs explicit against GH_HOST;
compare work-item sources by host-aware identity key; bail cwd-less
branch lookups when no repository candidate resolved; thread host
through the renderer work-item slug lookup.

Co-authored-by: Orca <help@stably.ai>

* Thread GitHub host through issue detail requests

Incorporates ghes-issue-host-support (ed6bb96ef): one hosted issue
repository identity is resolved before the details fan-out so comments,
timeline, participants, and mention lookups cannot drift across hosts,
with SSH guards so unresolved issue/PR repositories never fall through
to gh's default host.

Co-authored-by: Orca <help@stably.ai>

* Scope remaining GitHub rate-limit accounting

* Resolve typed PR lookups across hosted repository candidates

getWorkItem's PR path probes upstream-then-origin hosted candidates
instead of origin alone, so fork checkouts resolve the base repo's PR
with the right host; issue detail resolution reuses the up-front hosted
identity and keeps the SSH unresolved-host guards.

Co-authored-by: Orca <help@stably.ai>

* Refactor GitHub repository execution setup

* Carry host on smart-submit link intents

Co-authored-by: Orca <help@stably.ai>

* Carry the project host on GitHub item dialog origins

Co-authored-by: Orca <help@stably.ai>

* Keep GHES web ports but drop SSH transport ports in host identity

Supersedes PR #9118 on this branch: http(s) remote ports identify the
Enterprise web/API endpoint and are preserved, while ssh/git transport
ports (including ssh.github.com:443) never leak into gh's host identity.
Replaces the ssh.github.com:443 special case with the structural
protocol split and ports the PR's parsing test suite.

Co-authored-by: Orca <help@stably.ai>

* Support GitHub Enterprise diffs and mutations with host-scoped caches

Parse GitHub host identity from work-item URLs and carry it through PR/issue mutations, labels, and assignments. Bound rate-limit and scope-probe caches (1024 and 512 entries) to prevent unbounded growth when interacting with multiple GHES instances. Normalize repository identity keys to include host so github.com and GHES slugs don't collide in cache and equality checks.

* Support GitHub Enterprise diffs and mutations with host-scoped caches

- Carry host identity through PR mutations and reads so fork PRs on
  different GHES instances don't collide in cache or state tracking.
- Validate host authentication before routing requests to unconfigured
  Enterprise servers; ambient credentials must never reach untrusted hosts.
- Scope rate-limit guards and spend tracking per host so GHES quota stays
  independent from github.com quota.
- Respect explicit --hostname arguments in gh CLI calls ahead of GH_HOST or
  ambient defaults, so breaker state follows the actual request target.
- Detect implicit WSL runtimes from UNC paths for consistent host auth and
  execution-options scoping across mobile and desktop clients.

* Support GitHub Enterprise work-item diffs with host-scoped execution

Enterprise PRs must use their selected host consistently across diff, comments,
and file-content loads. Validate repository slugs before authenticated execution
to prevent path-injection via renderer overrides. Scope project browsing cache
and rate-limit tracking by host to prevent cross-host pollution. Use parsed
URLs as authoritative over ambient hosts for project resolution.

* Support GitHub Enterprise work-item diffs with host-scoped execution

Preserve host identity on PR/issue work items throughout the mutation and diff
pipeline so Enterprise instances (including ported endpoints like
github.acme.test:8443) can execute mutations without ambiguity. Rate-limit gh
commands by the pre-qualified --repo host, cache auth state per ported host,
and surface Enterprise hosts in project metadata and error messages.

* fix(review): drop dead rateLimitGuard/noteRateLimitSpend re-export

Both callers (project-view.ts, mutations.ts) moved to the host-scoped
repositoryRateLimitGuard/noteRepositoryRateLimitSpend; the bucket-only
re-export in internals.ts had zero importers left.

Co-authored-by: Orca <help@stably.ai>

* fix(ci): split Enterprise host work-item tests under max-lines

Move GHES/SSH host-routing cases out of work-item-details.test.ts so
the suite stays within the 800-line test max-lines budget.

* test(github): align mocks with host-scoped repository resolution

- Route origin repository resolution through getOwnerRepoForRemote, not getOwnerRepo, to match production path
- Pin github.com host on origin results so host-less fixtures pass host gate in resolveGitHubApiRepository
- Add generation-based invalidation to prevent stale slug-cache writes from in-flight resolutions
- Fix ref-sync race in ProjectPicker: use useLayoutEffect so committed tree owns browse cache key
- Defer handledCrossRepoUrlRef assignment in SmartWorkspaceNameField until resolution succeeds
- Update Enterprise host routing: found work items must not silently fall back to default host when unresolved
- Normalize GHES avatar URLs: accept explicit port 443 as canonical form, not a fallback trigger

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-20 18:55:45 -07:00
Brennan Benson e719ef1a57 fix(mobile): survive connection-migration cutovers during worktree create (#9234)
* fix(mobile): survive connection-migration cutovers during worktree create

A worktree.create in flight when the mobile transport migrates (relay/direct
hand-off on shoddy cellular, relay lease rotation, relay recovery) rejects with
"RPC interrupted by connection migration" even though the host completed it —
leaving the Create Workspace modal stuck while the worktree exists on desktop.
A naive retry hits a name collision and spawns a duplicate.

Mirror the existing mobile terminal-create idempotency: worktree.create now
accepts an optional clientMutationId that the host dedupes (in-flight + brief
post-success TTL), and mobile mints one key per candidate name and re-issues
the create on a cutover so the retry reconciles instead of duplicating.

* fix(mobile): gate worktree cutover replay by capability

* fix(mobile): await worktree replay capability
2026-07-17 15:56:43 -07:00
Brennan BensonandBrennan Benson 53a09afbef feat(mobile): match desktop's Smart workspace source picker exactly (#7985)
* feat(mobile): start a workspace from a branch, issue/PR, or Linear ticket

Unify mobile workspace creation with desktop. The "+" Create Workspace
modal now has a primary "Start from" field that opens a tabbed search
drawer (Branch · GitHub · GitLab · Linear), letting a user start a
workspace from an existing/new git branch, a GitHub issue/PR, a GitLab
issue/MR, or a Linear ticket — in addition to the default blank workspace.

No new backend is required: the search RPCs (github.listWorkItems,
gitlab.listWorkItems, linear.searchIssues/listIssues, repo.searchRefs) and
the worktree.create linked-item params were already used by the mobile
Tasks screen. This surfaces them in the create flow, reusing the existing
pure modules (buildTaskWorkspaceCreateParams, shouldResolveHostedReviewStartPoint,
filterAvailableTaskProviders).

Details:
- New pure modules: workspace-source-selection, use-workspace-source-search,
  source-workspace-create, worktree-create-retry, blank-workspace-create
  (the blank/retry path extracted from the modal for reuse + line budget).
- New UI: WorkspaceSourcePickerDrawer (+ row) and SetupHookTrustDrawer
  (extracted from the modal).
- Older paired desktops (missing the mobile.tasks.v1 capability) degrade to
  Branch + Blank only; GitLab/Linear tabs appear only when available.
- GitHub/GitLab sources pin their repo; switching repos resets the source.
  PR/MR sources resolve their base branch at create time; SSH repos gate
  search until connected (Linear search is repo/SSH-independent).

* fix(mobile): hydrate settings/trust before availability probes settle

Review fixes for #7985: setTrustedOrcaHooks/setRuntimeSettings no longer
wait on status.get/preflight.check/linear.status (a first-open
preflight.check can take seconds, widening the spurious setup-trust
re-prompt window). Also adds param-parity tests for createBlankWorkspace
and a GitLab MR base-resolve test.

* feat(mobile): match desktop's Smart source picker exactly

Rework the mobile create-workspace source picker to be a faithful port of
desktop's Smart picker instead of the earlier divergent "Start from" drawer.

The mobile field is now the workspace-name input AND the source search, with the
exact desktop tabs — Smart · GitHub · Linear · GitLab · Branch · Name. "Smart"
fans out across GitHub + GitLab + Linear + branches, prepends a "Use '<name>'"
row, and resolves pasted URLs / #123 / STA-42 to exact items (with a cross-repo
switch prompt). Selecting a source shows a pill and moves the editable name into
Advanced. The invented "Blank workspace" concept is removed — the neutral state
is just a typed/empty name (blank submit still yields a creature name).

DRY: the pure desktop logic (smart-workspace-source-results, -command-value,
github-links, gitlab-links, work-item-link-query-bounds, github-work-item-identity)
moves to src/shared/new-workspace/ with re-export shims at the old renderer paths,
so both renderer and mobile share one implementation. composer-branch-selection
and workspace-name were already shared and are reused directly.

Two read-only lookup RPCs are allowlisted for mobile so pasted GitLab URLs and
cross-repo GitHub URLs resolve to exact items (github.workItemByOwnerRepo,
gitlab.workItemByPath).

New mobile modules are split for max-lines: use-mobile-composer-source (selection
state + desktop-parity handlers, PR/MR base resolve), use-smart-workspace-source
+ smart-source-fan-out/-search-requests/-paste-intent (RPC orchestration),
composer-linked-work-item / work-item-lookup-text / mobile-smart-source-modes
(pure logic), and SmartWorkspaceSourceField/Drawer/Row + SmartWorkspaceAdvancedFields.
Replaces WorkspaceSourcePickerDrawer/Row, workspace-source-selection,
use-workspace-source-search, and MobileWorkspaceNameInput.

Reviewed by three adversarial agents + re-reviewed after fixes: GitHub search now
returns issues AND PRs (not issues-only), Linear defaults to assigned, create-branch
preserves slashy names, cross-repo PR base resolves against the item's own repo,
displayName is suppressed for user-edited names, and the smart-mode GitHub fan-out
respects availability. tsc/oxlint/max-lines-ratchet clean; 1328 mobile tests pass.

* fix(mobile): keep smart source drawer fully visible

* refactor: share workspace creation behavior across clients

* fix: address workspace creation review findings

---------

Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
2026-07-13 15:38:02 -07:00
Neil f9e18910ae chore(lint): adopt unicorn/prefer-import-meta-properties (error) (#6847)
Migrate fileURLToPath(import.meta.url) / dirname(...) boilerplate to the
native import.meta.dirname / import.meta.filename, then enable the rule
at error so new code stays on the native form.

The oxlint autofix rewrites the expression but leaves the now-unused
node:url / node:path imports behind (which the already-enabled
no-unused-vars=error would then flag), so this commit also removes those
34 orphaned imports — trimming the named import where other names are
still used, deleting the line where it was the sole import.

Scope is build scripts + Node-env tests only (config/scripts, tools/
benchmarks, *.test.{ts,mjs}, vitest configs); zero shipped runtime code.
The native properties are exact equivalents (Node >= 20.11; repo is on
24), so behavior is unchanged.

Verified: oxlint 0 errors tree-wide (root + mobile), oxfmt clean,
typecheck (node+cli+web) + mobile tsc pass, root vitest 22825 passed /
0 failed, mobile vitest 1018 passed. Exercised the rewritten scripts
directly: build:relay (6 targets), ensure-native-runtime,
verify-macos-entitlements all run correctly with import.meta.dirname.
2026-06-29 23:37:30 -07:00
Brennan Benson bed50606f2 Launch and detect Qwen Code via its qwen executable (#6530) 2026-06-27 22:02:52 -07:00
0c0367ea88 feat(agents): native Xiaomi MiMo Code support (#6239)
* feat(agents): native Xiaomi MiMo Code support

Add mimo-code TUI agent (detect mimo, --prompt, --session resume).
Inject MIMOCODE_HOME overlay and /hook/mimo-code status plugin on mimo
launch when agent status hooks are enabled; restore via shell-ready
wrappers. Reuse OpenCode-family hook normalization in the listener.

SSH remote MiMo hook overlays are not included (local/daemon first).

Closes #6220

* fix(mimo): remirror overlay config idempotently

rmSync overlay config before mirror so a second mimo launch does not
hit EEXIST in mirrorEntry and fall back to the user MIMOCODE_HOME.

* review: harden mimo code support

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Doan Bac Tam <24356000+doanbactam@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-24 00:17:49 -07:00
99b5de6e3d Add Ante as a supported CLI agent (#5193)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: brennanb2025 <brennankbenson@gmail.com>
2026-06-17 11:12:45 -07:00
Yang,Zhou 74e2c952f1 feat: add Devin CLI as supported TUI agent (#5282)
* feat: add Devin CLI as supported TUI agent

Register Devin across shared agent config, telemetry, desktop/mobile
catalogs, localization, and README. Uses stdin-after-start prompt
delivery with --permission-mode bypass default args.

* test: assert Devin default --permission-mode bypass in startup plans
2026-06-12 16:10:09 -07:00
Jinwoo Hong 7e73cd4719 fix(mobile): use Claude icon for Agent Teams
Use the Claude logo for the Claude Agent Teams option on mobile.
2026-06-11 14:38:36 -07:00
Jinwoo HongandOrca 66035716f0 Fix mobile diff syntax highlighting (#4922)
Co-authored-by: Orca <help@stably.ai>
2026-06-08 17:41:27 -07:00
Jinwoo HongandOrca f5efdd5945 Fix remote runtime terminal sharing with mobile (#4901)
Co-authored-by: Orca <help@stably.ai>
2026-06-08 14:26:07 -07:00
Jinwoo HongandOrca 880fd964ac Fix issue workspace names with apostrophes (#4848)
Co-authored-by: Orca <help@stably.ai>
2026-06-07 19:57:52 -07:00
Jinwoo HongandOrca aa8451b3e9 Fix mobile Linear issue list parsing (#4576)
Co-authored-by: Orca <help@stably.ai>
2026-06-03 11:06:43 -07:00
Neil 0bb62b9d29 Add braces to mobile control flow (#4351) 2026-05-31 21:32:37 -07:00
Neil 7fa23737ad perf: stream mobile PR diff previews (#4071)
Stream capped mobile PR file diff previews while preserving exact total row counts.
2026-05-31 03:21:57 -07:00
Neil 549ab29489 perf: clean mobile task copy feedback timers (#4047) 2026-05-31 02:27:58 -07:00
aa5d911c36 fix: address pr-bug-scan validated finding from #3583 (#3856)
Removed duplicate 'openclaude' entry at index 3 of MOBILE_TUI_AGENT_AUTO_PICK_ORDER so mobile pickers no longer render two OpenClaude rows or emit duplicate-key warnings.

Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-05-31 01:49:27 -07:00
Neil 8d8dc9ae05 Resolve mobile task agent selection before commit (#3261) 2026-05-30 18:58:55 -07:00
Neil c99eb29bb0 chore: remove unused code 2026-05-30 11:05:58 -07:00
Neil c9f0fa92be fix: include OpenClaude in mobile agent catalog (#3722) 2026-05-30 07:39:55 -07:00
Jinjing bda658a364 Add agent enablement controls (#2972)
* Add agent enablement controls

Implements the Enable/Disable Agents Dashboard behavior described in docs/enable-disable-agents-dashboard.md, including persisted agent enablement state, filtered launch surfaces, and settings UI affordances.

* Clarify agent availability controls

* Respect disabled agents across workspace and AI defaults

- Filter disabled TUI agents from mobile and quick workspace selection
- Avoid implicitly choosing disabled agents for commit/PR AI settings
- Broadcast settings changes to open windows for disabled-agent updates

* rm design doc

* fix: complete agent enablement propagation
2026-05-28 17:29:49 -07:00
Jinwoo HongandOrca 6f18d362cc Improve mobile markdown editing (#2826)
Co-authored-by: Orca <help@stably.ai>
2026-05-26 00:07:53 -07:00
Neil ab80339158 Fix shell-launched OMP status hooks (#2821)
* Fix shell-launched OMP status hooks

* Update oh-my-pi branding

* Add OMP to mobile agent metadata
2026-05-25 22:20:15 -07:00
Jinwoo HongandOrca b1973657ea Add mobile Tasks parity (#2452)
Co-authored-by: Orca <help@stably.ai>
2026-05-21 20:26:07 -07:00