mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
feat(x/renderer): cut the active chat over to the sessions runtime (stage 5b)
The active chat tab now renders from useSessionChat; background tabs keep the legacy cached view state until stage 7 retires it. - Send path: sessions:create + sessions:sendMessage with the structural UserMessage (userMessageContext carries datetime + middle-pane — the cache-safe per-message tier) and per-message turn config (model override + composition: workDirId/voice/search/codeMode, permissionMode -> autoPermission). Attachments/mentions ride as real content parts — the old 'as unknown as string' cast is gone. - Stop / permission / ask-human handlers route through the hook against the latest turn; permission scope is passed as audit metadata (session grants are a deferred v2 feature). Stop state clears when the turn settles. - Chat history lists sessions (sessions:list/delete); selecting one just switches the active session — the hook loads the conversation, so the 200-line run-log replay in loadRun is gone. - Work-dir persistence unchanged: the per-chat sidecar file is keyed by the session id, which the resolver reads via composition.workDirId. Renderer tsc/vite build clean; all 324 workspace tests pass; renderer lint identical to baseline (108 pre-existing findings, none in new code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
df607ea510
commit
63917df061
1 changed files with 113 additions and 264 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useState, useRef } from 'react'
|
||||
import { workspace } from '@x/shared';
|
||||
import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js';
|
||||
import { RunEvent } from '@x/shared/src/runs.js';
|
||||
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
||||
import './App.css'
|
||||
import z from 'zod';
|
||||
|
|
@ -9,6 +9,7 @@ import { CheckIcon, LoaderIcon, PanelLeftIcon, ArrowLeft, ArrowRight, MessageSqu
|
|||
import { cn } from '@/lib/utils';
|
||||
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
||||
import { ChatSidebar } from './components/chat-sidebar';
|
||||
import { useSessionChat } from '@/hooks/useSessionChat';
|
||||
import { ChatHeader } from './components/chat-header';
|
||||
import { ChatEmptyState } from './components/chat-empty-state';
|
||||
import { ChatInputWithMentions, type PermissionMode, type StagedAttachment } from './components/chat-input-with-mentions';
|
||||
|
|
@ -96,7 +97,6 @@ import {
|
|||
type ChatViewportAnchorState,
|
||||
type ChatTabViewState,
|
||||
type ConversationItem,
|
||||
type ToolCall,
|
||||
createEmptyChatTabViewState,
|
||||
getWebSearchCardData,
|
||||
getAppActionCardData,
|
||||
|
|
@ -126,7 +126,6 @@ import { useTheme } from '@/contexts/theme-context'
|
|||
|
||||
type DirEntry = z.infer<typeof workspace.DirEntry>
|
||||
type RunEventType = z.infer<typeof RunEvent>
|
||||
type ListRunsResponseType = z.infer<typeof ListRunsResponse>
|
||||
|
||||
interface TreeNode extends DirEntry {
|
||||
children?: TreeNode[]
|
||||
|
|
@ -938,6 +937,10 @@ function App() {
|
|||
}, [conversation])
|
||||
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
|
||||
const [runId, setRunId] = useState<string | null>(null)
|
||||
// New runtime: the active session's chat data + actions. All logic lives in
|
||||
// SessionChatStore (tested headlessly); the hook is a thin subscription.
|
||||
// runId IS the session id in the sessions runtime.
|
||||
const sessionChat = useSessionChat(runId)
|
||||
const runIdRef = useRef<string | null>(null)
|
||||
const loadRunRequestIdRef = useRef(0)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
|
@ -945,7 +948,7 @@ function App() {
|
|||
const processingRunIdsRef = useRef<Set<string>>(new Set())
|
||||
const streamingBuffersRef = useRef<Map<string, { assistant: string }>>(new Map())
|
||||
const [isStopping, setIsStopping] = useState(false)
|
||||
const [stopClickedAt, setStopClickedAt] = useState<number | null>(null)
|
||||
const [, setStopClickedAt] = useState<number | null>(null)
|
||||
const [agentId] = useState<string>('copilot')
|
||||
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
|
|
@ -1960,21 +1963,16 @@ function App() {
|
|||
// Load runs list (all pages)
|
||||
const loadRuns = useCallback(async () => {
|
||||
try {
|
||||
const allRuns: RunListItem[] = []
|
||||
let cursor: string | undefined = undefined
|
||||
|
||||
// Fetch all pages
|
||||
do {
|
||||
const result: ListRunsResponseType = await window.ipc.invoke('runs:list', { cursor })
|
||||
allRuns.push(...result.runs)
|
||||
cursor = result.nextCursor
|
||||
} while (cursor)
|
||||
|
||||
// Filter for copilot chats only (Code-section sessions live in the Code view)
|
||||
const copilotRuns = allRuns.filter((run: RunListItem) => run.agentId === 'copilot' && run.useCase !== 'code_session')
|
||||
setRuns(copilotRuns)
|
||||
const { sessions } = await window.ipc.invoke('sessions:list', {})
|
||||
setRuns(sessions.map((entry) => ({
|
||||
id: entry.sessionId,
|
||||
title: entry.title ?? 'New chat',
|
||||
createdAt: entry.createdAt,
|
||||
modifiedAt: entry.updatedAt,
|
||||
agentId: entry.lastAgentId ?? 'copilot',
|
||||
})))
|
||||
} catch (err) {
|
||||
console.error('Failed to load runs:', err)
|
||||
console.error('Failed to load sessions:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
@ -2073,193 +2071,30 @@ function App() {
|
|||
}
|
||||
}, [backgroundTasks, loadBackgroundTasks])
|
||||
|
||||
// Load a specific run and populate conversation
|
||||
// Switch the active session. The useSessionChat hook loads and follows the
|
||||
// conversation; this only resets composer/tab state.
|
||||
const loadRun = useCallback(async (id: string) => {
|
||||
const requestId = (loadRunRequestIdRef.current += 1)
|
||||
setConversation([])
|
||||
setCurrentAssistantMessage('')
|
||||
setRunId(id)
|
||||
setMessage('')
|
||||
setIsProcessing(false)
|
||||
setIsStopping(false)
|
||||
setStopClickedAt(null)
|
||||
setPendingPermissionRequests(new Map())
|
||||
setPendingAskHumanRequests(new Map())
|
||||
setAllPermissionRequests(new Map())
|
||||
setPermissionResponses(new Map())
|
||||
setAutoPermissionDecisions(new Map())
|
||||
try {
|
||||
const run = await window.ipc.invoke('runs:fetch', { runId: id })
|
||||
if (loadRunRequestIdRef.current !== requestId) return
|
||||
|
||||
// Parse the log events into conversation items
|
||||
const items: ConversationItem[] = []
|
||||
const toolCallMap = new Map<string, ToolCall>()
|
||||
|
||||
for (const event of run.log) {
|
||||
switch (event.type) {
|
||||
case 'message': {
|
||||
const msg = event.message
|
||||
if (msg.role === 'user' || msg.role === 'assistant') {
|
||||
// Extract text content from message
|
||||
let textContent = ''
|
||||
let msgAttachments: ChatMessage['attachments'] = undefined
|
||||
if (typeof msg.content === 'string') {
|
||||
textContent = msg.content
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
const contentParts = msg.content as Array<{
|
||||
type: string
|
||||
text?: string
|
||||
path?: string
|
||||
filename?: string
|
||||
mimeType?: string
|
||||
size?: number
|
||||
toolCallId?: string
|
||||
toolName?: string
|
||||
arguments?: ToolUIPart['input']
|
||||
}>
|
||||
|
||||
textContent = contentParts
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => part.text || '')
|
||||
.join('')
|
||||
|
||||
const attachmentParts = contentParts.filter((part) => part.type === 'attachment' && part.path)
|
||||
if (attachmentParts.length > 0) {
|
||||
msgAttachments = attachmentParts.map((part) => ({
|
||||
path: part.path!,
|
||||
filename: part.filename || part.path!.split('/').pop() || part.path!,
|
||||
mimeType: part.mimeType || 'application/octet-stream',
|
||||
size: part.size,
|
||||
}))
|
||||
}
|
||||
|
||||
// Also extract tool-call parts from assistant messages
|
||||
if (msg.role === 'assistant') {
|
||||
for (const part of contentParts) {
|
||||
if (part.type === 'tool-call' && part.toolCallId && part.toolName) {
|
||||
const toolCall: ToolCall = {
|
||||
id: part.toolCallId,
|
||||
name: part.toolName,
|
||||
input: normalizeToolInput(part.arguments),
|
||||
status: 'pending',
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
}
|
||||
toolCallMap.set(toolCall.id, toolCall)
|
||||
items.push(toolCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (textContent || msgAttachments) {
|
||||
items.push({
|
||||
id: event.messageId,
|
||||
role: msg.role,
|
||||
content: textContent,
|
||||
attachments: msgAttachments,
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'tool-invocation': {
|
||||
// Update existing tool call status or create new one
|
||||
const existingTool = event.toolCallId ? toolCallMap.get(event.toolCallId) : null
|
||||
if (existingTool) {
|
||||
existingTool.input = normalizeToolInput(event.input)
|
||||
existingTool.status = 'running'
|
||||
} else {
|
||||
const toolCall: ToolCall = {
|
||||
id: event.toolCallId || `tool-${Date.now()}-${Math.random()}`,
|
||||
name: event.toolName,
|
||||
input: normalizeToolInput(event.input),
|
||||
status: 'running',
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
}
|
||||
toolCallMap.set(toolCall.id, toolCall)
|
||||
items.push(toolCall)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'tool-result': {
|
||||
const existingTool = event.toolCallId ? toolCallMap.get(event.toolCallId) : null
|
||||
if (existingTool) {
|
||||
existingTool.result = event.result
|
||||
existingTool.status = 'completed'
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'error': {
|
||||
items.push({
|
||||
id: `error-${Date.now()}-${Math.random()}`,
|
||||
kind: 'error',
|
||||
message: event.error,
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'llm-stream-event': {
|
||||
// We don't need to reconstruct streaming events for history
|
||||
// Reasoning is captured in the final message
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loadRunRequestIdRef.current !== requestId) return
|
||||
|
||||
// Track permission requests and responses from history
|
||||
const allPermissionRequests = new Map<string, z.infer<typeof ToolPermissionRequestEvent>>()
|
||||
const permResponseMap = new Map<string, 'approve' | 'deny'>()
|
||||
const autoPermissionDecisions = new Map<string, z.infer<typeof ToolPermissionAutoDecisionEvent>>()
|
||||
const askHumanRequests = new Map<string, z.infer<typeof AskHumanRequestEvent>>()
|
||||
const respondedAskHumanIds = new Set<string>()
|
||||
|
||||
for (const event of run.log) {
|
||||
if (event.type === 'tool-permission-request') {
|
||||
allPermissionRequests.set(event.toolCall.toolCallId, event)
|
||||
} else if (event.type === 'tool-permission-response') {
|
||||
permResponseMap.set(event.toolCallId, event.response)
|
||||
} else if (event.type === 'tool-permission-auto-decision') {
|
||||
autoPermissionDecisions.set(event.toolCallId, event)
|
||||
} else if (event.type === 'ask-human-request') {
|
||||
askHumanRequests.set(event.toolCallId, event)
|
||||
} else if (event.type === 'ask-human-response') {
|
||||
respondedAskHumanIds.add(event.toolCallId)
|
||||
}
|
||||
}
|
||||
if (loadRunRequestIdRef.current !== requestId) return
|
||||
|
||||
// Separate pending vs responded permission requests
|
||||
const pendingPerms = new Map<string, z.infer<typeof ToolPermissionRequestEvent>>()
|
||||
for (const [id, req] of allPermissionRequests.entries()) {
|
||||
if (!permResponseMap.has(id)) {
|
||||
pendingPerms.set(id, req)
|
||||
}
|
||||
}
|
||||
|
||||
const pendingAsks = new Map<string, z.infer<typeof AskHumanRequestEvent>>()
|
||||
for (const [id, req] of askHumanRequests.entries()) {
|
||||
if (!respondedAskHumanIds.has(id)) {
|
||||
pendingAsks.set(id, req)
|
||||
}
|
||||
}
|
||||
if (loadRunRequestIdRef.current !== requestId) return
|
||||
|
||||
// Set the conversation and runId
|
||||
setConversation(items)
|
||||
setRunId(id)
|
||||
setMessage('')
|
||||
// Reconcile composer state with THIS run. Loading a run while another one
|
||||
// is mid-turn (e.g. binding a code session steals the single chat tab)
|
||||
// must not leave isProcessing/isStopping pointing at the old run — that
|
||||
// wedges the composer: stop targets the new run (a no-op) while the old
|
||||
// run's processing-end arrives flagged as non-active and clears nothing.
|
||||
setIsProcessing(processingRunIdsRef.current.has(id))
|
||||
setIsStopping(false)
|
||||
setStopClickedAt(null)
|
||||
setCurrentAssistantMessage(streamingBuffersRef.current.get(id)?.assistant ?? '')
|
||||
setPendingPermissionRequests(pendingPerms)
|
||||
setPendingAskHumanRequests(pendingAsks)
|
||||
setAllPermissionRequests(allPermissionRequests)
|
||||
setPermissionResponses(permResponseMap)
|
||||
setAutoPermissionDecisions(autoPermissionDecisions)
|
||||
|
||||
// Restore the run's per-chat work directory into the tab it was loaded into.
|
||||
// Restore the session's per-chat work directory into the active tab.
|
||||
const tabId = activeChatTabIdRef.current
|
||||
const wd = await loadRunWorkDir(id)
|
||||
if (loadRunRequestIdRef.current !== requestId) return
|
||||
setWorkDirByTab((prev) => ({ ...prev, [tabId]: wd }))
|
||||
} catch (err) {
|
||||
console.error('Failed to load run:', err)
|
||||
console.error('Failed to load session work dir:', err)
|
||||
}
|
||||
}, [loadRunWorkDir])
|
||||
|
||||
|
|
@ -2743,15 +2578,11 @@ function App() {
|
|||
let currentRunId = runId
|
||||
let isNewRun = false
|
||||
let newRunCreatedAt: string | null = null
|
||||
const selected = selectedModelByTabRef.current.get(submitTabId)
|
||||
if (!currentRunId) {
|
||||
const selected = selectedModelByTabRef.current.get(submitTabId)
|
||||
const run = await window.ipc.invoke('runs:create', {
|
||||
agentId,
|
||||
...(selected ? { model: selected.model, provider: selected.provider } : {}),
|
||||
permissionMode: permissionMode ?? 'manual',
|
||||
})
|
||||
currentRunId = run.id
|
||||
newRunCreatedAt = run.createdAt
|
||||
const createdSession = await window.ipc.invoke('sessions:create', {})
|
||||
currentRunId = createdSession.sessionId
|
||||
newRunCreatedAt = new Date().toISOString()
|
||||
setRunId(currentRunId)
|
||||
analytics.chatSessionCreated(currentRunId)
|
||||
// Update active chat tab's runId to the new run
|
||||
|
|
@ -2770,6 +2601,30 @@ function App() {
|
|||
let titleSource = userMessage
|
||||
const hasMentions = (mentions?.length ?? 0) > 0
|
||||
|
||||
// Per-message turn config. Composition inputs land in the system prompt
|
||||
// via the agent resolver; keep them session-sticky where possible so the
|
||||
// provider prefix cache survives across turns.
|
||||
const sendConfig = {
|
||||
agent: {
|
||||
agentId,
|
||||
overrides: {
|
||||
...(selected ? { model: { provider: selected.provider, model: selected.model } } : {}),
|
||||
composition: {
|
||||
workDirId: currentRunId,
|
||||
...(pendingVoiceInputRef.current ? { voiceInput: true } : {}),
|
||||
...(ttsEnabledRef.current ? { voiceOutput: ttsModeRef.current } : {}),
|
||||
...(searchEnabled ? { searchEnabled: true } : {}),
|
||||
...(codeMode ? { codeMode } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
autoPermission: (permissionMode ?? 'manual') === 'auto',
|
||||
}
|
||||
const userMessageContextFor = (middlePane: Awaited<ReturnType<typeof buildMiddlePaneContext>>) => ({
|
||||
currentDateTime: new Date().toISOString(),
|
||||
middlePane: middlePane ?? { kind: 'empty' as const },
|
||||
})
|
||||
|
||||
if (hasAttachments || hasMentions) {
|
||||
type ContentPart =
|
||||
| { type: 'text'; text: string }
|
||||
|
|
@ -2812,17 +2667,15 @@ function App() {
|
|||
titleSource = stagedAttachments[0]?.filename ?? mentions?.[0]?.displayName ?? mentions?.[0]?.path ?? ''
|
||||
}
|
||||
|
||||
// Shared IPC payload types can lag until package rebuilds; runtime validation still enforces schema.
|
||||
const attachmentPayload = contentParts as unknown as string
|
||||
const middlePaneContext = await buildMiddlePaneContext()
|
||||
await window.ipc.invoke('runs:createMessage', {
|
||||
runId: currentRunId,
|
||||
message: attachmentPayload,
|
||||
voiceInput: pendingVoiceInputRef.current || undefined,
|
||||
voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined,
|
||||
searchEnabled: searchEnabled || undefined,
|
||||
codeMode: codeMode || undefined,
|
||||
middlePaneContext,
|
||||
await window.ipc.invoke('sessions:sendMessage', {
|
||||
sessionId: currentRunId,
|
||||
input: {
|
||||
role: 'user',
|
||||
content: contentParts,
|
||||
userMessageContext: userMessageContextFor(middlePaneContext),
|
||||
},
|
||||
config: sendConfig,
|
||||
})
|
||||
analytics.chatMessageSent({
|
||||
voiceInput: pendingVoiceInputRef.current || undefined,
|
||||
|
|
@ -2831,14 +2684,14 @@ function App() {
|
|||
})
|
||||
} else {
|
||||
const middlePaneContext = await buildMiddlePaneContext()
|
||||
await window.ipc.invoke('runs:createMessage', {
|
||||
runId: currentRunId,
|
||||
message: userMessage,
|
||||
voiceInput: pendingVoiceInputRef.current || undefined,
|
||||
voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined,
|
||||
searchEnabled: searchEnabled || undefined,
|
||||
codeMode: codeMode || undefined,
|
||||
middlePaneContext,
|
||||
await window.ipc.invoke('sessions:sendMessage', {
|
||||
sessionId: currentRunId,
|
||||
input: {
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
userMessageContext: userMessageContextFor(middlePaneContext),
|
||||
},
|
||||
config: sendConfig,
|
||||
})
|
||||
analytics.chatMessageSent({
|
||||
voiceInput: pendingVoiceInputRef.current || undefined,
|
||||
|
|
@ -2875,20 +2728,34 @@ function App() {
|
|||
handlePromptSubmitRef.current?.({ text: `${name} connected successfully.`, files: [] })
|
||||
}, [])
|
||||
|
||||
// Active tab renders from the sessions hook; background tabs keep the
|
||||
// legacy cached view state until stage 7 retires it.
|
||||
const chatTabStatesForRender = React.useMemo(() => {
|
||||
if (!sessionChat.chatState) return chatViewStateByTab
|
||||
return {
|
||||
...chatViewStateByTab,
|
||||
[activeChatTabId]: { runId, ...sessionChat.chatState },
|
||||
}
|
||||
}, [chatViewStateByTab, sessionChat.chatState, activeChatTabId, runId])
|
||||
|
||||
// The composer's stop state clears when the active turn settles.
|
||||
useEffect(() => {
|
||||
if (sessionChat.chatState && !sessionChat.chatState.isProcessing) {
|
||||
setIsStopping(false)
|
||||
setStopClickedAt(null)
|
||||
}
|
||||
}, [sessionChat.chatState])
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (!runId) return
|
||||
const now = Date.now()
|
||||
const isForce = isStopping && stopClickedAt !== null && (now - stopClickedAt) < 2000
|
||||
|
||||
setStopClickedAt(now)
|
||||
setStopClickedAt(Date.now())
|
||||
setIsStopping(true)
|
||||
|
||||
try {
|
||||
await window.ipc.invoke('runs:stop', { runId, force: isForce })
|
||||
await sessionChat.stop()
|
||||
} catch (error) {
|
||||
console.error('Failed to stop run:', error)
|
||||
console.error('Failed to stop turn:', error)
|
||||
}
|
||||
}, [runId, isStopping, stopClickedAt])
|
||||
}, [runId, sessionChat])
|
||||
|
||||
const handlePermissionResponse = useCallback(async (
|
||||
toolCallId: string,
|
||||
|
|
@ -2898,33 +2765,17 @@ function App() {
|
|||
) => {
|
||||
if (!runId) return
|
||||
|
||||
// Optimistically update the UI immediately
|
||||
setPermissionResponses(prev => {
|
||||
const next = new Map(prev)
|
||||
next.set(toolCallId, response)
|
||||
return next
|
||||
})
|
||||
setPendingPermissionRequests(prev => {
|
||||
const next = new Map(prev)
|
||||
next.delete(toolCallId)
|
||||
return next
|
||||
})
|
||||
|
||||
void subflow // subflows retired with the runs runtime
|
||||
try {
|
||||
await window.ipc.invoke('runs:authorizePermission', {
|
||||
runId,
|
||||
authorization: { subflow, toolCallId, response, scope }
|
||||
})
|
||||
await sessionChat.respondToPermission(
|
||||
toolCallId,
|
||||
response === 'approve' ? 'allow' : 'deny',
|
||||
scope ? { scope } : undefined,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to authorize permission:', error)
|
||||
// Revert the optimistic update on error
|
||||
setPermissionResponses(prev => {
|
||||
const next = new Map(prev)
|
||||
next.delete(toolCallId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}, [runId])
|
||||
}, [runId, sessionChat])
|
||||
|
||||
// Answer a mid-run permission request from a code_agent_run coding turn. The
|
||||
// pending ask lives on the tool call itself, so we optimistically clear it and
|
||||
|
|
@ -2948,15 +2799,13 @@ function App() {
|
|||
|
||||
const handleAskHumanResponse = useCallback(async (toolCallId: string, subflow: string[], response: string) => {
|
||||
if (!runId) return
|
||||
void subflow // subflows retired with the runs runtime
|
||||
try {
|
||||
await window.ipc.invoke('runs:provideHumanInput', {
|
||||
runId,
|
||||
reply: { subflow, toolCallId, response }
|
||||
})
|
||||
await sessionChat.answerAskHuman(toolCallId, response)
|
||||
} catch (error) {
|
||||
console.error('Failed to provide human input:', error)
|
||||
}
|
||||
}, [runId])
|
||||
}, [runId, sessionChat])
|
||||
|
||||
const dismissBrowserOverlay = useCallback(() => {
|
||||
setIsBrowserOpen(false)
|
||||
|
|
@ -6007,7 +5856,7 @@ function App() {
|
|||
onSelectRun={(rid) => void navigateToView({ type: 'chat', runId: rid })}
|
||||
onDeleteRun={async (rid) => {
|
||||
try {
|
||||
await window.ipc.invoke('runs:delete', { runId: rid })
|
||||
await window.ipc.invoke('sessions:delete', { sessionId: rid })
|
||||
await loadRuns()
|
||||
} catch (err) {
|
||||
console.error('Failed to delete run:', err)
|
||||
|
|
@ -6454,11 +6303,11 @@ function App() {
|
|||
}}
|
||||
onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })}
|
||||
onOpenFullScreen={toggleRightPaneMaximize}
|
||||
conversation={conversation}
|
||||
currentAssistantMessage={currentAssistantMessage}
|
||||
chatTabStates={chatViewStateByTab}
|
||||
conversation={sessionChat.chatState?.conversation ?? conversation}
|
||||
currentAssistantMessage={sessionChat.chatState?.currentAssistantMessage ?? currentAssistantMessage}
|
||||
chatTabStates={chatTabStatesForRender}
|
||||
viewportAnchors={chatViewportAnchorByTab}
|
||||
isProcessing={isProcessing}
|
||||
isProcessing={sessionChat.chatState ? sessionChat.chatState.isProcessing : isProcessing}
|
||||
isStopping={isStopping}
|
||||
onStop={handleStop}
|
||||
onSubmit={handlePromptSubmit}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue