Reduce unnecessary re-renders in agent panel and input components
Narrow zustand selectors to only subscribe to fields each component actually uses. Split nested objects (availableModes, projectPlacement) into separate selectors with structural equality checks. Remove the updatedAt processing-spinner hack in agent-input-area — the server already guarantees agent status is "running" before ACKing the send request, so isProcessing can clear on submit success directly.
This commit is contained in:
@@ -46,9 +46,7 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for full setup, build sync requir
|
|||||||
- **NEVER add auth checks to tests** — agent providers handle their own auth.
|
- **NEVER add auth checks to tests** — agent providers handle their own auth.
|
||||||
- **Always run typecheck after every change.**
|
- **Always run typecheck after every change.**
|
||||||
|
|
||||||
## Orchestrator mode
|
## Debugging
|
||||||
|
|
||||||
- Prefix agent titles with "🎭" (e.g., "🎭 Feature Implementation")
|
|
||||||
- Launch agents in the most permissive mode
|
Find the complete daemon logs and traces in the $PASEO_HOME/daemon.log
|
||||||
- Set cwd to the repository root
|
|
||||||
- When agent control tool calls fail, list agents first — it may be a wait timeout
|
|
||||||
|
|||||||
@@ -537,14 +537,10 @@ export function AgentConfigRow({
|
|||||||
}, [modeOptions]);
|
}, [modeOptions]);
|
||||||
|
|
||||||
const modelSelectOptions: ComboSelectOption[] = useMemo(() => {
|
const modelSelectOptions: ComboSelectOption[] = useMemo(() => {
|
||||||
const opts: ComboSelectOption[] = [{ id: "", label: "Auto" }];
|
return models.map((model) => ({
|
||||||
for (const model of models) {
|
id: model.id,
|
||||||
opts.push({
|
label: model.label,
|
||||||
id: model.id,
|
}));
|
||||||
label: model.label,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return opts;
|
|
||||||
}, [models]);
|
}, [models]);
|
||||||
|
|
||||||
const thinkingSelectOptions: ComboSelectOption[] = useMemo(
|
const thinkingSelectOptions: ComboSelectOption[] = useMemo(
|
||||||
@@ -586,7 +582,7 @@ export function AgentConfigRow({
|
|||||||
title="Select model"
|
title="Select model"
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
options={modelSelectOptions}
|
options={modelSelectOptions}
|
||||||
placeholder="Auto"
|
placeholder={isModelLoading ? "Loading..." : "Select model"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
isLoading={isModelLoading}
|
isLoading={isModelLoading}
|
||||||
onSelect={onSelectModel}
|
onSelect={onSelectModel}
|
||||||
@@ -777,10 +773,8 @@ export function ModelDropdown({
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const anchorRef = useRef<View>(null);
|
const anchorRef = useRef<View>(null);
|
||||||
|
|
||||||
const selectedLabel = selectedModel
|
const selectedLabel = models.find((model) => model.id === selectedModel)?.label ?? selectedModel ?? "Select model";
|
||||||
? (models.find((model) => model.id === selectedModel)?.label ?? selectedModel)
|
const placeholder = isLoading && models.length === 0 ? "Loading..." : "Select model";
|
||||||
: "Automatic";
|
|
||||||
const placeholder = isLoading && models.length === 0 ? "Loading..." : "Automatic";
|
|
||||||
const helperText = error
|
const helperText = error
|
||||||
? undefined
|
? undefined
|
||||||
: isLoading
|
: isLoading
|
||||||
@@ -790,34 +784,20 @@ export function ModelDropdown({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const opts: ComboSelectOption[] = [
|
return models.map((model) => ({
|
||||||
{
|
id: model.id,
|
||||||
id: "",
|
label: model.label,
|
||||||
label: "Automatic (provider default)",
|
description: model.description,
|
||||||
description: "Let the assistant pick the recommended model.",
|
}));
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const model of models) {
|
|
||||||
opts.push({
|
|
||||||
id: model.id,
|
|
||||||
label: model.label,
|
|
||||||
description: model.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return opts;
|
|
||||||
}, [models]);
|
}, [models]);
|
||||||
|
|
||||||
const handleOpen = useCallback(() => setIsOpen(true), []);
|
const handleOpen = useCallback(() => setIsOpen(true), []);
|
||||||
const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []);
|
const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []);
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
if (id === "") {
|
onSelect(id);
|
||||||
onClear();
|
|
||||||
} else {
|
|
||||||
onSelect(id);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[onClear, onSelect],
|
[onSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { View, Pressable, Text, ActivityIndicator, Platform } from "react-native";
|
import { View, Pressable, Text, ActivityIndicator, Platform } from "react-native";
|
||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useShallow } from "zustand/shallow";
|
||||||
import { ArrowUp, Square, Pencil, AudioLines } from "lucide-react-native";
|
import { ArrowUp, Square, Pencil, AudioLines } from "lucide-react-native";
|
||||||
import Animated from "react-native-reanimated";
|
import Animated from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -126,7 +127,14 @@ export function AgentInputArea({
|
|||||||
agentDirectoryStatus === "revalidating" ||
|
agentDirectoryStatus === "revalidating" ||
|
||||||
agentDirectoryStatus === "error_after_ready");
|
agentDirectoryStatus === "error_after_ready");
|
||||||
|
|
||||||
const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId));
|
const agentState = useSessionStore(
|
||||||
|
useShallow((state) => {
|
||||||
|
const agent = state.sessions[serverId]?.agents?.get(agentId) ?? null;
|
||||||
|
return {
|
||||||
|
status: agent?.status ?? null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const queuedMessagesRaw = useSessionStore((state) =>
|
const queuedMessagesRaw = useSessionStore((state) =>
|
||||||
state.sessions[serverId]?.queuedMessages?.get(agentId),
|
state.sessions[serverId]?.queuedMessages?.get(agentId),
|
||||||
@@ -272,41 +280,8 @@ export function AgentInputArea({
|
|||||||
onSubmitMessageRef.current = onSubmitMessage;
|
onSubmitMessageRef.current = onSubmitMessage;
|
||||||
}, [onSubmitMessage]);
|
}, [onSubmitMessage]);
|
||||||
|
|
||||||
const isAgentRunning = agent?.status === "running";
|
const isAgentRunning = agentState.status === "running";
|
||||||
const agentUpdatedAtMs = agent?.updatedAt?.getTime() ?? 0;
|
const hasAgent = agentState.status !== null;
|
||||||
|
|
||||||
const prevIsAgentRunningRef = useRef(isAgentRunning);
|
|
||||||
const latestAgentUpdatedAtRef = useRef(agentUpdatedAtMs);
|
|
||||||
useEffect(() => {
|
|
||||||
const previousUpdatedAt = latestAgentUpdatedAtRef.current;
|
|
||||||
if (agentUpdatedAtMs < previousUpdatedAt) {
|
|
||||||
if (isProcessing && !isAgentRunning) {
|
|
||||||
prevIsAgentRunningRef.current = false;
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wasRunning = prevIsAgentRunningRef.current;
|
|
||||||
let shouldClearProcessing = false;
|
|
||||||
|
|
||||||
if (isProcessing) {
|
|
||||||
const hasEnteredRunning = !wasRunning && isAgentRunning;
|
|
||||||
const hasFreshRunningUpdateWhileRunning =
|
|
||||||
wasRunning && isAgentRunning && agentUpdatedAtMs > previousUpdatedAt;
|
|
||||||
const hasStoppedRunning = wasRunning && !isAgentRunning;
|
|
||||||
|
|
||||||
shouldClearProcessing =
|
|
||||||
hasEnteredRunning || hasFreshRunningUpdateWhileRunning || hasStoppedRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevIsAgentRunningRef.current = isAgentRunning;
|
|
||||||
latestAgentUpdatedAtRef.current = agentUpdatedAtMs;
|
|
||||||
|
|
||||||
if (shouldClearProcessing) {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
}, [agentUpdatedAtMs, isAgentRunning, isProcessing]);
|
|
||||||
|
|
||||||
const updateQueue = useCallback(
|
const updateQueue = useCallback(
|
||||||
(updater: (current: QueuedMessage[]) => QueuedMessage[]) => {
|
(updater: (current: QueuedMessage[]) => QueuedMessage[]) => {
|
||||||
@@ -348,7 +323,7 @@ export function AgentInputArea({
|
|||||||
message,
|
message,
|
||||||
imageAttachments,
|
imageAttachments,
|
||||||
forceSend,
|
forceSend,
|
||||||
isAgentRunning: agent?.status === "running",
|
isAgentRunning: agentState.status === "running",
|
||||||
// Parent-managed submits are still valid submit paths even when the
|
// Parent-managed submits are still valid submit paths even when the
|
||||||
// transport is disconnected, because the parent decides the failure mode.
|
// transport is disconnected, because the parent decides the failure mode.
|
||||||
canSubmit: Boolean(sendAgentMessageRef.current || onSubmitMessageRef.current),
|
canSubmit: Boolean(sendAgentMessageRef.current || onSubmitMessageRef.current),
|
||||||
@@ -481,7 +456,7 @@ export function AgentInputArea({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleCancelAgent() {
|
function handleCancelAgent() {
|
||||||
if (!agent || agent.status !== "running" || isCancellingAgent) {
|
if (!isAgentRunning || isCancellingAgent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isConnected || !client) {
|
if (!isConnected || !client) {
|
||||||
@@ -495,7 +470,7 @@ export function AgentInputArea({
|
|||||||
const isVoiceModeForAgent = voice?.isVoiceModeForAgent(serverId, agentId) ?? false;
|
const isVoiceModeForAgent = voice?.isVoiceModeForAgent(serverId, agentId) ?? false;
|
||||||
|
|
||||||
const handleToggleRealtimeVoice = useCallback(() => {
|
const handleToggleRealtimeVoice = useCallback(() => {
|
||||||
if (!voice || !isConnected || !agent) {
|
if (!voice || !isConnected || !hasAgent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (voice.isVoiceSwitching) {
|
if (voice.isVoiceSwitching) {
|
||||||
@@ -512,7 +487,7 @@ export function AgentInputArea({
|
|||||||
toast.error(message);
|
toast.error(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [agent, agentId, isConnected, serverId, toast, voice]);
|
}, [agentId, hasAgent, isConnected, serverId, toast, voice]);
|
||||||
|
|
||||||
function handleEditQueuedMessage(id: string) {
|
function handleEditQueuedMessage(id: string) {
|
||||||
const item = queuedMessages.find((q) => q.id === id);
|
const item = queuedMessages.find((q) => q.id === id);
|
||||||
@@ -604,7 +579,7 @@ export function AgentInputArea({
|
|||||||
|
|
||||||
const rightContent = (
|
const rightContent = (
|
||||||
<View style={styles.rightControls}>
|
<View style={styles.rightControls}>
|
||||||
{!isVoiceModeForAgent && agent ? (
|
{!isVoiceModeForAgent && hasAgent ? (
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
onPress={handleToggleRealtimeVoice}
|
onPress={handleToggleRealtimeVoice}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export async function submitAgentInput<TImage>(
|
|||||||
try {
|
try {
|
||||||
await input.submitMessage({ message: trimmedMessage, imageAttachments });
|
await input.submitMessage({ message: trimmedMessage, imageAttachments });
|
||||||
input.clearDraft("sent");
|
input.clearDraft("sent");
|
||||||
|
input.setIsProcessing(false);
|
||||||
return "submitted";
|
return "submitted";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
input.onSubmitError?.(error);
|
input.onSubmitError?.(error);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { View, Text, Platform, Pressable, Keyboard } from "react-native";
|
import { View, Text, Platform, Pressable, Keyboard } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useShallow } from "zustand/shallow";
|
||||||
|
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||||
import { Brain, ChevronDown, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react-native";
|
import { Brain, ChevronDown, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react-native";
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
||||||
@@ -603,8 +605,29 @@ function ControlledStatusBar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_MODES: AgentMode[] = [];
|
||||||
|
|
||||||
export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
||||||
const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId));
|
const agent = useSessionStore(
|
||||||
|
useShallow((state) => {
|
||||||
|
const currentAgent = state.sessions[serverId]?.agents?.get(agentId) ?? null;
|
||||||
|
return currentAgent
|
||||||
|
? {
|
||||||
|
provider: currentAgent.provider,
|
||||||
|
cwd: currentAgent.cwd,
|
||||||
|
currentModeId: currentAgent.currentModeId,
|
||||||
|
runtimeModelId: currentAgent.runtimeInfo?.model ?? null,
|
||||||
|
model: currentAgent.model,
|
||||||
|
thinkingOptionId: currentAgent.thinkingOptionId,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const availableModes = useStoreWithEqualityFn(
|
||||||
|
useSessionStore,
|
||||||
|
(state) => state.sessions[serverId]?.agents?.get(agentId)?.availableModes ?? EMPTY_MODES,
|
||||||
|
(a, b) => a === b || JSON.stringify(a) === JSON.stringify(b),
|
||||||
|
);
|
||||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||||
|
|
||||||
const modelsQuery = useQuery({
|
const modelsQuery = useQuery({
|
||||||
@@ -631,23 +654,23 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
const models = modelsQuery.data ?? null;
|
const models = modelsQuery.data ?? null;
|
||||||
|
|
||||||
const displayMode =
|
const displayMode =
|
||||||
agent?.availableModes?.find((mode) => mode.id === agent.currentModeId)?.label ||
|
availableModes.find((mode) => mode.id === agent?.currentModeId)?.label ||
|
||||||
agent?.currentModeId ||
|
agent?.currentModeId ||
|
||||||
"default";
|
"default";
|
||||||
|
|
||||||
const modelSelection = resolveAgentModelSelection({
|
const modelSelection = resolveAgentModelSelection({
|
||||||
models,
|
models,
|
||||||
runtimeModelId: agent?.runtimeInfo?.model,
|
runtimeModelId: agent?.runtimeModelId,
|
||||||
configuredModelId: agent?.model,
|
configuredModelId: agent?.model,
|
||||||
explicitThinkingOptionId: agent?.thinkingOptionId,
|
explicitThinkingOptionId: agent?.thinkingOptionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const modeOptions = useMemo<StatusOption[]>(() => {
|
const modeOptions = useMemo<StatusOption[]>(() => {
|
||||||
return (agent?.availableModes ?? []).map((mode) => ({
|
return availableModes.map((mode) => ({
|
||||||
id: mode.id,
|
id: mode.id,
|
||||||
label: mode.label,
|
label: mode.label,
|
||||||
}));
|
}));
|
||||||
}, [agent?.availableModes]);
|
}, [availableModes]);
|
||||||
|
|
||||||
const modelOptions = useMemo<StatusOption[]>(() => {
|
const modelOptions = useMemo<StatusOption[]>(() => {
|
||||||
return (models ?? []).map((model) => ({ id: model.id, label: model.label }));
|
return (models ?? []).map((model) => ({ id: model.id, label: model.label }));
|
||||||
@@ -668,9 +691,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
<ControlledStatusBar
|
<ControlledStatusBar
|
||||||
provider={agent.provider}
|
provider={agent.provider}
|
||||||
modeOptions={
|
modeOptions={
|
||||||
modeOptions.length > 0
|
modeOptions.length > 0 ? modeOptions : [{ id: agent.currentModeId ?? "", label: displayMode }]
|
||||||
? modeOptions
|
|
||||||
: [{ id: agent.currentModeId ?? "", label: displayMode }]
|
|
||||||
}
|
}
|
||||||
selectedModeId={agent.currentModeId ?? undefined}
|
selectedModeId={agent.currentModeId ?? undefined}
|
||||||
onSelectMode={(modeId) => {
|
onSelectMode={(modeId) => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
import type { StreamItem } from "@/types/stream";
|
import type { StreamItem } from "@/types/stream";
|
||||||
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";
|
||||||
import type { Agent } from "@/contexts/session-context";
|
import type { AgentScreenAgent } from "@/hooks/use-agent-screen-state-machine";
|
||||||
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 type { DaemonClient } from "@server/client/daemon-client";
|
import type { DaemonClient } from "@server/client/daemon-client";
|
||||||
@@ -82,7 +82,7 @@ export interface AgentStreamViewHandle {
|
|||||||
export interface AgentStreamViewProps {
|
export interface AgentStreamViewProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
agent: Agent;
|
agent: AgentScreenAgent;
|
||||||
streamItems: StreamItem[];
|
streamItems: StreamItem[];
|
||||||
pendingPermissions: Map<string, PendingPermission>;
|
pendingPermissions: Map<string, PendingPermission>;
|
||||||
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ const INLINE_MODEL_THRESHOLD = 8;
|
|||||||
|
|
||||||
type DrillDownView = { provider: string };
|
type DrillDownView = { provider: string };
|
||||||
|
|
||||||
function resolveDefaultModelLabel(models: AgentModelDefinition[] | undefined): string | null {
|
function resolveDefaultModelLabel(models: AgentModelDefinition[] | undefined): string {
|
||||||
if (!models || models.length === 0) {
|
if (!models || models.length === 0) {
|
||||||
return null;
|
return "Select model";
|
||||||
}
|
}
|
||||||
return (models.find((model) => model.isDefault) ?? models[0])?.label ?? null;
|
return (models.find((model) => model.isDefault) ?? models[0])?.label ?? "Select model";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CombinedModelSelectorProps {
|
interface CombinedModelSelectorProps {
|
||||||
@@ -75,7 +75,7 @@ export function CombinedModelSelector({
|
|||||||
const models = allProviderModels.get(selectedProvider);
|
const models = allProviderModels.get(selectedProvider);
|
||||||
if (!models) return isLoading ? "Loading..." : "Select model";
|
if (!models) return isLoading ? "Loading..." : "Select model";
|
||||||
const model = models.find((m) => m.id === selectedModel);
|
const model = models.find((m) => m.id === selectedModel);
|
||||||
return model?.label ?? resolveDefaultModelLabel(models) ?? "Select model";
|
return model?.label ?? resolveDefaultModelLabel(models);
|
||||||
}, [allProviderModels, selectedProvider, selectedModel, isLoading]);
|
}, [allProviderModels, selectedProvider, selectedModel, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -693,6 +693,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
const agentDefinition = providerDefinitionMap.get(formState.provider);
|
const agentDefinition = providerDefinitionMap.get(formState.provider);
|
||||||
const modeOptions = agentDefinition?.modes ?? [];
|
const modeOptions = agentDefinition?.modes ?? [];
|
||||||
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
|
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
|
||||||
|
const resolvedModelId = effectiveModel?.id ?? formState.model;
|
||||||
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
|
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
|
||||||
const isModelLoading = providerModelsQuery.isLoading || providerModelsQuery.isFetching;
|
const isModelLoading = providerModelsQuery.isLoading || providerModelsQuery.isFetching;
|
||||||
const modelError =
|
const modelError =
|
||||||
@@ -709,7 +710,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
setProviderFromUser,
|
setProviderFromUser,
|
||||||
selectedMode: formState.modeId,
|
selectedMode: formState.modeId,
|
||||||
setModeFromUser,
|
setModeFromUser,
|
||||||
selectedModel: formState.model,
|
selectedModel: resolvedModelId,
|
||||||
setModelFromUser,
|
setModelFromUser,
|
||||||
selectedThinkingOptionId: formState.thinkingOptionId,
|
selectedThinkingOptionId: formState.thinkingOptionId,
|
||||||
setThinkingOptionFromUser,
|
setThinkingOptionFromUser,
|
||||||
@@ -735,7 +736,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
|||||||
formState.serverId,
|
formState.serverId,
|
||||||
formState.provider,
|
formState.provider,
|
||||||
formState.modeId,
|
formState.modeId,
|
||||||
formState.model,
|
resolvedModelId,
|
||||||
formState.thinkingOptionId,
|
formState.thinkingOptionId,
|
||||||
formState.workingDir,
|
formState.workingDir,
|
||||||
setSelectedServerId,
|
setSelectedServerId,
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import type { Agent } from "@/contexts/session-context";
|
|
||||||
|
export interface AgentScreenAgent {
|
||||||
|
serverId: string;
|
||||||
|
id: string;
|
||||||
|
status: "initializing" | "idle" | "running" | "error" | "closed";
|
||||||
|
cwd: string;
|
||||||
|
projectPlacement?: {
|
||||||
|
checkout?: {
|
||||||
|
cwd?: string;
|
||||||
|
isGit?: boolean;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
export type AgentScreenMissingState =
|
export type AgentScreenMissingState =
|
||||||
| { kind: "idle" }
|
| { kind: "idle" }
|
||||||
@@ -8,8 +20,8 @@ export type AgentScreenMissingState =
|
|||||||
| { kind: "error"; message: string };
|
| { kind: "error"; message: string };
|
||||||
|
|
||||||
export interface AgentScreenMachineInput {
|
export interface AgentScreenMachineInput {
|
||||||
agent: Agent | null;
|
agent: AgentScreenAgent | null;
|
||||||
placeholderAgent: Agent | null;
|
placeholderAgent: AgentScreenAgent | null;
|
||||||
missingAgentState: AgentScreenMissingState;
|
missingAgentState: AgentScreenMissingState;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isArchivingCurrentAgent: boolean;
|
isArchivingCurrentAgent: boolean;
|
||||||
@@ -31,7 +43,7 @@ export type AgentScreenToastLatch = "none" | "history_refresh" | "sync_error";
|
|||||||
|
|
||||||
export interface AgentScreenMachineMemory {
|
export interface AgentScreenMachineMemory {
|
||||||
hasRenderedReady: boolean;
|
hasRenderedReady: boolean;
|
||||||
lastReadyAgent: Agent | null;
|
lastReadyAgent: AgentScreenAgent | null;
|
||||||
activeToastLatch: AgentScreenToastLatch;
|
activeToastLatch: AgentScreenToastLatch;
|
||||||
hadInitialSyncFailure: boolean;
|
hadInitialSyncFailure: boolean;
|
||||||
}
|
}
|
||||||
@@ -70,7 +82,7 @@ export type AgentScreenViewState =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
tag: "ready";
|
tag: "ready";
|
||||||
agent: Agent;
|
agent: AgentScreenAgent;
|
||||||
source: "authoritative" | "optimistic" | "stale";
|
source: "authoritative" | "optimistic" | "stale";
|
||||||
sync: AgentScreenReadySyncState;
|
sync: AgentScreenReadySyncState;
|
||||||
isArchiving: boolean;
|
isArchiving: boolean;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { ActivityIndicator, Platform, Text, View } from "react-native";
|
import { ActivityIndicator, Platform, Text, View } from "react-native";
|
||||||
import ReanimatedAnimated from "react-native-reanimated";
|
import ReanimatedAnimated from "react-native-reanimated";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { useShallow } from "zustand/shallow";
|
||||||
|
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||||
import { Bot } from "lucide-react-native";
|
import { Bot } from "lucide-react-native";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { AgentStreamView, type AgentStreamViewHandle } from "@/components/agent-stream-view";
|
import { AgentStreamView, type AgentStreamViewHandle } from "@/components/agent-stream-view";
|
||||||
@@ -16,6 +18,7 @@ import { useAgentAttentionClear } from "@/hooks/use-agent-attention-clear";
|
|||||||
import { useAgentInitialization } from "@/hooks/use-agent-initialization";
|
import { useAgentInitialization } from "@/hooks/use-agent-initialization";
|
||||||
import {
|
import {
|
||||||
useAgentScreenStateMachine,
|
useAgentScreenStateMachine,
|
||||||
|
type AgentScreenAgent,
|
||||||
type AgentScreenMissingState,
|
type AgentScreenMissingState,
|
||||||
} from "@/hooks/use-agent-screen-state-machine";
|
} from "@/hooks/use-agent-screen-state-machine";
|
||||||
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
||||||
@@ -78,11 +81,21 @@ function useAgentPanelDescriptor(
|
|||||||
target: { kind: "agent"; agentId: string },
|
target: { kind: "agent"; agentId: string },
|
||||||
context: { serverId: string },
|
context: { serverId: string },
|
||||||
): PanelDescriptor {
|
): PanelDescriptor {
|
||||||
const agent = useSessionStore(
|
const descriptorState = useSessionStore(
|
||||||
(state) => state.sessions[context.serverId]?.agents?.get(target.agentId) ?? null,
|
useShallow((state) => {
|
||||||
|
const agent = state.sessions[context.serverId]?.agents?.get(target.agentId) ?? null;
|
||||||
|
return {
|
||||||
|
provider: agent?.provider ?? "codex",
|
||||||
|
title: agent?.title ?? null,
|
||||||
|
status: agent?.status ?? null,
|
||||||
|
pendingPermissionCount: agent?.pendingPermissions.length ?? 0,
|
||||||
|
requiresAttention: agent?.requiresAttention ?? false,
|
||||||
|
attentionReason: agent?.attentionReason ?? null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const provider = agent?.provider ?? "codex";
|
const provider = descriptorState.provider;
|
||||||
const label = resolveWorkspaceAgentTabLabel(agent?.title);
|
const label = resolveWorkspaceAgentTabLabel(descriptorState.title);
|
||||||
const icon = provider === "claude" ? ClaudeIcon : provider === "codex" ? CodexIcon : Bot;
|
const icon = provider === "claude" ? ClaudeIcon : provider === "codex" ? CodexIcon : Bot;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -90,12 +103,12 @@ function useAgentPanelDescriptor(
|
|||||||
subtitle: `${formatProviderLabel(provider)} agent`,
|
subtitle: `${formatProviderLabel(provider)} agent`,
|
||||||
titleState: label ? "ready" : "loading",
|
titleState: label ? "ready" : "loading",
|
||||||
icon,
|
icon,
|
||||||
statusBucket: agent
|
statusBucket: descriptorState.status
|
||||||
? deriveSidebarStateBucket({
|
? deriveSidebarStateBucket({
|
||||||
status: agent.status,
|
status: descriptorState.status,
|
||||||
pendingPermissionCount: agent.pendingPermissions.length,
|
pendingPermissionCount: descriptorState.pendingPermissionCount,
|
||||||
requiresAttention: agent.requiresAttention,
|
requiresAttention: descriptorState.requiresAttention,
|
||||||
attentionReason: agent.attentionReason,
|
attentionReason: descriptorState.attentionReason,
|
||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
@@ -243,8 +256,25 @@ function AgentPanelBody({
|
|||||||
addImagesRef.current = addImages;
|
addImagesRef.current = addImages;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const agent = useSessionStore((state) =>
|
const agentState = useSessionStore(
|
||||||
agentId ? state.sessions[serverId]?.agents?.get(agentId) : undefined,
|
useShallow((state) => {
|
||||||
|
const agent = agentId ? state.sessions[serverId]?.agents?.get(agentId) ?? null : null;
|
||||||
|
return {
|
||||||
|
serverId: agent?.serverId ?? null,
|
||||||
|
id: agent?.id ?? null,
|
||||||
|
status: agent?.status ?? null,
|
||||||
|
cwd: agent?.cwd ?? null,
|
||||||
|
archivedAt: agent?.archivedAt ?? null,
|
||||||
|
requiresAttention: agent?.requiresAttention ?? false,
|
||||||
|
attentionReason: agent?.attentionReason ?? null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const projectPlacement = useStoreWithEqualityFn(
|
||||||
|
useSessionStore,
|
||||||
|
(state) =>
|
||||||
|
agentId ? (state.sessions[serverId]?.agents?.get(agentId)?.projectPlacement ?? null) : null,
|
||||||
|
(a, b) => a === b || JSON.stringify(a) === JSON.stringify(b),
|
||||||
);
|
);
|
||||||
const streamItemsRaw = useSessionStore((state) =>
|
const streamItemsRaw = useSessionStore((state) =>
|
||||||
agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined,
|
agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined,
|
||||||
@@ -318,52 +348,14 @@ function AgentPanelBody({
|
|||||||
agentId,
|
agentId,
|
||||||
client,
|
client,
|
||||||
isConnected,
|
isConnected,
|
||||||
requiresAttention: agent?.requiresAttention,
|
requiresAttention: agentState.requiresAttention,
|
||||||
attentionReason: agent?.attentionReason,
|
attentionReason: agentState.attentionReason,
|
||||||
isScreenFocused: isPaneFocused,
|
isScreenFocused: isPaneFocused,
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur;
|
clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur;
|
||||||
}, [attentionController.clearOnAgentBlur]);
|
}, [attentionController.clearOnAgentBlur]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// DEBUG: track which selector values change between renders
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const debugPrevRef = useRef<Record<string, unknown>>({});
|
|
||||||
useEffect(() => {
|
|
||||||
const prev = debugPrevRef.current;
|
|
||||||
const curr: Record<string, unknown> = {
|
|
||||||
agent,
|
|
||||||
"agent?.status": agent?.status,
|
|
||||||
"agent?.cwd": agent?.cwd,
|
|
||||||
"agent?.updatedAt": agent?.updatedAt,
|
|
||||||
"agent?.requiresAttention": agent?.requiresAttention,
|
|
||||||
streamItemsRaw,
|
|
||||||
"streamItems.length": streamItems.length,
|
|
||||||
allPendingPermissions,
|
|
||||||
isInitializingFromMap,
|
|
||||||
historySyncGeneration,
|
|
||||||
hasAppliedAuthoritativeHistory,
|
|
||||||
agentHistorySyncGeneration,
|
|
||||||
hasSession,
|
|
||||||
isPaneFocused,
|
|
||||||
isConnected,
|
|
||||||
};
|
|
||||||
const changed: string[] = [];
|
|
||||||
for (const key of Object.keys(curr)) {
|
|
||||||
if (!Object.is(prev[key], curr[key])) {
|
|
||||||
changed.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changed.length > 0 && Object.keys(prev).length > 0) {
|
|
||||||
console.log("[AgentPanelBody] values changed:", changed.join(", "), {
|
|
||||||
changed: Object.fromEntries(changed.map((k) => [k, { prev: prev[k], curr: curr[k] }])),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
debugPrevRef.current = curr;
|
|
||||||
});
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({
|
const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({
|
||||||
mode: "translate",
|
mode: "translate",
|
||||||
});
|
});
|
||||||
@@ -488,49 +480,34 @@ function AgentPanelBody({
|
|||||||
}, [optimisticStreamItems, streamItems]);
|
}, [optimisticStreamItems, streamItems]);
|
||||||
|
|
||||||
const shouldUseOptimisticStream = isPendingCreateForPanel && optimisticStreamItems.length > 0;
|
const shouldUseOptimisticStream = isPendingCreateForPanel && optimisticStreamItems.length > 0;
|
||||||
const authoritativeStatus = agent?.status;
|
const authoritativeStatus = agentState.status;
|
||||||
const isAuthoritativeBootstrapping =
|
const isAuthoritativeBootstrapping =
|
||||||
authoritativeStatus === "initializing" || authoritativeStatus === "idle";
|
authoritativeStatus === "initializing" || authoritativeStatus === "idle";
|
||||||
const showPendingCreateSubmitLoading =
|
const showPendingCreateSubmitLoading =
|
||||||
isPendingCreateForPanel && (!authoritativeStatus || isAuthoritativeBootstrapping);
|
isPendingCreateForPanel && (!authoritativeStatus || isAuthoritativeBootstrapping);
|
||||||
const canFinalizePendingCreate = Boolean(authoritativeStatus) && !isAuthoritativeBootstrapping;
|
const canFinalizePendingCreate = Boolean(authoritativeStatus) && !isAuthoritativeBootstrapping;
|
||||||
|
|
||||||
const placeholderAgent: Agent | null = useMemo(() => {
|
const agent: AgentScreenAgent | null =
|
||||||
|
agentState.serverId && agentState.id && agentState.status && agentState.cwd
|
||||||
|
? {
|
||||||
|
serverId: agentState.serverId,
|
||||||
|
id: agentState.id,
|
||||||
|
status: agentState.status,
|
||||||
|
cwd: agentState.cwd,
|
||||||
|
projectPlacement,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const placeholderAgent: AgentScreenAgent | null = useMemo(() => {
|
||||||
if (!shouldUseOptimisticStream || !agentId) {
|
if (!shouldUseOptimisticStream || !agentId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const now = new Date();
|
|
||||||
return {
|
return {
|
||||||
serverId,
|
serverId,
|
||||||
id: agentId,
|
id: agentId,
|
||||||
provider: "claude",
|
|
||||||
status: "running",
|
status: "running",
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
lastUserMessageAt: now,
|
|
||||||
lastActivityAt: now,
|
|
||||||
capabilities: {
|
|
||||||
supportsStreaming: true,
|
|
||||||
supportsSessionPersistence: false,
|
|
||||||
supportsDynamicModes: false,
|
|
||||||
supportsMcpServers: false,
|
|
||||||
supportsReasoningStream: false,
|
|
||||||
supportsToolInvocations: false,
|
|
||||||
},
|
|
||||||
currentModeId: null,
|
|
||||||
availableModes: [],
|
|
||||||
pendingPermissions: [],
|
|
||||||
persistence: null,
|
|
||||||
runtimeInfo: {
|
|
||||||
provider: "claude",
|
|
||||||
sessionId: null,
|
|
||||||
model: null,
|
|
||||||
modeId: null,
|
|
||||||
},
|
|
||||||
title: "Agent",
|
|
||||||
cwd: ".",
|
cwd: ".",
|
||||||
model: null,
|
projectPlacement: null,
|
||||||
labels: {},
|
|
||||||
};
|
};
|
||||||
}, [agentId, serverId, shouldUseOptimisticStream]);
|
}, [agentId, serverId, shouldUseOptimisticStream]);
|
||||||
|
|
||||||
@@ -642,7 +619,7 @@ function AgentPanelBody({
|
|||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (agent || shouldUseOptimisticStream) {
|
if (agentState.id || shouldUseOptimisticStream) {
|
||||||
if (missingAgentState.kind !== "idle") {
|
if (missingAgentState.kind !== "idle") {
|
||||||
setMissingAgentState({ kind: "idle" });
|
setMissingAgentState({ kind: "idle" });
|
||||||
}
|
}
|
||||||
@@ -717,7 +694,7 @@ function AgentPanelBody({
|
|||||||
setMissingAgentState({ kind: "error", message });
|
setMissingAgentState({ kind: "error", message });
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
agent,
|
agentState.id,
|
||||||
agentId,
|
agentId,
|
||||||
client,
|
client,
|
||||||
ensureAgentIsInitialized,
|
ensureAgentIsInitialized,
|
||||||
@@ -803,7 +780,7 @@ function AgentPanelBody({
|
|||||||
</ReanimatedAnimated.View>
|
</ReanimatedAnimated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{agentId && !isArchivingCurrentAgent && !agent?.archivedAt ? (
|
{agentId && !isArchivingCurrentAgent && !agentState.archivedAt ? (
|
||||||
<AgentInputArea
|
<AgentInputArea
|
||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
@@ -832,7 +809,7 @@ function AgentPanelBody({
|
|||||||
streamViewRef.current?.scrollToBottom("message-sent");
|
streamViewRef.current?.scrollToBottom("message-sent");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : agentId && agent?.archivedAt ? (
|
) : agentId && agentState.archivedAt ? (
|
||||||
<ArchivedAgentCallout serverId={serverId} agentId={agentId} />
|
<ArchivedAgentCallout serverId={serverId} agentId={agentId} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user