diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4d77e6..c7a45cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [3.13.1](https://github.com/breaking-brake/cc-wf-studio/compare/v3.13.0...v3.13.1) (2026-01-09) + +### Improvements + +* add session reconnection warning dialog for AI refinement ([#410](https://github.com/breaking-brake/cc-wf-studio/issues/410)) ([c19aa3c](https://github.com/breaking-brake/cc-wf-studio/commit/c19aa3ce526120b22e3516dba88a65e2ad424eac)) + ## [3.13.0](https://github.com/breaking-brake/cc-wf-studio/compare/v3.12.10...v3.13.0) (2026-01-09) ### Features diff --git a/package-lock.json b/package-lock.json index 6c81a096..5b3ba8dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-wf-studio", - "version": "3.13.0", + "version": "3.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio", - "version": "3.13.0", + "version": "3.13.1", "license": "AGPL-3.0-or-later", "dependencies": { "@modelcontextprotocol/sdk": "^1.25.2", diff --git a/package.json b/package.json index 80f83a8d..d88a22f1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cc-wf-studio", "displayName": "Claude Code Workflow Studio", "description": "Visual workflow editor for Claude Code Slash Commands, Sub Agents, Agent Skills, and MCP Tools", - "version": "3.13.0", + "version": "3.13.1", "publisher": "breaking-brake", "icon": "resources/icon.png", "repository": { diff --git a/src/extension/commands/workflow-refinement.ts b/src/extension/commands/workflow-refinement.ts index 6736af11..efbeae62 100644 --- a/src/extension/commands/workflow-refinement.ts +++ b/src/extension/commands/workflow-refinement.ts @@ -191,6 +191,7 @@ export async function handleRefineWorkflow( updatedConversationHistory: updatedHistory, executionTimeMs: result.executionTimeMs, timestamp: new Date().toISOString(), + sessionReconnected: result.sessionReconnected, }); return; } @@ -261,6 +262,7 @@ export async function handleRefineWorkflow( updatedConversationHistory: updatedHistory, executionTimeMs: result.executionTimeMs, timestamp: new Date().toISOString(), + sessionReconnected: result.sessionReconnected, }); } catch (error) { const executionTimeMs = Date.now() - startTime; @@ -451,6 +453,7 @@ async function handleRefineSubAgentFlow( updatedConversationHistory: updatedHistory, executionTimeMs: result.executionTimeMs, timestamp: new Date().toISOString(), + sessionReconnected: result.sessionReconnected, }); return; } @@ -522,6 +525,7 @@ async function handleRefineSubAgentFlow( updatedConversationHistory: updatedHistory, executionTimeMs: result.executionTimeMs, timestamp: new Date().toISOString(), + sessionReconnected: result.sessionReconnected, }); } catch (error) { const executionTimeMs = Date.now() - startTime; diff --git a/src/extension/services/refinement-service.ts b/src/extension/services/refinement-service.ts index 2d47687f..d25391a3 100644 --- a/src/extension/services/refinement-service.ts +++ b/src/extension/services/refinement-service.ts @@ -55,6 +55,8 @@ export interface RefinementResult { executionTimeMs: number; /** New session ID from CLI (for session continuation) */ newSessionId?: string; + /** Whether session was reconnected due to session expiration (fallback occurred) */ + sessionReconnected?: boolean; } /** @@ -386,6 +388,9 @@ export async function refineWorkflow( allowedTools ); + // Track whether session was reconnected due to fallback + let sessionReconnected = false; + // Fallback: Retry without session ID if session resume failed if (!cliResult.success && conversationHistory.sessionId) { const errorDetails = cliResult.error?.details?.toLowerCase() || ''; @@ -395,6 +400,8 @@ export async function refineWorkflow( 'session expired', 'invalid session', 'no such session', + 'no conversation found with session id', + 'not a valid uuid', ].some((pattern) => errorDetails.includes(pattern) || errorMessage.includes(pattern)); if (isSessionError) { @@ -405,6 +412,9 @@ export async function refineWorkflow( errorMessage: cliResult.error?.message, }); + // Mark that session reconnection occurred + sessionReconnected = true; + // Retry without session ID cliResult = onProgress ? await executeClaudeCodeCLIStreaming( @@ -428,6 +438,22 @@ export async function refineWorkflow( } } + // Detect silent session switch (CLI started new session without returning error) + // This happens when CLI-side session was cleared (e.g., via /clear command) + if ( + cliResult.success && + conversationHistory.sessionId && + cliResult.sessionId && + cliResult.sessionId !== conversationHistory.sessionId + ) { + log('WARN', 'Session was silently replaced by CLI', { + requestId, + previousSessionId: conversationHistory.sessionId, + newSessionId: cliResult.sessionId, + }); + sessionReconnected = true; + } + if (!cliResult.success || !cliResult.output) { // CLI execution failed - record metrics if (collectMetrics) { @@ -460,6 +486,7 @@ export async function refineWorkflow( }, executionTimeMs: cliResult.executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } @@ -489,6 +516,7 @@ export async function refineWorkflow( }, executionTimeMs: cliResult.executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } @@ -505,6 +533,7 @@ export async function refineWorkflow( clarificationMessage: aiResponse.message || 'Please provide more details', executionTimeMs: cliResult.executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } @@ -524,6 +553,7 @@ export async function refineWorkflow( }, executionTimeMs: cliResult.executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } @@ -544,6 +574,7 @@ export async function refineWorkflow( }, executionTimeMs: cliResult.executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } @@ -577,6 +608,7 @@ export async function refineWorkflow( }, executionTimeMs: cliResult.executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } @@ -625,6 +657,7 @@ export async function refineWorkflow( })), executionTimeMs: cliResult.executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } @@ -660,6 +693,7 @@ export async function refineWorkflow( aiMessage: aiResponse.message, executionTimeMs, newSessionId: cliResult.sessionId, + sessionReconnected, }; } catch (error) { const executionTimeMs = Date.now() - startTime; @@ -876,6 +910,10 @@ export interface SubAgentFlowRefinementResult { details?: string; }; executionTimeMs: number; + /** New session ID from CLI (for session continuation) */ + newSessionId?: string; + /** Whether session was reconnected due to session expiration (fallback occurred) */ + sessionReconnected?: boolean; } /** @@ -1268,6 +1306,8 @@ export async function refineSubAgentFlow( success: true, clarificationMessage: aiResponse.message || 'Please provide more details', executionTimeMs: cliResult.executionTimeMs, + newSessionId: cliResult.sessionId, + sessionReconnected: false, }; } @@ -1409,6 +1449,8 @@ export async function refineSubAgentFlow( refinedInnerWorkflow, aiMessage: aiResponse.message, executionTimeMs, + newSessionId: cliResult.sessionId, + sessionReconnected: false, // SubAgentFlow doesn't have session continuation yet }; } catch (error) { const executionTimeMs = Date.now() - startTime; diff --git a/src/shared/types/messages.ts b/src/shared/types/messages.ts index 758e60d9..93a835ef 100644 --- a/src/shared/types/messages.ts +++ b/src/shared/types/messages.ts @@ -308,6 +308,8 @@ export interface RefinementSuccessPayload { executionTimeMs: number; /** Response timestamp */ timestamp: string; // ISO 8601 + /** Whether session was reconnected due to session expiration (fallback occurred) */ + sessionReconnected?: boolean; } export interface RefinementFailedPayload { @@ -366,6 +368,8 @@ export interface RefinementClarificationPayload { executionTimeMs: number; /** Response timestamp */ timestamp: string; // ISO 8601 + /** Whether session was reconnected due to session expiration (fallback occurred) */ + sessionReconnected?: boolean; } export interface RefinementProgressPayload { @@ -401,6 +405,8 @@ export interface SubAgentFlowRefinementSuccessPayload { executionTimeMs: number; /** Response timestamp */ timestamp: string; // ISO 8601 + /** Whether session was reconnected due to session expiration (fallback occurred) */ + sessionReconnected?: boolean; } // ============================================================================ diff --git a/src/webview/package-lock.json b/src/webview/package-lock.json index cf36a1a1..eb476b28 100644 --- a/src/webview/package-lock.json +++ b/src/webview/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-wf-studio-webview", - "version": "3.13.0", + "version": "3.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio-webview", - "version": "3.13.0", + "version": "3.13.1", "license": "AGPL-3.0-or-later", "dependencies": { "@radix-ui/react-collapsible": "^1.1.12", diff --git a/src/webview/package.json b/src/webview/package.json index bde5f4ff..20f464ee 100644 --- a/src/webview/package.json +++ b/src/webview/package.json @@ -1,6 +1,6 @@ { "name": "cc-wf-studio-webview", - "version": "3.13.0", + "version": "3.13.1", "private": true, "license": "AGPL-3.0-or-later", "type": "module", diff --git a/src/webview/src/App.tsx b/src/webview/src/App.tsx index c4b94f89..0af97872 100644 --- a/src/webview/src/App.tsx +++ b/src/webview/src/App.tsx @@ -72,6 +72,7 @@ const App: React.FC = () => { () => ({ conversationHistory: refinementStore.conversationHistory, isProcessing: refinementStore.isProcessing, + sessionStatus: refinementStore.sessionStatus, currentInput: refinementStore.currentInput, currentRequestId: refinementStore.currentRequestId, setInput: refinementStore.setInput, diff --git a/src/webview/src/components/dialogs/AlertDialog.tsx b/src/webview/src/components/dialogs/AlertDialog.tsx new file mode 100644 index 00000000..7911fdc5 --- /dev/null +++ b/src/webview/src/components/dialogs/AlertDialog.tsx @@ -0,0 +1,126 @@ +/** + * AlertDialog Component + * + * シンプルな警告ダイアログコンポーネント + * OKボタンのみで、ユーザーに情報を通知する用途 + * Radix UI Dialogを使用 + */ + +import * as Dialog from '@radix-ui/react-dialog'; +import type React from 'react'; + +interface AlertDialogProps { + isOpen: boolean; + title: string; + message: string; + okLabel: string; + onClose: () => void; + /** Optional icon to display before title */ + icon?: React.ReactNode; +} + +/** + * 警告ダイアログコンポーネント + */ +export const AlertDialog: React.FC = ({ + isOpen, + title, + message, + okLabel, + onClose, + icon, +}) => { + return ( + !open && onClose()}> + + + + {/* Title with optional icon */} + + {icon} + {title} + + + {/* Message */} + + {message} + + + {/* OK Button */} +
+ +
+
+
+
+
+ ); +}; + +export default AlertDialog; diff --git a/src/webview/src/components/dialogs/RefinementChatPanel.tsx b/src/webview/src/components/dialogs/RefinementChatPanel.tsx index 8866b9c0..0408fb57 100644 --- a/src/webview/src/components/dialogs/RefinementChatPanel.tsx +++ b/src/webview/src/components/dialogs/RefinementChatPanel.tsx @@ -33,6 +33,7 @@ import { MessageList } from '../chat/MessageList'; import { SettingsDropdown } from '../chat/SettingsDropdown'; import { WarningBanner } from '../chat/WarningBanner'; import { ResizeHandle } from '../common/ResizeHandle'; +import { AlertDialog } from './AlertDialog'; import { ConfirmDialog } from './ConfirmDialog'; // Resizable panel configuration @@ -75,6 +76,7 @@ export function RefinementChatPanel({ const { conversationHistory, isProcessing, + sessionStatus, addUserMessage, addLoadingAiMessage, updateMessageContent, @@ -97,6 +99,10 @@ export function RefinementChatPanel({ useWorkflowStore(); const [isConfirmClearOpen, setIsConfirmClearOpen] = useState(false); + const [isSessionWarningOpen, setIsSessionWarningOpen] = useState(false); + + // Track previous sessionStatus to detect changes to 'reconnected' + const prevSessionStatusRef = useRef(sessionStatus); // Store validation errors by message ID for retry with error context const validationErrorsRef = useRef>(new Map()); @@ -121,6 +127,14 @@ export function RefinementChatPanel({ return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose, isProcessing]); + // Show warning dialog when sessionStatus changes to 'reconnected' + useEffect(() => { + if (sessionStatus === 'reconnected' && prevSessionStatusRef.current !== 'reconnected') { + setIsSessionWarningOpen(true); + } + prevSessionStatusRef.current = sessionStatus; + }, [sessionStatus]); + // Handle sending refinement request const handleSend = async (message: string) => { if (!conversationHistory || !activeWorkflow) { @@ -253,8 +267,11 @@ export function RefinementChatPanel({ updateMessageLoadingState(completionMessageId, false); // Preserve frontend messages (don't overwrite with server history) - // Pass sessionId for session continuation support - finishProcessing(result.payload.updatedConversationHistory?.sessionId); + // Pass sessionId and sessionReconnected for session status tracking + finishProcessing( + result.payload.updatedConversationHistory?.sessionId, + result.payload.sessionReconnected + ); } else { // No streaming or no explanatory text: just show completion message updateMessageContent(aiMessageId, result.payload.aiMessage.content); @@ -272,8 +289,11 @@ export function RefinementChatPanel({ if (hasReceivedProgress) { // Streaming occurred, use finishProcessing to preserve frontend messages - // Pass sessionId for session continuation support - finishProcessing(result.payload.updatedConversationHistory?.sessionId); + // Pass sessionId and sessionReconnected for session status tracking + finishProcessing( + result.payload.updatedConversationHistory?.sessionId, + result.payload.sessionReconnected + ); } else { // No streaming, update conversation history normally handleRefinementSuccess( @@ -439,7 +459,10 @@ export function RefinementChatPanel({ // Preserve frontend messages (don't overwrite with server history) // Pass sessionId for session continuation support - finishProcessing(result.payload.updatedConversationHistory?.sessionId); + finishProcessing( + result.payload.updatedConversationHistory?.sessionId, + result.payload.sessionReconnected + ); } else { // No streaming or no explanatory text: just show completion message updateMessageContent(aiMessageId, result.payload.aiMessage.content); @@ -457,8 +480,11 @@ export function RefinementChatPanel({ if (hasReceivedProgress) { // Streaming occurred, use finishProcessing to preserve frontend messages - // Pass sessionId for session continuation support - finishProcessing(result.payload.updatedConversationHistory?.sessionId); + // Pass sessionId and sessionReconnected for session status tracking + finishProcessing( + result.payload.updatedConversationHistory?.sessionId, + result.payload.sessionReconnected + ); } else { // No streaming, update conversation history normally handleRefinementSuccess( @@ -584,19 +610,21 @@ export function RefinementChatPanel({ flexShrink: 0, }} > -

