Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0c16e24
feat(server): add ACP registry agents
t3dotgg Sep 26, 2026
82ff020
fix(server): ACP registry approvals ask and close correctly
t3dotgg Sep 26, 2026
f34b0af
fix(server): keep managed binaries another instance still uses
t3dotgg Sep 26, 2026
d089b83
fix(server): cancel pending ACP URL sign-ins when a turn stops
t3dotgg Sep 26, 2026
b9a1926
fix(server): map every approval decision for MCP tool approvals
t3dotgg Sep 26, 2026
4b661ea
fix(server): ACP registry auto-approvals grant one call only
t3dotgg Sep 26, 2026
3268fc7
perf(server): reuse the fetched ACP Registry index for five minutes
t3dotgg Sep 26, 2026
861e17a
fix(server): show Plan only for ACP registry agents with a plan mode
t3dotgg Sep 26, 2026
aa19543
refactor(server): ACP registry uses the shared auto-approval picker
t3dotgg Sep 26, 2026
5531ead
fix(server): harden ACP registry installs and runner choice
t3dotgg Sep 26, 2026
b7f386a
fix(server): ACP registry form answers follow the agent's schema
t3dotgg Sep 26, 2026
2e7a70c
fix(web): middle-clicking Continue authentication records consent
t3dotgg Sep 26, 2026
8c01806
fix(server): a past ACP registry session no longer hides a failed sig…
t3dotgg Sep 26, 2026
84b8de7
fix(mobile): thread rows and fallback picks show the ACP Registry age…
t3dotgg Sep 26, 2026
2a67546
fix(server): ACP registry accepts commands in directories named like …
t3dotgg Sep 26, 2026
c36ec3c
fix(server): a queued ACP registry turn reads the model under the pro…
t3dotgg Sep 26, 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
60 changes: 59 additions & 1 deletion apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,67 @@ import { Image } from "expo-image";
import { Path, Svg } from "react-native-svg";
import { View } from "react-native";
import { providerInstanceInitials } from "@t3tools/client-runtime/state/provider-instance-display";
import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts";
import { useState } from "react";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { AppText as Text } from "./AppText";

type ProviderIconProps = {
readonly provider: string | null | undefined;
/** ACP Registry agents supply their own glyph from the official registry CDN. */
readonly iconUrl?: string | null | undefined;
readonly size?: number;
};

function AcpRegistryFallbackIcon(props: { readonly color: string; readonly size: number }) {
return (
<Svg width={props.size} height={props.size} viewBox="0 0 576 220" fill="none">
<Path
fill={props.color}
d="M568.003 115.821 517.278 27.966C507.183 10.482 489.084.023 468.894.023c-20.167 0-38.22 10.413-48.338 27.852L343.251 161.75H242.755c-6.525 0-12.369-3.365-15.62-9.004-3.274-5.639-3.274-12.369 0-18.03l50.726-87.855c3.251-5.639 9.094-9.027 15.62-9.027 6.525 0 12.346 3.365 15.62 9.027l3.024 5.229a6.81 6.81 0 0 0 5.911 3.411c2.433 0 4.707-1.319 5.912-3.433l13.437-23.555c1.41-2.479 1.137-5.571-.682-7.753C325.699 7.57 309.874 0 293.322 0c-.66 0-1.319 0-2.001.045-19.281.705-36.561 11.141-46.247 27.898l-44.859 77.714-44.405-76.509C145.465 11.209 126.594.023 106.608.023c-.659 0-1.319 0-2.001.045-19.28.705-36.56 11.141-46.246 27.898L7.658 115.821c-13.915 24.078-8.526 52.999 13.392 71.938 8.844 7.663 20.554 11.869 32.968 11.869h94.63c2.433 0 4.684-1.296 5.912-3.411l13.96-24.191a6.81 6.81 0 0 0 0-6.821c-1.228-2.115-3.479-3.411-5.912-3.411H56.042c-6.526 0-12.369-3.365-15.62-9.004-3.275-5.638-3.275-12.368 0-18.03l50.725-87.854c3.252-5.639 9.095-9.027 15.62-9.027 6.526 0 12.346 3.365 15.62 9.027l72.439 125.62c.205.364.432.682.705 1 3.229 5.139 7.299 9.959 12.255 14.256 8.845 7.662 20.554 11.869 32.968 11.869h80.67l-5.843 10.118a6.81 6.81 0 0 0 0 6.821c1.228 2.114 3.478 3.41 5.911 3.41h27.944c2.432 0 4.683-1.296 5.911-3.41l9.049-15.689 2.774-4.433.114-.205 85.99-149.334c3.251-5.639 9.095-9.027 15.62-9.027 6.526 0 12.369 3.365 15.62 9.027l50.726 87.855c3.251 5.639 3.274 12.391 0 18.03-3.252 5.639-9.095 9.027-15.62 9.027H418.669c-2.433 0-4.684 1.296-5.912 3.41l-13.983 24.192a6.81 6.81 0 0 0 0 6.821c1.228 2.114 3.479 3.41 5.912 3.41H518.21c21.6 0 41.085-11.436 50.816-29.83 9.027-17.053 8.64-37.22-1.045-54Z"
/>
</Svg>
);
}

