| name | claudette-debug |
| description | Debug the running Claudette Tauri app by executing JavaScript in the webview and reading results back. Inspect Zustand store state, trace state changes, monitor sessions long-term, run end-to-end UAT, and diagnose UI bugs in real-time. Only works in dev builds. |
| when_to_use | Use when the user asks to inspect UI state, debug the webview, check what's on screen, monitor agent activity, take a screenshot of the app, or diagnose rendering/layout issues. Also use proactively after UI changes to verify they render correctly in the running app. |
| argument-hint | [action] [args...] |
| allowed-tools | Bash Read Grep Glob |
Claudette Debug
Execute JavaScript inside the running Claudette Tauri webview via a TCP debug server on 127.0.0.1. Dev-build only (#[cfg(debug_assertions)]).
The server listens on port 19432 by default, but the devshell dev helper auto-selects a free port (and writes a discovery file) so multiple dev instances can run in parallel against different branches. debug-eval.sh discovers the right instance automatically — you do not need to know the port. Details in Port discovery below.
Quick Start
/claudette-debug status
/claudette-debug discover actions
/claudette-debug discover state
/claudette-debug send "read README.md"
/claudette-debug wait
/claudette-debug screenshot
/claudette-debug state
/claudette-debug eval 'return 1+1'
/claudette-debug monitor start
Prerequisites
- App running via the devshell
dev helper (or cargo tauri dev) — debug TCP server starts automatically
python3 in PATH (used by eval helper)
Do NOT launch the installed app. Never run osascript -e 'tell application "Claudette" to activate', open -a Claudette, or double-click /Applications/Claudette.app. The debug TCP server only exists in dev builds (gated by #[cfg(debug_assertions)]); the installed release build has no debug server and eval calls against it will fail or silently target the wrong process. If the dev build is not already running, ask the user to start dev — do not start it yourself and do not fall back to the installed app.
Windows
The skill works on Windows via Git Bash (the bash shipped with Git for Windows / MINGW64). Invoke the same scripts/*.sh scripts; they detect MSYS via uname -s and dispatch to PowerShell-only paths where needed (e.g. debug-screenshot.sh → debug-screenshot.ps1 for System.Drawing-based captures).
The dev entry point is scripts/dev.ps1 (or the dev PowerShell profile function it ships with). It mirrors dev.sh's discovery file at $env:TEMP\claudette-dev\<pid>.json, which Git Bash sees as /tmp/claudette-dev/*.json — so debug-eval.sh's auto-discovery just works. dev.ps1 runs the Tauri binary with --no-default-features --features devtools,server,alternative-backends (no voice, no tauri/custom-protocol) and binds Vite to 127.0.0.1 rather than ::1 — both are required to keep WebView2 happy and __CLAUDETTE_INVOKE__ set; if you change either, the debug eval server will hang.
Do NOT launch the MSI/NSIS-installed Claudette release for the same reason as macOS: no debug server, so eval calls fail or silently target a different process. If the dev build is not running, ask the user to run dev from PowerShell.
Port discovery
With the devshell dev helper, each dev instance probes for a free Vite port (starting at 14253) and a free debug port (starting at 19432), then writes ${TMPDIR:-/tmp}/claudette-dev/<pid>.json with fields {pid, debug_port, vite_port, cwd, branch, started_at}. The file is removed on clean exit.
debug-eval.sh resolves the port in this order:
$CLAUDETTE_DEBUG_PORT (explicit override)
- Live discovery file whose
cwd is an ancestor of the current $PWD (matches the worktree you're running the command from)
- The single live instance, if exactly one is running
- Legacy default
19432
If multiple instances are running and none match $PWD, the script exits with a list of {pid, port, branch, cwd} and asks the user to set CLAUDETTE_DEBUG_PORT or run from inside the target worktree. Stale files from crashed instances are cleaned up on access.
To force a specific instance: CLAUDETTE_DEBUG_PORT=19433 debug-eval.sh '...'.
Scripts
All scripts live in ${CLAUDE_SKILL_DIR}/scripts/:
| Script | Purpose |
|---|
scripts/debug-eval.sh | Single-shot JS eval via TCP |
scripts/debug-monitor.sh | Long-running session monitor (readable key names) |
scripts/debug-wait.sh | Poll until agent idle, return summary JSON |
scripts/debug-screenshot.sh | Cross-platform screenshot capture |
IMPORTANT: Always reference scripts via ${CLAUDE_SKILL_DIR}/scripts/ to ensure correct resolution regardless of working directory.
Architecture
Terminal ──TCP:<port>──> debug server ──eval()──> webview JS context
|
Terminal <──TCP─────── debug server <──invoke── webview (result callback)
- TCP server:
src-tauri/src/commands/debug.rs — wraps JS in async IIFE, evals in webview, 10s timeout
- Port:
19432 by default, overridable via $CLAUDETTE_DEBUG_PORT (set by the devshell dev helper per-instance)
- Input cap: 1 MB max per eval request
Troubleshooting
Symptoms ↔ likely cause when something feels off:
Quick identity check for any port:
CLAUDETTE_DEBUG_PORT=19432 debug-eval.sh 'return document.title'
How to Execute JS
JS must use return to send a value back:
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh 'return document.title'
For multiline JS, use heredoc:
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const s = window.__CLAUDETTE_STORE__.getState();
return s.workspaces.map(w => w.name);
JS
Available Globals (dev mode only)
| Global | Type | Description |
|---|
window.__CLAUDETTE_STORE__ | Zustand store | .getState() to read, .subscribe() to watch |
window.__CLAUDETTE_INVOKE__ | Tauri invoke | Call any Tauri command from eval'd JS |
window.__CLAUDETTE_CHAT_DEBUG__ | boolean | Toggle [chat-debug] console logging |
Actions
/claudette-debug [action] [args...]
discover actions — list all store functions with params
Runtime introspection — always reflects the current app, even after code changes.
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const s = window.__CLAUDETTE_STORE__.getState();
return Object.entries(s)
.filter(([, v]) => typeof v === 'function')
.map(([name, fn]) => {
const match = fn.toString().match(/^\(([^)]*)\)/);
const params = match ? match[1].trim() : '';
return name + '(' + params + ')';
})
.sort()
.join('\n');
JS
discover commands — show Tauri invoke commands
Read reference/tauri-commands.md for the complete list. Key commands for UAT:
sendChatMessage(sessionId, messageId?, content, mentionedFiles?, permissionLevel?, model?, fastMode?, thinkingEnabled?, planMode?, effort?, chromeEnabled?, disable1mContext?, backendId?, attachments?) — keyed on chat session, not workspace
createWorkspace(repoId, name, skipSetup?) -> CreateWorkspaceResult
loadInitialData() -> repos, workspaces, branches
loadDiffFiles(workspaceId) -> DiffFilesResult
stopAgent(sessionId) — keyed on chat session, not workspace
discover state — list all state slices
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const s = window.__CLAUDETTE_STORE__.getState();
return Object.entries(s)
.filter(([, v]) => typeof v !== 'function')
.map(([k, v]) => {
const type = Array.isArray(v) ? 'array' : v instanceof Set ? 'Set' : v === null ? 'null' : typeof v;
const size = Array.isArray(v) ? v.length : v instanceof Set ? v.size : v && typeof v === 'object' ? Object.keys(v).length : null;
const sizeStr = size !== null ? ' (' + size + ')' : '';
const preview = JSON.stringify(v, (_, val) => val instanceof Set ? [...val] : val);
return k + ': ' + type + sizeStr + ' = ' + (preview || 'undefined').substring(0, 80);
})
.sort()
.join('\n');
JS
status — one-shot comprehensive status
Returns everything needed to assess the active workspace in a single eval.
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const s = window.__CLAUDETTE_STORE__.getState();
const wsId = s.selectedWorkspaceId;
if (!wsId) return { error: 'no workspace selected' };
const ws = s.workspaces.find(w => w.id === wsId);
const msgs = s.chatMessages[wsId] || [];
const stream = s.streamingContent[wsId] || '';
const acts = s.toolActivities[wsId] || [];
const turns = s.completedTurns[wsId] || [];
const last = acts[acts.length - 1];
const c = document.querySelector('[class*="messages_"]');
const gap = c ? Math.round(c.scrollHeight - c.scrollTop - c.clientHeight) : -1;
const storeStatus = ws?.agent_status;
const isActive = storeStatus === 'Running' || stream.length > 0 || acts.length > 0 || (s.streamingThinking[wsId] || '').length > 0;
return {
workspace: ws?.name,
agentStatus: isActive ? 'Running' : storeStatus,
messageCount: msgs.length,
lastMessageRole: msgs[msgs.length - 1]?.role,
streaming: stream.length > 0,
streamingLength: stream.length,
activeTools: acts.length,
lastTool: last ? last.toolName + ': ' + (last.summary || '').substring(0, 60) : null,
completedTurns: turns.length,
scrollGap: gap,
atBottom: gap >= 0 && gap < 50,
pendingQuestion: !!s.agentQuestions[wsId],
pendingPlanApproval: !!s.planApprovals[wsId],
planMode: s.planMode[wsId] || false,
fastMode: s.fastMode[wsId] || false,
thinkingEnabled: s.thinkingEnabled[wsId] || false,
showThinking: s.showThinkingBlocks[wsId] === true,
thinkingLength: (s.streamingThinking[wsId] || '').length,
effortLevel: s.effortLevel[wsId] || 'auto',
};
JS
send "message" — send chat message to active workspace
Substitute MESSAGE_TEXT_HERE with the actual message. Escape backticks with ```.
Important: This sets agent_status to "Running" in the store before invoking the
Tauri command. The normal UI path (ChatPanel) does this in React; since we bypass that
component, we must replicate it here so that wait, monitor, and status see the
correct state.
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const s = window.__CLAUDETTE_STORE__.getState();
const wsId = s.selectedWorkspaceId;
if (!wsId) return 'ERROR: no workspace selected';
// After the multi-session refactor, send_chat_message is keyed on
// chat_session_id, not workspace_id. Resolve the active session from
// `selectedSessionIdByWorkspaceId` (set by the sidebar when a workspace
// becomes active). If absent, the first session in `sessionsByWorkspace`
// is the safe fallback — every workspace has at least one default session.
const sessionId = s.selectedSessionIdByWorkspaceId?.[wsId]
?? s.sessionsByWorkspace?.[wsId]?.[0]?.id;
if (!sessionId) return 'ERROR: no chat session for workspace';
const ws = s.workspaces.find(w => w.id === wsId);
if (ws?.agent_status === 'Running') return 'ERROR: agent already running';
s.updateWorkspace(wsId, { agent_status: 'Running' });
s.clearUnreadCompletion(wsId);
await window.__CLAUDETTE_INVOKE__('send_chat_message', {
sessionId,
messageId: null,
content: `MESSAGE_TEXT_HERE`,
mentionedFiles: null,
permissionLevel: null,
model: null,
fastMode: null,
thinkingEnabled: null,
planMode: null,
effort: null,
chromeEnabled: null,
disable1mContext: null,
backendId: null,
attachments: null,
});
return 'Sent to ' + ws.name;
JS
wait — block until agent idle
Polls every 2s until done. Must use run_in_background: true.
${CLAUDE_SKILL_DIR}/scripts/debug-wait.sh
Options: --timeout N (default 600s), --interval N (default 2s), --workspace ID
Returns: { running: false, agentStatus, messageCount, completedTurns, lastTurnTools, lastToolSummary, durationSeconds }
screenshot — capture screen for visual inspection
${CLAUDE_SKILL_DIR}/scripts/debug-screenshot.sh
Returns image path. Use the Read tool to view it. Options: --output PATH
- macOS:
screencapture -x (silent full-screen)
- Linux/Wayland:
grim
- Linux/X11:
import -window root or scrot
state — contextual store overview
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const s = window.__CLAUDETTE_STORE__.getState();
const wsId = s.selectedWorkspaceId;
const lines = [];
if (wsId) {
const ws = s.workspaces.find(w => w.id === wsId);
const msgs = s.chatMessages[wsId] || [];
const stream = s.streamingContent[wsId] || '';
const acts = s.toolActivities[wsId] || [];
const turns = s.completedTurns[wsId] || [];
const storeStatus = ws?.agent_status;
const isActive = storeStatus === 'Running' || stream.length > 0 || acts.length > 0 || (s.streamingThinking[wsId] || '').length > 0;
const effectiveStatus = isActive ? 'Running' : storeStatus;
lines.push('=== Active: ' + ws?.name + ' (' + wsId.substring(0, 8) + ') ===');
lines.push(' agentStatus: ' + effectiveStatus);
lines.push(' messages: ' + msgs.length + (msgs.length > 0 ? ' (last: ' + msgs[msgs.length-1].role + ')' : ''));
lines.push(' streaming: ' + (stream.length > 0 ? stream.length + ' chars' : 'false'));
lines.push(' activeTools: ' + acts.length + (acts.length > 0 ? ' (last: ' + acts[acts.length-1].toolName + ')' : ''));
lines.push(' completedTurns: ' + turns.length);
lines.push(' planMode: ' + (s.planMode[wsId] || false));
lines.push(' thinkingEnabled: ' + (s.thinkingEnabled[wsId] || false));
lines.push(' showThinking: ' + (s.showThinkingBlocks[wsId] === true));
lines.push(' thinkingLength: ' + (s.streamingThinking[wsId] || '').length);
lines.push(' effortLevel: ' + (s.effortLevel[wsId] || 'auto'));
} else {
lines.push('=== No workspace selected ===');
}
const others = s.workspaces.filter(w => w.id !== wsId);
if (others.length > 0) {
lines.push('');
lines.push('=== Other Workspaces ===');
others.forEach(w => {
const mc = (s.chatMessages[w.id] || []).length;
lines.push(' ' + w.name + ': ' + w.agent_status + ', ' + mc + ' msgs');
});
}
lines.push('');
lines.push('=== Global ===');
lines.push(' repositories: ' + s.repositories.length);
lines.push(' sidebar: ' + s.sidebarVisible + ', rightSidebar: ' + s.rightSidebarVisible);
lines.push(' terminal: ' + s.terminalPanelVisible);
lines.push(' diffFiles: ' + s.diffFiles.length);
lines.push(' theme: ' + s.currentThemeId);
return lines.join('\n');
JS
state <slice> — dump specific slice
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh 'return window.__CLAUDETTE_STORE__.getState().SLICE_NAME'
eval <js> — arbitrary JS
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh 'USER_JS_HERE'
monitor start — background session monitor
Run with run_in_background: true. Logs state changes with readable keys.
${CLAUDE_SKILL_DIR}/scripts/debug-monitor.sh
Output keys: toolCount, completedTurns, agentStatus, scrollGap, messageCount, lastToolSummary, inputJsonValid, streaming, thinking, thinkingEnabled, showThinking, effortLevel
Options: --expr 'JS', --interval N, --max N, --logfile PATH
monitor read — tail monitor log
tail -50 /tmp/claudette-debug/monitor.log
watch <slice> — subscribe to changes
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
window.__CLAUDETTE_DEBUG_UNSUB__?.();
window.__CLAUDETTE_DEBUG_UNSUB__ = window.__CLAUDETTE_STORE__.subscribe(
(state, prev) => {
if (state.SLICE_NAME !== prev.SLICE_NAME) {
console.log('[debug] SLICE_NAME changed:', { prev: prev.SLICE_NAME, next: state.SLICE_NAME });
}
}
);
return 'Watching SLICE_NAME — check webview console';
JS
unwatch
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
window.__CLAUDETTE_DEBUG_UNSUB__?.();
delete window.__CLAUDETTE_DEBUG_UNSUB__;
return 'All watchers removed';
JS
trace <action> — monkey-patch store action
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const store = window.__CLAUDETTE_STORE__;
const orig = store.getState().ACTION_NAME;
if (typeof orig !== 'function') return 'ERROR: ACTION_NAME is not a function';
store.setState({
ACTION_NAME: (...args) => {
console.log('[debug] ACTION_NAME called:', args);
return orig(...args);
}
});
window.__CLAUDETTE_DEBUG_TRACED__ = window.__CLAUDETTE_DEBUG_TRACED__ || [];
window.__CLAUDETTE_DEBUG_TRACED__.push({ name: 'ACTION_NAME', orig });
return 'Tracing ACTION_NAME — check webview console';
JS
untrace
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
(window.__CLAUDETTE_DEBUG_TRACED__ || []).forEach(({ name, orig }) => {
window.__CLAUDETTE_STORE__.setState({ [name]: orig });
});
window.__CLAUDETTE_DEBUG_TRACED__ = [];
return 'All traces removed';
JS
hotkeys on — lock hotkey badges visible for layout inspection
Overrides setMetaKeyHeld to ignore false — badges stay on through Cmd+Tab, blur, keypresses.
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const store = window.__CLAUDETTE_STORE__;
const orig = store.getState().setMetaKeyHeld;
window.__META_ORIG__ = orig;
store.setState({ setMetaKeyHeld: (held) => { if (held) orig(held); } });
orig(true);
return 'Hotkey badges LOCKED on';
JS
hotkeys off — unlock and hide hotkey badges
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const store = window.__CLAUDETTE_STORE__;
if (window.__META_ORIG__) {
store.setState({ setMetaKeyHeld: window.__META_ORIG__ });
window.__META_ORIG__(false);
delete window.__META_ORIG__;
}
return 'Hotkey badges unlocked';
JS
maximize — toggle window maximize for resize testing
Programmatically maximize the window, wait, then restore. Useful for testing resize flash behavior.
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const { getCurrentWindow } = window.__TAURI__.window ?? await import("@tauri-apps/api/window");
const win = getCurrentWindow();
const wasMax = await win.isMaximized();
if (wasMax) {
await win.unmaximize();
return 'Window unmaximized';
} else {
await win.maximize();
return 'Window maximized';
}
JS
maximize-cycle — maximize then restore after delay
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const { getCurrentWindow } = window.__TAURI__.window ?? await import("@tauri-apps/api/window");
const win = getCurrentWindow();
await win.maximize();
await new Promise(r => setTimeout(r, 1500));
await win.unmaximize();
return 'Maximize cycle complete';
JS
resize-info — inspect native window/webview layer state
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const { getCurrentWindow } = window.__TAURI__.window ?? await import("@tauri-apps/api/window");
const win = getCurrentWindow();
const size = await win.innerSize();
const pos = await win.innerPosition();
const isMax = await win.isMaximized();
const isFull = await win.isFullscreen();
const scaleFactor = await win.scaleFactor();
const root = document.documentElement;
const body = document.body;
const rootBg = getComputedStyle(root).backgroundColor;
const bodyBg = getComputedStyle(body).backgroundColor;
return {
windowSize: { width: size.width, height: size.height },
position: { x: pos.x, y: pos.y },
isMaximized: isMax,
isFullscreen: isFull,
scaleFactor,
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
rootBackground: rootBg,
bodyBackground: bodyBg,
htmlInlineStyle: root.style.cssText || '(none)',
};
JS
snapshot — full store state dump
${CLAUDE_SKILL_DIR}/scripts/debug-eval.sh <<'JS'
const state = window.__CLAUDETTE_STORE__.getState();
return Object.fromEntries(
Object.entries(state).filter(([, v]) => typeof v !== 'function')
);
JS
Supporting Files
Detailed reference material in the reference/ directory: