Skip to content

Community personalities + Writing Style + leaner agent prompt - #123

Merged
ddutchie merged 16 commits into
mainfrom
ddutchie/uxtweaks
Aug 11, 2026
Merged

ddutchie merged 16 commits into
mainfrom
ddutchie/uxtweaks

Conversation

@ddutchie

@ddutchie ddutchie commented Aug 11, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Three related additions plus fixes, on top of the branch's pre-existing mermaid fix:

  1. Community personalities — pick a personality next to the model selector in chat to layer behavioral rules onto the system prompt. Ships with Caveman, Grill Me, Junior to Senior, Last 20%, Ponytail (source-attributed) + originals; browse/install from the cairn-community catalog, create your own, manage in Settings → AI & Chat → Chat personality. "None" is the default and now persists across restarts.
  2. Writing Style — a new Settings → Writing Style section. A guided 5-step wizard collects persona → paste samples and/or analyse recent notes & tasks → a few questions → generates a full style guide + condensed cheat sheet (both editable). Chat, the coding agent, and the MCP server can all call get_user_writing_style({ mode: "cheatsheet" | "full" }) to draft content in the user's voice — lazy, never injected into the prompt.
  3. Leaner coding-agent system prompt — dropped the redundant tool-listing sections (~320 tokens/turn, ~27% of the prompt). Tool selection verified at least as accurate via a live A/B (pi-agent-prompt-trim.test.ts).

Plus: mermaid subgraph label contrast fix, personality-picker selector stability fix, "None" persistence fix.

Type of change

  • Bug fix
  • New feature
  • Refactor / code quality
  • Docs / changelog
  • Tests

Screenshots / recording

No screenshots attached — happy to add on request. The UI surface: a personality chip in the chat input footer, a Personality section in Settings → AI & Chat, and a new Settings → Writing Style section with a wizard modal.

Checklist

  • npm run type-check:all passes
  • npm run lint passes
  • npm test passes (150 files / 2077 tests — live suites gated off by default)
  • npm run test:e2e passes (not run locally; recommend running before merge — touches chat input + settings UI)
  • If this ships a major, user-facing feature: added entries to scripts/features.config.js (v2.6.12-chat-personalities, v2.6.12-writing-style) — generated JSON regenerated
  • No hardcoded colours — CSS variables only (var(--accent), var(--text-primary), etc.)
  • No text-[Npx] pixel font classes — rem equivalents only
  • New IPC handlers wrapped in handle() and return IpcResult<T>
  • New DB migrations appended (not edited) in schema.ts (migration 41 — user_style)
  • New SQL goes in electron/db/queries.ts (via re-exported user-style-queries.ts)
  • New dependencies/devDependencies — none added (N/A)
  • New MCP tools registered in electron/mcp/tools/index.ts dispatch + electron/lib/tool-schemas.ts Zod schema (get_user_writing_style)
  • --external:<pkg> flag changes — none (N/A)

Notes for reviewer

  • Companion repo PR: the community personalities catalog (personalities.json) lives in ddutchie/cairn-community — the Browse modal lists entries only once that repo is merged/published.
  • get_user_writing_style is deliberately lazy (tool-only). It returns configured: false when no style is set up so agents never invent a voice.
  • The Writing Style wizard generation is non-streaming (single callLLM); streaming into the preview is a possible follow-up.
  • The chat personality system-prompt layer (withPersonality) appends a delimited ## Personality: {name} block — never a replacement identity, and community prompts are validated to reject "You are …" openings.

Summary by CodeRabbit

  • New Features
    • Added customizable chat personalities with community browsing, installation, selection, removal, and custom personality creation.
    • Added guided Writing Style setup with editable full guides and condensed cheatsheets.
    • Drafting tools can automatically use the configured writing style, with full-guide access when requested.
    • Added writing-style management, regeneration, preview, and clearing in Settings.
  • Bug Fixes
    • Improved Mermaid subgraph label contrast in dark mode.
    • Reduced redundant coding-agent prompt tool listings to streamline interactions.

LLM-generated diagrams often give subgraphs (clusters) an explicit light
pastel fill while mermaid's label colour follows the theme — light text in
dark mode becomes invisible on a light fill. After render, read each
cluster's actual fill luminance and pin the label to a contrasting
dark/light colour, in both the inline and fullscreen views.
A personality is a set of behavioral rules appended to the chat system
prompt as a delimited style layer (never a replacement identity). Pick one
next to the model selector in the chat input; Default adds nothing.

- personalities.json manifest fetch/cache/IPC (mirrors providers)
- Zod schema rejects 'You are …' openings + thin one-liner prompts
- withPersonality() injection in the main-process chat loop (covers the
  default prompt, the graph-view override, and token-usage recompute)
- aiConfig.installedPersonalities + personalityId (persisted via
  localStorage + config-cache whitelist), actions for install/remove/
  create/select
- PersonalityPicker in ChatInputArea, BrowsePersonalitiesModal, and a
  Chat personality section in Settings → AI & Chat
- changelog v2.6.12 + What's New entry; tests for schema, fetch, store,
  config-cache, and injection
