Merge pull request #376 from rowboatlabs/dev

Dev
This commit is contained in:
Tushar 2026-02-16 21:42:56 +05:30 committed by GitHub
commit f68887496b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 2807 additions and 69 deletions

View file

@ -348,6 +348,10 @@ export function setupIpcHandlers() {
'runs:list': async (_event, args) => {
return runsCore.listRuns(args.cursor);
},
'runs:delete': async (_event, args) => {
await runsCore.deleteRun(args.runId);
return { success: true };
},
'models:list': async () => {
return await listOnboardingModels();
},

View file

@ -2306,6 +2306,17 @@ function App() {
onSelectRun: (runIdToLoad) => {
void navigateToView({ type: 'chat', runId: runIdToLoad })
},
onDeleteRun: async (runIdToDelete) => {
try {
await window.ipc.invoke('runs:delete', { runId: runIdToDelete })
if (runId === runIdToDelete) {
void navigateToView({ type: 'chat', runId: null })
}
await loadRuns()
} catch (err) {
console.error('Failed to delete run:', err)
}
},
onSelectBackgroundTask: (taskName) => {
void navigateToView({ type: 'task', name: taskName })
},

View file

@ -667,7 +667,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
<Input
value={activeConfig.model}
onChange={(e) => updateProviderConfig(llmProvider, { model: e.target.value })}
placeholder="Enter model ID"
placeholder="Enter model"
/>
) : (
<Select

View file

@ -374,7 +374,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
<Input
value={activeConfig.model}
onChange={(e) => updateConfig(provider, { model: e.target.value })}
placeholder="Enter model ID"
placeholder="Enter model"
/>
) : (
<Select

View file

@ -18,7 +18,6 @@ import {
LoaderIcon,
Settings,
Square,
SquarePen,
Trash2,
} from "lucide-react"
@ -27,6 +26,15 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import {
Sidebar,
SidebarContent,
@ -128,6 +136,7 @@ const SERVICE_LABELS: Record<string, string> = {
type TasksActions = {
onNewChat: () => void
onSelectRun: (runId: string) => void
onDeleteRun: (runId: string) => void
onSelectBackgroundTask?: (taskName: string) => void
}
@ -1004,19 +1013,10 @@ function TasksSection({
backgroundTasks?: BackgroundTaskItem[]
selectedBackgroundTask?: string | null
}) {
const [pendingDeleteRunId, setPendingDeleteRunId] = useState<string | null>(null)
return (
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
{/* Sticky New Chat button - matches Knowledge section height */}
<div className="sticky top-0 z-10 bg-sidebar border-b border-sidebar-border py-0.5">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton onClick={actions?.onNewChat} className="gap-2">
<SquarePen className="size-4 shrink-0" />
<span className="text-sm">New chat</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</div>
<SidebarGroupContent className="flex-1 overflow-y-auto">
{/* Background Tasks Section */}
{backgroundTasks.length > 0 && (
@ -1054,7 +1054,7 @@ function TasksSection({
</div>
<SidebarMenu>
{runs.map((run) => (
<SidebarMenuItem key={run.id}>
<SidebarMenuItem key={run.id} className="group/chat-item">
<SidebarMenuButton
isActive={currentRunId === run.id}
onClick={() => actions?.onSelectRun(run.id)}
@ -1065,10 +1065,23 @@ function TasksSection({
) : null}
<span className="min-w-0 flex-1 truncate text-sm">{run.title || '(Untitled chat)'}</span>
{run.createdAt ? (
<span className="shrink-0 text-[10px] text-muted-foreground">
<span className={`shrink-0 text-[10px] text-muted-foreground${processingRunIds?.has(run.id) ? '' : ' group-hover/chat-item:hidden'}`}>
{formatRunTime(run.createdAt)}
</span>
) : null}
{!processingRunIds?.has(run.id) && (
<button
type="button"
className="shrink-0 hidden group-hover/chat-item:flex items-center justify-center text-muted-foreground hover:text-destructive transition-colors"
onClick={(e) => {
e.stopPropagation()
setPendingDeleteRunId(run.id)
}}
aria-label="Delete chat"
>
<Trash2 className="size-3.5" />
</button>
)}
</div>
</SidebarMenuButton>
</SidebarMenuItem>
@ -1077,6 +1090,35 @@ function TasksSection({
</>
)}
</SidebarGroupContent>
{/* Delete confirmation dialog */}
<Dialog open={!!pendingDeleteRunId} onOpenChange={(open) => { if (!open) setPendingDeleteRunId(null) }}>
<DialogContent showCloseButton={false} className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Delete chat</DialogTitle>
<DialogDescription>
Are you sure you want to delete this chat?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" size="sm" onClick={() => setPendingDeleteRunId(null)}>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => {
if (pendingDeleteRunId) {
actions?.onDeleteRun(pendingDeleteRunId)
}
setPendingDeleteRunId(null)
}}
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</SidebarGroup>
)
}

View file

@ -12,6 +12,7 @@ export interface IRunsRepo {
fetch(id: string): Promise<z.infer<typeof Run>>;
list(cursor?: string): Promise<z.infer<typeof ListRunsResponse>>;
appendEvents(runId: string, events: z.infer<typeof RunEvent>[]): Promise<void>;
delete(id: string): Promise<void>;
}
/**
@ -236,4 +237,9 @@ export class FSRunsRepo implements IRunsRepo {
...(nextCursor ? { nextCursor } : {}),
};
}
async delete(id: string): Promise<void> {
const filePath = path.join(WorkDir, 'runs', `${id}.jsonl`);
await fsp.unlink(filePath);
}
}

View file

@ -6,6 +6,7 @@ import { IRunsRepo } from "./repo.js";
import { IAgentRuntime } from "../agents/runtime.js";
import { IBus } from "../application/lib/bus.js";
import { IAbortRegistry } from "./abort-registry.js";
import { IRunsLock } from "./lock.js";
import { forceCloseAllMcpClients } from "../mcp/mcp.js";
export async function createRun(opts: z.infer<typeof CreateRunOptions>): Promise<z.infer<typeof Run>> {
@ -65,6 +66,19 @@ export async function stop(runId: string, force: boolean = false): Promise<void>
// This avoids duplicate events and ensures proper sequencing.
}
export async function deleteRun(runId: string): Promise<void> {
const runsLock = container.resolve<IRunsLock>('runsLock');
if (!await runsLock.lock(runId)) {
throw new Error(`Cannot delete run ${runId}: run is currently active`);
}
try {
const repo = container.resolve<IRunsRepo>('runsRepo');
await repo.delete(runId);
} finally {
await runsLock.release(runId);
}
}
export async function fetchRun(runId: string): Promise<z.infer<typeof Run>> {
const repo = container.resolve<IRunsRepo>('runsRepo');
return repo.fetch(runId);

View file

@ -173,6 +173,12 @@ const ipcSchemas = {
}),
res: ListRunsResponse,
},
'runs:delete': {
req: z.object({
runId: z.string(),
}),
res: z.object({ success: z.boolean() }),
},
'runs:events': {
req: z.null(),
res: z.null(),