Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type { FlowChatState, Session } from '../../../../../flow_chat/types/flow
import { useSceneStore } from '../../../../stores/sceneStore';
import { useApp } from '../../../../hooks/useApp';
import type { SceneTabId } from '../../../SceneBar/types';
import type { SessionMode } from '../../../../stores/sessionModeStore';
import { useWorkspaceContext } from '@/infrastructure/contexts/WorkspaceContext';
import { createLogger } from '@/shared/utils/logger';
import './SessionsSection.scss';
Expand All @@ -28,7 +27,7 @@ const AGENT_SCENE: SceneTabId = 'session';

type SessionMode = 'code' | 'cowork' | 'claw';

const resolveSessionMode = (session: Session): SessionMode => {
const resolveSessionModeType = (session: Session): SessionMode => {
const normalizedMode = session.mode?.toLowerCase();
if (normalizedMode === 'cowork') return 'cowork';
if (normalizedMode === 'claw') return 'claw';
Expand Down Expand Up @@ -145,7 +144,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
const matched = rawTitle.match(/^(?:新建会话|New Session)\s*(\d+)$/i);
if (!matched) return rawTitle;

const mode = resolveSessionMode(session);
const mode = resolveSessionModeType(session);
const label =
mode === 'cowork'
? t('nav.sessions.newCoworkSession')
Expand Down Expand Up @@ -217,7 +216,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
) : (
visibleSessions.map(session => {
const isEditing = editingSessionId === session.sessionId;
const sessionModeKey = resolveSessionMode(session);
const sessionModeKey = resolveSessionModeType(session);
const sessionTitle = resolveSessionTitle(session);
const SessionIcon =
sessionModeKey === 'cowork'
Expand Down
16 changes: 13 additions & 3 deletions src/web-ui/src/flow_chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
// History navigation state
const [historyIndex, setHistoryIndex] = useState(-1);
const [savedDraft, setSavedDraft] = useState('');
const { messages: inputHistory, addMessage: addToHistory } = useInputHistoryStore();
const { addMessage: addToHistory, getSessionHistory } = useInputHistoryStore();

const contexts = useContextStore(state => state.contexts);
const addContext = useContextStore(state => state.addContext);
Expand All @@ -80,6 +80,9 @@ export const ChatInput: React.FC<ChatInputProps> = ({

const activeSessionState = useActiveSessionState();
const currentSessionId = activeSessionState.sessionId;

// Get input history for current session (after currentSessionId is defined)
const inputHistory = currentSessionId ? getSessionHistory(currentSessionId) : [];
const derivedState = useSessionDerivedState(currentSessionId);
const { transition, setQueuedInput } = useSessionStateMachineActions(currentSessionId);
const stateMachine = useSessionStateMachine(currentSessionId);
Expand Down Expand Up @@ -112,6 +115,11 @@ export const ChatInput: React.FC<ChatInputProps> = ({
setChatInputExpanded(inputState.isExpanded);
}, [inputState.isExpanded, setChatInputExpanded]);

// Reset history index when switching sessions
useEffect(() => {
setHistoryIndex(-1);
}, [currentSessionId]);

const { sendMessage } = useMessageSender({
currentSessionId: currentSessionId || undefined,
contexts,
Expand Down Expand Up @@ -626,8 +634,10 @@ export const ChatInput: React.FC<ChatInputProps> = ({

const message = inputState.value.trim();

// Add to history before clearing
addToHistory(message);
// Add to history before clearing (session-scoped)
if (currentSessionId) {
addToHistory(currentSessionId, message);
}
setHistoryIndex(-1);
setSavedDraft('');

Expand Down
92 changes: 68 additions & 24 deletions src/web-ui/src/flow_chat/store/inputHistoryStore.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,110 @@
/**
* Input history store for navigating previously sent messages.
* Provides terminal-like up/down arrow navigation through message history.
* History is now session-scoped - each session maintains its own input history.
*/

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export interface InputHistoryState {
/** List of previously sent messages (most recent first) */
messages: string[];
/** Maximum number of messages to keep */
/** Map of sessionId to list of previously sent messages (most recent first) */
messagesBySession: Record<string, string[]>;
/** Maximum number of messages to keep per session */
maxHistorySize: number;

/** Add a message to history */
addMessage: (message: string) => void;
/** Clear all history */
clearHistory: () => void;
/** Get message at index (0 = most recent) */
getMessage: (index: number) => string | null;
/** Get total count */
getCount: () => number;
/** Add a message to history for a specific session */
addMessage: (sessionId: string, message: string) => void;
/** Clear history for a specific session */
clearHistory: (sessionId?: string) => void;
/** Get message at index for a specific session (0 = most recent) */
getMessage: (sessionId: string, index: number) => string | null;
/** Get total count for a specific session */
getCount: (sessionId: string) => number;
/** Get all history for a specific session */
getSessionHistory: (sessionId: string) => string[];
}

export const useInputHistoryStore = create<InputHistoryState>()(
persist(
(set, get) => ({
messages: [],
messagesBySession: {},
maxHistorySize: 100,

addMessage: (message: string) => {
addMessage: (sessionId: string, message: string) => {
const trimmed = message.trim();
if (!trimmed) return;
if (!trimmed || !sessionId) return;

set((state) => {
const sessionHistory = state.messagesBySession[sessionId] || [];

// Don't add duplicates in a row
if (state.messages[0] === trimmed) {
if (sessionHistory[0] === trimmed) {
return state;
}

// Remove the message if it exists elsewhere in history
const filtered = state.messages.filter(m => m !== trimmed);
const filtered = sessionHistory.filter(m => m !== trimmed);

// Add to front, limit size
const newMessages = [trimmed, ...filtered].slice(0, state.maxHistorySize);

return { messages: newMessages };
return {
messagesBySession: {
...state.messagesBySession,
[sessionId]: newMessages
}
};
});
},

clearHistory: () => set({ messages: [] }),
clearHistory: (sessionId?: string) => {
if (!sessionId) {
// Clear all history
set({ messagesBySession: {} });
} else {
// Clear only specific session
set((state) => {
const newHistory = { ...state.messagesBySession };
delete newHistory[sessionId];
return { messagesBySession: newHistory };
});
}
},

getMessage: (sessionId: string, index: number) => {
const { messagesBySession } = get();
const sessionHistory = messagesBySession[sessionId] || [];
if (index < 0 || index >= sessionHistory.length) return null;
return sessionHistory[index];
},

getMessage: (index: number) => {
const { messages } = get();
if (index < 0 || index >= messages.length) return null;
return messages[index];
getCount: (sessionId: string) => {
const { messagesBySession } = get();
return (messagesBySession[sessionId] || []).length;
},

getCount: () => get().messages.length,
getSessionHistory: (sessionId: string) => {
const { messagesBySession } = get();
return messagesBySession[sessionId] || [];
},
}),
{
name: 'bitfun-input-history',
version: 1,
version: 2, // Bump version to migrate from old format
migrate: (persistedState: any, version: number) => {
if (version < 2) {
// Migrate from old global format to new session-scoped format
// Old format: { messages: string[] }
// New format: { messagesBySession: Record<string, string[]> }
return {
messagesBySession: {},
maxHistorySize: persistedState.maxHistorySize || 100,
// Don't migrate old global history - users will start fresh
};
}
return persistedState;
},
}
)
);
Loading