@CLAUDE.local.md
This repository is a template with sensible defaults for building Tauri React apps.
If you need to check some codex app-server related things, use "codex app-server generate-json-schema --out ./codex-schema" to generate schema and check local dir ./codex-schema for schemas.
- Read @docs/tasks.md for task management
- Review
docs/developer/architecture-guide.mdfor high-level patterns - Check
docs/developer/for system-specific patterns (command-system.md, performance-patterns.md, etc.) - Check git status and project structure
CRITICAL: Follow these strictly:
- Read Before Editing: Always read files first to understand context
- Follow Established Patterns: Use patterns from this file and
docs/developer - Senior Architect Mindset: Consider performance, maintainability, testability
- Batch Operations: Use multiple tool calls in single responses
- Match Code Style: Follow existing formatting and patterns
- Test Coverage: Write comprehensive tests for business logic
- Quality Gates: Run
bun run check:allafter significant changes - Dev Server: You may start the dev server (
bun run tauri dev) when needed - No Unsolicited Commits: Only when explicitly requested
- Documentation: Update relevant
docs/developer/files for new patterns - Removing files: Always use
rm -f
CRITICAL: Use Tauri v2 docs only. Always use modern Rust formatting: format!("{variable}")
useState (component) → Zustand (global UI) → TanStack Query (persistent data)
Decision: Is data needed across components? → Does it persist between sessions?
// ✅ GOOD: Use getState() to avoid render cascades
const handleAction = useCallback(() => {
const { data, setData } = useStore.getState()
setData(newData)
}, []) // Empty deps = stable
// ❌ BAD: Store subscriptions cause cascades
const { data, setData } = useStore()
const handleAction = useCallback(() => {
setData(newData)
}, [data, setData]) // Re-creates constantly- Rust → React:
app.emit("event-name", data)→listen("event-name", handler) - React → Rust:
invoke("command_name", args)with TanStack Query - Commands: All actions flow through centralized command system
- Context7 First: Always use Context7 for framework docs before WebSearch
- Version Requirements: Tauri v2.x, shadcn/ui v4.x, Tailwind v4.x, React 19.x, Zustand v5.x, Vite v7.x, Vitest v4.x
Document discoveries here. When encountering major/minor findings during development, ask the user if they should be saved to this file for future reference.
CRITICAL: There are two patterns for Rust-TypeScript serialization. Pick ONE per struct and be consistent.
Pattern A: snake_case (for persisted/settings data)
- Used for:
AppPreferences,UIState, and other persisted data - Rust structs use snake_case by default (e.g.,
active_worktree_id) - TypeScript interfaces must match exactly (e.g.,
active_worktree_id, NOTactiveWorktreeId) - See
src/types/preferences.tsandsrc/types/ui-state.tsfor examples
Pattern B: camelCase with #[serde(rename_all = "camelCase")] (for API/command data)
- Used for: Data passed between frontend and Tauri commands (e.g.,
IssueContext,PullRequestContext) - Add
#[serde(rename_all = "camelCase")]to Rust struct - TypeScript uses standard camelCase (e.g.,
headRefName,baseRefName) - See
src-tauri/src/projects/github_issues.rsfor examples
Common error: invalid args for command: missing field 'field_name'
- This means Rust expects snake_case but frontend sent camelCase (or vice versa)
- Fix: Add
#[serde(rename_all = "camelCase")]to the Rust struct, OR change TypeScript to snake_case
Session-specific UI state (e.g., answered questions, fixed review findings) must be persisted via the existing Tauri backend system, not Zustand middleware:
- Add fields to
src/types/ui-state.ts(TypeScript interface, usesnake_case) - Add fields to
src-tauri/src/lib.rs(RustUIStatestruct with#[serde(default)]) - Update
src/hooks/useUIStatePersistence.ts:- Extract state in
getCurrentUIState()(map camelCase store → snake_case UIState, convert Sets to arrays) - Restore state in initialization effect (map snake_case UIState → camelCase store, convert arrays back to Sets)
- Track changes in subscription effect to trigger saves
- Extract state in
Key insight: The hasFollowUpMessage check in ChatWindow.tsx (checks if a user message follows an assistant message) is meant as a fallback but may have timing issues with TanStack Query. Persisting state directly provides reliable rendering.
CRITICAL: Never subscribe to a getter function and call it directly in JSX. This creates NO subscription to the underlying data.
// BAD: Subscribes to function reference (stable), NOT to viewingLogsTab data
const isViewingLogs = useChatStore(state => state.isViewingLogs)
return isViewingLogs(worktreeId) ? <LogsView /> : <ChatView />
// viewingLogsTab changes will NOT trigger re-render!
// GOOD: Subscribes to actual data - triggers re-render when data changes
const isViewingLogsTab = useChatStore(state =>
state.activeWorktreeId ? state.viewingLogsTab[state.activeWorktreeId] ?? false : false
)
return isViewingLogsTab ? <LogsView /> : <ChatView />When getter functions ARE okay:
- Passing to memoized children as props (children handle their own rendering)
- Using inside
useMemowith proper data dependencies - Using inside callbacks obtained via
getState()
The bug: Zustand selectors subscribe to whatever the selector returns. If you return a function, you subscribe to that function reference (which never changes), not the data the function reads internally.
CRITICAL: Every Zustand set() call notifies ALL subscribers, even if the value didn't change. useShallow only prevents re-renders if the selected field references are identical. Store mutations that spread new objects ({...state.field, [id]: value}) without checking whether the value actually changed will cause unnecessary re-renders across every component subscribing to that store.
// ✅ GOOD: Guard against no-op updates
addSendingSession: sessionId =>
set(state => {
if (state.sendingSessionIds[sessionId]) return state // No new ref
return {
sendingSessionIds: { ...state.sendingSessionIds, [sessionId]: true },
}
})
// ❌ BAD: Always creates new object reference, triggers all subscribers
addSendingSession: sessionId =>
set(state => ({
sendingSessionIds: { ...state.sendingSessionIds, [sessionId]: true },
}))Guard patterns by type:
- Boolean Records:
if (state.field[id]) return state/if (!(id in state.field)) return state - Value Records:
if (state.field[id] === value) return state - Array fields:
if (!existing || existing.output === output) return state - Set fields:
if (existingSet.has(value)) return state
To diagnose unnecessary re-renders, temporarily install why-did-you-render:
bun add -d @welldone-software/why-did-you-render- Create
src/wdyr.ts:if (import.meta.env.DEV) { const whyDidYouRender = ( await import('@welldone-software/why-did-you-render') ).default whyDidYouRender(React, { trackAllPureComponents: false }) }
- Import at top of
src/main.tsx:import './wdyr' - Annotate suspect components:
MyComponent.whyDidYouRender = true - Check browser console for "Re-rendered because of hook changes" / "different objects that are equal by value"
- Before releasing: Remove all annotations,
wdyr.ts, its import, andbun remove @welldone-software/why-did-you-render
Read this before changing PI chat parsing (src-tauri/src/chat/pi.rs).
Interactive Jean chat runs PI through Jean's detached PI RPC host on Unix/macOS:
Jean launches itself with --jean-pi-rpc-host; that host owns a pi --mode rpc
child, keeps stdin open after Jean quits, writes PI stdout JSONL into Jean's
run log, and accepts local socket commands for prompt, steer, and abort.
The socket lives under Jean's app-data directory with a short filename to stay
within macOS Unix socket path limits. This is required because PI RPC exits when
stdin closes. Windows/non-detached fallbacks may still use direct child
execution and are non-survivable.
Jean persists its own session/run JSONL (run logs) — PI's event/session format is only used to parse PI output before writing Jean history. References: https://pi.dev/docs/latest/rpc and https://pi.dev/docs/latest/session-format
- Streaming assistant text arrives as
message_update/assistantevents. Deltas are read fromassistantMessageEvent.delta(typetext_delta), or a top-leveldelta/textfield. Thinking deltas usedelta.type == "thinking_delta". - Final assistant content (text, thinking, tool calls), tool results, and
usage are nested under
type: "message"lifecycle entries —message.roleisassistant(content blocks:text,thinking,toolCall) ortoolResult. - Tool lifecycle also emitted as discrete
tool_execution_start/tool_callandtool_execution_end/tool_resultevents. - Session id comes from
session_id/sessionId, or atype: "session"entry'sid. This becomes Jean'spi_session_idresume id. - Usage is read from
usage/token_usageonmessage,message_end,turn_end,agent_end, orresultevents. - Steering uses the RPC
steercommand throughsteer_pi_turn. Only plain queued prompts (no files/images/skills/text attachments) are steerable. - Background recovery mirrors Codex's survivable app-server pattern:
host-backed PI runs store the host PID on the run, are treated as detached
survivable sessions, and
resume_sessiontails the run JSONL with the PI parser after Jean restarts. The host appends a synthetictype: "result"marker afteragent_endso completed-while-closed runs recover as completed.
Parsing is incremental: merge_pi_line() merges one parsed line into the
accumulating PiResponse. Both the batch parser (parse_pi_json_stream_inner)
and the live streaming parser (parse_pi_stream) call it per line — never
re-parse the whole accumulated buffer (avoids O(n²) on long sessions).
CRITICAL: When using --json-schema with Claude CLI, structured output is returned via a tool call, not plain text.
- Claude uses a synthetic
StructuredOutputtool to return JSON schema responses - The data is in
message.content[].inputwherecontent[].name == "StructuredOutput" - Regular text blocks may still appear before the tool call (e.g., "I'll create...")
- The
resultfield does NOT contain the structured data
Stream-JSON output structure:
{
"type": "assistant",
"message": {
"content": [
{ "type": "text", "text": "I'll create a structured summary..." },
{
"type": "tool_use",
"id": "toolu_xxx",
"name": "StructuredOutput",
"input": { "slug": "my-slug", "summary": "..." }
}
]
}
}Extraction pattern (see src-tauri/src/chat/commands.rs:extract_text_from_stream_json):
for block in content {
if block.get("type") == Some("tool_use")
&& block.get("name") == Some("StructuredOutput") {
return block.get("input").clone(); // This is your JSON schema data
}
}Usage in this codebase:
- Context summarization:
execute_summarization_claude()uses--json-schemato get{summary, slug}- Schema constant:
CONTEXT_SUMMARY_SCHEMAinsrc-tauri/src/chat/commands.rs
- Schema constant:
- PR content generation:
generate_pr_content()uses--json-schemato get{title, body}- Schema constant:
PR_CONTENT_SCHEMAinsrc-tauri/src/projects/commands.rs - Tauri command:
create_pr_with_ai_content- creates PR with AI-generated title/body
- Schema constant:
Pattern: For operations that run in the background (not in chat), use toast notifications instead of inline UI state indicators.
// ✅ GOOD: Toast-based feedback for background operations
const handleBackgroundOperation = useCallback(async () => {
const toastId = toast.loading('Operation in progress...')
try {
const result = await invoke<ResultType>('backend_command', { args })
// Invalidate relevant queries to refresh UI
queryClient.invalidateQueries({ queryKey: ['relevant-query'] })
toast.success(`Success: ${result.message}`, { id: toastId })
} catch (error) {
toast.error(`Failed: ${error}`, { id: toastId })
}
}, [queryClient])
// ❌ BAD: Zustand state for loading indicators
const [isLoading, setIsLoading] = useState(false)
// ... requires passing state through props, tracking lifecycle, etc.Key points:
- Use
toast.loading()at start, update withtoast.success/error()using sameid - For opening URLs in Tauri, use
openUrlfrom@tauri-apps/plugin-opener(notwindow.open) - Close modals immediately after dispatching action (don't wait for completion)
- Invalidate TanStack Query caches after mutations to refresh UI
Current background operations using this pattern:
handleSaveContextinChatWindow.tsx- saves context with AI summarizationhandleOpenPrinChatWindow.tsx- creates PR with AI-generated title/bodyhandleCommitinChatWindow.tsx- creates commit with AI-generated message (usescreate_commit_with_aicommand with JSON schema)handleReviewinChatWindow.tsx- runs AI code review, stores results in Zustand/UI state, shows in ReviewResultsPanel (usesrun_review_with_aicommand with JSON schema)
Toast action buttons:
toast.success('PR created', {
id: toastId,
action: {
label: 'Open',
onClick: () => openUrl(result.url), // Use Tauri plugin, not window.open
},
})CRITICAL: On Windows, every std::process::Command::new() call opens a visible console window that briefly flashes on screen unless CREATE_NO_WINDOW (0x08000000) is set via creation_flags().
Use silent_command() for all background operations:
use crate::platform::silent_command;
// ✅ GOOD: No console window flash
let output = silent_command("git")
.args(["status", "--porcelain"])
.current_dir(repo_path)
.output()?;
// ❌ BAD: Flashes a console window on Windows
let output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(repo_path)
.output()?;Keep Command::new() ONLY for commands that intentionally open UI:
- File managers:
open,explorer,xdg-open - Terminals:
wt,powershell,cmd, terminal emulators - Editors:
code,cursor,xed - macOS automation:
osascript
For detached processes that need both CREATE_NO_WINDOW and CREATE_NEW_PROCESS_GROUP: use silent_command() but re-set both flags via creation_flags() (it replaces, doesn't merge).
For opening URLs in the browser: use open_url_in_browser() (not raw cmd /c start). On Windows the shell-association path still uses cmd /c start, but only through silent_command() so the intermediary console never flashes (issue #588).
The helpers are defined in jean-core/src/platform/process.rs and re-exported via pub use process::* in platform/mod.rs (open_url_in_browser is also re-exported from jean_core).
"Canvas" refers to ProjectCanvasView (src/components/dashboard/ProjectCanvasView.tsx):
- Project-level canvas showing worktrees as compact list rows (with section headers)
- Sessions are opened via
SessionChatModaloverlay - Navigation: clicking "back" from ChatWindow returns to ProjectCanvasView via
clearActiveWorktree() - Clicking a worktree in the sidebar stays on ProjectCanvasView (does not open ChatWindow)
Shared Hooks (in src/components/chat/hooks/):
useCanvasKeyboardNav.ts- Arrow key navigation (up/down), Enter selectionuseCanvasShortcutEvents.ts- Event handlers foropen-plan,open-recap,approve-plan, etc.useCanvasStoreState.ts- Subscribes to chat store state needed forSessionCardData
Shared Components:
SessionListRow.tsx- Compact row component for list viewsession-card-utils.tsx-computeSessionCardData(),SessionCardData, andSessionCardPropstypes
Keyboard-only affordances are native-desktop only by default:
- Hide
<Kbd>shortcut hints in web access and mobile views unless the shortcut is explicitly useful there. - Disable matching keyboard-only default actions in web access/mobile (examples: toast default action
Alt+Enter, unread session mark-readR). - Keep the click/tap action available; only gate the desktop keyboard hint/handler.
- Use
isNativeApp()plususeIsMobile()/viewport width for gating, and add tests for native desktop, web access, and mobile.
Images pasted or dropped into chat are processed before saving (process_image() in src-tauri/src/chat/commands.rs):
- Resize: Max 1568px on longest side (Claude's internal limit — anything larger gets downscaled by Claude anyway, wasting bandwidth). Images below 200px on any edge may degrade Claude's vision performance.
- Compress: Opaque PNGs → JPEG at 85% quality (typically 5-10x smaller). PNGs with transparency stay PNG.
- Skip: GIFs (may be animated), images < 50KB, already-compressed formats (JPEG/WebP below 1568px).
- Performance: Uses
Triangle(bilinear) filter for resize, runs inspawn_blockingto avoid blocking async runtime. - Token cost:
(width × height) / 750tokens per image. Max optimal size (~1568×1568) ≈ 3,280 tokens. - Reference: https://platform.claude.com/docs/en/build-with-claude/vision
Three files need updating when adding a new model option:
src/types/preferences.ts— Add toClaudeModeltype union andmodelOptionsarray (full labels like "Claude Fable 5" or "Claude Sonnet 4.6"). Current first-party Claude Code model IDs use API-style names such asclaude-fable-5,claude-opus-4-8[1m], andclaude-sonnet-4-6[1m]; legacy/provider aliases may still useopus,sonnet, orhaiku.src/store/chat-store.ts— Add to duplicatedClaudeModeltype union (line ~27)src/components/chat/ChatToolbar.tsx— Add toMODEL_OPTIONSarray (short labels like "Sonnet 4.6")
No Rust changes needed — model is stored as String in AppPreferences and passed directly to --model CLI flag.
When adding a backend like Claude, Codex, OpenCode, Cursor, Pi, Command Code, or a future CLI/API backend, verify the integration is complete across Rust, TypeScript, UI, persistence, and web access.
Backend capability classification:
- Classify transport shape: persistent server/API, streaming CLI, final-output CLI, or non-chat helper
- Document capability flags: streaming, structured tool calls, resume/session id, cancellation, interactive approvals, MCP, model listing, usage, images/files
- Define MVP fallback behavior for unsupported capabilities before wiring UI affordances
- If backend is final-output-only, emit one synthetic final
chat:chunkand persist Jean-managed transcript/context - If backend has no resume/session id, do not fake resume support; store Jean session id only
- If backend has no structured tool events, skip tool-call UI or synthesize only safe high-level placeholders
Backend identity and preferences:
- Add backend enum/type in Rust (
src-tauri/src/chat/types.rs) and TypeScript (src/types/chat.ts,src/types/preferences.ts) - Add backend label/icon/model options (
src/components/ui/backend-label.tsx,src/components/icons/,ChatToolbar.tsx) - Add persisted preferences for default backend, selected model, reasoning/effort, source (
jeanvspath) when applicable - Add project-level
default_backendsupport and build/yolo backend/model/effort overrides - Keep persisted preference fields in
snake_caseand provide serde/default migration safety
Install, status, auth, and login:
- Add CLI module (
src-tauri/src/<backend>_cli/) with config/status/auth/install/update commands as needed - Avoid binary-name ambiguity; support canonical binary and documented aliases when applicable (e.g.
cmd,command-code) - For npm-distributed CLIs, decide PATH-only vs Jean-managed npm install/update/uninstall before adding Settings controls
- Detect whether backend is installed before checking auth
- Add auth status command and frontend hook/types (e.g.
check_<backend>_auth,use<Backend>CliAuth,src/types/<backend>-cli.ts) - Auth result distinguishes installed+authenticated, installed+unauthenticated, not installed, command failed, and unknown/error
- Add login/relogin commands if the backend supports them
- If login requires terminal/browser, open the terminal login command, auth URL, or backend-native login flow
- Add Settings → General login/relogin buttons with loading state and toast feedback
- Re-fetch auth status after login/relogin
- Include backend auth readiness in onboarding; do not mark backend ready unless installed and authenticated
- Support both Jean-managed binary and system PATH binary login flows
- Register every status/auth/login command in both
src-tauri/src/lib.rsandsrc-tauri/src/http_server/dispatch.rs - Add tests/mocks for authenticated, unauthenticated, not installed, login failure, and
jeanvspathsource
Main chat execution:
- Create execution module (
src-tauri/src/chat/<backend>.rs) and export it fromchat/mod.rs - Return the common response shape: content, backend resume id, tool calls, content blocks, cancelled flag, usage, and error state if needed
- Route backend in
src-tauri/src/chat/commands.rssend-message match - Store backend resume id on Rust/TS
Sessionand persist/restore it - Map backend streaming to common events:
chat:chunk,chat:tool-use,chat:tool-result,chat:tool-block,chat:thinking,chat:done,chat:error,chat:cancelled - Preserve ordered
ContentBlock[], normalize tool call IDs/names, and attach outputs to matching tool calls - Add cancellation support in
src-tauri/src/chat/registry.rs(process kill, interrupt request, or cancel flag) - Update run logs, incomplete-run recovery, synthetic plan injection, and session resume behavior
System prompts, modes, and context:
- Assemble prompts with project custom prompt, global system prompt, execution-mode instruction,
RECAP_INSTRUCTION, language preference, and parallel-execution prompt when enabled - Use the backend-native system prompt mechanism when available; otherwise use a safe fallback
- Map Jean execution modes (
plan,build,yolo) to backend-native sandbox/approval controls - Update
getSupportedExecutionModes,isExecutionModeSupported, andnormalizeExecutionModeForBackend - Support backend-native plan tool/approval flow or synthesize a Jean-compatible plan tool call
- Apply build/yolo backend/model/effort overrides when approving plans
- Merge all relevant context: project/worktree, GitHub issue/PR/security/advisory, Linear issue, saved context files, linked projects, attached files/images, and denied-message re-send context
- Embed context directly when the backend cannot access external files/APIs
Permissions, magic prompts, providers, MCP, and UI:
- Add permission/user-input approval structs, events, UI, persistence, and approve/deny commands if backend supports them
- Add one-shot execution support for all magic prompt operations (session naming, context summary, PR content, commit message, code review, resolve conflicts, release notes, investigations, review comments)
- Add robust structured JSON extraction for one-shot outputs; define the backend-specific strategy (native JSON schema/tool call, strict JSON text, repair pass, or unsupported-with-UI-disable)
- For non-JSON-capable backends, wrap prompts with strict JSON instructions and implement tolerant extraction/failure messages
- Update Magic Prompts UI backend/model/default presets and per-prompt backend/provider/model/effort resolution
- Add provider/profile support if backend supports custom routing; respect project/global/per-prompt provider precedence
- Add MCP discovery/health/toggle support if backend supports MCP, including settings-pane grouping, chat status dots, and backend-specific auth hints
- Update frontend chat/settings/onboarding/usage UI and backend-specific pending request components
- Add backend to favorite models and fast-mode model handling if relevant
- Update toolbar/model picker behavior for backend tabs, locked sessions, search scoping, keyboard shortcuts, favorites, and fast-mode controls
Web access, tests, and docs:
- Register every new
#[tauri::command]in both nativegenerate_handler![]and WebSocket dispatch - Use dispatch helpers (
field,field_opt,from_field,from_field_opt,to_value) and emit cache invalidation for mutations - Use
silent_command()for background processes to avoid Windows console flashes - Add Rust tests for parsing, default backend resolution, cancellation, one-shot JSON extraction, and auth/login
- Add TS/component/E2E tests for preferences, auth/login UI, execution mode normalization, backend selection, plan approval, cancellation, and magic prompt overrides
- Update developer docs, user docs, troubleshooting, and all comments that list supported backends
Projects have an optional worktrees_dir: Option<String> field that overrides the default ~/jean base directory for worktree creation.
- Rust:
Project.worktrees_dirinsrc-tauri/src/projects/types.rs - TypeScript:
Project.worktrees_dirinsrc/types/projects.ts - Path resolution:
get_project_worktrees_dir(name, custom_base_dir)insrc-tauri/src/projects/storage.rs- When
Some(dir)→<dir>/<project-name>/<worktree-name> - When
None→~/jean/<project-name>/<worktree-name> - The
<project-name>subdirectory is always appended to prevent collisions when multiple projects share the same custom base dir
- When
- UI: "Worktrees Location" section in
src/components/projects/panes/GeneralPane.tsx(Browse + Save + Reset) - Saved via:
update_project_settingsTauri command,worktrees_dir: Option<Option<String>>param (outer Option = not updating, inner Option = clear/set)
CRITICAL: Every new #[tauri::command] must ALSO be registered in the WebSocket dispatch handler, or it will only work in the native app and fail with "Unknown command" in web access.
Two places to register every command:
src-tauri/src/lib.rs—tauri::generate_handlersrc-tauri/src/http_server/dispatch.rs—dispatch_command()match arms (WebSocket transport)
Dispatch pattern:
// In dispatch.rs — match arm inside dispatch_command()
"my_new_command" => {
// Use field() for camelCase/snake_case dual-key extraction
let worktree_id: String = field(&args, "worktreeId", "worktree_id")?;
// Use from_field() for single-key extraction
let name: String = from_field(&args, "name")?;
// Use from_field_opt() / field_opt() for Option<T>
let model: Option<String> = from_field_opt(&args, "model")?;
let result = crate::my_module::my_new_command(app.clone(), worktree_id, name, model).await?;
to_value(result) // or Ok(Value::Null) for () return
}Helper functions in dispatch.rs:
to_value(result)— Serialize return value toserde_json::Valuefrom_field(&args, "key")— Extract required field (single key)from_field_opt(&args, "key")— Extract optional field (single key)field(&args, "camelKey", "snake_key")— Extract required field (tries camelCase first, then snake_case)field_opt(&args, "camelKey", "snake_key")— Extract optional field (dual-key)emit_cache_invalidation(app, &["sessions", "session"])— Broadcast cache invalidation to all clients
Module path rules (use re-exports, NOT private ::commands:: paths):
crate::chat::— chat commands (re-exported viapub use commands::*)crate::codex_cli::— codex CLI commands (re-exported viapub use commands::*)crate::projects::— project/git/github/linear commands (re-exported viapub use commands::*,pub use github_issues::*, etc.)crate::background_tasks::commands::— background task commands (public module, direct access)crate::— top-level commands defined inlib.rs(e.g.,save_cli_profile)
For State<'_, T> params (e.g., BackgroundTaskManager): extract via app.state::<T>():
let state = app.state::<crate::background_tasks::BackgroundTaskManager>();
crate::background_tasks::commands::my_command(state, arg)?;Checklist for new commands:
- Add
#[tauri::command]function - Register in
lib.rsgenerate_handler![] - Add match arm in
dispatch.rsdispatch_command() - Add
emit_cache_invalidation()if the command mutates data that other clients should refresh