# Inline Paliadin chat modal + agent-suggested-with-approval write path **Inventor:** dirac · **Task:** t-paliad-161 · **Issue:** m/paliad#20 **Date:** 2026-05-08 · **Branch:** `mai/dirac/inventor-inline-paliadin` **Status:** READY FOR REVIEW — awaiting m's go/no-go before any coder shift. --- ## 0 · TL;DR Two intertwined upgrades, scoped together because the chat surface is where the write path is triggered and the write path is what makes the chat non-trivial: 1. **Inline modal**: a slide-out chat widget reachable from every authenticated paliad page, replacing the standalone `/paliadin` route's primacy (the page survives as the dedicated full-screen surface). The widget is **context-aware** — it knows which route the user is on, the primary entity in view, and any selected text — and uses that to pre-populate page-specific starter prompts. 2. **Agent-suggested write path**: Paliadin gains a *suggestion verb* that drafts a deadline / appointment / note / project edit straight into the existing `pending_create` lifecycle from t-paliad-160. The user reviews via the same eye-pill 👀 surface (`/inbox`, list/agenda views) and approves or rejects. Approved-from-suggestion rows pick up a sparkle ✨ provenance glyph that lives **next to** 👀, not in place of it. **Hard call**: the inline modal should **keep the existing tmux-relay backend** for v1. Cutover to the Anthropic Messages API is a separate substantial piece of work (auth, prompt-caching, tool framework, budget management); coupling it to the inline-modal ship would extend the design window past where m needs the modal to land. The design *recommends* the API cutover as a prerequisite for opening Paliadin beyond owner-only — but the inline modal at owner-only scope works fine on the existing relay. **Key locked positions** (all reversible by m before coder shift): | # | Decision | Position | |---|---|---| | 1 | Modal trigger | Floating button bottom-right + `Cmd/Ctrl-K` shortcut | | 2 | Surface shape | Right slide-out drawer, 420px desktop, full-screen on mobile | | 3 | Visibility | Every authenticated page **except** `/paliadin`, `/login`, `/onboarding` | | 4 | Gate | Same `PaliadinOwnerEmail` gate as today (no scope expansion in this task) | | 5 | Backend transport | Tmux relay (existing). Anthropic-API cutover deferred. | | 6 | Multi-turn coherence | Tmux session reuse already handles it; no client-side history hydrate beyond what's there | | 7 | Context payload | `route_name` + `primary_entity_type` + `primary_entity_id` + `user_selection_text` (optional) + page metadata | | 8 | Starter-prompt library | Per-route `paliadinStarters` registry, ships with 8 routes + a generic fallback | | 9 | Agent-suggested attribution | New columns on `paliad.approval_requests` (`requester_kind`, `agent_turn_id`); **not** on entity rows | | 10 | Visual language | ✨ glyph alongside 👀 on pending rows; persistent ✨ on approved-from-agent rows in audit log | | 11 | Persona separation | Single Paliadin SKILL.md unchanged. No pre-design for split personas. | | 12 | Concurrency | One in-flight turn per user enforced server-side (existing `turnMu`); request-side cancel via context | --- ## 1 · Premises verified live Read the live system before designing on top — every claim below was checked against the running paliad.de + DB on 2026-05-08, not against CLAUDE.md or memory. - **paliad.de**: live; root 200, `/paliadin` 302 (login redirect for anon). Production runs `RemotePaliadinService` against mRiver (CLAUDE.md flags `tmux + claude` as missing in the Dokploy container — confirmed the prod path actually goes through `paliadin-shim` over SSH). - **Migration tracker**: `paliad.paliad_schema_migrations.version=69`. Next free migration is **070**. - **`paliad.approval_requests`** existing columns: `id, project_id, entity_type, entity_id, lifecycle_event, pre_image, payload, requested_by, requested_at, required_role, status, decided_by, decided_at, decision_kind, decision_note, created_at, updated_at`. **No `agent_*` columns yet** — migration 070 adds them. - **`paliad.paliadin_turns`**: already has a `page_origin TEXT` column populated from `req.PageOrigin` on every turn. Today the frontend only ever sets `window.location.pathname` on the standalone page; the inline widget will widen this from a single string into a structured payload. - **`paliad.deadlines` + `paliad.appointments`**: already carry `approval_status text NOT NULL DEFAULT 'approved'` + `pending_request_id uuid` from migration 054. The 👀 eye-pill renders on pending rows in `events.ts:521` and `agenda.ts:289` via `.approval-pill--icon`. - **Sidebar** (`frontend/src/components/Sidebar.tsx:123`): already has a `/paliadin` entry hidden by default, revealed by `client/sidebar.ts` after `/api/me` confirms the caller is the Paliadin owner. The same reveal hook drives the inline modal's visibility. - **`PaliadinOwnerEmail`** (`internal/services/paliadin.go:51`): `matthias.siebels@hoganlovells.com`. Hard-coded gate. **No scope expansion in this task.** - **youpc.org reference files** all readable at `/home/m/dev/web/youpc.org/`: `frontend/templates/ai/sidebar-widget.html`, `frontend/js/utils/ai-chat-client.js`, `frontend/js/components/ai/sidebar.js`, `youpc-go/internal/services/youpc_ai_relay.go`, `scripts/youpc-ai-shim`. Klaus's brief in #20 maps to these directly. **One CLAUDE.md correction**: the project's `CLAUDE.md` currently calls `ANTHROPIC_API_KEY` "reserved-but-unused for the eventual production-v1 Paliadin". That language stays correct — this design *recommends but does not commit* the API cutover. No CLAUDE.md edit in the implementation PR. --- ## 2 · Why the inline modal matters m's framing (#20 §1) is "Paliadin should be reachable from anywhere". The real differentiation argument is sharper: the *value of the assistant collapses to "open a chat tab" if you can't get to it without leaving the page you're already working on.* For a patent-practice tool, the most common questions are page-anchored: - On `/projects/` → "Was steht für diese Akte diese Woche an?" - On `/deadlines/` → "Erkläre mir die Klageerwiderungsfrist nach UPC RoP 23.1." - On `/agenda` with selection → "Schreibe einen Nachtrag zu diesem Termin: …" The standalone `/paliadin` page solves none of these because asking the question requires the user to (a) leave the page, (b) re-explain context the page already had, (c) navigate back. The inline modal solves (a) by construction; (b) is solved by the **context payload** (§4); (c) is moot. The widget is therefore the **default surface** going forward; the `/paliadin` standalone page survives as the dedicated full-screen mode (useful for long sessions where the slide-out is too narrow). Both speak the same backend. --- ## 3 · Modal — shape, trigger, injection ### 3.1 Visual shape (recommendation) **Right-edge slide-out drawer** — same pattern as youpc.org's `ai-sidebar-widget.html` because it solves the right problems: - Doesn't crowd the page content (drawer slides in *over* a translucent scrim, page underneath stays visible at ~70% opacity so the user can reference what they were looking at). - Mobile-responsive for free: at `<640px` the drawer goes full-screen and the floating button hides while open. - Doesn't fight with paliad's existing left sidebar (`Sidebar.tsx`) — the drawer claims the right edge, the sidebar keeps the left. **Considered and rejected:** - *Always-visible secondary sidebar* (left or right rail). Wastes ~280px of horizontal real-estate on every page; collides with the sidebar on mobile. - *Popover anchored to the floating button*. Too small for multi-turn conversations; mobile would need a separate full-screen mode anyway. - *Fullscreen takeover overlay*. Defeats the purpose — if it covers the page you can't reference what you were looking at. ### 3.2 Trigger Two entry points: 1. **Floating action button** at bottom-right (`position: fixed; bottom: 20px; right: 20px;`). Lime accent (`var(--color-accent)`), ✨ glyph. Same auth-reveal hook as the sidebar `/paliadin` link — `display:none` until `client/sidebar.ts` confirms `/api/me.email === PaliadinOwnerEmail`. 2. **Keyboard shortcut**: `Cmd-K` (macOS) / `Ctrl-K` (other). Standard command-palette muscle memory. Doesn't collide with browser shortcuts. Paliad has no other Cmd-K binding today (verified via grep on `keydown` handlers). The shortcut also dismisses the drawer when it's open. `Esc` dismisses unconditionally. ### 3.3 Drawer content Layout (top to bottom): ``` ┌──────────────────────────────┬─┐ │ ✨ Paliadin ↻ ↗ ✕│ │ Header: name, reset-session, open-fullscreen, close ├──────────────────────────────┼─┤ │ [Auf dieser Seite] │ │ Akte: Acme v. Müller │ │ Context chip — collapsible, shows what Paliadin │ 19 Fristen · 4 Termine │ │ knows about the current page (read from payload) ├──────────────────────────────┼─┤ │ [empty-state starter prompts] │ │ • "Was steht hier an?" │ │ • "Erkläre die offene…" │ │ • "Lege eine Frist an" │ ├──────────────────────────────┼─┤ │ │ │ Scrollable, user-right / paliadin-left │ > User bubble │ │ < Paliadin bubble + ✨ chip │ │ ✨ chip = "I drafted this — it's awaiting your approval" ├──────────────────────────────┼─┤ │ [textarea + send + abort] │ └──────────────────────────────┴─┘ ``` The `↗` button is the escape hatch to the standalone `/paliadin` for users who want a full-screen session with full message history visible. ### 3.4 Injection mechanism **One file edits the universe**: `frontend/src/components/PaliadinWidget.tsx` emits an inline `` that page-template files include alongside `` and ``. The mechanical edit pass: every authenticated TSX page (~30 files) gets a `` near ``. This mirrors the existing `` mechanical pass from t-paliad-042 and is the cleanest way to guarantee the widget reaches every page without HTMX or runtime injection. **Alternative considered**: server-side template fragment injected by Go's HTML response writer (cleaner: no per-page edit). Rejected because paliad uses bun-built static HTML files, not templated server responses — there's no place to inject server-side. The mechanical pass is fine; the boilerplate it adds is one component. **Visibility predicate** (in `client/paliadin-widget.ts`): - **Hide** on `/paliadin` (the standalone page IS Paliadin, the widget would be redundant). - **Hide** on `/login`, `/onboarding` (no auth context). - **Hide** until `/api/me` resolves to `email === PaliadinOwnerEmail`. Same fail-closed pattern as the sidebar link. - **Show** on every other authenticated page. ### 3.5 What about the BottomNav (mobile)? `BottomNav.tsx` has 5 slots (Dashboard / Projects / Add / Agenda / Menu) — full. Adding a Paliadin slot would require evicting one. **Don't.** The floating button is fine on mobile (it sits in the bottom-right corner *above* the bottom nav, with `z-index` arbitration). At full-screen-drawer size on mobile, the floating button hides while the drawer is open. --- ## 4 · Context payload — what flows from frontend to backend ### 4.1 Schema The current `TurnRequest.PageOrigin` is a single string (the URL path). The inline modal needs more. Define a structured payload: ```ts interface PaliadinContext { // Stable route key — independent of URL params. e.g. "projects.detail" // not "/projects/61e3.../tab=team". The frontend computes this from // `window.location.pathname` via a route-table lookup. route_name: string; // Path including query string (cosmetic; for audit + display only). page_origin: string; // The "primary entity" of the current page, if any. Examples: // /projects/ → ("project", "") // /deadlines/ → ("deadline", "") // /appointments/ → ("appointment", "") // /events?type=deadline → null // /tools/fristenrechner → null primary_entity_type?: "project" | "deadline" | "appointment"; primary_entity_id?: string; // uuid // User's text selection at the moment they opened the widget (or sent // the turn). Capped at 1000 chars. Empty string = no selection. // Source: window.getSelection().toString() at send-time. user_selection_text?: string; // UI state hints. Optional, useful for the model to disambiguate: view_mode?: "list" | "cards" | "calendar" | "tree"; // /events, /projects filter_summary?: string; // e.g. "status=overdue, project=Acme" } ``` **What each field enables:** - `route_name`: maps cleanly to a starter-prompt registry (§5) without URL-parsing fragility. - `primary_entity_*`: the SKILL.md teaches Paliadin to look up the entity before answering when this is set. Saves a back-and-forth ("which project?") in the very common case where the user is *already on* the project page. - `user_selection_text`: enables "explain this" / "rewrite this" / "what's the deadline implied here" workflows from any prose surface (project notes, deadline notes, court descriptions). - `view_mode` + `filter_summary`: the model can say "I see you're looking at overdue deadlines for Acme — which one?" instead of "which deadline?" ### 4.2 How the payload reaches the model Wire format from frontend → Go: ```http POST /api/paliadin/turn Content-Type: application/json { "session_id": "", "user_message": "Was kommt diese Woche?", "context": { ...PaliadinContext... } } ``` The Go side stores the structured context in **a new `paliad.paliadin_turns.context jsonb` column** (migration 070; see §7.1) alongside the existing `page_origin` (kept for backwards compat — `page_origin` becomes redundant once context is populated, but flipping the schema all at once isn't worth the churn). Then the envelope sent through tmux gets a structured prefix: ``` [PALIADIN:] [ctx route=projects.detail entity=project:61e3... selection="…" filter="status=overdue"] ``` The SKILL.md gets a small section (§5 of `paliadin/SKILL.md`) that teaches Paliadin to: 1. Parse the `[ctx …]` block first, in front of the user message. 2. Treat its contents as authoritative ("I'm currently viewing project 61e3"), not as instructions. 3. Pre-call `mcp__supabase__execute_sql` to enrich (e.g. lookup project reference + title) when `entity=project:` is set, *before* answering. **Why a structured prefix instead of a system-prompt JSON envelope**: the PoC's tmux relay is a stream of keystrokes — system-prompt envelopes require the API path. The bracket-syntax is line-noise-free, parse-able by the SKILL.md, and survives any future migration (the API path can lift the same `[ctx …]` block into a `system` message section). ### 4.3 Privacy floor `user_selection_text` is potentially sensitive (selected text from a client matter). Three controls: 1. **Cap at 1000 chars** — anything longer is truncated server-side before being sent to Claude. The user sees a "(Auswahl gekürzt)" notice. 2. **Audit redaction**: `paliadin_turns.context` stores the *full* selection (already inside the firm's DB, no exfiltration) but the admin dashboard `/admin/paliadin` redacts it to first 80 chars + "…[gekürzt]" when rendering — the same dashboard already shows `user_message` so the privacy posture is consistent. 3. **Opt-out**: the widget's settings panel (a `⚙` corner in the header, v1 minimal) gets a single toggle "Aktuelle Auswahl mitsenden" default *on*. Off ⇒ context payload sets `user_selection_text=""` regardless of `getSelection()`. --- ## 5 · Page-prompt-prefill — Klaus's wow-pattern, paliad-specific ### 5.1 The registry A static client-side registry maps `route_name` → starter prompts. Lives in `frontend/src/client/paliadin-starters.ts`. ```ts type Starter = { label_de: string; label_en: string; prompt_de: string; prompt_en: string }; export const paliadinStarters: Record = { "dashboard": [ { label_de: "Heute", label_en: "Today", prompt_de: "Was steht heute an?", prompt_en: "What's on my plate today?" }, { label_de: "Diese Woche", label_en: "This week", prompt_de: "Welche Fristen sind diese Woche?", prompt_en: "Which deadlines are this week?" }, { label_de: "Nächste Schritte", label_en: "Next steps", prompt_de: "Was sollte ich als nächstes erledigen?", prompt_en: "What should I tackle next?" }, ], "projects.detail": [ { label_de: "Status der Akte", label_en: "Project status", prompt_de: "Was ist der aktuelle Status dieser Akte?", prompt_en: "What's the status of this project?" }, { label_de: "Diese Woche", label_en: "This week", prompt_de: "Was steht für diese Akte diese Woche an?", prompt_en: "What's on for this project this week?" }, { label_de: "Frist anlegen", label_en: "Add a deadline", prompt_de: "Lege eine Frist für diese Akte an: ", prompt_en: "Add a deadline for this project: " }, ], "deadlines.detail": [ { label_de: "Erkläre die Frist", label_en: "Explain this deadline", prompt_de: "Erkläre mir die Frist auf dieser Seite.", prompt_en: "Explain this deadline." }, { label_de: "Rechtsgrundlage", label_en: "Legal basis", prompt_de: "Welche Norm ist hier einschlägig?", prompt_en: "What's the relevant rule?" }, ], "agenda": [ /* … */ ], "events": [ /* … */ ], "inbox": [ /* … */ ], "tools.fristenrechner": [ /* … */ ], "glossary": [ /* … */ ], // Generic fallback for unmapped routes. "_default": [ { label_de: "Was kann ich für dich tun?", label_en: "What can I help with?", prompt_de: "", prompt_en: "" }, ], }; ``` The widget's empty state renders the matching starter list. Click → the prompt populates the textarea (or sends immediately if `prompt_de` is empty — letting the user type their own). Picking up "Lege eine Frist an: " seeds the input *partially* so the user finishes the sentence — a deliberate friction-reducer for the common "draft and approve" workflow. ### 5.2 Why per-route registry, not LLM-generated suggestions? Considered: dynamically ask Paliadin to suggest 3 starters based on context. Rejected because: 1. **Latency**: every drawer-open would burn a full turn before the user even types. The PoC's tmux turn is ~2-5 seconds cold; that's an unusable empty state. 2. **Determinism**: m's audience (PA team) needs predictable affordances. "What does this thing know how to do?" answered the same way each visit beats "what does this thing know how to do *today*?" 3. **Translatability**: hand-crafted bilingual starters live next to the rest of the i18n. LLM-generated would be one language at a time. The registry is small (~10 routes × 3 starters × 2 langs = ~60 strings) and lives next to `i18n.ts` patterns m's team already understands. --- ## 6 · Backend transport — tmux relay vs Anthropic API ### 6.1 Recommendation: keep tmux relay for v1 of the inline modal Two reasons: 1. **Scope discipline**: the inline modal's user-visible payoff is independent of which backend serves it. Cutover to the API is a 4-6 commit piece of substantial work (auth headers, prompt-cache management, tool-definition framework, streaming format conversion, budget controls, audit reshape, plus the existing tmux path needs to remain as fallback during rollout). Bundling it with the inline modal doubles the design's blast radius for no inline-modal-side benefit. 2. **Owner-only scope**: paliad's user base today is `PaliadinOwnerEmail = m`. One user. The tmux relay's serialised one-turn-at-a-time, ~2-5s cold start, ~1-3s warm response holds up fine for one user clicking through the day. ### 6.2 What the API cutover *would* fix (recommend as Phase 2) When scope expands beyond owner-only — even just to "m + 2 PA colleagues for piloting" — the tmux relay starts to bend: - **Concurrency**: serialised turn lock means PA-A waits while PA-B thinks. Per-user tmux sessions help but mRiver still has finite resources. - **Latency**: ~2s cold tmux start is ok for one user; bad for "I just opened the widget, ask a quick question, close" rhythm at scale. - **Cost vs subscription**: m's Claude Code subscription covers his personal turns. Multi-user would either need m's account to absorb the load (dubious) or the firm's enterprise key (the actual prod path). - **Streaming**: tmux streaming today is the youpc.org-style "tail the response file as it grows" stopgap. Real token streaming (TTFB <1s) needs the API. The API cutover should therefore be **a prerequisite for opening Paliadin beyond owner-only**. The inline modal's design assumes API-cutover-ready boundaries (the relay interface in §6.4) so when m flips the switch, the inline-modal frontend doesn't change. ### 6.3 Why not cutover now anyway? It's tempting because: - The CLAUDE.md note about `ANTHROPIC_API_KEY` reserved-but-unused has been there since 2026-04-16 and would benefit from being un-deferred. - The inline modal is the natural moment to revisit infrastructure. - Klaus's youpc.org has built a relay-interface abstraction (`youpcAIRelay` interface in `youpc_ai_relay.go`) that paliad could borrow for the swap point. **Counter-arguments that win:** - Today's tmux relay shipped only 2-3 days ago (`paliadin_remote.go` reference t-paliad-151). It's not a legacy substrate to escape — it's fresh code that hasn't earned a rewrite yet. - The compliance question for the API path (HLC-key vs personal-key, audit retention requirements, prompt-logging policy) hasn't been resolved with HLC IT. m flagged this as the **biggest open question** in the t-paliad-146 design and it's still open. - Inline modal can ship entirely on the existing relay; if the API cutover comes later, the modal doesn't have to re-ship. **Therefore**: design a small interface seam (§6.4) so v1 doesn't paint us into a tmux-only corner, but don't pay the cutover cost in this PR. ### 6.4 Relay-interface seam (small, optional, recommended) Mirror youpc.org's pattern (`youpc_ai_relay.go`) but smaller — paliad has one role, no streaming variant yet: ```go // internal/services/paliadin_relay.go (new) type PaliadinRelay interface { RunTurn(ctx context.Context, session string, turnID uuid.UUID, envelope string) ([]byte, error) Reset(ctx context.Context, session string) error HealthGate(ctx context.Context, session string) error } ``` `LocalPaliadinService` and `RemotePaliadinService` keep their current shapes; the audit-row writes (`paliadinDB`) stay shared. `RunTurn` becomes a thin wrapper that builds the envelope (with the new `[ctx …]` block from §4.2) and delegates to the relay. A future `httpAPIRelay` slots in beside the SSH one without touching the audit/turn-row code. **Don't extract the interface unless the inline modal's PR organically needs it.** If the modal can ship without restructuring the existing relay, the abstraction-cost is negative. --- ## 7 · Agent-suggested write path — schema + flow ### 7.1 Schema decision: extend `approval_requests`, not entity rows The brief listed three candidate locations: | Option | Where the marker lives | Verdict | |---|---|---| | A | `boolean agent_suggested` on `paliad.deadlines` / `paliad.appointments` | **Reject**: pollutes domain tables; survives past approval (the entity is no longer "agent-suggested" once it's been live for six months); doesn't carry which agent / which turn | | B | `text suggested_by_agent` on entity rows (multi-agent provenance) | Same problems as A; "agent name" never used because we have one agent | | C | New columns on `paliad.approval_requests` linking back to the suggesting turn | **Recommended** | The `approval_request` row IS the audit-chain entry; the entity row is just current state. Provenance information belongs on the audit-chain row where it can persist forever without polluting the entity schema. **Migration 070 (proposed):** ```sql ALTER TABLE paliad.approval_requests -- 'user' = direct user create; 'agent' = drafted by Paliadin from a chat turn. ADD COLUMN requester_kind text NOT NULL DEFAULT 'user' CHECK (requester_kind IN ('user', 'agent')), -- When requester_kind='agent', the chat turn the suggestion came from. -- NULL otherwise. ON DELETE SET NULL — the audit record survives even -- if the turn row is purged (paliadin_turns has no retention policy -- today, but design for it). ADD COLUMN agent_turn_id uuid REFERENCES paliad.paliadin_turns(turn_id) ON DELETE SET NULL, ADD CONSTRAINT approval_requests_agent_xor CHECK ( (requester_kind = 'agent' AND agent_turn_id IS NOT NULL) OR (requester_kind = 'user' AND agent_turn_id IS NULL) ); CREATE INDEX approval_requests_agent_turn_idx ON paliad.approval_requests (agent_turn_id) WHERE agent_turn_id IS NOT NULL; -- paliadin_turns also gets the structured context column. ALTER TABLE paliad.paliadin_turns ADD COLUMN context jsonb; ``` `requested_by` continues to be the user uuid — even for agent suggestions the user is the *initiator* (Paliadin acts on their behalf, never autonomously). `requester_kind` distinguishes "the user typed Speichern" from "the user typed `/lege eine Frist an: …` to Paliadin and Paliadin drafted it; the user has not yet approved". ### 7.2 The flow 1. **User asks Paliadin**: "Lege eine Frist für diese Akte an: 16.05. Klageerwiderung Acme". 2. **Paliadin's SKILL.md gets a new section**: "Agent-suggested writes" that teaches it to call a new MCP tool `paliad__suggest_deadline` (and siblings for appointment / project_note / project_attach). The tool's server-side handler: - Validates the user has visibility on the project (existing `can_see_project`). - Calls `DeadlineService.Create` *with the new `IsAgentSuggestion=true` flag* and `agent_turn_id=`. - Inside the create-tx, after the entity insert, the existing approval hookup runs: `ApprovalService.SubmitCreate(...)`. **Critical change**: when `IsAgentSuggestion` is set, the submit unconditionally creates an approval request *even if no policy applies* — the agent path is approval-gated by construction, not by partner-unit policy. 3. **Eye-pill 👀 + sparkle ✨** render on the resulting row in `/inbox`, `/deadlines`, `/agenda`. Click → standard approve/reject UI. Approve flips status to `approved`, sets `decision_kind='peer'` (or admin_override if global_admin), the entity becomes live. 4. **Audit chain on the project's Verlauf**: - `deadline_approval_requested` event with `metadata.requester_kind='agent'` + `metadata.agent_turn_id=`. Verlauf renderer picks this up and labels the event "Paliadin hat eine Frist vorgeschlagen ✨". - `deadline_approval_approved` with the user as `decided_by` + the existing `decision_kind` ladder. Verlauf renders "Anna hat Paliadin's Vorschlag genehmigt ✨". ### 7.3 Why agent-suggested unconditionally goes through approval Two reasons: 1. **Trust gradient**: even if a partner has direct create authority on their own projects (no policy = no approval needed today), an agent suggesting on their behalf is qualitatively different. Visible review keeps the user in the loop. 2. **Single audit shape**: today the partner-unit policy decides which creates need approval; bypassing that for agent suggestions creates a second code path. Forcing agent suggestions into the approval pipeline means there's exactly one "agent created an entity" audit shape (the approval_request row). A user who finds the per-suggestion review tedious can request `/genehmige einfach alles was Paliadin vorschlägt` — but that's a Phase 2 setting ("auto-approve agent suggestions on projects where I'm lead"), explicitly out-of-scope for v1 (and m says so in #20: "Multi-turn agent loops … Every creation gets the user's eye."). ### 7.4 What entities can Paliadin suggest in v1? The brief mentions "deadlines, appointments, notes, project-tree edits". Recommend ordering by reversibility + audit complexity: | Entity | v1? | Why | |---|---|---| | Deadline create | **Yes** | Highest-value (Klaus would rate this top), well-supported by existing `pending_create` lifecycle | | Appointment create | **Yes** | Same lifecycle substrate; symmetric tool | | Project note (`project_events.note`) | **Yes** | Read-only audit event, no approval gate today — but for agent-authored notes route through approval anyway (consistency) | | Project-tree edit (move, rename) | **No, defer** | Approval lifecycle for project moves doesn't exist; designing it is its own task. | | Deadline / appointment **edit** | **No, defer** | Edits today only need approval when date-fields change (t-paliad-138 §Q4). Agent edits would need their own design pass for "what changes does the user see in the diff?" | | Deadline **complete** | **No, defer** | Same reason — complete already has approval lifecycle, but the agent path is qualitatively different (a deadline being marked done is high-stakes; design it after a v1 lands and we see how often agent-creates need editing) | **v1 = create only**. Edits/completes are a Phase 2 expansion. --- ## 8 · Visual language — ✨ alongside 👀, not in place of ### 8.1 Design `.approval-pill--agent` is a new modifier that sits **next to** the existing `.approval-pill--icon` (the 👀 glyph), not replacing it. | Row state | Pill rendering | |---|---| | `approval_status='pending'` AND `requester_kind='user'` | 👀 | | `approval_status='pending'` AND `requester_kind='agent'` | 👀 ✨ | | `approval_status='approved'` AND `requester_kind='user'` | (no pill) | | `approval_status='approved'` AND `requester_kind='agent'` | ✨ (subtle, in the row's *secondary* badge slot — not a pill) | The 👀 + ✨ pairing communicates: "this is awaiting approval *and* came from Paliadin". Hover (`title` attr) on ✨ reads: "Paliadin hat das vorgeschlagen — angeklickt klärt". **Why both glyphs, not a fused single glyph?** The two questions ("is this awaiting approval?" / "did a human or Paliadin originate this?") are orthogonal — a future autopilot mode might let some agent suggestions auto-approve, in which case 👀 disappears but ✨ stays. Keeping them separate keeps the visual taxonomy decomposable. ### 8.2 Where ✨ renders Three surfaces: 1. **Eye-pill row** (`/inbox`, `/deadlines`, `/agenda`, project detail, /events): 👀 ✨ side-by-side when applicable. Same `.approval-pill` shape, separate elements. 2. **Audit log** (`/admin/audit-log` + project Verlauf): the row's "approved by" line gets a trailing ✨ when the underlying request had `requester_kind='agent'`. Reads "Anna ✨ Schmidt" → tooltip "Über Paliadin vorgeschlagen, von Anna genehmigt". 3. **Approval request inbox card**: the requester's name in the inbox card gets a subtle "✨ Paliadin (für Anna)" badge instead of just "Anna" when `requester_kind='agent'`. ### 8.3 The "+p" annotation question m's #20 said: "we say USER + p or with a star or something". The "+p" text annotation reads in audit logs but doesn't scan in a pill row (✨ is recognisable; "+p" is not without learning). **Recommend**: ✨ as the universal glyph. Reserve a textual fallback for compliance-export contexts where emojis don't render — there the audit string becomes "Anna [agent: Paliadin]" rather than "Anna ✨". --- ## 9 · Persona separation m's brief asked whether to lean on klaus's "scope-bouncer in SKILL.md" pattern (Hugo refuses legal questions, points at Lexie; Lexie refuses "how do I subscribe?", points at Hugo) for paliad — i.e. pre-design multi-persona infrastructure. **Recommendation: don't.** Paliad has one Paliadin (Patentpraxis assistant at HLC's Patent team). The youpc.org split exists because *youpc.org has fundamentally different audiences* — public visitors (Hugo handles "how does this site work?") and premium-beta lawyers (Lexie does case-law research). Their refusal scopes are different because their users are different. Paliad's audience is one cohesive group: HLC PA team. They want one assistant that does "everything PA-relevant" — Aktenmanagement, Fristen, Begriffe, Gerichte, UPC-Recht. There's no audience pair that requires distinct refusal scopes. **If Phase 2 wants to add a case-law research persona** (e.g. cross-link to youpc.org's Lexie) — *that's a separate skill alongside Paliadin*, not a persona-split inside Paliadin. The infrastructure for that already exists in Claude Code's skill router (multiple skills, each its own description/persona). **No SKILL.md changes for persona separation in this design**. The skill gets §4.2's `[ctx …]` parser added, plus §7.2's `paliad__suggest_*` tool guidance, but the persona stays "der Paliad-Patentpraxis-Assistent". --- ## 10 · Phasing & implementation surface ### 10.1 Suggested phasing (single PR is feasible; split optional) **Slice A — schema + relay seam** (~1 commit) - Migration 070: `approval_requests.requester_kind` + `agent_turn_id` + xor-check + index; `paliadin_turns.context jsonb`. - Optional `PaliadinRelay` interface extraction (skip if it makes the PR bigger without removing duplication). **Slice B — context payload + SKILL.md update** (~1 commit) - Wire structured `PaliadinContext` from frontend → Go → tmux envelope. - SKILL.md `[ctx …]` parsing + behaviour. - `client/paliadin-context.ts` route-table + entity extraction (one file). - `/api/paliadin/turn` accepts the new body shape (backwards-compatible: old `page_origin` still honoured if `context` is absent). **Slice C — inline widget** (~1 commit, biggest) - `frontend/src/components/PaliadinWidget.tsx`. - `client/paliadin-widget.ts` (drawer state, sending, history, hide-on-route). - `client/paliadin-starters.ts` registry (8 routes + default). - Mechanical pass: every authenticated TSX adds ``. - CSS: `.paliadin-widget`, `.paliadin-drawer`, `.paliadin-trigger`, `.paliadin-context-chip`, ~150 lines of `global.css`. - ~30 i18n keys. **Slice D — agent-suggested write path** (~1 commit) - `paliad__suggest_deadline` + `paliad__suggest_appointment` MCP tools (or HTTP tool, depending on how the MCP scope already wires — `internal/handlers/paliadin_tools.go` if new file warranted). - `DeadlineService.Create` / `AppointmentService.Create` accept a `IsAgentSuggestion bool` + `AgentTurnID *uuid.UUID` plumbed into `ApprovalService.SubmitCreate` (which gets a sibling `SubmitAgentCreate` that always creates a request even without policy). - SKILL.md adds the §7.2 "Agent-suggested writes" instruction block. **Slice E — visual language** (~1 commit) - `.approval-pill--agent` CSS. - `events.ts`, `agenda.ts`, `inbox.ts` render ✨ when `requester_kind='agent'`. - Audit-log + Verlauf renderer extends to surface ✨ on approved-from-agent events. - ~10 i18n keys for the badges + tooltips. **Recommended PR shape**: single PR with five commits in this order. Slice A's migration is independent (can deploy without the rest); Slice D needs B + C; Slice E builds on D. If sliced into multiple PRs, A and B-C can ship independently of D-E (modal works as read-only chat without the write path; that's already an upgrade). ### 10.2 Files of note for the implementer **New files:** - `internal/db/migrations/070_paliadin_inline.{up,down}.sql` - `internal/handlers/paliadin_tools.go` (suggest verbs) - `internal/services/paliadin_relay.go` (optional interface) - `frontend/src/components/PaliadinWidget.tsx` - `frontend/src/client/paliadin-widget.ts` - `frontend/src/client/paliadin-starters.ts` - `frontend/src/client/paliadin-context.ts` **Edits:** - `internal/services/paliadin.go` (TurnRequest gains structured Context; insertTurnRow stores it) - `internal/services/approval_service.go` (SubmitCreate accepts agent-flag; SubmitAgentCreate variant) - `internal/services/deadline_service.go`, `internal/services/appointment_service.go` (Create accepts IsAgentSuggestion + AgentTurnID; threads to ApprovalService) - `internal/handlers/paliadin.go` (turnRequest body schema) - `frontend/src/client/events.ts`, `agenda.ts`, `inbox.ts` (✨ render) - `frontend/src/styles/global.css` (drawer + ✨ pill CSS) - `frontend/src/client/i18n.ts` (~40 new keys × 2 langs) - `frontend/src/components/Sidebar.tsx` — no edit (the existing sidebar link logic already gates on owner; no new entries) - ~30 page TSX files: mechanical `` add (~1 line each) - `~/.claude/skills/paliadin/SKILL.md` (via `scripts/install-paliadin-skill`): add §4.2 ctx-parser block + §7.2 suggest-tools block **Total estimated surface**: comparable to t-paliad-146 (the original Paliadin design — ~3500-4500 LoC) plus the agent-suggest write path (~1000 LoC). Single PR is feasible if the implementer is pattern-fluent; split is fine. --- ## 11 · Open questions for m These are the calls m has to make before any coder shift starts. ### Q1 — Scope gate: still owner-only? The inline modal's design assumes `PaliadinOwnerEmail` stays as the only gate (m only). When does scope expand? - (a) **Stays owner-only for v1** of inline modal — recommended; matches brief. ← **inventor's pick** - (b) Extend to a beta-features whitelist (firm-wide email domain + flag). - (c) Expand to all of `hoganlovells.com` immediately. Requires API cutover (Phase 2 prerequisite). ### Q2 — Backend: tmux relay or Anthropic API for the inline modal? - (a) **Keep tmux relay** for v1 — recommended; ships fastest. ← **inventor's pick** - (b) Cutover to Anthropic API now — slower ship; better long-term. - (c) Both: ship tmux v1, design the API path as a parallel deferred PR. ### Q3 — Agent-suggested entities in v1: where to draw the line? - (a) **Create-only**: deadline, appointment, note. Defer edits/completes/project-tree. ← **inventor's pick** - (b) Create + edit (deadline + appointment). - (c) Create + edit + complete + project-tree. ### Q4 — Visual language for agent provenance? - (a) **✨ glyph alongside 👀** — recommended; orthogonal to lifecycle. ← **inventor's pick** - (b) "+p" text annotation in audit lines only; no glyph in pills. - (c) Replace 👀 with ✨ for agent-pending rows (single glyph, more compact). ### Q5 — Selection text in context payload — default on or off? - (a) **Default on**, opt-out via widget settings — recommended. ← **inventor's pick** - (b) Default off, opt-in via widget settings. - (c) Always on, no toggle. ### Q6 — Widget visibility scope: everywhere except `/paliadin`, or finer? - (a) **Everywhere except `/paliadin`, `/login`, `/onboarding`** — recommended; lowest cognitive load. ← **inventor's pick** - (b) Only on data-bearing pages (dashboard, projects, deadlines, agenda, events, inbox); hide on tool pages (fristenrechner etc.). - (c) User-configurable per page. ### Q7 — Modal vs dialog: drawer + scrim, or non-modal floating panel? - (a) **Modal slide-out drawer with scrim** (focus-traps) — recommended. ← **inventor's pick** - (b) Non-modal floating panel (page stays interactive while widget is open). ### Q8 — Keyboard shortcut for opening: Cmd-K? - (a) **Cmd-K / Ctrl-K** — recommended. ← **inventor's pick** - (b) Different shortcut (m to specify). - (c) No shortcut, button-only. ### Q9 — Context payload truncation cap (selection text)? - (a) **1000 chars** — recommended; balances usefulness vs prompt-bloat. ← **inventor's pick** - (b) Higher cap (5000 chars). - (c) Lower cap (300 chars). ### Q10 — Persona separation pre-design? - (a) **Single Paliadin, no scope-bouncer pattern** — recommended; YAGNI. ← **inventor's pick** - (b) Add scope-bouncer pattern now (Paliadin refuses non-paliad questions, points at... where?). - (c) Pre-design split with a second skill (Phase 2 case-law researcher). ### Q11 — Auto-approve some agent suggestions? - (a) **No, every agent suggestion needs the user's eye** — recommended; matches m's #20 verbatim. ← **inventor's pick** - (b) Auto-approve agent suggestions on projects where the user is lead. - (c) Auto-approve when the suggestion was a direct response to "Lege … an" (user opted in by phrasing). ### Q12 — Recommended implementer? Same substrate as t-paliad-146 + t-paliad-160 + t-paliad-138 (paliadin, approval pipeline, eye-pill UI). Pattern-fluent Sonnet work. - (a) **Any pattern-fluent Sonnet coder** — recommended. ← **inventor's pick** - (b) The same coder who shipped t-paliad-160 (deepest context on the approval pipeline). - (c) Two coders: one on Slices A-C (modal + context), one on Slices D-E (agent-suggest + visual language). --- ## 12 · Out of scope (for now) — preserved Per m's brief: - Direct Paliadin write permission (no RLS bypass, no agent service-role identity). The approval gate stays the only path agents take into prod data. - Multi-turn agent loops — no chained writes without per-step user approval. - Production-v1 Anthropic API cutover for the existing standalone `/paliadin` route (recommended in §6 as a *prerequisite* for opening beyond owner-only, but not committed in this task). - Edits / completes / project-tree as agent-suggestible entities (§7.4 defers to Phase 2). - Persona separation infrastructure (§9 defers indefinitely). --- ## 13 · Trade-offs flagged | Trade-off | What we accept | Mitigation | |---|---|---| | Tmux-relay v1 caps concurrency at one turn per user | Owner-only v1 makes this fine | Spec the relay-interface seam (§6.4) so API cutover is non-disruptive | | Mechanical `` pass touches ~30 files | Same pattern as t-paliad-042 PWAHead, low risk | One commit per slice keeps blame surface tight | | Agent suggestions unconditionally route through approval | Some users may find it tedious | Phase 2 auto-approve setting (m wants Q11 = no, so this isn't urgent) | | Two glyphs (👀 + ✨) might confuse first-time approvers | Slight onboarding cost | Tooltip on hover; admin/onboarding doc one-liner | | Selection-text in context payload risks accidental info leakage | Low (data already in DB) | Cap + redaction in admin dashboard (§4.3) | | Per-route starter registry needs maintenance as routes evolve | Yes; cost is real | Default fallback ensures no route is silent; route renames are caught by build (registry imports route names as a const map) | --- ## 14 · Implementation hygiene - **No bare CSS tokens.** New `.paliadin-widget*` + `.approval-pill--agent` CSS uses existing `--color-*` / `--accent-*` / `--bg-soft` tokens. The reminder from t-paliad-150 (third occurrence of bare-token leaks) holds. - **No RAISE EXCEPTION in migration 070** — Maria's build constraint. - **No `2>&1` on diagnostic** — global rule. - **i18n must compile** — every new label gets a key in `client/i18n.ts` + DE/EN values; `bun run build` regenerates `i18n-keys.ts`. - **Build + vet + test gate** — `go build ./...` + `go vet ./...` + `go test ./...` + `cd frontend && bun run build` all clean before push. - **Don't self-merge** — push branch, comment on Gitea #20, await m's merge gate. - **Don't close issue #20** — m closes issues. Set `done` label on approval. --- ## 15 · End-of-shift checklist (this design) - [x] Read m/paliad#20 + Klaus's reply (msg #1563 / comment). - [x] Read existing `paliadin.go` + `paliadin_remote.go` + `approval_service.go` + `paliadin-shim` + `install-paliadin-skill` + `~/.claude/skills/paliadin/SKILL.md`. - [x] Read youpc.org reference: `sidebar-widget.html` + `sidebar.js` + `ai-chat-client.js` + `youpc_ai_relay.go`. - [x] Verify live state: paliad.de up, migration tracker at 69, schema columns matched expectations, eye-pill 👀 already wired. - [x] Take a position on every decision in the brief (see §0 table; §11 for the open questions). - [x] No hour estimates anywhere in the doc. - [x] Recommend implementer + phasing. - [ ] Commit this doc on `mai/dirac/inventor-inline-paliadin`. - [ ] Push branch. - [ ] Comment on Gitea #20 with summary + doc link. - [ ] File mBrian synthesis node under `topic-paliadin` (or equivalent). - [ ] `mai report completed "DESIGN READY FOR REVIEW: …"` and **stop**. Do not auto-flip to coder. --- *Inventor parked after this commit. The head will surface to m for the go/no-go gate before any coder shift begins. Skipping that gate has burned commits before (m/mAi#142); the gate is non-negotiable.*