Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e379ba5
Add Devin CLI provider
Derpedyea Jul 3, 2026
4f7c853
Address Devin PR review comments
Derpedyea Jul 4, 2026
26cfd06
Prepare Devin ACP integration for readiness
Derpedyea Jul 4, 2026
ff20f15
Fix Windows PATH capture command
Derpedyea Jul 4, 2026
9e9a5c8
Seed Devin models with one-shot discovery
Derpedyea Jul 4, 2026
dc90c27
Reject incomplete ACP JSON generations
Derpedyea Jul 4, 2026
df00c91
Harden Devin ACP provider recovery
Derpedyea Jul 4, 2026
93b74f9
Reject stale ACP user input responses
Derpedyea Jul 4, 2026
0fba505
Remove local artifact ignores from PR
Derpedyea Jul 4, 2026
eddf227
Prune redundant Devin adapter tests
Derpedyea Jul 4, 2026
3990205
Add Devin ask-question ACP support
Derpedyea Jul 5, 2026
6ae7978
Add Devin plan extension callbacks
Derpedyea Jul 5, 2026
24e4ff3
Fix ACP interrupt and Devin input handling
Derpedyea Jul 5, 2026
a28ae06
Support custom Devin reasoning levels
Derpedyea Jul 7, 2026
38e6c79
Fix Devin error detail conventions
Derpedyea Jul 8, 2026
2d896b8
Fix Devin ACP plan mode mapping
Derpedyea Jul 8, 2026
129a47f
Handle Devin private elicitation
Derpedyea Jul 9, 2026
4a96454
Improve Devin ACP integration
Derpedyea Jul 9, 2026
0502963
Fix Devin ACP plan approval handling
Derpedyea Jul 9, 2026
d4429a6
Handle Devin custom elicitation answers
Derpedyea Jul 9, 2026
cd7fefe
Fix pending user input single-select submit
Derpedyea Jul 9, 2026
12f0c1b
Harden Devin ACP elicitation transport
Derpedyea Jul 9, 2026
88eaeb1
Merge branch 'main' into codex/add-devin-cli-provider
Derpedyea Jul 9, 2026
558b9a9
Fix Devin elicitation response IDs
Derpedyea Jul 9, 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
10 changes: 9 additions & 1 deletion apps/desktop/src/shell/DesktopShellEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,17 @@ const captureWindowsEnvironmentCommand = (names: ReadonlyArray<string>) =>
[
"$ErrorActionPreference = 'Stop'",
...names.flatMap((name) => {
const valueCapture =
name.toUpperCase() === "PATH"
? [
"$parts = @([Environment]::GetEnvironmentVariable('Path', 'Process'), [Environment]::GetEnvironmentVariable('Path', 'Machine'), [Environment]::GetEnvironmentVariable('Path', 'User')) | Where-Object { $null -ne $_ -and $_.Length -gt 0 }",
"$value = [string]::Join(';', $parts)",
]
: [`$value = [Environment]::GetEnvironmentVariable('${name}')`];

return [
`Write-Output '${startMarker(name)}'`,
`$value = [Environment]::GetEnvironmentVariable('${name}')`,
...valueCapture,
"if ($null -ne $value -and $value.Length -gt 0) { Write-Output $value }",
`Write-Output '${endMarker(name)}'`,
];
Expand Down
14 changes: 11 additions & 3 deletions apps/mobile/src/features/threads/PendingUserInputCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { Pressable, View } from "react-native";

import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
import { cn } from "../../lib/cn";
import type { PendingUserInput, PendingUserInputDraftAnswer } from "../../lib/threadActivity";
import type {
PendingUserInput,
PendingUserInputAnswers,
PendingUserInputDraftAnswer,
} from "../../lib/threadActivity";

export interface PendingUserInputCardProps {
readonly pendingUserInput: PendingUserInput;
readonly drafts: Record<string, PendingUserInputDraftAnswer>;
readonly answers: Record<string, string> | null;
readonly answers: PendingUserInputAnswers | null;
readonly respondingUserInputId: ApprovalRequestId | null;
readonly onSelectOption: (
requestId: ApprovalRequestId,
Expand All @@ -34,6 +38,9 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
</Text>
{props.pendingUserInput.questions.map((question) => {
const draft = props.drafts[question.id];
const selectedOptionLabels = Array.isArray(draft?.selectedOptionLabels)
? draft.selectedOptionLabels
: [];
return (
<View key={question.id} className="gap-2 pt-1">
<Text className="font-t3-bold text-xs uppercase tracking-[1px] text-neutral-500 dark:text-neutral-500">
Expand All @@ -45,7 +52,8 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
<View className="flex-row flex-wrap gap-2.5">
{question.options.map((option) => {
const selected =
draft?.selectedOptionLabel === option.label && !draft.customAnswer?.trim().length;
selectedOptionLabels.includes(option.label) &&
!draft?.customAnswer?.trim().length;
return (
<Pressable
key={option.label}
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { scopedThreadKey } from "../../lib/scopedEntities";
import type {
PendingApproval,
PendingUserInput,
PendingUserInputAnswers,
PendingUserInputDraftAnswer,
ThreadFeedEntry,
} from "../../lib/threadActivity";
Expand All @@ -56,7 +57,7 @@ export interface ThreadDetailScreenProps {
readonly respondingApprovalId: ApprovalRequestId | null;
readonly activePendingUserInput: PendingUserInput | null;
readonly activePendingUserInputDrafts: Record<string, PendingUserInputDraftAnswer>;
readonly activePendingUserInputAnswers: Record<string, string> | null;
readonly activePendingUserInputAnswers: PendingUserInputAnswers | null;
readonly respondingUserInputId: ApprovalRequestId | null;
readonly draftMessage: string;
readonly draftAttachments: ReadonlyArray<DraftComposerImageAttachment>;
Expand Down
85 changes: 73 additions & 12 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ export interface PendingUserInput {
}

export interface PendingUserInputDraftAnswer {
readonly selectedOptionLabel?: string;
readonly selectedOptionLabels?: ReadonlyArray<string>;
readonly customAnswer?: string;
}

export type PendingUserInputAnswers = Record<string, string | ReadonlyArray<string>>;

export interface ThreadFeedActivity {
readonly id: string;
readonly createdAt: string;
Expand Down Expand Up @@ -182,7 +184,8 @@ function parseUserInputQuestions(
) {
return null;
}
const options = question.options
const rawOptions = question.options;
const options = rawOptions
.map<UserInputQuestion["options"][number] | null>((option) => {
if (!option || typeof option !== "object") return null;
const record = option as Record<string, unknown>;
Expand All @@ -195,14 +198,15 @@ function parseUserInputQuestions(
};
})
.filter((option): option is UserInputQuestion["options"][number] => option !== null);
if (options.length === 0) {
if (rawOptions.length > 0 && options.length === 0) {
return null;
}
return {
id: question.id,
header: question.header,
question: question.question,
options,
required: question.required !== false,
multiSelect: question.multiSelect === true,
};
})
Expand All @@ -219,14 +223,41 @@ function normalizeDraftAnswer(value: string | undefined): string | null {
return trimmed.length > 0 ? trimmed : null;
}

function normalizeSelectedOptionLabels(value: ReadonlyArray<string> | undefined): string[] {
if (!Array.isArray(value)) {
return [];
}

const normalized: string[] = [];
for (const entry of value) {
const trimmed = entry.trim();
if (trimmed.length > 0) {
normalized.push(trimmed);
}
}

return Array.from(new Set(normalized));
}

function resolvePendingUserInputAnswer(
question: UserInputQuestion,
draft: PendingUserInputDraftAnswer | undefined,
): string | null {
): string | ReadonlyArray<string> | null {
const customAnswer = normalizeDraftAnswer(draft?.customAnswer);
if (customAnswer) {
return customAnswer;
}
return normalizeDraftAnswer(draft?.selectedOptionLabel);

const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels);
if (question.multiSelect) {
return selectedOptionLabels.length > 0 ? selectedOptionLabels : null;
}

return selectedOptionLabels[0] ?? null;
}

function isRequiredQuestion(question: UserInputQuestion): boolean {
return question.required !== false;
}

function deriveWorkLogEntries(
Expand Down Expand Up @@ -1284,24 +1315,54 @@ export function setPendingUserInputCustomAnswer(
draft: PendingUserInputDraftAnswer | undefined,
customAnswer: string,
): PendingUserInputDraftAnswer {
const selectedOptionLabel =
customAnswer.trim().length > 0 ? undefined : draft?.selectedOptionLabel;
const selectedOptionLabels =
customAnswer.trim().length > 0
? undefined
: normalizeSelectedOptionLabels(draft?.selectedOptionLabels);
return {
customAnswer,
...(selectedOptionLabel ? { selectedOptionLabel } : {}),
...(selectedOptionLabels && selectedOptionLabels.length > 0 ? { selectedOptionLabels } : {}),
};
}

export function togglePendingUserInputOptionSelection(
question: UserInputQuestion,
draft: PendingUserInputDraftAnswer | undefined,
optionLabel: string,
): PendingUserInputDraftAnswer {
if (question.multiSelect) {
const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels);
const nextSelectedOptionLabels = selectedOptionLabels.includes(optionLabel)
? selectedOptionLabels.filter((label) => label !== optionLabel)
: [...selectedOptionLabels, optionLabel];

return {
customAnswer: "",
...(nextSelectedOptionLabels.length > 0
? { selectedOptionLabels: nextSelectedOptionLabels }
: {}),
};
}

return {
customAnswer: "",
selectedOptionLabels: [optionLabel],
};
}

export function buildPendingUserInputAnswers(
questions: ReadonlyArray<UserInputQuestion>,
draftAnswers: Record<string, PendingUserInputDraftAnswer>,
): Record<string, string> | null {
const answers: Record<string, string> = {};
): PendingUserInputAnswers | null {
const answers: PendingUserInputAnswers = {};

for (const question of questions) {
const answer = resolvePendingUserInputAnswer(draftAnswers[question.id]);
const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]);
if (!answer) {
return null;
if (isRequiredQuestion(question)) {
return null;
}
continue;
}
answers[question.id] = answer;
}
Expand Down
32 changes: 24 additions & 8 deletions apps/mobile/src/state/use-selected-thread-requests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useAtomValue } from "@effect/atom-react";
import { useCallback, useMemo, useState } from "react";

import { ApprovalRequestId, type ProviderApprovalDecision } from "@t3tools/contracts";
import {
ApprovalRequestId,
type ProviderApprovalDecision,
type UserInputQuestion,
} from "@t3tools/contracts";
import { Atom } from "effect/unstable/reactivity";

import { threadEnvironment } from "../state/threads";
Expand All @@ -11,6 +15,8 @@ import {
derivePendingApprovals,
derivePendingUserInputs,
setPendingUserInputCustomAnswer,
togglePendingUserInputOptionSelection,
type PendingUserInputAnswers,
type PendingUserInputDraftAnswer,
} from "../lib/threadActivity";
import { appAtomRegistry } from "./atom-registry";
Expand All @@ -22,15 +28,21 @@ const userInputDraftsByRequestKeyAtom = Atom.make<
Record<string, Record<string, PendingUserInputDraftAnswer>>
>({}).pipe(Atom.keepAlive, Atom.withLabel("mobile:user-input-drafts"));

function setUserInputDraftOption(requestKey: string, questionId: string, label: string): void {
function setUserInputDraftOption(
requestKey: string,
question: UserInputQuestion,
label: string,
): void {
const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom);
appAtomRegistry.set(userInputDraftsByRequestKeyAtom, {
...current,
[requestKey]: {
...current[requestKey],
[questionId]: {
selectedOptionLabel: label,
},
[question.id]: togglePendingUserInputOptionSelection(
question,
current[requestKey]?.[question.id],
label,
),
},
});
}
Expand Down Expand Up @@ -86,7 +98,7 @@ export function useSelectedThreadRequests() {
scopedRequestKey(selectedThreadShell.environmentId, activePendingUserInput.requestId)
] ?? {})
: {};
const activePendingUserInputAnswers = activePendingUserInput
const activePendingUserInputAnswers: PendingUserInputAnswers | null = activePendingUserInput
? buildPendingUserInputAnswers(activePendingUserInput.questions, activePendingUserInputDrafts)
: null;

Expand All @@ -97,9 +109,13 @@ export function useSelectedThreadRequests() {
}

const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId);
setUserInputDraftOption(requestKey, questionId, label);
const question = activePendingUserInput?.questions.find((entry) => entry.id === questionId);
if (!question) {
return;
}
setUserInputDraftOption(requestKey, question, label);
},
[selectedThreadShell],
[activePendingUserInput, selectedThreadShell],
);

const onChangeUserInputCustomAnswer = useCallback(
Expand Down
3 changes: 2 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"build:bundle": "vp pack",
"start": "node dist/bin.mjs",
"typecheck": "tsgo --noEmit",
"test": "vp test run"
"test": "vp test run",
"test:devin": "vp test run src/provider/Layers/DevinAdapter.test.ts src/provider/Layers/DevinProvider.test.ts src/provider/acp/DevinAcpSupport.test.ts src/provider/acp/DevinElicitation.test.ts src/provider/acp/AcpAdapterRuntime.test.ts"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.170",
Expand Down
Loading
Loading