State Management
Ghost’s frontend manages all application state through Zustand — a lightweight state management library for React. Instead of one giant global store, Ghost splits its state across 21 focused stores, each responsible for a single domain (flows, sessions, devices, addons, etc.). Each store is a plain JavaScript object containing state fields and action methods — no middleware, no reducers, no boilerplate. The only persistence is localStorage for a handful of UI preferences (theme, pinned items, layout choices).
Think of each store like a filing cabinet drawer. The flow store holds all the network traffic data. The session store holds session information. The addon store holds addon scripts and their log output. Components reach into whichever drawers they need, and when a WebSocket event arrives from the backend, the App.tsx event router drops the update into the right drawer automatically.
Store Inventory
Section titled “Store Inventory”| # | Store | Key State | Live Updates | Caps |
|---|---|---|---|---|
| 1 | useFlowStore | flows[], selectedFlowId, selectedFlow, hostStats, appStats, deviceStats, multi-select with Shift+Click range | flow.created/updated/deleted | 5,000 per fetch, 100ms batch |
| 2 | useUIStore | appMode, theme, searchQuery, filters, viewMode, groupMode, slideOverPanel, activePanel, previousPanel, wsStatus, 11 localStorage keys | finding.* (version bump) | — |
| 3 | useSessionStore | sessions[], activeSessionId | session.created/updated/deleted | — |
| 4 | useProxyStore | status (running, address, systemProxy) | proxy.started/stopped/status | — |
| 5 | useAgentStore | conversations[], messages[], isStreaming, plan, runMetrics, options | SSE streaming (12 event types including options for interactive choices) | — |
| 6 | useAddonStore | addons[], logs[], selectedAddonId | addon.created/updated/deleted/log | 500 log entries |
| 7 | useExtensionStore | connected, interactions[], tabUrl, everSeen | extension.connected/disconnected/interaction/tab_switch | 50 interactions |
| 8 | useBreakpointStore | rules[], pendingFlows: Map | flow.breakpoint/resumed | — |
| 9 | useRulesStore | rules[], selectedRuleId | rule.created/updated/deleted | — |
| 10 | useInjectionRulesStore | rules[], selectedRuleId | injection_rule.created/updated/deleted | — |
| 11 | useDeviceStore | devices[], connecting timeout | device.discovered/updated/connected/disconnected/removed | 120s timeout |
| 12 | useArtifactStore | artifacts[], optimistic delete | artifact.created/deleted | — |
| 13 | useFridaStore | status, devices/apps/processes/sessions/scripts, consoleLines, batched output | frida.session.started/stopped, frida.output | 500 lines/session, 100ms batch |
| 14 | useAttackerStore | running, summary, wordlists, completed/total/matched, latestResults | attacker:started/progress/completed/error | 50 results |
| 15 | useUpdaterStore | available, updateInfo, downloading, progress, installed | Tauri updater events | 30s check timeout |
| 16 | useSetupStore | completed, phase, certInstalled, certSkipped | — | — |
| 17 | useComparisonStore | sessionA/B, comparison, selectedEndpointKey, flow pair loading | — | — |
| 18 | useSettingsStore | settings, validating, validationResult | — | — |
| 19 | useInspectorStore | hierarchy, selectedElementId, selectors[], bugReport, screenshot freeze/scrub, interaction mode | — | — |
Plus two more stores referenced in imports (21 total store files in the stores/ directory).
Flow Store
Section titled “Flow Store”The flow store is the most complex store, managing the primary data model — all captured HTTP traffic. It handles thousands of flows, multi-select, live filtering, batched updates, and on-demand detail loading.
| Field | Type | Purpose |
|---|---|---|
flows | FlowDTO[] | The visible flow list. Uses lightweight summary DTOs (no bodies or headers — just method, status, host, path, timing, size). Default fetch limit: 5,000 flows. |
total | number | Total flow count from the backend (may be larger than flows.length if not all are loaded). |
loading | boolean | Whether a flow fetch is in progress. |
error | string | null | Error message from the last failed operation. |
newFlowIds | Set<string> | Set of flow IDs that just arrived via WebSocket. Used to trigger a CSS flash animation on new rows in the traffic list. IDs are cleared after the animation completes. |
selectedFlowId | string | null | The currently selected flow (single-click). |
selectedFlow | FlowDTO | null | The full detail DTO for the selected flow (loaded on demand via a separate API call, includes headers and body). |
detailLoading | boolean | Whether the detail fetch for the selected flow is in progress. |
comparisonFlowId | string | null | A second flow selected for side-by-side comparison. |
comparisonFlow | FlowDTO | null | Full detail DTO for the comparison flow. |
comparisonPickerOpen | boolean | Whether the comparison flow picker dialog is open. |
selectedFlowIds | Set<string> | Set of flow IDs selected via multi-select (Ctrl+Click). Used for bulk operations like delete or tag. |
lastSelectedIndex | number | null | Index of the last selected flow. Used for Shift+Click range selection — clicking with Shift held selects all flows between lastSelectedIndex and the clicked index. |
hostStats | HostStatDTO[] | Aggregated request counts per hostname. Drives the domain navigator in the left sidebar. |
appStats | AppStatDTO[] | Aggregated request counts per source application. Drives the app navigator. |
deviceStats | DeviceStatDTO[] | Aggregated request counts per device. Drives the device navigator. |
Batched Updates
Section titled “Batched Updates”The addFlow and updateFlow actions use an internal batching mechanism to prevent excessive React re-renders during burst traffic. Instead of updating the store on every single WebSocket event (which could fire hundreds of times per second during active browsing), events are collected in a pending buffer and flushed every 100ms in a single state update.
Both functions are implemented as IIFEs (Immediately Invoked Function Expressions) that return closures — they maintain internal pending arrays and timer references in their closure scope. Each has a _cancel() method used by clearFlows() to prevent stale batches from re-inserting deleted flows.
Live Filter Matching
Section titled “Live Filter Matching”When a flow.created WebSocket event arrives, the flow doesn’t automatically appear in the traffic list. It first passes through matchesClientFilter() — a client-side filter function that checks, in order:
- Method filter — case-insensitive match against the HTTP method (GET, POST, etc.)
- Status code filter — supports exact codes (404), range wildcards (2xx, 3xx), and comparisons (>399, <500)
- Host filter — substring match from structured filter conditions
- Content type filter — substring match from structured filter conditions
- Domain navigator selection — exact match against the selected domain in the scope panel
- Content type panel selection — substring match against the selected content category
- App navigator selection — exact match against the selected application
- Device navigator selection — matches against device ID or client IP
- Free text search — case-insensitive substring search across method, host, path, and URL
Only flows that pass ALL active filters are added to the visible list. This means your live feed always respects whatever you’re currently filtering — if you’re looking at “status:>399”, only error responses appear in real time, not all traffic.
Stats Refresh
Section titled “Stats Refresh”Host, app, and device statistics are refreshed via a debounced callback with a 2-second delay triggered by flow creation or deletion events. This prevents hammering the stats API during burst traffic — if 100 flows arrive in 1 second, the stats refresh fires once, 2 seconds after the last event.
UI Store
Section titled “UI Store”The UI store is the central state hub for everything related to layout, appearance, navigation, and mode switching. It has more state fields than any other store (over 30) and interacts with several other stores.
Key Responsibilities
Section titled “Key Responsibilities”Mode management: Tracks appMode (QA vs Security), securityMode (web vs mobile), and scanMode (passive, active-safe, active-full). Switching appMode is a heavy operation — it resets all filters, selections, slide-over panels, inspector state, Frida state, sort settings, flow selection, and security center view. This clean slate ensures mode switches start fresh.
Theme: Manages dark/light theme. Switching the theme updates the DOM immediately (adds/removes dark class on the document root) and persists the choice to localStorage.
Layout controls: View mode (list vs waterfall), group mode (none, by host, by path, by tag), panel layout (horizontal vs vertical split), detail layout (stacked vs side-by-side), and scope panel collapse state.
Scope selection: Tracks which domains, apps, devices, and content types the user has selected in the left sidebar. These selections drive the flow list’s client-side filtering.
Pinned items: Users can pin frequently-used domains, apps, and devices to keep them at the top of the navigator lists. Pinned items are persisted to localStorage so they survive app restarts.
Filter presets: Named filter queries that the user has saved (e.g., “API Errors” = status:>399 content_type:json). Stored in localStorage.
Slide-over panel: Tracks which slide-over panel is currently open (or null for closed). Opening a panel sets slideOverPanel to the panel ID string.
Exclusive center panel routing: The UI store manages a single activePanel: string | null slot for panels that replace the traffic view (Attack Planner, Session Compare, Inspector, Frida, Findings). Panel definitions are in stores/panel-registry.ts. openPanel(id) captures a PanelSnapshot (label + restore closure) of the current panel before switching, enabling context-aware back navigation. goBack() restores the previous panel via its closure. closePanel() returns to the traffic view.
Cross-store interactions: The UI store reaches into other stores when needed:
setAppMode()callsuseFlowStore.getState().setSelectedFlowId(null)andclearSelection()to deselect flows, resetsactivePanelto nullcloseFridaPanel()callsuseFridaStore.getState().reset()to clean up Frida stateopenInspector(deviceId)callsopenPanel('inspector')then sets the inspector device ID
localStorage Persistence
Section titled “localStorage Persistence”11 keys are persisted to localStorage — these are the only persistent state in the entire frontend:
| Key | What It Stores |
|---|---|
ghost-app-mode | QA or Security mode |
ghost-theme | Dark or Light theme |
ghost-pinned-hosts | JSON array of pinned domain names |
ghost-pinned-apps | JSON array of pinned application names |
ghost-pinned-devices | JSON array of pinned device IDs |
ghost-detail-layout | Inspector layout (stacked or side-by-side) |
ghost-panel-layout | Split panel orientation (horizontal or vertical) |
ghost-filter-presets | JSON array of saved filter presets |
ghost-security-mode | Security sub-mode (web or mobile) |
ghost-scan-mode | Security scan mode (passive, active-safe, active-full) |
ghost-security-sidebar-tab | Active tab in security sidebar |
No other store uses localStorage or any persistence middleware.
Agent Store (SSE Streaming)
Section titled “Agent Store (SSE Streaming)”The agent store manages the AI chat conversation using Server-Sent Events (SSE) — not WebSocket. When the user sends a message, the store opens an SSE connection to POST /api/v1/agent/chat and processes streaming events as they arrive. This is different from the WebSocket connection used for traffic events — SSE was chosen for the agent because it supports POST request bodies (to send the user’s message) and has a simpler lifecycle for request-response streaming.
SSE Events
Section titled “SSE Events”| Event | What It Does |
|---|---|
chunk | Appends a text token to the current assistant message (the message builds up word by word as tokens stream in) |
tool_call | Creates a new tool call card in the message (showing the tool name and input parameters). Marked as pending: true until the result arrives. |
tool_result | Marks the matching tool call as complete and attaches the result text |
plan_created | Sets the structured task plan (the agent’s step-by-step execution plan) |
plan_revised | Updates the plan with a revised version (agent adjusted its approach) |
plan_completed | Marks the plan as completed |
step_completed | Marks a specific step in the plan as completed |
options | Sets options on the current message — renders an OptionsPanel with interactive choice cards and free-text input. The agent loop pauses until the user responds. |
steer | Acknowledgment that a steer message was received (no UI update) |
metrics | Updates the run metrics display (token usage, tool call counts, iteration count). Run Summary is hidden when termination reason is "waiting for user choice". |
done | Finalizes the message, assigns permanent IDs, sets activeConversationId, refreshes the conversation list |
error | Stops streaming and shows the error |
Race Protection
Section titled “Race Protection”The agent store uses two mechanisms to prevent stale data:
_abortController— anAbortControllerthat cancels the active SSE stream when the user starts a new conversation, loads a different conversation, or explicitly stops generation_streamGeneration— a monotonic counter that increments on every conversation switch. SSE callbacks check this counter before updating state, ensuring events from a previous conversation don’t corrupt the current one.
Breakpoint Store
Section titled “Breakpoint Store”The breakpoint store manages flows that are paused mid-flight — waiting for the user to inspect and optionally modify them before they continue to the server (request breakpoints) or to the client (response breakpoints).
The pendingFlows field is a Map keyed by flow ID, where each value is a BreakpointEvent containing:
flow_id— the flow being heldphase— whether this is arequestorresponsebreakpointmethod,url,host,path— request identityheaders,body— request headers and body (editable)status_code,response_headers,response_body— response data (for response breakpoints, editable)
When a flow.breakpoint WebSocket event arrives, the flow is added to the map and the BreakpointEditor overlay appears. When the user resumes the flow (with or without modifications), the resumeFlow action sends the modifications to the backend, and the flow.resumed WebSocket event removes the flow from the pending map.
Device Store
Section titled “Device Store”The device store tracks all connected mobile devices and simulators/emulators for the inspector feature. It includes a 120-second connecting timeout — if a device stays in the connecting state for 2 minutes (device connection involves starting WDA on iOS or atx-agent on Android, which can be slow), it automatically transitions to an error state with the message “Connection timed out — device may still be starting. Try again.”
The timeout is tracked per-device using a module-level Map<string, timeout>. When a terminal state arrives via WebSocket (connected, disconnected, error), the timeout is cleared.
The inspectDevice action is a convenience method that resolves a traffic-level device key to an inspector device, auto-connects if needed, and opens the inspector panel — all in one click from the device navigator in the scope panel.
Extension Store
Section titled “Extension Store”The extension store tracks the browser extension’s connection status and captured interactions (clicks, form inputs, navigation, etc.). Interactions are capped at 50 entries — oldest entries are dropped when the cap is reached.
The everSeen flag tracks whether the extension has ever connected during this session. This drives the status bar indicator — it only shows “Extension” after the first connection, to avoid showing a permanently “disconnected” indicator to users who don’t use the extension.
The extension.interaction WebSocket event requires camelCase-to-snake_case mapping because the browser extension sends events in camelCase format but the frontend DTOs use snake_case.
Frida Store
Section titled “Frida Store”The Frida store manages runtime instrumentation sessions, including device/app selection, script editing, saved scripts, and console output. It’s one of the most complex stores with over 30 state fields and 20+ actions.
Console Output Batching
Section titled “Console Output Batching”Frida scripts can produce console output at very high rates (hundreds of lines per second during hooking). To prevent React from re-rendering on every single line, the store batches console output using a module-level buffer:
- Lines are collected in a
consoleBatchMap (keyed by session key) - A flush timer fires every 100ms
- On flush, all buffered lines are applied to the store in a single state update
- Each session is capped at 500 console lines (oldest lines are trimmed when the cap is reached)
The batching mechanism is module-level (not part of the Zustand state) so it survives store resets. It uses a _generationId counter to discard stale batches — when the user switches devices or resets the store, the generation ID increments, and any in-flight batches from the previous generation are silently dropped.
Race Protection
Section titled “Race Protection”Like the agent store, the Frida store uses a _generationId for async race protection. All async actions (device selection, app listing, session management) check the generation ID before applying their results. This prevents stale data from a slow API response from overwriting the state for a newly-selected device.
Attacker Store
Section titled “Attacker Store”The attacker store manages the request fuzzer’s lifecycle. The attacker runs asynchronously on the backend — the frontend sends a start request and receives progress updates via WebSocket events.
The latestResults field caps at 50 entries and only keeps “interesting” results (requests that matched at least one match rule). This prevents memory growth during large attacks that might test thousands of payloads.
Comparison Store
Section titled “Comparison Store”The comparison store manages session-to-session comparison data (session selection, comparison results, flow pair loading). Routing is handled by the UI store’s activePanel — the comparison view renders when activePanel === 'comparison'. The store includes race protection:
_fetchGenId— monotonic counter that prevents stale comparison results from overwriting newer ones_abortController— cancels in-flight comparison API requests when the user changes sessions
Same-session comparison (comparing a session against itself) is explicitly rejected with an error message.
Updater Store
Section titled “Updater Store”The updater store checks for Ghost updates via the Tauri updater plugin. It dynamically imports the @tauri-apps/plugin-updater package (not loaded at startup to avoid blocking the initial render). The update check has a 30-second timeout and only runs inside the Tauri environment (checks for __TAURI_INTERNALS__ on the window object).
The download flow tracks progress through three phases: Started (sets total bytes), Progress (increments downloaded bytes and calculates percentage), and Finished (sets 100% and marks as installed). After installation, relaunch() is called to restart the app.
WebSocket Synchronization Pattern
Section titled “WebSocket Synchronization Pattern”All stores that handle WebSocket events follow the same pattern:
- Event arrives in
App.tsx’shandleWSEventcallback - App routes the event to the appropriate store’s sync method based on the event type string
- Store updates its state optimistically (directly mutates the Zustand state — no re-fetch from the server)
- Related stores may be notified (e.g., flow creation triggers stats refresh, finding creation bumps the findings version counter and shows a toast)
This means the frontend stays in sync with the backend without polling. WebSocket is the primary data channel for all real-time updates, and the REST API is used only for initial data loading, user-initiated actions, and the polling fallback when WebSocket is disconnected.
Complete Event Routing Table
Section titled “Complete Event Routing Table”42 distinct WebSocket event types are routed to 12 different stores (2 events are received but not surfaced in the UI):
| Events | Target Store |
|---|---|
flow.created, flow.updated, flow.deleted | useFlowStore |
session.created, session.updated, session.deleted | useSessionStore |
proxy.started, proxy.stopped, proxy.status | useProxyStore |
addon.created, addon.updated, addon.deleted, addon.log | useAddonStore |
extension.connected, extension.disconnected, extension.interaction, extension.tab_switch | useExtensionStore |
extension.console_error, extension.navigation | (received but not surfaced) |
flow.breakpoint, flow.resumed | useBreakpointStore |
rule.created, rule.updated, rule.deleted | useRulesStore |
injection_rule.created, injection_rule.updated, injection_rule.deleted | useInjectionRulesStore |
device.discovered, device.updated, device.connected, device.disconnected, device.removed | useDeviceStore |
artifact.created, artifact.deleted | useArtifactStore |
finding.created, finding.updated, finding.deleted | useUIStore |
frida.session.started, frida.session.stopped, frida.output | useFridaStore |
attacker:started, attacker:progress, attacker:completed, attacker:error | useAttackerStore |
All Caps and Limits
Section titled “All Caps and Limits”| Store | Constant | Value | What Happens When Exceeded |
|---|---|---|---|
| useFlowStore | Fetch limit | 5,000 | Only the most recent 5,000 flows are loaded per fetch. Use fetchMore() to paginate. |
| useFlowStore | Batch interval | 100ms | addFlow and updateFlow events are buffered and applied in batches every 100ms to reduce React re-renders. |
| useAddonStore | MAX_LOGS | 500 | Oldest log entries are trimmed. Newest entries are prepended. |
| useExtensionStore | MAX_RECENT_INTERACTIONS | 50 | Oldest interactions are dropped. |
| useFridaStore | MAX_CONSOLE_LINES | 500 per session | Oldest console lines are trimmed per Frida session. |
| useFridaStore | CONSOLE_BATCH_INTERVAL | 100ms | Console output lines are buffered and flushed every 100ms. |
| useAttackerStore | MAX_LATEST_RESULTS | 50 | Only the 50 most recent interesting attack results are kept. |
| useDeviceStore | CONNECTING_TIMEOUT_MS | 120,000ms (2 min) | Device auto-transitions to error state with a timeout message. |
| useUpdaterStore | Check timeout | 30,000ms (30s) | Update check times out after 30 seconds. |