mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 02:32:47 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c28d917a4b | ||
|
|
2f6b8cea01 |
@@ -5,8 +5,8 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
|
||||||
useState,
|
useState,
|
||||||
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { View, Text, Pressable, Platform, ActivityIndicator } from "react-native";
|
import { View, Text, Pressable, Platform, ActivityIndicator } from "react-native";
|
||||||
import Markdown from "react-native-markdown-display";
|
import Markdown from "react-native-markdown-display";
|
||||||
@@ -42,6 +42,7 @@ import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-typ
|
|||||||
import type { Agent } from "@/contexts/session-context";
|
import type { Agent } from "@/contexts/session-context";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
|
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
|
||||||
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
import type { DaemonClient } from "@server/client/daemon-client";
|
import type { DaemonClient } from "@server/client/daemon-client";
|
||||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||||
import { QuestionFormCard } from "./question-form-card";
|
import { QuestionFormCard } from "./question-form-card";
|
||||||
@@ -85,6 +86,7 @@ export interface AgentStreamViewProps {
|
|||||||
agent: Agent;
|
agent: Agent;
|
||||||
streamItems: StreamItem[];
|
streamItems: StreamItem[];
|
||||||
pendingPermissions: Map<string, PendingPermission>;
|
pendingPermissions: Map<string, PendingPermission>;
|
||||||
|
isPaneFocused?: boolean;
|
||||||
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
||||||
isAuthoritativeHistoryReady?: boolean;
|
isAuthoritativeHistoryReady?: boolean;
|
||||||
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
||||||
@@ -98,6 +100,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
agent,
|
agent,
|
||||||
streamItems,
|
streamItems,
|
||||||
pendingPermissions,
|
pendingPermissions,
|
||||||
|
isPaneFocused = false,
|
||||||
routeBottomAnchorRequest = null,
|
routeBottomAnchorRequest = null,
|
||||||
isAuthoritativeHistoryReady = true,
|
isAuthoritativeHistoryReady = true,
|
||||||
onOpenWorkspaceFile,
|
onOpenWorkspaceFile,
|
||||||
@@ -208,6 +211,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
const pendingPermissionItems = useMemo(
|
||||||
|
() => Array.from(pendingPermissions.values()).filter((perm) => perm.agentId === agentId),
|
||||||
|
[pendingPermissions, agentId],
|
||||||
|
);
|
||||||
|
|
||||||
const baseRenderModel = useMemo(() => {
|
const baseRenderModel = useMemo(() => {
|
||||||
return buildAgentStreamRenderModel({
|
return buildAgentStreamRenderModel({
|
||||||
@@ -490,18 +497,18 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
[getGapBetween, renderStreamItemContent, agent.status, streamRenderStrategy],
|
[getGapBetween, renderStreamItemContent, agent.status, streamRenderStrategy],
|
||||||
);
|
);
|
||||||
|
|
||||||
const pendingPermissionItems = useMemo(
|
|
||||||
() => Array.from(pendingPermissions.values()).filter((perm) => perm.agentId === agentId),
|
|
||||||
[pendingPermissions, agentId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const showWorkingIndicator = agent.status === "running";
|
const showWorkingIndicator = agent.status === "running";
|
||||||
const renderModel = useMemo<AgentStreamRenderModel>(() => {
|
const renderModel = useMemo<AgentStreamRenderModel>(() => {
|
||||||
const pendingPermissionsNode =
|
const pendingPermissionsNode =
|
||||||
pendingPermissionItems.length > 0 ? (
|
pendingPermissionItems.length > 0 ? (
|
||||||
<View style={stylesheet.permissionsContainer}>
|
<View style={stylesheet.permissionsContainer}>
|
||||||
{pendingPermissionItems.map((permission) => (
|
{pendingPermissionItems.map((permission, index) => (
|
||||||
<PermissionRequestCard key={permission.key} permission={permission} client={client} />
|
<PermissionRequestCard
|
||||||
|
key={permission.key}
|
||||||
|
permission={permission}
|
||||||
|
client={client}
|
||||||
|
shortcutActive={isPaneFocused && index === 0}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -525,7 +532,14 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
workingIndicator: workingIndicatorNode,
|
workingIndicator: workingIndicatorNode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [baseRenderModel, client, getGapBetween, pendingPermissionItems, showWorkingIndicator]);
|
}, [
|
||||||
|
baseRenderModel,
|
||||||
|
client,
|
||||||
|
getGapBetween,
|
||||||
|
isPaneFocused,
|
||||||
|
pendingPermissionItems,
|
||||||
|
showWorkingIndicator,
|
||||||
|
]);
|
||||||
|
|
||||||
const listEmptyComponent = useMemo(() => {
|
const listEmptyComponent = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
@@ -727,9 +741,11 @@ function WorkingIndicator() {
|
|||||||
function PermissionRequestCard({
|
function PermissionRequestCard({
|
||||||
permission,
|
permission,
|
||||||
client,
|
client,
|
||||||
|
shortcutActive,
|
||||||
}: {
|
}: {
|
||||||
permission: PendingPermission;
|
permission: PendingPermission;
|
||||||
client: DaemonClient | null;
|
client: DaemonClient | null;
|
||||||
|
shortcutActive: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
@@ -740,9 +756,6 @@ function PermissionRequestCard({
|
|||||||
const description = request.description ?? "";
|
const description = request.description ?? "";
|
||||||
|
|
||||||
const planMarkdown = useMemo(() => {
|
const planMarkdown = useMemo(() => {
|
||||||
if (!request) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const planFromMetadata =
|
const planFromMetadata =
|
||||||
typeof request.metadata?.planText === "string" ? request.metadata.planText : undefined;
|
typeof request.metadata?.planText === "string" ? request.metadata.planText : undefined;
|
||||||
if (planFromMetadata) {
|
if (planFromMetadata) {
|
||||||
@@ -868,6 +881,7 @@ function PermissionRequestCard({
|
|||||||
resetPermissionMutation();
|
resetPermissionMutation();
|
||||||
setRespondingAction(null);
|
setRespondingAction(null);
|
||||||
}, [permission.request.id, resetPermissionMutation]);
|
}, [permission.request.id, resetPermissionMutation]);
|
||||||
|
|
||||||
const handleResponse = useCallback(
|
const handleResponse = useCallback(
|
||||||
(response: AgentPermissionResponse) => {
|
(response: AgentPermissionResponse) => {
|
||||||
respondToPermission({
|
respondToPermission({
|
||||||
@@ -881,12 +895,52 @@ function PermissionRequestCard({
|
|||||||
[permission.agentId, permission.request.id, respondToPermission],
|
[permission.agentId, permission.request.id, respondToPermission],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDeny = useCallback(() => {
|
||||||
|
setRespondingAction("deny");
|
||||||
|
handleResponse({
|
||||||
|
behavior: "deny",
|
||||||
|
message: "Denied by user",
|
||||||
|
});
|
||||||
|
}, [handleResponse]);
|
||||||
|
|
||||||
|
const handleAccept = useCallback(() => {
|
||||||
|
setRespondingAction("accept");
|
||||||
|
handleResponse({ behavior: "allow" });
|
||||||
|
}, [handleResponse]);
|
||||||
|
|
||||||
|
const handlePromptSelection = useCallback(
|
||||||
|
(action: { id: string; index?: number }): boolean => {
|
||||||
|
if (action.id !== "agent.prompt.select" || request.kind === "question" || isResponding) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (action.index === 1) {
|
||||||
|
handleDeny();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (action.index === 2) {
|
||||||
|
handleAccept();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[handleAccept, handleDeny, isResponding, request.kind],
|
||||||
|
);
|
||||||
|
|
||||||
|
useKeyboardActionHandler({
|
||||||
|
handlerId: `agent-prompt-permission:${permission.key}`,
|
||||||
|
actions: ["agent.prompt.select"],
|
||||||
|
enabled: shortcutActive && request.kind !== "question" && !isResponding,
|
||||||
|
priority: 150,
|
||||||
|
handle: handlePromptSelection,
|
||||||
|
});
|
||||||
|
|
||||||
if (request.kind === "question") {
|
if (request.kind === "question") {
|
||||||
return (
|
return (
|
||||||
<QuestionFormCard
|
<QuestionFormCard
|
||||||
permission={permission}
|
permission={permission}
|
||||||
onRespond={handleResponse}
|
onRespond={handleResponse}
|
||||||
isResponding={isResponding}
|
isResponding={isResponding}
|
||||||
|
shortcutActive={shortcutActive}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -958,13 +1012,7 @@ function PermissionRequestCard({
|
|||||||
},
|
},
|
||||||
pressed ? permissionStyles.optionButtonPressed : null,
|
pressed ? permissionStyles.optionButtonPressed : null,
|
||||||
]}
|
]}
|
||||||
onPress={() => {
|
onPress={handleDeny}
|
||||||
setRespondingAction("deny");
|
|
||||||
handleResponse({
|
|
||||||
behavior: "deny",
|
|
||||||
message: "Denied by user",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
>
|
>
|
||||||
{respondingAction === "deny" ? (
|
{respondingAction === "deny" ? (
|
||||||
@@ -989,10 +1037,7 @@ function PermissionRequestCard({
|
|||||||
},
|
},
|
||||||
pressed ? permissionStyles.optionButtonPressed : null,
|
pressed ? permissionStyles.optionButtonPressed : null,
|
||||||
]}
|
]}
|
||||||
onPress={() => {
|
onPress={handleAccept}
|
||||||
setRespondingAction("accept");
|
|
||||||
handleResponse({ behavior: "allow" });
|
|
||||||
}}
|
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
>
|
>
|
||||||
{respondingAction === "accept" ? (
|
{respondingAction === "accept" ? (
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { View, Text, TextInput, Pressable, ActivityIndicator, Platform } from "react-native";
|
import { View, Text, TextInput, Pressable, ActivityIndicator, Platform } from "react-native";
|
||||||
import { StyleSheet, useUnistyles, UnistylesRuntime } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles, UnistylesRuntime } from "react-native-unistyles";
|
||||||
import { Check, CircleHelp, X } from "lucide-react-native";
|
import { Check, CircleHelp, X } from "lucide-react-native";
|
||||||
import type { PendingPermission } from "@/types/shared";
|
import type { PendingPermission } from "@/types/shared";
|
||||||
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
||||||
|
|
||||||
interface QuestionOption {
|
interface QuestionOption {
|
||||||
@@ -17,6 +18,9 @@ interface Question {
|
|||||||
multiSelect: boolean;
|
multiSelect: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuestionSelections = Record<number, Set<number>>;
|
||||||
|
type QuestionOtherTexts = Record<number, string>;
|
||||||
|
|
||||||
function parseQuestions(input: unknown): Question[] | null {
|
function parseQuestions(input: unknown): Question[] | null {
|
||||||
if (
|
if (
|
||||||
typeof input !== "object" ||
|
typeof input !== "object" ||
|
||||||
@@ -26,128 +30,334 @@ function parseQuestions(input: unknown): Question[] | null {
|
|||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = (input as Record<string, unknown>).questions as unknown[];
|
const raw = (input as Record<string, unknown>).questions as unknown[];
|
||||||
const questions: Question[] = [];
|
const questions: Question[] = [];
|
||||||
|
|
||||||
for (const item of raw) {
|
for (const item of raw) {
|
||||||
if (typeof item !== "object" || item === null) return null;
|
if (typeof item !== "object" || item === null) {
|
||||||
const q = item as Record<string, unknown>;
|
return null;
|
||||||
if (typeof q.question !== "string" || typeof q.header !== "string") return null;
|
}
|
||||||
if (!Array.isArray(q.options)) return null;
|
|
||||||
|
const question = item as Record<string, unknown>;
|
||||||
|
if (typeof question.question !== "string" || typeof question.header !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(question.options)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const options: QuestionOption[] = [];
|
const options: QuestionOption[] = [];
|
||||||
for (const opt of q.options as unknown[]) {
|
for (const option of question.options as unknown[]) {
|
||||||
if (typeof opt !== "object" || opt === null) return null;
|
if (typeof option !== "object" || option === null) {
|
||||||
const o = opt as Record<string, unknown>;
|
return null;
|
||||||
if (typeof o.label !== "string") return null;
|
}
|
||||||
|
|
||||||
|
const candidate = option as Record<string, unknown>;
|
||||||
|
if (typeof candidate.label !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
label: o.label,
|
label: candidate.label,
|
||||||
description: typeof o.description === "string" ? o.description : undefined,
|
description: typeof candidate.description === "string" ? candidate.description : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
questions.push({
|
questions.push({
|
||||||
question: q.question,
|
question: question.question,
|
||||||
header: q.header,
|
header: question.header,
|
||||||
options,
|
options,
|
||||||
multiSelect: q.multiSelect === true,
|
multiSelect: question.multiSelect === true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return questions.length > 0 ? questions : null;
|
return questions.length > 0 ? questions : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneSelections(input: QuestionSelections): QuestionSelections {
|
||||||
|
const next: QuestionSelections = {};
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
next[Number(key)] = new Set(value);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQuestionAnswered(input: {
|
||||||
|
selections: QuestionSelections;
|
||||||
|
otherTexts: QuestionOtherTexts;
|
||||||
|
questionIndex: number;
|
||||||
|
}): boolean {
|
||||||
|
const selected = input.selections[input.questionIndex];
|
||||||
|
const otherText = input.otherTexts[input.questionIndex]?.trim();
|
||||||
|
return (selected && selected.size > 0) || Boolean(otherText && otherText.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFirstUnansweredQuestionIndex(input: {
|
||||||
|
questions: readonly Question[];
|
||||||
|
selections: QuestionSelections;
|
||||||
|
otherTexts: QuestionOtherTexts;
|
||||||
|
}): number | null {
|
||||||
|
for (let questionIndex = 0; questionIndex < input.questions.length; questionIndex += 1) {
|
||||||
|
if (
|
||||||
|
!isQuestionAnswered({
|
||||||
|
selections: input.selections,
|
||||||
|
otherTexts: input.otherTexts,
|
||||||
|
questionIndex,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return questionIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAllQuestionsAnswered(input: {
|
||||||
|
questions: readonly Question[];
|
||||||
|
selections: QuestionSelections;
|
||||||
|
otherTexts: QuestionOtherTexts;
|
||||||
|
}): boolean {
|
||||||
|
return input.questions.every((_, questionIndex) =>
|
||||||
|
isQuestionAnswered({
|
||||||
|
selections: input.selections,
|
||||||
|
otherTexts: input.otherTexts,
|
||||||
|
questionIndex,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionAnswers(input: {
|
||||||
|
questions: readonly Question[];
|
||||||
|
selections: QuestionSelections;
|
||||||
|
otherTexts: QuestionOtherTexts;
|
||||||
|
}): Record<string, string> {
|
||||||
|
const answers: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (let questionIndex = 0; questionIndex < input.questions.length; questionIndex += 1) {
|
||||||
|
const question = input.questions[questionIndex];
|
||||||
|
if (!question) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = input.selections[questionIndex];
|
||||||
|
const otherText = input.otherTexts[questionIndex]?.trim();
|
||||||
|
|
||||||
|
if (otherText && otherText.length > 0) {
|
||||||
|
answers[question.header] = otherText;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected || selected.size === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = Array.from(selected)
|
||||||
|
.map((optionIndex) => question.options[optionIndex]?.label)
|
||||||
|
.filter((label): label is string => typeof label === "string");
|
||||||
|
if (labels.length > 0) {
|
||||||
|
answers[question.header] = labels.join(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectQuestionOption(input: {
|
||||||
|
questions: readonly Question[];
|
||||||
|
selections: QuestionSelections;
|
||||||
|
otherTexts: QuestionOtherTexts;
|
||||||
|
questionIndex: number;
|
||||||
|
optionIndex: number;
|
||||||
|
}): {
|
||||||
|
selections: QuestionSelections;
|
||||||
|
otherTexts: QuestionOtherTexts;
|
||||||
|
} {
|
||||||
|
const question = input.questions[input.questionIndex];
|
||||||
|
if (!question || !question.options[input.optionIndex]) {
|
||||||
|
return {
|
||||||
|
selections: input.selections,
|
||||||
|
otherTexts: input.otherTexts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const selections = cloneSelections(input.selections);
|
||||||
|
const otherTexts = { ...input.otherTexts };
|
||||||
|
const current = selections[input.questionIndex] ?? new Set<number>();
|
||||||
|
const next = new Set(current);
|
||||||
|
|
||||||
|
if (question.multiSelect) {
|
||||||
|
if (next.has(input.optionIndex)) {
|
||||||
|
next.delete(input.optionIndex);
|
||||||
|
} else {
|
||||||
|
next.add(input.optionIndex);
|
||||||
|
}
|
||||||
|
} else if (next.has(input.optionIndex)) {
|
||||||
|
next.clear();
|
||||||
|
} else {
|
||||||
|
next.clear();
|
||||||
|
next.add(input.optionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
selections[input.questionIndex] = next;
|
||||||
|
delete otherTexts[input.questionIndex];
|
||||||
|
|
||||||
|
return { selections, otherTexts };
|
||||||
|
}
|
||||||
|
|
||||||
interface QuestionFormCardProps {
|
interface QuestionFormCardProps {
|
||||||
permission: PendingPermission;
|
permission: PendingPermission;
|
||||||
onRespond: (response: AgentPermissionResponse) => void;
|
onRespond: (response: AgentPermissionResponse) => void;
|
||||||
isResponding: boolean;
|
isResponding: boolean;
|
||||||
|
shortcutActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IS_WEB = Platform.OS === "web";
|
const IS_WEB = Platform.OS === "web";
|
||||||
|
|
||||||
export function QuestionFormCard({ permission, onRespond, isResponding }: QuestionFormCardProps) {
|
export function QuestionFormCard({
|
||||||
|
permission,
|
||||||
|
onRespond,
|
||||||
|
isResponding,
|
||||||
|
shortcutActive,
|
||||||
|
}: QuestionFormCardProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const questions = parseQuestions(permission.request.input);
|
const questions = parseQuestions(permission.request.input);
|
||||||
|
|
||||||
const [selections, setSelections] = useState<Record<number, Set<number>>>({});
|
const [selections, setSelections] = useState<QuestionSelections>({});
|
||||||
const [otherTexts, setOtherTexts] = useState<Record<number, string>>({});
|
const [otherTexts, setOtherTexts] = useState<QuestionOtherTexts>({});
|
||||||
const [respondingAction, setRespondingAction] = useState<"submit" | "dismiss" | null>(null);
|
const [respondingAction, setRespondingAction] = useState<"submit" | "dismiss" | null>(null);
|
||||||
|
|
||||||
const toggleOption = useCallback((qIndex: number, optIndex: number, multiSelect: boolean) => {
|
const submitResponses = useCallback(
|
||||||
setSelections((prev) => {
|
(input: { selections: QuestionSelections; otherTexts: QuestionOtherTexts }) => {
|
||||||
const current = prev[qIndex] ?? new Set<number>();
|
if (!questions) {
|
||||||
const next = new Set(current);
|
return;
|
||||||
if (multiSelect) {
|
|
||||||
if (next.has(optIndex)) {
|
|
||||||
next.delete(optIndex);
|
|
||||||
} else {
|
|
||||||
next.add(optIndex);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (next.has(optIndex)) {
|
|
||||||
next.clear();
|
|
||||||
} else {
|
|
||||||
next.clear();
|
|
||||||
next.add(optIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { ...prev, [qIndex]: next };
|
|
||||||
});
|
|
||||||
setOtherTexts((prev) => {
|
|
||||||
if (!prev[qIndex]) return prev;
|
|
||||||
const next = { ...prev };
|
|
||||||
delete next[qIndex];
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setOtherText = useCallback((qIndex: number, text: string) => {
|
setRespondingAction("submit");
|
||||||
setOtherTexts((prev) => ({ ...prev, [qIndex]: text }));
|
onRespond({
|
||||||
if (text.length > 0) {
|
behavior: "allow",
|
||||||
setSelections((prev) => {
|
updatedInput: {
|
||||||
if (!prev[qIndex] || prev[qIndex].size === 0) return prev;
|
...permission.request.input,
|
||||||
return { ...prev, [qIndex]: new Set<number>() };
|
answers: buildQuestionAnswers({
|
||||||
|
questions,
|
||||||
|
selections: input.selections,
|
||||||
|
otherTexts: input.otherTexts,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onRespond, permission.request.input, questions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleOption = useCallback(
|
||||||
|
(input: { questionIndex: number; optionIndex: number }) => {
|
||||||
|
if (!questions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = selectQuestionOption({
|
||||||
|
questions,
|
||||||
|
selections,
|
||||||
|
otherTexts,
|
||||||
|
questionIndex: input.questionIndex,
|
||||||
|
optionIndex: input.optionIndex,
|
||||||
|
});
|
||||||
|
setSelections(next.selections);
|
||||||
|
setOtherTexts(next.otherTexts);
|
||||||
|
},
|
||||||
|
[otherTexts, questions, selections],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setOtherText = useCallback((input: { questionIndex: number; text: string }) => {
|
||||||
|
setOtherTexts((previous) => ({ ...previous, [input.questionIndex]: input.text }));
|
||||||
|
if (input.text.length > 0) {
|
||||||
|
setSelections((previous) => {
|
||||||
|
if (!previous[input.questionIndex] || previous[input.questionIndex]?.size === 0) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
return { ...previous, [input.questionIndex]: new Set<number>() };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!questions) {
|
const handleSubmit = useCallback(() => {
|
||||||
return null;
|
submitResponses({ selections, otherTexts });
|
||||||
}
|
}, [otherTexts, selections, submitResponses]);
|
||||||
|
|
||||||
const allAnswered = questions.every((_, qIndex) => {
|
const handleDeny = useCallback(() => {
|
||||||
const selected = selections[qIndex];
|
|
||||||
const otherText = otherTexts[qIndex]?.trim();
|
|
||||||
return (selected && selected.size > 0) || (otherText && otherText.length > 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
setRespondingAction("submit");
|
|
||||||
const answers: Record<string, string> = {};
|
|
||||||
for (let i = 0; i < questions!.length; i++) {
|
|
||||||
const q = questions![i];
|
|
||||||
const selected = selections[i];
|
|
||||||
const otherText = otherTexts[i]?.trim();
|
|
||||||
|
|
||||||
if (otherText && otherText.length > 0) {
|
|
||||||
answers[q.header] = otherText;
|
|
||||||
} else if (selected && selected.size > 0) {
|
|
||||||
const labels = Array.from(selected).map((idx) => q.options[idx].label);
|
|
||||||
answers[q.header] = labels.join(", ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRespond({
|
|
||||||
behavior: "allow",
|
|
||||||
updatedInput: { ...permission.request.input, answers },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeny() {
|
|
||||||
setRespondingAction("dismiss");
|
setRespondingAction("dismiss");
|
||||||
onRespond({
|
onRespond({
|
||||||
behavior: "deny",
|
behavior: "deny",
|
||||||
message: "Dismissed by user",
|
message: "Dismissed by user",
|
||||||
});
|
});
|
||||||
|
}, [onRespond]);
|
||||||
|
|
||||||
|
const handlePromptSelection = useCallback(
|
||||||
|
(action: { id: string; index?: number }): boolean => {
|
||||||
|
if (action.id !== "agent.prompt.select" || isResponding || !questions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionIndex = findFirstUnansweredQuestionIndex({
|
||||||
|
questions,
|
||||||
|
selections,
|
||||||
|
otherTexts,
|
||||||
|
});
|
||||||
|
if (questionIndex === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionIndex = (action.index ?? 0) - 1;
|
||||||
|
const question = questions[questionIndex];
|
||||||
|
if (!question?.options[optionIndex]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = selectQuestionOption({
|
||||||
|
questions,
|
||||||
|
selections,
|
||||||
|
otherTexts,
|
||||||
|
questionIndex,
|
||||||
|
optionIndex,
|
||||||
|
});
|
||||||
|
setSelections(next.selections);
|
||||||
|
setOtherTexts(next.otherTexts);
|
||||||
|
|
||||||
|
const shouldAutoSubmit =
|
||||||
|
areAllQuestionsAnswered({
|
||||||
|
questions,
|
||||||
|
selections: next.selections,
|
||||||
|
otherTexts: next.otherTexts,
|
||||||
|
}) && questions.every((candidate) => candidate.multiSelect !== true);
|
||||||
|
|
||||||
|
if (shouldAutoSubmit) {
|
||||||
|
submitResponses(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[isResponding, otherTexts, questions, selections, submitResponses],
|
||||||
|
);
|
||||||
|
|
||||||
|
useKeyboardActionHandler({
|
||||||
|
handlerId: `agent-prompt-question:${permission.key}`,
|
||||||
|
actions: ["agent.prompt.select"],
|
||||||
|
enabled: shortcutActive && !isResponding && questions !== null,
|
||||||
|
priority: 150,
|
||||||
|
handle: handlePromptSelection,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!questions) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allAnswered = areAllQuestionsAnswered({
|
||||||
|
questions,
|
||||||
|
selections,
|
||||||
|
otherTexts,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -158,24 +368,24 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{questions.map((q, qIndex) => {
|
{questions.map((question, questionIndex) => {
|
||||||
const selected = selections[qIndex] ?? new Set<number>();
|
const selected = selections[questionIndex] ?? new Set<number>();
|
||||||
const otherText = otherTexts[qIndex] ?? "";
|
const otherText = otherTexts[questionIndex] ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={qIndex} style={styles.questionBlock}>
|
<View key={questionIndex} style={styles.questionBlock}>
|
||||||
<View style={styles.questionHeader}>
|
<View style={styles.questionHeader}>
|
||||||
<Text style={[styles.questionText, { color: theme.colors.foreground }]}>
|
<Text style={[styles.questionText, { color: theme.colors.foreground }]}>
|
||||||
{q.question}
|
{question.question}
|
||||||
</Text>
|
</Text>
|
||||||
<CircleHelp size={14} color={theme.colors.foregroundMuted} />
|
<CircleHelp size={14} color={theme.colors.foregroundMuted} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.optionsWrap}>
|
<View style={styles.optionsWrap}>
|
||||||
{q.options.map((opt, optIndex) => {
|
{question.options.map((option, optionIndex) => {
|
||||||
const isSelected = selected.has(optIndex);
|
const isSelected = selected.has(optionIndex);
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={optIndex}
|
key={optionIndex}
|
||||||
style={({ pressed, hovered = false }) => [
|
style={({ pressed, hovered = false }) => [
|
||||||
styles.optionItem,
|
styles.optionItem,
|
||||||
(hovered || isSelected) && {
|
(hovered || isSelected) && {
|
||||||
@@ -183,22 +393,27 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
|
|||||||
},
|
},
|
||||||
pressed && styles.optionItemPressed,
|
pressed && styles.optionItemPressed,
|
||||||
]}
|
]}
|
||||||
onPress={() => toggleOption(qIndex, optIndex, q.multiSelect)}
|
onPress={() =>
|
||||||
|
toggleOption({
|
||||||
|
questionIndex,
|
||||||
|
optionIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
>
|
>
|
||||||
<View style={styles.optionItemContent}>
|
<View style={styles.optionItemContent}>
|
||||||
<View style={styles.optionTextBlock}>
|
<View style={styles.optionTextBlock}>
|
||||||
<Text style={[styles.optionLabel, { color: theme.colors.foreground }]}>
|
<Text style={[styles.optionLabel, { color: theme.colors.foreground }]}>
|
||||||
{opt.label}
|
{option.label}
|
||||||
</Text>
|
</Text>
|
||||||
{opt.description ? (
|
{option.description ? (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.optionDescription,
|
styles.optionDescription,
|
||||||
{ color: theme.colors.foregroundMuted },
|
{ color: theme.colors.foregroundMuted },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{opt.description}
|
{option.description}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
@@ -222,12 +437,21 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
|
|||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
// @ts-expect-error - outlineStyle is web-only
|
// @ts-expect-error - outlineStyle is web-only
|
||||||
IS_WEB && { outlineStyle: "none", outlineWidth: 0, outlineColor: "transparent" },
|
IS_WEB && {
|
||||||
|
outlineStyle: "none",
|
||||||
|
outlineWidth: 0,
|
||||||
|
outlineColor: "transparent",
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
placeholder="Other..."
|
placeholder="Other..."
|
||||||
placeholderTextColor={theme.colors.foregroundMuted}
|
placeholderTextColor={theme.colors.foregroundMuted}
|
||||||
value={otherText}
|
value={otherText}
|
||||||
onChangeText={(text) => setOtherText(qIndex, text)}
|
onChangeText={(text) =>
|
||||||
|
setOtherText({
|
||||||
|
questionIndex,
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
}
|
||||||
editable={!isResponding}
|
editable={!isResponding}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -161,6 +161,15 @@ export function useKeyboardShortcuts({
|
|||||||
switch (input.action) {
|
switch (input.action) {
|
||||||
case "agent.new":
|
case "agent.new":
|
||||||
return openProjectPicker();
|
return openProjectPicker();
|
||||||
|
case "agent.prompt.select":
|
||||||
|
if (!input.payload || typeof input.payload !== "object" || !("index" in input.payload)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return keyboardActionDispatcher.dispatch({
|
||||||
|
id: "agent.prompt.select",
|
||||||
|
scope: "workspace",
|
||||||
|
index: input.payload.index,
|
||||||
|
});
|
||||||
case "workspace.tab.new":
|
case "workspace.tab.new":
|
||||||
return keyboardActionDispatcher.dispatch({
|
return keyboardActionDispatcher.dispatch({
|
||||||
id: "workspace.tab.new",
|
id: "workspace.tab.new",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type MessageInputKeyboardActionKind =
|
|||||||
|
|
||||||
export type KeyboardActionId =
|
export type KeyboardActionId =
|
||||||
| "agent.new"
|
| "agent.new"
|
||||||
|
| "agent.prompt.select"
|
||||||
| "workspace.tab.new"
|
| "workspace.tab.new"
|
||||||
| "workspace.tab.close.current"
|
| "workspace.tab.close.current"
|
||||||
| "workspace.tab.navigate.index"
|
| "workspace.tab.navigate.index"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type KeyboardActionScope = "global" | "message-input" | "sidebar" | "workspace";
|
export type KeyboardActionScope = "global" | "message-input" | "sidebar" | "workspace";
|
||||||
|
|
||||||
export type KeyboardActionId =
|
export type KeyboardActionId =
|
||||||
|
| "agent.prompt.select"
|
||||||
| "message-input.focus"
|
| "message-input.focus"
|
||||||
| "message-input.dictation-toggle"
|
| "message-input.dictation-toggle"
|
||||||
| "message-input.dictation-cancel"
|
| "message-input.dictation-cancel"
|
||||||
@@ -26,6 +27,7 @@ export type KeyboardActionId =
|
|||||||
| "worktree.archive";
|
| "worktree.archive";
|
||||||
|
|
||||||
export type KeyboardActionDefinition =
|
export type KeyboardActionDefinition =
|
||||||
|
| { id: "agent.prompt.select"; scope: KeyboardActionScope; index: number }
|
||||||
| { id: "message-input.focus"; scope: KeyboardActionScope }
|
| { id: "message-input.focus"; scope: KeyboardActionScope }
|
||||||
| { id: "message-input.dictation-toggle"; scope: KeyboardActionScope }
|
| { id: "message-input.dictation-toggle"; scope: KeyboardActionScope }
|
||||||
| { id: "message-input.dictation-cancel"; scope: KeyboardActionScope }
|
| { id: "message-input.dictation-cancel"; scope: KeyboardActionScope }
|
||||||
|
|||||||
@@ -261,9 +261,15 @@ describe("keyboard-shortcuts", () => {
|
|||||||
action: "sidebar.toggle.left",
|
action: "sidebar.toggle.left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "keeps Mod+. as sidebar toggle fallback",
|
name: "matches Mod+. to toggle both sidebars on non-mac",
|
||||||
event: { key: ".", code: "Period", ctrlKey: true },
|
event: { key: ".", code: "Period", ctrlKey: true },
|
||||||
context: { isMac: false },
|
context: { isMac: false },
|
||||||
|
action: "sidebar.toggle.both",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches Ctrl+B sidebar toggle on non-mac",
|
||||||
|
event: { key: "b", code: "KeyB", ctrlKey: true },
|
||||||
|
context: { isMac: false },
|
||||||
action: "sidebar.toggle.left",
|
action: "sidebar.toggle.left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -289,6 +295,13 @@ describe("keyboard-shortcuts", () => {
|
|||||||
preventDefault: false,
|
preventDefault: false,
|
||||||
stopPropagation: false,
|
stopPropagation: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "routes number keys to prompt selection outside editable scopes",
|
||||||
|
event: { key: "2", code: "Digit2" },
|
||||||
|
context: { focusScope: "other" },
|
||||||
|
action: "agent.prompt.select",
|
||||||
|
payload: { index: 2 },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(matchingCases)("$name", ({
|
it.each(matchingCases)("$name", ({
|
||||||
@@ -344,11 +357,6 @@ describe("keyboard-shortcuts", () => {
|
|||||||
event: { key: "k", code: "KeyK", ctrlKey: true },
|
event: { key: "k", code: "KeyK", ctrlKey: true },
|
||||||
context: { isMac: false, focusScope: "terminal" },
|
context: { isMac: false, focusScope: "terminal" },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "does not bind Ctrl+B on non-mac",
|
|
||||||
event: { key: "b", code: "KeyB", ctrlKey: true },
|
|
||||||
context: { isMac: false },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "does not route message-input actions when terminal is focused",
|
name: "does not route message-input actions when terminal is focused",
|
||||||
event: { key: "d", code: "KeyD", metaKey: true },
|
event: { key: "d", code: "KeyD", metaKey: true },
|
||||||
@@ -364,6 +372,11 @@ describe("keyboard-shortcuts", () => {
|
|||||||
event: { key: " ", code: "Space" },
|
event: { key: " ", code: "Space" },
|
||||||
context: { focusScope: "message-input" },
|
context: { focusScope: "message-input" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "does not route prompt selection while typing in the message input",
|
||||||
|
event: { key: "1", code: "Digit1" },
|
||||||
|
context: { focusScope: "message-input" },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(nonMatchingCases)("$name", ({ event, context }) => {
|
it.each(nonMatchingCases)("$name", ({ event, context }) => {
|
||||||
@@ -467,6 +480,9 @@ describe("keyboard-shortcut help sections", () => {
|
|||||||
"workspace-tab-close-current": ["meta", "W"],
|
"workspace-tab-close-current": ["meta", "W"],
|
||||||
"workspace-pane-split-right": ["mod", "\\"],
|
"workspace-pane-split-right": ["mod", "\\"],
|
||||||
"workspace-pane-close": ["mod", "shift", "W"],
|
"workspace-pane-close": ["mod", "shift", "W"],
|
||||||
|
"agent-prompt-select-1": ["1"],
|
||||||
|
"agent-prompt-select-2": ["2"],
|
||||||
|
"agent-prompt-select-3": ["3"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -477,10 +493,11 @@ describe("keyboard-shortcut help sections", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "uses mod+period as non-mac left sidebar shortcut",
|
name: "uses mod+b and mod+period for non-mac sidebar shortcuts",
|
||||||
context: { isMac: false, isDesktop: false },
|
context: { isMac: false, isDesktop: false },
|
||||||
expectedKeys: {
|
expectedKeys: {
|
||||||
"toggle-left-sidebar": ["mod", "."],
|
"toggle-left-sidebar": ["mod", "B"],
|
||||||
|
"toggle-both-sidebars": ["mod", "."],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -814,6 +814,48 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
keys: ["Esc"],
|
keys: ["Esc"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "agent-prompt-select-1",
|
||||||
|
action: "agent.prompt.select",
|
||||||
|
combo: "1",
|
||||||
|
repeat: false,
|
||||||
|
when: { focusScope: "other", commandCenter: false },
|
||||||
|
payload: { type: "index" },
|
||||||
|
help: {
|
||||||
|
id: "agent-prompt-select-1",
|
||||||
|
section: "agent-input",
|
||||||
|
label: "Select prompt option 1",
|
||||||
|
keys: ["1"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agent-prompt-select-2",
|
||||||
|
action: "agent.prompt.select",
|
||||||
|
combo: "2",
|
||||||
|
repeat: false,
|
||||||
|
when: { focusScope: "other", commandCenter: false },
|
||||||
|
payload: { type: "index" },
|
||||||
|
help: {
|
||||||
|
id: "agent-prompt-select-2",
|
||||||
|
section: "agent-input",
|
||||||
|
label: "Select prompt option 2",
|
||||||
|
keys: ["2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agent-prompt-select-3",
|
||||||
|
action: "agent.prompt.select",
|
||||||
|
combo: "3",
|
||||||
|
repeat: false,
|
||||||
|
when: { focusScope: "other", commandCenter: false },
|
||||||
|
payload: { type: "index" },
|
||||||
|
help: {
|
||||||
|
id: "agent-prompt-select-3",
|
||||||
|
section: "agent-input",
|
||||||
|
label: "Select prompt option 3",
|
||||||
|
keys: ["3"],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "message-input-send-enter",
|
id: "message-input-send-enter",
|
||||||
action: "message-input.action",
|
action: "message-input.action",
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ function AgentPanelBody({
|
|||||||
attentionReason: agent?.attentionReason,
|
attentionReason: agent?.attentionReason,
|
||||||
isScreenFocused: isPaneFocused,
|
isScreenFocused: isPaneFocused,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur;
|
clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur;
|
||||||
}, [attentionController.clearOnAgentBlur]);
|
}, [attentionController.clearOnAgentBlur]);
|
||||||
@@ -796,6 +797,7 @@ function AgentPanelBody({
|
|||||||
agent={effectiveAgent}
|
agent={effectiveAgent}
|
||||||
streamItems={shouldUseOptimisticStream ? mergedStreamItems : streamItems}
|
streamItems={shouldUseOptimisticStream ? mergedStreamItems : streamItems}
|
||||||
pendingPermissions={pendingPermissions}
|
pendingPermissions={pendingPermissions}
|
||||||
|
isPaneFocused={isPaneFocused}
|
||||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||||
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
|
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
|
||||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
|
|||||||
Reference in New Issue
Block a user