Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a737e70
feat(web): add modular theme library (#5226)
StiensWout Aug 7, 2026
bc0bc0d
fix(web): reading a thread clears Done; Woke is dismissible (#5579)
t3dotgg Aug 7, 2026
a4085f2
perf(dev): faster cold-start for dev web serving (#5584)
t3dotgg Aug 7, 2026
4ada441
fix(dev): Improve instructions for --share (#5586)
t3dotgg Aug 7, 2026
9aae251
fix(mobile): improve keyboard avoiding (#5451)
jmeistrich Aug 7, 2026
3302369
fix(web): live background-work banner no longer hides behind the upda…
t3dotgg Aug 7, 2026
4d9b1ec
fix(web): stabilize chat timeline positioning (#5449)
jmeistrich Aug 7, 2026
b9de1cc
feat(server): record runtime mode per turn and on mode changes (#5593)
t3dotgg Aug 7, 2026
3eac1cc
feat(web): thread actions from the chat header title (#5592)
t3dotgg Aug 7, 2026
f15d948
fix(mobile): avoid iOS terminal reset on clear (#5440)
justynleung Aug 7, 2026
f1148c4
fix(mobile): pad scroll views above Android nav bar (#5415)
StoneCotton Aug 7, 2026
6114dbe
fix(mobile): keep Android chat text from showing through the composer…
PollyGlot Aug 7, 2026
9ebdd5d
fix(mobile): repair Clerk auth navigation headers (#5140)
gabrielelpidio Aug 7, 2026
ace29a4
Bump mobile app version to 1.0.2 (#5588)
juliusmarminge Aug 7, 2026
ec3b66c
feat(web): click the pin icon to unpin a thread (#5578)
UtkarshUsername Aug 7, 2026
a20506b
fix(web): show the correctly matching shortcut in new thread button's…
UtkarshUsername Aug 7, 2026
01269ed
fix(mobile): reconnects no longer shift the thread list (#5372)
t3dotgg Aug 7, 2026
ba7ea41
feat(web): drag pinned threads into your own order (#5581)
t3dotgg Aug 7, 2026
ee3e216
chore(ci): vouch StiensWout (#5637)
t3-code[bot] Aug 7, 2026
529913a
feat(desktop): remember recently used sites in the Browser panel (#5270)
chrisdeeming Aug 7, 2026
071dfdf
chore: vouch chrisdeeming (#5641)
t3-code[bot] Aug 7, 2026
2b8266d
feat(web): make sidebar artwork theme-aware (#5636)
maria-rcks Aug 7, 2026
2935704
fix(web): reconnect the composer seam for remote non-Git projects (#5…
caezium Aug 7, 2026
b572da2
fix(mobile): bump APP_VERSION default to 1.0.2 for #5588
cursoragent Aug 8, 2026
a204a7c
fix(mobile): format app.config.ts version field
cursoragent Aug 8, 2026
c508cab
fix: adapt upstream #5140 to fork
cursoragent Aug 8, 2026
e5e7b92
fix: format mobile composer route transition css
cursoragent Aug 8, 2026
733c9dd
Revert "fix: adapt upstream #5140 to fork"
cursoragent Aug 8, 2026
b807d0c
fix(deps): refresh lockfile for Clerk catalog bump (#5140)
cursoragent Aug 8, 2026
041e872
fix(web): format index.css for vp check
cursoragent Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions .agents/skills/test-t3-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Treat the overall testing or implementation loop—not an assistant turn or one
- Do not stop the server merely because one verification pass completed or because you are yielding a response to the user.
- Before starting another environment, check whether the existing process and browser tab still serve the task. Reuse them when healthy instead of discarding useful state.
- On a later turn, verify that the existing process is alive and reuse its printed ports and base directory. If it exited, restart with the same base directory; create a new pairing token only when the browser session is no longer valid.
- Tell the user when a test environment remains available, including its non-secret web URL when useful. Never include a pairing token.
- Tell the user when a test environment remains available, including its non-secret web URL when useful. Include a pairing token only when the user still needs to pair (see below).

## Authenticate the browser on the first navigation

Expand All @@ -50,24 +50,13 @@ Treat the overall testing or implementation loop—not an assistant turn or one
4. Wait for the pairing exchange and redirect to finish before navigating elsewhere.
5. Continue in the same browser context so its stored bearer session remains available.

Treat pairing URLs as secrets. Do not copy them into final responses, screenshots, committed files, or durable logs. A pairing token is short-lived and single-use; opening the URL in another browser or opening it twice can consume it.
Keep pairing URLs out of screenshots, committed files, and durable logs. When the user asked for a shared environment, the deliverable IS the full pairing URL — paste it in your reply, token and all; a bare origin is useless to them. A pairing token is short-lived and single-use; opening the URL in another browser or opening it twice can consume it, so never open a URL you handed to the user.

## Recover a consumed or expired pairing token

Create another token against the same database and web URL as the running dev server:
Run `node apps/server/src/bin.ts pair` from the repository root. It discovers the running dev server (worktree `.t3` first, same precedence as the dev runner) and prints a fresh `Pair URL` against the server's current web origin, including a `--share` tailnet origin. Pass `--base-dir <base-dir>` only when the server was started with `--home-dir`, using the identical path.

```bash
T3CODE_PORT=<server-port> node apps/server/src/bin.ts auth pairing create \
--base-dir <base-dir> \
--dev-url <web-url> \
--base-url <web-url> \
--ttl 15m \
--label agent-ui-test
```

Use the `Pair URL` from this command once. Derive `<server-port>` and `<web-url>` from the current dev-runner output, including any automatically selected port offset. Setting `T3CODE_PORT` keeps the administrative CLI from probing for an unrelated free port.

Always pass `--dev-url` for a dev-runner environment so the generated pairing URL uses the current web origin. An explicit base directory stores runtime state in `<base-dir>/userdata`; the `<base-dir>/dev` fallback is only used by an implicit dev home. A worktree-local `.t3` counts as explicit, so its state lives in `<worktree>/.t3/userdata`. Use `auth pairing list` to inspect active token metadata; it intentionally cannot reveal token secrets.
Tokens from `pair` carry standard client scopes. The startup pairing URL carries admin scopes; if the user needs Settings → Connections management (`access:write`), restart the server and hand over the new startup URL instead.

## Inspect or seed SQLite state

Expand Down
2 changes: 2 additions & 0 deletions .github/VOUCHED.td
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# Keep entries sorted alphabetically.
github:adityavardhansharma
github:binbandit
github:chrisdeeming
github:chuks-qua
github:cursoragent
github:gbarros-dev
Expand All @@ -26,6 +27,7 @@ github:notkainoa
github:PatrickBauer
github:realAhmedRoach
github:shiroyasha9
github:StiensWout
github:Yash-Singh1
github:eggfriedrice24
github:Ymit24
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ Adapters are registered in `provider/Layers/ProviderAdapterRegistry.ts` and look

Provider runtime activity is normalized into canonical `OrchestrationEvent`s by the ingestion layer, persisted in a SQLite event store with sequence-based ordering, and projected into in-memory materialized views. Clients receive ordered events via Effect RPC streams (replay + live merge). Command receipts provide idempotency for reconnects and retries.

## Local Development Notes

- `vp i` installs. Worktrees get this from the `t3.json` setup script; if module resolution looks broken, it probably did not run.
- `vp run dev` starts server and web. In a worktree, state defaults to that worktree's gitignored `.t3`, which deliberately outranks an ambient `T3CODE_HOME` so you cannot land on shared state by accident. An explicit `--home-dir` still wins.
- Ports derive from the worktree path and are stable across restarts, but read the real ones from the `[dev-runner]` line since occupied ports shift.
- Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, paste that full URL (token included) in your reply. Do not wire up `tailscale serve` by hand for this, and do not open the URL yourself.
- The web app requires pairing. Hand over the pairing URL, not the bare origin. A URL without its token is useless to whoever you gave it to. If the token got consumed, mint a fresh one with `node apps/server/src/bin.ts pair` — note it carries standard scopes, while the startup URL carries admin scopes (needed for Settings → Connections management).
- Stop what you started, by the PID you tracked.

### Effect Architecture

The server uses Effect throughout for dependency injection, typed errors, and streaming:
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/electron/ElectronDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ export class ElectronDialogPickFolderError extends Schema.TaggedErrorClass<Elect
}
}

export class ElectronDialogPickFilesError extends Schema.TaggedErrorClass<ElectronDialogPickFilesError>()(
"ElectronDialogPickFilesError",
{
ownerWindowId: Schema.NullOr(Schema.Number),
defaultPath: Schema.NullOr(Schema.String),
cause: Schema.Defect(),
},
) {
override get message(): string {
const owner = this.ownerWindowId === null ? "the application" : `window ${this.ownerWindowId}`;
const defaultPath = this.defaultPath === null ? "no default path" : this.defaultPath;
return `Failed to open the Electron file picker for ${owner} with ${defaultPath}.`;
}
}

export class ElectronDialogConfirmError extends Schema.TaggedErrorClass<ElectronDialogConfirmError>()(
"ElectronDialogConfirmError",
{
Expand Down Expand Up @@ -69,6 +84,7 @@ export class ElectronDialogShowErrorBoxError extends Schema.TaggedErrorClass<Ele

export const ElectronDialogError = Schema.Union([
ElectronDialogPickFolderError,
ElectronDialogPickFilesError,
ElectronDialogConfirmError,
ElectronDialogShowMessageBoxError,
ElectronDialogShowErrorBoxError,
Expand All @@ -81,6 +97,12 @@ export interface ElectronDialogPickFolderInput {
readonly defaultPath: Option.Option<string>;
}

export interface ElectronDialogPickFilesInput {
readonly owner: Option.Option<Electron.BrowserWindow>;
readonly defaultPath: Option.Option<string>;
readonly filters: readonly Electron.FileFilter[];
}

export interface ElectronDialogConfirmInput {
readonly owner: Option.Option<Electron.BrowserWindow>;
readonly message: string;
Expand All @@ -92,6 +114,9 @@ export class ElectronDialog extends Context.Service<
readonly pickFolder: (
input: ElectronDialogPickFolderInput,
) => Effect.Effect<Option.Option<string>, ElectronDialogPickFolderError>;
readonly pickFiles: (
input: ElectronDialogPickFilesInput,
) => Effect.Effect<readonly string[], ElectronDialogPickFilesError>;
readonly confirm: (
input: ElectronDialogConfirmInput,
) => Effect.Effect<boolean, ElectronDialogConfirmError>;
Expand Down Expand Up @@ -137,6 +162,32 @@ export const make = ElectronDialog.of({
}
return Option.fromNullishOr(result.filePaths[0]);
}),
pickFiles: Effect.fn("desktop.electron.dialog.pickFiles")(function* (input) {
const ownerWindowId = Option.match(input.owner, {
onNone: () => null,
onSome: (owner) => owner.id,
});
const defaultPath = Option.getOrNull(input.defaultPath);
const openDialogOptions: Electron.OpenDialogOptions = {
properties: ["openFile", "multiSelections"],
filters: [...input.filters],
...(defaultPath === null ? {} : { defaultPath }),
};
const result = yield* Effect.tryPromise({
try: () =>
Option.match(input.owner, {
onNone: () => Electron.dialog.showOpenDialog(openDialogOptions),
onSome: (owner) => Electron.dialog.showOpenDialog(owner, openDialogOptions),
}),
catch: (cause) =>
new ElectronDialogPickFilesError({
ownerWindowId,
defaultPath,
cause,
}),
});
return result.canceled ? [] : result.filePaths;
}),
confirm: Effect.fn("desktop.electron.dialog.confirm")(function* (input) {
const normalizedMessage = input.message.trim();
if (normalizedMessage.length === 0) {
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
openLogDir,
openExternal,
pickFolder,
pickThemeFiles,
readLogFile,
setTheme,
showContextMenu,
Expand Down Expand Up @@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setWslOnly);

yield* ipc.handle(pickFolder);
yield* ipc.handle(pickThemeFiles);
yield* ipc.handle(confirm);
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
export const PICK_THEME_FILES_CHANNEL = "desktop:pick-theme-files";
export const CONFIRM_CHANNEL = "desktop:confirm";
export const SET_THEME_CHANNEL = "desktop:set-theme";
export const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
Expand Down
52 changes: 51 additions & 1 deletion apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import {
DesktopAppBrandingSchema,
DesktopEnvironmentBootstrapSchema,
DesktopThemeSchema,
PickedThemeFileSchema,
PickFolderOptionsSchema,
PRIMARY_LOCAL_ENVIRONMENT_ID,
type DesktopEnvironmentBootstrap,
type PickedThemeFile,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as NodeOS from "node:os";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

Expand Down Expand Up @@ -371,3 +375,49 @@ export const openLogDir = DesktopIpc.makeIpcMethod({
yield* shell.openPath(environment.logDir).pipe(Effect.ignore);
}),
});

/** Theme files are a few KB; anything larger returns empty text and lets the
* renderer reject it by size without the contents ever crossing the bridge. */
const PICKED_THEME_FILE_MAX_BYTES = 256 * 1024;

export const pickThemeFiles = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PICK_THEME_FILES_CHANNEL,
payload: Schema.Undefined,
result: Schema.NullOr(Schema.Array(PickedThemeFileSchema)),
handler: Effect.fn("desktop.ipc.window.pickThemeFiles")(function* () {
const dialog = yield* ElectronDialog.ElectronDialog;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
// The VS Code extensions directory is the same dotfolder on Windows,
// macOS, and Linux; when it is missing the picker opens wherever the
// platform would by default.
const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions");
const defaultPath = yield* fileSystem
.exists(extensionsDir)
.pipe(Effect.orElseSucceed(() => false));
const paths = yield* dialog.pickFiles({
owner: yield* electronWindow.focusedMainOrFirst,
defaultPath: defaultPath ? Option.some(extensionsDir) : Option.none(),
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (paths.length === 0) {
return null;
}
return yield* Effect.forEach(paths, (filePath) => {
const name = path.basename(filePath);
return Effect.gen(function* () {
const info = yield* fileSystem.stat(filePath);
const size = Number(info.size);
if (size > PICKED_THEME_FILE_MAX_BYTES) {
return { name, size, text: "" } satisfies PickedThemeFile;
}
const text = yield* fileSystem.readFileString(filePath);
return { name, size, text } satisfies PickedThemeFile;
}).pipe(
// An unreadable file degrades to an entry the renderer reports.
Effect.orElseSucceed((): PickedThemeFile => ({ name, size: 0, text: "" })),
);
});
}),
});
1 change: 1 addition & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
setWslDistro: (distro) => ipcRenderer.invoke(IpcChannels.SET_WSL_DISTRO_CHANNEL, distro),
setWslOnly: (enabled) => ipcRenderer.invoke(IpcChannels.SET_WSL_ONLY_CHANNEL, enabled),
pickFolder: (options) => ipcRenderer.invoke(IpcChannels.PICK_FOLDER_CHANNEL, options),
pickThemeFiles: () => ipcRenderer.invoke(IpcChannels.PICK_THEME_FILES_CHANNEL, undefined),
confirm: (message) => ipcRenderer.invoke(IpcChannels.CONFIRM_CHANNEL, message),
setTheme: (theme) => ipcRenderer.invoke(IpcChannels.SET_THEME_CHANNEL, theme),
showContextMenu: (items, position) =>
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {

const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {
pickFolder: () => Effect.succeed(Option.none()),
pickFiles: () => Effect.succeed([]),
confirm: () => Effect.succeed(false),
showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),
showErrorBox: () => Effect.void,
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const repoEnv = loadRepoEnv();
Object.assign(process.env, repoEnv);

const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT);
const APP_VERSION = process.env.MOBILE_APP_VERSION?.trim() || "1.0.1";
const APP_VERSION = process.env.MOBILE_APP_VERSION?.trim() || "1.0.2";
const ANDROID_VERSION_CODE = resolveAndroidVersionCode(process.env.MOBILE_ANDROID_VERSION_CODE);
const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1";

Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,12 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
return
}

if buffer.isEmpty {
feedData(Data("\u{1B}[3J\u{1B}[H\u{1B}[2J".utf8))
lastAppliedBuffer = ""
return
}

if buffer.hasPrefix(lastAppliedBuffer) {
let suffix = String(buffer.dropFirst(lastAppliedBuffer.count))
feedData(Data(suffix.utf8))
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,10 @@
"@react-native-menu/menu"
]
}
},
"reanimated": {
"staticFeatureFlags": {
"DISABLE_COMMIT_PAUSING_MECHANISM": true
}
}
}
23 changes: 13 additions & 10 deletions apps/mobile/src/components/CompactBrandTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";
const IOS_NATIVE_LEADING_TITLE_OFFSET = -6;
const IPAD_NATIVE_LEADING_TITLE_OFFSET = 7;

/**
* Horizontal correction applied to content rendered in the brand title slot,
* shared with the connection-status swap so both align identically.
*/
export function brandTitleOffset(nativeLeadingItem: boolean): number {
if (Platform.OS !== "ios") return 0;
if (nativeLeadingItem) {
return Platform.isPad ? IPAD_NATIVE_LEADING_TITLE_OFFSET : IOS_NATIVE_LEADING_TITLE_OFFSET;
}
return Platform.isPad ? IPAD_HOME_TITLE_OFFSET : 0;
}

/**
* Compact brand lockup sized for native navigation bars.
*/
Expand All @@ -28,16 +40,7 @@ export function CompactBrandTitle(
const mutedColor = useThemeColor("--color-foreground-muted");
const subtleColor = useThemeColor("--color-subtle");
const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
const titleOffset =
Platform.OS !== "ios"
? 0
: props.nativeLeadingItem
? Platform.isPad
? IPAD_NATIVE_LEADING_TITLE_OFFSET
: IOS_NATIVE_LEADING_TITLE_OFFSET
: Platform.isPad
? IPAD_HOME_TITLE_OFFSET
: 0;
const titleOffset = brandTitleOffset(props.nativeLeadingItem === true);

return (
<View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ export function ConnectionsRouteScreen() {
contentInsetAdjustmentBehavior="automatic"
showsVerticalScrollIndicator={false}
className="flex-1"
contentInset={{ bottom: Math.max(insets.bottom, 18) + 18 }}
contentContainerStyle={{
paddingBottom: Math.max(insets.bottom, 18) + 18,
paddingHorizontal: 20,
paddingTop: 16,
}}
Expand Down
35 changes: 23 additions & 12 deletions apps/mobile/src/features/home/HomeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
} from "../layout/native-mail-search-toolbar";
import type { HomeProjectSortOrder } from "./homeThreadList";
import { WorkspaceConnectionTitle } from "./WorkspaceConnectionTitle";
import {
buildHomeListFilterMenu,
type HomeListFilterMenuEnvironment,
Expand All @@ -47,6 +48,7 @@ export function HomeHeader(props: {
readonly onProjectChange: (projectKey: string | null) => void;
readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
readonly onOpenEnvironments: () => void;
readonly onOpenSettings: () => void;
readonly onStartNewTask: () => void;
}) {
Expand Down Expand Up @@ -207,18 +209,27 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
>
<View className="w-full max-w-[720px] self-center gap-3">
<View className="flex-row items-center gap-2.5">
<View className="flex-1 flex-row items-center gap-2">
{/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */}
<T3Wordmark color={iconColor} height={15} />
<RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted">
Code
</RNText>
<View className="rounded-full bg-subtle px-2 py-0.75">
<RNText className="text-[11px] font-t3-bold tracking-[1.1px] text-foreground-muted uppercase">
{stageLabel}
</RNText>
</View>
</View>
{/* Brand slot doubles as the connection status surface: while an
environment reconnects, the lockup fades to a status label in
place (no layout shift in the list below). */}
<WorkspaceConnectionTitle
grow
onPress={props.onOpenEnvironments}
brand={
<View className="flex-row items-center gap-2">
{/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */}
<T3Wordmark color={iconColor} height={15} />
<RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted">
Code
</RNText>
<View className="rounded-full bg-subtle px-2 py-0.75">
<RNText className="text-[11px] font-t3-bold tracking-[1.1px] text-foreground-muted uppercase">
{stageLabel}
</RNText>
</View>
</View>
}
/>

<ControlPillMenu
actions={menuActions}
Expand Down
Loading
Loading