Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f7efb53
feat(web): truncate branch names and paths in the middle (#12805)
maria-rcks Sep 21, 2026
dca84ef
fix(web): paste markdown with inline code inside bold, italic, or str…
Gigioxx Sep 21, 2026
a9ab904
feat(web): show the pull request refresh spinning in the detail heade…
maria-rcks Sep 21, 2026
a4bc7de
fix(web): dismiss composer suggestions with Escape (#12836)
Yash-Singh1 Sep 21, 2026
a6cb1dd
fix(mobile): keep the source worktree when starting a thread on a bra…
SunkenInTime Sep 21, 2026
6cc7f70
fix(web): keep composer controls visible while they fit (#12837)
maria-rcks Sep 21, 2026
5c5fa5f
feat(devices): show installed and running tool versions per host (#12…
juliusmarminge Sep 21, 2026
3b836f9
feat(devices): show automatic update progress and host retry (#12817)
juliusmarminge Sep 21, 2026
61b25b8
feat(devices): add read-only update discovery and remote ownership (#…
juliusmarminge Sep 21, 2026
790be6d
fix(devices): safely reclaim obsolete managed tool versions (#12819)
juliusmarminge Sep 21, 2026
30e3649
fix(web): match thread notification icons to sidebar status (#12806)
maria-rcks Sep 21, 2026
f391b88
fix(web): move sidebar shelves as one block (#11772)
maria-rcks Sep 21, 2026
6b0a04a
fix(web): offer undo after unpinning a thread (#10744)
saphid Sep 21, 2026
5781b52
feat(web): undo settle, snooze and archive, with a mod+z shortcut (#1…
juliusmarminge Sep 21, 2026
1eeeabb
fix(mobile): use a proper pull request icon on iOS (#12855)
juliusmarminge Sep 21, 2026
b379b5b
test(web): remove redundant favicon test (#12856)
t3-code[bot] Sep 21, 2026
1de563c
feat(devices): offer manual updates in tool version details (#12877)
juliusmarminge Sep 21, 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: 17 additions & 2 deletions apps/mobile/src/components/AppSymbol.ios.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import IconGitPullRequest from "@tabler/icons-react-native/IconGitPullRequest";
import { SymbolView as ExpoSymbolView } from "expo-symbols";
import { withUniwind } from "uniwind";
import type { AppSymbolViewProps } from "./AppSymbol";
Expand All @@ -6,10 +7,24 @@ export type { SFSymbol } from "expo-symbols";
export type { AppSymbolName } from "./AppSymbol";

/**
* Keep the iOS implementation isolated from the Android Tabler fallback so
* Metro does not initialize the icon package when iOS renders SF Symbols.
* Use SF Symbols on iOS except for pull requests, which have no matching
* native glyph. Import only that Tabler icon to keep the bundle small.
*/
function AppSymbolView(props: AppSymbolViewProps) {
const name = typeof props.name === "string" ? props.name : props.name.ios;
if (name === "arrow.triangle.pull") {
return (
<IconGitPullRequest
accessibilityLabel={props.accessibilityLabel}
color={props.tintColor}
size={props.size}
strokeWidth={2}
style={props.style}
testID={props.testID}
/>
);
}

return <ExpoSymbolView {...props} />;
}

Expand Down
58 changes: 56 additions & 2 deletions apps/mobile/src/features/devices/DevicePreviewRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
deviceToolVersionLabels,
deviceToolUpdateOwnership,
deviceToolUpdatePolicy,
} from "@t3tools/client-runtime/state/device";
import { useIsFocused, useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { EnvironmentId, ThreadId } from "@t3tools/contracts";
Expand Down Expand Up @@ -73,6 +78,7 @@ function DevicePreviewScreen({
const [inputConnected, setInputConnected] = useState(false);
const [streamAttempt, setStreamAttempt] = useState(0);
const [shuttingDown, setShuttingDown] = useState(false);
const retryHost = useAtomCommand(deviceEnvironment.list);
const shutdown = useAtomCommand(deviceEnvironment.shutdown, { reportFailure: false });
const streamRef = useRef<DeviceStreamRef>(null);
const state = useEnvironmentQuery(deviceEnvironment.state({ environmentId, input: {} }));
Expand Down Expand Up @@ -116,10 +122,58 @@ function DevicePreviewScreen({
};

const controls: ScreenHeaderMenuItem[] = [
...(state.data?.hosts
.filter(
(host) =>
state.data?.supportsHostRetry && state.data.hostStatuses[host.id]?.status === "failed",
)
.map((host) => ({
id: `retry-${host.id}`,
title: `Retry ${host.label}`,
icon: "arrow.clockwise" as const,
onPress: () => {
void retryHost({ environmentId, input: { retryHostId: host.id } });
},
})) ?? []),
...(state.data?.supportsToolInspection
? [
{
id: "check-device-tools",
title: "Check device tool versions",
icon: "arrow.clockwise" as const,
onPress: () => {
void retryHost({ environmentId, input: { inspectOnly: true } });
},
},
]
: []),
{
id: "device-tools",
title: "Device tool versions",
icon: "info.circle",
onPress: () =>
Alert.alert(
"Device tool versions",
deviceToolUpdateOwnership +
"\n\n" +
deviceToolUpdatePolicy(
state.data?.hosts.find((host) => host.id === preview?.session.hostId)?.tools,
) +
"\n\n" +
deviceToolVersionLabels(
state.data?.hosts.find((host) => host.id === preview?.session.hostId)?.tools,
).join("\n") +
"\n" +
(state.data?.hosts.find((host) => host.id === preview?.session.hostId)
?.toolInspectionError ??
state.data?.hostStatuses[preview?.session.hostId ?? ""]?.detail ??
""),
),
},
{
id: "reload",
title: "Reload stream",
icon: "arrow.clockwise",
icon: "arrow.clockwise" as const,
disabled: !preview || shuttingDown,
onPress: () => {
setInputConnected(false);
Expand Down Expand Up @@ -149,7 +203,7 @@ function DevicePreviewScreen({
{
id: "rotate",
title: "Rotate device",
icon: "arrow.clockwise",
icon: "arrow.clockwise" as const,
disabled: !inputConnected,
onPress: () => streamRef.current?.rotate(),
},
Expand Down
9 changes: 9 additions & 0 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -873,12 +873,20 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {

useEffect(() => {
if (
!selectedProjectDraftKey ||
!defaultWorkspaceModeSettled ||
workspaceMode !== "worktree" ||
selectedBranchName !== null
) {
return;
}
// The draft screen writes a thread's branch and worktree into the draft in
// the same commit this effect runs, so the rendered selection above can be
// stale. Re-read the draft before replacing it.
const live = getComposerDraftSnapshot(selectedProjectDraftKey).workspaceSelection;
if (live && (live.mode !== "worktree" || live.branch !== null)) {
return;
}
// The default may only exist as origin/<default> (isRemote), which
// availableBranches filters out — search the unfiltered refs for it.
const preferredBranch =
Expand All @@ -894,6 +902,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
defaultWorkspaceModeSettled,
selectBranch,
selectedBranchName,
selectedProjectDraftKey,
workspaceMode,
]);

Expand Down
20 changes: 19 additions & 1 deletion apps/server/src/auth/RpcAuthorization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from "@t3tools/contracts";
import { describe, expect, it } from "@effect/vitest";

import { RPC_REQUIRED_SCOPES, requiredScopeForRpcMethod } from "./RpcAuthorization.ts";
import {
RPC_REQUIRED_SCOPES,
requiredScopeForRpcMethod,
requiredScopeForDeviceList,
} from "./RpcAuthorization.ts";

describe("RPC authorization scopes", () => {
it("declares exactly one scope for every RPC in the server group", () => {
Expand Down Expand Up @@ -71,3 +75,17 @@ describe("RPC authorization scopes", () => {
}
});
});

it("requires operate permission for host retry while preserving read-only listing", () => {
expect(requiredScopeForDeviceList({})).toBe(AuthOrchestrationReadScope);
expect(requiredScopeForDeviceList({ retryHostId: "remote-host" })).toBe(
AuthOrchestrationOperateScope,
);
});

it("requires operate permission for tool updates even alongside a read-only check", () => {
expect(requiredScopeForDeviceList({ updateTool: "agent", inspectOnly: true })).toBe(
AuthOrchestrationOperateScope,
);
expect(requiredScopeForDeviceList({ updateTool: "hub" })).toBe(AuthOrchestrationOperateScope);
});
7 changes: 7 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type DeviceListInput,
AuthAccessReadScope,
AuthOrchestrationOperateScope,
AuthOrchestrationReadScope,
Expand Down Expand Up @@ -180,3 +181,9 @@ export function requiredScopeForRpcMethod(method: string): AuthEnvironmentScope
}
return requiredScope;
}

/** Retrying can install or restart tools even though ordinary listing is readable. */
export const requiredScopeForDeviceList = (input: DeviceListInput): AuthEnvironmentScope =>
input.retryHostId || input.updateTool
? AuthOrchestrationOperateScope
: AuthOrchestrationReadScope;
5 changes: 3 additions & 2 deletions apps/server/src/device/DeviceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export class DeviceHost extends Context.Service<
{
readonly id: DeviceHostId;
readonly summary: Effect.Effect<DeviceHostSummary>;
readonly inspect?: Effect.Effect<DeviceHostSummary, DeviceHostError>;
readonly platformAvailability: (
platform: DevicePlatform,
) => Effect.Effect<DevicePlatformAvailability>;
Expand All @@ -88,11 +89,11 @@ export class DeviceHost extends Context.Service<
* concurrent callers share one start, and a ready host returns immediately.
*/
readonly ensureReady: (
onPhase: (phase: "installing" | "starting") => Effect.Effect<void>,
onPhase: (phase: "installing" | "starting", detail?: string) => Effect.Effect<void>,
) => Effect.Effect<DeviceHostReady, DeviceHostError | NodeRuntimeUnavailableError>;
/** Installs and starts agent-device after the user grants agent access. */
readonly ensureAgentReady: (
onPhase: (phase: "installing" | "starting") => Effect.Effect<void>,
onPhase: (phase: "installing" | "starting", detail?: string) => Effect.Effect<void>,
) => Effect.Effect<
DeviceHostAgentReady,
DeviceHostError | DeviceHostTimeoutError | NodeRuntimeUnavailableError
Expand Down
Loading
Loading