function AcpRegistryProviderIcon(props: {
readonly color: string;
readonly iconUrl: string | null | undefined;
readonly size: number;
}) {
const iconUrl = resolveOfficialAcpRegistryIconUrl(props.iconUrl);
const [image, setImage] = useState<{
readonly iconUrl: string;
readonly status: "loaded" | "failed";
} | null>(null);
const currentImage = image?.iconUrl === iconUrl ? image : null;
const loaded = currentImage?.status === "loaded";

return (
<View style={{ width: props.size, height: props.size }}>
{!loaded ? <AcpRegistryFallbackIcon color={props.color} size={props.size} /> : null}
{iconUrl !== null && currentImage?.status !== "failed" ? (
<Image
accessibilityIgnoresInvertColors
cachePolicy="memory-disk"
contentFit="contain"
recyclingKey={iconUrl}
source={{ uri: iconUrl }}
style={{
position: "absolute",
width: props.size,
height: props.size,
opacity: loaded ? 1 : 0,
tintColor: props.color,
}}
onError={() => setImage({ iconUrl, status: "failed" })}
onLoad={() => setImage({ iconUrl, status: "loaded" })}
/>
) : null}
</View>
);
}

export function ProviderIcon(props: ProviderIconProps) {
const { themeAppearance } = useAppearancePreferences();
const isDarkMode = themeAppearance === "dark";
Expand All @@ -26,6 +79,10 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "acpRegistry") {
return <AcpRegistryProviderIcon color={mono} iconUrl={props.iconUrl} size={size} />;
Comment thread
t3dotgg marked this conversation as resolved.
}

if (props.provider === "claudeAgent") {
return (
<Svg width={size} height={size} viewBox="0 0 256 257" fill="none">
Expand Down Expand Up @@ -95,6 +152,7 @@ export function ProviderIcon(props: ProviderIconProps) {
*/
export function ProviderInstanceIcon(props: {
readonly provider: string | null | undefined;
readonly iconUrl?: string | undefined;
readonly size?: number;
readonly displayName: string;
readonly accentColor?: string;
Expand All @@ -104,7 +162,7 @@ export function ProviderInstanceIcon(props: {
return (
<View style={{ position: "relative" }}>
<View style={{ opacity: 0.6 }}>
<ProviderIcon provider={props.provider} size={props.size} />
<ProviderIcon provider={props.provider} iconUrl={props.iconUrl} size={props.size} />
</View>
{props.showBadge ? (
<View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,11 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
<View key={provider.instanceId}>
<View className="gap-1 p-4">
<View className="flex-row items-center gap-2">
<ProviderIcon provider={provider.driver} size={18} />
<ProviderIcon
iconUrl={provider.iconUrl}
provider={provider.driver}
size={18}
/>
<Text className="min-w-0 flex-1 text-base font-t3-medium text-foreground">
{provider.displayName ?? provider.driver}
</Text>
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,7 @@ export function NewTaskDraftScreen(props: {
emphasized
renderIcon={(size) => (
<ProviderIcon
iconUrl={flow.selectedModelOption?.providerIconUrl}
provider={flow.selectedModelOption?.providerDriver}
size={size}
/>
Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
accessibilityLabel="Model and reasoning settings"
emphasized
renderIcon={(size) => (
<ProviderIcon provider={currentModelOption?.providerDriver} size={size} />
<ProviderIcon
iconUrl={currentModelOption?.providerIconUrl}
provider={currentModelOption?.providerDriver}
size={size}
/>
)}
label={currentModelOption?.label ?? currentModelSelection.model}
maxWidth="100%"
Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/src/features/threads/ThreadSettingsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const FAVORITES_PROVIDER_FILTER = "@favorites";
/** Provider catalog header with its harness logo and disclosure state. */
function ProviderHeader(props: {
readonly driver: string | undefined;
readonly iconUrl: string | undefined;
readonly label: string;
readonly collapsible: boolean;
readonly collapsed: boolean;
Expand All @@ -131,7 +132,7 @@ function ProviderHeader(props: {
}) {
const content = (
<>
<ProviderIcon provider={props.driver} size={15} />
<ProviderIcon iconUrl={props.iconUrl} provider={props.driver} size={15} />
<Text className="text-sm font-t3-medium text-foreground-muted">{props.label}</Text>
{props.collapsible ? (
<>
Expand Down Expand Up @@ -514,6 +515,7 @@ function useThreadSettingsSession() {
type ThreadSettingsProviderCatalog = {
readonly key: string;
readonly driver: string | undefined;
readonly iconUrl: string | undefined;
readonly label: string;
readonly collapsible: boolean;
readonly collapsed: boolean;
Expand Down Expand Up @@ -582,6 +584,7 @@ function ThreadSettingsProviderListHeader(props: {
collapsible={props.provider.collapsible}
collapsed={props.provider.collapsed}
driver={props.provider.driver}
iconUrl={props.provider.iconUrl}
label={props.provider.label}
modelCount={props.provider.modelCount}
onToggle={onToggle}
Expand Down Expand Up @@ -643,6 +646,7 @@ function useThreadSettingsCatalogItems(
const provider: ThreadSettingsProviderCatalog = {
key: group.providerKey,
driver,
iconUrl: group.models[0]?.providerIconUrl,
label: group.providerLabel,
collapsible,
collapsed,
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
{props.providerInstance ? (
<ProviderInstanceIcon
provider={props.providerInstance.driverKind}
iconUrl={props.providerInstance.iconUrl}
size={14}
displayName={props.providerInstance.displayName}
accentColor={props.providerInstance.accentColor}
Expand Down
13 changes: 13 additions & 0 deletions apps/mobile/src/features/threads/thread-provider-instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function makeConfig(
readonly driver: string;
readonly displayName?: string;
readonly accentColor?: string;
readonly iconUrl?: string;
}>,
): ServerConfig {
return { providers } as unknown as ServerConfig;
Expand Down Expand Up @@ -98,6 +99,18 @@ describe("resolveThreadProviderInstance", () => {

expect(resolveThreadProviderInstance(serverConfigs, thread)?.showBadge).toBe(false);
});

it("carries an ACP Registry agent's icon to the thread row", () => {
const environmentId = EnvironmentId.make("environment-a");
const iconUrl = "https://cdn.agentclientprotocol.com/registry/v1/latest/kimi.svg";
const serverConfigs = new Map<EnvironmentId, ServerConfig>([
[environmentId, makeConfig([{ instanceId: "acp_kimi", driver: "acpRegistry", iconUrl }])],
]);

expect(
resolveThreadProviderInstance(serverConfigs, makeThread(environmentId, "acp_kimi"))?.iconUrl,
).toBe(iconUrl);
});
});

describe("createThreadRowProviderInstanceResolver", () => {
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/features/threads/thread-provider-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface ThreadRowProviderInstance {
readonly driverKind: ProviderDriverKind;
readonly displayName: string;
readonly accentColor?: string | undefined;
/** ACP Registry agent icon; other drivers draw their built-in glyph. */
readonly iconUrl?: string | undefined;
readonly showBadge: boolean;
}

Expand All @@ -36,6 +38,7 @@ export function resolveThreadProviderInstance(
};
return {
...entry,
...(snapshot.iconUrl ? { iconUrl: snapshot.iconUrl } : {}),
showBadge: shouldShowInstanceBadge(
entry,
providers.map((provider) => ({ driverKind: provider.driver })),
Expand Down
26 changes: 26 additions & 0 deletions apps/mobile/src/lib/modelOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,32 @@ describe("mobile model options", () => {
expect(resolveSelectableModelSelection(null, disabled)).toBe(disabled);
});

it("keeps an ACP Registry agent's icon on a selection outside its model list", () => {
const config = {
providers: [
{
instanceId: "acp_kimi",
driver: "acpRegistry",
displayName: "Kimi",
iconUrl: "https://cdn.agentclientprotocol.com/registry/v1/latest/kimi.svg",
enabled: true,
installed: true,
auth: { status: "authenticated" },
models: [],
},
],
} as unknown as ServerConfig;
const selection = { instanceId: ProviderInstanceId.make("acp_kimi"), model: "removed-model" };

expect(buildModelOptions(config, selection)).toMatchObject([
{
providerDriver: "acpRegistry",
providerIconUrl: "https://cdn.agentclientprotocol.com/registry/v1/latest/kimi.svg",
selection,
},
]);
});

describe("Antigravity selections", () => {
const selection = {
instanceId: ProviderInstanceId.make("google_work"),
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/lib/modelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ModelOption = {
readonly providerKey: string;
readonly providerLabel: string;
readonly providerDriver: string;
readonly providerIconUrl?: string | undefined;
readonly isDefault: boolean;
readonly isLegacy: boolean;
readonly isUnavailable?: boolean;
Expand Down Expand Up @@ -173,6 +174,7 @@ export function buildModelOptions(
providerKey: provider.instanceId,
providerLabel,
providerDriver: provider.driver,
...(provider.iconUrl ? { providerIconUrl: provider.iconUrl } : {}),
isDefault: model.isDefault === true,
isLegacy: model.isLegacy === true,
capabilities: model.capabilities,
Expand Down Expand Up @@ -220,6 +222,7 @@ export function buildModelOptions(
providerKey: fallbackModelSelection.instanceId,
providerLabel,
providerDriver,
...(provider?.iconUrl ? { providerIconUrl: provider.iconUrl } : {}),
isDefault: false,
isLegacy: model?.isLegacy === true,
...(isModelSelectionUnavailable(config, fallbackModelSelection)
Expand Down
3 changes: 3 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,9 @@ const program = Effect.gen(function* () {
type: "object",
properties: {
approved: { type: "boolean", title: "Approved" },
color: { type: "string", title: "Color", enum: ["red", "blue"] },
tags: { type: "array", title: "Tags", items: { type: "string", enum: ["a", "b"] } },
count: { type: "integer", title: "Count" },
},
},
...(emitMcpToolApprovalElicitation
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.serverGetSettings]: AuthOrchestrationReadScope,
[WS_METHODS.serverUpdateSettings]: AuthOrchestrationOperateScope,
[WS_METHODS.serverDiscoverSourceControl]: AuthOrchestrationReadScope,
[WS_METHODS.serverSearchAcpRegistry]: AuthOrchestrationReadScope,
[WS_METHODS.serverPrepareAcpRegistryAgent]: AuthOrchestrationOperateScope,
[WS_METHODS.serverUninstallAcpRegistryManagedBinary]: AuthOrchestrationOperateScope,
[WS_METHODS.serverAcceptAcpRegistryUrlAuth]: AuthOrchestrationOperateScope,
[WS_METHODS.serverGetTraceDiagnostics]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetProcessDiagnostics]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetHostResources]: AuthOrchestrationReadScope,
Expand Down
Loading
Loading