agent-Specialization/static/src/stores/ui.ts
JOJO 2f75c1c8bb feat: stable version before virtual monitor timing fix
Current status includes:
- Virtual monitor surface and components
- Monitor store for state management
- Tool call animations and transitions
- Liquid glass shader integration

Known issue to fix: Tool status display timing - "正在xx" appears
after tool execution completes instead of when tool call starts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-13 17:12:12 +08:00

294 lines
8.0 KiB
TypeScript

import { defineStore } from 'pinia';
type PanelMode = 'files' | 'todo' | 'subAgents';
type ResizingPanel = 'left' | 'right' | null;
type MobileOverlayTarget = 'conversation' | 'workspace' | 'focus' | null;
interface QuotaToast {
message: string;
type?: string;
}
interface ToastItem {
id: number;
title?: string;
message: string;
type?: string;
closable: boolean;
timeoutId: ReturnType<typeof setTimeout> | null;
}
interface ToastOptions {
title?: string;
message?: string;
type?: string;
closable?: boolean;
duration?: number | null;
}
interface ConfirmDialogOptions {
title?: string;
message?: string;
confirmText?: string;
cancelText?: string;
}
interface ConfirmDialogState extends ConfirmDialogOptions {
visible: boolean;
}
interface EasterEggState {
active: boolean;
effect: string | null;
payload: any;
instance: any;
cleanupTimer: ReturnType<typeof setTimeout> | null;
destroying: boolean;
destroyPromise: Promise<void> | null;
}
interface UiState {
sidebarCollapsed: boolean;
workspaceCollapsed: boolean;
chatDisplayMode: 'chat' | 'monitor';
panelMode: PanelMode;
panelMenuOpen: boolean;
leftWidth: number;
rightWidth: number;
rightCollapsed: boolean;
isResizing: boolean;
resizingPanel: ResizingPanel;
minPanelWidth: number;
maxPanelWidth: number;
quotaToast: QuotaToast | null;
quotaToastTimer: ReturnType<typeof setTimeout> | null;
toastQueue: ToastItem[];
nextToastId: number;
confirmDialog: ConfirmDialogState | null;
pendingConfirmResolver: ((value: boolean) => void) | null;
easterEgg: EasterEggState;
isMobileViewport: boolean;
mobileOverlayMenuOpen: boolean;
activeMobileOverlay: MobileOverlayTarget;
}
export const useUiStore = defineStore('ui', {
state: (): UiState => ({
sidebarCollapsed: true,
workspaceCollapsed: false,
chatDisplayMode: 'chat',
panelMode: 'files',
panelMenuOpen: false,
leftWidth: 350,
rightWidth: 420,
rightCollapsed: true,
isResizing: false,
resizingPanel: null,
minPanelWidth: 350,
maxPanelWidth: 600,
quotaToast: null,
quotaToastTimer: null,
toastQueue: [],
nextToastId: 1,
confirmDialog: null,
pendingConfirmResolver: null,
easterEgg: {
active: false,
effect: null,
payload: null,
instance: null,
cleanupTimer: null,
destroying: false,
destroyPromise: null
},
isMobileViewport: false,
mobileOverlayMenuOpen: false,
activeMobileOverlay: null
}),
actions: {
setSidebarCollapsed(collapsed: boolean) {
this.sidebarCollapsed = collapsed;
},
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
},
setWorkspaceCollapsed(collapsed: boolean) {
this.workspaceCollapsed = collapsed;
},
setChatDisplayMode(mode: 'chat' | 'monitor') {
this.chatDisplayMode = mode;
},
toggleChatDisplayMode() {
this.chatDisplayMode = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
},
toggleWorkspaceCollapsed() {
this.workspaceCollapsed = !this.workspaceCollapsed;
},
setPanelMode(mode: PanelMode) {
this.panelMode = mode;
},
setPanelMenuOpen(open: boolean) {
this.panelMenuOpen = open;
},
togglePanelMenu() {
this.panelMenuOpen = !this.panelMenuOpen;
},
setLeftWidth(width: number) {
this.leftWidth = width;
},
setRightWidth(width: number) {
this.rightWidth = width;
},
setRightCollapsed(collapsed: boolean) {
this.rightCollapsed = collapsed;
},
setResizing(state: boolean, panel: ResizingPanel = null) {
this.isResizing = state;
this.resizingPanel = state ? panel : null;
},
setPanelBounds(min: number, max: number) {
this.minPanelWidth = min;
this.maxPanelWidth = max;
},
setIsMobileViewport(isMobile: boolean) {
this.isMobileViewport = isMobile;
if (!isMobile) {
this.mobileOverlayMenuOpen = false;
this.activeMobileOverlay = null;
}
},
setMobileOverlayMenuOpen(open: boolean) {
this.mobileOverlayMenuOpen = open;
},
toggleMobileOverlayMenu() {
this.mobileOverlayMenuOpen = !this.mobileOverlayMenuOpen;
},
setActiveMobileOverlay(target: MobileOverlayTarget) {
this.activeMobileOverlay = target;
},
closeMobileOverlay() {
this.activeMobileOverlay = null;
},
showQuotaToastMessage(message: string, type: string = 'fast', duration = 5000) {
this.quotaToast = { message, type };
if (this.quotaToastTimer) {
clearTimeout(this.quotaToastTimer);
}
this.quotaToastTimer = setTimeout(() => {
this.dismissQuotaToast();
}, duration);
},
dismissQuotaToast() {
if (this.quotaToastTimer) {
clearTimeout(this.quotaToastTimer);
this.quotaToastTimer = null;
}
this.quotaToast = null;
},
pushToast(options: ToastOptions = {}) {
const title = options.title || '';
const message = options.message || '';
if (!title && !message) {
return 0;
}
const id = this.nextToastId++;
const entry: ToastItem = {
id,
title,
message,
type: options.type || 'info',
closable: options.closable !== false,
timeoutId: null
};
const duration = Object.prototype.hasOwnProperty.call(options, 'duration') ? options.duration : 4000;
if (duration !== null) {
entry.timeoutId = setTimeout(() => this.dismissToast(id), duration);
}
this.toastQueue.push(entry);
return id;
},
updateToast(id: number, patch: ToastOptions = {}) {
const entry = this.toastQueue.find(item => item.id === id);
if (!entry) {
return;
}
if (patch.title !== undefined) {
entry.title = patch.title;
}
if (patch.message !== undefined) {
entry.message = patch.message;
}
if (patch.type !== undefined) {
entry.type = patch.type;
}
if (patch.closable !== undefined) {
entry.closable = patch.closable;
}
if (Object.prototype.hasOwnProperty.call(patch, 'duration')) {
if (entry.timeoutId) {
clearTimeout(entry.timeoutId);
entry.timeoutId = null;
}
if (patch.duration !== null && typeof patch.duration === 'number') {
entry.timeoutId = setTimeout(() => this.dismissToast(id), patch.duration);
}
}
},
dismissToast(id: number) {
const index = this.toastQueue.findIndex(item => item.id === id);
if (index === -1) {
return;
}
const [entry] = this.toastQueue.splice(index, 1);
if (entry && entry.timeoutId) {
clearTimeout(entry.timeoutId);
}
},
requestConfirm(options: ConfirmDialogOptions = {}) {
return new Promise<boolean>((resolve) => {
if (this.pendingConfirmResolver) {
const previous = this.pendingConfirmResolver;
this.pendingConfirmResolver = null;
previous(false);
}
this.confirmDialog = {
visible: true,
title: options.title || '确认操作',
message: options.message || '请确认本次操作',
confirmText: options.confirmText || '确认',
cancelText: options.cancelText || '取消'
};
this.pendingConfirmResolver = resolve;
});
},
resolveConfirm(choice: boolean) {
if (this.confirmDialog) {
this.confirmDialog.visible = false;
}
const resolver = this.pendingConfirmResolver;
this.pendingConfirmResolver = null;
this.confirmDialog = null;
if (resolver) {
resolver(!!choice);
}
},
setEasterEggState(patch: Partial<EasterEggState>) {
this.easterEgg = {
...this.easterEgg,
...patch
};
},
clearEasterEgg() {
this.easterEgg = {
active: false,
effect: null,
payload: null,
instance: null,
cleanupTimer: null,
destroying: false,
destroyPromise: null
};
}
}
});