Files
adiuvAI/AI_REFACTOR_PLAN.md
roberto b06f5f6022 step 6.1 complete: auth gate in AppShell + LoginForm
- LoginForm.tsx: centered login/register screen with spring animations
- AppShell: queries auth.status on startup; renders LoginForm full-screen when authenticated === false; passes through while loading to avoid flicker
- Settings AccountSection: removed inline login form (AppShell now gates auth); always shows account info + sign out

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:54:44 +01:00

27 KiB
Raw Blame History

AI Refactor Plan — Adiuva Electron App

Objective: Transform the Electron app into a backend-powered client. All AI intelligence (chat, tool calling, embeddings) lives on the backend. The Electron app owns the local database, executes structured CRUD operations from backend tools via Drizzle ORM, and handles auth, backup, and offline graceful degradation.

Backend: Lives at ../adiuva-api/. FastAPI + LiteLLM + 4 chat agents (task, checkpoint, project, note). Backend plan: ../adiuva-api/AI_REFACTOR_PLAN.md.

Protocol: Execute steps sequentially. Each step is atomic and committable. Mark [x] when done.


Architecture

Renderer (React 19)  ──ipcLink──►  Main (tRPC + SQLite)  ──HTTP/WS──►  Backend (FastAPI + LiteLLM)
     UI only                        Data + Drizzle executor             All AI intelligence

Data flow for chat (bidirectional WebSocket):

  1. User types message in renderer → tRPC ai.chat mutation
  2. Main process builds ChatContext (queries SQLite for tasks, notes, profile)
  3. Main opens WS to backend /api/v1/chat/stream?token=<jwt>, sends chat_request frame
  4. Backend classifies intent → routes to agent → agent calls LLM with tools
  5. LLM calls a tool (e.g. list_tasks) → tool calls execute_on_client():
    • Backend sends tool_call frame: {id, action:"select", table:"tasks", filters:{...}}
    • Electron receives frame → Drizzle executor: db.select().from(tasks).where(...) → real rows
    • Electron sends tool_result frame: {id, rows: [{id, title, ...}, ...]}
    • Tool receives real data → returns formatted string to LLM
  6. Steps 5 repeats (max 5 iterations) until LLM has enough data to respond
  7. Backend streams response text → text_chunk frames → main forwards via ai:stream IPC → renderer
  8. Backend sends final frame: {"done": true, "response": "..."}

No local LLM. When offline, AI features show "You're offline" — all other features (tasks, notes, projects) work normally.


WS Protocol — Typed Frames

Direction type Payload
Client → Server chat_request { message, context }
Server → Client text_chunk { text: string }
Server → Client tool_call { id, action, table?, data?, filters?, vector?, limit? }
Client → Server tool_result { id, row?, rows?, results?, deleted?, ok?, error? }
Server → Client final { response: string }
Server → Client ping {}

Tool call actions (Electron → Drizzle mapping):

action Drizzle call Returns
select db.select().from(table).where(filters).all() { rows: [...] }
get db.select().from(table).where(eq(id, ...)).get() { row: {...} | null }
insert db.insert(table).values({id: uuid(), ...data, createdAt: now()}).returning().get() { row: {...} }
update db.update(table).set(data.updates).where(eq(id,...)).returning().get() { row: {...} }
delete db.delete(table).where(eq(id,...)).run() { deleted: true }
vector_upsert LanceDB delete-then-add with pre-computed vector { ok: true }
vector_search LanceDB table.search(vector).limit(n) { results: [{id, content, score}...] }

Electron generates id (UUID v4) and createdAt/updatedAt (Unix ms) for inserts. Backend never generates IDs.


Phase 0 — API Contracts & Types

