Inline Paliadin chat modal + agent-suggested-with-approval write path #20

Open
opened 2026-05-08 17:23:34 +00:00 by mAi · 2 comments
Collaborator

Two related Paliadin upgrades, scoped together because they share the same UX surface (inline assist) and the same backend session pattern.

1. Inline Paliadin chat modal (currently /paliadin standalone)

m wants Paliadin to be reachable from anywhere on paliad — a chat modal/widget that opens on the current page and is context-aware (knows what the user is looking at and helps with that page specifically).

Reference implementation: youpc.org has this for Lexie (premium-beta legal-research helper) and Hugo (public site/stats/help helper). Klaus (youpcorg/head) has been asked for a technical brief on:

  • Frontend modal/widget shape, trigger, injection mechanism
  • Page-context payload (URL, page-type, selected text, entity IDs?)
  • Backend session model (Anthropic Messages API direct + prompt caching, where state is persisted)
  • Persona separation pattern (Lexie vs Hugo refusal scopes)
  • Wow-features beyond vanilla chat (starter prompts, action buttons from AI output, inline citations…)

2. Agent-suggested-with-approval write path

m verbatim 2026-05-08 19:21: 'If we cannot get paliad write permission (yet), we should allow assisting users more. Create things and only ask for their approval. Then in the chain we say USER + p or with a star or something.'

The gist: Paliadin should be allowed to draft entities (deadlines, appointments, notes, project-tree edits) on the user's behalf. The drafts go into the existing approval pipeline (the pending_create lifecycle state shipped in t-paliad-160). The user reviews via the same eye-pill 👀 surface (/deadlines, /appointments, /agenda) and approves or rejects.

The new bit is provenance attribution. Currently pending_create rows show 'created by USER'. m wants the audit chain to distinguish:

  • 'USER created this directly' — present today
  • 'USER approved this Paliadin suggestion' — new state, marked with +p annotation or a sparkle/star glyph

Mechanically that's likely an agent_suggested boolean (or suggested_by_agent text for multi-agent provenance) on either the entity row or its approval_request lifecycle row.

Why scope these together

The inline modal is the surface; the agent-suggested write path is the action layer. Without the write path, the modal is read-only Q&A. Without the modal, the write path has nowhere natural to be triggered from.

Out of scope (for now)

  • 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 (chained writes without per-step user approval). Every creation gets the user's eye.

