better panels

This commit is contained in:
2026-02-18 11:33:30 +00:00
parent fee9682fca
commit 5e2b2fad81
11 changed files with 70 additions and 42 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ export function App() {
<Route path="/chat/opencode/new" element={<Dashboard.OpenCodeChat />} /> <Route path="/chat/opencode/new" element={<Dashboard.OpenCodeChat />} />
<Route path="/chat/opencode/:sessionId" element={<Dashboard.OpenCodeChat />} /> <Route path="/chat/opencode/:sessionId" element={<Dashboard.OpenCodeChat />} />
<Route path="/plans" element={<Dashboard.Plans />} /> <Route path="/plans" element={<Dashboard.Plans />} />
<Route path="/files" element={<Dashboard.Files />} /> <Route path="/files" element={<Dashboard.FilesPage />} />
<Route path="/terminal" element={<Dashboard.Terminal />} /> <Route path="/terminal" element={<Dashboard.Terminal />} />
<Route path="/code-editor" element={<Dashboard.CodeEditor />} /> <Route path="/code-editor" element={<Dashboard.CodeEditor />} />
<Route path="/skills" element={<Dashboard.Skills />} /> <Route path="/skills" element={<Dashboard.Skills />} />
@@ -13,27 +13,27 @@ import { Card } from '@/components/Card';
import type { ChatMessage } from 'widgets/Chat'; import type { ChatMessage } from 'widgets/Chat';
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude'; import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat'; import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
type ResourceSummary = { type CapabilitySummary = {
dirName: string; dirName: string;
name: string; name: string;
description: string; description: string;
scope: 'global' | 'user'; scope: 'global' | 'user';
}; };
type ResourceDetail = ResourceSummary & { type CapabilityDetail = CapabilitySummary & {
body: string; body: string;
rawFrontmatter: string; rawFrontmatter: string;
filePath: string; filePath: string;
chatSessionId: string | null; chatSessionId: string | null;
}; };
type ResourcePageProps = { type CapabilityPageProps = {
kind: string; kind: string;
endpoint: string; endpoint: string;
queryKey: string; queryKey: string;
}; };
type ResourceChatProps = { type CapabilityChatProps = {
kind: string; kind: string;
endpoint: string; endpoint: string;
dirName: string; dirName: string;
@@ -44,7 +44,7 @@ type ResourceChatProps = {
onResponseEnd?: () => void; onResponseEnd?: () => void;
}; };
const ResourceChat = ({ const CapabilityChat = ({
kind, kind,
endpoint, endpoint,
dirName, dirName,
@@ -53,7 +53,7 @@ const ResourceChat = ({
chatSessionId, chatSessionId,
isNew, isNew,
onResponseEnd, onResponseEnd,
}: ResourceChatProps) => { }: CapabilityChatProps) => {
const client = useClient(); const client = useClient();
const claudeModels = useVisibleClaudeModels(); const claudeModels = useVisibleClaudeModels();
const seedFile = `${kind.toUpperCase()}.md`; const seedFile = `${kind.toUpperCase()}.md`;
@@ -129,7 +129,7 @@ const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
); );
}; };
export const ResourcePage = ({ kind, endpoint, queryKey }: ResourcePageProps) => { export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps) => {
const client = useClient(); const client = useClient();
const qc = useQueryClient(); const qc = useQueryClient();
const [selected, setSelected] = useState<string | null>(null); const [selected, setSelected] = useState<string | null>(null);
@@ -142,9 +142,9 @@ export const ResourcePage = ({ kind, endpoint, queryKey }: ResourcePageProps) =>
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
const newNameRef = useRef<HTMLInputElement | null>(null); const newNameRef = useRef<HTMLInputElement | null>(null);
const { data: items = [] } = useQuery<ResourceSummary[]>({ const { data: items = [] } = useQuery<CapabilitySummary[]>({
queryKey: [queryKey], queryKey: [queryKey],
queryFn: () => client.get<ResourceSummary[]>(endpoint), queryFn: () => client.get<CapabilitySummary[]>(endpoint),
}); });
useEffect(() => { useEffect(() => {
@@ -153,9 +153,9 @@ export const ResourcePage = ({ kind, endpoint, queryKey }: ResourcePageProps) =>
} }
}, [items, selected]); }, [items, selected]);
const { data: detail } = useQuery<ResourceDetail>({ const { data: detail } = useQuery<CapabilityDetail>({
queryKey: [queryKey, selected], queryKey: [queryKey, selected],
queryFn: () => client.get<ResourceDetail>(`${endpoint}/${selected}`), queryFn: () => client.get<CapabilityDetail>(`${endpoint}/${selected}`),
enabled: !!selected, enabled: !!selected,
}); });
@@ -369,7 +369,7 @@ export const ResourcePage = ({ kind, endpoint, queryKey }: ResourcePageProps) =>
<X className="h-3.5 w-3.5 text-duck-dark/50" /> <X className="h-3.5 w-3.5 text-duck-dark/50" />
</button> </button>
</div> </div>
<ResourceChat <CapabilityChat
kind={kind.toLowerCase()} kind={kind.toLowerCase()}
key={detail.filePath} key={detail.filePath}
endpoint={endpoint} endpoint={endpoint}
@@ -26,7 +26,6 @@ import { useFiles, type DirEntry, useTasks, type TaskSummary, Breadcrumb, Toolba
import { useUserState } from '@/state/useUserState'; import { useUserState } from '@/state/useUserState';
import { useAuth } from 'hooks/useAuth'; import { useAuth } from 'hooks/useAuth';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Card } from '@/components/Card';
import { FileGrid } from './FileGrid'; import { FileGrid } from './FileGrid';
import { FileViewer } from './FileViewer'; import { FileViewer } from './FileViewer';
import { TaskRunnerModal } from './TaskRunnerModal'; import { TaskRunnerModal } from './TaskRunnerModal';
@@ -374,12 +373,7 @@ export const Files = () => {
return ( return (
<> <>
<div className="flex items-center justify-center h-full p-0 md:p-4"> <div className="flex flex-col h-full overflow-hidden">
<Card
className={`flex flex-col overflow-hidden transition-all duration-200 ${
fullscreen ? 'w-[90vw] h-[90%]' : 'w-full h-full md:w-[90vw] md:h-[90%] lg:w-[50vw]'
}`}
>
{/* Toolbar */} {/* Toolbar */}
<div className="shrink-0 flex items-center gap-2 md:gap-3 px-3 md:px-4 h-12 md:h-14 border-b border-duck-dark/10 overflow-hidden"> <div className="shrink-0 flex items-center gap-2 md:gap-3 px-3 md:px-4 h-12 md:h-14 border-b border-duck-dark/10 overflow-hidden">
<Toolbar <Toolbar
@@ -638,7 +632,6 @@ export const Files = () => {
</ContextMenu> </ContextMenu>
)} )}
</div> </div>
</Card>
</div> </div>
<FileViewer <FileViewer
@@ -1,2 +1,14 @@
export { Files } from './Screen'; import { Card } from '@/components/Card';
import { Files as FilesInner } from './Screen';
export { FilesInner as Files };
export const FilesPage = () => (
<div className="flex items-center justify-center h-full p-0 md:p-4">
<Card className="flex flex-col overflow-hidden w-full h-full md:w-[90vw] md:h-[90%] lg:w-[50vw]">
<FilesInner />
</Card>
</div>
);
export { FileBrowser as FileBrowserWidget } from './Widget'; export { FileBrowser as FileBrowserWidget } from './Widget';
@@ -15,7 +15,7 @@ const DEFAULT_HOME_LAYOUT: LayoutNode = {
id: 'home-left', id: 'home-left',
direction: 'vertical', direction: 'vertical',
children: [ children: [
{ size: 50, node: { type: 'panel', id: 'home-tl', widgetType: 'chat-launcher' } }, { size: 50, node: { type: 'panel', id: 'home-tl', widgetType: 'chat' } },
{ size: 50, node: { type: 'panel', id: 'home-bl', widgetType: 'file-browser' } }, { size: 50, node: { type: 'panel', id: 'home-bl', widgetType: 'file-browser' } },
], ],
}, },
@@ -1,3 +1,3 @@
import { ResourcePage } from '../ResourcePage'; import { CapabilityPage } from '../CapabilityPage';
export const Processes = () => <ResourcePage kind="Process" endpoint="/processes" queryKey="processes" />; export const Processes = () => <CapabilityPage kind="Process" endpoint="/processes" queryKey="processes" />;
@@ -1,3 +1,3 @@
import { ResourcePage } from '../ResourcePage'; import { CapabilityPage } from '../CapabilityPage';
export const Skills = () => <ResourcePage kind="Skill" endpoint="/skills" queryKey="skills" />; export const Skills = () => <CapabilityPage kind="Skill" endpoint="/skills" queryKey="skills" />;
@@ -1,3 +1,3 @@
import { ResourcePage } from '../ResourcePage'; import { CapabilityPage } from '../CapabilityPage';
export const Tasks = () => <ResourcePage kind="Task" endpoint="/tasks" queryKey="tasks" />; export const Tasks = () => <CapabilityPage kind="Task" endpoint="/tasks" queryKey="tasks" />;
@@ -1,20 +1,45 @@
import { useState } from 'react';
import { MessageSquare, FolderOpen, History, Music, Code, TerminalSquare, LayoutGrid } from 'lucide-react'; import { MessageSquare, FolderOpen, History, Music, Code, TerminalSquare, LayoutGrid } from 'lucide-react';
import type { WidgetRegistry } from '@/components/Workspace'; import type { WidgetRegistry } from '@/components/Workspace';
import { CodeEditorView } from 'widgets/CodeEditor'; import { CodeEditorView } from 'widgets/CodeEditor';
import { TerminalView } from 'widgets/Terminal'; import { TerminalView } from 'widgets/Terminal';
import { ChatLauncher } from '../Home/ChatLauncher'; import { useClaude } from '../Chat/useClaude';
import { useOpenCode } from '../Chat/useOpenCode';
import { ChatPanel } from '../Chat/ChatPanel';
import { useVisibleClaudeModels, useVisibleOpenCodeModels } from '@/state/useModels';
import { ChatHistoryWidget as ChatHistory } from '../ChatHistory'; import { ChatHistoryWidget as ChatHistory } from '../ChatHistory';
import { FileBrowserWidget as FileBrowser } from '../Files'; import { Files } from '../Files';
import { Catalog } from 'sounds'; import { Catalog } from 'sounds';
import { WorkspaceListWidget } from './WorkspaceListWidget'; import { WorkspaceListWidget } from './WorkspaceListWidget';
const ChatWidget = () => {
const [provider, setProvider] = useState<'claude' | 'opencode'>('claude');
return provider === 'claude' ? (
<ClaudeChatWidget key="claude" onProviderChange={setProvider} />
) : (
<OpenCodeChatWidget key="opencode" onProviderChange={setProvider} />
);
};
const ClaudeChatWidget = ({ onProviderChange }: { onProviderChange: (p: 'claude' | 'opencode') => void }) => {
const claude = useClaude();
const models = useVisibleClaudeModels();
return <ChatPanel chat={claude} provider="claude" availableModels={models} onProviderChange={onProviderChange} />;
};
const OpenCodeChatWidget = ({ onProviderChange }: { onProviderChange: (p: 'claude' | 'opencode') => void }) => {
const opencode = useOpenCode();
const models = useVisibleOpenCodeModels();
return <ChatPanel chat={opencode} provider="opencode" availableModels={models} onProviderChange={onProviderChange} />;
};
const CodeEditorWrapper = () => <CodeEditorView className="h-full w-full" />; const CodeEditorWrapper = () => <CodeEditorView className="h-full w-full" />;
const TerminalWrapper = () => <TerminalView className="h-full w-full p-2" />; const TerminalWrapper = () => <TerminalView className="h-full w-full p-2" />;
export const widgetRegistry: WidgetRegistry = { export const widgetRegistry: WidgetRegistry = {
'chat-launcher': { name: 'Chat', icon: MessageSquare, component: () => <ChatLauncher /> }, 'chat': { name: 'Chat', icon: MessageSquare, component: ChatWidget },
'file-browser': { name: 'File Browser', icon: FolderOpen, component: () => <FileBrowser /> }, 'file-browser': { name: 'File Browser', icon: FolderOpen, component: () => <Files /> },
'chat-history': { name: 'Chat History', icon: History, component: () => <ChatHistory /> }, 'chat-history': { name: 'Chat History', icon: History, component: () => <ChatHistory /> },
'sound-library': { name: 'Sound Library', icon: Music, component: () => <Catalog /> }, 'sound-library': { name: 'Sound Library', icon: Music, component: () => <Catalog /> },
'code-editor': { name: 'Code Editor', icon: Code, component: CodeEditorWrapper }, 'code-editor': { name: 'Code Editor', icon: Code, component: CodeEditorWrapper },
@@ -5,7 +5,7 @@ export * from './OnboardingAdmin';
export * from './PasskeyGate'; export * from './PasskeyGate';
export * from './Plans'; export * from './Plans';
export * from './Processes'; export * from './Processes';
export * from './ResourcePage'; export * from './CapabilityPage';
export * from './Settings'; export * from './Settings';
export * from './Skills'; export * from './Skills';
export * from './TaskLogs'; export * from './TaskLogs';
@@ -1,4 +1,5 @@
import type { LayoutPanel, WidgetRegistry } from './types'; import type { LayoutPanel, WidgetRegistry } from './types';
import { Card } from '../Card';
import { WidgetPicker } from './WidgetPicker'; import { WidgetPicker } from './WidgetPicker';
import { LayoutEditor } from './LayoutEditor'; import { LayoutEditor } from './LayoutEditor';
@@ -20,10 +21,7 @@ export const PanelSlot = ({ panel, registry, editing, isLastPanel, onSetWidget,
if (!WidgetComponent) { if (!WidgetComponent) {
return ( return (
<div <Card className="relative h-full w-full flex flex-col items-center justify-center gap-3 p-4">
className="relative h-full w-full flex flex-col items-center justify-center gap-3 p-4 rounded-lg border backdrop-blur-xl"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' }}
>
<WidgetPicker registry={registry} onSelect={(type) => onSetWidget(panel.id, type)} /> <WidgetPicker registry={registry} onSelect={(type) => onSetWidget(panel.id, type)} />
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
@@ -50,18 +48,18 @@ export const PanelSlot = ({ panel, registry, editing, isLastPanel, onSetWidget,
</button> </button>
)} )}
</div> </div>
</div> </Card>
); );
} }
return ( return (
<div <div
className="relative h-full w-full overflow-hidden rounded-lg border backdrop-blur-xl" className="relative h-full w-full overflow-hidden rounded-lg border backdrop-blur-xl p-2"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' }} style={{ backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' }}
> >
<div className="h-full w-full [&>*]:!h-full [&>*]:!flex [&>*]:!flex-col [&>*]:!rounded-none [&>*]:!border-0 [&>*]:!shadow-none [&>*>*:last-child]:!flex-1 [&>*>*:last-child]:!min-h-0 [&>*>*:last-child]:!max-h-none [&>*>*:last-child]:!overflow-auto" style={{ ['--card-bg' as string]: 'transparent', ['--card-grid-color' as string]: 'transparent' }}> <Card className="h-full w-full overflow-hidden p-0 [&>*]:!h-full [&>*]:!flex [&>*]:!flex-col [&>*]:!rounded-none [&>*]:!border-0 [&>*]:!shadow-none [&>*>*:last-child]:!flex-1 [&>*>*:last-child]:!min-h-0 [&>*>*:last-child]:!max-h-none [&>*>*:last-child]:!overflow-hidden">
<WidgetComponent panelId={panel.id} /> <WidgetComponent panelId={panel.id} />
</div> </Card>
{editing && ( {editing && (
<LayoutEditor <LayoutEditor
panelId={panel.id} panelId={panel.id}