Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aa81d12
fix(desktop): import CA trust variables from the login shell (#19604)
maria-rcks Oct 14, 2026
78f4b69
fix(server): preview copy only forwards text from the viewer's copy k…
maria-rcks Oct 14, 2026
f87dd14
fix(muse): Supervised threads ask before commands again (#19558)
maria-rcks Oct 14, 2026
22af947
fix(server): restarts no longer stall on old subagent result deliveri…
t3dotgg Oct 14, 2026
f4df794
fix(clients): a thread comes back when its subagent needs approval (#…
t3dotgg Oct 14, 2026
52ffe0c
fix(usage): cold usage scans no longer stall the server (#19446)
t3dotgg Oct 14, 2026
39aeaf4
fix(server): "Always allow this session" stops Codex asking for every…
t3dotgg Oct 14, 2026
7bdfed9
fix(web): keep vertical cursor stops outside styled markers (#19651)
Yash-Singh1 Oct 14, 2026
5a4e998
fix(web): mermaid code toggle stays in place when switching views (#1…
maria-rcks Oct 14, 2026
40a7908
fix(server): Reveal in File Explorer and browser launches work on Win…
t3dotgg Oct 14, 2026
742c229
fix(server): GitHub App tokens can merge and use other write actions …
shivamhwp Oct 14, 2026
ab3a3cd
perf(server): commands on busy threads stop decoding thousands of fin…
t3dotgg Oct 15, 2026
ec1313c
fix(server): use workspace-scoped Bitbucket permissions API (#10984)
RealDoc06 Oct 15, 2026
55dfc5f
fix(composer): read dropped threads across environments (#15210)
Bil0000 Oct 15, 2026
755accf
test(web): remove duplicate change request parser tests (#13591)
t3-code[bot] Oct 15, 2026
924fb0c
fix(server): a delegated task's question ends the parent's blocking w…
flamboh Oct 15, 2026
fdacb3a
fix(server): a delegated task's question now wakes its parent agent (…
flamboh Oct 15, 2026
c814e51
fix(server): a cancelled delegated task no longer shows its child's q…
flamboh Oct 15, 2026
21bedfb
feat(storage): clean up settled thread worktrees (#15208)
Bil0000 Oct 15, 2026
e536bfd
feat(web,mobile): waiting status shows a dot-dashed icon and a timer …
maria-rcks Oct 15, 2026
339179f
feat(pr): delete branches after merge or close (#19257)
Bil0000 Oct 15, 2026
13e4715
chore: sync upstream/main at 339179f50
Fryuni Oct 15, 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
26 changes: 26 additions & 0 deletions apps/desktop/src/shell/DesktopShellEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,32 @@ describe("DesktopShellEnvironment", () => {
}),
);

it.effect("hydrates missing CA trust variables from the login shell", () =>
Effect.gen(function* () {
const env: NodeJS.ProcessEnv = {
SHELL: "/bin/zsh",
PATH: "/usr/bin",
SSL_CERT_DIR: "/etc/ssl/certs",
};

yield* runShellEnvironment({
env,
platform: "darwin",
handler: () =>
envOutput({
PATH: "/opt/zerobrew/bin:/usr/bin",
SSL_CERT_FILE: "/opt/zerobrew/etc/ca-certificates/cacert.pem",
SSL_CERT_DIR: "/opt/zerobrew/etc/ca-certificates",
NODE_EXTRA_CA_CERTS: "/Users/me/corp-root.pem",
}),
});

assert.equal(env.SSL_CERT_FILE, "/opt/zerobrew/etc/ca-certificates/cacert.pem");
assert.equal(env.SSL_CERT_DIR, "/etc/ssl/certs");
assert.equal(env.NODE_EXTRA_CA_CERTS, "/Users/me/corp-root.pem");
}),
);

it.effect("keeps mise's PATH record with the login shell PATH", () =>
Effect.gen(function* () {
const env: NodeJS.ProcessEnv = {
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/shell/DesktopShellEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ const LOGIN_SHELL_ENV_NAMES = [
"XDG_SESSION_DESKTOP",
"XDG_SESSION_TYPE",
"WAYLAND_DISPLAY",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"NODE_EXTRA_CA_CERTS",
"T3CODE_TELEMETRY_ENABLED",
"__MISE_DIFF",
] as const;
Expand Down Expand Up @@ -454,6 +457,12 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix
"XDG_DATA_HOME",
"XDG_RUNTIME_DIR",
"WAYLAND_DISPLAY",
// CA trust set in the shell profile. Without it, a Node or OpenSSL build
// that has no bundled roots (a shared-OpenSSL node on PATH) cannot verify
// TLS, so provider updaters and agents fail to reach their registries.
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"NODE_EXTRA_CA_CERTS",
// The telemetry opt-out is documented as a shell variable; GUI launches never see it.
"T3CODE_TELEMETRY_ENABLED",
] as const) {
Expand Down
12 changes: 8 additions & 4 deletions apps/mobile/src/components/AndroidScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppText as Text } from "./AppText";
import { cn } from "../lib/cn";
import { MaterialIconButton } from "./MaterialIconButton";
import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu";
import { splitAndroidHeaderActions } from "./headerMenu.android";
import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
import { useMaterialToolbarLayout } from "./useMaterialToolbarLayout";

Expand All @@ -16,6 +17,8 @@ export interface AndroidHeaderAction {
readonly disabled?: boolean;
readonly loading?: boolean;
readonly selected?: boolean;
readonly destructive?: boolean;
readonly overflowOnly?: boolean;
}

export function AndroidHeaderIconButton(props: {
Expand Down Expand Up @@ -45,10 +48,10 @@ export function AndroidScreenHeader(props: {
props.embedded,
);
const [headerWidth, setHeaderWidth] = useState(0);
const actions = props.actions ?? [];
const directCount = actions.length > 2 ? (headerWidth >= 600 ? 3 : 1) : actions.length;
const visibleActions = actions.slice(0, directCount);
const overflowActions = actions.slice(directCount);
const { visibleActions, overflowActions } = splitAndroidHeaderActions(
props.actions ?? [],
headerWidth >= 600,
);

return (
<View
Expand Down Expand Up @@ -115,6 +118,7 @@ export function AndroidScreenHeader(props: {
title: action.accessibilityLabel,
attributes: {
disabled: Boolean(action.disabled),
destructive: action.destructive,
state: action.selected ? "on" : undefined,
},
}))}
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/components/AppSymbol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const ANDROID_ICON_BY_SF_SYMBOL = {
circle: IconCircle,
"circle.dashed": IconCircleDashed,
"circle.inset.filled": IconCircleDot,
"circle.dashed.inset.filled": IconCircleDot,
clock: IconClock,
timer: IconClock,
ticket: IconTicket,
Expand Down
13 changes: 13 additions & 0 deletions apps/mobile/src/components/headerMenu.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,16 @@ export function findHeaderMenuAction(
}
return undefined;
}

export function splitAndroidHeaderActions<Action extends { readonly overflowOnly?: boolean }>(
actions: ReadonlyArray<Action>,
wide: boolean,
) {
const inline = actions.filter((action) => !action.overflowOnly);
const directCount = actions.length > 2 ? (wide ? 3 : 1) : inline.length;
const visibleActions = inline.slice(0, directCount);
return {
visibleActions,
overflowActions: actions.filter((action) => !visibleActions.includes(action)),
};
}
38 changes: 37 additions & 1 deletion apps/mobile/src/components/headerMenu.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vite-plus/test";
import { androidHeaderMenuActions, findHeaderMenuAction } from "./headerMenu.android";
import {
androidHeaderMenuActions,
findHeaderMenuAction,
splitAndroidHeaderActions,
} from "./headerMenu.android";
import type { ScreenHeaderMenuItem } from "./ScreenHeader.types";

const items: ReadonlyArray<ScreenHeaderMenuItem> = [
Expand Down Expand Up @@ -63,4 +67,36 @@ describe("header menus", () => {
expect(findHeaderMenuAction(items, "sessions")).toBeUndefined();
expect(findHeaderMenuAction(items, "removed-session")).toBeUndefined();
});

it("keeps overflow-only Android header actions in the overflow menu", () => {
const action = (accessibilityLabel: string, overflowOnly?: boolean) => ({
accessibilityLabel,
icon: "trash" as const,
onPress: () => {},
overflowOnly,
});
const labels = (
split: ReturnType<typeof splitAndroidHeaderActions<ReturnType<typeof action>>>,
) => ({
visible: split.visibleActions.map((item) => item.accessibilityLabel),
overflow: split.overflowActions.map((item) => item.accessibilityLabel),
});
const open = action("Open");
const remove = action("Delete", true);
expect(labels(splitAndroidHeaderActions([open, action("Comment")], false))).toEqual({
visible: ["Open", "Comment"],
overflow: [],
});
expect(labels(splitAndroidHeaderActions([open, remove], true))).toEqual({
visible: ["Open"],
overflow: ["Delete"],
});
expect(labels(splitAndroidHeaderActions([remove], true))).toEqual({
visible: [],
overflow: ["Delete"],
});
expect(
labels(splitAndroidHeaderActions([action("Review"), action("Comment"), open, remove], true)),
).toEqual({ visible: ["Review", "Comment", "Open"], overflow: ["Delete"] });
});
});
4 changes: 3 additions & 1 deletion apps/mobile/src/features/home/useThreadListActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,9 @@ export function useThreadListActions(): {
if (!canSnooze(thread, { now: new Date().toISOString() })) {
Alert.alert(
"Could not snooze thread",
thread.hasPendingApprovals || thread.hasPendingUserInput
thread.hasPendingApprovals ||
thread.hasPendingUserInput ||
thread.pendingSubagentRequest != null
? "This thread is waiting on you. Respond to the pending request before snoozing it."
: "This thread is still starting a turn. Try again once it's running.",
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ import {
PR_ROW_EXIT,
PR_POSITION_LAYOUT,
} from "./pullRequestAnimations";
import { openOnProviderLabel } from "./pullRequestActions";
import {
canPerformPullRequestAction,
openOnProviderLabel,
PR_ACTION_ICONS,
PR_ACTION_LABELS,
} from "./pullRequestActions";
import * as Clipboard from "expo-clipboard";
import { ControlPillMenu } from "../../components/ControlPillMenu";
import { MaterialIconButton } from "../../components/MaterialIconButton";
import { MaterialButton } from "../../components/MaterialButton";
import { usePullRequestReviewDraft } from "./usePullRequestReviewDraft";
import { usePullRequestFilesViewed } from "./usePullRequestFilesViewed";
import { usePullRequestPermissions } from "./usePullRequestPermissions";
import { usePullRequestAction } from "./usePullRequestAction";
import { annotatePullRequestDiff, reviewPosition } from "./pullRequestDiffComments";
import { usePullRequestFileFilter } from "./usePullRequestFileFilter";
import { isPullRequestTestFile } from "./pullRequestFileFilters";
Expand Down Expand Up @@ -341,6 +347,15 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
const summary = params.initialSummary;
const identity = pr ?? summary;
const permissions = usePullRequestPermissions(params, pr);
const action = usePullRequestAction(params, pr);
const showsDeleteBranch = pr !== null && canPerformPullRequestAction(pr, "delete-branch");
const confirmDeleteBranch = () =>
pr &&
action.confirm(
"delete-branch",
PR_ACTION_LABELS["delete-branch"],
`Delete ${pr.headBranch} from ${pr.headRepositoryNameWithOwner ?? pr.repository}? Local branches and worktrees are not changed.`,
);
const stack = usePullRequestStack(params, pr);
const refreshActivity = useAtomRefresh(composerPullRequests.activity(requestTarget(params)));
const refreshGuide = useAtomRefresh(composerPullRequests.reviewGuide(requestTarget(params)));
Expand Down Expand Up @@ -417,6 +432,18 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
},
]
: []),
...(showsDeleteBranch
? [
{
accessibilityLabel: PR_ACTION_LABELS["delete-branch"],
icon: PR_ACTION_ICONS["delete-branch"],
disabled: !action.available("delete-branch"),
destructive: true,
overflowOnly: true,
onPress: confirmDeleteBranch,
},
]
: []),
]}
/>
) : null}
Expand Down Expand Up @@ -456,6 +483,25 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
</NativeHeaderToolbar.Label>
</NativeHeaderToolbar.MenuAction>
) : null}
{showsDeleteBranch ? (
<NativeHeaderToolbar.MenuAction
icon={PR_ACTION_ICONS["delete-branch"]}
destructive
disabled={!action.available("delete-branch")}
subtitle={
action.busy
? "Deleting…"
: connected
? pr.headBranch
: "Reconnect to delete this branch."
}
onPress={confirmDeleteBranch}
>
<NativeHeaderToolbar.Label>
{PR_ACTION_LABELS["delete-branch"]}
</NativeHeaderToolbar.Label>
</NativeHeaderToolbar.MenuAction>
) : null}
</NativeHeaderToolbar.Menu>
</NativeHeaderToolbar>
<View
Expand Down
13 changes: 13 additions & 0 deletions apps/mobile/src/features/pull-requests/pullRequestActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ describe("mobile pull request actions", () => {
expect(mergeSummary(unknown, true)).toBe("Squash");
});

it("offers deleting the source branch only once merged or closed on a host that supports it", () => {
const capabilities = { ...detail().capabilities, deleteBranch: true };
expect(canPerformPullRequestAction(detail({ capabilities }), "delete-branch")).toBe(false);
for (const state of ["merged", "closed"] as const) {
expect(canPerformPullRequestAction(detail({ state, capabilities }), "delete-branch")).toBe(
true,
);
expect(canPerformPullRequestAction(detail({ state }), "delete-branch")).toBe(false);
}
const refused = detail({ state: "merged", capabilities, viewerPermissions: { actions: [] } });
expect(canPerformPullRequestAction(refused, "delete-branch")).toBe(false);
});

it("cannot merge when the repository has disabled every offered strategy", () => {
const pr = detail({ mergeCapabilities: { merge: false, squash: false, rebase: false } });
expect(canPerformPullRequestAction(pr, "merge")).toBe(false);
Expand Down
7 changes: 6 additions & 1 deletion apps/mobile/src/features/pull-requests/pullRequestActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type ActionDetail = Pick<
> & {
readonly capabilities: Pick<
PullRequestDetail["capabilities"],
"actions" | "mergeMethods" | "updateMethods"
"actions" | "mergeMethods" | "updateMethods" | "deleteBranch"
>;
readonly viewerPermissions: Pick<
PullRequestDetail["viewerPermissions"],
Expand Down Expand Up @@ -41,6 +41,7 @@ export const PR_ACTION_LABELS = {
"enable-auto-merge": "Enable auto-merge",
"disable-auto-merge": "Disable auto-merge",
revert: "Create revert pull request",
"delete-branch": "Delete branch",
"approve-workflows": "Approve workflows",
} as const satisfies Record<PullRequestAction, string>;

Expand All @@ -54,6 +55,7 @@ export const PR_ACTION_ICONS = {
"enable-auto-merge": "clock",
"disable-auto-merge": "xmark",
revert: "arrow.uturn.backward",
"delete-branch": "trash",
"approve-workflows": "checkmark",
} as const satisfies Record<PullRequestAction, AppSymbolName>;

Expand All @@ -68,6 +70,7 @@ export const ALL_PR_ACTIONS: readonly PullRequestAction[] = [
"close",
"reopen",
"revert",
"delete-branch",
];

export function allowedMergeMethods(pr: ActionDetail) {
Expand All @@ -86,6 +89,8 @@ export function canPerformPullRequestAction(pr: ActionDetail, action: PullReques
return false;
if (action === "reopen") return pr.state === "closed";
if (action === "revert") return pr.state === "merged";
if (action === "delete-branch")
return pr.state !== "open" && pr.capabilities.deleteBranch === true;
if (pr.state !== "open") return false;
switch (action) {
case "update-branch":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff"
import { usePullRequestPermissions } from "./usePullRequestPermissions";

/** Small row actions stay in place, preserving expanded rows and scroll position. */
export function usePullRequestAction(params: PullRequestParams, pr: PullRequestDetail) {
export function usePullRequestAction(params: PullRequestParams, pr: PullRequestDetail | null) {
const target = pullRequestTarget(params);
const permissions = usePullRequestPermissions(params, pr);
const run = useAtomCommand(composerPullRequests.runAction, { reportFailure: false });
Expand All @@ -23,7 +23,7 @@ export function usePullRequestAction(params: PullRequestParams, pr: PullRequestD
const pending = useRef(false);
const [busy, setBusy] = useState(false);
const available = (action: PullRequestAction) =>
!busy && permissions.canWrite && canPerformPullRequestAction(pr, action);
pr !== null && !busy && permissions.canWrite && canPerformPullRequestAction(pr, action);
const perform = async (action: PullRequestAction, updateMethod?: PullRequestUpdateMethod) => {
if (pending.current || !available(action)) return;
pending.current = true;
Expand Down Expand Up @@ -51,7 +51,7 @@ export function usePullRequestAction(params: PullRequestParams, pr: PullRequestD
{ text: "Cancel", style: "cancel" },
{
text: title,
style: action === "close" ? "destructive" : "default",
style: action === "close" || action === "delete-branch" ? "destructive" : "default",
onPress: () => void perform(action),
},
]);
Expand Down
14 changes: 6 additions & 8 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import type { ComposerTextPaste } from "../../native/T3ComposerEditor.types";
import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import type { HandoffEndpoints } from "@t3tools/client-runtime/handoff";
import type {
CodexFeedbackSubmission,
EnvironmentThreadStatus,
import {
childThreadInputsTitle,
type CodexFeedbackSubmission,
type EnvironmentThreadStatus,
} from "@t3tools/client-runtime/state/threads";
import { useKeyboardChatComposerInset, useKeyboardScrollToEnd } from "@legendapp/list/keyboard";
import { resolveProviderSkillsForCwd } from "@t3tools/client-runtime/providerSkills";
Expand Down Expand Up @@ -523,11 +524,8 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
if (childInput && contentPresentationKind === "ready") {
return {
kind: "child-input",
label:
childThreadInputs.length === 1
? "Subagent needs input"
: `${childThreadInputs.length} subagents need input`,
accessibilityLabel: `Open question from ${childInput.title}`,
label: childThreadInputsTitle(childThreadInputs),
accessibilityLabel: `Open ${childInput.hasPendingApprovals ? "approval" : "question"} from ${childInput.title}`,
onPress: () => {
Keyboard.dismiss();
navigation.navigate("Thread", {
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const STATUS_LABEL_BY_STATUS: Partial<Record<ThreadListV2Status, StatusLabel>> =
},
waiting: {
label: "Waiting",
icon: "clock",
icon: "circle.dashed.inset.filled",
className: "text-foreground-muted",
iconTintClassName: "accent-foreground-muted",
},
Expand Down
Loading
Loading