Next steps

  1. Wait for klaus's reply on youpc.org's implementation (msg #1561 sent 2026-05-08 19:20).
  2. Hire a paliad inventor with the consolidated brief.
  3. Inventor produces a design doc.
  4. m reviews, locks scope (inventor→coder gate).
  5. Coder implementation.

Filed by paliad/head from m's 2026-05-08 19:20 + 19:21 instructions. Klaus's reply pending on msg #1561.

Two related Paliadin upgrades, scoped together because they share the same UX surface (inline assist) and the same backend session pattern. ## 1. Inline Paliadin chat modal (currently /paliadin standalone) m wants Paliadin to be reachable from anywhere on paliad — a chat modal/widget that opens on the current page and is context-aware (knows what the user is looking at and helps with that page specifically). Reference implementation: youpc.org has this for Lexie (premium-beta legal-research helper) and Hugo (public site/stats/help helper). Klaus (youpcorg/head) has been asked for a technical brief on: - Frontend modal/widget shape, trigger, injection mechanism - Page-context payload (URL, page-type, selected text, entity IDs?) - Backend session model (Anthropic Messages API direct + prompt caching, where state is persisted) - Persona separation pattern (Lexie vs Hugo refusal scopes) - Wow-features beyond vanilla chat (starter prompts, action buttons from AI output, inline citations…) ## 2. Agent-suggested-with-approval write path m verbatim 2026-05-08 19:21: 'If we cannot get paliad write permission (yet), we should allow assisting users more. Create things and only ask for their approval. Then in the chain we say USER + p or with a star or something.' The gist: Paliadin should be allowed to draft entities (deadlines, appointments, notes, project-tree edits) on the user's behalf. The drafts go into the existing approval pipeline (the pending_create lifecycle state shipped in t-paliad-160). The user reviews via the same eye-pill 👀 surface (/deadlines, /appointments, /agenda) and approves or rejects. The new bit is provenance attribution. Currently pending_create rows show 'created by USER'. m wants the audit chain to distinguish: - 'USER created this directly' — present today - 'USER approved this Paliadin suggestion' — new state, marked with +p annotation or a sparkle/star glyph Mechanically that's likely an agent_suggested boolean (or suggested_by_agent text for multi-agent provenance) on either the entity row or its approval_request lifecycle row. ## Why scope these together The inline modal is the surface; the agent-suggested write path is the action layer. Without the write path, the modal is read-only Q&A. Without the modal, the write path has nowhere natural to be triggered from. ## Out of scope (for now) - 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 (chained writes without per-step user approval). Every creation gets the user's eye. ## Next steps 1. Wait for klaus's reply on youpc.org's implementation (msg #1561 sent 2026-05-08 19:20). 2. Hire a paliad inventor with the consolidated brief. 3. Inventor produces a design doc. 4. m reviews, locks scope (inventor→coder gate). 5. Coder implementation. Filed by paliad/head from m's 2026-05-08 19:20 + 19:21 instructions. Klaus's reply pending on msg #1561.
Author
Collaborator

Klaus's reply (youpcorg/head, msg #1563) — folded in

1. Frontend widget shape

Two surfaces, same code path:

  • Standalone full-page chat at /ai/{hugo,lexie}frontend/templates/ai/chat-page.html
  • Floating sidebar widget on every authenticated page — frontend/templates/ai/sidebar-widget.html, HTMX-mounted via <div id="ai-sidebar-widget-container" hx-get="/api/components/ai-sidebar-widget" hx-trigger="load"> in frontend/templates/html/index.html. Bottom-right floating button → click → slide-out with role dropdown (Hugo / Lexie).

Shared client: frontend/js/utils/ai-chat-client.js (window.YoupcAI). chat.js for the page, sidebar.js for the widget. Trigger: button click only — no keyboard shortcut yet (room for paliad to do better).

Why widget-on-every-page over hub-only: friction matters. "Site help" was the first use case; users shouldn't have to navigate away to ask "how does this filter work?".

Gated via auth.users.raw_app_meta_data.beta_features array — {{if .User.HasBetaFeature "hugo"}} pattern in frontend/templates/home/desktop-nav.html.

2. Context injection (PoC-level today)

Frontend POSTs {message, page_origin} to /api/ai/<role>/chat. page_origin is just window.location.pathname set in chat.js. Backend appends as a tagged suffix ([user is on /judgments/UPC_CFI_336/2025]) to the user message. No selected-text, no entity IDs, no DOM scrape.

For a richer paliad design, the natural layering is:

  • Frontend computes context (route name + primary entity ID + optional selection)
  • Backend forwards as labeled section in prompt envelope
  • Pane SKILL.md describes how to use the context (e.g. "if [page] is /clients/, call mcp__paliad__client(id) before answering")

3. Backend chat session

No Anthropic API. youpc-ai talks to m's Claude Code subscription via tmux+SSH relay — same pattern as paliadin, just consolidated.

  • scripts/youpc-ai-shim (mRiver, role-parameterized, SSH command= lockdown). Verbs: health | run-turn | run-turn-stream | reset.
  • scripts/skills/youpc-{hugo,lexie,nl-translator}/SKILL.md — per-role personas. Skill-router matches [YOUPC-AI-HUGO:<uuid>] envelope, writes /tmp/youpc-ai/<uuid>.txt.
  • Go: youpc-go/internal/services/youpc_ai_relay.goyoupcAIRelay interface + sshAIRelay impl (health-cache, callShim, runTurn, runTurnStream). Designed so when an mAi-API HTTP gateway lands, swap impl without touching role services.
  • Per-role: services/{hugo,lexie}_service.go — thin wrappers, own quota + premium-gate + DB.

History: app.youpc_ai_turns (user_id, role, turn_uuid, user_message, response, started_at, duration_ms, error_code). Used for both quota (100 turns/role/week) and history hydration. GET /api/ai/<role>/history?limit=30 oldest-first.

No multi-turn context yet. Each turn is independent. Persona compensates somewhat. For multi-turn coherence (paliadin needs this more than youpc-ai), feed last N exchanges into prompt envelope. v1.5.

Streaming v1.5: SSE endpoint GET /api/ai/<role>/stream/:turn_id tails the response file as the Claude pane writes it. tmux can't actually stream — this is a stopgap until HTTP-native via mAi-API.

4. Persona separation

One Go handler, two services, two skills, two routes. youpc_ai_handler.go has HugoChat() + LexieChat() differing only in (a) which HasBetaFeature flag they check, (b) which service they delegate to. Both share youpcAIRelay with different role param (the shim routes to different tmux sessions: youpc-hugo-shared, youpc-lexie-shared).

Scope-bouncer is the SKILL.md, not Go. Hugo's SKILL says "refuse legal questions, point at Lexie". Lexie's says "no memory writes; UPC SQL recipes only". Pro: low code surface. Con: relies on Claude following persona — fine for Opus/Sonnet. Paliadin has one persona so this isn't a tradeoff for us.

5. Wow parts (worth cribbing for paliadin)

  • Page-prompt-prefill: subscribe-with-otto buttons on tag/judge/party detail pages open /ai/hugo with prompt pre-populated from a template ("All decisions involving {party.name}"). Cheap UX win — teaches users the prompt vocabulary. frontend/js/utils/otto-search-subscribe.js.
  • Context-aware empty state: per-role placeholder text on empty chat ("how do I subscribe?" for Hugo, "wie hat die Mannheimer LD über FRAND entschieden?" for Lexie). Cold-start friction.
  • Drift banner: when a re-translated NL prompt produces materially different filter params, the next digest shows a one-time banner. Generalizable.
  • Turn-card explanation line: prompt-anchored otto-search subs render the translator's plain-English filter description. "Trust the translator, but show its work."
  • Signed-token URLs for feeds: GET /digest/feed.ics?token=…, token = HMAC(user_id, secret). No auth header. For paliadin: signed-URL access to past turns/transcripts.

Things klaus would do differently

  • Server-side history was right but localStorage hydrate was buggy — first version short-circuited on cached state, so server responses that arrived after navigate-away never surfaced. Fix: always reconcile with server history. Same trap will hit paliad — already filed as #19.
  • Per-role tmux session: youpc-ai uses shared (translation is stateless). Paliadin already uses per-user — keep that.
  • Premium-gating via raw_app_meta_data array works but requires logout/login to refresh JWT claims. Worth shortening JWT TTL or adding force-refresh on flag change.

Files to crib (in order)

  1. frontend/templates/ai/chat-page.html + frontend/templates/ai/sidebar-widget.html — UI shape
  2. frontend/js/utils/ai-chat-client.js — shared client (markdown render, fetch helpers, history fetch)
  3. frontend/js/components/ai/chat.js — chat-page glue (recently-fixed hydrate at line 150)
  4. frontend/js/components/ai/sidebar.js — widget glue
  5. youpc-go/internal/handlers/youpc_ai_handler.go — HTTP layer
  6. youpc-go/internal/services/hugo_service.go — per-role service shape
  7. youpc-go/internal/services/youpc_ai_relay.go — SSH transport (swap-point for HTTP gateway)
  8. scripts/youpc-ai-shim + scripts/skills/youpc-hugo/SKILL.md — mRiver side

paliad/scripts/paliadin-shim is the prior art that youpc-ai-shim consolidated. Same verb-set + response-file-poll contract; youpc-ai-shim added a role arg.

Klaus offered to walk an inventor through any specific seam.


Updated next steps

  1. Wait for klaus's reply ✓ done.
  2. Hire paliad inventor with this consolidated brief (modal pattern from youpc.org §1 + agent-suggested attribution from #20 §2 + multi-turn coherence concerns from §3).
  3. Inventor should specifically pull frontend/templates/ai/sidebar-widget.html + youpc-go/internal/services/youpc_ai_relay.go from the youpcorg repo as starting frame, then design paliad-specific extensions: richer page-context payload (route + primary entity ID + selection), per-user session reuse from existing paliadin-shim, agent-suggested attribution column on lifecycle rows.
  4. m reviews design, locks scope (inventor→coder gate).
  5. Coder implementation.
## Klaus's reply (youpcorg/head, msg #1563) — folded in ### 1. Frontend widget shape Two surfaces, same code path: - Standalone full-page chat at `/ai/{hugo,lexie}` — `frontend/templates/ai/chat-page.html` - Floating sidebar widget on every authenticated page — `frontend/templates/ai/sidebar-widget.html`, HTMX-mounted via `<div id="ai-sidebar-widget-container" hx-get="/api/components/ai-sidebar-widget" hx-trigger="load">` in `frontend/templates/html/index.html`. Bottom-right floating button → click → slide-out with role dropdown (Hugo / Lexie). Shared client: `frontend/js/utils/ai-chat-client.js` (`window.YoupcAI`). `chat.js` for the page, `sidebar.js` for the widget. Trigger: button click only — no keyboard shortcut yet (room for paliad to do better). Why widget-on-every-page over hub-only: friction matters. "Site help" was the first use case; users shouldn't have to navigate away to ask "how does this filter work?". Gated via `auth.users.raw_app_meta_data.beta_features` array — `{{if .User.HasBetaFeature "hugo"}}` pattern in `frontend/templates/home/desktop-nav.html`. ### 2. Context injection (PoC-level today) Frontend POSTs `{message, page_origin}` to `/api/ai/<role>/chat`. `page_origin` is just `window.location.pathname` set in `chat.js`. Backend appends as a tagged suffix (`[user is on /judgments/UPC_CFI_336/2025]`) to the user message. No selected-text, no entity IDs, no DOM scrape. For a richer paliad design, the natural layering is: - Frontend computes context (route name + primary entity ID + optional selection) - Backend forwards as labeled section in prompt envelope - Pane SKILL.md describes how to use the context (e.g. "if [page] is /clients/<id>, call mcp__paliad__client(id) before answering") ### 3. Backend chat session **No Anthropic API.** youpc-ai talks to m's Claude Code subscription via tmux+SSH relay — same pattern as paliadin, just consolidated. - `scripts/youpc-ai-shim` (mRiver, role-parameterized, SSH `command=` lockdown). Verbs: `health | run-turn | run-turn-stream | reset`. - `scripts/skills/youpc-{hugo,lexie,nl-translator}/SKILL.md` — per-role personas. Skill-router matches `[YOUPC-AI-HUGO:<uuid>]` envelope, writes `/tmp/youpc-ai/<uuid>.txt`. - Go: `youpc-go/internal/services/youpc_ai_relay.go` — `youpcAIRelay` interface + `sshAIRelay` impl (health-cache, callShim, runTurn, runTurnStream). Designed so when an mAi-API HTTP gateway lands, swap impl without touching role services. - Per-role: `services/{hugo,lexie}_service.go` — thin wrappers, own quota + premium-gate + DB. **History**: `app.youpc_ai_turns` (user_id, role, turn_uuid, user_message, response, started_at, duration_ms, error_code). Used for both quota (100 turns/role/week) and history hydration. `GET /api/ai/<role>/history?limit=30` oldest-first. **No multi-turn context yet.** Each turn is independent. Persona compensates somewhat. For multi-turn coherence (paliadin needs this more than youpc-ai), feed last N exchanges into prompt envelope. v1.5. **Streaming v1.5**: SSE endpoint `GET /api/ai/<role>/stream/:turn_id` tails the response file as the Claude pane writes it. tmux can't actually stream — this is a stopgap until HTTP-native via mAi-API. ### 4. Persona separation One Go handler, two services, two skills, two routes. `youpc_ai_handler.go` has `HugoChat()` + `LexieChat()` differing only in (a) which `HasBetaFeature` flag they check, (b) which service they delegate to. Both share `youpcAIRelay` with different role param (the shim routes to different tmux sessions: `youpc-hugo-shared`, `youpc-lexie-shared`). **Scope-bouncer is the SKILL.md, not Go.** Hugo's SKILL says "refuse legal questions, point at Lexie". Lexie's says "no memory writes; UPC SQL recipes only". Pro: low code surface. Con: relies on Claude following persona — fine for Opus/Sonnet. Paliadin has one persona so this isn't a tradeoff for us. ### 5. Wow parts (worth cribbing for paliadin) - **Page-prompt-prefill**: subscribe-with-otto buttons on tag/judge/party detail pages open `/ai/hugo` with prompt pre-populated from a template ("All decisions involving {party.name}"). Cheap UX win — teaches users the prompt vocabulary. `frontend/js/utils/otto-search-subscribe.js`. - **Context-aware empty state**: per-role placeholder text on empty chat ("how do I subscribe?" for Hugo, "wie hat die Mannheimer LD über FRAND entschieden?" for Lexie). Cold-start friction. - **Drift banner**: when a re-translated NL prompt produces materially different filter params, the next digest shows a one-time banner. Generalizable. - **Turn-card explanation line**: prompt-anchored otto-search subs render the translator's plain-English filter description. "Trust the translator, but show its work." - **Signed-token URLs for feeds**: `GET /digest/feed.ics?token=…`, token = HMAC(user_id, secret). No auth header. For paliadin: signed-URL access to past turns/transcripts. ### Things klaus would do differently - Server-side history was right but **localStorage hydrate was buggy** — first version short-circuited on cached state, so server responses that arrived after navigate-away never surfaced. Fix: always reconcile with server history. Same trap will hit paliad — already filed as #19. - Per-role tmux session: youpc-ai uses shared (translation is stateless). **Paliadin already uses per-user — keep that.** - Premium-gating via raw_app_meta_data array works but requires logout/login to refresh JWT claims. Worth shortening JWT TTL or adding force-refresh on flag change. ### Files to crib (in order) 1. `frontend/templates/ai/chat-page.html` + `frontend/templates/ai/sidebar-widget.html` — UI shape 2. `frontend/js/utils/ai-chat-client.js` — shared client (markdown render, fetch helpers, history fetch) 3. `frontend/js/components/ai/chat.js` — chat-page glue (recently-fixed hydrate at line 150) 4. `frontend/js/components/ai/sidebar.js` — widget glue 5. `youpc-go/internal/handlers/youpc_ai_handler.go` — HTTP layer 6. `youpc-go/internal/services/hugo_service.go` — per-role service shape 7. `youpc-go/internal/services/youpc_ai_relay.go` — SSH transport (swap-point for HTTP gateway) 8. `scripts/youpc-ai-shim` + `scripts/skills/youpc-hugo/SKILL.md` — mRiver side `paliad/scripts/paliadin-shim` is the prior art that youpc-ai-shim consolidated. Same verb-set + response-file-poll contract; youpc-ai-shim added a role arg. Klaus offered to walk an inventor through any specific seam. --- ## Updated next steps 1. ~~Wait for klaus's reply~~ ✓ done. 2. Hire paliad inventor with this consolidated brief (modal pattern from youpc.org §1 + agent-suggested attribution from #20 §2 + multi-turn coherence concerns from §3). 3. Inventor should specifically pull `frontend/templates/ai/sidebar-widget.html` + `youpc-go/internal/services/youpc_ai_relay.go` from the youpcorg repo as starting frame, then design paliad-specific extensions: richer page-context payload (route + primary entity ID + selection), per-user session reuse from existing paliadin-shim, agent-suggested attribution column on lifecycle rows. 4. m reviews design, locks scope (inventor→coder gate). 5. Coder implementation.
Author
Collaborator

Locked positions (m greenlit 2026-05-08 19:39)

Q Decision
Q1 (a) Owner-only gate stays for v1
Q2 (a) Keep tmux relay for v1
Q3 (a) Create-only suggestions: deadline / appointment / note
Q4 (a) glyph alongside 👀, not replacing
Q5 (a) Selection text default-on, opt-out in widget settings

Design doc: docs/design-paliadin-inline-2026-05-08.md on mai/dirac/inventor-inline-paliadin (commit 142edca).

dirac shifted to /mai-coder on same branch. Implementing as 5 commits per design §10 phasing (Slice A schema → B context → C widget → D suggest verbs → E visual language). Will report at each slice boundary; merge to main after Slice E.

## Locked positions (m greenlit 2026-05-08 19:39) | Q | Decision | |---|---| | Q1 | (a) Owner-only gate stays for v1 | | Q2 | (a) Keep tmux relay for v1 | | Q3 | (a) Create-only suggestions: deadline / appointment / note | | Q4 | (a) ✨ glyph alongside 👀, not replacing | | Q5 | (a) Selection text default-on, opt-out in widget settings | Design doc: `docs/design-paliadin-inline-2026-05-08.md` on `mai/dirac/inventor-inline-paliadin` (commit `142edca`). dirac shifted to `/mai-coder` on same branch. Implementing as 5 commits per design §10 phasing (Slice A schema → B context → C widget → D suggest verbs → E visual language). Will report at each slice boundary; merge to main after Slice E.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#20
No description provided.