docs: contextual sidebar agent design
Replace floating-chat (double-click) with adiuva trigger button + right-side resizable sidebar that persists across navigation and shares the chat surface with home chat. Includes deprecation sweep of floating_* code paths, Langfuse prompt swap, and SQLite-backed chat history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
384
docs/2026-05-14-contextual-sidebar-agent-design.md
Normal file
384
docs/2026-05-14-contextual-sidebar-agent-design.md
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
# Contextual Sidebar Agent — Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-14
|
||||||
|
**Status:** Approved, ready for implementation plan
|
||||||
|
**Scope:** Replace the legacy floating-chat (double-click-on-entity) flow with a contextual right-side chat sidebar that survives navigation, anchors to the current page's entity, and shares chat surface code with the home chat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goals
|
||||||
|
|
||||||
|
1. Replace the double-click "floating chat" with an explicit **adiuva trigger button** in the top-right of Timeline, Tasks, Projects and Notes pages.
|
||||||
|
2. Clicking the trigger opens a **right-side, resizable sidebar** with the same chat UI as the home chat.
|
||||||
|
3. The sidebar **persists across route changes** (no remount, no history reload). A silent system message informs the agent each time the user navigates.
|
||||||
|
4. The agent receives a **base context payload** every turn (`page`, `entityType`, `entityId`, `entityName`, `counts`) and can pull full details on demand via a single `get_page_details` tool.
|
||||||
|
5. Chat history **survives application restart** (SQLite-backed sessions, same model for home and contextual).
|
||||||
|
6. **All deprecated code paths removed** from frontend, main process, backend, Langfuse and electron-store. Pre-1.0 clean refactor [[feedback_clean_refactor_dev]].
|
||||||
|
|
||||||
|
Out of scope (next sprint): note write/update/summarize tools.
|
||||||
|
|
||||||
|
## 2. UX
|
||||||
|
|
||||||
|
### 2.1 Trigger button
|
||||||
|
|
||||||
|
- Bare elevated button anchored top-right of each page header.
|
||||||
|
- 48×48, 14px radius, dark surface (#1d1d20), multilayer shadow (Copilot-style elevation).
|
||||||
|
- Icon = `adiuvAI/assets/logo/logo-mark.svg` (compass needle, gold + light south for dark mode). Settles continuously (6s `compass-settle` keyframes, ported from `logo-mark.svg`).
|
||||||
|
- Hover: shadow deepens + subtle gold ambient glow halo behind the button. **No lift, no animation speedup.**
|
||||||
|
- Active: scale to 0.97.
|
||||||
|
- Pages with trigger: `/timeline`, `/tasks`, `/projects`, `/projects/$projectId`, `/notes/$noteId`.
|
||||||
|
- Home (`/`) does NOT mount the trigger — home keeps full-width `AIChatPanel`.
|
||||||
|
|
||||||
|
### 2.2 Sidebar shell
|
||||||
|
|
||||||
|
- Right side of `AppShell`, between the page and the screen edge.
|
||||||
|
- Resizable via shadcn `ResizablePanelGroup` + `ResizableHandle withHandle` (same control as task-brief, see [TaskCarousel.tsx:157-190](adiuvAI/src/renderer/components/brief/TaskCarousel.tsx#L157-L190)).
|
||||||
|
- Default size 32% of viewport. Min 24% (~320px on common widths). Max 55%.
|
||||||
|
- Width persisted as percentage in electron-store key `chat.sidebar.size`.
|
||||||
|
|
||||||
|
### 2.3 Sidebar layout
|
||||||
|
|
||||||
|
- **No header bar, no internal section dividers.** Pure chat surface.
|
||||||
|
- Two **elevated icon buttons floating top-right** (same recipe as trigger, 32×32 scale, 10px radius): `+` new chat, `✕` close.
|
||||||
|
- Messages scroll inside a single area. Bottom of the area is padded so that the last messages can scroll behind the input.
|
||||||
|
- Input is absolutely positioned at the bottom with a 64px gradient mask above it (`linear-gradient(to bottom, transparent → bg)`), so chat content fades visually under the translucent input box.
|
||||||
|
- Input box: `rgba(29,29,32,.85)`, `backdrop-filter: blur(14px)`, 16px radius, soft shadow — matches the home chat input treatment.
|
||||||
|
- No scope chip rendered. The user knows what page they are on; the agent sees the scope in the payload.
|
||||||
|
|
||||||
|
### 2.4 Sidebar lifecycle
|
||||||
|
|
||||||
|
- Mounted **once** at AppShell level inside `ContextualChatProvider`. Survives all route transitions.
|
||||||
|
- Default state: closed.
|
||||||
|
- Open state remembered for the current session (renderer memory); reset to closed on app restart.
|
||||||
|
- Sidebar is hidden on the home route regardless of open state.
|
||||||
|
|
||||||
|
### 2.5 Page-change behaviour
|
||||||
|
|
||||||
|
- Each page calls `useContextualScope(scope)` in its render.
|
||||||
|
- Provider diffs the new scope against the previous one. If they differ, it sends a `contextual_scope_update` WS frame to the backend.
|
||||||
|
- Backend appends a `system` message to the session buffer (`"User navigated to {scope}. Treat this as the new active context."`) and returns an ack. **No LLM call, no tokens.**
|
||||||
|
- User sees no visible divider in chat. The new context kicks in on the next user turn.
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ AppShell (mounted once) ─────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ResizablePanelGroup direction="horizontal" │
|
||||||
|
│ ┌ Page (Outlet) ────────────┐ ┌ Sidebar (when !home) ──┐ │
|
||||||
|
│ │ useContextualScope({...}) │ │ ChatSurface │ │
|
||||||
|
│ │ Top-right trigger button │──│ + elevated controls │ │
|
||||||
|
│ └───────────────────────────┘ └────────────────────────┘ │
|
||||||
|
│ ContextualChatProvider │
|
||||||
|
│ { open, size, sessionId, scope, │
|
||||||
|
│ messages, isStreaming, send, toggle, │
|
||||||
|
│ setScope, newChat } │
|
||||||
|
└───────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼ WS /api/v1/device
|
||||||
|
contextual_request | contextual_scope_update
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
run_contextual_stream()
|
||||||
|
Langfuse: contextual_system
|
||||||
|
Tools: get_page_details, create_task,
|
||||||
|
create_note, update_task,
|
||||||
|
create_timeline_event
|
||||||
|
```
|
||||||
|
|
||||||
|
Invariants:
|
||||||
|
|
||||||
|
- `ContextualChatProvider` is mounted once. Sidebar tree never unmounts on nav → no reload, full history kept in renderer state.
|
||||||
|
- The chat surface (`<ChatSurface>`) is a shared component reused by `AIChatPanel` (home) and `ContextualSidebar`.
|
||||||
|
- Sidebar history lives in SQLite via two new tables; renderer hydrates from disk on app start. Same persistence model used by home chat after the refactor.
|
||||||
|
- Backend is stateless w.r.t. chat history — each request carries the history payload from the client. Agent session buffer is purely an in-memory short-cache.
|
||||||
|
|
||||||
|
## 4. Frontend
|
||||||
|
|
||||||
|
### 4.1 New files (`adiuvAI/src/renderer/`)
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `components/ai/ChatSurface.tsx` | Headless chat: messages list, streaming, markdown, input. Props: `{messages, onSend, isStreaming, variant: 'home' \| 'contextual'}` |
|
||||||
|
| `components/ai/ContextualSidebar.tsx` | Right sidebar shell: floating elevated controls, embeds `<ChatSurface variant="contextual">`, input fade. |
|
||||||
|
| `components/ai/AdiuvaTriggerButton.tsx` | 48px elevated trigger button with compass needle (`logo-mark.svg`). Reads `useContextualChat().toggle()`. |
|
||||||
|
| `context/ContextualChatContext.tsx` | Provider: `{open, size, sessionId, scope, messages, isStreaming, send, toggle, setScope, newChat, setSize}`. Hydrates session from SQLite on mount. |
|
||||||
|
| `hooks/useContextualScope.ts` | `useContextualScope(scope)` — page calls in render. Effect diffs scope; on change emits `contextual_scope_update`. |
|
||||||
|
| `hooks/useChatStream.ts` | Shared streaming engine extracted from `useAIChat.ts`. Consumed by both home and contextual. |
|
||||||
|
|
||||||
|
### 4.2 Edits
|
||||||
|
|
||||||
|
- `components/ai/AIChatPanel.tsx` — becomes a thin wrapper around `<ChatSurface variant="home">`. Keeps home-specific shell (suggestion chips, brief carousel hooks). All chat plumbing moves out.
|
||||||
|
- `hooks/useAIChat.ts` — `'home'` and `'global'` branches kept (via the new `useChatStream`). `'floating'` branch deleted.
|
||||||
|
- `components/ai/ChatInputBox.tsx` — `'floating'` cache key replaced by `'contextual'`. Old drafts are wiped (acceptable pre-1.0).
|
||||||
|
- `components/layout/AppShell.tsx` — wrap `<Outlet>` in `<ContextualChatProvider>`. When the current route is **not** home, render the outlet inside a `ResizablePanelGroup` with a second `ResizablePanel` for `<ContextualSidebar>` shown only when `chat.open === true`. On home, render the outlet directly (the full-width home `AIChatPanel` owns its own layout). Remove `<FloatingChatProvider>` wrap and `useDoubleClickAI()` call.
|
||||||
|
- Each route gains a `<AdiuvaTriggerButton>` in the page header area and calls `useContextualScope(scope)` in render:
|
||||||
|
- [routes/timeline.tsx](adiuvAI/src/renderer/routes/timeline.tsx)
|
||||||
|
- [routes/tasks.tsx](adiuvAI/src/renderer/routes/tasks.tsx)
|
||||||
|
- [routes/projects.tsx](adiuvAI/src/renderer/routes/projects.tsx) and `routes/projects.$projectId.tsx`
|
||||||
|
- [routes/notes.$noteId.tsx](adiuvAI/src/renderer/routes/notes.$noteId.tsx)
|
||||||
|
|
||||||
|
### 4.3 Scope payload shape
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ContextualScope =
|
||||||
|
| { page: 'timeline'; entityType: null }
|
||||||
|
| { page: 'tasks'; entityType: null }
|
||||||
|
| { page: 'projects-list'; entityType: null }
|
||||||
|
| { page: 'project';
|
||||||
|
entityType: 'project';
|
||||||
|
entityId: string;
|
||||||
|
entityName: string;
|
||||||
|
counts: { tasks: number; notes: number; milestones: number } }
|
||||||
|
| { page: 'note';
|
||||||
|
entityType: 'note';
|
||||||
|
entityId: string;
|
||||||
|
entityName: string;
|
||||||
|
projectId: string | null;
|
||||||
|
charCount: number };
|
||||||
|
```
|
||||||
|
|
||||||
|
For global list pages (`tasks`, `projects-list`, `timeline`), the scope may also include the renderer-side active filters so that `get_page_details` can apply them.
|
||||||
|
|
||||||
|
### 4.4 Elevated button recipe (Tailwind-compatible CSS, ported to component)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.adiuva-btn {
|
||||||
|
width: 48px; height: 48px;
|
||||||
|
background: #1d1d20;
|
||||||
|
border: 1px solid rgba(255,255,255,.06);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255,255,255,.05) inset,
|
||||||
|
0 2px 4px -1px rgba(0,0,0,.5),
|
||||||
|
0 8px 16px -4px rgba(0,0,0,.55),
|
||||||
|
0 20px 36px -10px rgba(0,0,0,.6);
|
||||||
|
transition: box-shadow .3s ease, background .2s ease;
|
||||||
|
}
|
||||||
|
.adiuva-btn:hover {
|
||||||
|
background: #26262a;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255,255,255,.06) inset,
|
||||||
|
0 4px 8px -2px rgba(0,0,0,.6),
|
||||||
|
0 18px 28px -8px rgba(0,0,0,.65),
|
||||||
|
0 30px 60px -14px rgba(251,200,129,.22);
|
||||||
|
}
|
||||||
|
.adiuva-btn:active { transform: scale(.97); }
|
||||||
|
.adiuva-btn.sm { width: 32px; height: 32px; border-radius: 10px; }
|
||||||
|
.needle-g {
|
||||||
|
transform-origin: 32px 32px;
|
||||||
|
animation: compass-settle 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes compass-settle {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
20% { transform: rotate(4deg); }
|
||||||
|
50% { transform: rotate(-3deg); }
|
||||||
|
80% { transform: rotate(2deg); }
|
||||||
|
100% { transform: rotate(0deg); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Light-mode variant: bg `#ffffff`, border `#c8c3cd` (dusty lavender per brand), shadows softer; settle and halo unchanged. Light-mode colors confirmed against [adiuvAI/.claude/CLAUDE.md Design Context].
|
||||||
|
|
||||||
|
## 5. Backend
|
||||||
|
|
||||||
|
### 5.1 WS frames (`api/app/api/routes/device_ws.py`)
|
||||||
|
|
||||||
|
Dispatch table:
|
||||||
|
|
||||||
|
- `home_request` — unchanged.
|
||||||
|
- `contextual_request` — **new**. `{ session_id, message, scope, history? }`. Handler: `_handle_contextual_request` → `run_contextual_stream`.
|
||||||
|
- `contextual_scope_update` — **new**. `{ session_id, scope }`. Handler: appends system message to session buffer, returns `{ type: 'contextual_scope_ack' }`. No LLM call.
|
||||||
|
- `brief_request`, `task_brief_request` — unchanged (out of scope).
|
||||||
|
- `floating_request` — **removed**.
|
||||||
|
|
||||||
|
### 5.2 Runner (`api/app/core/deep_agent.py`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def run_contextual_stream(*, user, db, session_id, message, scope, history):
|
||||||
|
system_prompt = await get_prompt_or_fallback(
|
||||||
|
"contextual_system", _CONTEXTUAL_SYSTEM_PROMPT,
|
||||||
|
)
|
||||||
|
scope_block = _render_scope_block(scope)
|
||||||
|
sys = f"{system_prompt}\n\n## Current view\n{scope_block}"
|
||||||
|
sys += _language_instruction(user)
|
||||||
|
tools = [
|
||||||
|
get_page_details_tool,
|
||||||
|
create_task_tool,
|
||||||
|
update_task_tool,
|
||||||
|
create_note_tool,
|
||||||
|
create_timeline_event_tool,
|
||||||
|
]
|
||||||
|
yield from _run_agent_loop(
|
||||||
|
sys=sys, tools=tools, message=message, history=history,
|
||||||
|
user=user, db=db, session_id=session_id, channel="contextual",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`_render_scope_block(scope)` produces a single-paragraph human-readable summary like:
|
||||||
|
`"User is viewing the project Acme Q3 launch. It has 12 tasks (8 completed), 4 notes, 3 milestones. Active milestone: Beta cut, overdue 2 days."`
|
||||||
|
|
||||||
|
### 5.3 Langfuse prompt
|
||||||
|
|
||||||
|
- Create a **new** prompt `contextual_system` in Langfuse (do not rename `floating_system` yet — coexist during the rollout).
|
||||||
|
- After cutover (milestone M6), delete `floating_system` from Langfuse.
|
||||||
|
- Fallback constant `_CONTEXTUAL_SYSTEM_PROMPT` lives in `deep_agent.py`. Body (text):
|
||||||
|
|
||||||
|
```
|
||||||
|
You are adiuvAI's contextual assistant. The user is working inside the app and has opened a side chat anchored to a specific view ("current view"). Help them act on that view: recap, plan, create entities, answer questions.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Base context (current view summary) is provided every turn. Treat it as ground truth for ids and names; never invent them.
|
||||||
|
2. When the user asks about details not in the base context (e.g. "what tasks are blocking the launch milestone"), call `get_page_details` for the relevant entity before answering. Don't guess.
|
||||||
|
3. When the user requests an action that creates or updates an entity:
|
||||||
|
- If the current view is a project and no project is specified, use the current project automatically.
|
||||||
|
- If the current view is the global Tasks / Projects / Timeline list and no project is specified, ASK before attaching to any project. Don't silently create orphan entities.
|
||||||
|
4. The current view can change mid-conversation (user navigates). When you see a system message "User navigated to ...", treat the new view as the active context. Prior turns remain visible but the active scope shifts.
|
||||||
|
5. Notes: you can read note bodies via `get_page_details({entityType:'note'})`. You CANNOT edit, summarize-to-replace, or append. Tell the user "note editing is coming in a later release" if asked.
|
||||||
|
6. Be concise. Default to 1-3 short paragraphs. Bullet lists fine. Don't restate the user's request.
|
||||||
|
7. Never expose ids in prose. Use names. Ids only travel through tool calls.
|
||||||
|
```
|
||||||
|
|
||||||
|
Home prompt `home_system` is unchanged.
|
||||||
|
|
||||||
|
### 5.4 Session buffer
|
||||||
|
|
||||||
|
Extend `app/core/agent_session_buffer.py` to accept `channel="contextual"`. The buffer is in-memory only; durable history lives client-side (§6).
|
||||||
|
|
||||||
|
## 6. Data model — client-side SQLite
|
||||||
|
|
||||||
|
Two new tables in [adiuvAI/src/main/db/schema.ts](adiuvAI/src/main/db/schema.ts):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const aiChatSessions = sqliteTable("ai_chat_sessions", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
channel: text("channel").notNull(), // 'home' | 'contextual'
|
||||||
|
title: text("title"),
|
||||||
|
createdAt: integer("created_at").notNull(),
|
||||||
|
updatedAt: integer("updated_at").notNull(),
|
||||||
|
lastScope: text("last_scope"), // JSON ContextualScope | null
|
||||||
|
});
|
||||||
|
|
||||||
|
export const aiChatMessages = sqliteTable("ai_chat_messages", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
sessionId: text("session_id").notNull(), // logical FK, cascade in tRPC
|
||||||
|
role: text("role").notNull(), // 'user' | 'assistant' | 'system'
|
||||||
|
content: text("content").notNull(),
|
||||||
|
toolCalls: text("tool_calls"), // JSON | null
|
||||||
|
toolResults: text("tool_results"), // JSON | null
|
||||||
|
scope: text("scope"), // JSON snapshot at msg time | null
|
||||||
|
createdAt: integer("created_at").notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Indexes: `(session_id, created_at)`, `(channel, updated_at DESC)`. New Drizzle migration `0011_ai_chat_history.sql`.
|
||||||
|
|
||||||
|
New tRPC sub-router `aiChat` in [adiuvAI/src/main/router/index.ts](adiuvAI/src/main/router/index.ts):
|
||||||
|
|
||||||
|
- `aiChat.listSessions({ channel })`
|
||||||
|
- `aiChat.getSession({ id })` → `{ session, messages }`
|
||||||
|
- `aiChat.createSession({ channel, initialScope? })` → `{ id }`
|
||||||
|
- `aiChat.appendMessage({ sessionId, role, content, toolCalls?, toolResults?, scope? })`
|
||||||
|
- `aiChat.deleteSession({ id })`
|
||||||
|
|
||||||
|
Renderer flow:
|
||||||
|
|
||||||
|
1. On `ContextualChatProvider` mount: read electron-store `chat.contextual.lastSessionId`. If set, `getSession` → hydrate. Else create on first send.
|
||||||
|
2. Each user send + assistant complete → `appendMessage`.
|
||||||
|
3. "New chat" button → `createSession`, swap stored sessionId.
|
||||||
|
|
||||||
|
Home chat receives the same persistence model (key `chat.home.lastSessionId`).
|
||||||
|
|
||||||
|
## 7. Tools
|
||||||
|
|
||||||
|
| Tool | Args | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `get_page_details` | `{ entityType, entityId }` for entity views; `{ entityType }` for list views | Snapshot JSON (see below) |
|
||||||
|
| `create_task` | `{ title, dueAt?, projectId?, ... }` | `{ taskId }` |
|
||||||
|
| `update_task` | `{ taskId, patch }` | `{ ok: true }` |
|
||||||
|
| `create_note` | `{ title, body?, projectId? }` | `{ noteId }` |
|
||||||
|
| `create_timeline_event` | `{ type, title, dueAt, projectId?, deps? }` | `{ eventId }` (verify tool exists in current backend during M5; if missing, scope it in or remove from this list) |
|
||||||
|
|
||||||
|
`get_page_details` is dispatched client-side via [drizzle-executor.ts](adiuvAI/src/main/api/drizzle-executor.ts). Supported scopes:
|
||||||
|
|
||||||
|
- `entityType: 'project'` → `{ project, tasks[], notes[summary only], milestones[], comments[] }`
|
||||||
|
- `entityType: 'task'` → `{ task, project, comments[], deps[] }`
|
||||||
|
- `entityType: 'note'` → `{ note: { ..., body } }`
|
||||||
|
- `entityType: 'tasks_all'` → `{ tasks[] filtered by renderer view filters }`
|
||||||
|
- `entityType: 'projects_all'` → `{ projects[] }`
|
||||||
|
- `entityType: 'timeline_all'` → `{ events[] }`
|
||||||
|
|
||||||
|
Default-projectId is **not** applied automatically by the executor. The prompt instructs the LLM to attach the current project when scope is a project; on global list pages the LLM is told to ask first.
|
||||||
|
|
||||||
|
Note write/edit tools (`update_note`, `summarize_note`, `append_to_note`) are explicitly out of scope and **not** wired into `tools` for `run_contextual_stream`.
|
||||||
|
|
||||||
|
## 8. Deprecation removal
|
||||||
|
|
||||||
|
**Renderer (adiuvAI/src/renderer/)**:
|
||||||
|
|
||||||
|
- DELETE `components/ai/FloatingChat.tsx`
|
||||||
|
- DELETE `context/FloatingChatContext.tsx`
|
||||||
|
- DELETE `hooks/useDoubleClickAI.ts`
|
||||||
|
- EDIT `components/layout/AppShell.tsx` — remove provider wrap and double-click hook.
|
||||||
|
- STRIP every `data-ai-section="..."` attribute. Known sites:
|
||||||
|
- [routes/tasks.tsx:16-25](adiuvAI/src/renderer/routes/tasks.tsx#L16-L25) (`tasks-overview`, `tasks-list`)
|
||||||
|
- [components/timeline/TimelineGanttView.tsx:95-110](adiuvAI/src/renderer/components/timeline/TimelineGanttView.tsx#L95-L110)
|
||||||
|
- [components/projects/ProjectDetail.tsx:62-100](adiuvAI/src/renderer/components/projects/ProjectDetail.tsx#L62-L100)
|
||||||
|
- [routes/notes.$noteId.tsx:44-50](adiuvAI/src/renderer/routes/notes.$noteId.tsx#L44-L50)
|
||||||
|
- Final grep sweep before merge — fail PR if any remain.
|
||||||
|
- EDIT `components/ai/ChatInputBox.tsx` — remove `'floating'` cache key.
|
||||||
|
- EDIT `hooks/useAIChat.ts` — remove `'floating'` branch.
|
||||||
|
|
||||||
|
**Main process (adiuvAI/src/main/)**:
|
||||||
|
|
||||||
|
- EDIT `api/backend-client.ts` — remove `sendFloatingRequest()`; add `sendContextualRequest()` + `sendContextualScopeUpdate()`.
|
||||||
|
- EDIT `ai/orchestrator.ts` — remove floating delegation; add contextual delegation.
|
||||||
|
- EDIT `db/schema.ts` — add `aiChatSessions` + `aiChatMessages`. New migration `0011_ai_chat_history.sql`.
|
||||||
|
- EDIT `router/index.ts` — add `aiChat` sub-router.
|
||||||
|
|
||||||
|
**Backend (api/app/)**:
|
||||||
|
|
||||||
|
- EDIT `api/routes/device_ws.py` — delete `_handle_floating_request` (line 292) and its dispatch entry; add `_handle_contextual_request` and `_handle_contextual_scope_update`.
|
||||||
|
- EDIT `core/deep_agent.py`:
|
||||||
|
- DELETE `run_floating_stream` (line 1460) and `_FLOATING_SYSTEM_PROMPT`.
|
||||||
|
- ADD `run_contextual_stream`, `_CONTEXTUAL_SYSTEM_PROMPT`, `_render_scope_block`.
|
||||||
|
- EDIT `core/agent_session_buffer.py` — accept `channel="contextual"`.
|
||||||
|
|
||||||
|
**Langfuse**:
|
||||||
|
|
||||||
|
- CREATE prompt `contextual_system` (body per §5.3).
|
||||||
|
- After M6 deploy verified: DELETE prompt `floating_system`.
|
||||||
|
|
||||||
|
**Tests (api/tests/)**:
|
||||||
|
|
||||||
|
- DELETE / RENAME `test_*floating*` to contextual equivalents.
|
||||||
|
- ADD tests:
|
||||||
|
- `contextual_scope_update` no LLM call, system message appended.
|
||||||
|
- Scope block rendering for each `entityType`.
|
||||||
|
- Tool list contains `get_page_details` and entity-create tools, NOT note edit tools.
|
||||||
|
- Default-project rule via prompt-driven smoke (mocked LLM).
|
||||||
|
|
||||||
|
**Electron-store keys**:
|
||||||
|
|
||||||
|
- ADD `chat.sidebar.size`, `chat.contextual.lastSessionId`, `chat.contextual.open`, `chat.home.lastSessionId`.
|
||||||
|
- REMOVE any `floating.*` keys present (sweep).
|
||||||
|
|
||||||
|
## 9. Milestones (commit-per-step, see [[feedback_plan_workflow]])
|
||||||
|
|
||||||
|
| # | Title | What ships | Verifiable by |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **M1** | DB + persistence foundation | Schema, migration, `aiChat` tRPC sub-router | tRPC devtools |
|
||||||
|
| **M2** | ChatSurface refactor | `ChatSurface`, `useChatStream` extracted; home rewired; floating still works | Home chat unchanged behavior |
|
||||||
|
| **M3** | Backend contextual frame + prompt | `contextual_request`, `contextual_scope_update`, `run_contextual_stream`, Langfuse `contextual_system`, fallback constant; old floating still alive | Backend tests, manual WS frame |
|
||||||
|
| **M4** | Frontend sidebar shell + provider | `ContextualChatProvider`, `ContextualSidebar`, `AdiuvaTriggerButton`, `useContextualScope`; trigger on all 4 page types; `ResizablePanelGroup` in AppShell | Open sidebar on each page, chat works, survives nav |
|
||||||
|
| **M5** | Tools wiring | `get_page_details` dispatcher in drizzle-executor for all scopes; entity-create tools confirmed reachable | Manual recap + create-task smoke |
|
||||||
|
| **M6** | Deprecation sweep | Delete `FloatingChat*`, `useDoubleClickAI`, all `data-ai-section`, `_handle_floating_request`, `run_floating_stream`, `_FLOATING_SYSTEM_PROMPT`, `sendFloatingRequest`, floating draft cache, floating electron-store keys, Langfuse `floating_system` | Grep sweep, app still boots |
|
||||||
|
| **M7** | Polish | New-chat button UX, light-mode elevated styling, width persistence verified across restart, empty-state copy on global-list pages | Manual UX pass |
|
||||||
|
|
||||||
|
## 10. Risks
|
||||||
|
|
||||||
|
- **WS reconnect mid-stream:** existing `_mark_runs_disconnected` covers this. Client retries with same `sessionId`, replays history from SQLite.
|
||||||
|
- **Race: nav while assistant streaming.** Provider serializes outbound frames — `scope_update` is queued and applied after stream completes.
|
||||||
|
- **Resize during stream:** shadcn `ResizablePanel` re-renders cheap, no observable impact.
|
||||||
|
- **Migration rollout:** Langfuse `contextual_system` must exist in production before backend code referencing it deploys. Coordinate prompt-create in M3 step.
|
||||||
|
- **Old session continuity:** floating chat had no persisted history, so there is nothing to migrate. Existing draft cache `'floating'` keys are wiped on first run (acceptable pre-1.0).
|
||||||
Reference in New Issue
Block a user