Step 0.1 — Define backend API contract types

  • Create src/shared/api-types.ts with Zod schemas + inferred types
  • Create src/shared/batch-types.ts with batch builder + storage types
  • Update tsconfig.json paths — added @shared/* alias
  • Outcome: Type-safe contracts for all backend communication.

Phase 1 — Auth & Backend Client

Step 1.1 — Align shared types with backend schemas

  • Update src/shared/api-types.ts to match backend app/schemas.py exactly:
    • AuthTokens.expiresAt: change from z.string().datetime() to z.number().int() (Unix epoch)
    • ChatContext: replace with backend's flat structure — { userProfile, relevantDocuments, recentTasks, conversationHistory }; remove UI-only fields (type, projectId, uiContext)
    • Remove PlanAction entirely — no more action descriptors
    • ChatResponse: just { response: string } — no actions array
    • Align PlanStep / ExecutionPlan with backend or remove if plan mode is deferred
  • Add WebSocket frame Zod schemas:
    • ToolCallAction enum: select, get, insert, update, delete, vector_upsert, vector_search
    • WsToolCall: { type: "tool_call", id: string, action, table?, data?, filters?, vector?, limit? }
    • WsToolResult: { type: "tool_result", id: string, row?, rows?, results?, deleted?, ok?, error? }
    • WsTextChunk, WsFinal, WsPing, WsChatRequest
    • WsServerFrame / WsClientFrame discriminated unions
  • Create src/shared/casing.ts:
    • toSnakeCase(obj) — deep-converts camelCase keys to snake_case (outgoing)
    • toCamelCase(obj) — deep-converts snake_case keys to camelCase (incoming)
  • Create UIChatContext type in src/renderer/hooks/useAIChat.ts for renderer-only fields
  • Files: src/shared/api-types.ts, src/shared/casing.ts, src/renderer/hooks/useAIChat.ts
  • Outcome: Shared types match the live backend 1:1. WS frames are fully typed.

Step 1.2 — Auth manager + tRPC procedures

  • Create src/main/auth/auth-manager.ts:
    • AuthManager class (singleton):
      • register(email, password): Promise<AuthTokens> — POST /api/v1/auth/register
      • login(email, password): Promise<AuthTokens> — POST /api/v1/auth/login
      • logout(): void — clears stored tokens
      • getAccessToken(): string | null — current JWT
      • refreshToken(): Promise<void> — POST /api/v1/auth/refresh
      • isAuthenticated(): boolean
      • getProfile(): Promise<UserProfile> — GET /api/v1/auth/me
    • Token storage: reuse src/main/ai/token.ts (safeStorage + electron-store fallback)
    • Auto-refresh: check token expiry on every getAccessToken() call; if < 5 min remaining, refresh in background
  • Add authRouter tRPC sub-router to src/main/router/index.ts
  • Update src/main/store.ts: add backendUrl: string
  • Files: src/main/auth/auth-manager.ts, src/main/router/index.ts, src/main/store.ts
  • Outcome: Electron can authenticate with the backend. JWTs stored securely.

Step 1.3 — Backend client with bidirectional WebSocket

  • Create src/main/api/backend-client.ts:
    • BackendClient class (singleton):
      • Constructor: reads backendUrl from store, gets JWT from AuthManager
      • chatStream(request: ChatRequest, onChunk: (text: string) => void): Promise<ChatResponse>:
        1. Opens WS to /api/v1/chat/stream?token=<jwt>
        2. Sends { type: "chat_request", ... } frame
        3. Message loop:
          • text_chunk → calls onChunk(text)
          • tool_call → calls DrizzleExecutor.execute(payload), sends back { type: "tool_result", id, ... }
          • final → resolves with { response }
          • ping → ignore
      • isOnline(): Promise<boolean> — GET /api/v1/health with 3s timeout
      • embedText(text: string): Promise<number[]> — POST /api/v1/storage/vectors/embed
      • All requests include Authorization: Bearer <jwt> header
      • Auto-retry with exponential backoff (max 3 attempts) for non-auth errors
      • Response parsing: toCamelCase() on all incoming JSON
      • Request serialization: toSnakeCase() on all outgoing JSON
    • Error categorization: 401 → AuthExpiredError, 429 → RateLimitError, 5xx → ServerError, timeout → OfflineError
  • Files: src/main/api/backend-client.ts
  • Outcome: Type-safe HTTP + bidirectional WS client. Tool calls handled in the message loop.

Step 1.4 — Drizzle executor (the dumb Electron layer)

  • Create src/main/api/drizzle-executor.ts:
    • Table registry: map string names → Drizzle table objects from src/main/db/schema.ts:
      { tasks, projects, clients, checkpoints, notes, taskComments }
      
    • execute(payload): Promise<object> — dispatches on payload.action:
      • select: db.select().from(table) + build .where() from payload.filters using Drizzle eq()/and()/like() + optional .orderBy() → returns { rows }
      • get: db.select().from(table).where(eq(table.id, payload.data.id)).get() → returns { row }
      • insert: db.insert(table).values({id: crypto.randomUUID(), ...payload.data, createdAt: Date.now()}).returning().get() → returns { row }
      • update: db.update(table).set(payload.data.updates).where(eq(table.id, payload.data.id)).returning().get() → returns { row }
      • delete: db.delete(table).where(eq(table.id, payload.data.id)).run() → returns { deleted: true }
      • vector_upsert: calls upsertWithVector() from vectordb.ts with pre-computed vector → returns { ok: true }
      • vector_search: LanceDB table.search(payload.vector).limit(payload.limit) → returns { results }
    • Filter builder: maps {key: value} objects → Drizzle and(eq(table[key], value), ...). Special cases:
      • null value → isNull(table[key])
      • search key → like(table.title, '%value%') or like(table.content, '%value%')
      • orderBy key → .orderBy(asc(table[field])) or .orderBy(desc(...))
      • includeArchived: false → adds eq(table.status, 'active') filter
      • dueDateFrom/dueDateTobetween(table.dueDate, from, to)
    • Security: validate table against registry (reject unknown), validate action against enum
    • Uses getDb() from src/main/db/index.ts — same Drizzle instance as everywhere else
  • Files: src/main/api/drizzle-executor.ts
  • Outcome: ~120 lines. Backend sends structured ops, Electron maps to Drizzle. No SQL building.

Step 1.5 — Refactor orchestrator to delegate to backend

  • Replace src/main/ai/orchestrator.ts entirely (996 lines → ~190 lines):
    • orchestrate({ message, context, sender }):
      1. Check BackendClient.isOnline() — if offline, return { response: '', error: 'You are offline.' }
      2. Check AuthManager.isAuthenticated() — if not, return { response: '', error: 'Please log in.' }
      3. Build ChatContext from local SQLite (userProfile, recentTasks, conversationHistory)
      4. Call BackendClient.chatStream(request, chunk => sendStreamChunk(sender, chunk, false))
        • tool_call frames handled inside the WS message loop (Step 1.3)
      5. On completion: sendStreamChunk(sender, '', true)
    • No PlanRunner, no action handling — writes happen mid-conversation via tool calls
    • Keep sendStreamChunk() IPC helper
    • Export orchestrate() and dailyBrief()
  • Update aiRouter in src/main/router/index.ts:
    • Remove setToken mutation and hasToken query (replaced by auth.status)
    • Keep chat mutation (same interface) and dailyBrief
  • Update src/renderer/components/ai/AIChatPanel.tsx:
    • Replace trpc.ai.hasToken.useQuery() with trpc.auth.status.useQuery()
    • Update auth-gate condition and daily brief trigger to use authStatusQuery.data?.authenticated
    • Replace KeyRound icon + provider-config messaging with LogIn icon + login messaging
  • Files: src/main/ai/orchestrator.ts, src/main/router/index.ts, src/renderer/hooks/useAIChat.ts
  • Outcome: ~916 lines removed. Chat works through backend. All tool execution is bidirectional.

Step 1.6 — Migrate embeddings to backend

  • Update src/main/db/vectordb.ts:
    • Add upsertWithVector(noteId, projectId, content, vector) — takes pre-computed vector, stores in LanceDB
    • Update upsertNoteEmbedding() → calls BackendClient.embedText(content)upsertWithVector()
    • Keep searchNotes() and migrateNotesIfNeeded() (migration will call backend for embeddings)
    • If offline: skip embedding (next edit will re-embed when online)
    • Add searchNotesByVector(vector, limit) for direct pre-computed-vector search
  • Update src/main/api/drizzle-executor.ts: use searchNotesByVector with pre-computed vector from tool call payload
  • Delete src/main/ai/embeddings.ts
  • Files: src/main/db/vectordb.ts, src/main/api/drizzle-executor.ts, src/main/ai/embeddings.ts (deleted)
  • Outcome: Embeddings generated by backend /vectors/embed. Local LanceDB for storage + search.

Phase 2 — Remove Local AI Stack

Step 2.1 — Remove local AI code and dependencies

  • Delete src/main/ai/llm.ts, src/main/ai/chat-copilot.ts, src/main/ai/copilot.ts, src/main/ai/provider.ts
  • Remove import './ai/copilot' and initAI() from src/main/index.ts
  • Remove deps: @langchain/core, @langchain/openai, @langchain/anthropic, @langchain/langgraph, @github/copilot-sdk
  • Clean up src/main/store.ts (remove aiProvider; kept encryptedTokens — still used by token.tsauth-manager.ts for JWT storage)
  • Clean up vite.main.config.mts (remove externalized LangChain/Copilot packages)
  • Clean up forge.config.ts (remove LangChain/Copilot from externalPackages; remove copilot-sdk clipboard cleanup block)
  • Files: src/main/ai/{llm,chat-copilot,copilot,provider}.ts (deleted), package.json, src/main/index.ts, src/main/store.ts, vite.main.config.mts, forge.config.ts
  • Outcome: 34 npm packages removed. No LangChain, no Copilot SDK, no local LLM.

Phase 3 — Agent System (Local Directory + Cloud Connectors)

Two agent types at launch: Local Directory Agent (watches folders, Electron reads + pre-processes, backend runs AI) and Cloud Connector Agent (Gmail, Teams — 100% backend-managed). All configs live on the backend (synced, device-bound for local agents). Backend triggers agent runs via new WS frames when Electron is connected. Extracted data inserts into existing tables (tasks, notes, checkpoints) with isAiSuggested=1. Configuration prompts are built via a dedicated "Chatbot Journey" (multi-turn AI conversation on a dedicated page).

Backend Phase 3 plan: ../adiuva-api/AI_REFACTOR_PLAN.md Phase 3 section.

Cloud Agent Flow:
  Backend cron ──► Backend fetches Gmail/Teams ──► Backend AI analyzes
    ──► WS tool_call(insert, table:'tasks') ──► Electron persists locally

Local Agent Flow:
  Backend detects Electron online ──► WS agent_run frame (config + prompt)
    ──► Electron reads files + pre-processes ──► WS agent_data frame (content)
    ──► Backend AI analyzes with user prompt ──► WS tool_call(insert) ──► Electron persists

Key constraints:

  • Local agents only run when Electron is active AND on the device where the path was configured
  • Cloud agents only push results when Electron is connected (no server-side content storage)
  • All AI communication goes through the backend (no local LLM)
  • Tier gating: free=2 active, pro=10, power/team=unlimited

Step 3.1 — WS frame types + agent handler

  • Update src/shared/api-types.ts:
    • Add WsAgentRun schema: { type: "agent_run", run_id, agent_id, config: { paths, file_extensions, prompt_template, data_types } }
    • Add WsAgentData schema: { type: "agent_data", run_id, files: [{ path, name, content, metadata }] }
    • Add WsAgentComplete schema: { type: "agent_complete", run_id, files_read, errors }
    • Add WsDeviceHello schema: { type: "device_hello", device_id, agent_ids }
    • Extend WsServerFrame discriminated union with agent_run
    • Extend WsClientFrame with agent_data, agent_complete, device_hello
  • Update src/main/api/backend-client.ts:
    • In WS message loop, handle agent_run frames:
      1. Read files from configured paths using the local agent handler (Step 3.2)
      2. Send agent_data frames back with pre-processed content
      3. Continue handling tool_call frames for DB inserts as usual
  • Files: src/shared/api-types.ts, src/main/api/backend-client.ts
  • Outcome: Electron can receive agent trigger frames and respond with file data.

Step 3.2 — Local file reader

  • Create src/main/agents/file-reader.ts:
    • readDirectory(paths: string[], extensions: string[]): AsyncGenerator<FileData> — recursively reads configured directories, filters by extension
    • preProcess(filePath: string): { name, content, metadata }:
      • .txt, .md, .eml — read as text
      • .pdf — text extraction (dep: pdf-parse)
      • .docx — text extraction (dep: mammoth)
      • .csv, .json — read as structured text
      • Binary files: skip with warning
    • Respects path boundaries (no symlink escape, no .. traversal)
    • Chunks large files (>50KB) to stay within LLM context limits
    • Returns { path, name, content, metadata: { size, mtime, extension } }
  • Update BackendClient.handleAgentRun() to call readAgentFiles() and return { files, errors, filesRead }
  • Files: src/main/agents/file-reader.ts, src/main/api/backend-client.ts, package.json (pdf-parse, mammoth added)
  • Dependencies: pdf-parse, mammoth
  • Outcome: Electron can safely read + pre-process local files for AI analysis.

Step 3.3 — Device ID management

  • Update src/main/store.ts: add deviceId: string (UUID generated once on first launch and persisted)
  • Add getDeviceId() helper — lazily generates UUID v4 on first call, persists it; subsequent calls return the same value
  • Add settings.deviceId tRPC query to settingsRouter — renderer can read the device ID; Step 3.4 (agent router) injects it into local agent config creation calls to the backend
  • Electron sends deviceId when creating local agent configs → backend stores it (Step 3.4)
  • When backend triggers a local agent run, it checks config.device_id matches the connected Electron's deviceId (Step 3.5)
  • Files: src/main/store.ts, src/main/router/index.ts
  • Outcome: Local agents are device-bound. Only triggered on the correct machine.

Step 3.4 — Agent tRPC router

  • Add agentRouter to src/main/router/index.ts:
    • agent.catalog — query: proxy to backend GET /api/v1/agents/catalog
    • agent.local.list / agent.local.create / agent.local.update / agent.local.delete — proxy to backend with deviceId injected
    • agent.cloud.list / agent.cloud.create / agent.cloud.update / agent.cloud.delete — proxy to backend
    • agent.runs — query: proxy to backend run log
    • agent.runNow — mutation: proxy to backend manual trigger
    • agent.journey.start / agent.journey.message — proxy chatbot journey endpoints
    • All proxy calls include JWT from AuthManager + snake_case/camelCase conversion
  • Also added response schemas to src/shared/api-types.ts: AgentCatalogItemSchema, LocalAgentConfigSchema, CloudAgentConfigSchema, AgentRunLogSchema, JourneyMessageSchema
  • Added proxyGet/proxyPost/proxyPut/proxyDelete methods to BackendClient (authenticated, casing-converted HTTP proxies)
  • Files: src/main/router/index.ts, src/shared/api-types.ts, src/main/api/backend-client.ts
  • Outcome: Renderer can manage agents through tRPC — all requests proxied to backend.

Step 3.5 — Persistent WS connection for agent triggers

  • Update src/main/api/backend-client.ts:
    • connectPersistent() — opens persistent WS to /api/v1/ws/device?token=<jwt> on app start
    • On connect: sends device_hello frame with deviceId and active agent IDs
    • Handles incoming agent_run frames → dispatches to file reader → sends agent_data back
    • Handles tool_call frames for DB inserts (same as chat WS)
    • handleAgentRunAndSend() — validates device ID, calls handleAgentRun(), sends agent_data + agent_complete frames
    • Auto-reconnects on disconnect with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s cap)
    • Heartbeat WS-level ping every 30s; pong/message timeout triggers force-reconnect
    • disconnectPersistent() — disables reconnect, clears timers, closes WS cleanly
  • Call connectPersistent() from src/main/index.ts after auth check on app startup
  • will-quit handler in src/main/index.ts calls disconnectPersistent() for clean exit
  • authRouter.login calls connectPersistent() on success
  • authRouter.logout calls disconnectPersistent()
  • Device ID validation in handleAgentRunAndSend() (completes Step 3.3 final checkbox)

Step 3.6 — Agent Library page

  • Created src/renderer/routes/settings.tsx:
    • Settings page with 2-column layout (left nav: General, Account, Agents, Appearance)
    • Agents section is the agent library — catalog grid + my agents list with status indicators
    • Settings icon in sidebar navigates to /settings (replaced dropdown)
    • validateSearch for deep-link to specific section (e.g. ?section=account)
  • Added route to src/renderer/routeTree.gen.ts
  • Updated sidebar nav in src/renderer/components/layout/AppShell.tsx (Settings is now a link)

Step 3.7 — Agent config dialogs

  • LocalAgentConfigPanel component (inline, inside expanded agent row in Settings → Agents):
    • Native dialog.showOpenDialog directory picker (via new dialog:showOpenDialog IPC + window.electronDialog bridge)
    • File extension filter (preset groups + custom)
    • Data type selector (checkboxes: tasks, notes, checkpoints, projects)
    • Schedule picker (preset: every 15min, hourly, 6h, daily, manual)
    • "Customize AI Prompt" button → opens Chatbot Journey dialog
  • CloudAgentConfigPanel component (inline, inside expanded agent row):
    • Provider badge + OAuth placeholder note
    • Data type selector + schedule picker
    • "Customize AI Prompt" button
  • AddAgentDialog for creating new agents from the catalog
  • Added dialog:showOpenDialog IPC handler in src/main/index.ts + window.electronDialog exposed in src/preload/trpc.ts + type declared in src/renderer/lib/ipcLink.ts
  • Files: src/renderer/routes/settings.tsx, src/main/index.ts, src/preload/trpc.ts, src/renderer/lib/ipcLink.ts
  • Outcome: Users can fully configure local and cloud agents from the Settings → Agents section.

Step 3.8 — Chatbot Journey page

  • JourneyDialog component in src/renderer/routes/settings.tsx:
    • Dialog with spring-animated chat interface (message list, input, send button)
    • Starts via agent.journey.start (passes agentType + optional agentId) on mount
    • Multi-turn via agent.journey.message tRPC calls
    • Shows generated prompt preview when done === true / promptTemplate present
    • "Save & apply" button: saves promptTemplate to agent via agent.local.update / agent.cloud.update
    • Works in both Create flow (from AddAgentDialog) and Edit flow (from expanded agent row)
  • Files: src/renderer/routes/settings.tsx
  • Outcome: Users configure AI prompts through a guided conversation, directly inside agent config.

Step 3.9 — Agent run logs UI

  • Create src/renderer/components/agents/AgentRunLog.tsx:
    • Per-agent run history: timestamp, status badge, items processed/created, errors
    • Lazy-loaded (only fetches when agent row is expanded), limit 10 runs
    • Skeleton loading state + "No runs yet" empty state
    • Per-run expandable error list (click to reveal all error strings)
    • Duration display (completedAt - startedAt formatted as Xs / Xm Ys)
    • Data via agent.runs tRPC query
  • Integrated into AgentRow in src/renderer/routes/settings.tsx — replaced inline block
  • Files: src/renderer/components/agents/AgentRunLog.tsx, src/renderer/routes/settings.tsx
  • Outcome: Users see full history and status of each agent's runs with expandable error details.

Phase 4 — Security: E2E Backup & Offline

Step 4.1 — E2E encrypted backup

  • src/main/backup/e2e-crypto.ts + backup-manager.ts
  • Outcome: User data never leaves the device unencrypted.

Step 4.2 — Offline sync queue

  • src/main/backup/sync-queue.ts + sync_queue table
  • Outcome: Queued actions auto-sync when online.

Step 4.3 (SQLCipher) — Dropped. OS-level FDE covers at-rest encryption for a local-first desktop app. Backups already E2E encrypted via Argon2id + AES-256-GCM. Native module build complexity, ~10% perf overhead, and key management UX friction not justified by the threat model.


Phase 5 — Shared Memory DEPRECATED

Superseded by V3 architecture. The backend now implements a 4-tier memory system (Core, Associative, Episodic, Proactive) with per-user Fernet encryption — see ../adiuva-api/V3_MIGRATION_PLAN.md Steps 67. Memory lives server-side, not in Electron SQLite. The Electron orchestrator's buildChatContext() is removed in V3 (server fetches data on-demand via tool_call reverse API). Chat history is handled by conversationHistory passed in home_request frames.

See: V3_ELECTRON_MIGRATION_PLAN.md for the replacement architecture.


Phase 6 — Renderer UI Updates

Step 6.1 — Auth UI + settings restructure

  • LoginForm.tsx — centered login/register screen (src/renderer/components/auth/LoginForm.tsx)
  • Auth gate in AppShell — shows LoginForm when auth.status returns authenticated: false; passes through while loading to avoid flicker; staleTime: 5min to avoid hammering backend
  • SettingsPage.tsx Account section simplified — login form removed (AppShell handles it), always shows profile + sign out

Step 6.2 — ChatPage with context panel DEPRECATED

Superseded by V3. Home chat with block rendering (charts, entities, tables, timelines) and FloatingChat with domain navigation replace this. See V3_ELECTRON_MIGRATION_PLAN.md Steps 47.

Step 6.3 — BatchBuilderPage

  • Natural language input, config preview, connector/storage/schedule pickers, batch cards, test runner

Step 6.4 — PluginStorePage

  • Marketplace + installed tabs, permission dialog on install

Step 6.5 — DataManagerPage

  • Storage overview, per-source cards, migration wizard

Step 6.6 — ActivityLogPage

  • Filterable activity table with CSV export

Phase 7 — Cleanup & Hardening

Step 7.1 — Error handling and logging

Step 7.2 — Integration tests


Dependencies to Add

Package Purpose
ws WebSocket client for backend streaming
argon2 Key derivation for E2E backup
node-cron Batch agent scheduling
chokidar File watching (plugin)
imapflow IMAP client (plugin)

Dependencies to Remove

Package Reason
@langchain/core No local LLM
@langchain/openai No local LLM
@langchain/anthropic No local LLM
@langchain/langgraph No local orchestrator
@github/copilot-sdk No local Copilot

Execution Notes

  • Phase 1 is the critical path. Auth + backend client + drizzle executor + orchestrator refactor must land first.
  • Steps 1.11.4 are additive — existing app keeps working until Step 1.5 swaps the orchestrator.
  • Step 2.1 is the point of no return — after removing LangChain, there's no local AI fallback.
  • Phase B (backend changes) must land before Phase 1.31.5 — Electron needs the bidirectional WS to talk to.
  • Phase 3 and Phase 4 are independent — can be parallelized after Phase 2.
  • One step at a time. Mark [x] and commit with step N.N complete: <outcome>.