Files
MosaicIQ/MosaicIQ/src/hooks/useTabs.ts

242 lines
7.0 KiB
TypeScript

import { useState, useCallback } from 'react';
import {
PendingAgentApproval,
TerminalEntry,
TerminalRequestState,
WorkspaceTerminalState,
} from '../types/terminal';
const DEFAULT_WELCOME_MESSAGE =
'MosaicIQ Financial Terminal v1.0\nUse /portfolio to open portfolio tools.\nSlash commands (/) clear the panel. Natural language builds a conversation.';
const createInitialTerminalState = (entryId: string): WorkspaceTerminalState => ({
history: [
{
id: entryId,
type: 'system',
content: DEFAULT_WELCOME_MESSAGE,
timestamp: new Date(),
},
],
chatSessionId: undefined,
inputDraft: '',
isProcessing: false,
activeRequest: null,
pendingApproval: null,
scrollTop: 0,
stickToBottom: true,
collapsedThinkingEntryIds: [],
});
export interface Workspace {
id: string;
name: string;
terminal: WorkspaceTerminalState;
createdAt: Date;
portfolioName?: string | null;
}
export const useTabs = () => {
const [workspaces, setWorkspaces] = useState<Workspace[]>([
{
id: '1',
name: 'Terminal 1',
terminal: createInitialTerminalState('welcome'),
createdAt: new Date(),
},
]);
const [activeWorkspaceId, setActiveWorkspaceId] = useState('1');
const activeWorkspace = workspaces.find(w => w.id === activeWorkspaceId) || workspaces[0];
const createWorkspace = useCallback(() => {
const newWorkspace: Workspace = {
id: Date.now().toString(),
name: `Terminal ${workspaces.length + 1}`,
terminal: createInitialTerminalState(`welcome-${Date.now()}`),
createdAt: new Date(),
};
setWorkspaces(prev => [...prev, newWorkspace]);
setActiveWorkspaceId(newWorkspace.id);
return newWorkspace.id;
}, [workspaces.length]);
const closeWorkspace = useCallback((id: string) => {
// Don't allow closing the last workspace
if (workspaces.length === 1) return;
setWorkspaces(prev => {
const filtered = prev.filter(w => w.id !== id);
// If we closed the active workspace, switch to another
if (id === activeWorkspaceId) {
const newActive = filtered[0];
setActiveWorkspaceId(newActive.id);
}
return filtered;
});
}, [workspaces, activeWorkspaceId]);
const setActiveWorkspace = useCallback((id: string) => {
setActiveWorkspaceId(id);
}, []);
const updateWorkspaceTerminal = useCallback(
(
id: string,
updater: (terminal: WorkspaceTerminalState) => WorkspaceTerminalState,
) => {
setWorkspaces((prev) =>
prev.map((workspace) =>
workspace.id === id
? { ...workspace, terminal: updater(workspace.terminal) }
: workspace,
),
);
},
[],
);
const updateWorkspaceHistory = useCallback((id: string, history: TerminalEntry[]) => {
updateWorkspaceTerminal(id, (terminal) => ({ ...terminal, history }));
}, []);
const appendWorkspaceEntry = useCallback((id: string, entry: TerminalEntry) => {
// Appending in place keeps a stable entry id available for later stream updates.
updateWorkspaceTerminal(id, (terminal) => ({
...terminal,
history: [...terminal.history, entry],
}));
}, [updateWorkspaceTerminal]);
const updateWorkspaceEntry = useCallback(
(
id: string,
entryId: string,
updater: (entry: TerminalEntry) => TerminalEntry,
) => {
// Update a single entry without rebuilding unrelated workspaces.
updateWorkspaceTerminal(id, (terminal) => ({
...terminal,
history: terminal.history.map((entry) =>
entry.id === entryId ? updater(entry) : entry,
),
}));
},
[updateWorkspaceTerminal],
);
const clearWorkspace = useCallback((id: string) => {
updateWorkspaceTerminal(id, (terminal) => ({
...terminal,
history: [],
chatSessionId: undefined,
inputDraft: '',
isProcessing: false,
activeRequest: null,
pendingApproval: null,
scrollTop: 0,
stickToBottom: true,
collapsedThinkingEntryIds: [],
}));
}, [updateWorkspaceTerminal]);
const setWorkspaceSession = useCallback((id: string, chatSessionId?: string) => {
// Session ids are scoped per workspace so each tab can maintain an independent conversation.
updateWorkspaceTerminal(id, (terminal) => ({ ...terminal, chatSessionId }));
}, [updateWorkspaceTerminal]);
const setWorkspaceInputDraft = useCallback((id: string, inputDraft: string) => {
updateWorkspaceTerminal(id, (terminal) => ({ ...terminal, inputDraft }));
}, [updateWorkspaceTerminal]);
const setWorkspaceProcessing = useCallback((id: string, isProcessing: boolean) => {
updateWorkspaceTerminal(id, (terminal) => ({ ...terminal, isProcessing }));
}, [updateWorkspaceTerminal]);
const setWorkspaceActiveRequest = useCallback(
(id: string, activeRequest: TerminalRequestState | null) => {
updateWorkspaceTerminal(id, (terminal) => ({ ...terminal, activeRequest }));
},
[updateWorkspaceTerminal],
);
const setWorkspacePendingApproval = useCallback(
(id: string, pendingApproval: PendingAgentApproval | null) => {
updateWorkspaceTerminal(id, (terminal) => ({ ...terminal, pendingApproval }));
},
[updateWorkspaceTerminal],
);
const setWorkspaceScrollState = useCallback(
(
id: string,
scrollState: Pick<WorkspaceTerminalState, 'scrollTop' | 'stickToBottom'>,
) => {
updateWorkspaceTerminal(id, (terminal) => ({
...terminal,
scrollTop: scrollState.scrollTop,
stickToBottom: scrollState.stickToBottom,
}));
},
[updateWorkspaceTerminal],
);
const toggleWorkspaceThinkingCollapse = useCallback(
(id: string, entryId: string) => {
updateWorkspaceTerminal(id, (terminal) => {
const collapsedThinkingEntryIds = terminal.collapsedThinkingEntryIds.includes(entryId)
? terminal.collapsedThinkingEntryIds.filter((currentId) => currentId !== entryId)
: [...terminal.collapsedThinkingEntryIds, entryId];
return {
...terminal,
collapsedThinkingEntryIds,
};
});
},
[updateWorkspaceTerminal],
);
const renameWorkspace = useCallback((id: string, name: string) => {
setWorkspaces(prev =>
prev.map(w =>
w.id === id ? { ...w, name } : w
)
);
}, []);
const setPortfolioName = useCallback((id: string, portfolioName: string | null) => {
setWorkspaces(prev =>
prev.map(w =>
w.id === id ? { ...w, portfolioName } : w
)
);
}, []);
return {
workspaces,
activeWorkspace,
activeWorkspaceId,
createWorkspace,
closeWorkspace,
setActiveWorkspace,
updateWorkspaceHistory,
appendWorkspaceEntry,
updateWorkspaceEntry,
clearWorkspace,
setWorkspaceSession,
setWorkspaceInputDraft,
setWorkspaceProcessing,
setWorkspaceActiveRequest,
setWorkspacePendingApproval,
setWorkspaceScrollState,
toggleWorkspaceThinkingCollapse,
updateWorkspaceTerminal,
renameWorkspace,
setPortfolioName,
};
};