Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/renderer/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
type CSSProperties,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
Expand Down Expand Up @@ -52,6 +53,7 @@ import { cn } from '@/renderer/shared/lib/utils';
import { Button } from '@/renderer/shared/ui/button';
import { TooltipProvider } from '@/renderer/shared/ui/tooltip';
import { WorkflowProjectActionsMenu } from '@/renderer/features/workflow/components/WorkflowProjectActionsMenu';
import { createSearchIndex } from '@/renderer/features/workflow/model/search-index';
import {
MAC_TITLEBAR_OVERLAY_HEIGHT,
MAC_TITLEBAR_SIDEBAR_TOGGLE_GAP,
Expand Down Expand Up @@ -108,13 +110,19 @@ export default function AppShell() {
agents,
appendTranscriptPage,
clearAgentAssignments,
allWorkflowItems,
} = useAppStore(
useShallow((state) => ({
agents: state.agents,
appendTranscriptPage: state.appendTranscriptPage,
allWorkflowItems: state.items,
clearAgentAssignments: state.clearAgentAssignments,
})),
);
const searchIndex = useMemo(
() => createSearchIndex(allWorkflowItems, agents, projects),
[agents, allWorkflowItems, projects],
);
const showContextPanel = route === 'agent' && isContextPanelOpen && !!activeAgent;
const titlebarAreaRect = useWindowControlsOverlay(isMac);
const { isCompactShell, usesInlineContext, usesOverlayContext } =
Expand Down Expand Up @@ -602,6 +610,7 @@ export default function AppShell() {
onToggleContextPanel={controller.handleToggleContextPanel}
open={isCommandOpen}
projects={projects}
searchIndex={searchIndex}
/>

<CreateAgentDialog
Expand Down
61 changes: 58 additions & 3 deletions src/renderer/app/shell/CommandMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// Command menu UI.

import { startTransition } from 'react';
import {
startTransition,
useMemo,
useState,
} from 'react';
import {
Bot,
FileText,
PanelRight,
Plus,
Settings2,
Sparkles,
} from 'lucide-react';

import type { SearchIndex } from '@/renderer/features/workflow/model/search-index';
import { useDesktopPlatform } from '@/renderer/shared/lib/use-desktop-platform';
import {
CommandDialog,
Expand Down Expand Up @@ -64,6 +70,7 @@ interface CommandMenuProps {
onToggleContextPanel: () => void;
open: boolean;
projects: CommandProject[];
searchIndex: SearchIndex;
}

/** Renders the command menu UI. */
Expand All @@ -83,21 +90,69 @@ export function CommandMenu({
onToggleContextPanel,
open,
projects,
searchIndex,
}: CommandMenuProps) {
const { modifierLabel } = useDesktopPlatform();
const [query, setQuery] = useState('');
const searchResults = useMemo(
() => searchIndex.search(query, 10),
[query, searchIndex],
);
const isSearching = query.trim().length > 0;

const closeAndRun = (handler: () => void) => {
onOpenChange(false);
setQuery('');
startTransition(() => {
handler();
});
};

return (
<CommandDialog onOpenChange={onOpenChange} open={open}>
<CommandInput placeholder="Jump to a project, work item, agent, or action…" />
<CommandDialog
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setQuery('');
}

onOpenChange(nextOpen);
}}
open={open}
>
<CommandInput
onValueChange={setQuery}
placeholder="Search work items, projects, agents, or actions..."
value={query}
/>
<CommandList className="thin-scrollbar">
<CommandEmpty>No matching projects, work items, or actions.</CommandEmpty>
{isSearching ? (
<>
<CommandGroup heading="Work item matches">
{searchResults.map((item) => (
<CommandItem
key={item.itemId}
onSelect={() => closeAndRun(() => onSelectItem(item.itemId))}
value={`${item.title} ${item.statusLabel} ${item.assigneeName ?? ''} ${item.snippet}`}
>
<FileText className="h-4 w-4 text-app-muted" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate">{item.title}</span>
<span className="truncate text-xs text-app-muted">
{item.statusLabel} · {item.assigneeName ?? 'No agent'} · {item.projectName}
</span>
<span className="mt-1 line-clamp-2 text-xs leading-5 text-app-muted">
{item.snippet}
</span>
</div>
</CommandItem>
))}
</CommandGroup>

<CommandSeparator />
</>
) : null}

<CommandGroup heading="Actions">
<CommandItem onSelect={() => closeAndRun(onCreateItem)}>
<Plus className="h-4 w-4 text-app-muted" />
Expand Down
53 changes: 44 additions & 9 deletions src/renderer/app/workspaces/WorkflowWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
Plus,
X,
} from 'lucide-react';
import { useState } from 'react';
import {
useMemo,
useState,
} from 'react';
import { useShallow } from 'zustand/react/shallow';

import { CompactShellToolbar } from '@/renderer/app/shell/CompactShellToolbar';
Expand All @@ -13,13 +16,18 @@ import { useWorkflowSession } from '@/renderer/app/store/selectors';
import { useAppStore } from '@/renderer/app/store/use-app-store';
import { CreateProjectDialog } from '@/renderer/features/workflow/components/CreateProjectDialog';
import { CreateWorkItemDialog } from '@/renderer/features/workflow/components/CreateWorkItemDialog';
import { FilterPanel } from '@/renderer/features/workflow/components/FilterPanel';
import { WorkflowBoard } from '@/renderer/features/workflow/components/WorkflowBoard';
import { WorkflowItemInspector } from '@/renderer/features/workflow/components/WorkflowItemInspector';
import { WorkflowProjectActivity } from '@/renderer/features/workflow/components/WorkflowProjectActivity';
import { WorkflowProjectActionsMenu } from '@/renderer/features/workflow/components/WorkflowProjectActionsMenu';
import { WorkflowProjectAgents } from '@/renderer/features/workflow/components/WorkflowProjectAgents';
import { WorkflowProjectSettings } from '@/renderer/features/workflow/components/WorkflowProjectSettings';
import { presentWorkflowEventTimestamp } from '@/renderer/features/workflow/model/workflow-presenters';
import {
defaultWorkflowItemFilters,
filterWorkflowItems,
} from '@/renderer/features/workflow/model/workflow-filters';
import { agentRuntime } from '@/renderer/features/agents/runtime/agent-runtime';
import {
Dialog,
Expand Down Expand Up @@ -71,6 +79,7 @@ export function WorkflowWorkspace({
}: WorkflowWorkspaceProps) {
const commands = useAppCommands();
const [isDeleteProjectOpen, setDeleteProjectOpen] = useState(false);
const [workflowItemFilters, setWorkflowItemFilters] = useState(defaultWorkflowItemFilters);
const [cachedActivityEntriesByProjectId, setCachedActivityEntriesByProjectId] = useState<Record<string, Array<
WorkflowProjectActivityEntry & { createdAtLabel: string }
>>>({});
Expand Down Expand Up @@ -108,6 +117,7 @@ export function WorkflowWorkspace({
activityEntries,
activitySummary,
filteredItemSummaries,
items,
isWorkflowHydrated,
projectAgents,
projects,
Expand Down Expand Up @@ -136,6 +146,29 @@ export function WorkflowWorkspace({
...activitySummary,
hasOlderEntries: mergedActivityEntries.length < activitySummary.totalEntryCount,
};
const boardItemSummaries = useMemo(() => {
const baseFilteredItemIds = new Set(filteredItemSummaries.map((item) => item.id));
const advancedFilteredItemIds = new Set(
filterWorkflowItems(
items.filter((item) => baseFilteredItemIds.has(item.id)),
workflowItemFilters,
).map((item) => item.id),
);

return filteredItemSummaries.filter((item) => advancedFilteredItemIds.has(item.id));
}, [filteredItemSummaries, items, workflowItemFilters]);
const boardFilterPanel = (
<FilterPanel
agents={projectAgents.map((agent) => ({
id: agent.id,
name: agent.name,
}))}
filters={workflowItemFilters}
matchCount={boardItemSummaries.length}
onChange={setWorkflowItemFilters}
totalCount={filteredItemSummaries.length}
/>
);

if (!isWorkflowHydrated) {
return (
Expand All @@ -155,7 +188,7 @@ export function WorkflowWorkspace({
}

/** Handles primary agent assignment. Persisting the snapshot triggers the main-process scheduler. */
const handleAssignPrimaryAgent = async (
const handleAssignPrimaryAgent = (
itemId: string,
input: { agentId: string | null; agentName?: string | null },
) => {
Expand Down Expand Up @@ -184,7 +217,7 @@ export function WorkflowWorkspace({
{ openRoute: false },
);

await handleAssignPrimaryAgent(itemId, {
handleAssignPrimaryAgent(itemId, {
agentId,
agentName: suggestedName,
});
Expand Down Expand Up @@ -279,9 +312,10 @@ export function WorkflowWorkspace({
const boardView = (
<>
{isCompactShell ? (
<div className="flex h-full min-h-0 flex-col">
<div className="flex h-full min-h-0 flex-col gap-4 overflow-hidden">
{boardFilterPanel}
<WorkflowBoard
items={filteredItemSummaries}
items={boardItemSummaries}
onMoveItem={moveItem}
onSelectItem={(itemId) => {
selectItem(itemId);
Expand All @@ -291,9 +325,10 @@ export function WorkflowWorkspace({
</div>
) : (
<div className="grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1fr)_380px]">
<div className="min-h-0 overflow-hidden rounded-[28px] border border-app-border bg-app-panel/70 px-4 py-4">
<div className="flex min-h-0 flex-col gap-4 overflow-hidden rounded-[28px] border border-app-border bg-app-panel/70 px-4 py-4">
{boardFilterPanel}
<WorkflowBoard
items={filteredItemSummaries}
items={boardItemSummaries}
onMoveItem={moveItem}
onSelectItem={(itemId) => {
selectItem(itemId);
Expand All @@ -312,7 +347,7 @@ export function WorkflowWorkspace({
addTask(itemId, title);
}}
onAssignPrimaryAgent={(itemId, input) => {
void handleAssignPrimaryAgent(itemId, input);
handleAssignPrimaryAgent(itemId, input);
}}
onCreateAgent={(itemId) => {
void handleCreateAgentForItem(itemId);
Expand Down Expand Up @@ -343,7 +378,7 @@ export function WorkflowWorkspace({
addTask(itemId, title);
}}
onAssignPrimaryAgent={(itemId, input) => {
void handleAssignPrimaryAgent(itemId, input);
handleAssignPrimaryAgent(itemId, input);
}}
onCreateAgent={(itemId) => {
void handleCreateAgentForItem(itemId);
Expand Down
Loading