wip: still working on agentic UI

This commit is contained in:
2026-02-25 23:14:43 +01:00
parent 7808ce74ac
commit 021cddefa3
7 changed files with 362 additions and 67 deletions

View File

@@ -79,6 +79,51 @@ describe('assistantActionDispatcher', () => {
expect(setActiveView).toHaveBeenCalledWith('tags');
});
it('supports setActiveView action alias for protocol compatibility', () => {
const setActiveView = vi.fn();
const result = dispatchAssistantAction(
{
action: 'setActiveView',
payload: { view: 'chat' },
},
{
setSelectedPost: vi.fn(),
setSelectedMedia: vi.fn(),
openTab: vi.fn(),
setActiveView,
toggleSidebar: vi.fn(),
togglePanel: vi.fn(),
toggleAssistantSidebar: vi.fn(),
},
);
expect(result.handled).toBe(true);
expect(setActiveView).toHaveBeenCalledWith('chat');
});
it('supports openPanel action alias for protocol compatibility', () => {
const togglePanel = vi.fn();
const result = dispatchAssistantAction(
{
action: 'openPanel',
},
{
setSelectedPost: vi.fn(),
setSelectedMedia: vi.fn(),
openTab: vi.fn(),
setActiveView: vi.fn(),
toggleSidebar: vi.fn(),
togglePanel,
toggleAssistantSidebar: vi.fn(),
},
);
expect(result.handled).toBe(true);
expect(togglePanel).toHaveBeenCalledTimes(1);
});
it('rejects switchView payload when view is invalid', () => {
const setActiveView = vi.fn();

View File

@@ -1,38 +1,103 @@
import { describe, expect, it } from 'vitest';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const root = path.resolve(__dirname, '../../..');
async function read(relativePath: string): Promise<string> {
return readFile(path.join(root, relativePath), 'utf8');
}
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render } from '@testing-library/react';
import { AssistantSidebar } from '../../../src/renderer/components/AssistantSidebar/AssistantSidebar';
import { AssistantPanelControls } from '../../../src/renderer/components/AssistantPanelControls';
import { useAppStore } from '../../../src/renderer/store';
describe('assistant sidebar guard rails', () => {
it('keeps assistant sidebar self-contained and avoids opening chat tabs directly', async () => {
const sidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
beforeEach(() => {
useAppStore.setState({ tabs: [], activeTabId: null, activeView: 'posts' });
expect(sidebar).not.toContain('openChatTab(');
expect(sidebar).not.toContain("type: 'chat'");
window.electronAPI.chat = {
checkReady: vi.fn().mockResolvedValue({ ready: true }),
validateApiKey: vi.fn(),
setApiKey: vi.fn(),
getApiKey: vi.fn(),
getProtocolHealth: vi.fn(),
getAvailableModels: vi.fn(),
setDefaultModel: vi.fn(),
getSystemPrompt: vi.fn(),
setSystemPrompt: vi.fn(),
getConversations: vi.fn(),
createConversation: vi.fn(),
getConversation: vi.fn(),
updateConversation: vi.fn(),
deleteConversation: vi.fn(),
sendMessage: vi.fn(),
addSystemEvent: vi.fn(),
abortMessage: vi.fn(),
getHistory: vi.fn().mockResolvedValue([]),
clearMessages: vi.fn(),
setConversationModel: vi.fn(),
analyzeTaxonomy: vi.fn(),
analyzeMediaImage: vi.fn(),
onStreamDelta: vi.fn(() => vi.fn()),
onToolCall: vi.fn(() => vi.fn()),
onToolResult: vi.fn(() => vi.fn()),
onTitleUpdated: vi.fn(() => vi.fn()),
} as never;
});
it('renders extended widget branches for assistant panel', async () => {
const controls = await read('src/renderer/components/AssistantPanelControls/AssistantPanelControls.tsx');
const sidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
it('keeps assistant sidebar self-contained and avoids opening chat tabs on mount', () => {
render(React.createElement(AssistantSidebar));
expect(controls).toContain("element.type === 'chart'");
expect(controls).toContain("element.type === 'form'");
expect(controls).toContain("element.type === 'datePicker'");
expect(controls).toContain("element.type === 'card'");
expect(controls).toContain("element.type === 'image'");
expect(controls).toContain("element.type === 'tabs'");
expect(controls).toContain("element.type === 'input'");
expect(sidebar).toContain('<AssistantPanelControls');
expect(useAppStore.getState().tabs.some((tab) => tab.type === 'chat')).toBe(false);
});
it('persists assistant action feedback events to chat history', async () => {
const sidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
it('renders rich assistant panel widget branches at runtime', () => {
const onAction = vi.fn();
expect(sidebar).toContain('chat.addSystemEvent');
const { container } = render(
React.createElement(AssistantPanelControls, {
elements: [
{ type: 'chart', chartType: 'bar', title: 'Trend', series: [{ label: 'Jan', value: 10 }] },
{
type: 'form',
formId: 'f1',
submitLabel: 'Submit',
action: 'submitNeedsInput',
fields: [{ key: 'name', label: 'Name', inputType: 'text', required: true }],
},
{ type: 'datePicker', key: 'date', label: 'Date', submitLabel: 'Pick', action: 'submitNeedsInput' },
{ type: 'card', title: 'Card', body: 'Body', actions: [{ label: 'Open', action: 'openSettings' }] },
{ type: 'image', src: 'https://example.com/a.png', caption: 'Image', action: 'openSettings' },
{
type: 'tabs',
tabs: [{ id: 'tab-1', label: 'Tab 1', elements: [{ type: 'text', text: 'Inside tab' }] }],
},
{ type: 'input', key: 'query', label: 'Query', inputType: 'text', submitLabel: 'Run', action: 'openSettings' },
],
onAction,
}),
);
expect(container.querySelector('.assistant-panel-chart')).not.toBeNull();
expect(container.querySelector('.assistant-panel-form')).not.toBeNull();
expect(container.querySelector('.assistant-panel-card')).not.toBeNull();
expect(container.querySelector('.assistant-panel-image')).not.toBeNull();
expect(container.querySelector('.assistant-panel-tabs')).not.toBeNull();
});
it('enforces action confirmation policy before dispatching assistant actions', () => {
const onAction = vi.fn();
const confirmMock = vi.fn().mockReturnValue(true);
Object.defineProperty(window, 'confirm', {
value: confirmMock,
configurable: true,
});
const { getByText } = render(
React.createElement(AssistantPanelControls, {
elements: [{ type: 'action', label: 'Open Settings', action: 'openSettings' }],
actionPolicies: { openSettings: 'confirm' },
onAction,
}),
);
fireEvent.click(getByText('Open Settings'));
expect(confirmMock).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenCalledWith('openSettings', undefined);
});
});

View File

@@ -1,24 +1,61 @@
import { describe, expect, it } from 'vitest';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const root = path.resolve(__dirname, '../../..');
async function read(relativePath: string): Promise<string> {
return readFile(path.join(root, relativePath), 'utf8');
}
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ChatPanel } from '../../../src/renderer/components/ChatPanel/ChatPanel';
import { AssistantSidebar } from '../../../src/renderer/components/AssistantSidebar/AssistantSidebar';
describe('chat surface mode usage guards', () => {
it('uses shared mode config in both chat surfaces', async () => {
const chatPanel = await read('src/renderer/components/ChatPanel/ChatPanel.tsx');
const assistantSidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
beforeEach(() => {
if (!Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = vi.fn();
}
expect(chatPanel).toContain('getChatSurfaceMode(');
expect(assistantSidebar).toContain('getChatSurfaceMode(');
window.electronAPI.chat = {
checkReady: vi.fn().mockResolvedValue({ ready: true }),
validateApiKey: vi.fn(),
setApiKey: vi.fn(),
getApiKey: vi.fn(),
getProtocolHealth: vi.fn(),
getAvailableModels: vi.fn().mockResolvedValue({
success: true,
models: [{ id: 'gpt-5', name: 'GPT-5' }],
}),
setDefaultModel: vi.fn(),
getSystemPrompt: vi.fn(),
setSystemPrompt: vi.fn(),
getConversations: vi.fn(),
createConversation: vi.fn(),
getConversation: vi.fn().mockResolvedValue({
id: 'conv-tab',
title: 'Chat',
model: 'gpt-5',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
updateConversation: vi.fn(),
deleteConversation: vi.fn(),
sendMessage: vi.fn(),
addSystemEvent: vi.fn(),
abortMessage: vi.fn(),
getHistory: vi.fn().mockResolvedValue([]),
clearMessages: vi.fn(),
setConversationModel: vi.fn(),
analyzeTaxonomy: vi.fn(),
analyzeMediaImage: vi.fn(),
onStreamDelta: vi.fn(() => vi.fn()),
onToolCall: vi.fn(() => vi.fn()),
onToolResult: vi.fn(() => vi.fn()),
onTitleUpdated: vi.fn(() => vi.fn()),
} as never;
});
expect(chatPanel).toContain('showModelSelector');
expect(chatPanel).toContain('showWelcomeTips');
expect(assistantSidebar).toContain('showWelcomeTips');
expect(assistantSidebar).toContain('showToolMarkers');
it('shows model selector in tab chat but not in assistant sidebar', async () => {
const { container: tabContainer } = render(React.createElement(ChatPanel, { conversationId: 'conv-tab' }));
expect(tabContainer.querySelector('.model-selector-button')).not.toBeNull();
render(React.createElement(AssistantSidebar));
expect(screen.queryByText('gpt-5')).toBeNull();
});
});

View File

@@ -1,25 +1,79 @@
import { describe, expect, it } from 'vitest';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const root = path.resolve(__dirname, '../../..');
async function read(relativePath: string): Promise<string> {
return readFile(path.join(root, relativePath), 'utf8');
}
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import { ChatPanel } from '../../../src/renderer/components/ChatPanel/ChatPanel';
import { AssistantSidebar } from '../../../src/renderer/components/AssistantSidebar/AssistantSidebar';
import { useAppStore } from '../../../src/renderer/store';
describe('chat surface shared usage guards', () => {
it('uses shared chat surface state hook and transcript renderer in both surfaces', async () => {
const chatPanel = await read('src/renderer/components/ChatPanel/ChatPanel.tsx');
const assistantSidebar = await read('src/renderer/components/AssistantSidebar/AssistantSidebar.tsx');
beforeEach(() => {
if (!Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = vi.fn();
}
expect(chatPanel).toContain('useChatSurfaceState(');
expect(chatPanel).toContain('<ChatTranscript');
expect(chatPanel).toContain('<AssistantPanelControls');
expect(chatPanel).toContain('extractAssistantResponseContent(');
useAppStore.setState({ tabs: [], activeTabId: null, activeView: 'posts' });
expect(assistantSidebar).toContain('useChatSurfaceState(');
expect(assistantSidebar).toContain('<ChatTranscript');
expect(assistantSidebar).toContain('<AssistantPanelControls');
window.electronAPI.chat = {
checkReady: vi.fn().mockResolvedValue({ ready: true }),
validateApiKey: vi.fn(),
setApiKey: vi.fn(),
getApiKey: vi.fn(),
getProtocolHealth: vi.fn(),
getAvailableModels: vi.fn().mockResolvedValue({
success: true,
models: [{ id: 'gpt-5', name: 'GPT-5' }],
}),
setDefaultModel: vi.fn(),
getSystemPrompt: vi.fn(),
setSystemPrompt: vi.fn(),
getConversations: vi.fn(),
createConversation: vi.fn(),
getConversation: vi.fn().mockResolvedValue({
id: 'conv-tab',
title: 'Chat',
model: 'gpt-5',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
updateConversation: vi.fn(),
deleteConversation: vi.fn(),
sendMessage: vi.fn(),
addSystemEvent: vi.fn(),
abortMessage: vi.fn(),
getHistory: vi.fn().mockResolvedValue([]),
clearMessages: vi.fn(),
setConversationModel: vi.fn(),
analyzeTaxonomy: vi.fn(),
analyzeMediaImage: vi.fn(),
onStreamDelta: vi.fn(() => vi.fn()),
onToolCall: vi.fn(() => vi.fn()),
onToolResult: vi.fn(() => vi.fn()),
onTitleUpdated: vi.fn(() => vi.fn()),
} as never;
});
it('wires chat panel to load data and subscribe to stream/tool/title events', () => {
render(React.createElement(ChatPanel, { conversationId: 'conv-tab' }));
expect(window.electronAPI.chat.checkReady).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.getConversation).toHaveBeenCalledWith('conv-tab');
expect(window.electronAPI.chat.getHistory).toHaveBeenCalledWith('conv-tab');
expect(window.electronAPI.chat.getAvailableModels).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.onStreamDelta).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.onToolCall).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.onToolResult).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.onTitleUpdated).toHaveBeenCalledTimes(1);
});
it('wires assistant sidebar stream subscriptions and does not auto-open chat tab', () => {
render(React.createElement(AssistantSidebar));
expect(window.electronAPI.chat.onStreamDelta).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.onToolCall).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.onToolResult).toHaveBeenCalledTimes(1);
expect(window.electronAPI.chat.onTitleUpdated).toHaveBeenCalledTimes(1);
expect(useAppStore.getState().tabs.some((tab) => tab.type === 'chat')).toBe(false);
});
});