- {panelTitle} -

+
+

+ {panelTitle} +

+
+ + {/* Session Reconnection Warning Dialog */} + setIsSessionWarningOpen(false)} + icon={} + />
); diff --git a/src/webview/src/hooks/useLocalRefinementChatState.ts b/src/webview/src/hooks/useLocalRefinementChatState.ts index dc5e127b..2c36277e 100644 --- a/src/webview/src/hooks/useLocalRefinementChatState.ts +++ b/src/webview/src/hooks/useLocalRefinementChatState.ts @@ -7,6 +7,7 @@ import type { ConversationHistory, ConversationMessage } from '@shared/types/workflow-definition'; import { useCallback, useMemo, useState } from 'react'; +import type { SessionStatus } from '../stores/refinement-store'; import type { RefinementChatState, RefinementErrorCode } from '../types/refinement-chat-state'; interface UseLocalRefinementChatStateOptions { @@ -63,13 +64,17 @@ export function useLocalRefinementChatState( // Local state const [conversationHistory, setConversationHistory] = useState(null); const [isProcessing, setIsProcessing] = useState(false); + const [sessionStatus, setSessionStatus] = useState('none'); const [currentInput, setCurrentInput] = useState(''); const [currentRequestId, setCurrentRequestId] = useState(null); // Initialize or reset history const initializeHistory = useCallback((history: ConversationHistory | null) => { - setConversationHistory(history ?? createEmptyConversationHistory()); + const historyToUse = history ?? createEmptyConversationHistory(); + setConversationHistory(historyToUse); setIsProcessing(false); + // Set session status based on whether sessionId exists + setSessionStatus(historyToUse.sessionId ? 'connected' : 'none'); setCurrentInput(''); setCurrentRequestId(null); }, []); @@ -78,6 +83,7 @@ export function useLocalRefinementChatState( const reset = useCallback(() => { setConversationHistory(null); setIsProcessing(false); + setSessionStatus('none'); setCurrentInput(''); setCurrentRequestId(null); }, []); @@ -185,6 +191,7 @@ export function useLocalRefinementChatState( const clearHistory = useCallback(() => { setConversationHistory(createEmptyConversationHistory()); + setSessionStatus('none'); }, []); const startProcessing = useCallback((requestId: string) => { @@ -192,9 +199,23 @@ export function useLocalRefinementChatState( setCurrentRequestId(requestId); }, []); - const finishProcessing = useCallback(() => { + const finishProcessing = useCallback((sessionId?: string, sessionReconnected?: boolean) => { setIsProcessing(false); setCurrentRequestId(null); + + // Update session status and sessionId in history + if (sessionId) { + const newSessionStatus: SessionStatus = sessionReconnected ? 'reconnected' : 'connected'; + setSessionStatus(newSessionStatus); + setConversationHistory((prev) => { + if (!prev) return prev; + return { + ...prev, + sessionId, + updatedAt: new Date().toISOString(), + }; + }); + } }, []); const handleRefinementSuccess = useCallback( @@ -223,6 +244,7 @@ export function useLocalRefinementChatState( () => ({ conversationHistory, isProcessing, + sessionStatus, currentInput, currentRequestId, setInput: setCurrentInput, @@ -244,6 +266,7 @@ export function useLocalRefinementChatState( [ conversationHistory, isProcessing, + sessionStatus, currentInput, currentRequestId, canSend, diff --git a/src/webview/src/i18n/translation-keys.ts b/src/webview/src/i18n/translation-keys.ts index dea07aed..a8912d3f 100644 --- a/src/webview/src/i18n/translation-keys.ts +++ b/src/webview/src/i18n/translation-keys.ts @@ -416,6 +416,11 @@ export interface WebviewTranslationKeys { // Refinement Success Messages 'refinement.success.defaultMessage': string; + // Refinement Session Status + 'refinement.session.warningDialog.title': string; + 'refinement.session.warningDialog.message': string; + 'refinement.session.warningDialog.ok': string; + // Refinement Errors 'refinement.error.emptyMessage': string; 'refinement.error.messageTooLong': string; diff --git a/src/webview/src/i18n/translations/en.ts b/src/webview/src/i18n/translations/en.ts index 21268b14..c8100d99 100644 --- a/src/webview/src/i18n/translations/en.ts +++ b/src/webview/src/i18n/translations/en.ts @@ -458,6 +458,12 @@ export const enWebviewTranslations: WebviewTranslationKeys = { // Refinement Success Messages 'refinement.success.defaultMessage': 'Workflow has been updated.', + // Refinement Session Status + 'refinement.session.warningDialog.title': 'AI Editing Session Reconnected', + 'refinement.session.warningDialog.message': + 'The AI conversation session could not be continued due to reasons such as loading a workflow shared by others or session expiration, so a new conversation session was started.\n\nAdditional context that the AI remembered in the previous conversation session (file contents, tool execution results, etc.) may have been lost.\n\nPlease re-share any relevant information in your message if needed.', + 'refinement.session.warningDialog.ok': 'OK', + // Refinement Errors 'refinement.error.emptyMessage': 'Please enter a message', 'refinement.error.messageTooLong': 'Message is too long (max {max} characters)', diff --git a/src/webview/src/i18n/translations/ja.ts b/src/webview/src/i18n/translations/ja.ts index 437f6fd7..a5dc1be6 100644 --- a/src/webview/src/i18n/translations/ja.ts +++ b/src/webview/src/i18n/translations/ja.ts @@ -457,6 +457,12 @@ export const jaWebviewTranslations: WebviewTranslationKeys = { // Refinement Success Messages 'refinement.success.defaultMessage': 'ワークフローを編集しました。', + // Refinement Session Status + 'refinement.session.warningDialog.title': 'AI編集のセッションが再接続されました', + 'refinement.session.warningDialog.message': + '他者から共有されたワークフローの読み込みや、セッションの有効期限切れなどの理由で、AI会話セッションを継続できなかったため、新しい会話セッションを開始しました。\n\n前の会話セッションでAIが記憶していた追加のコンテキスト(ファイルの内容、ツール実行結果など)は失われている可能性があります。\n\n必要に応じて、関連する情報を改めてメッセージで伝えてください。', + 'refinement.session.warningDialog.ok': 'OK', + // Refinement Errors 'refinement.error.emptyMessage': 'メッセージを入力してください', 'refinement.error.messageTooLong': 'メッセージが長すぎます(最大{max}文字)', diff --git a/src/webview/src/i18n/translations/ko.ts b/src/webview/src/i18n/translations/ko.ts index a4c73fdb..3d18e575 100644 --- a/src/webview/src/i18n/translations/ko.ts +++ b/src/webview/src/i18n/translations/ko.ts @@ -458,6 +458,12 @@ export const koWebviewTranslations: WebviewTranslationKeys = { // Refinement Success Messages 'refinement.success.defaultMessage': '워크플로를 편집했습니다.', + // Refinement Session Status + 'refinement.session.warningDialog.title': 'AI 편집 세션이 재연결되었습니다', + 'refinement.session.warningDialog.message': + '다른 사용자가 공유한 워크플로우를 불러오거나 세션 만료 등의 이유로 AI 대화 세션을 계속할 수 없어 새 대화 세션을 시작했습니다.\n\n이전 대화 세션에서 AI가 기억하고 있던 추가 컨텍스트(파일 내용, 도구 실행 결과 등)는 손실되었을 수 있습니다.\n\n필요한 경우 관련 정보를 메시지로 다시 전달해 주세요.', + 'refinement.session.warningDialog.ok': 'OK', + // Refinement Errors 'refinement.error.emptyMessage': '메시지를 입력하세요', 'refinement.error.messageTooLong': '메시지가 너무 깁니다 (최대 {max}자)', diff --git a/src/webview/src/i18n/translations/zh-CN.ts b/src/webview/src/i18n/translations/zh-CN.ts index a1dd7e99..ded928f3 100644 --- a/src/webview/src/i18n/translations/zh-CN.ts +++ b/src/webview/src/i18n/translations/zh-CN.ts @@ -442,6 +442,12 @@ export const zhCNWebviewTranslations: WebviewTranslationKeys = { // Refinement Success Messages 'refinement.success.defaultMessage': '已编辑工作流。', + // Refinement Session Status + 'refinement.session.warningDialog.title': 'AI编辑会话已重新连接', + 'refinement.session.warningDialog.message': + '由于加载他人共享的工作流或会话过期等原因,无法继续AI对话会话,已开始新的对话会话。\n\n之前对话会话中AI记住的额外上下文(文件内容、工具执行结果等)可能已丢失。\n\n如有需要,请在消息中重新分享相关信息。', + 'refinement.session.warningDialog.ok': 'OK', + // Refinement Errors 'refinement.error.emptyMessage': '请输入消息', 'refinement.error.messageTooLong': '消息太长(最多{max}个字符)', diff --git a/src/webview/src/i18n/translations/zh-TW.ts b/src/webview/src/i18n/translations/zh-TW.ts index 12b9e1ae..7dc4e078 100644 --- a/src/webview/src/i18n/translations/zh-TW.ts +++ b/src/webview/src/i18n/translations/zh-TW.ts @@ -442,6 +442,12 @@ export const zhTWWebviewTranslations: WebviewTranslationKeys = { // Refinement Success Messages 'refinement.success.defaultMessage': '已編輯工作流程。', + // Refinement Session Status + 'refinement.session.warningDialog.title': 'AI編輯會話已重新連接', + 'refinement.session.warningDialog.message': + '由於載入他人共享的工作流程或會話過期等原因,無法繼續AI對話會話,已開始新的對話會話。\n\n之前對話會話中AI記住的額外上下文(檔案內容、工具執行結果等)可能已遺失。\n\n如有需要,請在訊息中重新分享相關資訊。', + 'refinement.session.warningDialog.ok': 'OK', + // Refinement Errors 'refinement.error.emptyMessage': '請輸入訊息', 'refinement.error.messageTooLong': '訊息太長(最多{max}個字元)', diff --git a/src/webview/src/stores/refinement-store.ts b/src/webview/src/stores/refinement-store.ts index e18ec36a..cdef8352 100644 --- a/src/webview/src/stores/refinement-store.ts +++ b/src/webview/src/stores/refinement-store.ts @@ -102,6 +102,18 @@ function saveAllowedToolsToStorage(tools: string[]): void { } } +// ============================================================================ +// Session Status Type +// ============================================================================ + +/** + * Session status for display in UI + * - 'none': No session (new conversation, no prior context) + * - 'connected': sessionId exists and is valid (session continuing) + * - 'reconnected': Session fallback occurred (previous session expired) + */ +export type SessionStatus = 'none' | 'connected' | 'reconnected'; + // ============================================================================ // Store State Interface // ============================================================================ @@ -118,6 +130,9 @@ interface RefinementStore { selectedModel: ClaudeModel; allowedTools: string[]; + // Session Status + sessionStatus: SessionStatus; + // SubAgentFlow Refinement State targetType: 'workflow' | 'subAgentFlow'; targetSubAgentFlowId: string | null; @@ -150,10 +165,22 @@ interface RefinementStore { * Finish processing without replacing conversation history. * Use this when frontend has already managed messages (e.g., streaming with explanatory text). * Optionally accepts sessionId to persist for session continuation. + * @param sessionId - New session ID from CLI + * @param sessionReconnected - Whether session fallback occurred */ - finishProcessing: (sessionId?: string) => void; + finishProcessing: (sessionId?: string, sessionReconnected?: boolean) => void; clearHistory: () => void; + // Session Status Actions + /** + * Set session status to 'reconnected' (called when session fallback occurred) + */ + setSessionReconnected: () => void; + /** + * Clear session status (called when history is cleared) + */ + clearSessionStatus: () => void; + // Phase 3.7: Message operations for loading state addLoadingAiMessage: (messageId: string) => void; updateMessageLoadingState: (messageId: string, isLoading: boolean) => void; @@ -181,6 +208,12 @@ interface RefinementStore { // Computed canSend: () => boolean; shouldShowWarning: () => boolean; + + // ============================================================================ + // DEBUG: Temporary sessionId editor for testing session reconnection + // TODO: Remove this before merging to main + // ============================================================================ + debugSetSessionId: (sessionId: string | undefined) => void; } // ============================================================================ @@ -202,6 +235,9 @@ export const useRefinementStore = create((set, get) => ({ selectedModel: loadModelFromStorage(), // Load from localStorage, default: 'haiku' allowedTools: loadAllowedToolsFromStorage(), // Load from localStorage, default: DEFAULT_ALLOWED_TOOLS + // Session Status Initial State + sessionStatus: 'none', + // SubAgentFlow Refinement Initial State targetType: 'workflow', targetSubAgentFlowId: null, @@ -265,10 +301,13 @@ export const useRefinementStore = create((set, get) => ({ loadConversationHistory: (history: ConversationHistory | undefined) => { if (history) { - set({ conversationHistory: history }); + // Set sessionStatus based on whether sessionId exists + const sessionStatus = history.sessionId ? 'connected' : 'none'; + set({ conversationHistory: history, sessionStatus }); } else { // Initialize new conversation if no history exists get().initConversation(); + set({ sessionStatus: 'none' }); } }, @@ -325,9 +364,13 @@ export const useRefinementStore = create((set, get) => ({ set({ isProcessing: false, currentRequestId: null }); }, - finishProcessing: (sessionId?: string) => { + finishProcessing: (sessionId?: string, sessionReconnected?: boolean) => { const history = get().conversationHistory; if (sessionId && history) { + // Determine session status + // If sessionReconnected is true, set to 'reconnected', otherwise 'connected' + const newSessionStatus = sessionReconnected ? 'reconnected' : 'connected'; + // Update sessionId in conversationHistory for session continuation set({ conversationHistory: { @@ -337,6 +380,7 @@ export const useRefinementStore = create((set, get) => ({ }, isProcessing: false, currentRequestId: null, + sessionStatus: newSessionStatus, }); } else { set({ isProcessing: false, currentRequestId: null }); @@ -354,10 +398,20 @@ export const useRefinementStore = create((set, get) => ({ updatedAt: new Date().toISOString(), sessionId: undefined, // Clear session for fresh start }, + sessionStatus: 'none', // Clear session status for fresh start }); } }, + // Session Status Actions + setSessionReconnected: () => { + set({ sessionStatus: 'reconnected' }); + }, + + clearSessionStatus: () => { + set({ sessionStatus: 'none' }); + }, + // Phase 3.7: Message operations addLoadingAiMessage: (messageId: string) => { const history = get().conversationHistory; @@ -520,4 +574,23 @@ export const useRefinementStore = create((set, get) => ({ // Show warning when 20 or more iterations have been completed return conversationHistory.currentIteration >= 20; }, + + // ============================================================================ + // DEBUG: Temporary sessionId editor for testing session reconnection + // TODO: Remove this before merging to main + // ============================================================================ + debugSetSessionId: (sessionId: string | undefined) => { + const history = get().conversationHistory; + if (history) { + console.log('[DEBUG] Setting sessionId:', { previous: history.sessionId, new: sessionId }); + set({ + conversationHistory: { + ...history, + sessionId: sessionId || undefined, + updatedAt: new Date().toISOString(), + }, + sessionStatus: sessionId ? 'connected' : 'none', + }); + } + }, })); diff --git a/src/webview/src/types/refinement-chat-state.ts b/src/webview/src/types/refinement-chat-state.ts index a7532c19..8afb1d42 100644 --- a/src/webview/src/types/refinement-chat-state.ts +++ b/src/webview/src/types/refinement-chat-state.ts @@ -6,6 +6,7 @@ */ import type { ConversationHistory, ConversationMessage } from '@shared/types/workflow-definition'; +import type { SessionStatus } from '../stores/refinement-store'; /** Error codes for refinement failures */ export type RefinementErrorCode = @@ -26,6 +27,7 @@ export interface RefinementChatState { // State conversationHistory: ConversationHistory | null; isProcessing: boolean; + sessionStatus: SessionStatus; currentInput: string; currentRequestId: string | null; @@ -45,7 +47,7 @@ export interface RefinementChatState { removeMessage: (messageId: string) => void; clearHistory: () => void; startProcessing: (requestId: string) => void; - finishProcessing: (sessionId?: string) => void; + finishProcessing: (sessionId?: string, sessionReconnected?: boolean) => void; handleRefinementSuccess: ( aiMessage: ConversationMessage, updatedHistory: ConversationHistory