Update files
This commit is contained in:
@@ -1052,8 +1052,11 @@ function PermissionRequestCard({
|
|||||||
isPending: isResponding,
|
isPending: isResponding,
|
||||||
} = permissionMutation;
|
} = permissionMutation;
|
||||||
|
|
||||||
|
const [respondingAction, setRespondingAction] = useState<"accept" | "deny" | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetPermissionMutation();
|
resetPermissionMutation();
|
||||||
|
setRespondingAction(null);
|
||||||
}, [permission.request.id, resetPermissionMutation]);
|
}, [permission.request.id, resetPermissionMutation]);
|
||||||
const handleResponse = useCallback(
|
const handleResponse = useCallback(
|
||||||
(response: AgentPermissionResponse) => {
|
(response: AgentPermissionResponse) => {
|
||||||
@@ -1086,7 +1089,7 @@ function PermissionRequestCard({
|
|||||||
style={[
|
style={[
|
||||||
permissionStyles.container,
|
permissionStyles.container,
|
||||||
{
|
{
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface1,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -1120,19 +1123,9 @@ function PermissionRequestCard({
|
|||||||
Proposed plan
|
Proposed plan
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<View
|
<Markdown style={markdownStyles} rules={markdownRules}>
|
||||||
style={[
|
{planMarkdown}
|
||||||
permissionStyles.contentCard,
|
</Markdown>
|
||||||
{
|
|
||||||
backgroundColor: theme.colors.surface0,
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Markdown style={markdownStyles} rules={markdownRules}>
|
|
||||||
{planMarkdown}
|
|
||||||
</Markdown>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -1144,7 +1137,7 @@ function PermissionRequestCard({
|
|||||||
testID="permission-request-question"
|
testID="permission-request-question"
|
||||||
style={[
|
style={[
|
||||||
permissionStyles.question,
|
permissionStyles.question,
|
||||||
{ color: theme.colors.mutedForeground },
|
{ color: theme.colors.foregroundMuted },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
How would you like to proceed?
|
How would you like to proceed?
|
||||||
@@ -1158,29 +1151,26 @@ function PermissionRequestCard({
|
|||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="permission-request-deny"
|
testID="permission-request-deny"
|
||||||
style={(state) => {
|
style={({ pressed, hovered = false }) => [
|
||||||
const hovered = Boolean((state as any).hovered);
|
permissionStyles.optionButton,
|
||||||
const pressed = Boolean(state.pressed);
|
{
|
||||||
return [
|
backgroundColor: hovered
|
||||||
permissionStyles.optionButton,
|
? theme.colors.surface2
|
||||||
{
|
: theme.colors.surface1,
|
||||||
backgroundColor: hovered
|
borderColor: theme.colors.borderAccent,
|
||||||
? theme.colors.surface1
|
},
|
||||||
: theme.colors.surface2,
|
pressed ? permissionStyles.optionButtonPressed : null,
|
||||||
borderColor: theme.colors.borderAccent,
|
]}
|
||||||
},
|
onPress={() => {
|
||||||
pressed ? permissionStyles.optionButtonPressed : null,
|
setRespondingAction("deny");
|
||||||
];
|
|
||||||
}}
|
|
||||||
onPress={() =>
|
|
||||||
handleResponse({
|
handleResponse({
|
||||||
behavior: "deny",
|
behavior: "deny",
|
||||||
message: "Denied by user",
|
message: "Denied by user",
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
>
|
>
|
||||||
{isResponding ? (
|
{respondingAction === "deny" ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
||||||
) : (
|
) : (
|
||||||
<View style={permissionStyles.optionContent}>
|
<View style={permissionStyles.optionContent}>
|
||||||
@@ -1199,32 +1189,31 @@ function PermissionRequestCard({
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="permission-request-accept"
|
testID="permission-request-accept"
|
||||||
style={(state) => {
|
style={({ pressed, hovered = false }) => [
|
||||||
const hovered = Boolean((state as any).hovered);
|
permissionStyles.optionButton,
|
||||||
const pressed = Boolean(state.pressed);
|
{
|
||||||
return [
|
backgroundColor: hovered
|
||||||
permissionStyles.optionButton,
|
? theme.colors.surface2
|
||||||
{
|
: theme.colors.surface1,
|
||||||
backgroundColor: hovered
|
borderColor: theme.colors.borderAccent,
|
||||||
? theme.colors.surface1
|
},
|
||||||
: theme.colors.surface2,
|
pressed ? permissionStyles.optionButtonPressed : null,
|
||||||
borderColor: theme.colors.primary,
|
]}
|
||||||
},
|
onPress={() => {
|
||||||
pressed ? permissionStyles.optionButtonPressed : null,
|
setRespondingAction("accept");
|
||||||
];
|
handleResponse({ behavior: "allow" });
|
||||||
}}
|
}}
|
||||||
onPress={() => handleResponse({ behavior: "allow" })}
|
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
>
|
>
|
||||||
{isResponding ? (
|
{respondingAction === "accept" ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.primary} />
|
<ActivityIndicator size="small" color={theme.colors.foreground} />
|
||||||
) : (
|
) : (
|
||||||
<View style={permissionStyles.optionContent}>
|
<View style={permissionStyles.optionContent}>
|
||||||
<Check size={14} color={theme.colors.primary} />
|
<Check size={14} color={theme.colors.foreground} />
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
permissionStyles.optionText,
|
permissionStyles.optionText,
|
||||||
{ color: theme.colors.primary },
|
{ color: theme.colors.foreground },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
Accept
|
Accept
|
||||||
@@ -1365,8 +1354,8 @@ const permissionStyles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
lineHeight: 22,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
@@ -1378,13 +1367,6 @@ const permissionStyles = StyleSheet.create((theme) => ({
|
|||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
contentCard: {
|
|
||||||
padding: theme.spacing[3],
|
|
||||||
borderRadius: theme.borderRadius.lg,
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
flexShrink: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
},
|
|
||||||
question: {
|
question: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
marginTop: theme.spacing[1],
|
marginTop: theme.spacing[1],
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { View, Text, TextInput, Pressable, ActivityIndicator } from "react-native";
|
import {
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
View,
|
||||||
import { Check, X, Send } from "lucide-react-native";
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Pressable,
|
||||||
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
|
} from "react-native";
|
||||||
|
import { StyleSheet, useUnistyles, UnistylesRuntime } from "react-native-unistyles";
|
||||||
|
import { Check, CircleHelp, X } from "lucide-react-native";
|
||||||
import type { PendingPermission } from "@/types/shared";
|
import type { PendingPermission } from "@/types/shared";
|
||||||
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
||||||
|
|
||||||
@@ -59,18 +66,23 @@ interface QuestionFormCardProps {
|
|||||||
isResponding: boolean;
|
isResponding: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IS_WEB = Platform.OS === "web";
|
||||||
|
|
||||||
export function QuestionFormCard({
|
export function QuestionFormCard({
|
||||||
permission,
|
permission,
|
||||||
onRespond,
|
onRespond,
|
||||||
isResponding,
|
isResponding,
|
||||||
}: QuestionFormCardProps) {
|
}: QuestionFormCardProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const isMobile =
|
||||||
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const questions = parseQuestions(permission.request.input);
|
const questions = parseQuestions(permission.request.input);
|
||||||
|
|
||||||
// selections[questionIndex] = Set of selected option indices
|
|
||||||
const [selections, setSelections] = useState<Record<number, Set<number>>>({});
|
const [selections, setSelections] = useState<Record<number, Set<number>>>({});
|
||||||
// otherTexts[questionIndex] = custom "Other" text
|
|
||||||
const [otherTexts, setOtherTexts] = useState<Record<number, string>>({});
|
const [otherTexts, setOtherTexts] = useState<Record<number, string>>({});
|
||||||
|
const [respondingAction, setRespondingAction] = useState<
|
||||||
|
"submit" | "dismiss" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const toggleOption = useCallback(
|
const toggleOption = useCallback(
|
||||||
(qIndex: number, optIndex: number, multiSelect: boolean) => {
|
(qIndex: number, optIndex: number, multiSelect: boolean) => {
|
||||||
@@ -93,7 +105,6 @@ export function QuestionFormCard({
|
|||||||
}
|
}
|
||||||
return { ...prev, [qIndex]: next };
|
return { ...prev, [qIndex]: next };
|
||||||
});
|
});
|
||||||
// Clear "Other" text when an option is selected
|
|
||||||
setOtherTexts((prev) => {
|
setOtherTexts((prev) => {
|
||||||
if (!prev[qIndex]) return prev;
|
if (!prev[qIndex]) return prev;
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
@@ -106,7 +117,6 @@ export function QuestionFormCard({
|
|||||||
|
|
||||||
const setOtherText = useCallback((qIndex: number, text: string) => {
|
const setOtherText = useCallback((qIndex: number, text: string) => {
|
||||||
setOtherTexts((prev) => ({ ...prev, [qIndex]: text }));
|
setOtherTexts((prev) => ({ ...prev, [qIndex]: text }));
|
||||||
// Clear option selections when typing "Other"
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
setSelections((prev) => {
|
setSelections((prev) => {
|
||||||
if (!prev[qIndex] || prev[qIndex].size === 0) return prev;
|
if (!prev[qIndex] || prev[qIndex].size === 0) return prev;
|
||||||
@@ -126,6 +136,7 @@ export function QuestionFormCard({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
|
setRespondingAction("submit");
|
||||||
const answers: Record<string, string> = {};
|
const answers: Record<string, string> = {};
|
||||||
for (let i = 0; i < questions!.length; i++) {
|
for (let i = 0; i < questions!.length; i++) {
|
||||||
const q = questions![i];
|
const q = questions![i];
|
||||||
@@ -147,6 +158,7 @@ export function QuestionFormCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDeny() {
|
function handleDeny() {
|
||||||
|
setRespondingAction("dismiss");
|
||||||
onRespond({
|
onRespond({
|
||||||
behavior: "deny",
|
behavior: "deny",
|
||||||
message: "Dismissed by user",
|
message: "Dismissed by user",
|
||||||
@@ -158,7 +170,7 @@ export function QuestionFormCard({
|
|||||||
style={[
|
style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{
|
{
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface1,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -169,68 +181,62 @@ export function QuestionFormCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={qIndex} style={styles.questionBlock}>
|
<View key={qIndex} style={styles.questionBlock}>
|
||||||
<Text
|
<View style={styles.questionHeader}>
|
||||||
style={[styles.header, { color: theme.colors.foregroundMuted }]}
|
<Text
|
||||||
>
|
style={[styles.questionText, { color: theme.colors.foreground }]}
|
||||||
{q.header}
|
>
|
||||||
</Text>
|
{q.question}
|
||||||
<Text
|
</Text>
|
||||||
style={[styles.questionText, { color: theme.colors.foreground }]}
|
<CircleHelp size={14} color={theme.colors.foregroundMuted} />
|
||||||
>
|
</View>
|
||||||
{q.question}
|
|
||||||
</Text>
|
|
||||||
<View style={styles.optionsWrap}>
|
<View style={styles.optionsWrap}>
|
||||||
{q.options.map((opt, optIndex) => {
|
{q.options.map((opt, optIndex) => {
|
||||||
const isSelected = selected.has(optIndex);
|
const isSelected = selected.has(optIndex);
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={optIndex}
|
key={optIndex}
|
||||||
style={(state) => {
|
style={({ pressed, hovered = false }) => [
|
||||||
const hovered = Boolean((state as any).hovered);
|
styles.optionItem,
|
||||||
return [
|
(hovered || isSelected) && {
|
||||||
styles.chip,
|
backgroundColor: theme.colors.surface2,
|
||||||
{
|
},
|
||||||
borderColor: isSelected
|
pressed && styles.optionItemPressed,
|
||||||
? theme.colors.accent
|
]}
|
||||||
: theme.colors.border,
|
onPress={() =>
|
||||||
backgroundColor: isSelected
|
toggleOption(qIndex, optIndex, q.multiSelect)
|
||||||
? `${theme.colors.accent}18`
|
}
|
||||||
: hovered
|
|
||||||
? theme.colors.surface1
|
|
||||||
: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}}
|
|
||||||
onPress={() => toggleOption(qIndex, optIndex, q.multiSelect)}
|
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
>
|
>
|
||||||
<View style={styles.chipContent}>
|
<View style={styles.optionItemContent}>
|
||||||
{q.multiSelect && isSelected ? (
|
<View style={styles.optionTextBlock}>
|
||||||
<Check size={14} color={theme.colors.accent} />
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.optionLabel,
|
||||||
|
{ color: theme.colors.foreground },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</Text>
|
||||||
|
{opt.description ? (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.optionDescription,
|
||||||
|
{ color: theme.colors.foregroundMuted },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{opt.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
{isSelected ? (
|
||||||
|
<View style={styles.optionCheckSlot}>
|
||||||
|
<Check
|
||||||
|
size={16}
|
||||||
|
color={theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.chipLabel,
|
|
||||||
{
|
|
||||||
color: isSelected
|
|
||||||
? theme.colors.accent
|
|
||||||
: theme.colors.foreground,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
{opt.description ? (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.chipDescription,
|
|
||||||
{ color: theme.colors.foregroundMuted },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{opt.description}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -240,11 +246,13 @@ export function QuestionFormCard({
|
|||||||
styles.otherInput,
|
styles.otherInput,
|
||||||
{
|
{
|
||||||
borderColor: otherText.length > 0
|
borderColor: otherText.length > 0
|
||||||
? theme.colors.accent
|
? theme.colors.borderAccent
|
||||||
: theme.colors.border,
|
: theme.colors.border,
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
|
// @ts-expect-error - outlineStyle is web-only
|
||||||
|
IS_WEB && { outlineStyle: "none", outlineWidth: 0, outlineColor: "transparent" },
|
||||||
]}
|
]}
|
||||||
placeholder="Other..."
|
placeholder="Other..."
|
||||||
placeholderTextColor={theme.colors.foregroundMuted}
|
placeholderTextColor={theme.colors.foregroundMuted}
|
||||||
@@ -256,25 +264,31 @@ export function QuestionFormCard({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.actionsContainer,
|
||||||
|
!isMobile && styles.actionsContainerDesktop,
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={(state) => {
|
style={({ pressed, hovered = false }) => [
|
||||||
const hovered = Boolean((state as any).hovered);
|
styles.actionButton,
|
||||||
return [
|
{
|
||||||
styles.actionButton,
|
backgroundColor: hovered
|
||||||
{
|
? theme.colors.surface2
|
||||||
backgroundColor: hovered
|
: theme.colors.surface1,
|
||||||
? theme.colors.surface1
|
borderColor: theme.colors.borderAccent,
|
||||||
: theme.colors.surface2,
|
},
|
||||||
borderColor: theme.colors.border,
|
pressed && styles.optionItemPressed,
|
||||||
},
|
]}
|
||||||
];
|
|
||||||
}}
|
|
||||||
onPress={handleDeny}
|
onPress={handleDeny}
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
>
|
>
|
||||||
{isResponding ? (
|
{respondingAction === "dismiss" ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color={theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.actionContent}>
|
<View style={styles.actionContent}>
|
||||||
<X size={14} color={theme.colors.foregroundMuted} />
|
<X size={14} color={theme.colors.foregroundMuted} />
|
||||||
@@ -291,34 +305,49 @@ export function QuestionFormCard({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={(state) => {
|
style={({ pressed, hovered = false }) => {
|
||||||
const hovered = Boolean((state as any).hovered);
|
|
||||||
const disabled = !allAnswered || isResponding;
|
const disabled = !allAnswered || isResponding;
|
||||||
return [
|
return [
|
||||||
styles.actionButton,
|
styles.actionButton,
|
||||||
{
|
{
|
||||||
backgroundColor: hovered && !disabled
|
backgroundColor:
|
||||||
? theme.colors.surface1
|
hovered && !disabled
|
||||||
: theme.colors.surface2,
|
? theme.colors.surface2
|
||||||
|
: theme.colors.surface1,
|
||||||
borderColor: disabled
|
borderColor: disabled
|
||||||
? theme.colors.border
|
? theme.colors.border
|
||||||
: theme.colors.accent,
|
: theme.colors.borderAccent,
|
||||||
opacity: disabled ? 0.5 : 1,
|
opacity: disabled ? 0.5 : 1,
|
||||||
},
|
},
|
||||||
|
pressed && !disabled ? styles.optionItemPressed : null,
|
||||||
];
|
];
|
||||||
}}
|
}}
|
||||||
onPress={handleSubmit}
|
onPress={handleSubmit}
|
||||||
disabled={!allAnswered || isResponding}
|
disabled={!allAnswered || isResponding}
|
||||||
>
|
>
|
||||||
{isResponding ? (
|
{respondingAction === "submit" ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.accent} />
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color={theme.colors.foreground}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.actionContent}>
|
<View style={styles.actionContent}>
|
||||||
<Send size={14} color={allAnswered ? theme.colors.accent : theme.colors.foregroundMuted} />
|
<Check
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
allAnswered
|
||||||
|
? theme.colors.foreground
|
||||||
|
: theme.colors.foregroundMuted
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.actionText,
|
styles.actionText,
|
||||||
{ color: allAnswered ? theme.colors.accent : theme.colors.foregroundMuted },
|
{
|
||||||
|
color: allAnswered
|
||||||
|
? theme.colors.foreground
|
||||||
|
: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
@@ -333,7 +362,6 @@ export function QuestionFormCard({
|
|||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
marginVertical: theme.spacing[3],
|
|
||||||
padding: theme.spacing[3],
|
padding: theme.spacing[3],
|
||||||
borderRadius: theme.spacing[2],
|
borderRadius: theme.spacing[2],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -342,55 +370,70 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
questionBlock: {
|
questionBlock: {
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
header: {
|
questionHeader: {
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontWeight: theme.fontWeight.semibold,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
questionText: {
|
|
||||||
fontSize: theme.fontSize.base,
|
|
||||||
fontWeight: theme.fontWeight.medium,
|
|
||||||
},
|
|
||||||
optionsWrap: {
|
|
||||||
flexDirection: "row",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
chip: {
|
|
||||||
paddingVertical: theme.spacing[2],
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
borderRadius: theme.borderRadius.md,
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
gap: theme.spacing[1],
|
|
||||||
},
|
|
||||||
chipContent: {
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingBottom: theme.spacing[1],
|
||||||
|
},
|
||||||
|
questionText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
optionsWrap: {
|
||||||
gap: theme.spacing[1],
|
gap: theme.spacing[1],
|
||||||
},
|
},
|
||||||
chipLabel: {
|
optionItem: {
|
||||||
fontSize: theme.fontSize.sm,
|
flexDirection: "row",
|
||||||
fontWeight: theme.fontWeight.medium,
|
alignItems: "center",
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
|
borderRadius: theme.borderRadius.md,
|
||||||
},
|
},
|
||||||
chipDescription: {
|
optionItemPressed: {
|
||||||
|
opacity: 0.9,
|
||||||
|
},
|
||||||
|
optionItemContent: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
optionTextBlock: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 2,
|
||||||
|
},
|
||||||
|
optionLabel: {
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
},
|
||||||
|
optionDescription: {
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
lineHeight: 16,
|
lineHeight: 16,
|
||||||
},
|
},
|
||||||
|
optionCheckSlot: {
|
||||||
|
width: 16,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginLeft: "auto",
|
||||||
|
},
|
||||||
otherInput: {
|
otherInput: {
|
||||||
borderWidth: theme.borderWidth[1],
|
borderWidth: 1,
|
||||||
borderRadius: theme.borderRadius.md,
|
borderRadius: theme.borderRadius.lg,
|
||||||
paddingVertical: theme.spacing[2],
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingVertical: theme.spacing[3],
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
actions: {
|
actionsContainer: {
|
||||||
flexDirection: "row",
|
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
marginTop: theme.spacing[1],
|
},
|
||||||
|
actionsContainerDesktop: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
actionButton: {
|
actionButton: {
|
||||||
flex: 1,
|
|
||||||
paddingVertical: theme.spacing[2],
|
paddingVertical: theme.spacing[2],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.md,
|
borderRadius: theme.borderRadius.md,
|
||||||
@@ -404,6 +447,5 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
actionText: {
|
actionText: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.normal,
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1171,10 +1171,33 @@ export class AgentManager {
|
|||||||
break;
|
break;
|
||||||
case "turn_failed":
|
case "turn_failed":
|
||||||
agent.lastError = event.error;
|
agent.lastError = event.error;
|
||||||
|
for (const [requestId] of agent.pendingPermissions) {
|
||||||
|
agent.pendingPermissions.delete(requestId);
|
||||||
|
if (!options?.fromHistory) {
|
||||||
|
this.dispatchStream(agent.id, {
|
||||||
|
type: "permission_resolved",
|
||||||
|
provider: event.provider,
|
||||||
|
requestId,
|
||||||
|
resolution: { behavior: "deny", message: "Turn failed" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emitState(agent);
|
||||||
break;
|
break;
|
||||||
case "turn_canceled":
|
case "turn_canceled":
|
||||||
// Cancellation is not an error, just clear any previous error
|
|
||||||
agent.lastError = undefined;
|
agent.lastError = undefined;
|
||||||
|
for (const [requestId] of agent.pendingPermissions) {
|
||||||
|
agent.pendingPermissions.delete(requestId);
|
||||||
|
if (!options?.fromHistory) {
|
||||||
|
this.dispatchStream(agent.id, {
|
||||||
|
type: "permission_resolved",
|
||||||
|
provider: event.provider,
|
||||||
|
requestId,
|
||||||
|
resolution: { behavior: "deny", message: "Interrupted" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emitState(agent);
|
||||||
break;
|
break;
|
||||||
case "permission_requested":
|
case "permission_requested":
|
||||||
agent.pendingPermissions.set(event.request.id, event.request);
|
agent.pendingPermissions.set(event.request.id, event.request);
|
||||||
|
|||||||
@@ -625,6 +625,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.pendingInterruptPromise = this.interruptActiveTurn().catch((error) => {
|
this.pendingInterruptPromise = this.interruptActiveTurn().catch((error) => {
|
||||||
this.logger.warn({ err: error }, "Failed to interrupt during cancel");
|
this.logger.warn({ err: error }, "Failed to interrupt during cancel");
|
||||||
});
|
});
|
||||||
|
this.flushPendingToolCalls();
|
||||||
// Push turn_canceled before ending the queue so consumers get proper lifecycle signals
|
// Push turn_canceled before ending the queue so consumers get proper lifecycle signals
|
||||||
queue.push({
|
queue.push({
|
||||||
type: "turn_canceled",
|
type: "turn_canceled",
|
||||||
@@ -1084,6 +1085,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
// Only emit if not already emitted by requestCancel() (indicated by turnCancelRequested).
|
// Only emit if not already emitted by requestCancel() (indicated by turnCancelRequested).
|
||||||
const wasSuperseded = this.currentTurnId !== turnId;
|
const wasSuperseded = this.currentTurnId !== turnId;
|
||||||
if (wasSuperseded && !completedNormally && !this.turnCancelRequested) {
|
if (wasSuperseded && !completedNormally && !this.turnCancelRequested) {
|
||||||
|
this.flushPendingToolCalls();
|
||||||
queue.push({
|
queue.push({
|
||||||
type: "turn_canceled",
|
type: "turn_canceled",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
@@ -1423,6 +1425,21 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.pushEvent({ type: "timeline", item, provider: "claude" });
|
this.pushEvent({ type: "timeline", item, provider: "claude" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private flushPendingToolCalls() {
|
||||||
|
for (const [id, entry] of this.toolUseCache) {
|
||||||
|
if (entry.started) {
|
||||||
|
this.pushToolCall({
|
||||||
|
name: entry.name,
|
||||||
|
status: "failed",
|
||||||
|
callId: id,
|
||||||
|
input: entry.input,
|
||||||
|
error: { message: "Interrupted" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.toolUseCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
private pushToolCall(
|
private pushToolCall(
|
||||||
data: Omit<ToolCallTimelineItem, "type">,
|
data: Omit<ToolCallTimelineItem, "type">,
|
||||||
target?: AgentTimelineItem[]
|
target?: AgentTimelineItem[]
|
||||||
|
|||||||
Reference in New Issue
Block a user