ui-spa

A set of rules for an embedded single-page chat interface, where the page updates without full browser reloads. It covers the thinking indicator, workspace and Git context controls, and rebuilding the embedded UI after changes.

In plain words
What is it for?
Use it when editing the chat UI’s thinking row, context chips, folder and branch controls, worktree selection, workspace state logic, or embedded-asset build process.
Why use it?
It keeps related interface elements positioned and connected consistently, while ensuring changed frontend assets are included in the Go application.

Cursor rule for Cursor

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add rules/coddy-project/coddy-agent/ui-spa
Clone the repo
git clone --depth 1 https://github.com/coddy-project/coddy-agent

Made for: Cursor.

Per session 0 Nothing until a file matches its globs; then the whole rule loads.
When invoked 2,430 The whole file, excluding the scripts and references it only reads on demand.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.00000 $0.02430
Opus 5 $0.00000 $0.01215
Sonnet 5 $0.00000 $0.00486
Haiku 4.5 $0.00000 $0.00243

Measured 2d ago against content hash d0839e5c6d87, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

ui-spa scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

.cursor/rules/ui-spa.mdc · 28 lines

How it starts

The opening of the file, as written. The whole thing — 28 lines — stays where its author put it; the contents beside it link to each section on GitHub.

Embedded UI (external/ui)

  • Rebuild go:embed assets after UI changes: make build TAGS="http ui" (runs ui-build).
  • Thinking disclosure row - duration must sit next to the thinking label, not at the trailing edge of the column. Markup keeps .thinking-dur inside .thinking-left in ThinkingMessage.tsx. Styles use .thinking-left { gap: 0 5px; } in styles.css. Do not drive label vs timer spacing with justify-content: space-between on summary.thinking-summary.
  • Workspace context chips (WorkspaceChips.tsx, first child of .composer-card) - folder / branch pills plus a worktree checkbox (real input[type=checkbox], composer-worktree-checkbox), backed by GET /coddy/workspace/context and POST /coddy/sessions/{id}/workspace. Keep the pure logic in chat/workspaceContext.ts and the MRU recents in chat/workspaceRecents.ts (do not inline them). The folder menu is Claude Desktop style: Recent rows (current marked ) + Open folder… opening the WorkspaceFolderModal.tsx filesystem browser (rows navigate, Open picks the browsed folder). The branch chip renders only when is_git_repo; the checkbox is checked+disabled when the session already runs in a linked worktree. Chosen once: with a non-empty transcript the chips lock (workspaceLocked) and the server returns 409. Menus reuse the mode-menu family: anchored portal on desktop (opens-down on hero, opens-up docked), bottom sheet on isMobileShell. Pre-session picks are pending in App.tsx (pendingWorkspaceRef) and applied on first send before POST /v1/responses. Contract: DESIGN.md (Composer workspace chips), docs/ui.md (Per-session workspace).
  • Composer context ring (indicator left of Send in Composer.tsx)
    • The ring itself is only a stroked arc (relative context fill). Do not place a numeric percent label inside or on top of it. Percent usage and token counts belong in the tooltip only.
    • Idle home (contextIdle when there is no sessionId): keep the arc at zero fill. Tooltip body is exactly: first line No context usage yet, second line Max context <n> (no session usage lines, no model name line).
    • With an active session (including hero with #/s/... in the hash): show fill from tokenUsage vs maxContextTokens. Tooltip lists percent line, optional Input/Output/Total line(s) when tokenUsage is present, then Max context line. Do not add a Model … line in this tooltip (Mode exposes agent / plan; Model is the YAML backend pill next to it).
    • Tooltip presentation: reuse the rail-tip styling (same family as narrow navbar hints). Anchor it above the ring, horizontally centered on it, with a comfortable width (use composer-context-tip-host / composer-context-tip rules in styles.css; avoid a cramped single-column tooltip).
    • Prefer no extra chrome on the meter (no bordered "button tile" behind the SVG); .context-ring styling stays minimal. Stroke colors come from --coddy-context-ring-inner / --coddy-context-ring-fg (defined per theme in styles.css).
  • Send/stop circle (Composer.tsx #btn-send) - composer-icon is a perfect circle (border-radius: 50%, square box). The shared composer-run-icon class is also used by scheduler run/stop buttons. Play uses ~22px ; stop uses .composer-stop-square (14x14px, centered). Ring + stop stay in composer-bar-actions on the right (DESIGN.md, Composer primary action).
  • Model selector menu (Composer.tsx, mode-menu--llm) - backend ids are vendor/model. Keep the filter/group/threshold logic in chat/llmModelMenu.ts (do not inline it): vendor headers (mode-menu-group-label) render only when more than one vendor is present (shouldGroupLlmModels); a filter input (mode-menu-filter, data-testid="model-menu-filter", auto-focused) renders only when the backend count exceeds LLM_MENU_FILTER_THRESHOLD (5) and matches vendor / model name / full id case-insensitively (filterLlmModels); rows scroll under a ~5-row cap (mode-menu-scroll, max-height: min(175px, 50vh) in styles.css). Rows show displayLlmId (model name only) with the full id in title; Enter picks the first match, Escape closes, empty result renders model-menu-empty.
  • Mobile menu sheet - on narrow shells (isMobileShell via shellBreakpoint.ts, max-width: 1199px) the Mode / Model / Reasoning portal menus render as a full-width bottom sheet (mode-menu--sheet, same family as the slash/at picker sheet) over a dimmed scrim (mode-menu-backdrop--scrim), not the anchored mode-menu--portal dropdown. Drive this from menuUseSheet = isMobileShell in Composer.tsx; the sheet overrides the desktop mode-menu--llm width cap and uses a 46vh scroll cap. Keep the anchor positioning only on the desktop portal branch.
  • Tool call timer - while an unresolved permission_prompt references a tool call id, ToolCallMessage freezes the thinking-dur label (permissionWaiting via permissionPendingToolCallIds).
  • Permission after reload - SSE rows persist in localStorage (permissionPromptSessionStore.ts); pending run_command / fs tools without a tool result also get a synthetic permission_prompt on GET .../messages merge (restorePermissionPrompts.ts). Stop glyph is .composer-send-glyph > .composer-stop-square (never both classes on one node).
  • Composer keyboard shortcuts - Enter and Ctrl+Enter send on desktop; Shift+Enter inserts a newline (browser default, not intercepted). On mobile/narrow viewports (max-width: 1199px shell stack breakpoint, isMobileShell via useSyncExternalStore over subscribeShellStack from shellBreakpoint.ts), Enter inserts a newline and send is button-only. The picker Enter handlers (slash/at menus) take precedence and run before the send logic on all viewports.
  • Multimodal model flag - GET /v1/models exposes multimodal: bool per entry from YAML models[].multimodal. App.tsx reads it into ModelInfo.multimodal, derives llmModelMultimodal (useMemo over current llmModel), and passes it through ChatScreenComposer as llmModelMultimodal?: boolean. Only render file attachment UI (file picker button, attachment previews) when llmModelMultimodal is true; keep the prop optional so the component degrades gracefully when models are not configured. After a successful PUT /coddy/config save, Settings fires onConfigSavedApp.tsx bumps modelsEpoch → re-fetches /v1/models so the attachment button appears without a page reload.
  • File attachment flow - Composer holds attachedFiles: File[] state; the hidden <input type="file"> ref (data-testid="composer-file-input") feeds setAttachedFiles. When sending, if files are present, onSend(text, files) passes them up; App.tsx reads each as a data URL (FileReader) and adds inline_files: [{name, data_url}] to the POST /v1/responses body. For agent / plan turns: the backend saves each file to ~/.coddy/sessions/<id>/assets/ with 0o444 permissions and injects a <coddy_session_assets> XML annotation into the user message content so the model can read or cp the files. The SPA strips this annotation from the display (and from copy-to-clipboard) via stripCoddyAttachmentsForUserDisplay in stripCoddyAttachments.ts and instead renders file chips (name + type icon) above the user bubble using msg-user-files / msg-user-file-chip CSS classes; parseSessionAssetFiles in the same file re-derives chip metadata from the XML on page reload so chips persist. For direct YAML model: each entry becomes an image_url content part sent inline to the provider. Multiple files in one request are supported; duplicate asset names are disambiguated with _1, _2 suffixes by SavePartsToAssets in internal/session/assets.go.
  • Mobile MQ helpers (shellBreakpoint.ts) - subscribeShellStack, snapshotShellStack, serverSnapshotShellStack are exported for useSyncExternalStore use anywhere in the SPA. Do not duplicate these in component-local functions.
  • Authoritative layout and tokens remain in the repo root DESIGN.md.

Read the full file on GitHub · 28 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 2d ago First seen · 28 lines · 0 tokens per session scan A d0839e5c6d87

Subscribe to this mod's changes

ui-spa is a cursor rule published in the GitHub repository coddy-project/coddy-agent (127 stars, last pushed 4d ago), licensed MIT. It costs nothing until one of its globs matches a file; then it loads 2,430 tokens. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.