- 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>
27 KiB
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):
- User types message in renderer → tRPC
ai.chatmutation - Main process builds
ChatContext(queries SQLite for tasks, notes, profile) - Main opens WS to backend
/api/v1/chat/stream?token=<jwt>, sendschat_requestframe - Backend classifies intent → routes to agent → agent calls LLM with tools
- LLM calls a tool (e.g.
list_tasks) → tool callsexecute_on_client():- Backend sends
tool_callframe:{id, action:"select", table:"tasks", filters:{...}} - Electron receives frame → Drizzle executor:
db.select().from(tasks).where(...)→ real rows - Electron sends
tool_resultframe:{id, rows: [{id, title, ...}, ...]} - Tool receives real data → returns formatted string to LLM
- Backend sends
- Steps 5 repeats (max 5 iterations) until LLM has enough data to respond
- Backend streams response text →
text_chunkframes → main forwards viaai:streamIPC → renderer - Backend sends
finalframe:{"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.tswith Zod schemas + inferred types - Create
src/shared/batch-types.tswith batch builder + storage types - Update
tsconfig.jsonpaths — 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.tsto match backendapp/schemas.pyexactly:AuthTokens.expiresAt: change fromz.string().datetime()toz.number().int()(Unix epoch)ChatContext: replace with backend's flat structure —{ userProfile, relevantDocuments, recentTasks, conversationHistory }; remove UI-only fields (type,projectId,uiContext)- Remove
PlanActionentirely — no more action descriptors ChatResponse: just{ response: string }— noactionsarray- Align
PlanStep/ExecutionPlanwith backend or remove if plan mode is deferred
- Add WebSocket frame Zod schemas:
ToolCallActionenum:select,get,insert,update,delete,vector_upsert,vector_searchWsToolCall:{ 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,WsChatRequestWsServerFrame/WsClientFramediscriminated 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
UIChatContexttype insrc/renderer/hooks/useAIChat.tsfor 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:AuthManagerclass (singleton):register(email, password): Promise<AuthTokens>— POST/api/v1/auth/registerlogin(email, password): Promise<AuthTokens>— POST/api/v1/auth/loginlogout(): void— clears stored tokensgetAccessToken(): string | null— current JWTrefreshToken(): Promise<void>— POST/api/v1/auth/refreshisAuthenticated(): booleangetProfile(): 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
authRoutertRPC sub-router tosrc/main/router/index.ts - Update
src/main/store.ts: addbackendUrl: 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:BackendClientclass (singleton):- Constructor: reads
backendUrlfrom store, gets JWT fromAuthManager chatStream(request: ChatRequest, onChunk: (text: string) => void): Promise<ChatResponse>:- Opens WS to
/api/v1/chat/stream?token=<jwt> - Sends
{ type: "chat_request", ... }frame - Message loop:
text_chunk→ callsonChunk(text)tool_call→ callsDrizzleExecutor.execute(payload), sends back{ type: "tool_result", id, ... }final→ resolves with{ response }ping→ ignore
- Opens WS to
isOnline(): Promise<boolean>— GET/api/v1/healthwith 3s timeoutembedText(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
- Constructor: reads
- 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 onpayload.action:select:db.select().from(table)+ build.where()frompayload.filtersusing Drizzleeq()/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: callsupsertWithVector()fromvectordb.tswith pre-computed vector → returns{ ok: true }vector_search: LanceDBtable.search(payload.vector).limit(payload.limit)→ returns{ results }
- Filter builder: maps
{key: value}objects → Drizzleand(eq(table[key], value), ...). Special cases:nullvalue →isNull(table[key])searchkey →like(table.title, '%value%')orlike(table.content, '%value%')orderBykey →.orderBy(asc(table[field]))or.orderBy(desc(...))includeArchived: false→ addseq(table.status, 'active')filterdueDateFrom/dueDateTo→between(table.dueDate, from, to)
- Security: validate
tableagainst registry (reject unknown), validateactionagainst enum - Uses
getDb()fromsrc/main/db/index.ts— same Drizzle instance as everywhere else
- Table registry: map string names → Drizzle table objects from
- 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.tsentirely (996 lines → ~190 lines):orchestrate({ message, context, sender }):- Check
BackendClient.isOnline()— if offline, return{ response: '', error: 'You are offline.' } - Check
AuthManager.isAuthenticated()— if not, return{ response: '', error: 'Please log in.' } - Build
ChatContextfrom local SQLite (userProfile, recentTasks, conversationHistory) - Call
BackendClient.chatStream(request, chunk => sendStreamChunk(sender, chunk, false))tool_callframes handled inside the WS message loop (Step 1.3)
- On completion:
sendStreamChunk(sender, '', true)
- Check
- No PlanRunner, no action handling — writes happen mid-conversation via tool calls
- Keep
sendStreamChunk()IPC helper - Export
orchestrate()anddailyBrief()
- Update
aiRouterinsrc/main/router/index.ts:- Remove
setTokenmutation andhasTokenquery (replaced byauth.status) - Keep
chatmutation (same interface) anddailyBrief
- Remove
- Update
src/renderer/components/ai/AIChatPanel.tsx:- Replace
trpc.ai.hasToken.useQuery()withtrpc.auth.status.useQuery() - Update auth-gate condition and daily brief trigger to use
authStatusQuery.data?.authenticated - Replace
KeyRoundicon + provider-config messaging withLogInicon + login messaging
- Replace
- 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()→ callsBackendClient.embedText(content)→upsertWithVector() - Keep
searchNotes()andmigrateNotesIfNeeded()(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
- Add
- Update
src/main/api/drizzle-executor.ts: usesearchNotesByVectorwith 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'andinitAI()fromsrc/main/index.ts - Remove deps:
@langchain/core,@langchain/openai,@langchain/anthropic,@langchain/langgraph,@github/copilot-sdk - Clean up
src/main/store.ts(removeaiProvider; keptencryptedTokens— still used bytoken.ts→auth-manager.tsfor JWT storage) - Clean up
vite.main.config.mts(remove externalized LangChain/Copilot packages) - Clean up
forge.config.ts(remove LangChain/Copilot fromexternalPackages; 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.mdPhase 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
WsAgentRunschema:{ type: "agent_run", run_id, agent_id, config: { paths, file_extensions, prompt_template, data_types } } - Add
WsAgentDataschema:{ type: "agent_data", run_id, files: [{ path, name, content, metadata }] } - Add
WsAgentCompleteschema:{ type: "agent_complete", run_id, files_read, errors } - Add
WsDeviceHelloschema:{ type: "device_hello", device_id, agent_ids } - Extend
WsServerFramediscriminated union withagent_run - Extend
WsClientFramewithagent_data,agent_complete,device_hello
- Add
- Update
src/main/api/backend-client.ts:- In WS message loop, handle
agent_runframes:- Read files from configured paths using the local agent handler (Step 3.2)
- Send
agent_dataframes back with pre-processed content - Continue handling
tool_callframes for DB inserts as usual
- In WS message loop, handle
- 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 extensionpreProcess(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 callreadAgentFiles()and return{ files, errors, filesRead } - Files:
src/main/agents/file-reader.ts,src/main/api/backend-client.ts,package.json(pdf-parse,mammothadded) - 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: adddeviceId: 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.deviceIdtRPC query tosettingsRouter— renderer can read the device ID; Step 3.4 (agent router) injects it into local agent config creation calls to the backend - Electron sends
deviceIdwhen creating local agent configs → backend stores it (Step 3.4) - When backend triggers a local agent run, it checks
config.device_idmatches the connected Electron'sdeviceId(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
agentRoutertosrc/main/router/index.ts:agent.catalog— query: proxy to backendGET /api/v1/agents/catalogagent.local.list/agent.local.create/agent.local.update/agent.local.delete— proxy to backend withdeviceIdinjectedagent.cloud.list/agent.cloud.create/agent.cloud.update/agent.cloud.delete— proxy to backendagent.runs— query: proxy to backend run logagent.runNow— mutation: proxy to backend manual triggeragent.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/proxyDeletemethods toBackendClient(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_helloframe withdeviceIdand active agent IDs - Handles incoming
agent_runframes → dispatches to file reader → sendsagent_databack - Handles
tool_callframes for DB inserts (same as chat WS) handleAgentRunAndSend()— validates device ID, callshandleAgentRun(), sendsagent_data+agent_completeframes- 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()fromsrc/main/index.tsafter auth check on app startup will-quithandler insrc/main/index.tscallsdisconnectPersistent()for clean exitauthRouter.logincallsconnectPersistent()on successauthRouter.logoutcallsdisconnectPersistent()- 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) validateSearchfor 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 ✅
LocalAgentConfigPanelcomponent (inline, inside expanded agent row in Settings → Agents):- Native
dialog.showOpenDialogdirectory picker (via newdialog:showOpenDialogIPC +window.electronDialogbridge) - 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
- Native
CloudAgentConfigPanelcomponent (inline, inside expanded agent row):- Provider badge + OAuth placeholder note
- Data type selector + schedule picker
- "Customize AI Prompt" button
AddAgentDialogfor creating new agents from the catalog- Added
dialog:showOpenDialogIPC handler insrc/main/index.ts+window.electronDialogexposed insrc/preload/trpc.ts+ type declared insrc/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 ✅
JourneyDialogcomponent insrc/renderer/routes/settings.tsx:- Dialog with spring-animated chat interface (message list, input, send button)
- Starts via
agent.journey.start(passesagentType+ optionalagentId) on mount - Multi-turn via
agent.journey.messagetRPC calls - Shows generated prompt preview when
done === true/promptTemplatepresent - "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.runstRPC query
- Integrated into
AgentRowinsrc/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_queuetable- 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.mdSteps 6–7. Memory lives server-side, not in Electron SQLite. The Electron orchestrator'sbuildChatContext()is removed in V3 (server fetches data on-demand via tool_call reverse API). Chat history is handled byconversationHistorypassed inhome_requestframes.See:
V3_ELECTRON_MIGRATION_PLAN.mdfor 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— showsLoginFormwhenauth.statusreturnsauthenticated: false; passes through while loading to avoid flicker;staleTime: 5minto avoid hammering backend SettingsPage.tsxAccount 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.mdSteps 4–7.
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.1–1.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.3–1.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 withstep N.N complete: <outcome>.