…m prompt

The pi-agent prompt listed every tool under '## Coding tools' and
'## Cairn tools' (and '## Available tools' in plan mode), but the model
already receives each tool's full definition in the tools array — the
listing was pure overhead. Removed both sections; the base identity,
context, mandatory workflow, and coding guidelines are untouched.

- ~318 tokens saved per turn on the coding agent (~27% of the prompt)
- new pi-agent-prompt-trim.test.ts: offline assertions that the sections
  are gone + deterministic token savings; opt-in live A/B
  (CAIRN_LIVE_TESTS=1) that replays a tool-selection task set through
  runToolLoop with the legacy vs trimmed prompt and asserts the trimmed
  prompt selects the correct tools at least as often with fewer prompt
  tokens (verified live: 3/7 vs 2/7 correct, 4% fewer tokens)
…cess

Two related bugs when no personality is installed yet:

- useShallow selectors returned a fresh array from `?? []` inside the
  selector, breaking snapshot caching (React infinite-loop guard fired as
  'getSnapshot should be cached' / 'Maximum update depth exceeded'). Hoist
  the fallback out of the selector (same pattern as ProviderManager).
- PersonalityPicker read `installedPersonalities.find` on the possibly-
  undefined store field, throwing 'Cannot read properties of undefined'.
  All list access now goes through the null-safe personalityList.
…t, get_user_writing_style tool

A Writing Style system so AI-drafted content (emails, replies, notes, PRDs)
sounds like the user. Guided 5-step wizard generates a full style guide plus
a condensed cheat sheet; chat, the coding agent, and the MCP server can all
read them via get_user_writing_style (lazy — fetched only when drafting).

- DB: migration 41 user_style single-row table + queries (upsert/clear,
  persona JSON parsed defensively); re-exported from db/queries
- Tool: TOOL_SCHEMAS + labels + chat-executor case + MCP executor case;
  added to CAIRN_TOOL_NAMES + PLAN_MODE_ALLOWED for the coding agent;
  system-prompt hints in chat (buildSystemPrompt) and agent guidelines.
  Returns configured:false when unset — never invents a style.
- Wizard (Settings → Writing Style): persona → paste samples and/or
  analyse recent notes & tasks → gap questions → generate full guide →
  generate cheat sheet, both editable before saving (source guided/analyzed)
- Generation: user-style:generate IPC → callLLM with structured prompts
  (lib/user-style-prompt.ts); new 'writing-style' usage source
- Settings: Writing Style nav item + section with previews/regenerate/clear
- changelog v2.6.12 + What's New entry; tests: queries, executor (chat),
  MCP baseline, store slice
setPersonality(null) cleared personalityId in the store, but the backend
config cache kept the previous value (Electron structured-clone drops
undefined, so the cache's 'preserve when not a string' rule never saw the
clear). On the next launch the cache won the hydration merge and
re-selected the last personality — so 'None' could never be kept as default.

- 'None' is now stored as an explicit null (survives JSON + IPC), and
  config-cache treats an incoming null as CLEAR (absent = preserve, for
  connection-only saves)
- picker: trigger + row now read 'None — no personality' instead of the
  ambiguous 'Personality'/'Default'
- AISettings: explicit 'None' row with Active/Inactive states (replaces
  the misleading per-row 'Default' label)
- tests: store null-persistence + config-cache null-clear
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ddutchie, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 50fc73ab-eae4-4cf4-a144-c6bf138b8827

📥 Commits

Reviewing files that changed from the base of the PR and between 7c9b16e and 053d482.

📒 Files selected for processing (23)
  • electron/ipc/chat-executor.test.ts
  • electron/ipc/chat-executor.ts
  • electron/ipc/chat.ts
  • electron/ipc/user-style-handlers.ts
  • electron/lib/chat-loop.ts
  • electron/lib/compaction.ts
  • electron/lib/llm-stream.test.ts
  • electron/lib/llm-stream.ts
  • electron/lib/llm.ts
  • electron/lib/pi-agent-prompt-trim.test.ts
  • electron/lib/pi-agent-prompt.ts
  • electron/lib/tools.ts
  • electron/lib/user-style-generation.live.test.ts
  • electron/mcp/tools/index.ts
  • electron/preload.ts
  • shared/chat/registry-schema.ts
  • src/app/globals.css
  • src/components/notes/MermaidDiagram.tsx
  • src/components/settings/AISettings.tsx
  • src/components/settings/UserStyleSettings.tsx
  • src/components/settings/UserStyleWizardModal.tsx
  • src/components/ui/personality-picker.tsx
  • src/store/slices/ui.ts
📝 Walkthrough

Walkthrough

The change adds chat personalities with community browsing, installation, custom creation, and prompt application. It adds guided writing-style generation, persistence, editing, and retrieval tools. It also trims agent prompts and improves Mermaid subgraph label contrast.

Changes

Chat Personalities

