From 6604f183fe551bf99e67240aefcdfeab8739beec Mon Sep 17 00:00:00 2001 From: ParkerES Date: Sun, 12 Apr 2026 00:14:25 -0400 Subject: [PATCH 001/489] feat(alerts): add UPDATE.ALERT IPC channel, Zod schema, service method, and handler Co-Authored-By: Claude Sonnet 4.6 --- src/main/features/alerts/alert-handlers.ts | 2 ++ src/main/features/alerts/alert-service.ts | 23 ++++++++++++++++++++++ src/shared/ipc/misc/alerts.channels.ts | 1 + src/shared/ipc/misc/alerts.contract.ts | 13 ++++++++++++ 4 files changed, 39 insertions(+) diff --git a/src/main/features/alerts/alert-handlers.ts b/src/main/features/alerts/alert-handlers.ts index 8d18a6af..02829291 100644 --- a/src/main/features/alerts/alert-handlers.ts +++ b/src/main/features/alerts/alert-handlers.ts @@ -14,6 +14,8 @@ export function registerAlertHandlers(router: IpcRouter, service: AlertService): router.handle(ALERTS.CREATE.ALERT, (data) => Promise.resolve(service.createAlert(data))); + router.handle(ALERTS.UPDATE.ALERT, (data) => Promise.resolve(service.updateAlert(data))); + router.handle(ALERTS.DISMISS.ALERT, ({ id }) => Promise.resolve(service.dismissAlert(id))); router.handle(ALERTS.DELETE.ALERT, ({ id }) => Promise.resolve(service.deleteAlert(id))); diff --git a/src/main/features/alerts/alert-service.ts b/src/main/features/alerts/alert-service.ts index c29ebc0e..60d9fd8d 100644 --- a/src/main/features/alerts/alert-service.ts +++ b/src/main/features/alerts/alert-service.ts @@ -27,6 +27,7 @@ const logger = createScopedLogger('alert-service'); export interface AlertService { createAlert: (data: CreateAlertInput) => Alert; listAlerts: (includeExpired?: boolean) => Alert[]; + updateAlert: (data: UpdateAlertInput) => Alert; dismissAlert: (id: string) => Alert; deleteAlert: (id: string) => { success: boolean }; checkAlerts: () => void; @@ -43,6 +44,14 @@ interface CreateAlertInput { linkedTo?: AlertLinkedTo; } +interface UpdateAlertInput { + id: string; + message?: string; + triggerAt?: string; + recurring?: RecurringConfig | null; + linkedTo?: AlertLinkedTo; +} + function migrateFromJson(db: AdcDatabase, dataDir: string): void { const existing = db.select().from(alerts).limit(1).all(); if (existing.length > 0) return; @@ -210,6 +219,20 @@ export function createAlertService(deps: { return allAlerts.filter((a) => !a.dismissed || new Date(a.triggerAt) > now); }, + updateAlert(data) { + const { id, ...fields } = data; + const updates: Partial = {}; + if (fields.message !== undefined) updates.message = fields.message; + if (fields.triggerAt !== undefined) updates.triggerAt = fields.triggerAt; + if ('recurring' in fields) updates.recurring = fields.recurring ?? null; + if (fields.linkedTo !== undefined) updates.linkedTo = fields.linkedTo ?? null; + + db.update(alerts).set(updates).where(eq(alerts.id, id)).run(); + const updated = findAlert(id); + router.emit(ALERTS_EVENTS.ALERT.CHANGED, { alertId: id }); + return updated; + }, + dismissAlert(id) { const alert = findAlert(id); db.update(alerts).set({ dismissed: true }).where(eq(alerts.id, id)).run(); diff --git a/src/shared/ipc/misc/alerts.channels.ts b/src/shared/ipc/misc/alerts.channels.ts index 518753bd..24c742ce 100644 --- a/src/shared/ipc/misc/alerts.channels.ts +++ b/src/shared/ipc/misc/alerts.channels.ts @@ -3,6 +3,7 @@ import { domain, events } from '../channel-builder'; export const ALERTS = domain('alerts', { LIST: ['all'], CREATE: ['alert'], + UPDATE: ['alert'], DISMISS: ['alert'], DELETE: ['alert'], }); diff --git a/src/shared/ipc/misc/alerts.contract.ts b/src/shared/ipc/misc/alerts.contract.ts index 5f9598d6..2a86b743 100644 --- a/src/shared/ipc/misc/alerts.contract.ts +++ b/src/shared/ipc/misc/alerts.contract.ts @@ -34,6 +34,15 @@ export const AlertSchema = z.object({ createdAt: z.string(), }); +export type UpdateAlertInput = z.infer; +export const UpdateAlertInputSchema = z.object({ + id: z.string(), + message: z.string().optional(), + triggerAt: z.string().optional(), + recurring: RecurringConfigSchema.nullable().optional(), + linkedTo: AlertLinkedToSchema.optional(), +}); + export const alertsInvoke = { [ALERTS.LIST.ALL]: { input: z.object({ includeExpired: z.boolean().optional() }), @@ -50,6 +59,10 @@ export const alertsInvoke = { }), output: AlertSchema, }, + [ALERTS.UPDATE.ALERT]: { + input: UpdateAlertInputSchema, + output: AlertSchema, + }, [ALERTS.DISMISS.ALERT]: { input: z.object({ id: z.string() }), output: AlertSchema, From 055eb5e184440d6f17de411a97dcb62546e32f7c Mon Sep 17 00:00:00 2001 From: ParkerES Date: Sun, 12 Apr 2026 00:14:28 -0400 Subject: [PATCH 002/489] feat(dashboard): add UPDATE.CAPTURE IPC channel and service method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add UPDATE verb to DASHBOARD channel builder - Add UpdateCaptureInputSchema (id, text) to dashboard schemas - Wire update channel in contract with CaptureSchema output - Add updateCapture(id, text) method to DashboardService - Register thin handler in dashboard-handlers.ts Note: MAX_RECENT = 5 only exists in renderer QuickCapture.tsx (display limit), not in the service — no service-layer limit to remove. Co-Authored-By: Claude Sonnet 4.6 --- src/main/features/dashboard/dashboard-handlers.ts | 4 ++++ src/main/features/dashboard/dashboard-service.ts | 14 ++++++++++++++ src/shared/ipc/dashboard/channels.ts | 1 + src/shared/ipc/dashboard/contract.ts | 6 +++++- src/shared/ipc/dashboard/schemas.ts | 5 +++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/features/dashboard/dashboard-handlers.ts b/src/main/features/dashboard/dashboard-handlers.ts index 09cf462e..76f0fa8e 100644 --- a/src/main/features/dashboard/dashboard-handlers.ts +++ b/src/main/features/dashboard/dashboard-handlers.ts @@ -14,6 +14,10 @@ export function registerDashboardHandlers(router: IpcRouter, service: DashboardS Promise.resolve(service.createCapture(text, id)), ); + router.handle(DASHBOARD.UPDATE.CAPTURE, ({ id, text }) => + Promise.resolve(service.updateCapture(id, text)), + ); + router.handle(DASHBOARD.DELETE.CAPTURE, ({ id }) => Promise.resolve(service.deleteCapture(id)), ); diff --git a/src/main/features/dashboard/dashboard-service.ts b/src/main/features/dashboard/dashboard-service.ts index 87bf8f83..0644d774 100644 --- a/src/main/features/dashboard/dashboard-service.ts +++ b/src/main/features/dashboard/dashboard-service.ts @@ -30,6 +30,7 @@ export interface Capture { export interface DashboardService { listCaptures: () => Capture[]; createCapture: (text: string, id?: string) => Capture; + updateCapture: (id: string, text: string) => Capture; deleteCapture: (id: string) => { success: boolean }; } @@ -85,6 +86,19 @@ export function createDashboardService(deps: { return capture; }, + updateCapture(id, text) { + const result = db.update(captures).set({ text }).where(eq(captures.id, id)).run(); + if (result.changes === 0) { + throw new Error(`Capture not found: ${id}`); + } + const updated = db.select().from(captures).where(eq(captures.id, id)).get(); + if (!updated) { + throw new Error(`Capture not found after update: ${id}`); + } + router.emit(DASHBOARD_EVENTS.CAPTURE.CHANGED, { captureId: id }); + return updated; + }, + deleteCapture(id) { const result = db.delete(captures).where(eq(captures.id, id)).run(); if (result.changes === 0) { diff --git a/src/shared/ipc/dashboard/channels.ts b/src/shared/ipc/dashboard/channels.ts index 0bedbbd1..7b34d9f9 100644 --- a/src/shared/ipc/dashboard/channels.ts +++ b/src/shared/ipc/dashboard/channels.ts @@ -3,6 +3,7 @@ import { domain, events } from '../channel-builder'; export const DASHBOARD = domain('dashboard', { LIST: ['captures'], CREATE: ['capture'], + UPDATE: ['capture'], DELETE: ['capture'], }); diff --git a/src/shared/ipc/dashboard/contract.ts b/src/shared/ipc/dashboard/contract.ts index 040615bc..615c4c3b 100644 --- a/src/shared/ipc/dashboard/contract.ts +++ b/src/shared/ipc/dashboard/contract.ts @@ -9,7 +9,7 @@ import { z } from 'zod'; import { SuccessResponseSchema } from '../common/schemas'; import { DASHBOARD, DASHBOARD_EVENTS } from './channels'; -import { CaptureSchema } from './schemas'; +import { CaptureSchema, UpdateCaptureInputSchema } from './schemas'; export const dashboardInvoke = { [DASHBOARD.LIST.CAPTURES]: { @@ -20,6 +20,10 @@ export const dashboardInvoke = { input: z.object({ id: z.string().optional(), text: z.string() }), output: CaptureSchema, }, + [DASHBOARD.UPDATE.CAPTURE]: { + input: UpdateCaptureInputSchema, + output: CaptureSchema, + }, [DASHBOARD.DELETE.CAPTURE]: { input: z.object({ id: z.string() }), output: SuccessResponseSchema, diff --git a/src/shared/ipc/dashboard/schemas.ts b/src/shared/ipc/dashboard/schemas.ts index 6ec3f491..1af727fa 100644 --- a/src/shared/ipc/dashboard/schemas.ts +++ b/src/shared/ipc/dashboard/schemas.ts @@ -11,3 +11,8 @@ export const CaptureSchema = z.object({ text: z.string(), createdAt: z.string(), }); + +export const UpdateCaptureInputSchema = z.object({ + id: z.string(), + text: z.string(), +}); From 94c416cb388324bbddd72c8ae25c53c69d340bd1 Mon Sep 17 00:00:00 2001 From: ParkerES Date: Sun, 12 Apr 2026 00:24:41 -0400 Subject: [PATCH 003/489] feat(progress): add pr_number, pr_status, jira_url columns; drop tags and branch Migration 0012 adds three columns that round-trip through the IPC layer and removes two unused columns. Updates schema.ts, rowToTask(), updateTask(), createTask(), and migrateFromFilesystem() to match. Co-Authored-By: Claude Sonnet 4.6 --- drizzle/0012_progress_tasks_schema.sql | 8 ++++++++ drizzle/meta/_journal.json | 7 +++++++ src/main/features/progress/progress-service.ts | 12 +++++++++--- src/main/features/progress/schema.ts | 7 ++++--- 4 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 drizzle/0012_progress_tasks_schema.sql diff --git a/drizzle/0012_progress_tasks_schema.sql b/drizzle/0012_progress_tasks_schema.sql new file mode 100644 index 00000000..069bc08e --- /dev/null +++ b/drizzle/0012_progress_tasks_schema.sql @@ -0,0 +1,8 @@ +-- Add pr_number, pr_status, jira_url columns to progress_tasks +-- Drop unused tags and branch columns + +ALTER TABLE progress_tasks ADD COLUMN pr_number INTEGER; +ALTER TABLE progress_tasks ADD COLUMN pr_status TEXT; +ALTER TABLE progress_tasks ADD COLUMN jira_url TEXT; +ALTER TABLE progress_tasks DROP COLUMN tags; +ALTER TABLE progress_tasks DROP COLUMN branch; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c8cb2e9f..9f65af14 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1775843200000, "tag": "0011_uuid_natural_key_tables", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1775843300000, + "tag": "0012_progress_tasks_schema", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/main/features/progress/progress-service.ts b/src/main/features/progress/progress-service.ts index a67f5ea5..f4c8f3a1 100644 --- a/src/main/features/progress/progress-service.ts +++ b/src/main/features/progress/progress-service.ts @@ -255,10 +255,11 @@ async function migrateFromFilesystem(db: AdcDatabase, projectPath: string): Prom title: task.title, status: task.status, priority: task.priority, - tags: [], jiraKey: task.jiraTicket ?? null, + jiraUrl: task.jiraUrl ?? null, prUrl: task.prUrl ?? null, - branch: null, + prNumber: task.prNumber ?? null, + prStatus: task.prStatus ?? null, lastSessionId: task.lastSessionId ?? null, lastAgentName: task.lastAgentName ?? null, completedAt: task.completedAt ?? null, @@ -391,7 +392,10 @@ function rowToTask( status: row.status as ProgressStatus, priority: row.priority as ProgressPriority, jiraTicket: row.jiraKey ?? undefined, + jiraUrl: row.jiraUrl ?? undefined, prUrl: row.prUrl ?? undefined, + prNumber: row.prNumber ?? undefined, + prStatus: row.prStatus ?? undefined, lastSessionId: row.lastSessionId ?? undefined, lastAgentName: row.lastAgentName ?? undefined, completedAt: row.completedAt ?? undefined, @@ -827,7 +831,6 @@ export function createProgressService( title, status: 'backlog', priority, - tags: [], description: description || null, createdAt: now, updatedAt: now, @@ -905,7 +908,10 @@ export function createProgressService( if (updates.status !== undefined) dbUpdates.status = updates.status; if (updates.priority !== undefined) dbUpdates.priority = updates.priority; if (updates.jiraTicket !== undefined) dbUpdates.jiraKey = updates.jiraTicket; + if (updates.jiraUrl !== undefined) dbUpdates.jiraUrl = updates.jiraUrl; if (updates.prUrl !== undefined) dbUpdates.prUrl = updates.prUrl; + if (updates.prNumber !== undefined) dbUpdates.prNumber = updates.prNumber; + if (updates.prStatus !== undefined) dbUpdates.prStatus = updates.prStatus; if (updates.lastSessionId !== undefined) dbUpdates.lastSessionId = updates.lastSessionId; if (updates.lastAgentName !== undefined) dbUpdates.lastAgentName = updates.lastAgentName; if (updates.completedAt !== undefined) dbUpdates.completedAt = updates.completedAt; diff --git a/src/main/features/progress/schema.ts b/src/main/features/progress/schema.ts index 3576abde..5c3a0f31 100644 --- a/src/main/features/progress/schema.ts +++ b/src/main/features/progress/schema.ts @@ -1,4 +1,4 @@ -import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; export const progressTasks = sqliteTable('progress_tasks', { slug: text('slug').primaryKey(), @@ -6,10 +6,11 @@ export const progressTasks = sqliteTable('progress_tasks', { title: text('title').notNull(), status: text('status').notNull(), priority: text('priority').notNull().default('medium'), - tags: text('tags', { mode: 'json' }).$type().notNull().default([]), jiraKey: text('jira_key'), + jiraUrl: text('jira_url'), prUrl: text('pr_url'), - branch: text('branch'), + prNumber: integer('pr_number'), + prStatus: text('pr_status'), lastSessionId: text('last_session_id'), lastAgentName: text('last_agent_name'), completedAt: text('completed_at'), From 09c57561715bba061b9e16a826d162a45828bda2 Mon Sep 17 00:00:00 2001 From: ParkerES Date: Sun, 12 Apr 2026 00:42:56 -0400 Subject: [PATCH 004/489] feat(tasks): add EditProgressTaskDialog wired to grid row and detail row Co-Authored-By: Claude Sonnet 4.6 --- .../components/EditProgressTaskDialog.tsx | 215 ++++++++++++++++++ .../detail/ProgressTaskDetailRow.tsx | 20 +- .../components/grid/ProgressTaskGrid.tsx | 45 +++- 3 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 src/renderer/features/tasks/components/EditProgressTaskDialog.tsx diff --git a/src/renderer/features/tasks/components/EditProgressTaskDialog.tsx b/src/renderer/features/tasks/components/EditProgressTaskDialog.tsx new file mode 100644 index 00000000..bf264169 --- /dev/null +++ b/src/renderer/features/tasks/components/EditProgressTaskDialog.tsx @@ -0,0 +1,215 @@ +/** + * EditProgressTaskDialog — Edit title, description, priority, and status + * for an existing progress task. + * + * Opened from: + * - ProgressTaskGrid row action column (edit icon button) + * - ProgressTaskDetailRow footer edit button + */ + +import { useState } from 'react'; + +import type { ProgressPriority, ProgressStatus, ProgressTask } from '@shared/types/progress'; + +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Spinner, + Text, + Textarea, +} from '@ui'; + +import { useUpdateProgressTask } from '../api/useProgressMutations'; + +// ── Constants ─────────────────────────────────────────────── + +const PRIORITY_OPTIONS: Array<{ value: ProgressPriority; label: string }> = [ + { value: 'low', label: 'Low' }, + { value: 'normal', label: 'Normal' }, + { value: 'high', label: 'High' }, + { value: 'urgent', label: 'Urgent' }, +]; + +const STATUS_OPTIONS: Array<{ value: ProgressStatus; label: string }> = [ + { value: 'backlog', label: 'Backlog' }, + { value: 'researching', label: 'Researching' }, + { value: 'research_done', label: 'Research Done' }, + { value: 'planning', label: 'Planning' }, + { value: 'plan_ready', label: 'Plan Ready' }, + { value: 'executing', label: 'Executing' }, + { value: 'review', label: 'Review' }, + { value: 'done', label: 'Done' }, + { value: 'error', label: 'Error' }, +]; + +// ── Props ─────────────────────────────────────────────────── + +interface EditProgressTaskDialogProps { + task: ProgressTask; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +// ── Component ─────────────────────────────────────────────── + +export function EditProgressTaskDialog({ task, open, onOpenChange }: EditProgressTaskDialogProps) { + const updateTask = useUpdateProgressTask(); + + const [title, setTitle] = useState(task.title); + const [description, setDescription] = useState(task.description); + const [priority, setPriority] = useState(task.priority); + const [status, setStatus] = useState(task.status); + const [error, setError] = useState(null); + + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) { + // Reset form to task values on close + setTitle(task.title); + setDescription(task.description); + setPriority(task.priority); + setStatus(task.status); + setError(null); + } + onOpenChange(nextOpen); + } + + async function handleSubmit(e: React.SyntheticEvent) { + e.preventDefault(); + + if (title.trim().length === 0) { + setError('Title is required.'); + return; + } + + setError(null); + + const updates: { + title?: string; + description?: string; + priority?: ProgressPriority; + status?: ProgressStatus; + } = {}; + + if (title.trim() !== task.title) updates.title = title.trim(); + if (description.trim() !== task.description) updates.description = description.trim(); + if (priority !== task.priority) updates.priority = priority; + if (status !== task.status) updates.status = status; + + try { + await updateTask.mutateAsync({ slug: task.slug, updates }); + handleOpenChange(false); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update task.'); + } + } + + const isSubmitting = updateTask.isPending; + + return ( + + + + Edit Task + + Update title, description, priority, and status for this task. + + + +
+
+ + setTitle(e.target.value)} + /> +
+ +
+ +