Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,12 @@ function ProviderAccount({
!signedIn &&
!auth.error &&
state?.methods === undefined;
const hasAdvertisedSignIn = (state?.methods?.length ?? 0) > 0;
const needsExternalSetup =
!active &&
!signedIn &&
!isDiscovering &&
!hasAdvertisedSignIn &&
(provider.setup?.canAuthenticate === false ||
(provider.driver === "acpRegistry" && state?.methods?.length === 0));
const url =
Expand Down Expand Up @@ -232,7 +235,7 @@ function ProviderAccount({
const canSignIn =
!active &&
!needsExternalSetup &&
provider.setup?.canAuthenticate !== false &&
(provider.setup?.canAuthenticate !== false || hasAdvertisedSignIn) &&
provider.enabled &&
provider.installed;
const signInDisabled = disabled || state === null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ describe("AddProviderInstanceDialog environment routing", () => {
expect(actions.onOpenChange).toHaveBeenCalledWith(false);
});

it("creates a local command in the selected environment without a sign-in step", async () => {
it("creates a local command in the selected environment and opens sign-in", async () => {
setup();
await using _screen = await render(dialog());
await click("Show more");
Expand All @@ -271,7 +271,7 @@ describe("AddProviderInstanceDialog environment routing", () => {
await type('input[aria-label="Environment variable value 1"]', "work");
await click("Next");
await type("#add-provider-label", "Deepseek Harness");
await click("Add instance");
await click("Continue to sign-in");

expect(settingsHooks.useMutation).toHaveBeenCalledWith(remoteEnvironmentId);
expect(settingsHooks.mutate).toHaveBeenCalledWith({
Expand All @@ -287,8 +287,8 @@ describe("AddProviderInstanceDialog environment routing", () => {
],
},
});
expect(actions.onOpenChange).toHaveBeenCalledWith(false);
expect(signInStep()).toBeNull();
expect(actions.onOpenChange).not.toHaveBeenCalled();
expect(signInStep()?.textContent).toContain("remote-device/acpRegistry_deepseek_harness");
});

it("rejects a queued save after the provider grant is revoked", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const ACP_REGISTRY_IDENTITY_STEP = 1;

export const ADD_PROVIDER_WIZARD_STEPS = ["Provider", "Identity", "Config"] as const;
export const ACP_REGISTRY_WIZARD_STEPS = ["Provider", "Identity", "Sign in"] as const;
export const LOCAL_ACP_WIZARD_STEPS = ["Provider", "Identity"] as const;

export interface ProviderIdentityDraft {
readonly label: string;
Expand Down
26 changes: 8 additions & 18 deletions apps/web/src/components/settings/AddProviderInstanceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import { WizardPanel, WizardPopup, WizardHeader, WizardFooter } from "../ui/wiza
import {
ADD_PROVIDER_WIZARD_STEPS,
ACP_REGISTRY_WIZARD_STEPS,
LOCAL_ACP_WIZARD_STEPS,
resolveAcpRegistryWizardNavigation,
resolveWizardNavigation,
updateProviderIdentityDraft,
Expand Down Expand Up @@ -277,7 +276,7 @@ export function AddProviderInstanceDialog({
if (navigation.kind === "blocked") {
setHasAttemptedSubmit(true);
}
if (isAcpRegistry && !isLocalAcp && navigation.kind === "navigate" && navigation.step === 2) {
if (isAcpRegistry && navigation.kind === "navigate" && navigation.step === 2) {
void handleSave();
return;
}
Expand All @@ -287,15 +286,10 @@ export function AddProviderInstanceDialog({
const navigateToStep = (requestedStep: number) => {
applyWizardNavigation(
isAcpRegistry
? isLocalAcp
? resolveWizardNavigation(wizardStep, requestedStep, LOCAL_ACP_WIZARD_STEPS.length, {
instanceIdError,
prerequisite: { step: 0, error: acpSelectionError },
})
: resolveAcpRegistryWizardNavigation(wizardStep, requestedStep, {
instanceIdError,
selectionError: acpSelectionError,
})
? resolveAcpRegistryWizardNavigation(wizardStep, requestedStep, {
instanceIdError,
selectionError: acpSelectionError,
})
: resolveWizardNavigation(wizardStep, requestedStep, ADD_PROVIDER_WIZARD_STEPS.length, {
instanceIdError,
}),
Expand Down Expand Up @@ -409,7 +403,7 @@ export function AddProviderInstanceDialog({
return;
}
onCreated?.(brandedId, driver);
if (isAcpRegistry && !isLocalAcp) {
if (isAcpRegistry) {
setCreatedInstanceId(brandedId);
setIsSaving(false);
setWizardStep(2);
Expand Down Expand Up @@ -445,7 +439,7 @@ export function AddProviderInstanceDialog({
currentStep={wizardStep}
summaries={wizardStepSummaries}
instanceIdError={instanceIdError}
steps={isLocalAcp ? LOCAL_ACP_WIZARD_STEPS : ACP_REGISTRY_WIZARD_STEPS}
steps={ACP_REGISTRY_WIZARD_STEPS}
disabled={isSaving || isPreparingRegistryAgent || createdInstanceId !== null}
identityStep={1}
prerequisite={{ step: 0, error: acpSelectionError }}
Expand Down Expand Up @@ -752,11 +746,7 @@ export function AddProviderInstanceDialog({
disabled={isSaving || !canManageProviders}
onClick={() => void handleSave()}
>
{isSaving
? "Adding..."
: isAcpRegistry && !isLocalAcp
? "Continue to sign-in"
: "Add instance"}
{isSaving ? "Adding..." : isAcpRegistry ? "Continue to sign-in" : "Add instance"}
</Button>
)}
</WizardFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ export function ProviderAuthenticationSection({
!signedIn &&
!query.error &&
auth?.methods === undefined;
const hasAdvertisedSignIn = (auth?.methods?.length ?? 0) > 0;
const needsExternalSetup =
!active &&
!signedIn &&
!isDiscovering &&
!hasAdvertisedSignIn &&
(provider.setup?.canAuthenticate === false ||
(provider.driver === "acpRegistry" && auth?.methods?.length === 0));
const usesCredentials =
Expand Down Expand Up @@ -276,7 +279,9 @@ export function ProviderAuthenticationSection({
>
Cancel
</Button>
) : !active && !needsExternalSetup && provider.setup?.canAuthenticate !== false ? (
) : !active &&
!needsExternalSetup &&
(provider.setup?.canAuthenticate !== false || hasAdvertisedSignIn) ? (
<Button
size="sm"
variant="outline"
Expand Down
31 changes: 27 additions & 4 deletions docs/user/providers-acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,39 @@ provider instance's environment overrides to use ACP v1.
Search only shows agents that can run on the connected server. Registry agents are third-party
code; review an agent's source and license before adding it.

Search also lists DeepSeek Harness and ZCode. T3 Code installs an agent when that command is not already available:

| Agent | What T3 Code runs |
| ------------------------------------------------------------------- | ------------------------------------------------------- |
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | `dsh --profile acp` from `@deepseek-ai/dsh` |
| [ZCode bridge](https://github.com/supermomonga/zcode-acp) | the `zcode-acp` binary from the bridge's GitHub release |

DeepSeek Harness is the `dsh` CLI. When `dsh` is already installed, T3 Code runs that copy with
`--profile acp` and keeps the credentials already stored in its profile. Otherwise T3 Code installs
`@deepseek-ai/dsh`. ACP mode is a profile, not a separate program, and the sign-in step can be
skipped when the CLI advertises no login.
ZCode still needs ZCode desktop 3.11.2. The unofficial bridge rejects other desktop versions. Select
**Add** for either agent, then complete the sign-in step.

## Add a local command

In **Settings → Providers → Add provider**, select **Show more**, then **Local ACP command**. Enter the executable
name or path on the selected environment, add one literal argument per row, and choose a display
name. For DeepSeek Harness, use `dsh` with arguments `--profile` and `acp`.
name.

Available models and session controls follow each agent's advertised capabilities. DeepSeek's
automation ACP profile does not provide transcript replay or session forking. Existing instances
can also be used from mobile.

Local commands require no registry entry or download. Environment overrides apply to the command;
credentials and skills remain managed by the installed agent or its wrapper. Models and reasoning
options come from ACP, including model IDs that contain JSON. T3 Code launches the executable directly
without expanding shell expressions.

On Windows, standard npm Node.js launchers (such as `dsh.cmd`) are resolved to their
Node.js script and launched directly. Custom batch scripts still require an explicit underlying
executable and literal arguments.

## Where agents run

Registry agents always run on the machine that hosts your T3 Code server. That stays true when you
Expand All @@ -53,9 +75,10 @@ but keeps package installs. To use an existing local binary, set **Executable ov

## Signing in

Open the agent's account section in **Settings → Providers** on web or desktop. Choose
**Sign in** and, if the agent offers several methods, select one. For an installed, configured
provider, mobile also offers **Settings → Providers**.
Open the agent's account section in **Settings → Providers** on web or desktop, or finish the
sign-in step shown after adding the instance. Choose **Sign in** and, if the agent offers several
methods, select one. Local commands use this same account section once they advertise a login.
For an installed, configured provider, mobile also offers **Settings → Providers**.

Browser sign-in shows the agent's URL and waits for you to open or copy it before telling the
agent to proceed. The page opens on your device, while the agent runs on the environment.
Expand Down
Loading
Loading