Layer / File(s) Summary
Registry validation and persistence
shared/chat/registry-schema.ts, electron/lib/community-registry.ts, electron/lib/config-cache.ts, electron/preload.ts, electron/ipc/community-registry-handlers.ts, src/types/index.ts, electron/lib/*test.ts
Community personality manifests are validated, cached, fetched, and exposed through IPC. Installed personalities and the active personality ID persist in cached AI configuration.
Personality management UI and state
src/store/slices/ui.ts, src/store/slices/ui.test.ts, src/components/ui/personality-picker.tsx, src/components/chat/BrowsePersonalitiesModal.tsx, src/components/settings/AISettings.tsx, src/components/chat/ChatInputArea.tsx, src/components/chat/chat-panel/index.tsx, src/hooks/useChatStream.ts, scripts/features.config.js
The application supports community and custom personalities, selection, removal, browsing, installation, filtering, and settings management.
Personality application
electron/lib/tools.ts, electron/ipc/chat.ts, electron/lib/with-personality.test.ts
The selected personality is passed with chat requests and appended to the system prompt. Prompt token breakdowns use the same enhanced prompt.

Writing Style

Layer / File(s) Summary
Writing-style storage contract
electron/db/schema.ts, electron/db/user-style-queries.ts, electron/db/user-style-queries.test.ts, electron/db/queries.ts, electron/db/usage-queries.ts, src/types/index.ts
A singleton user_style record stores persona data, full and condensed guides, source metadata, and update time. Query helpers support normalized reads, upserts, field preservation, and clearing.
Generation and retrieval APIs
electron/lib/user-style-prompt.ts, electron/ipc/user-style-handlers.ts, electron/ipc/handlers.ts, electron/preload.ts, electron/lib/tool-schemas.ts, electron/lib/tools.ts, electron/ipc/chat-executor.ts, electron/ipc/chat-executor.test.ts, electron/mcp/tools/index.ts, electron/lib/pi-agent-loop.ts, scripts/features.config.js
The wizard can generate full guides and cheatsheets. Chat and MCP tools return the cheatsheet by default and the full guide when requested. Agents can access the writing-style tool.
Writing-style state and settings UI
src/store/index.ts, src/store/slices/user-style.ts, src/store/slices/user-style.test.ts, src/components/settings/settings-view.tsx, src/components/settings/UserStyleSettings.tsx, src/components/settings/UserStyleWizardModal.tsx
The store and settings screens support setup, editing, previewing, regeneration, saving, fetching, and clearing writing-style data.

Agent Prompt Optimization

Layer / File(s) Summary
Trimmed agent prompts
electron/lib/pi-agent-prompt.ts, electron/lib/pi-agent-prompt-trim.test.ts, changelogs/v2.6.12.md
Execute and plan prompts remove redundant tool listings while retaining skills and workflow content. Offline and optional live tests compare prompt size and tool-selection behavior.

Mermaid Label Contrast

Layer / File(s) Summary
Subgraph label contrast
src/components/notes/MermaidDiagram.tsx, src/components/notes/MermaidDiagram.test.ts
Rendered Mermaid subgraph labels use contrasting text based on cluster fill luminance in inline and fullscreen diagrams.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PersonalityPicker
  participant ChatPanel
  participant ElectronChat
  participant LLM
  User->>PersonalityPicker: Select personality
  PersonalityPicker->>ChatPanel: Update active personality
  ChatPanel->>ElectronChat: Send personality name and prompt
  ElectronChat->>LLM: Apply personality to system prompt
  LLM-->>User: Return chat response
Loading
sequenceDiagram
  participant User
  participant UserStyleWizardModal
  participant UserStyleIPC
  participant LLM
  participant ChatTool
  User->>UserStyleWizardModal: Submit persona and writing samples
  UserStyleWizardModal->>UserStyleIPC: Generate full guide
  UserStyleIPC->>LLM: Send style-generation prompt
  LLM-->>UserStyleIPC: Return Markdown guide
  UserStyleWizardModal->>UserStyleIPC: Generate condensed guide
  UserStyleIPC->>LLM: Send condensation prompt
  LLM-->>UserStyleIPC: Return Markdown cheatsheet
  ChatTool->>UserStyleIPC: Request stored writing style
  UserStyleIPC-->>ChatTool: Return cheatsheet or full guide
Loading

Possibly related PRs

  • ddutchie/cairn#92: Extends the community registry infrastructure used for personality fetching, validation, and caching.
  • ddutchie/cairn#82: Shares prompt and tool optimization work in the agent system-prompt infrastructure.
  • ddutchie/cairn#90: Shares chat request and assistant streaming integration points.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three primary changes: community personalities, Writing Style, and a leaner agent prompt.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ddutchie/uxtweaks

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (2)
electron/ipc/chat.ts (1)

163-163: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Hoist the composed system prompt into one constant.

Line 239 rebuilds the prompt on every usage round. buildSystemPrompt embeds new Date().toLocaleDateString(...), so the recomputed string can differ from the string sent at line 163 if a turn crosses midnight. calculatePromptBreakdown then skips the in-array system message and measures a different text than the one sent. Compute the prompt once and reuse it.

♻️ Proposed refactor
+    const systemContent = withPersonality(buildSystemPrompt(req), req.personality);
+
     const messages: OpenAIMessage[] = [
       {
         role: resolveSystemRole({ isReasoningModel: req.config?.isReasoningModel, baseUrl, provider, modelId: model }),
-        content: withPersonality(buildSystemPrompt(req), req.personality),
+        content: systemContent,
       },
       try {
-        const rawBreakdown = calculatePromptBreakdown(withPersonality(buildSystemPrompt(req), req.personality), messages, allTools);
+        const rawBreakdown = calculatePromptBreakdown(systemContent, messages, allTools);

Also applies to: 239-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/chat.ts` at line 163, In the request handling flow around the
system message construction, compute the composed prompt from
buildSystemPrompt(req) and withPersonality once in a shared constant, then reuse
that constant both for the message content and the later
calculatePromptBreakdown usage. Remove the second prompt reconstruction so both
paths measure and send the identical prompt.
src/components/notes/MermaidDiagram.test.ts (1)

4-35: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Add DOM-level coverage for the contrast correction.

These tests cover only parseColor and colorLuminance. They do not exercise enforceClusterLabelContrast, the inline render path at Line 177, or the modal render path at Line 123.

Add a DOM test with SVG and HTML cluster labels. Assert the applied CSS properties for dark, light, and medium fills. This will catch selector, cascade, and contrast regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/notes/MermaidDiagram.test.ts` around lines 4 - 35, Add
DOM-level tests covering enforceClusterLabelContrast through both the inline
render path and modal render path, using SVG and HTML cluster labels with dark,
light, and medium fills. Assert the resulting CSS properties on each label,
including the expected contrast correction, so selector and cascade behavior are
exercised rather than testing only parseColor and colorLuminance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/chat-executor.ts`:
- Around line 290-310: The writing-style handlers return only the requested
guide even when it is empty, despite the record being configured with the
alternate guide. Update the get_user_writing_style logic in
electron/ipc/chat-executor.ts (lines 290-310) and the corresponding MCP handler
in electron/mcp/tools/index.ts (lines 182-203) to fall back to fullGuide when
cheatsheet is empty and to cheatsheet when fullGuide is empty. Add coverage in
electron/ipc/chat-executor.test.ts (lines 1371-1388) for full-guide-only and
cheatsheet-only records, preserving the configured response metadata.

In `@electron/lib/pi-agent-prompt-trim.test.ts`:
- Around line 179-188: Update the onUsage callback passed to runToolLoop so
promptTokens accumulates usage across all tool-loop rounds by adding each pt
value instead of overwriting the previous total. Preserve the existing
toolErrors and called tracking behavior.
- Around line 212-213: Update electron/db/client.ts so Electron-specific
BetterSqlite3 binding resolution happens inside initDb rather than at module
load, and export a test-only createInMemoryDb() factory. Replace both direct new
BetterSqlite3(":memory:") constructions in the test with createInMemoryDb(),
using the factory for both database handles.

In `@electron/lib/pi-agent-prompt.ts`:
- Line 98: Update the Plan Mode prompt text around the ensure_note PRD
instructions to include the same get_user_writing_style requirement used by the
execute prompt, before drafting or updating the PRD. Preserve the existing plan
title and note-reuse guidance while ensuring the configured writing style is
applied to all Plan Mode PRD content.

In `@electron/lib/tools.ts`:
- Around line 191-197: Define one shared maximum-length constant for personality
prompts and enforce it both in the registry schema/custom-personality form
before persistence and in withPersonality before composing the system prompt.
Ensure overly long prompts are rejected or truncated consistently, and reuse the
same constant across all validation and runtime paths.

In `@src/components/notes/MermaidDiagram.tsx`:
- Line 102: Update the label color assignment in MermaidDiagram to use the
existing semantic CSS custom properties for the dark and light label colors
instead of hardcoded hex values, while preserving the luminance-based selection.
- Around line 100-102: Update the label-color selection in the Mermaid diagram
rendering logic around colorLuminance to compute WCAG contrast ratios between
the fill and both candidate colors, then choose whichever candidate has higher
contrast instead of using the fixed 0.5 luminance threshold. Add a medium-gray
test covering rgb(128, 128, 128), and adjust the candidate palette if the
implementation must guarantee a 4.5:1 contrast ratio.
- Around line 93-102: Update enforceClusterLabelContrast to handle both Mermaid
label variants: continue applying the computed contrast color via the SVG fill
attribute for .cluster-label text elements, and apply it via CSS color for
.cluster-label span HTML labels. Define the light and dark label color constants
before processing labels, and ensure both label types use the same
luminance-based selection.
- Around line 98-101: Update the rectangle color handling around attrFill, fill,
and colorLuminance so that when the attribute color fails to parse, retry
colorLuminance with getComputedStyle(rect).fill before continuing. Preserve the
existing skip behavior only when both the attribute and computed-style colors
are invalid, while retaining the current handling for missing or "none"
attributes.

In `@src/components/settings/AISettings.tsx`:
- Around line 307-358: Update the personality selectors in AISettings, including
the “None” row and each installed row rendered by the installedPersonalities
map, to use keyboard-operable button elements instead of clickable divs.
Preserve their selection behavior and styling, and restructure each installed
row so the remove control remains a sibling rather than a nested interactive
element; retain removePersonality and event propagation behavior.

In `@src/components/settings/UserStyleSettings.tsx`:
- Around line 46-65: Update regenerateCheatsheet to catch failures from
generateUserStyle and saveUserStyle, render an appropriate failure state, and
prevent the rejection from escaping the void click handler. Explicitly detect
when window.electron?.generateUserStyle is unavailable and report that as a
failure; retain setRegenerating(false) in the existing cleanup path.

In `@src/components/settings/UserStyleWizardModal.tsx`:
- Around line 305-328: Update the footer actions in UserStyleWizardModal to
render a Next button for steps 0 and 1, advancing via setStep when clicked.
Disable it while busy or when canProceed(step) is false, while preserving the
existing Cancel/Back behavior and generation actions for later steps.

In `@src/components/ui/personality-picker.tsx`:
- Around line 200-207: Update the remove button in the personality picker to
remain visually discoverable when keyboard-focused, while preserving the hover
visibility behavior; add a reliable accessible name to the button in addition to
the existing title, using the surrounding removePersonality control.

In `@src/store/slices/ui.ts`:
- Around line 789-819: Restrict name-based deduplication in
installCommunityPersonality to existing rows whose source is "community", both
in the initial existing lookup and the matchIdx lookup. Continue matching any
row by communityId, but prevent custom personalities with the same name from
being replaced.

---

Nitpick comments:
In `@electron/ipc/chat.ts`:
- Line 163: In the request handling flow around the system message construction,
compute the composed prompt from buildSystemPrompt(req) and withPersonality once
in a shared constant, then reuse that constant both for the message content and
the later calculatePromptBreakdown usage. Remove the second prompt
reconstruction so both paths measure and send the identical prompt.

In `@src/components/notes/MermaidDiagram.test.ts`:
- Around line 4-35: Add DOM-level tests covering enforceClusterLabelContrast
through both the inline render path and modal render path, using SVG and HTML
cluster labels with dark, light, and medium fills. Assert the resulting CSS
properties on each label, including the expected contrast correction, so
selector and cascade behavior are exercised rather than testing only parseColor
and colorLuminance.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 619e5b30-3d38-4cf3-b68f-2965ed5656a4

📥 Commits

Reviewing files that changed from the base of the PR and between 3b06511 and d920116.

📒 Files selected for processing (44)
  • changelogs/v2.6.12.md
  • electron/db/queries.ts
  • electron/db/schema.ts
  • electron/db/usage-queries.ts
  • electron/db/user-style-queries.test.ts
  • electron/db/user-style-queries.ts
  • electron/ipc/chat-executor.test.ts
  • electron/ipc/chat-executor.ts
  • electron/ipc/chat.ts
  • electron/ipc/community-registry-handlers.ts
  • electron/ipc/handlers.ts
  • electron/ipc/user-style-handlers.ts
  • electron/lib/community-registry.test.ts
  • electron/lib/community-registry.ts
  • electron/lib/config-cache.test.ts
  • electron/lib/config-cache.ts
  • electron/lib/pi-agent-loop.ts
  • electron/lib/pi-agent-prompt-trim.test.ts
  • electron/lib/pi-agent-prompt.ts
  • electron/lib/tool-schemas.ts
  • electron/lib/tools.ts
  • electron/lib/user-style-prompt.ts
  • electron/lib/with-personality.test.ts
  • electron/mcp/tools/index.ts
  • electron/preload.ts
  • scripts/features.config.js
  • shared/chat/registry-schema.ts
  • src/components/chat/BrowsePersonalitiesModal.tsx
  • src/components/chat/ChatInputArea.tsx
  • src/components/chat/chat-panel/index.tsx
  • src/components/notes/MermaidDiagram.test.ts
  • src/components/notes/MermaidDiagram.tsx
  • src/components/settings/AISettings.tsx
  • src/components/settings/UserStyleSettings.tsx
  • src/components/settings/UserStyleWizardModal.tsx
  • src/components/settings/settings-view.tsx
  • src/components/ui/personality-picker.tsx
  • src/hooks/useChatStream.ts
  • src/store/index.ts
  • src/store/slices/ui.test.ts
  • src/store/slices/ui.ts
  • src/store/slices/user-style.test.ts
  • src/store/slices/user-style.ts
  • src/types/index.ts

Comment thread electron/ipc/chat-executor.ts
Comment thread electron/lib/pi-agent-prompt-trim.test.ts
Comment thread electron/lib/pi-agent-prompt-trim.test.ts
Comment thread electron/lib/pi-agent-prompt.ts Outdated
Comment thread electron/lib/tools.ts
Comment thread src/components/settings/AISettings.tsx Outdated
Comment thread src/components/settings/UserStyleSettings.tsx
Comment thread src/components/settings/UserStyleWizardModal.tsx
Comment thread src/components/ui/personality-picker.tsx
Comment thread src/store/slices/ui.ts
…elect dropdown

- Steps 0-1 had no way to advance: only the Generate buttons existed in the
  footer. Added Next (step 0, gated on persona; step 1 always), and 'Skip for
  now' on the generation steps so the Edit flow can move on without
  regenerating a prefilled guide.
- Replaced the native <select> context picker with the app's styled Select
  component (shared dropdown styling + check gutter).
…utput

The guided generator could return 'token soup' — long scrambled fragments
spliced from the sample text (seen when a pasted doc / weak endpoint
overwhelms the model). No decode bug: callLLM's SSE parsing is the same
path the chat loop uses. Hardened generation:

- Bigger token budget (maxTokens 8192) and lower temperature (0.3) for the
  generation call (callLLM now accepts opts.temperature/maxTokens)
- Bounded raw material: pasted samples truncated to 2k chars each / 12k
  total so a giant document can't drown the prompt
- Coherence gate: output must contain >=6 (full) / >=3 (cheatsheet) ##
  headings; otherwise retry once at temp 0.1, and if still unusable throw
  a clear error naming the model instead of showing/saving garbage
- tests: guard thresholds + sample capping
…ed generate fn

The wizard hit 'unusable output' — the model (deepseek-v4-flash) is
non-deterministic and sometimes returns token soup. A live reproduction
confirmed it also produces a clean 12-section guide, so the failure was a
bad draw caught by the coherence gate, not a broken pipeline.

- extract generateUserStyleMarkdown() (prompt build + callLLM + retry +
  gate) so the IPC handler and the test drive identical code
- new gated live test (CAIRN_LIVE_TESTS=1): full guide AND cheat sheet
  must pass isUsableGuide against the configured endpoint — verified green
  against the ocgo proxy + deepseek-v4-flash
…ne-shots

Root cause of the Writing Style 'unusable output': on reasoning-model
gateways, LONG one-shot prose streamed back as token soup (verified: same
request clean non-streamed, garbled streamed — and adding tools/max_tokens
did not change it). Chat/agent loops were unaffected because their turns are
short and tool-oriented.

- callLLM gains stream:false support (reads the final message + usage) and
  the Writing Style generator uses it — proven clean end-to-end on
  deepseek-v4-flash via opencode.ai/zen/go (full guide 12 headings)
- callLLM's streaming path now consumes via consumeAssistantStream (the
  shared buffered SSE parser the chat/agent loops use) instead of the old
  inline split-per-chunk loop that could mangle SSE events split across TCP
  reads and ignored reasoning fields — fixing all one-shot tools
- dynamic import to avoid the llm <-> llm-stream module cycle
- tests: llm-stream/llm-sendable/llm-url/token-breakdown + user-style
  guards all green
…arkdown preview

Wizard now generates in real time instead of a one-shot spinner:
- user-style:generateStream runs runToolLoop with a READ-ONLY tool set
  (get_project_context_pack, search_notes, get_note, search_tasks,
  get_task) when 'analyse my notes' is on — the guide streams into the
  preview and each note-read shows as a live tool chip; no write tools,
  no get_active_context (IDs passed in the prompt). Verified live:
  toolCalls=[...search_notes, get_note...] → 12-heading guide.
- 'Optimize' button on the full-guide step restructures an existing guide
  into the canonical 12-section format (shared buildUserStylePromptPair so
  one-shot + streaming can't drift; optimize gated like full).
- Edit ↔ Preview toggle on the guide/cheat-sheet steps using the shared
  NoteMarkdownPreview (inline); Settings previews render markdown too.
- live test: streaming/tools path + one-shot full/cheatsheet all green on
  deepseek-v4-flash via zen/go.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/components/settings/UserStyleWizardModal.tsx (1)

461-465: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Apply canProceed(1) to the step 1 Next button.

canProceed(1) requires at least one sample, but the button ignores it. A user can advance with no samples and then generate a style guide from persona text only. If that path is intentional, delete the unused branch in canProceed to avoid a misleading contract.

🐛 Proposed fix
           {step === 1 && (
-            <Button size="sm" disabled={busy} onClick={() => setStep(2)}>
+            <Button size="sm" disabled={busy || !canProceed(1)} onClick={() => setStep(2)}>
               Next
             </Button>
           )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/settings/UserStyleWizardModal.tsx` around lines 461 - 465,
Update the step 1 Next Button in UserStyleWizardModal to disable when either
busy or canProceed(1) is false, preventing advancement without a sample;
preserve the existing click behavior.
🧹 Nitpick comments (2)
electron/ipc/user-style-handlers.ts (1)

115-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validate the precondition before you build the prompt.

Line 115 builds the prompt with input.fullGuide ?? "" for the cheatsheet and optimize steps. Line 117 then rejects a missing fullGuide. Move the check above the builder so the code never constructs a prompt from an empty source. The same ordering exists in the streaming path at lines 195 and 200.

♻️ Proposed reorder
-  const { systemPrompt, userPrompt } = buildUserStylePromptPair(step, input);
-
   if ((step === "cheatsheet" || step === "optimize") && !input.fullGuide) {
     throw new Error(step === "cheatsheet" ? "No full guide to condense." : "No full guide to optimize.");
   }
+  const { systemPrompt, userPrompt } = buildUserStylePromptPair(step, input);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/user-style-handlers.ts` around lines 115 - 119, Move the
missing-fullGuide validation above buildUserStylePromptPair in the non-streaming
handler, so cheatsheet and optimize steps reject before prompt construction.
Apply the same reorder in the streaming path: validate the precondition before
its buildUserStylePromptPair call while preserving the existing error messages
and other steps’ behavior.
electron/lib/user-style-generation.live.test.ts (1)

36-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import the shared tool set and prompt builder instead of copying them.

Lines 36-38 duplicate WRITING_STYLE_TOOLS, and lines 105-107 duplicate the full system prompt and the "Active context" hint. The handler already exports buildUserStylePromptPair. If the handler changes these strings, the live test still passes against stale text. Export WRITING_STYLE_TOOLS and writingStyleToolsOverride from electron/ipc/user-style-handlers.ts and reuse them here.

Also applies to: 105-113

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/lib/user-style-generation.live.test.ts` around lines 36 - 38,
Replace the duplicated WRITING_STYLE_TOOLS set and full prompt/“Active context”
text in the live test with imports from the user-style handler. Export and reuse
WRITING_STYLE_TOOLS, writingStyleToolsOverride, and the existing
buildUserStylePromptPair from user-style-handlers.ts so the test always reflects
the handler’s current configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/user-style-handlers.ts`:
- Around line 178-180: Update the stream request cleanup in the generation
handler’s finally block to delete the abortControllers entry only when it still
references that run’s AbortController, preserving a newer controller created for
the same event.sender.id. Apply the same ownership check in the user-style:abort
handler if it removes the map entry, so abort operations cannot clear a newer
generation’s controller.

In `@src/components/settings/UserStyleWizardModal.tsx`:
- Around line 452-454: Update the footer controls in UserStyleWizardModal so a
running streaming generation exposes a Cancel action instead of disabling the
only control. When the Cancel action is invoked, call
window.electron?.abortUserStyleStream?.() and reset busy, streaming, and the
listener array, while preserving the existing Cancel/Back behavior when
streaming is false.
- Around line 189-198: Stop passing credentials from the renderer in the
user-style generation flow: remove the config payload, including
aiConfig.apiKey, from the electron.generateUserStyleStream call near the
activeProject lookup, and remove the corresponding optional config field from
the user-style:generateStream request type. Keep credential resolution in the
main process via resolveChatConfig() and resolveLlmApiKey().

---

Duplicate comments:
In `@src/components/settings/UserStyleWizardModal.tsx`:
- Around line 461-465: Update the step 1 Next Button in UserStyleWizardModal to
disable when either busy or canProceed(1) is false, preventing advancement
without a sample; preserve the existing click behavior.

---

Nitpick comments:
In `@electron/ipc/user-style-handlers.ts`:
- Around line 115-119: Move the missing-fullGuide validation above
buildUserStylePromptPair in the non-streaming handler, so cheatsheet and
optimize steps reject before prompt construction. Apply the same reorder in the
streaming path: validate the precondition before its buildUserStylePromptPair
call while preserving the existing error messages and other steps’ behavior.

In `@electron/lib/user-style-generation.live.test.ts`:
- Around line 36-38: Replace the duplicated WRITING_STYLE_TOOLS set and full
prompt/“Active context” text in the live test with imports from the user-style
handler. Export and reuse WRITING_STYLE_TOOLS, writingStyleToolsOverride, and
the existing buildUserStylePromptPair from user-style-handlers.ts so the test
always reflects the handler’s current configuration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c4103bf-8a2e-4dfe-9cc4-65b9ecb0dfdd

📥 Commits

Reviewing files that changed from the base of the PR and between d920116 and 7c9b16e.

📒 Files selected for processing (8)
  • electron/ipc/user-style-handlers.test.ts
  • electron/ipc/user-style-handlers.ts
  • electron/lib/llm.ts
  • electron/lib/user-style-generation.live.test.ts
  • electron/lib/user-style-prompt.ts
  • electron/preload.ts
  • src/components/settings/UserStyleSettings.tsx
  • src/components/settings/UserStyleWizardModal.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • electron/preload.ts
  • src/components/settings/UserStyleSettings.tsx
  • electron/lib/user-style-prompt.ts

Comment thread electron/ipc/user-style-handlers.ts
Comment thread src/components/settings/UserStyleWizardModal.tsx
Comment thread src/components/settings/UserStyleWizardModal.tsx Outdated
…2x faster)

Measured on deepseek-v4-flash via zen/go: 'none' cuts wall time roughly in
half (full guide 38.7s→20.9s, 0 reasoning tokens) while still producing a
usable 12-heading guide; 'low' ≈ default here.

- callLLM: new opts.reasoningEffort → reasoning_effort in the body
- buildChatCompletionsBody: optional reasoningEffort, threaded from
  ChatRequest.config through runToolLoop
- writing-style one-shot + streaming generation both set 'none'
  (opt-in only — chat/agent keep the endpoint default)
- live suite green: full 18.5s, cheat 33.7s, streamed+tools 42.6s
…/422)

reasoning_effort is ignored by non-reasoning models but a strict
OpenAI-compatible server may reject the unknown field. Both call paths now
retry once without it on 400/422:

- callLLM: rebuilds the body without reasoning_effort and re-posts
- runToolLoop: doFetch + buildBody helpers, same retry for the streaming path
- test: buildChatCompletionsBody includes/omits reasoning_effort correctly

So the ~2x speedup from reasoning_effort:none stays for reasoning models,
and non-reasoning models / strict endpoints fall back cleanly.
Functional:
- get_user_writing_style falls back to the alternate guide when the
  requested one is empty (chat-executor + MCP + tests)
- chat.ts: compose the system prompt ONCE so the token-usage breakdown
  measures the exact text sent (avoids date drift across a midnight turn)
- plan-mode prompt now calls get_user_writing_style before drafting PRDs
- user-style handlers: validate missing fullGuide BEFORE building the
  prompt; abort-controller map entry is only cleared when still owned by
  the run (prevents clearing a newer generation's controller)
- pi-agent A/B test accumulates promptTokens across tool-loop rounds

Security:
- wizard no longer sends apiKey/baseUrl/model over IPC — main resolves via
  resolveChatConfig/resolveLlmApiKey

Accessibility / UX:
- AISettings personality rows are keyboard-operable buttons; remove is a
  sibling (no nested interactive)
- wizard: Cancel action during streaming (aborts + resets); step-1 Next
  gated on canProceed(1)
- personality-picker remove button focus-visible + aria-label

Data integrity:
- installCommunityPersonality name-dedup only matches community rows, so a
  custom personality with the same name is never replaced

Robustness:
- regenerateCheatsheet catches failures + reports when generation is
  unavailable
- shared MAX_PERSONALITY_PROMPT_CHARS enforced in the registry schema
  (zod .max), the create forms (maxLength) and withPersonality (truncate)
- Mermaid cluster labels use semantic --mermaid-label-* tokens, WCAG
  contrast-ratio selection, HTML .cluster-label span support, and a
  computed-style luminance fallback
- live test reuses the handler's writingStyleToolsOverride +
  buildUserStylePromptPair (no duplicated config)

Skipped (documented pattern / low value): createInMemoryDb() factory
(AGENTS.md exempts test code from the ABI constraint — tests construct
in-memory DBs directly) and Mermaid DOM-level tests (implementation now
covered; existing unit tests remain).
…+ reasoning_effort none

Audit of one-shot LLM callers showed compaction was the last surface still
using the naive decoder.decode(value).split('\n')-per-chunk SSE loop — the
same bug fixed in callLLM (mangled records straddling TCP reads, ignored
reasoning fields). Rewrote it to consume via consumeAssistantStream, added
reasoning_effort:none (direct transform, ~2x faster) with a 400/422
fallback that retries without it for non-reasoning models.

Status across one-shot tools:
- callLLM (robust parser + options): commit message, PR description,
  explain, PRD, Idea Flow summary, spawn-tasks, writing style
- compaction: NOW fixed (was the last buggy inline parser)
- tool-builder: already non-streaming (res.json) — no SSE bug; left as-is
  because it's a tool-call path where reasoning_effort:none could degrade
  the generated tool definition
- chat-subagent-loop: uses runToolLoop (robust)
@ddutchie

Copy link
Copy Markdown
Owner Author

@coderabbitai are we good to go?

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="ddutchie/cairn"
pr="123"

echo "== Pull request =="
gh pr view "$pr" --repo "$repo" --json state,isDraft,mergeStateStatus,reviewDecision,statusCheckRollup \
  --jq '{state, isDraft, mergeStateStatus, reviewDecision, checks: [.statusCheckRollup[]? | {name, status, conclusion, detailsUrl}]}'

echo
echo "== Reviews =="
gh api "repos/$repo/pulls/$pr/reviews" --paginate \
  --jq '.[] | {user: .user.login, state, submitted_at, body}'

echo
echo "== Open review threads =="
pr_id="$(gh api graphql -F owner="ddutchie" -F name="cairn" -F number="$pr" -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          isResolved
          comments(first: 10) {
            nodes { author { login } body path line originalLine }
          }
        }
      }
    }
  }
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | {author: .author.login, path, line, originalLine, body}')"
if [ -n "$pr_id" ]; then
  printf '%s\n' "$pr_id"
else
  echo "No open review threads."
fi

Length of output: 33067


@ddutchie Not yet.

All CI checks pass, including E2E tests. The PR is mergeable and not a draft.

One unresolved CodeRabbit finding remains in electron/lib/tools.ts at Line 204. Personality prompts have no shared maximum length. An oversized prompt can be stored and added to the system prompt.

Add and enforce a shared prompt-length limit, or resolve the finding with a valid rationale. After that, this PR is ready to go.

You are interacting with an AI system.

@ddutchie
ddutchie merged commit eb80b29 into main Aug 11, 2026
6 checks passed
@ddutchie
ddutchie deleted the ddutchie/uxtweaks branch August 11, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant