wip: agui integration

This commit is contained in:
2026-02-25 19:51:58 +01:00
parent 5efbcfe03a
commit fcdf869a7c
59 changed files with 3467 additions and 267 deletions

View File

@@ -0,0 +1,250 @@
.assistant-sidebar {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
overflow-y: auto;
background-color: var(--vscode-sideBar-background);
color: var(--vscode-sideBar-foreground);
}
.assistant-sidebar-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.assistant-sidebar-header p {
margin: 6px 0 0;
font-size: 12px;
opacity: 0.85;
}
.assistant-sidebar-context {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
border: 1px solid var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border));
border-radius: 6px;
background: var(--vscode-editorWidget-background, rgba(0, 0, 0, 0.2));
}
.assistant-sidebar-context-label {
font-size: 11px;
opacity: 0.75;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.assistant-sidebar-context-value {
font-size: 12px;
word-break: break-word;
}
.assistant-sidebar-prompt {
width: 100%;
resize: vertical;
min-height: 120px;
padding: 10px;
border-radius: 6px;
border: 1px solid var(--vscode-input-border, transparent);
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font: inherit;
}
.assistant-sidebar-start-button {
align-self: flex-start;
}
.assistant-sidebar-error {
margin: 0;
color: var(--vscode-errorForeground);
font-size: 12px;
}
.assistant-sidebar-panel-output {
display: flex;
flex-direction: column;
gap: 8px;
border-top: 1px solid var(--vscode-panel-border);
padding-top: 10px;
}
.assistant-sidebar-metric {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 8px;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
}
.assistant-sidebar-metric-label {
font-size: 12px;
opacity: 0.85;
}
.assistant-sidebar-metric-value {
font-size: 14px;
}
.assistant-sidebar-table {
width: 100%;
border-collapse: collapse;
}
.assistant-sidebar-table th,
.assistant-sidebar-table td {
border: 1px solid var(--vscode-panel-border);
padding: 6px;
font-size: 12px;
text-align: left;
}
.assistant-sidebar-raw-message {
border-top: 1px solid var(--vscode-panel-border);
padding-top: 8px;
font-size: 12px;
white-space: pre-wrap;
}
.assistant-sidebar-widget-block {
display: flex;
flex-direction: column;
gap: 6px;
}
.assistant-sidebar-widget-label {
font-size: 12px;
opacity: 0.9;
}
.assistant-sidebar-widget-input {
width: 100%;
padding: 8px;
}
.assistant-sidebar-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
}
.assistant-sidebar-chart {
display: flex;
flex-direction: column;
gap: 6px;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 8px;
}
.assistant-sidebar-chart-title {
margin: 0;
font-weight: 600;
}
.assistant-sidebar-chart-type {
font-size: 11px;
text-transform: uppercase;
opacity: 0.7;
}
.assistant-sidebar-chart-item {
display: grid;
grid-template-columns: minmax(48px, auto) 1fr auto;
gap: 8px;
align-items: center;
font-size: 12px;
}
.assistant-sidebar-chart-item progress {
width: 100%;
}
.assistant-sidebar-form {
display: flex;
flex-direction: column;
gap: 8px;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 8px;
}
.assistant-sidebar-form-title {
margin: 0;
font-weight: 600;
}
.assistant-sidebar-card {
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.assistant-sidebar-card h4,
.assistant-sidebar-card p {
margin: 0;
}
.assistant-sidebar-card-subtitle {
font-size: 12px;
opacity: 0.8;
}
.assistant-sidebar-card-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.assistant-sidebar-image {
margin: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.assistant-sidebar-image img {
max-width: 100%;
border-radius: 6px;
border: 1px solid var(--vscode-panel-border);
}
.assistant-sidebar-image figcaption {
font-size: 12px;
opacity: 0.85;
}
.assistant-sidebar-tabs {
display: flex;
flex-direction: column;
gap: 8px;
}
.assistant-sidebar-tab-strip {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.assistant-sidebar-tab-button.active {
border-color: var(--vscode-focusBorder);
}
.assistant-sidebar-tab-panel {
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}

View File

@@ -0,0 +1,239 @@
import React, { useMemo, useState } from 'react';
import { useAppStore } from '../../store';
import { resolveAssistantEditorContext } from '../../navigation/assistantPromptContext';
import { planAssistantRequest } from '../../navigation/assistantConversation';
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
import { ensureConversationId } from '../../navigation/chatSession';
import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
import { useChatMessageSender } from '../../navigation/useChatMessageSender';
import { useChatSurfaceState } from '../../navigation/useChatSurfaceState';
import { ChatTranscript } from '../ChatSurface';
import { AssistantPanelControls } from '../AssistantPanelControls';
import { useI18n } from '../../i18n';
import '../../styles/chatSurface.css';
import './AssistantSidebar.css';
export const AssistantSidebar: React.FC = () => {
const { t: tr } = useI18n();
const surfaceMode = getChatSurfaceMode('sidebar');
const [prompt, setPrompt] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [conversationId, setConversationId] = useState<string | null>(null);
const [panelElements, setPanelElements] = useState<AssistantPanelElement[]>([]);
const [actionError, setActionError] = useState<string | null>(null);
const {
tabs,
activeTabId,
posts,
media,
setSelectedPost,
setSelectedMedia,
openTab,
setActiveView,
toggleSidebar,
togglePanel,
toggleAssistantSidebar,
} = useAppStore();
const { sendMessage: sendChatMessage } = useChatMessageSender({
chatService: window.electronAPI?.chat,
});
const {
messages,
isStreaming,
streamingContent,
toolEvents,
beginUserTurn,
finalizeAssistantTurn,
appendAssistantMessage,
stopStreaming,
} = useChatSurfaceState();
const activeTab = useMemo(() => tabs.find((tab) => tab.id === activeTabId) ?? null, [tabs, activeTabId]);
const editorContext = useMemo(
() => resolveAssistantEditorContext({ activeTab, posts, media }),
[activeTab, posts, media],
);
const contextSummary = useMemo(() => {
if (!editorContext) {
return tr('assistantSidebar.context.none');
}
const title = editorContext.title ? `${editorContext.title}` : '';
const id = editorContext.id ? ` (${editorContext.id})` : '';
return `${editorContext.tabType}${id}${title}`;
}, [editorContext, tr]);
const persistActionEvent = async (message: string) => {
if (!conversationId) {
return;
}
try {
await window.electronAPI?.chat.addSystemEvent(conversationId, message);
} catch (error) {
console.error('Failed to persist assistant action event:', error);
}
};
const handleStart = async () => {
const trimmed = prompt.trim();
if (!trimmed || isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const chatService = window.electronAPI?.chat;
if (!chatService) {
throw new Error('Chat service unavailable');
}
const resolvedConversationId = await ensureConversationId({
currentConversationId: conversationId,
createTitle: tr('assistantSidebar.conversationTitle'),
chatService,
});
if (!conversationId) {
setConversationId(resolvedConversationId);
}
const requestPlan = planAssistantRequest({
conversationId,
userPrompt: trimmed,
context: editorContext,
});
beginUserTurn(resolvedConversationId, trimmed);
const sendResult = await sendChatMessage({
conversationId: resolvedConversationId,
message: requestPlan.outboundMessage,
metadata: { surface: 'sidebar' },
});
if (!sendResult.success) {
appendAssistantMessage(
resolvedConversationId,
tr('chat.errorPrefix', { error: sendResult.error || tr('chat.errorNoResponse') }),
);
stopStreaming();
throw new Error(sendResult.error || 'Failed to send assistant message');
}
if (sendResult.message) {
const parsedResponse = extractAssistantResponseContent(sendResult.message);
finalizeAssistantTurn(resolvedConversationId, parsedResponse.displayText);
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
} else {
appendAssistantMessage(resolvedConversationId, tr('chat.errorEmptyResponse'));
stopStreaming();
}
setPrompt('');
} catch (error) {
console.error('Failed to start assistant conversation:', error);
setErrorMessage(tr('assistantSidebar.error.startFailed'));
} finally {
setIsSubmitting(false);
}
};
const handleAssistantAction = (action: string, payload?: Record<string, unknown>) => {
const result = dispatchAssistantAction(
{
action,
payload,
},
{
setSelectedPost,
setSelectedMedia,
openTab,
setActiveView,
toggleSidebar,
togglePanel,
toggleAssistantSidebar,
},
);
if (!result.handled) {
setActionError(result.error || tr('assistantSidebar.error.actionFailed'));
void persistActionEvent(
`Assistant action failed: ${action}${result.error ? ` (${result.error})` : ''}`,
);
return;
}
setActionError(null);
void persistActionEvent(
`Assistant action executed: ${action}${payload ? ` ${JSON.stringify(payload)}` : ''}`,
);
};
return (
<div className="assistant-sidebar chat-surface">
<div className="assistant-sidebar-header">
<h3>{tr('assistantSidebar.title')}</h3>
<p>{tr('assistantSidebar.description')}</p>
</div>
<div className="assistant-sidebar-context chat-surface-section">
<span className="assistant-sidebar-context-label">{tr('assistantSidebar.context.label')}</span>
<span className="assistant-sidebar-context-value">{contextSummary}</span>
</div>
<textarea
className="assistant-sidebar-prompt chat-surface-input"
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder={tr('assistantSidebar.prompt.placeholder')}
rows={6}
/>
<button
type="button"
className="assistant-sidebar-start-button"
disabled={isSubmitting}
onClick={() => void handleStart()}
>
{isSubmitting ? tr('assistantSidebar.button.starting') : tr('assistantSidebar.button.start')}
</button>
{errorMessage && <p className="assistant-sidebar-error chat-surface-error">{errorMessage}</p>}
{actionError && <p className="assistant-sidebar-error chat-surface-error">{actionError}</p>}
{surfaceMode.showWelcomeTips && messages.length === 0 && !isStreaming && (
<div className="assistant-sidebar-raw-message chat-surface-section">
{tr('chat.welcomeDescription')}
</div>
)}
{messages.length > 0 && (
<div className="assistant-sidebar-raw-message chat-surface-section">
<ChatTranscript
messages={messages}
isStreaming={isStreaming}
streamingContent={streamingContent}
toolEvents={toolEvents}
assistantRoleLabel={tr('chat.role.assistant')}
userRoleLabel={tr('chat.role.you')}
showToolMarkers={surfaceMode.showToolMarkers}
/>
</div>
)}
{panelElements.length > 0 && (
<AssistantPanelControls elements={panelElements} onAction={handleAssistantAction} />
)}
</div>
);
};
export default AssistantSidebar;

View File

@@ -0,0 +1 @@
export { AssistantSidebar } from './AssistantSidebar';