mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-12 01:00:54 +00:00
* fix(ai): keep dropped AI chats recoverable with dangling marking and /rebind Closing a member terminal or dissolving a workspace used to clear the chat's active pointer, making it invisible (scope rank 0) and unrecoverable from the UI even though the record survived in storage. - orphan cleanup now marks dropped chats `scopeDangling: true` and keeps record + active pointer intact; the flag clears when the target returns or the chat is rebound - `/chats` lists dangling chats with a numbered inline affordance and hint line - new `/rebind <n>` panel action rewrites `scope` to the panel's current scope, clears the marker and restores the active pointer - i18n keys added for en / zh-CN / zh-TW / es / ru Stacked on #4018. Refs #4015 (R2). * fix(workspace): reuse existing workspace id when merging tabs with a workspace createWorkspaceFromSessions silently no-opped when either merged tab already belonged to a workspace (e.g. dragging a workspace pane onto a standalone terminal), and the older dissolve->recreate behavior regenerated the id that workspace-scoped AI chats bind to. When one of the merged tabs has a workspace, the orphan tab now joins that existing workspace (pane inserted next to the anchor session, tab slot collapsed), so workspace-scoped chat bindings survive. Merging two standalone tabs still creates a fresh workspace. Stacked on #4018. Refs #4015 (R3). * fix(ai): persist cleaned active map on dangling chat rebind rebindSessionScope was updating React state to drop stale old-scope pointers, then rebuilding localStorage from the pre-rebind snapshot and resurrecting them. Persist the same cleaned map via rebindActiveSessionMap. * fix(ai): persist rebind map inside setState; wire backup actions on terminal panel - Write cleaned active-session map from inside the rebind updater so a deferred React setState cannot skip snapshot/localStorage persistence. - Pass exportSessionsBackup/importSessionsBackup through TerminalLayerSupport and include them in AI_CHAT_SIDE_PANEL_AI_STATE_KEYS so history-drawer backup buttons work on the normal terminal path. * fix(ai): guard backup restore races and use app-wide live targets for /chats - Skip restore when in-memory sessions already exist; apply snapshot, storage write, and AI state emit atomically inside the empty-state path so peer useAIState instances see the restore. - Thread validAIScopeTargetIds into AIChatSidePanel as liveScopeTargetIds so /chats does not mark chats bound to other open terminals as dangling. * fix(ai): open rebound chat in panel view and refresh /chats listing - rebindSessionScope also sets the panel view to the rebound session so an existing draft/other chat does not keep hiding the recovery. - After /rebind, rewrite the stored listing instead of rebuilding from the stale render so the chat is no longer offered as dangling. * fix(workspace): invert join split when dragged pane owns workspace; refresh /chats titles - When merging a workspace-owned dragged pane onto a standalone tab, the orphan is inserted — invert the drop position so the previewed side matches. - Compare workspaces in AIChatSidePanel memo so /chats picks up renames. * fix(ai): validate live sessions array on boot; mark imported orphans dangling - Reject parseable non-array store values (e.g. {}) via readLiveSessionsArray so boot restore is not blocked by a corrupt-but-truthy value. - After manual import, re-run orphan cleanup with the last known live target set so chats bound to closed scopes become dangling and /rebind-able. * fix(ai): merge imported sessions before orphan cleanup Apply import merge to the snapshot/ref synchronously, then run dangling marking, so a deferred setState cannot let cleanup rewrite over the import. * fix(ai): mark restored/imported orphans dangling even with no live targets Restore and import do not change live terminals/workspaces, so the orphan cleanup effect will not re-run. Always call cleanup against the last known live target set after those paths — an empty set is a valid observed state (nothing mounted), not an unavailable snapshot — so chats bound to missing scopes become dangling and /chats can offer /rebind. * fix(ai): apply backup restore synchronously before dangling cleanup Do not gate restore + orphan marking on a setState updater having run. A deferred updater left didRestore false, skipped cleanup, and could leave restored chats with missing scopes unmarked and unrebindable from /chats. * fix(ai): complete /rebind from a partial slash picker selection Selecting Rebind after typing `/re` left the composer unchanged, so a following number became `/re 1` and could be sent to the agent. Replace the slash query with `/rebind ` the same way Rename does. * fix(ai): replace hostIds on rebind; skip non-array backup generations - Rebind builds a fresh destination scope (including hostIds from the panel) instead of spreading the old scope, so history matching follows the new terminal/workspace host. - readLatestGeneration treats valid JSON that is not a session array as unusable and continues to older generations, matching corrupt skips. * fix(ai): revalidate dangling chat before /rebind from stale /chats listing Skip rebind when the cached entry is deleted or already bound to a live scope; drop stale listing entries so a second panel cannot steal the chat. * fix(ai): reject malformed /rebind numbers Require the entire trimmed argument to be digits so /rebind 1foo and /rebind 1.5 show usage instead of silently targeting index 1. --------- Co-authored-by: binaricat <binaricat@users.noreply.github.com> Co-authored-by: 陈大猫 <16399091+binaricat@users.noreply.github.com>
490 lines
20 KiB
TypeScript
490 lines
20 KiB
TypeScript
import React, { type Dispatch, type SetStateAction } from 'react';
|
|
import { History, Plus, Undo2 } from 'lucide-react';
|
|
import type { AIPermissionMode, AISession, ChatMessage, DiscoveredAgent, ExternalAgentConfig, AgentModelPreset, ProviderConfig, UploadedFile } from '../infrastructure/ai/types';
|
|
import type { Host, VaultNote } from '../types';
|
|
import type { UserSkillOption } from './ai/userSkillsState';
|
|
import type { AIQuickMessage } from '../infrastructure/ai/quickMessages';
|
|
import { Button } from './ui/button';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
|
import AgentSelector from './ai/AgentSelector';
|
|
import ChatInput from './ai/ChatInput';
|
|
import ChatMessageList from './ai/ChatMessageList';
|
|
import ConversationExport from './ai/ConversationExport';
|
|
import { SessionHistoryDrawer, formatRelativeTime } from './AIChatSessionHistoryDrawer';
|
|
import { cn } from '../lib/utils';
|
|
import { TERMINAL_SIDE_PANEL_INNER_HEADER_CLASS } from './terminalLayer/terminalSidePanelChrome';
|
|
import {
|
|
getAIPanelDiagnosticHiddenParts,
|
|
getAIPanelProfilerProps,
|
|
isAIPanelDiagnosticPartHidden,
|
|
} from './ai/aiPanelDiagnostics';
|
|
|
|
type Translate = (key: string) => string;
|
|
type ExportFormat = 'md' | 'json' | 'txt';
|
|
type TerminalSessionSummary = {
|
|
sessionId: string;
|
|
hostname: string;
|
|
label: string;
|
|
connected: boolean;
|
|
};
|
|
|
|
interface AIChatPanelContentProps {
|
|
t: Translate;
|
|
currentAgentId: string;
|
|
externalAgents: ExternalAgentConfig[];
|
|
discoveredAgents: DiscoveredAgent[];
|
|
isDiscovering: boolean;
|
|
handleAgentChange: (agentId: string) => void;
|
|
handleEnableDiscoveredAgent: (agent: DiscoveredAgent) => void;
|
|
rediscover: () => void;
|
|
handleOpenSettings: () => void;
|
|
activeSession: AISession | null;
|
|
handleExport: (format: ExportFormat) => void;
|
|
showHistory: boolean;
|
|
setShowHistory: Dispatch<SetStateAction<boolean>>;
|
|
handleNewChat: () => void;
|
|
historySessions: AISession[];
|
|
activeSessionId: string | null;
|
|
handleSelectSession: (sessionId: string) => void;
|
|
handleDeleteSession: (event: React.MouseEvent, sessionId: string) => void;
|
|
/** Fork the conversation up to a completed assistant response into a new session. */
|
|
onForkFromMessage?: (messageId: string) => void;
|
|
messages: ChatMessage[];
|
|
isStreaming: boolean;
|
|
activeCompaction?: import('../application/state/useAgentCompactionUi').ActiveCompactionUi | null;
|
|
contextUsage?: import('../application/state/useAgentCompactionUi').AgentContextUsage | null;
|
|
inputValue: string;
|
|
setInputValue: (value: string) => void;
|
|
handleSend: () => void;
|
|
handleCompact: () => void;
|
|
/** Rename a chat by id (history drawer edit affordance). Panel action, never agent content. */
|
|
onRenameSession?: (sessionId: string, title: string) => void;
|
|
/** `/rename <title>` for the current chat. Panel action, never agent content. */
|
|
onRenameActiveChat?: (title: string) => void;
|
|
/** `/chats`: list the chats visible to this panel. Panel action, never agent content. */
|
|
onListChats?: () => void;
|
|
/** `/rebind <n>`: rebind a dangling chat to this panel's scope. Panel action, never agent content. */
|
|
onRebindChat?: (arg: string) => void;
|
|
/** `/chats` listing output, rendered as a system-style note above the composer. */
|
|
panelCommandNote?: string | null;
|
|
onDismissPanelCommandNote?: () => void;
|
|
/** Manual durability escape hatch: export/import the whole session store. */
|
|
onExportSessions?: () => void;
|
|
onImportSessions?: () => void;
|
|
/** Undo last turn is offered at a safe boundary (never mid-tool / mid-compaction). */
|
|
canUndoLastTurn?: boolean;
|
|
handleUndoLastTurn?: () => void;
|
|
canCompact?: boolean;
|
|
handleSteer: () => void;
|
|
handleStop: () => void;
|
|
canSteer: boolean;
|
|
isSteering: boolean;
|
|
steerWarning?: string;
|
|
lockTurnConfiguration: boolean;
|
|
canSendCurrentAgent: boolean;
|
|
providerDisplayName?: string;
|
|
modelDisplayName?: string;
|
|
modelCatalogWarning?: string;
|
|
/** Live SDK model catalog is loading (no built-in presets to show yet). */
|
|
modelCatalogLoading?: boolean;
|
|
/** Provenance of the active model list, rendered as the picker status chip. */
|
|
modelCatalogSource?: 'live' | 'last-known-good' | 'presets';
|
|
/** ISO timestamp of the successful fetch backing `modelCatalogSource`. */
|
|
modelCatalogFetchedAt?: string;
|
|
/** Retry a failed SDK model catalog load from the warning banner. */
|
|
handleModelCatalogRetry?: () => void;
|
|
agentModelPresets: AgentModelPreset[];
|
|
/** Suppress the composer picker's "use custom model" action (locked model). */
|
|
allowCustomModelEntry?: boolean;
|
|
selectedAgentModel: string;
|
|
handleAgentModelSelect: (modelId: string) => void;
|
|
cattyConfiguredProviders: ProviderConfig[];
|
|
effectiveActiveProvider?: ProviderConfig;
|
|
effectiveActiveModelId?: string;
|
|
handleAgentProviderModelSelect: (providerId: string, modelId: string, contextWindow?: number) => void;
|
|
selectedCattyThinking?: string;
|
|
handleCattyThinkingSelect?: (level: string) => void;
|
|
files: UploadedFile[];
|
|
addFiles: (inputFiles: File[]) => Promise<void>;
|
|
removeFile: (fileId: string) => void;
|
|
terminalSessions: TerminalSessionSummary[];
|
|
selectedUserSkills: UserSkillOption[];
|
|
userSkillOptions: UserSkillOption[];
|
|
quickMessages: AIQuickMessage[];
|
|
addSelectedUserSkill: (slug: string) => void;
|
|
removeSelectedUserSkill: (slug: string) => void;
|
|
globalPermissionMode: AIPermissionMode;
|
|
setGlobalPermissionMode?: (mode: AIPermissionMode) => void;
|
|
notes?: VaultNote[];
|
|
hosts?: Host[];
|
|
/** Mention Note: attach a Vault → Notes entry as inline context. */
|
|
onMentionNote?: (note: VaultNote) => void;
|
|
onOpenVaultNote?: (noteId: string) => void;
|
|
onOpenVaultHost?: (hostId: string) => void;
|
|
onOpenVaultSection?: (section: 'notes' | 'hosts') => void;
|
|
/** Hidden retained panels keep the composer warm without the message tree. */
|
|
parked?: boolean;
|
|
/** Disable header transitions while send preflight is in flight. */
|
|
sending?: boolean;
|
|
}
|
|
|
|
export const AIChatPanelContent: React.FC<AIChatPanelContentProps> = ({
|
|
t,
|
|
currentAgentId,
|
|
externalAgents,
|
|
discoveredAgents,
|
|
isDiscovering,
|
|
handleAgentChange,
|
|
handleEnableDiscoveredAgent,
|
|
rediscover,
|
|
handleOpenSettings,
|
|
activeSession,
|
|
handleExport,
|
|
showHistory,
|
|
setShowHistory,
|
|
handleNewChat,
|
|
historySessions,
|
|
activeSessionId,
|
|
handleSelectSession,
|
|
handleDeleteSession,
|
|
onForkFromMessage,
|
|
messages,
|
|
isStreaming,
|
|
activeCompaction = null,
|
|
contextUsage = null,
|
|
inputValue,
|
|
setInputValue,
|
|
handleSend,
|
|
handleCompact,
|
|
onRenameSession,
|
|
onRenameActiveChat,
|
|
onListChats,
|
|
onRebindChat,
|
|
panelCommandNote,
|
|
onDismissPanelCommandNote,
|
|
onExportSessions,
|
|
onImportSessions,
|
|
canUndoLastTurn = false,
|
|
handleUndoLastTurn,
|
|
canCompact = false,
|
|
handleSteer,
|
|
handleStop,
|
|
handleModelCatalogRetry,
|
|
canSteer,
|
|
isSteering,
|
|
steerWarning,
|
|
lockTurnConfiguration,
|
|
canSendCurrentAgent,
|
|
providerDisplayName,
|
|
modelDisplayName,
|
|
modelCatalogWarning,
|
|
modelCatalogLoading = false,
|
|
modelCatalogSource,
|
|
modelCatalogFetchedAt,
|
|
agentModelPresets,
|
|
allowCustomModelEntry = true,
|
|
selectedAgentModel,
|
|
handleAgentModelSelect,
|
|
cattyConfiguredProviders,
|
|
effectiveActiveProvider,
|
|
effectiveActiveModelId,
|
|
handleAgentProviderModelSelect,
|
|
selectedCattyThinking,
|
|
handleCattyThinkingSelect,
|
|
files,
|
|
addFiles,
|
|
removeFile,
|
|
terminalSessions,
|
|
selectedUserSkills,
|
|
userSkillOptions,
|
|
quickMessages,
|
|
addSelectedUserSkill,
|
|
removeSelectedUserSkill,
|
|
globalPermissionMode,
|
|
setGlobalPermissionMode,
|
|
notes = [],
|
|
hosts = [],
|
|
onMentionNote,
|
|
onOpenVaultNote,
|
|
onOpenVaultHost,
|
|
onOpenVaultSection,
|
|
parked = false,
|
|
sending = false,
|
|
}) => {
|
|
const mentionHosts = React.useMemo(
|
|
() => terminalSessions.map((session) => ({
|
|
sessionId: session.sessionId,
|
|
hostname: session.hostname,
|
|
label: session.label,
|
|
connected: session.connected,
|
|
})),
|
|
[terminalSessions],
|
|
);
|
|
const providerSwitcher = React.useMemo(
|
|
() => (
|
|
currentAgentId === 'catty' && cattyConfiguredProviders.length > 0
|
|
? {
|
|
providers: cattyConfiguredProviders,
|
|
selectedProviderId: effectiveActiveProvider?.id,
|
|
selectedModelId: effectiveActiveModelId || undefined,
|
|
onSelect: handleAgentProviderModelSelect,
|
|
}
|
|
: undefined
|
|
),
|
|
[
|
|
cattyConfiguredProviders,
|
|
currentAgentId,
|
|
effectiveActiveModelId,
|
|
effectiveActiveProvider?.id,
|
|
handleAgentProviderModelSelect,
|
|
],
|
|
);
|
|
const hiddenParts = getAIPanelDiagnosticHiddenParts();
|
|
const hideHeader = isAIPanelDiagnosticPartHidden('header', hiddenParts);
|
|
const hideHistory = isAIPanelDiagnosticPartHidden('history', hiddenParts);
|
|
const hideMessages = isAIPanelDiagnosticPartHidden('messages', hiddenParts);
|
|
const hideRecent = isAIPanelDiagnosticPartHidden('recent', hiddenParts);
|
|
const hideInput = isAIPanelDiagnosticPartHidden('input', hiddenParts);
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-background" data-section="ai-chat-panel">
|
|
{/* ── Header ── */}
|
|
{!hideHeader && (
|
|
<React.Profiler {...getAIPanelProfilerProps('AIChatPanel.Header')}>
|
|
<div className={cn(
|
|
TERMINAL_SIDE_PANEL_INNER_HEADER_CLASS,
|
|
'px-2 flex items-center justify-between border-b border-border/50',
|
|
)}>
|
|
<AgentSelector
|
|
currentAgentId={currentAgentId}
|
|
externalAgents={externalAgents}
|
|
discoveredAgents={discoveredAgents}
|
|
isDiscovering={isDiscovering}
|
|
parked={parked}
|
|
disabled={sending}
|
|
onSelectAgent={handleAgentChange}
|
|
onEnableDiscoveredAgent={handleEnableDiscoveredAgent}
|
|
onRediscover={rediscover}
|
|
onManageAgents={handleOpenSettings}
|
|
/>
|
|
<div className="flex items-center gap-0.5">
|
|
{handleUndoLastTurn && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 rounded-md text-muted-foreground/62 hover:bg-white/[0.05] hover:text-foreground"
|
|
aria-label={t('ai.chat.undoLastTurn')}
|
|
disabled={sending || isStreaming || !canUndoLastTurn}
|
|
onClick={handleUndoLastTurn}
|
|
>
|
|
<Undo2 size={12} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('ai.chat.undoLastTurn')}</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
<ConversationExport
|
|
session={activeSession}
|
|
onExport={handleExport}
|
|
className="h-6 w-6 rounded-md text-muted-foreground/62 hover:bg-white/[0.05] hover:text-foreground [&_svg]:size-3"
|
|
/>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 rounded-md text-muted-foreground/62 hover:bg-white/[0.05] hover:text-foreground"
|
|
disabled={sending}
|
|
onClick={() => setShowHistory(!showHistory)}
|
|
>
|
|
<History size={12} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('ai.chat.sessionHistory')}</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 rounded-md text-primary/82 hover:bg-primary/[0.10] hover:text-primary"
|
|
disabled={sending}
|
|
onClick={handleNewChat}
|
|
>
|
|
<Plus size={13} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('ai.chat.newChat')}</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</React.Profiler>
|
|
)}
|
|
|
|
{/* ── Main content ── */}
|
|
{!parked && showHistory && !hideHistory ? (
|
|
<React.Profiler {...getAIPanelProfilerProps('AIChatPanel.History')}>
|
|
<SessionHistoryDrawer
|
|
sessions={historySessions}
|
|
activeSessionId={activeSessionId}
|
|
onSelect={sending ? () => undefined : handleSelectSession}
|
|
onDelete={handleDeleteSession}
|
|
onRename={onRenameSession}
|
|
onExportSessions={onExportSessions}
|
|
onImportSessions={onImportSessions}
|
|
onClose={() => setShowHistory(false)}
|
|
/>
|
|
</React.Profiler>
|
|
) : (
|
|
<>
|
|
{/* Chat messages */}
|
|
{!parked && !hideMessages && (
|
|
<React.Profiler {...getAIPanelProfilerProps('AIChatPanel.Messages')}>
|
|
<ChatMessageList
|
|
messages={messages}
|
|
isStreaming={isStreaming}
|
|
activeSessionId={activeSessionId}
|
|
activeCompaction={activeCompaction}
|
|
contextCompaction={activeSession?.contextCompaction}
|
|
onForkFromMessage={onForkFromMessage}
|
|
notes={notes}
|
|
hosts={hosts}
|
|
onOpenVaultNote={onOpenVaultNote}
|
|
onOpenVaultHost={onOpenVaultHost}
|
|
onOpenVaultSection={onOpenVaultSection}
|
|
/>
|
|
</React.Profiler>
|
|
)}
|
|
|
|
{/* Recent sessions (Zed-style, shown when no messages) */}
|
|
{!parked && messages.length === 0 && historySessions.length > 0 && !hideRecent && (
|
|
<React.Profiler {...getAIPanelProfilerProps('AIChatPanel.Recent')}>
|
|
<div className="shrink-0 px-4 pb-1">
|
|
<div className="flex items-baseline justify-between mb-2">
|
|
<span className="text-[11px] text-muted-foreground/30 tracking-wide">{t('ai.chat.recent')}</span>
|
|
<button
|
|
onClick={() => setShowHistory(true)}
|
|
className="text-[11px] text-muted-foreground/30 hover:text-muted-foreground/50 transition-colors cursor-pointer"
|
|
>
|
|
{t('ai.chat.viewAll')}
|
|
</button>
|
|
</div>
|
|
{historySessions.slice(0, 3).map((session) => (
|
|
<button
|
|
key={session.id}
|
|
disabled={sending}
|
|
onClick={() => handleSelectSession(session.id)}
|
|
className="w-full flex items-baseline justify-between py-1.5 text-left hover:text-foreground transition-colors cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
>
|
|
<span className="text-[13px] text-foreground/60 truncate pr-4">
|
|
{session.title || t('ai.chat.untitled')}
|
|
</span>
|
|
<span className="text-[11px] text-muted-foreground/25 shrink-0">
|
|
{formatRelativeTime(new Date(session.updatedAt), t)}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</React.Profiler>
|
|
)}
|
|
|
|
{/* Input area */}
|
|
{!hideInput && (
|
|
<React.Profiler {...getAIPanelProfilerProps('AIChatPanel.Input')}>
|
|
<div>
|
|
{panelCommandNote ? (
|
|
<div role="status" className="mx-3 mb-1.5 rounded-md border border-border/50 bg-muted/30 px-2.5 py-1.5 text-[11px] leading-4 text-muted-foreground">
|
|
<span className="block max-h-48 overflow-y-auto whitespace-pre-wrap break-words">{panelCommandNote}</span>
|
|
{onDismissPanelCommandNote && (
|
|
<button
|
|
type="button"
|
|
aria-label={t('common.close')}
|
|
onClick={onDismissPanelCommandNote}
|
|
className="mt-1 text-[11px] text-muted-foreground/60 underline-offset-2 transition-colors hover:text-foreground hover:underline cursor-pointer"
|
|
>
|
|
{t('common.close')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
{steerWarning ? (
|
|
<div role="status" className="mx-3 mb-1.5 rounded-md border border-amber-500/25 bg-amber-500/10 px-2.5 py-1.5 text-[11px] leading-4 text-amber-600 dark:text-amber-400">
|
|
{steerWarning}
|
|
</div>
|
|
) : modelCatalogWarning ? (
|
|
<div role="status" className="mx-3 mb-1.5 flex items-center gap-2 rounded-md border border-amber-500/25 bg-amber-500/10 px-2.5 py-1.5 text-[11px] leading-4 text-amber-600 dark:text-amber-400">
|
|
<span className="min-w-0 flex-1">{modelCatalogWarning}</span>
|
|
{handleModelCatalogRetry && (
|
|
<button
|
|
onClick={handleModelCatalogRetry}
|
|
className="shrink-0 rounded px-1.5 py-0.5 text-[11px] font-medium text-amber-600 underline-offset-2 transition-colors hover:bg-amber-500/10 hover:text-amber-500 hover:underline cursor-pointer dark:text-amber-400 dark:hover:text-amber-300"
|
|
>
|
|
{t('ai.chat.modelCatalogRetry')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : modelCatalogLoading ? (
|
|
<div role="status" className="mx-3 mb-1.5 rounded-md bg-muted/40 px-2.5 py-1.5 text-[11px] leading-4 text-muted-foreground/70">
|
|
{t('ai.chat.loadingModels')}
|
|
</div>
|
|
) : null}
|
|
<ChatInput
|
|
parked={parked}
|
|
value={inputValue}
|
|
onChange={setInputValue}
|
|
onSend={handleSend}
|
|
onCompact={handleCompact}
|
|
onRenameSession={onRenameActiveChat}
|
|
onListChats={onListChats}
|
|
onRebindChat={onRebindChat}
|
|
canCompact={canCompact}
|
|
onSteer={handleSteer}
|
|
onStop={handleStop}
|
|
isStreaming={isStreaming}
|
|
canSteer={canSteer}
|
|
isSteering={isSteering}
|
|
lockTurnConfiguration={lockTurnConfiguration}
|
|
disabled={!canSendCurrentAgent}
|
|
providerName={providerDisplayName}
|
|
modelName={modelDisplayName}
|
|
agentName={currentAgentId === 'catty' ? 'Catty Agent' : externalAgents.find(a => a.id === currentAgentId)?.name}
|
|
modelPresets={agentModelPresets}
|
|
modelCatalogLoading={modelCatalogLoading}
|
|
modelCatalogSource={modelCatalogSource}
|
|
modelCatalogFetchedAt={modelCatalogFetchedAt}
|
|
modelCatalogWarning={modelCatalogWarning}
|
|
onRefreshModelCatalog={handleModelCatalogRetry}
|
|
allowCustomModelEntry={allowCustomModelEntry}
|
|
selectedModelId={selectedAgentModel}
|
|
onModelSelect={handleAgentModelSelect}
|
|
providerSwitcher={providerSwitcher}
|
|
pickerScope={currentAgentId}
|
|
thinkingLevel={currentAgentId === 'catty' ? selectedCattyThinking : undefined}
|
|
onThinkingLevelChange={currentAgentId === 'catty' ? handleCattyThinkingSelect : undefined}
|
|
files={files}
|
|
onAddFiles={addFiles}
|
|
onRemoveFile={removeFile}
|
|
hosts={mentionHosts}
|
|
notes={notes}
|
|
onMentionNote={onMentionNote}
|
|
selectedUserSkills={selectedUserSkills}
|
|
userSkills={userSkillOptions}
|
|
quickMessages={quickMessages}
|
|
onAddUserSkill={addSelectedUserSkill}
|
|
onRemoveUserSkill={removeSelectedUserSkill}
|
|
permissionMode={globalPermissionMode}
|
|
onPermissionModeChange={setGlobalPermissionMode}
|
|
contextUsage={contextUsage}
|
|
/>
|
|
</div>
|
|
</React.Profiler>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
};
|