242 lines
7.0 KiB
TypeScript
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,
|
|
};
|
|
};
|