mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Update files
This commit is contained in:
@@ -49,6 +49,7 @@ import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import { parseToolCallDisplay } from "@/utils/tool-call-parsers";
|
||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||
import { QuestionFormCard } from "./question-form-card";
|
||||
import { ToolCallSheetProvider } from "./tool-call-sheet";
|
||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
@@ -1070,6 +1071,16 @@ function PermissionRequestCard({
|
||||
[permission.agentId, permission.request.id, respondToPermission]
|
||||
);
|
||||
|
||||
if (request.kind === "question") {
|
||||
return (
|
||||
<QuestionFormCard
|
||||
permission={permission}
|
||||
onRespond={handleResponse}
|
||||
isResponding={isResponding}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -147,8 +147,12 @@ export const UserMessage = memo(function UserMessage({
|
||||
isLastInGroup = true,
|
||||
disableOuterSpacing,
|
||||
}: UserMessageProps) {
|
||||
const [messageHovered, setMessageHovered] = useState(false);
|
||||
const [copyButtonHovered, setCopyButtonHovered] = useState(false);
|
||||
const resolvedDisableOuterSpacing =
|
||||
useDisableOuterSpacing(disableOuterSpacing);
|
||||
const showCopyButton =
|
||||
Platform.OS !== "web" || messageHovered || copyButtonHovered;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -163,29 +167,31 @@ export const UserMessage = memo(function UserMessage({
|
||||
],
|
||||
]}
|
||||
>
|
||||
<Pressable style={userMessageStylesheet.content}>
|
||||
{({ hovered }) => {
|
||||
const showCopyButton = Platform.OS !== "web" || hovered;
|
||||
return (
|
||||
<>
|
||||
<View style={userMessageStylesheet.bubble}>
|
||||
<Text selectable style={userMessageStylesheet.text}>
|
||||
{message}
|
||||
</Text>
|
||||
</View>
|
||||
<TurnCopyButton
|
||||
getContent={() => message}
|
||||
containerStyle={[
|
||||
userMessageStylesheet.copyButton,
|
||||
showCopyButton
|
||||
? userMessageStylesheet.copyButtonVisible
|
||||
: userMessageStylesheet.copyButtonHidden,
|
||||
]}
|
||||
accessibilityLabel="Copy message"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
<Pressable
|
||||
style={userMessageStylesheet.content}
|
||||
onHoverIn={
|
||||
Platform.OS === "web" ? () => setMessageHovered(true) : undefined
|
||||
}
|
||||
onHoverOut={
|
||||
Platform.OS === "web" ? () => setMessageHovered(false) : undefined
|
||||
}
|
||||
>
|
||||
<View style={userMessageStylesheet.bubble}>
|
||||
<Text selectable style={userMessageStylesheet.text}>
|
||||
{message}
|
||||
</Text>
|
||||
</View>
|
||||
<TurnCopyButton
|
||||
getContent={() => message}
|
||||
containerStyle={[
|
||||
userMessageStylesheet.copyButton,
|
||||
showCopyButton
|
||||
? userMessageStylesheet.copyButtonVisible
|
||||
: userMessageStylesheet.copyButtonHidden,
|
||||
]}
|
||||
accessibilityLabel="Copy message"
|
||||
onHoverChange={setCopyButtonHovered}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -251,6 +257,7 @@ interface TurnCopyButtonProps {
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
accessibilityLabel?: string;
|
||||
copiedAccessibilityLabel?: string;
|
||||
onHoverChange?: (hovered: boolean) => void;
|
||||
}
|
||||
|
||||
export const TurnCopyButton = memo(function TurnCopyButton({
|
||||
@@ -258,6 +265,7 @@ export const TurnCopyButton = memo(function TurnCopyButton({
|
||||
containerStyle,
|
||||
accessibilityLabel,
|
||||
copiedAccessibilityLabel,
|
||||
onHoverChange,
|
||||
}: TurnCopyButtonProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -292,6 +300,8 @@ export const TurnCopyButton = memo(function TurnCopyButton({
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handleCopy}
|
||||
onHoverIn={Platform.OS === "web" ? () => onHoverChange?.(true) : undefined}
|
||||
onHoverOut={Platform.OS === "web" ? () => onHoverChange?.(false) : undefined}
|
||||
style={[turnCopyButtonStylesheet.container, containerStyle]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
|
||||
409
packages/app/src/components/question-form-card.tsx
Normal file
409
packages/app/src/components/question-form-card.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { View, Text, TextInput, Pressable, ActivityIndicator } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Check, X, Send } from "lucide-react-native";
|
||||
import type { PendingPermission } from "@/types/shared";
|
||||
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
|
||||
|
||||
interface QuestionOption {
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface Question {
|
||||
question: string;
|
||||
header: string;
|
||||
options: QuestionOption[];
|
||||
multiSelect: boolean;
|
||||
}
|
||||
|
||||
function parseQuestions(input: unknown): Question[] | null {
|
||||
if (
|
||||
typeof input !== "object" ||
|
||||
input === null ||
|
||||
!("questions" in input) ||
|
||||
!Array.isArray((input as Record<string, unknown>).questions)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const raw = (input as Record<string, unknown>).questions as unknown[];
|
||||
const questions: Question[] = [];
|
||||
for (const item of raw) {
|
||||
if (typeof item !== "object" || item === null) return null;
|
||||
const q = item as Record<string, unknown>;
|
||||
if (typeof q.question !== "string" || typeof q.header !== "string") return null;
|
||||
if (!Array.isArray(q.options)) return null;
|
||||
const options: QuestionOption[] = [];
|
||||
for (const opt of q.options as unknown[]) {
|
||||
if (typeof opt !== "object" || opt === null) return null;
|
||||
const o = opt as Record<string, unknown>;
|
||||
if (typeof o.label !== "string") return null;
|
||||
options.push({
|
||||
label: o.label,
|
||||
description: typeof o.description === "string" ? o.description : undefined,
|
||||
});
|
||||
}
|
||||
questions.push({
|
||||
question: q.question,
|
||||
header: q.header,
|
||||
options,
|
||||
multiSelect: q.multiSelect === true,
|
||||
});
|
||||
}
|
||||
return questions.length > 0 ? questions : null;
|
||||
}
|
||||
|
||||
interface QuestionFormCardProps {
|
||||
permission: PendingPermission;
|
||||
onRespond: (response: AgentPermissionResponse) => void;
|
||||
isResponding: boolean;
|
||||
}
|
||||
|
||||
export function QuestionFormCard({
|
||||
permission,
|
||||
onRespond,
|
||||
isResponding,
|
||||
}: QuestionFormCardProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const questions = parseQuestions(permission.request.input);
|
||||
|
||||
// selections[questionIndex] = Set of selected option indices
|
||||
const [selections, setSelections] = useState<Record<number, Set<number>>>({});
|
||||
// otherTexts[questionIndex] = custom "Other" text
|
||||
const [otherTexts, setOtherTexts] = useState<Record<number, string>>({});
|
||||
|
||||
const toggleOption = useCallback(
|
||||
(qIndex: number, optIndex: number, multiSelect: boolean) => {
|
||||
setSelections((prev) => {
|
||||
const current = prev[qIndex] ?? new Set<number>();
|
||||
const next = new Set(current);
|
||||
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 };
|
||||
});
|
||||
// Clear "Other" text when an option is selected
|
||||
setOtherTexts((prev) => {
|
||||
if (!prev[qIndex]) return prev;
|
||||
const next = { ...prev };
|
||||
delete next[qIndex];
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const setOtherText = useCallback((qIndex: number, text: string) => {
|
||||
setOtherTexts((prev) => ({ ...prev, [qIndex]: text }));
|
||||
// Clear option selections when typing "Other"
|
||||
if (text.length > 0) {
|
||||
setSelections((prev) => {
|
||||
if (!prev[qIndex] || prev[qIndex].size === 0) return prev;
|
||||
return { ...prev, [qIndex]: new Set<number>() };
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!questions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allAnswered = questions.every((_, qIndex) => {
|
||||
const selected = selections[qIndex];
|
||||
const otherText = otherTexts[qIndex]?.trim();
|
||||
return (selected && selected.size > 0) || (otherText && otherText.length > 0);
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
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() {
|
||||
onRespond({
|
||||
behavior: "deny",
|
||||
message: "Dismissed by user",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: theme.colors.surface2,
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{questions.map((q, qIndex) => {
|
||||
const selected = selections[qIndex] ?? new Set<number>();
|
||||
const otherText = otherTexts[qIndex] ?? "";
|
||||
|
||||
return (
|
||||
<View key={qIndex} style={styles.questionBlock}>
|
||||
<Text
|
||||
style={[styles.header, { color: theme.colors.foregroundMuted }]}
|
||||
>
|
||||
{q.header}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.questionText, { color: theme.colors.foreground }]}
|
||||
>
|
||||
{q.question}
|
||||
</Text>
|
||||
<View style={styles.optionsWrap}>
|
||||
{q.options.map((opt, optIndex) => {
|
||||
const isSelected = selected.has(optIndex);
|
||||
return (
|
||||
<Pressable
|
||||
key={optIndex}
|
||||
style={(state) => {
|
||||
const hovered = Boolean((state as any).hovered);
|
||||
return [
|
||||
styles.chip,
|
||||
{
|
||||
borderColor: isSelected
|
||||
? theme.colors.accent
|
||||
: theme.colors.border,
|
||||
backgroundColor: isSelected
|
||||
? `${theme.colors.accent}18`
|
||||
: hovered
|
||||
? theme.colors.surface1
|
||||
: theme.colors.surface2,
|
||||
},
|
||||
];
|
||||
}}
|
||||
onPress={() => toggleOption(qIndex, optIndex, q.multiSelect)}
|
||||
disabled={isResponding}
|
||||
>
|
||||
<View style={styles.chipContent}>
|
||||
{q.multiSelect && isSelected ? (
|
||||
<Check size={14} color={theme.colors.accent} />
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
styles.chipLabel,
|
||||
{
|
||||
color: isSelected
|
||||
? theme.colors.accent
|
||||
: theme.colors.foreground,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</View>
|
||||
{opt.description ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.chipDescription,
|
||||
{ color: theme.colors.foregroundMuted },
|
||||
]}
|
||||
>
|
||||
{opt.description}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.otherInput,
|
||||
{
|
||||
borderColor: otherText.length > 0
|
||||
? theme.colors.accent
|
||||
: theme.colors.border,
|
||||
color: theme.colors.foreground,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
]}
|
||||
placeholder="Other..."
|
||||
placeholderTextColor={theme.colors.foregroundMuted}
|
||||
value={otherText}
|
||||
onChangeText={(text) => setOtherText(qIndex, text)}
|
||||
editable={!isResponding}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable
|
||||
style={(state) => {
|
||||
const hovered = Boolean((state as any).hovered);
|
||||
return [
|
||||
styles.actionButton,
|
||||
{
|
||||
backgroundColor: hovered
|
||||
? theme.colors.surface1
|
||||
: theme.colors.surface2,
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
];
|
||||
}}
|
||||
onPress={handleDeny}
|
||||
disabled={isResponding}
|
||||
>
|
||||
{isResponding ? (
|
||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
||||
) : (
|
||||
<View style={styles.actionContent}>
|
||||
<X size={14} color={theme.colors.foregroundMuted} />
|
||||
<Text
|
||||
style={[
|
||||
styles.actionText,
|
||||
{ color: theme.colors.foregroundMuted },
|
||||
]}
|
||||
>
|
||||
Dismiss
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={(state) => {
|
||||
const hovered = Boolean((state as any).hovered);
|
||||
const disabled = !allAnswered || isResponding;
|
||||
return [
|
||||
styles.actionButton,
|
||||
{
|
||||
backgroundColor: hovered && !disabled
|
||||
? theme.colors.surface1
|
||||
: theme.colors.surface2,
|
||||
borderColor: disabled
|
||||
? theme.colors.border
|
||||
: theme.colors.accent,
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
},
|
||||
];
|
||||
}}
|
||||
onPress={handleSubmit}
|
||||
disabled={!allAnswered || isResponding}
|
||||
>
|
||||
{isResponding ? (
|
||||
<ActivityIndicator size="small" color={theme.colors.accent} />
|
||||
) : (
|
||||
<View style={styles.actionContent}>
|
||||
<Send size={14} color={allAnswered ? theme.colors.accent : theme.colors.foregroundMuted} />
|
||||
<Text
|
||||
style={[
|
||||
styles.actionText,
|
||||
{ color: allAnswered ? theme.colors.accent : theme.colors.foregroundMuted },
|
||||
]}
|
||||
>
|
||||
Submit
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
marginVertical: theme.spacing[3],
|
||||
padding: theme.spacing[3],
|
||||
borderRadius: theme.spacing[2],
|
||||
borderWidth: 1,
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
questionBlock: {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
header: {
|
||||
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",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
chipLabel: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
chipDescription: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
lineHeight: 16,
|
||||
},
|
||||
otherInput: {
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row",
|
||||
gap: theme.spacing[2],
|
||||
marginTop: theme.spacing[1],
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
alignItems: "center",
|
||||
borderWidth: theme.borderWidth[1],
|
||||
},
|
||||
actionContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
actionText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
}));
|
||||
@@ -165,7 +165,7 @@ export type AgentStreamEvent =
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export type AgentPermissionRequestKind = "tool" | "plan" | "mode" | "other";
|
||||
export type AgentPermissionRequestKind = "tool" | "plan" | "question" | "mode" | "other";
|
||||
|
||||
export type AgentPermissionUpdate = AgentMetadata;
|
||||
|
||||
|
||||
@@ -1247,6 +1247,36 @@ describe("convertClaudeHistoryEntry", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test("passes thinking blocks to mapBlocks for assistant entries", () => {
|
||||
const entry = {
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "Let me reason about this..." },
|
||||
{ type: "text", text: "Here is my answer." },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const mapBlocks = vi.fn().mockReturnValue([
|
||||
{ type: "reasoning", text: "Let me reason about this..." },
|
||||
{ type: "assistant_message", text: "Here is my answer." },
|
||||
]);
|
||||
const result = convertClaudeHistoryEntry(entry, mapBlocks);
|
||||
|
||||
expect(mapBlocks).toHaveBeenCalledTimes(1);
|
||||
const arg = mapBlocks.mock.calls[0][0];
|
||||
expect(arg).toEqual([
|
||||
{ type: "thinking", thinking: "Let me reason about this..." },
|
||||
{ type: "text", text: "Here is my answer." },
|
||||
]);
|
||||
expect(result).toEqual([
|
||||
{ type: "reasoning", text: "Let me reason about this..." },
|
||||
{ type: "assistant_message", text: "Here is my answer." },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
type StreamHydrationUpdate = {
|
||||
|
||||
@@ -31,6 +31,7 @@ import type {
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionRequestKind,
|
||||
AgentPermissionResponse,
|
||||
AgentPermissionUpdate,
|
||||
AgentPersistenceHandle,
|
||||
@@ -328,6 +329,17 @@ function isPermissionUpdate(value: AgentPermissionUpdate): value is PermissionUp
|
||||
return Array.isArray(rules) && typeof behavior === "string" && typeof destination === "string";
|
||||
}
|
||||
|
||||
function resolvePermissionKind(
|
||||
toolName: string,
|
||||
input: Record<string, unknown>
|
||||
): AgentPermissionRequestKind {
|
||||
if (toolName === "ExitPlanMode") return "plan";
|
||||
if (toolName === "AskUserQuestion" && Array.isArray(input.questions)) {
|
||||
return "question";
|
||||
}
|
||||
return "tool";
|
||||
}
|
||||
|
||||
export class ClaudeAgentClient implements AgentClient {
|
||||
readonly provider: "claude" = "claude";
|
||||
readonly capabilities = CLAUDE_CAPABILITIES;
|
||||
@@ -483,6 +495,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
private lastOptionsModel: string | null = null;
|
||||
private activeSidechains = new Map<string, string>();
|
||||
private compacting = false;
|
||||
private queryRestartNeeded = false;
|
||||
|
||||
constructor(
|
||||
config: ClaudeAgentConfig,
|
||||
@@ -722,37 +735,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
? thinkingOptionId
|
||||
: null;
|
||||
|
||||
const query = await this.ensureQuery();
|
||||
|
||||
if (!normalizedThinkingOptionId || normalizedThinkingOptionId === "default") {
|
||||
// Claude Code TUI exposes only ON/OFF. Default to OFF.
|
||||
try {
|
||||
await query.setMaxThinkingTokens(0);
|
||||
} catch {
|
||||
await query.setMaxThinkingTokens(1);
|
||||
}
|
||||
this.config.thinkingOptionId = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedThinkingOptionId === "on") {
|
||||
await query.setMaxThinkingTokens(null);
|
||||
this.config.thinkingOptionId = normalizedThinkingOptionId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedThinkingOptionId === "off") {
|
||||
try {
|
||||
await query.setMaxThinkingTokens(0);
|
||||
} catch {
|
||||
// Some runtimes may reject 0; use a tiny cap as "off".
|
||||
await query.setMaxThinkingTokens(1);
|
||||
}
|
||||
} else if (normalizedThinkingOptionId === "on") {
|
||||
this.config.thinkingOptionId = "on";
|
||||
} else if (normalizedThinkingOptionId === "off") {
|
||||
this.config.thinkingOptionId = "off";
|
||||
return;
|
||||
} else {
|
||||
throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`);
|
||||
this.queryRestartNeeded = true;
|
||||
}
|
||||
|
||||
getPendingPermissions(): AgentPermissionRequest[] {
|
||||
@@ -874,10 +866,18 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
private async ensureQuery(): Promise<Query> {
|
||||
if (this.query) {
|
||||
if (this.query && !this.queryRestartNeeded) {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
if (this.queryRestartNeeded && this.query) {
|
||||
this.input?.end();
|
||||
try { await this.query.return?.(); } catch { /* ignore */ }
|
||||
this.query = null;
|
||||
this.input = null;
|
||||
this.queryRestartNeeded = false;
|
||||
}
|
||||
|
||||
const input = new Pushable<SDKUserMessage>();
|
||||
const options = this.buildOptions();
|
||||
this.logger.debug({ options }, "claude query");
|
||||
@@ -894,11 +894,10 @@ class ClaudeAgentSession implements AgentSession {
|
||||
? configuredThinkingOptionId
|
||||
: "off";
|
||||
let maxThinkingTokens: number | undefined;
|
||||
if (typeof thinkingOptionId === "string" && thinkingOptionId.length > 0) {
|
||||
if (thinkingOptionId === "off") {
|
||||
maxThinkingTokens = 0;
|
||||
}
|
||||
// For "on" we omit maxThinkingTokens (SDK default max).
|
||||
if (thinkingOptionId === "on") {
|
||||
maxThinkingTokens = 10000;
|
||||
} else if (thinkingOptionId === "off") {
|
||||
maxThinkingTokens = 0;
|
||||
}
|
||||
|
||||
const appendedSystemPrompt = [
|
||||
@@ -1117,6 +1116,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.logger.info({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned");
|
||||
this.query = null;
|
||||
this.input = null;
|
||||
this.queryRestartNeeded = false;
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error }, "Failed to interrupt active turn");
|
||||
}
|
||||
@@ -1361,7 +1361,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
id: requestId,
|
||||
provider: "claude",
|
||||
name: toolName,
|
||||
kind: toolName === "ExitPlanMode" ? "plan" : "tool",
|
||||
kind: resolvePermissionKind(toolName, input),
|
||||
input,
|
||||
suggestions: options.suggestions?.map((suggestion) => ({ ...suggestion })),
|
||||
metadata: Object.keys(metadata).length ? metadata : undefined,
|
||||
|
||||
@@ -130,7 +130,7 @@ export const AgentPermissionRequestPayloadSchema: z.ZodType<AgentPermissionReque
|
||||
id: z.string(),
|
||||
provider: AgentProviderSchema,
|
||||
name: z.string(),
|
||||
kind: z.enum(["tool", "plan", "mode", "other"]),
|
||||
kind: z.enum(["tool", "plan", "question", "mode", "other"]),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
input: z.record(z.unknown()).optional(),
|
||||
|
||||
Reference in New Issue
Block a user