Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
cae6c66
fix(server): only Claude progress updates before a tool call show as …
maria-rcks Oct 14, 2026
ce6f8ce
fix(pr): keep known state in sync across controls (#18408)
maria-rcks Oct 14, 2026
7897a69
fix(web): review summary only says optional when it is (#18451)
flamboh Oct 14, 2026
e093789
fix(web): subagent lists in the composer banner no longer start lines…
flamboh Oct 14, 2026
cb395ee
fix(web,mobile): pull request filters no longer offer the Scratch fol…
flamboh Oct 14, 2026
464fad9
fix(web,mobile): pull request project filter follows the sidebar orde…
flamboh Oct 14, 2026
bf8ffe8
feat(settings): inactivity auto-settle can keep threads with a PR (#1…
tris203 Oct 14, 2026
e47bfe4
fix(server): Claude works next to an enterprise managed-mcp.json (#18…
SunkenInTime Oct 14, 2026
348f217
fix(web,mobile): muse code uses meta's official blue logo (#19562)
maria-rcks Oct 14, 2026
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
2d31acb
fix(web): the sidebar no longer keeps old renders alive (#19852)
t3dotgg Oct 15, 2026
313f23e
fix(web): update everything no longer waits on slow servers, and show…
t3dotgg Oct 15, 2026
45b5993
fix(web): text generation no longer falls back to the expensive flags…
t3dotgg Oct 15, 2026
cd34df5
feat(web): setting to open pull request links on GitHub (#19719)
t3dotgg Oct 15, 2026
00cdceb
fix(web): middle-click opens pull requests in the browser, like cmd+c…
t3dotgg Oct 15, 2026
0e1431f
fix(web): the chat view and composer no longer keep old renders alive…
t3dotgg Oct 15, 2026
0b68a4a
fix(server): steering no longer ends running subagents (#19210)
shivamhwp Oct 15, 2026
e6bbfdb
fix(web,mobile): sent queued messages say "from queue", not "queued" …
shivamhwp Oct 15, 2026
42da166
fix(server): OTLP exports say which machine and build they came from …
t3dotgg Oct 15, 2026
5fd5afc
Merge upstream/main through 42da166d13
andrewcai8 Oct 15, 2026
6d8f041
test(web): storage settings fixture includes the fork's cloud machine…
andrewcai8 Oct 15, 2026
c6b5d9a
test(server): fork Claude session fakes implement backgroundTasks
andrewcai8 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
5 changes: 5 additions & 0 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
makeTraceSink,
type SignalExport,
} from "@t3tools/shared/observability";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as SharedObservability from "@t3tools/shared/observability";
import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
import {
Expand Down Expand Up @@ -625,12 +626,16 @@ const layerTelemetry = Layer.unwrap(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const endpoints = yield* resolveOtlpEndpoints;
// `host.name` keeps machines apart on a shared collector; one set in
// `OTEL_RESOURCE_ATTRIBUTES` wins, since explicit attributes override it.
const resource = {
serviceName: "t3code-desktop",
serviceVersion: environment.appVersion,
attributes: {
"service.namespace": "t3code",
"service.runtime": "desktop",
"service.mode": environment.isDevelopment ? "development" : "packaged",
"host.name": endpoints.resourceAttributes["host.name"] || (yield* HostProcess.Hostname),
},
};

Expand Down
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
32 changes: 28 additions & 4 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Image } from "expo-image";
import { Path, Svg } from "react-native-svg";
import { Defs, LinearGradient, Path, Stop, Svg } from "react-native-svg";
import { View } from "react-native";
import { providerInstanceInitials } from "@t3tools/client-runtime/state/provider-instance-display";
import { useState } from "react";
import { useId, useState } from "react";
import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { AppText as Text } from "./AppText";
Expand Down Expand Up @@ -66,6 +66,7 @@ function AcpRegistryProviderIcon(props: {
export function ProviderIcon(props: ProviderIconProps) {
const { themeAppearance } = useAppearancePreferences();
const isDarkMode = themeAppearance === "dark";
const gradientId = useId();
const size = props.size ?? 16;
const mono = isDarkMode ? "#e5e5e5" : "#171717";

Expand Down Expand Up @@ -99,12 +100,35 @@ export function ProviderIcon(props: ProviderIconProps) {
isDarkMode ? fill.dark : fill.light;
return (
<Svg width={size} height={size} viewBox={packageIcon.viewBox} fill="none">
{packageIcon.paths.map((path) => (
<Defs>
{packageIcon.paths.map((path, index) =>
path.gradient ? (
<LinearGradient
key={path.d}
id={`${gradientId}-${index}`}
x1={path.gradient.x1}
y1={path.gradient.y1}
x2={path.gradient.x2}
y2={path.gradient.y2}
gradientUnits="userSpaceOnUse"
>
{path.gradient.stops.map((stop) => (
<Stop key={stop.offset} offset={stop.offset} stopColor={stop.color} />
))}
</LinearGradient>
) : null,
)}
</Defs>
{packageIcon.paths.map((path, index) => (
<Path
key={path.d}
d={path.d}
fillRule={path.fillRule}
fill={themeFill(path.fill ?? packageIcon.fill)}
fill={
path.gradient
? `url(#${gradientId}-${index})`
: themeFill(path.fill ?? packageIcon.fill)
}
/>
))}
</Svg>
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 @@ -312,7 +312,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
36 changes: 33 additions & 3 deletions apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { sortProjectsByGroupOrder } from "@t3tools/client-runtime/state/project-grouping";
import { useMemo } from "react";
import { ScrollView } from "react-native";
import type { ControlPillMenuProps } from "../../components/ControlPillMenu.types";
import { ListFilterMenu } from "../workspace/ListFilterMenu";
import { useProjects, useThreadShells } from "../../state/entities";
import { useMobileProjectGroupingSettings } from "../../state/project-grouping";
import { buildHomeProjectScopes, sortHomeProjectScopes } from "../home/homeThreadList";
import { DEFAULT_PR_LIST_CONTROLS, type PullRequestListControls } from "./pullRequestList";

const STATES = { open: "Open", closed: "Closed", merged: "Merged", all: "All states" } as const;
Expand Down Expand Up @@ -28,6 +33,30 @@ function choices(field: string, options: Readonly<Record<string, string>>, selec
}));
}

/** The project list's own order, which is the order its sidebar and new-task picker show. */
function useProjectsInSidebarOrder<Project extends { id: string; environmentId: string }>(
projects: readonly Project[],
) {
const allProjects = useProjects();
const threads = useThreadShells();
const { sidebarProjectGroupingMode } = useMobileProjectGroupingSettings();
const scopes = useMemo(
() =>
sortHomeProjectScopes({
scopes: buildHomeProjectScopes({
projects: allProjects,
environmentId: null,
projectGroupingMode: sidebarProjectGroupingMode,
}),
threads,
pendingTasks: [],
projectSortOrder: "updated_at",
}).map((scope) => ({ memberProjectRefs: scope.projectRefs })),
[allProjects, sidebarProjectGroupingMode, threads],
);
return useMemo(() => sortProjectsByGroupOrder(projects, scopes), [projects, scopes]);
}

export function PullRequestListFilters(props: {
readonly environments: readonly { id: string; label: string }[];
readonly environmentId: string | null;
Expand All @@ -39,6 +68,7 @@ export function PullRequestListFilters(props: {
readonly projects: readonly { id: string; title: string; environmentId: string }[];
}) {
const { value } = props;
const projects = useProjectsInSidebarOrder(props.projects);
const selectedLabels = value.filters.labels?.flat() ?? [];
const selectedLabelSet = new Set(selectedLabels);
const author = value.filters.author;
Expand Down Expand Up @@ -76,7 +106,7 @@ export function PullRequestListFilters(props: {
props.onChange({ ...value, sort: next as PullRequestListControls["sort"] });
break;
case "project": {
const project = props.projects.find(
const project = projects.find(
(project) => JSON.stringify([project.environmentId, project.id]) === next,
);
props.onChange({
Expand Down Expand Up @@ -165,15 +195,15 @@ export function PullRequestListFilters(props: {
{
id: "projects",
title:
props.projects.find(
projects.find(
(project) =>
project.id === value.projectId && project.environmentId === value.projectEnvironmentId,
)?.title ?? "Project",
subactions: choices(
"project",
Object.fromEntries([
["", "All projects"],
...props.projects.map((project) => [
...projects.map((project) => [
JSON.stringify([project.environmentId, project.id]),
project.title,
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function PullRequestReviewSheet({
/>
}
>
{reviewing && offered.length ? (
{reviewing && offered.length > 1 ? (
<SegmentedControl
options={offered.map((option) => ({ value: option.value, label: option.title }))}
selected={verdict?.value ?? "comment"}
Expand Down
Loading
Loading