Activate operational Web Mode: WS, tool execution, Ollama, sessions & self-evolution#23
Activate operational Web Mode: WS, tool execution, Ollama, sessions & self-evolution#23Neuro-Rift wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
|
Overall Grade Focus Area: Hygiene |
Security Reliability Complexity Hygiene |
Code Review Summary
| Analyzer | Status | Updated (UTC) | Details |
|---|---|---|---|
| Python | Feb 24, 2026 1:57p.m. | Review ↗ |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f59e197ce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let Some(session) = self.get_active_session() { | ||
| let mut session = session.write(); | ||
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { |
There was a problem hiding this comment.
Bind task completion updates to the originating session
execute_task captures the task's session_id before awaiting Python, but completion handling re-reads self.get_active_session(); if an operator switches sessions while a long-running tool is in flight, status/result updates are applied to the wrong session (or dropped when the task ID is not found), leaving the original task stuck in running and unsaved. Resolve completion and persistence against the captured session ID instead of the current active session.
Useful? React with 👍 / 👎.
| tokio::spawn(async move { | ||
| if let Err(e) = core_exec.execute_task(&task_id).await { | ||
| tracing::error!("Failed to execute task {}: {}", task_id, e); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Implement real task cancellation instead of state-only updates
Task execution is launched with tokio::spawn and the JoinHandle is discarded immediately, so cancel_task has nothing to abort and can only mutate task state. In practice, a canceled task keeps running in the Python bridge and may still emit task_failed/task_completed after task_cancelled, which breaks operator expectations and produces contradictory task lifecycle events.
Useful? React with 👍 / 👎.
| setTimeout(() => { | ||
| setMessages(prev => [ | ||
| { | ||
| id: `${Date.now()}-assistant`, | ||
| role: 'assistant', |
There was a problem hiding this comment.
Render backend chat responses in CommandPanel
After sending a real chat command, the panel inserts a hardcoded assistant reply on a timer and never subscribes to chat_response; with ChatWidget no longer mounted in layout, actual model output and AI failure details are not shown anywhere in Web Mode. This makes command chat look successful even when generation fails or returns different content.
Useful? React with 👍 / 👎.
📝 WalkthroughWalkthroughThe pull request introduces a comprehensive task execution and session management system with real-time WebSocket-driven event handling, task lifecycle management (queueing, execution, cancellation), Ollama integration for AI model status, session-based auditing, and refactored UI panels for live monitoring. Changes span the Rust core, Python bridge, and TypeScript frontend. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Web Client
participant WS as WebSocket
participant Core as Rust Core
participant PythonBridge as Python Bridge
participant Ollama as Ollama Service
Client->>WS: QueueTask (tool_name, target, args)
WS->>Core: queue_task()
activate Core
Core->>Core: Create task, mark Queued
Core->>WS: Broadcast TaskQueued
deactivate Core
Core-->>WS: return Some(task_id)
WS-->>Client: TaskQueued event
activate Client
Client->>WS: spawn execute_task(task_id) async
deactivate Client
WS->>Core: execute_task(task_id)
activate Core
Core->>Core: Mark Running, record start_time
Core->>WS: Broadcast TaskStarted
Core->>PythonBridge: execute_command (session_id, tool, args)
deactivate Core
WS-->>Client: TaskStarted event
activate PythonBridge
PythonBridge->>Ollama: Query model availability
Ollama-->>PythonBridge: model_count, available
PythonBridge->>PythonBridge: Execute tool, capture output
PythonBridge->>PythonBridge: Append audit entry
deactivate PythonBridge
PythonBridge-->>Core: Command result
Core->>Core: Update task status (Completed/Failed)
Core->>WS: Broadcast TaskOutput chunks
Core->>WS: Broadcast TaskCompleted or TaskFailed
WS-->>Client: TaskOutput events (streamed)
WS-->>Client: TaskCompleted event
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
|
CodeAnt AI is reviewing your PR. |
Nitpicks 🔍
|
| let mode = session.mode; | ||
|
|
||
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { | ||
| task.status = crate::state::TaskStatus::Running; |
There was a problem hiding this comment.
Suggestion: A task that has already been marked as cancelled can still be executed because execute_task unconditionally transitions it to Running and dispatches it to the Python bridge, so a user-issued cancel may be ignored if it races with execution startup. [logic error]
Severity Level: Critical 🚨
- ❌ Cancelled tools still execute via Python bridge executor.
- ⚠️ WebSocket TaskCancelled and TaskStarted events conflict for task.
- ⚠️ Tools page may show cancelled yet still-running tasks.
- ⚠️ User intent to abort long tools not reliably honored.| task.status = crate::state::TaskStatus::Running; | |
| if task.status == crate::state::TaskStatus::Cancelled { | |
| // Do not execute tasks that were cancelled before starting | |
| return Ok(()); | |
| } |
Steps of Reproduction ✅
1. Run the core binary (`core/neurorift-core/src/main.rs:7-8`) so that the WebSocket
server and command listener are active.
2. From a WebSocket client (e.g., Web Mode tools UI as described in the PR), send a
`QueueTask` event over WS matching `WSEvent::QueueTask`
(`core/neurorift-core/src/websocket/events.rs:144-148`); the server forwards it via
`WebSocketServer::handle_connection` (`websocket/mod.rs:76-85`) into the broadcast
channel.
3. Observe in `main.rs:70-126` that the command loop matches `QueueTask { tool_name,
target, args }` and calls `core_cmd.queue_task(...)`; on `Ok(Some(task_id))` it spawns
`core_exec.execute_task(&task_id)` in a new Tokio task (`main.rs:110-119`).
4. Immediately after the task is queued but before or while `execute_task` starts, send a
`CancelTask` event for the same `task_id` (matching `WSEvent::CancelTask` at
`events.rs:149-151`), which the command loop handles at `main.rs:127-131` by calling
`core_cmd.cancel_task(&task_id)`. This sets `task.status = TaskStatus::Cancelled` and
broadcasts `TaskCancelled` (`lib.rs:192-205`).
5. When the previously spawned `execute_task` runs, it calls `get_active_session()` and
locks the session (`lib.rs:210-213`), then executes the `if let Some(task) =
session.task_queue.iter_mut().find(...)` block at `lib.rs:215-225`, which unconditionally
sets `task.status = TaskStatus::Running` and broadcasts `TaskStarted` even if the task was
marked `Cancelled` by `cancel_task` in step 4.
6. The command then proceeds to build the Python `tool_execute` payload and call
`self.python_bridge.execute(command).await` (`lib.rs:233-242`), causing the cancelled task
to actually execute via the Python bridge (`python_bridge/mod.rs:40-52`), despite the user
having requested cancellation.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** core/neurorift-core/src/lib.rs
**Line:** 216:216
**Comment:**
*Logic Error: A task that has already been marked as cancelled can still be executed because `execute_task` unconditionally transitions it to `Running` and dispatches it to the Python bridge, so a user-issued cancel may be ignored if it races with execution startup.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| if let Some(session) = self.get_active_session() { | ||
| let session_id = session.read().id.clone(); | ||
| let _ = self.save_session(&session_id); |
There was a problem hiding this comment.
Suggestion: After a task completes, the core saves whichever session is currently active instead of the session that actually owned the task, so if the active session changes during execution the task's updated state may be written to the wrong session or not persisted at all. [logic error]
Severity Level: Major ⚠️
- ⚠️ Long-running tool updates may not persist for original session.
- ⚠️ Active session switching during tools causes mis-saved sessions.
- ⚠️ Session autosave behavior becomes misleading for multi-session users.
- ⚠️ Potentially stale task queues when reloading affected session.| if let Some(session) = self.get_active_session() { | |
| let session_id = session.read().id.clone(); | |
| let _ = self.save_session(&session_id); | |
| let _ = self.save_session(&session_id); |
Steps of Reproduction ✅
1. Start the core service (`core/neurorift-core/src/main.rs:7-38`) so WebSocket and
command listener are running.
2. From a client, create or load session A using `CreateSession` or `LoadSession`
WebSocket events (`websocket/events.rs:127-140`), which `main.rs:73-83` handle by calling
`core_cmd.create_session` / `core_cmd.load_session`; this sets `active_session` to A
(`lib.rs:83-108` for creation and `lib.rs:118-136` for load in the final file, implied by
create/load logic).
3. Queue a long-running tool in session A via a `QueueTask` event (`events.rs:144-148`),
which `main.rs:110-126` maps to `core_cmd.queue_task(...)` and then spawns
`core_exec.execute_task(&task_id)` for the returned `task_id`.
4. While `execute_task` is awaiting the Python bridge (`lib.rs:233-242`), trigger a
session change to session B (e.g., loading or creating another session) via
`CreateSession` or `LoadSession` WS events; `main.rs:73-83` calls
`create_session`/`load_session`, which update `active_session` to B (see `lib.rs` session
management).
5. After the Python bridge returns, `execute_task` reaches its trailing persistence block
at `lib.rs:303-305`, calls `get_active_session()` again, and then
`save_session(&session_id)` using the *current* active session (B), not the originally
captured `session_id` for A.
6. As a result, only session B is persisted via `SessionManager::save_session` (called
inside `save_session`, see `lib.rs:142-156` in the final file), while the updated task
state for session A may not be flushed to disk immediately, leading to inconsistent or
stale task information for session A after restart.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** core/neurorift-core/src/lib.rs
**Line:** 303:305
**Comment:**
*Logic Error: After a task completes, the core saves whichever session is currently active instead of the session that actually owned the task, so if the active session changes during execution the task's updated state may be written to the wrong session or not persisted at all.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| @@ -118,31 +182,45 @@ async def handle_tool_execute(command: Dict[str, Any]) -> Dict[str, Any]: | |||
| "error": result.error, | |||
| } | |||
|
|
|||
| root = _session_root(session_id) | |||
| (root / "tool_output.txt").write_text(result.raw_output or "", encoding="utf-8") | |||
| (root / "tool_result.json").write_text( | |||
| json.dumps(payload, indent=2, default=str), encoding="utf-8" | |||
| ) | |||
| _append_audit( | |||
| session_id, | |||
| { | |||
| "type": "tool_execute", | |||
| "tool": tool_name, | |||
| "target": target, | |||
| "status": result.status, | |||
There was a problem hiding this comment.
Suggestion: The tool execution status field returned from the Python bridge does not match what the Rust core expects: ExecutionManager sets result.status to "success"/"failed"/"error", but in NeuroRiftCore::execute_task the Rust side only treats a status of "completed" as success, so with the current passthrough ("status": result.status) even successful tool runs are interpreted as failures, causing TaskFailed events to be emitted and TaskCompleted never to be triggered. To fix this, normalize the status in the bridge so that successful executions are reported as "completed" while preserving error statuses. [logic error]
Severity Level: Critical 🚨
- ❌ Web-mode tool runs always reported as failed.
- ⚠️ UI never receives TaskCompleted, only TaskFailed events.
- ⚠️ Session task history misrepresents successful executions as failed.
- ⚠️ Automations depending on completion status cannot function correctly.| # Normalize status for Rust core: treat successful runs as "completed" | |
| bridge_status = "completed" if result.status == "success" else result.status | |
| payload = { | |
| "tool_name": result.tool_name, | |
| "command": result.command, | |
| "status": bridge_status, | |
| "raw_output": result.raw_output, | |
| "structured_output": result.structured_output, | |
| "duration_seconds": result.duration_seconds, | |
| "error": result.error, | |
| } | |
| root = _session_root(session_id) | |
| (root / "tool_output.txt").write_text(result.raw_output or "", encoding="utf-8") | |
| (root / "tool_result.json").write_text( | |
| json.dumps(payload, indent=2, default=str), encoding="utf-8" | |
| ) | |
| _append_audit( | |
| session_id, | |
| { | |
| "type": "tool_execute", | |
| "tool": tool_name, | |
| "target": target, | |
| "status": bridge_status, |
Steps of Reproduction ✅
1. Start the Rust core binary built from this PR (`core/neurorift-core/src/main.rs:7-27`),
which initializes the WebSocket server and Python bridge (`NeuroRiftCore::new` in
`core/neurorift-core/src/lib.rs:35-53`).
2. Connect a WebSocket client to `ws://127.0.0.1:8765` (logged in `main.rs:35-37`) and
send a JSON message encoding `WSEvent::QueueTask` with a valid tool, e.g.
`{"type":"queue_task","tool_name":"nmap","target":"example.com","args":{}}` per event enum
in `core/neurorift-core/src/websocket/events.rs:144-148`.
3. Observe the command listener in `core/neurorift-core/src/main.rs:66-126` handling
`QueueTask { tool_name, target, args }` (lines 110-126), which calls
`core_cmd.queue_task(...)` and then spawns `core_exec.execute_task(&task_id)`
(`lib.rs:168-189` for `queue_task`, `lib.rs:208-231` for `execute_task`).
4. In `NeuroRiftCore::execute_task` (`core/neurorift-core/src/lib.rs:208-241`), the core
builds a Python command with `"type": "tool_execute"` and forwards it via
`PythonBridge::execute` (`core/neurorift-core/src/python_bridge/mod.rs:40-52`) to the
Python adapter `/execute` endpoint implemented in `modules/web/bridge_server.py:69-102`.
5. The FastAPI route `execute_command` in `modules/web/bridge_server.py:69-99` dispatches
to `handle_tool_execute` when `cmd_type == "tool_execute"` (`line 87`), which constructs
the `ScanRequest` and `SessionContext` and invokes `execution_manager.execute_tool(...)`
(`bridge_server.py:156-173`).
6. `ExecutionManager.execute_tool` (`modules/orchestration/execution_manager.py:37-112`)
runs the external tool; on normal success (process return code 0) it constructs
`ToolExecutionResult` with `status="success"` (`line 91`), or `"failed"`/`"error"` on
non-zero or exceptions (`lines 91, 103-111`).
7. `handle_tool_execute` currently forwards `result.status` directly into the JSON payload
(`"status": result.status` at `bridge_server.py:175-182`) and returns it wrapped in a
`Response(success=True, data=payload)` from `execute_command`, which
`PythonBridge::extract_data` treats as successful (`python_bridge/mod.rs:12-25`).
8. Back in `NeuroRiftCore::execute_task`, the core interprets the bridge payload: it reads
`data["status"]` and computes `success` as `eq_ignore_ascii_case("completed")`
(`lib.rs:248-258). With the current Python status `"success"`, this comparison returns
`false`, so `success` is `false` and the task is always marked failed.
9. As a result, even when the external tool executed successfully, the task is updated to
`TaskStatus::Failed` and `TaskFailed` is broadcast (`lib.rs:260-288`) instead of
`TaskStatus::Completed` with `TaskCompleted` (`lib.rs:273-282`), which the frontend
listens for to mark tasks as completed. This misalignment occurs for every successful
`tool_execute` call because the bridge never emits `"completed"`.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** modules/web/bridge_server.py
**Line:** 175:196
**Comment:**
*Logic Error: The tool execution status field returned from the Python bridge does not match what the Rust core expects: `ExecutionManager` sets `result.status` to `"success"`/`"failed"`/`"error"`, but in `NeuroRiftCore::execute_task` the Rust side only treats a status of `"completed"` as success, so with the current passthrough (`"status": result.status`) even successful tool runs are interpreted as failures, causing `TaskFailed` events to be emitted and `TaskCompleted` never to be triggered. To fix this, normalize the status in the bridge so that successful executions are reported as `"completed"` while preserving error statuses.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| }, | ||
| }; | ||
| function normalizeMode(mode?: string) { | ||
| if (mode?.toUpperCase() === 'DEFENSIVE') return 'DEFENSIVE'; |
There was a problem hiding this comment.
Suggestion: The mode-normalization helper currently treats any non-DEFENSIVE value (including a valid STEALTH operational mode) as OFFENSIVE, so sessions running in STEALTH mode will be misrepresented throughout the UI and any logic that branches on the mode will behave incorrectly. [logic error]
Severity Level: Major ⚠️
- ⚠️ Web tools catalog mis-gates tools for STEALTH sessions.
- ⚠️ Any mode-based UI logic treats STEALTH as OFFENSIVE.| if (mode?.toUpperCase() === 'DEFENSIVE') return 'DEFENSIVE'; | |
| const upper = mode?.toUpperCase(); | |
| if (upper === 'DEFENSIVE') return 'DEFENSIVE'; | |
| if (upper === 'STEALTH') return 'STEALTH'; |
Steps of Reproduction ✅
1. The backend WebSocket protocol defines sessions with an `OperationalMode` including
`'STEALTH'` (see `web-ui/src/lib/types.ts:1` where `OperationalMode = 'OFFENSIVE' |
'DEFENSIVE' | 'STEALTH';` and `core/neurorift-core/src/websocket/events.rs:14-17` where
`SessionLoaded` carries a `state: SessionState`).
2. When the core runtime sends a `SessionLoaded` event for a session whose `state.mode` is
`'STEALTH'`, the frontend WebSocket adapter surfaces this as an event with `type:
'session_loaded'` and a `state` payload consumed by `useNeuroRift` in
`web-ui/src/lib/hooks.ts:54-55`.
3. `useNeuroRift` calls `normalizeSession(event.state)` (`hooks.ts:55`), which in turn
calls `normalizeMode(state?.mode)` at `hooks.ts:20`. The current `normalizeMode`
implementation at `hooks.ts:7-10` only returns `'DEFENSIVE'` when the uppercased value
equals `'DEFENSIVE'`, and returns `'OFFENSIVE'` for all other values.
4. As a result, a backend session whose actual mode is `'STEALTH'` is stored in React
state as `session.mode === 'OFFENSIVE'`. Downstream consumers such as the Tools page
filter at `web-ui/src/app/tools/page.tsx:52-56` (which checks
`tool.allowed_modes.includes(session.mode)`) cannot distinguish STEALTH from OFFENSIVE and
will treat a STEALTH session as OFFENSIVE.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** web-ui/src/lib/hooks.ts
**Line:** 8:8
**Comment:**
*Logic Error: The mode-normalization helper currently treats any non-`DEFENSIVE` value (including a valid `STEALTH` operational mode) as `OFFENSIVE`, so sessions running in STEALTH mode will be misrepresented throughout the UI and any logic that branches on the mode will behave incorrectly.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| }, ...prev]); | ||
| break; | ||
| case 'system_health': | ||
| setSystemHealth(prev => ({ ...prev, cpu: event.cpu, memory: event.memory })); |
There was a problem hiding this comment.
Suggestion: The system_health websocket event handler only updates CPU and memory, leaving latency stuck at its initial value (0), so any UI that displays latency will always show a stale number even when the backend sends up-to-date latency metrics. [logic error]
Severity Level: Major ⚠️
- ⚠️ Command header latency indicator always shows 0ms.
- ⚠️ Operators see misleading runtime telemetry for latency.| setSystemHealth(prev => ({ ...prev, cpu: event.cpu, memory: event.memory })); | |
| setSystemHealth(prev => ({ | |
| ...prev, | |
| cpu: event.cpu, | |
| memory: event.memory, | |
| latency: event.latency ?? prev.latency, | |
| })); |
Steps of Reproduction ✅
1. The backend WebSocket protocol exposes a `SystemHealth` event carrying CPU and memory
metrics (see `core/neurorift-core/src/websocket/events.rs:100-103`, `SystemHealth { cpu:
f32, memory: f32, timestamp: DateTime<Utc> }`).
2. On the frontend, `useNeuroRift` initializes `systemHealth` state with a latency field
at `web-ui/src/lib/hooks.ts:45` as `{ cpu: 0, memory: 0, latency: 0 }`.
3. Incoming WebSocket events are handled in the `ws.subscribe` switch in `useNeuroRift`
(`hooks.ts:52-120`). For the `'system_health'` event, the handler at `hooks.ts:106-108`
only updates `cpu` and `memory` with `setSystemHealth(prev => ({ ...prev, cpu: event.cpu,
memory: event.memory }));`, leaving `latency` unchanged from its initial value.
4. The command center header displays CPU, memory, and latency using `systemHealth` in
`web-ui/src/components/webmode/CommandCenterFrame.tsx:35-38`, including `LAT
{systemHealth.latency.toFixed(0)}ms`. When the app is running and `SystemHealth` events
are emitted, CPU and MEM values visibly change while `LAT` remains fixed at `0ms`,
demonstrating that latency is never updated from events.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** web-ui/src/lib/hooks.ts
**Line:** 107:107
**Comment:**
*Logic Error: The `system_health` websocket event handler only updates CPU and memory, leaving latency stuck at its initial value (0), so any UI that displays latency will always show a stale number even when the backend sends up-to-date latency metrics.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| case 'finding_discovered': | ||
| setSession(prev => prev ? { ...prev, findings: [event.finding, ...prev.findings] } : prev); | ||
| break; |
There was a problem hiding this comment.
Suggestion: Live finding_discovered events append the raw event.finding into session state without normalizing fields (especially severity), unlike the initial session load path; this can produce inconsistent finding shapes and severity casing that break consumers which expect the normalized Finding format. [logic error]
Severity Level: Major ⚠️
- ⚠️ Vulnerabilities UI may show inconsistent severity labels/styles.
- ⚠️ Mixed normalized/unnormalized finding shapes complicate consumers.| case 'finding_discovered': | |
| setSession(prev => prev ? { ...prev, findings: [event.finding, ...prev.findings] } : prev); | |
| break; | |
| case 'finding_discovered': { | |
| const finding = event.finding; | |
| const normalizedFinding = { | |
| id: finding.id, | |
| title: finding.title, | |
| description: finding.description, | |
| severity: String(finding.severity).toUpperCase(), | |
| tool_source: finding.tool_source, | |
| discovered_at: finding.discovered_at, | |
| }; | |
| setSession(prev => (prev ? { ...prev, findings: [normalizedFinding, ...prev.findings] } : prev)); | |
| break; | |
| } |
Steps of Reproduction ✅
1. The backend WebSocket protocol defines a `FindingDiscovered` event that carries a
`finding: Finding` payload (see `core/neurorift-core/src/websocket/events.rs:86-89`). The
same `Finding` struct is used in session state and deltas.
2. On initial session load, `useNeuroRift` normalizes all findings in `normalizeSession`
(`web-ui/src/lib/hooks.ts:23-29`), explicitly mapping to the frontend `Finding` type
(`web-ui/src/lib/types.ts:21-27`) and forcing `severity` to uppercase with `severity:
String(finding.severity).toUpperCase()`.
3. Live incremental findings from the WebSocket are handled in the `'finding_discovered'`
case at `hooks.ts:115-117`, where the code does `setSession(prev => prev ? { ...prev,
findings: [event.finding, ...prev.findings] } : prev);`, inserting the raw `event.finding`
object without passing it through the same normalization logic.
4. Any component that consumes `session.findings` from `useNeuroRift` (for example, the
vulnerabilities view in `web-ui/src/app/vulnerabilities/page.tsx:3-8` which imports and
uses `useNeuroRift`) can then observe a mixed array where findings loaded with the session
have normalized severity values and shape, while findings added via `finding_discovered`
may retain backend-specific casing or fields, leading to inconsistent severity values and
potential rendering or styling discrepancies.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** web-ui/src/lib/hooks.ts
**Line:** 115:117
**Comment:**
*Logic Error: Live `finding_discovered` events append the raw `event.finding` into session state without normalizing fields (especially `severity`), unlike the initial session load path; this can produce inconsistent finding shapes and severity casing that break consumers which expect the normalized `Finding` format.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.|
CodeAnt AI finished reviewing your PR. |
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web-ui/src/components/webmode/panels/CommandPanel.tsx (1)
3-61:⚠️ Potential issue | 🟡 MinorClear the delayed assistant timer on unmount.
setTimeoutcan fire after unmount and attempt a state update. Add a ref + cleanup to avoid React warnings and leaks.🧹 Suggested fix
-import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; @@ export function CommandPanel() { const { controlMode, sendSignal } = useWebModeContext(); const [input, setInput] = useState(''); + const replyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); @@ - setTimeout(() => { + replyTimeoutRef.current = setTimeout(() => { setMessages(prev => [ { id: `${Date.now()}-assistant`, role: 'assistant', text: 'Intent accepted. Routing through policy lattice for plan negotiation.', timestamp: new Date().toLocaleTimeString(), }, ...prev ]); }, 700); }; + + useEffect(() => { + return () => { + if (replyTimeoutRef.current) { + clearTimeout(replyTimeoutRef.current); + } + }; + }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-ui/src/components/webmode/panels/CommandPanel.tsx` around lines 3 - 61, The setTimeout in submitMessage (inside CommandPanel) can fire after the component unmounts and try to update messages; store the timeout id in a ref (e.g., assistantTimeoutRef) when calling setTimeout, then add a useEffect cleanup that clears the timeout (clearTimeout(assistantTimeoutRef.current)) and optionally nulls the ref on unmount to prevent the delayed callback from running and causing setMessages after unmount. Ensure you reference submitMessage, assistantTimeoutRef (or similar), and the cleanup useEffect in CommandPanel so the delayed assistant update is cancelled on unmount.
🧹 Nitpick comments (8)
web-ui/src/components/webmode/panels/PermissionsPanel.tsx (1)
10-29: No empty state whenapprovalsis empty.When the
approvalsarray is empty, the component silently renders an empty<div>with no user feedback. A brief placeholder would improve discoverability.✨ Proposed empty-state addition
<div className="space-y-3"> + {approvals.length === 0 && ( + <p className="text-xs text-neuro-text-muted uppercase tracking-[0.3em]">No pending approvals</p> + )} {approvals.map(approval => (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-ui/src/components/webmode/panels/PermissionsPanel.tsx` around lines 10 - 29, The component currently maps over approvals and renders nothing when approvals is empty; add an explicit empty-state render before the approvals.map (or via conditional rendering around it) that shows a brief placeholder card/message (e.g., "No pending approvals" plus optional helper text or CTA) using the same styling container (rounded-xl border bg-neuro-bg/70 p-4) so layout stays consistent; update the render logic in PermissionsPanel to display this placeholder when approvals.length === 0 and keep the existing approvals.map rendering unchanged for non-empty arrays.modules/web/bridge_server.py (4)
216-216: Unused variableparams(Ruff F841).
paramsis assigned but never read. Prefix with_to silence the lint warning until the stub is implemented.Proposed fix
- params = command.get("params", {}) + _params = command.get("params", {})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/web/bridge_server.py` at line 216, The variable params is assigned from command.get("params", {}) but never used, triggering a linter warning; rename it to _params (or prefix with an underscore) where it's assigned (the line using params = command.get("params", {})) to indicate an intentionally unused variable until the stub is implemented, ensuring the lint warning (Ruff F841) is silenced while preserving the original intent.
42-42:datetime.utcnow()is deprecated since Python 3.12.
datetime.utcnow()is used here (line 42) and in audit entries (lines 133, 197). It returns a naive datetime and is deprecated in favor ofdatetime.now(datetime.timezone.utc), which returns a timezone-aware object.Proposed fix
-from datetime import datetime +from datetime import datetime, timezoneThen replace all occurrences:
- / datetime.utcnow().strftime("%Y%m%d_%H%M%S") + / datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")- "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/web/bridge_server.py` at line 42, Replace all uses of datetime.utcnow() (e.g., the call that builds the timestamp with .strftime and the audit entry timestamps) with timezone-aware datetime.now(datetime.timezone.utc) so the datetimes are not naive; update any imports to reference datetime.timezone (or import timezone from datetime) and keep existing .strftime(...) calls unchanged to preserve formatting; ensure the audit entry creation paths that currently call datetime.utcnow() are updated to use datetime.now(datetime.timezone.utc) consistently across bridge_server.py.
143-153: Unusedcommandparameter & inefficient call ordering.
commandis never referenced (flagged by Ruff ARG001). Rename it to_commandor use**_to signal intent.Also,
list_models()is called unconditionally beforeis_available(). If Ollama is down, that's a wasted network call. Flip the order to short-circuit:Proposed fix
-async def handle_ai_status(command: Dict[str, Any]) -> Dict[str, Any]: - models = await ollama.list_models() - available = await ollama.is_available() +async def handle_ai_status(_command: Dict[str, Any]) -> Dict[str, Any]: + available = await ollama.is_available() + models = await ollama.list_models() if available else [] selected_model = await ollama.get_best_model() if available else None🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/web/bridge_server.py` around lines 143 - 153, Rename the unused parameter in handle_ai_status from command to _command (or **_ ) to silence ARG001, and reorder the Ollama calls so you call ollama.is_available() first; if available then await ollama.list_models() and await ollama.get_best_model(), otherwise set models to [] and selected_model to None; update the returned "model_count" to len(models) and "needs_pull" to available and selected_model is None accordingly.
260-264: Migrate@app.on_event("startup")tolifespancontext manager.FastAPI deprecated
on_eventin favor of thelifespanparameter (FastAPI 0.93.0+). Consider migrating to the async context manager pattern:from contextlib import asynccontextmanager `@asynccontextmanager` async def lifespan(app: FastAPI): # startup logger.info("🐍 NeuroRift Python Bridge started") logger.info("📡 Listening on http://127.0.0.1:8766") yield # shutdown (if needed) app = FastAPI(lifespan=lifespan)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/web/bridge_server.py` around lines 260 - 264, The startup_event function registered with `@app.on_event`("startup") is deprecated; replace it by creating an async context manager named lifespan (e.g., use asynccontextmanager) that performs the same startup logging and yield for runtime, then pass that lifespan to FastAPI via app = FastAPI(lifespan=lifespan), and remove the `@app.on_event`("startup") startup_event registration; ensure the new lifespan performs logger.info("🐍 NeuroRift Python Bridge started") and logger.info("📡 Listening on http://127.0.0.1:8766") before yielding and include any necessary shutdown logic after the yield.web-ui/src/components/webmode/panels/MemoryPulsePanel.tsx (1)
8-27: Clamp meter values to avoid overflow/NaN.
Config-derived values aren’t guaranteed to be 0–1, so the progress bar can exceed 100% or render NaN. Consider clamping with a safe fallback.♻️ Proposed adjustment
- const meters = [ - { label: 'Short-term', value: config.memory.usage }, - { label: 'Episodic', value: config.memory.reinforcement }, - { label: 'Preference', value: config.memory.decay }, - { label: 'Risk Profile', value: config.stealth.level }, - ]; + const clamp01 = (value: number) => Math.min(1, Math.max(0, value)); + const meters = [ + { label: 'Short-term', value: clamp01(config.memory.usage ?? 0) }, + { label: 'Episodic', value: clamp01(config.memory.reinforcement ?? 0) }, + { label: 'Preference', value: clamp01(config.memory.decay ?? 0) }, + { label: 'Risk Profile', value: clamp01(config.stealth.level ?? 0) }, + ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-ui/src/components/webmode/panels/MemoryPulsePanel.tsx` around lines 8 - 27, The progress meter values derived in the meters array (used in MemoryPulsePanel.tsx) can be outside 0–1 or undefined, causing Math.round(meter.value * 100) to produce NaN or the style width `${meter.value * 100}%` to exceed 100%; fix this by normalizing/clamping meter.value before display and styling (e.g., compute a safeValue = Number.isFinite(meter.value) ? Math.max(0, Math.min(1, meter.value)) : 0) and then use safeValue for Math.round(safeValue * 100) and for the style width in the render; update references in the meters mapping and where Math.round(meter.value * 100) and style width `${meter.value * 100}%` are used.web-ui/src/lib/hooks.ts (1)
48-137: Avoid re-subscribing on every session update.
useEffect([session])re-sendsget_session_listand re-subscribes on every session change (findings, status updates, etc.). This is noisy and can trigger redundant load attempts. Consider a stable subscription and track session in a ref.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-ui/src/lib/hooks.ts` around lines 48 - 137, The effect in hooks.ts currently depends on session causing re-subscribe and repeated ws.send calls; change the useEffect dependency to [] so it runs once, keep the getWebSocket() and initial ws.send({ type: 'get_session_list' }) inside that single-run effect, and replace any direct reads of session inside the ws.subscribe handler and handleSessionList with a mutable ref (e.g., sessionRef) that you update inside setSession so handlers use sessionRef.current to decide whether to load or update; ensure all state setters (setSession, setTasks, setAgents, setApprovals, setSystemHealth, setTorConnected, setBrowserActive) continue to be used for updates but do not trigger re-subscribing.web-ui/src/components/webmode/panels/ConfigMatrixPanel.tsx (1)
25-58: No immediate issue, but add defensive defaults if persistence is planned.Currently,
defaultConfigdefines all paths inconfigSchema, and configs are not persisted between sessions. However, if persistence (localStorage, database, etc.) is added in the future, old configs could lack newly-added schema fields. If you plan to add persistence, consider applying type-safe fallbacks to keep inputs controlled and prevent "undefined" displays:🔧 Proposed fallback pattern (for future persistence)
- {section.fields.map(field => { - const value = getValue(config, field.path as string); + {section.fields.map(field => { + const rawValue = getValue(config, field.path as string); + const value = + rawValue ?? + (field.type === 'toggle' + ? false + : field.type === 'slider' + ? field.min ?? 0 + : field.options?.[0]?.value ?? ''); return (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-ui/src/components/webmode/panels/ConfigMatrixPanel.tsx` around lines 25 - 58, The UI reads values via getValue(config, field.path) without fallbacks which can render "undefined" if persisted configs are missing new fields; update the rendering logic to derive a safe controlled value by falling back to field.default and then to the canonical defaultConfig (use getValue(defaultConfig, field.path)) when getValue(config, ...) returns undefined, and ensure the resulting value is correctly typed for updateConfig calls and for display/inputs in ConfigMatrixPanel (references: getValue, updateConfig, config, defaultConfig, field.type).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@core/neurorift-core/src/lib.rs`:
- Around line 244-306: The code uses self.get_active_session() twice which can
pick a different session if the user switches while the task runs; capture the
originating session id from the first session (e.g. let session_id =
session.read().id.clone() before taking the write lock or before any await) and
then use that session_id to look up and modify the same session (via your
session lookup/get method) and to call self.save_session(&session_id) at the end
instead of re-calling get_active_session(); update usages around
get_active_session, session.write(), and the final save to consistently use the
captured session_id to ensure results are applied to the originating session.
- Around line 191-299: cancel_task only flips the local status but execute_task
doesn't check for cancelled state so a cancelled task can still run and
overwrite status; update execute_task to (1) check the task's status after
finding it and before sending the execute command (skip execution and broadcast
WSEvent::TaskCancelled if status == TaskStatus::Cancelled), (2) also check again
before applying the result (if task.status == Cancelled, ignore the result and
do not broadcast Completed/Failed), and (3) if supported, add a cancellation
handshake to the python bridge (e.g., send a "cancel_task" command with task_id
via python_bridge or call a new python_bridge.cancel(task_id)) to stop in-flight
execution; reference the task.status field, cancel_task and execute_task
functions, python_bridge.execute, and
WSEvent::TaskCancelled/TaskCompleted/TaskFailed to locate code to change.
In `@core/neurorift-core/src/main.rs`:
- Around line 127-131: The error message in the CancelTask handling is
incorrect: when CancelTask { task_id } calls core_cmd.cancel_task(&task_id) and
it returns Err(e), update the tracing::error call to log a cancellation-specific
message (e.g., "Failed to cancel task") and include the error details and
task_id; locate the CancelTask match arm and the core_cmd.cancel_task call to
change the tracing::error invocation accordingly.
In `@modules/web/bridge_server.py`:
- Around line 238-257: The startup health dictionary "checks" currently sets
keys bridge_security, rate_limiter_active, memory_firewall_active,
contribution_firewall_active, and mode_governor_active to True unconditionally,
which makes checks["ok"] always true; update the code that builds "checks" (the
block that creates the checks dict and computes checks["ok"]) to call the real
verification functions or state flags instead of hardcoded True (or explicitly
mark them as stubs with TODO comments), and if Ollama is required for
operational readiness include "ollama_available" in the checks["ok"] computation
(i.e., compute checks["ok"] = all(checks[k] for k in [..., "ollama_available"])
or replace list with the actual required keys); ensure the keys "ollama_models"
remains informational only.
- Around line 112-125: handle_ai_generate raises HTTPException (e.g., when
ollama.is_available(), selected_model is missing, or response is None) but
execute_command currently catches all Exceptions and converts them into a
generic Response, which swallows HTTP status codes; update execute_command to
re-raise caught HTTPException instances (i.e., detect isinstance(e,
HTTPException) and raise) so the original HTTP status/description propagates to
the client, or alternatively change the raises in handle_ai_generate to non-HTTP
exceptions if you prefer uniform error wrapping; locate execute_command and
handle_ai_generate in bridge_server.py and implement the re-raise logic for
HTTPException to preserve semantics.
- Around line 163-164: Change the default parsing of the command "mode" so
missing or malformed values default to DEFENSIVE: replace the current mode_raw =
str(command.get("mode", "OFFENSIVE")).upper() and mode assignment with logic
that uses "DEFENSIVE" as the default (e.g., mode_raw = str(command.get("mode",
"DEFENSIVE")).upper()) and/or explicitly validate mode_raw against known values
before mapping to ToolMode (use ToolMode.DEFENSIVE when invalid); update the
variables mode_raw and mode (the block that sets mode) to implement this safer
default.
- Around line 38-51: _session_root currently builds a new timestamped Path on
every call causing scattered session directories; change it to compute and cache
a single root per session_id (e.g., keep a module-level dict mapping session_id
-> Path initialized once) so repeated calls (from _append_audit and
handle_tool_execute) return the same directory; implement a single initializer
(or lazy cache in _session_root) that creates the timestamped folder only the
first time for a given session_id, reuse that cached Path thereafter, and ensure
_append_audit and any callers use the cached root.
- Around line 38-44: _session_root currently interpolates the user-controlled
session_id into a filesystem path; sanitize and validate session_id before use
(e.g., allow only alphanumeric, dash/underscore, or strip path separators and
dots) so values like "../../etc" cannot escape the sessions directory, then
construct the path and resolve it and assert the resulting Path is a descendant
of the intended root directory (the "~/.neurorift/sessions" base) before calling
base.mkdir; update _session_root to perform the validation/sanitization and the
final ancestor check to prevent path traversal.
In `@web-ui/src/app/tools/page.tsx`:
- Around line 35-46: The live output buffer appended in the useEffect handler
(handler -> setLiveOutput) is unbounded; modify the append logic to trim output
to a fixed maximum (e.g., MAX_OUTPUT_CHARS) per taskId so only the last N
characters are kept. Add a constant like MAX_OUTPUT_CHARS and, when building the
new value for [taskId] in setLiveOutput, concatenate prev[taskId] and chunk then
slice the result to the last MAX_OUTPUT_CHARS characters before storing; apply
this inside the same handler where taskId and chunk are used to ensure each
task's liveOutput is capped.
In `@web-ui/src/components/webmode/CommandCenterSurface.tsx`:
- Around line 31-46: The lastSignal value can be undefined and currently renders
as the literal "undefined"; update the JSX in the CommandCenterSurface component
(the div with className "text-sm text-neuro-text-secondary") to guard against
missing signals by displaying a safe fallback (e.g. an em dash or empty string)
when lastSignal is falsy or undefined—use a nullish coalescing or ternary check
(e.g., lastSignal ?? '—' or lastSignal ? lastSignal : '—') so the UI never shows
"undefined".
In `@web-ui/src/components/webmode/panels/AgentGraphPanel.tsx`:
- Around line 13-43: The rendering can throw when status?.last_update is
invalid; update the AgentGraphPanel mapping (the agentOrder.map block) to
defensively parse/validate status?.last_update before calling
toLocaleTimeString: check that status?.last_update is a finite timestamp or
valid Date (e.g., Number.isFinite(Date.parse(...)) or create a Date and test
isNaN(date.getTime())), and if invalid use Date.now() or a formatted "N/A"
fallback; replace the direct new Date(status?.last_update ??
Date.now()).toLocaleTimeString() call with this safe logic so rendering cannot
crash on bad timestamps.
In `@web-ui/src/components/webmode/panels/PermissionsPanel.tsx`:
- Around line 25-26: PermissionsPanel currently renders non-interactive <span>
elements for Approve/Reject; update it to render semantic <button> elements and
attach onClick handlers that call new action dispatchers exported from
useNeuroRift (e.g., approveTask and rejectTask). Add approveTask(taskId,
optionalMeta) and rejectTask(taskId, reason) to the useNeuroRift hook
implementation so they send the appropriate approval response over the existing
WebSocket connection (reuse the hook's socket/message sender) and update local
approvals state; ensure these functions are returned from useNeuroRift alongside
{ session, agents, tasks, approvals, torConnected, systemHealth, browserActive,
metrics, setTorConnected }. In PermissionsPanel.tsx wire the buttons to call
approveTask/rejectTask with the relevant task id, add accessible attributes
(type="button", aria-label) and keyboard focusability, and handle optimistic UI
updates or error handling based on the hook's promise/result.
In `@web-ui/src/lib/hooks.ts`:
- Around line 7-10: The function normalizeMode currently maps any non-DEFENSIVE
string (including "STEALTH") to "OFFENSIVE"; update normalizeMode to explicitly
recognize allowed modes (at least "DEFENSIVE", "OFFENSIVE", and "STEALTH") and
return the uppercased input when it matches one of those, otherwise fall back to
the safe default ("OFFENSIVE") — locate the normalizeMode function and replace
the simple toUpperCase check with a whitelist check against these mode names so
STEALTH is preserved instead of coerced.
In `@web-ui/src/lib/websocket.ts`:
- Around line 12-91: The instance can remain permanently disconnected because
close() sets NeuroRiftSocket.intentionallyClosed = true and getWebSocket()
doesn't trigger reconnect when an instance exists with a null ws; fix by
resetting intentionallyClosed at the start of connect() (e.g.,
this.intentionallyClosed = false inside NeuroRiftSocket.connect) and also update
getWebSocket() to call instance.connect() when instance exists but instance.ws
is null (so an existing instance will attempt reconnection instead of being left
inert).
---
Outside diff comments:
In `@web-ui/src/components/webmode/panels/CommandPanel.tsx`:
- Around line 3-61: The setTimeout in submitMessage (inside CommandPanel) can
fire after the component unmounts and try to update messages; store the timeout
id in a ref (e.g., assistantTimeoutRef) when calling setTimeout, then add a
useEffect cleanup that clears the timeout
(clearTimeout(assistantTimeoutRef.current)) and optionally nulls the ref on
unmount to prevent the delayed callback from running and causing setMessages
after unmount. Ensure you reference submitMessage, assistantTimeoutRef (or
similar), and the cleanup useEffect in CommandPanel so the delayed assistant
update is cancelled on unmount.
---
Nitpick comments:
In `@modules/web/bridge_server.py`:
- Line 216: The variable params is assigned from command.get("params", {}) but
never used, triggering a linter warning; rename it to _params (or prefix with an
underscore) where it's assigned (the line using params = command.get("params",
{})) to indicate an intentionally unused variable until the stub is implemented,
ensuring the lint warning (Ruff F841) is silenced while preserving the original
intent.
- Line 42: Replace all uses of datetime.utcnow() (e.g., the call that builds the
timestamp with .strftime and the audit entry timestamps) with timezone-aware
datetime.now(datetime.timezone.utc) so the datetimes are not naive; update any
imports to reference datetime.timezone (or import timezone from datetime) and
keep existing .strftime(...) calls unchanged to preserve formatting; ensure the
audit entry creation paths that currently call datetime.utcnow() are updated to
use datetime.now(datetime.timezone.utc) consistently across bridge_server.py.
- Around line 143-153: Rename the unused parameter in handle_ai_status from
command to _command (or **_ ) to silence ARG001, and reorder the Ollama calls so
you call ollama.is_available() first; if available then await
ollama.list_models() and await ollama.get_best_model(), otherwise set models to
[] and selected_model to None; update the returned "model_count" to len(models)
and "needs_pull" to available and selected_model is None accordingly.
- Around line 260-264: The startup_event function registered with
`@app.on_event`("startup") is deprecated; replace it by creating an async context
manager named lifespan (e.g., use asynccontextmanager) that performs the same
startup logging and yield for runtime, then pass that lifespan to FastAPI via
app = FastAPI(lifespan=lifespan), and remove the `@app.on_event`("startup")
startup_event registration; ensure the new lifespan performs logger.info("🐍
NeuroRift Python Bridge started") and logger.info("📡 Listening on
http://127.0.0.1:8766") before yielding and include any necessary shutdown logic
after the yield.
In `@web-ui/src/components/webmode/panels/ConfigMatrixPanel.tsx`:
- Around line 25-58: The UI reads values via getValue(config, field.path)
without fallbacks which can render "undefined" if persisted configs are missing
new fields; update the rendering logic to derive a safe controlled value by
falling back to field.default and then to the canonical defaultConfig (use
getValue(defaultConfig, field.path)) when getValue(config, ...) returns
undefined, and ensure the resulting value is correctly typed for updateConfig
calls and for display/inputs in ConfigMatrixPanel (references: getValue,
updateConfig, config, defaultConfig, field.type).
In `@web-ui/src/components/webmode/panels/MemoryPulsePanel.tsx`:
- Around line 8-27: The progress meter values derived in the meters array (used
in MemoryPulsePanel.tsx) can be outside 0–1 or undefined, causing
Math.round(meter.value * 100) to produce NaN or the style width `${meter.value *
100}%` to exceed 100%; fix this by normalizing/clamping meter.value before
display and styling (e.g., compute a safeValue = Number.isFinite(meter.value) ?
Math.max(0, Math.min(1, meter.value)) : 0) and then use safeValue for
Math.round(safeValue * 100) and for the style width in the render; update
references in the meters mapping and where Math.round(meter.value * 100) and
style width `${meter.value * 100}%` are used.
In `@web-ui/src/components/webmode/panels/PermissionsPanel.tsx`:
- Around line 10-29: The component currently maps over approvals and renders
nothing when approvals is empty; add an explicit empty-state render before the
approvals.map (or via conditional rendering around it) that shows a brief
placeholder card/message (e.g., "No pending approvals" plus optional helper text
or CTA) using the same styling container (rounded-xl border bg-neuro-bg/70 p-4)
so layout stays consistent; update the render logic in PermissionsPanel to
display this placeholder when approvals.length === 0 and keep the existing
approvals.map rendering unchanged for non-empty arrays.
In `@web-ui/src/lib/hooks.ts`:
- Around line 48-137: The effect in hooks.ts currently depends on session
causing re-subscribe and repeated ws.send calls; change the useEffect dependency
to [] so it runs once, keep the getWebSocket() and initial ws.send({ type:
'get_session_list' }) inside that single-run effect, and replace any direct
reads of session inside the ws.subscribe handler and handleSessionList with a
mutable ref (e.g., sessionRef) that you update inside setSession so handlers use
sessionRef.current to decide whether to load or update; ensure all state setters
(setSession, setTasks, setAgents, setApprovals, setSystemHealth,
setTorConnected, setBrowserActive) continue to be used for updates but do not
trigger re-subscribing.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
web-ui/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (25)
.gitignoreconfigs/neurorift_config.jsoncore/neurorift-core/src/lib.rscore/neurorift-core/src/main.rscore/neurorift-core/src/python_bridge/mod.rscore/neurorift-core/src/websocket/events.rsmodules/web/bridge_server.pyweb-ui/src/app/tools/page.tsxweb-ui/src/components/webmode/CommandCenterSurface.tsxweb-ui/src/components/webmode/PanelRegistry.tsxweb-ui/src/components/webmode/WebModeProvider.tsxweb-ui/src/components/webmode/panels/ActivityStreamPanel.tsxweb-ui/src/components/webmode/panels/AgentGraphPanel.tsxweb-ui/src/components/webmode/panels/CommandPanel.tsxweb-ui/src/components/webmode/panels/ConfigMatrixPanel.tsxweb-ui/src/components/webmode/panels/ExecutionTimelinePanel.tsxweb-ui/src/components/webmode/panels/MemoryPulsePanel.tsxweb-ui/src/components/webmode/panels/OnlineModePanel.tsxweb-ui/src/components/webmode/panels/PermissionsPanel.tsxweb-ui/src/lib/evolution.tsweb-ui/src/lib/hooks.tsweb-ui/src/lib/types.tsweb-ui/src/lib/webmode/schema.tsweb-ui/src/lib/webmode/types.tsweb-ui/src/lib/websocket.ts
💤 Files with no reviewable changes (2)
- .gitignore
- web-ui/src/components/webmode/PanelRegistry.tsx
| /// Cancel a queued/running task. | ||
| pub fn cancel_task(&self, task_id: &str) -> Result<()> { | ||
| if let Some(session) = self.get_active_session() { | ||
| let mut session = session.write(); | ||
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { | ||
| task.status = crate::state::TaskStatus::Cancelled; | ||
| task.completed_at = Some(chrono::Utc::now()); | ||
| session.touch(); | ||
| self.ws_server.broadcast(WSEvent::TaskCancelled { | ||
| task_id: task_id.to_string(), | ||
| timestamp: chrono::Utc::now(), | ||
| }); | ||
| } | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Execute a task by ID through the Python bridge. | ||
| pub async fn execute_task(&self, task_id: &str) -> Result<()> { | ||
| let (session_id, tool_name, target, args, mode) = if let Some(session) = self.get_active_session() { | ||
| let mut session = session.write(); | ||
| let session_id = session.id.clone(); | ||
| let mode = session.mode; | ||
|
|
||
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { | ||
| task.status = crate::state::TaskStatus::Running; | ||
| task.started_at = Some(chrono::Utc::now()); | ||
| let args = serde_json::Value::Object(task.args.clone().into_iter().collect()); | ||
| let tool_name = task.tool_name.clone(); | ||
| let target = task.target.clone(); | ||
| self.ws_server.broadcast(WSEvent::TaskStarted { | ||
| task_id: task.id.clone(), | ||
| started_at: chrono::Utc::now(), | ||
| }); | ||
| (session_id, tool_name, target, args, mode) | ||
| } else { | ||
| return Ok(()); | ||
| } | ||
| } else { | ||
| return Ok(()); | ||
| }; | ||
|
|
||
| let command = serde_json::json!({ | ||
| "type": "tool_execute", | ||
| "tool": tool_name, | ||
| "target": target, | ||
| "args": args, | ||
| "mode": format!("{:?}", mode).to_uppercase(), | ||
| "session_id": session_id, | ||
| }); | ||
|
|
||
| let result = self.python_bridge.execute(command).await; | ||
|
|
||
| if let Some(session) = self.get_active_session() { | ||
| let mut session = session.write(); | ||
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { | ||
| match result { | ||
| Ok(data) => { | ||
| let raw_output = data | ||
| .get("raw_output") | ||
| .and_then(|v| v.as_str()) | ||
| .unwrap_or_default() | ||
| .to_string(); | ||
| let success = data | ||
| .get("status") | ||
| .and_then(|v| v.as_str()) | ||
| .map(|s| s.eq_ignore_ascii_case("completed")) | ||
| .unwrap_or(true); | ||
|
|
||
| task.status = if success { | ||
| crate::state::TaskStatus::Completed | ||
| } else { | ||
| crate::state::TaskStatus::Failed | ||
| }; | ||
| task.completed_at = Some(chrono::Utc::now()); | ||
| session.touch(); | ||
|
|
||
| self.ws_server.broadcast(WSEvent::TaskOutput { | ||
| task_id: task_id.to_string(), | ||
| chunk: raw_output.clone(), | ||
| }); | ||
|
|
||
| if success { | ||
| self.ws_server.broadcast(WSEvent::TaskCompleted { | ||
| task_id: task_id.to_string(), | ||
| result: crate::websocket::events::TaskResult { | ||
| success: true, | ||
| output: raw_output, | ||
| structured_data: data.get("structured_output").cloned(), | ||
| duration_ms: (data.get("duration_seconds").and_then(|v| v.as_f64()).unwrap_or(0.0) * 1000.0) as u64, | ||
| }, | ||
| }); | ||
| } else { | ||
| self.ws_server.broadcast(WSEvent::TaskFailed { | ||
| task_id: task_id.to_string(), | ||
| error: data.get("error").and_then(|v| v.as_str()).unwrap_or("tool execution failed").to_string(), | ||
| }); | ||
| } | ||
| } | ||
| Err(error) => { | ||
| task.status = crate::state::TaskStatus::Failed; | ||
| task.completed_at = Some(chrono::Utc::now()); | ||
| session.touch(); | ||
| self.ws_server.broadcast(WSEvent::TaskFailed { | ||
| task_id: task_id.to_string(), | ||
| error: error.to_string(), | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Cancellation is state-only; running tasks can still execute and overwrite status.
cancel_task only flips the task status locally. execute_task doesn’t check for Cancelled before running (or before applying results), so a task can still execute and emit TaskCompleted/Failed after cancellation. Consider guarding against cancelled tasks and—if supported—signaling the Python bridge to stop in-flight execution.
🛠️ Suggested guard before execution
- if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) {
+ if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) {
+ if task.status == crate::state::TaskStatus::Cancelled {
+ return Ok(());
+ }
task.status = crate::state::TaskStatus::Running;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/neurorift-core/src/lib.rs` around lines 191 - 299, cancel_task only
flips the local status but execute_task doesn't check for cancelled state so a
cancelled task can still run and overwrite status; update execute_task to (1)
check the task's status after finding it and before sending the execute command
(skip execution and broadcast WSEvent::TaskCancelled if status ==
TaskStatus::Cancelled), (2) also check again before applying the result (if
task.status == Cancelled, ignore the result and do not broadcast
Completed/Failed), and (3) if supported, add a cancellation handshake to the
python bridge (e.g., send a "cancel_task" command with task_id via python_bridge
or call a new python_bridge.cancel(task_id)) to stop in-flight execution;
reference the task.status field, cancel_task and execute_task functions,
python_bridge.execute, and WSEvent::TaskCancelled/TaskCompleted/TaskFailed to
locate code to change.
| if let Some(session) = self.get_active_session() { | ||
| let mut session = session.write(); | ||
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { | ||
| match result { | ||
| Ok(data) => { | ||
| let raw_output = data | ||
| .get("raw_output") | ||
| .and_then(|v| v.as_str()) | ||
| .unwrap_or_default() | ||
| .to_string(); | ||
| let success = data | ||
| .get("status") | ||
| .and_then(|v| v.as_str()) | ||
| .map(|s| s.eq_ignore_ascii_case("completed")) | ||
| .unwrap_or(true); | ||
|
|
||
| task.status = if success { | ||
| crate::state::TaskStatus::Completed | ||
| } else { | ||
| crate::state::TaskStatus::Failed | ||
| }; | ||
| task.completed_at = Some(chrono::Utc::now()); | ||
| session.touch(); | ||
|
|
||
| self.ws_server.broadcast(WSEvent::TaskOutput { | ||
| task_id: task_id.to_string(), | ||
| chunk: raw_output.clone(), | ||
| }); | ||
|
|
||
| if success { | ||
| self.ws_server.broadcast(WSEvent::TaskCompleted { | ||
| task_id: task_id.to_string(), | ||
| result: crate::websocket::events::TaskResult { | ||
| success: true, | ||
| output: raw_output, | ||
| structured_data: data.get("structured_output").cloned(), | ||
| duration_ms: (data.get("duration_seconds").and_then(|v| v.as_f64()).unwrap_or(0.0) * 1000.0) as u64, | ||
| }, | ||
| }); | ||
| } else { | ||
| self.ws_server.broadcast(WSEvent::TaskFailed { | ||
| task_id: task_id.to_string(), | ||
| error: data.get("error").and_then(|v| v.as_str()).unwrap_or("tool execution failed").to_string(), | ||
| }); | ||
| } | ||
| } | ||
| Err(error) => { | ||
| task.status = crate::state::TaskStatus::Failed; | ||
| task.completed_at = Some(chrono::Utc::now()); | ||
| session.touch(); | ||
| self.ws_server.broadcast(WSEvent::TaskFailed { | ||
| task_id: task_id.to_string(), | ||
| error: error.to_string(), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if let Some(session) = self.get_active_session() { | ||
| let session_id = session.read().id.clone(); | ||
| let _ = self.save_session(&session_id); | ||
| } |
There was a problem hiding this comment.
Use the originating session_id when applying results and saving.
After execution completes, you re-fetch the active session (Line 244 and Line 303). If the user switches sessions while the task runs, results can be written to the wrong session (or dropped). Use the captured session_id to look up and save the correct session.
✅ Suggested fix (use captured session_id)
- if let Some(session) = self.get_active_session() {
- let mut session = session.write();
+ if let Some(session_ref) = self.sessions.get(&session_id) {
+ let mut session = session_ref.value().write();
if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) {
match result {
Ok(data) => {
let raw_output = data
@@
}
}
}
- if let Some(session) = self.get_active_session() {
- let session_id = session.read().id.clone();
- let _ = self.save_session(&session_id);
- }
+ let _ = self.save_session(&session_id);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if let Some(session) = self.get_active_session() { | |
| let mut session = session.write(); | |
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { | |
| match result { | |
| Ok(data) => { | |
| let raw_output = data | |
| .get("raw_output") | |
| .and_then(|v| v.as_str()) | |
| .unwrap_or_default() | |
| .to_string(); | |
| let success = data | |
| .get("status") | |
| .and_then(|v| v.as_str()) | |
| .map(|s| s.eq_ignore_ascii_case("completed")) | |
| .unwrap_or(true); | |
| task.status = if success { | |
| crate::state::TaskStatus::Completed | |
| } else { | |
| crate::state::TaskStatus::Failed | |
| }; | |
| task.completed_at = Some(chrono::Utc::now()); | |
| session.touch(); | |
| self.ws_server.broadcast(WSEvent::TaskOutput { | |
| task_id: task_id.to_string(), | |
| chunk: raw_output.clone(), | |
| }); | |
| if success { | |
| self.ws_server.broadcast(WSEvent::TaskCompleted { | |
| task_id: task_id.to_string(), | |
| result: crate::websocket::events::TaskResult { | |
| success: true, | |
| output: raw_output, | |
| structured_data: data.get("structured_output").cloned(), | |
| duration_ms: (data.get("duration_seconds").and_then(|v| v.as_f64()).unwrap_or(0.0) * 1000.0) as u64, | |
| }, | |
| }); | |
| } else { | |
| self.ws_server.broadcast(WSEvent::TaskFailed { | |
| task_id: task_id.to_string(), | |
| error: data.get("error").and_then(|v| v.as_str()).unwrap_or("tool execution failed").to_string(), | |
| }); | |
| } | |
| } | |
| Err(error) => { | |
| task.status = crate::state::TaskStatus::Failed; | |
| task.completed_at = Some(chrono::Utc::now()); | |
| session.touch(); | |
| self.ws_server.broadcast(WSEvent::TaskFailed { | |
| task_id: task_id.to_string(), | |
| error: error.to_string(), | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| if let Some(session) = self.get_active_session() { | |
| let session_id = session.read().id.clone(); | |
| let _ = self.save_session(&session_id); | |
| } | |
| if let Some(session_ref) = self.sessions.get(&session_id) { | |
| let mut session = session_ref.value().write(); | |
| if let Some(task) = session.task_queue.iter_mut().find(|task| task.id == task_id) { | |
| match result { | |
| Ok(data) => { | |
| let raw_output = data | |
| .get("raw_output") | |
| .and_then(|v| v.as_str()) | |
| .unwrap_or_default() | |
| .to_string(); | |
| let success = data | |
| .get("status") | |
| .and_then(|v| v.as_str()) | |
| .map(|s| s.eq_ignore_ascii_case("completed")) | |
| .unwrap_or(true); | |
| task.status = if success { | |
| crate::state::TaskStatus::Completed | |
| } else { | |
| crate::state::TaskStatus::Failed | |
| }; | |
| task.completed_at = Some(chrono::Utc::now()); | |
| session.touch(); | |
| self.ws_server.broadcast(WSEvent::TaskOutput { | |
| task_id: task_id.to_string(), | |
| chunk: raw_output.clone(), | |
| }); | |
| if success { | |
| self.ws_server.broadcast(WSEvent::TaskCompleted { | |
| task_id: task_id.to_string(), | |
| result: crate::websocket::events::TaskResult { | |
| success: true, | |
| output: raw_output, | |
| structured_data: data.get("structured_output").cloned(), | |
| duration_ms: (data.get("duration_seconds").and_then(|v| v.as_f64()).unwrap_or(0.0) * 1000.0) as u64, | |
| }, | |
| }); | |
| } else { | |
| self.ws_server.broadcast(WSEvent::TaskFailed { | |
| task_id: task_id.to_string(), | |
| error: data.get("error").and_then(|v| v.as_str()).unwrap_or("tool execution failed").to_string(), | |
| }); | |
| } | |
| } | |
| Err(error) => { | |
| task.status = crate::state::TaskStatus::Failed; | |
| task.completed_at = Some(chrono::Utc::now()); | |
| session.touch(); | |
| self.ws_server.broadcast(WSEvent::TaskFailed { | |
| task_id: task_id.to_string(), | |
| error: error.to_string(), | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| let _ = self.save_session(&session_id); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/neurorift-core/src/lib.rs` around lines 244 - 306, The code uses
self.get_active_session() twice which can pick a different session if the user
switches while the task runs; capture the originating session id from the first
session (e.g. let session_id = session.read().id.clone() before taking the write
lock or before any await) and then use that session_id to look up and modify the
same session (via your session lookup/get method) and to call
self.save_session(&session_id) at the end instead of re-calling
get_active_session(); update usages around get_active_session, session.write(),
and the final save to consistently use the captured session_id to ensure results
are applied to the originating session.
| CancelTask { task_id } => { | ||
| tracing::info!("Received CancelTask: {}", task_id); | ||
| if let Err(e) = core_cmd.cancel_task(&task_id) { | ||
| tracing::error!("Failed to queue task: {}", e); | ||
| } |
There was a problem hiding this comment.
Fix incorrect error message in cancel path.
The cancellation error log still says “Failed to queue task,” which is misleading when debugging.
📝 Suggested fix
- if let Err(e) = core_cmd.cancel_task(&task_id) {
- tracing::error!("Failed to queue task: {}", e);
+ if let Err(e) = core_cmd.cancel_task(&task_id) {
+ tracing::error!("Failed to cancel task: {}", e);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CancelTask { task_id } => { | |
| tracing::info!("Received CancelTask: {}", task_id); | |
| if let Err(e) = core_cmd.cancel_task(&task_id) { | |
| tracing::error!("Failed to queue task: {}", e); | |
| } | |
| CancelTask { task_id } => { | |
| tracing::info!("Received CancelTask: {}", task_id); | |
| if let Err(e) = core_cmd.cancel_task(&task_id) { | |
| tracing::error!("Failed to cancel task: {}", e); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/neurorift-core/src/main.rs` around lines 127 - 131, The error message in
the CancelTask handling is incorrect: when CancelTask { task_id } calls
core_cmd.cancel_task(&task_id) and it returns Err(e), update the tracing::error
call to log a cancellation-specific message (e.g., "Failed to cancel task") and
include the error details and task_id; locate the CancelTask match arm and the
core_cmd.cancel_task call to change the tracing::error invocation accordingly.
| def _session_root(session_id: str) -> Path: | ||
| base = ( | ||
| Path(os.path.expanduser("~/.neurorift/sessions")) | ||
| / (session_id or "default") | ||
| / datetime.utcnow().strftime("%Y%m%d_%H%M%S") | ||
| ) | ||
| base.mkdir(parents=True, exist_ok=True) | ||
| return base | ||
|
|
||
|
|
||
| def _append_audit(session_id: str, payload: Dict[str, Any]) -> None: | ||
| root = _session_root(session_id) | ||
| with (root / "audit.jsonl").open("a", encoding="utf-8") as handle: | ||
| handle.write(json.dumps(payload) + "\n") |
There was a problem hiding this comment.
Critical bug: _session_root generates a unique directory on every call, breaking audit continuity and file co-location.
Because datetime.utcnow().strftime(...) is evaluated on each invocation, every call to _session_root (potentially) returns a different path. This has two cascading effects:
_append_auditnever truly appends — each call creates a fresh directory with a one-lineaudit.jsonl, so session audit history is scattered across many timestamped folders.- Tool output and its audit entry land in different directories — in
handle_tool_execute(line 185 vs line 190), two separate calls to_session_rootlikely produce two different paths.
The timestamp should be part of the session initialization (cached per session_id), not regenerated on every helper call.
Proposed fix: cache session root per session_id
+_session_roots: Dict[str, Path] = {}
+
def _session_root(session_id: str) -> Path:
- base = (
- Path(os.path.expanduser("~/.neurorift/sessions"))
- / (session_id or "default")
- / datetime.utcnow().strftime("%Y%m%d_%H%M%S")
- )
- base.mkdir(parents=True, exist_ok=True)
- return base
+ sid = session_id or "default"
+ if sid not in _session_roots:
+ base = (
+ Path(os.path.expanduser("~/.neurorift/sessions"))
+ / sid
+ / datetime.utcnow().strftime("%Y%m%d_%H%M%S")
+ )
+ base.mkdir(parents=True, exist_ok=True)
+ _session_roots[sid] = base
+ return _session_roots[sid]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@modules/web/bridge_server.py` around lines 38 - 51, _session_root currently
builds a new timestamped Path on every call causing scattered session
directories; change it to compute and cache a single root per session_id (e.g.,
keep a module-level dict mapping session_id -> Path initialized once) so
repeated calls (from _append_audit and handle_tool_execute) return the same
directory; implement a single initializer (or lazy cache in _session_root) that
creates the timestamped folder only the first time for a given session_id, reuse
that cached Path thereafter, and ensure _append_audit and any callers use the
cached root.
| def _session_root(session_id: str) -> Path: | ||
| base = ( | ||
| Path(os.path.expanduser("~/.neurorift/sessions")) | ||
| / (session_id or "default") | ||
| / datetime.utcnow().strftime("%Y%m%d_%H%M%S") | ||
| ) | ||
| base.mkdir(parents=True, exist_ok=True) |
There was a problem hiding this comment.
Path traversal risk: session_id is used unsanitized in filesystem path construction.
session_id originates from user-supplied command data (e.g., line 110, 162) and is interpolated directly into a path. A crafted value like "../../etc" or "default/../../root" would escape the intended sessions directory.
Sanitize by stripping path separators and special components, or validate against an allowlist pattern.
Proposed fix: sanitize session_id
+import re
+
+def _sanitize_session_id(session_id: str) -> str:
+ """Strip anything that could escape the sessions directory."""
+ sid = session_id or "default"
+ # Allow only alphanumeric, hyphens, and underscores
+ sid = re.sub(r"[^a-zA-Z0-9_\-]", "_", sid)
+ return sid or "default"
+
def _session_root(session_id: str) -> Path:
+ sid = _sanitize_session_id(session_id)
base = (
Path(os.path.expanduser("~/.neurorift/sessions"))
- / (session_id or "default")
+ / sid
/ datetime.utcnow().strftime("%Y%m%d_%H%M%S")
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@modules/web/bridge_server.py` around lines 38 - 44, _session_root currently
interpolates the user-controlled session_id into a filesystem path; sanitize and
validate session_id before use (e.g., allow only alphanumeric, dash/underscore,
or strip path separators and dots) so values like "../../etc" cannot escape the
sessions directory, then construct the path and resolve it and assert the
resulting Path is a descendant of the intended root directory (the
"~/.neurorift/sessions" base) before calling base.mkdir; update _session_root to
perform the validation/sanitization and the final ancestor check to prevent path
traversal.
| <div className="mb-6 flex flex-col gap-3"> | ||
| <div className="flex flex-wrap items-center gap-3"> | ||
| <span className="text-xs uppercase tracking-[0.4em] text-neuro-text-muted">Phase</span> | ||
| <span className="px-3 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60 text-xs">{phase}</span> | ||
| <span className="px-3 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60 text-xs">{controlMode.toUpperCase()} MODE</span> | ||
| <span className={cn( | ||
| "px-2 py-1 rounded bg-black/30 border border-white/5", | ||
| adapterMode === 'REAL' ? "text-emerald-400 border-emerald-500/20" : "text-amber-400 border-amber-500/20" | ||
| 'px-3 py-1 rounded-full border text-xs', | ||
| deviceTier === 'mobile' && 'border-cyan-400/60 text-cyan-200', | ||
| deviceTier === 'tablet' && 'border-indigo-400/60 text-indigo-200', | ||
| deviceTier === 'desktop' && 'border-emerald-400/60 text-emerald-200', | ||
| deviceTier === 'wide' && 'border-purple-400/60 text-purple-200' | ||
| )}> | ||
| {adapterMode} MODE | ||
| {deviceTier.toUpperCase()} TIER | ||
| </span> | ||
| <span className="text-neuro-text-muted">{lastSignal || 'System Ready'}</span> | ||
| </div> | ||
| <div className="text-sm text-neuro-text-secondary">{lastSignal}</div> |
There was a problem hiding this comment.
Guard lastSignal to avoid rendering “undefined”.
Line 46 will surface undefined/empty text if no signal exists yet, which reads like a glitch. A small fallback keeps the strip polished.
💡 Suggested fallback
- const policyContext = {
+ const policyContext = {
deviceTier,
controlMode,
sessionActive: Boolean(session),
};
+
+ const signalLabel = lastSignal?.trim() ? lastSignal : 'Awaiting signal…';- <div className="text-sm text-neuro-text-secondary">{lastSignal}</div>
+ <div className="text-sm text-neuro-text-secondary">{signalLabel}</div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/components/webmode/CommandCenterSurface.tsx` around lines 31 - 46,
The lastSignal value can be undefined and currently renders as the literal
"undefined"; update the JSX in the CommandCenterSurface component (the div with
className "text-sm text-neuro-text-secondary") to guard against missing signals
by displaying a safe fallback (e.g. an em dash or empty string) when lastSignal
is falsy or undefined—use a nullish coalescing or ternary check (e.g.,
lastSignal ?? '—' or lastSignal ? lastSignal : '—') so the UI never shows
"undefined".
| {agentOrder.map(agent => { | ||
| const status = agents[agent]; | ||
| return ( | ||
| <div | ||
| key={name} | ||
| key={agent} | ||
| className={cn( | ||
| "absolute transform -translate-x-1/2 -translate-y-1/2 transition-all duration-500", | ||
| "flex flex-col items-center justify-center p-2 rounded-lg border backdrop-blur-sm cursor-help z-10", | ||
| isActive ? "bg-neuro-surface/90 border-cyan-400/50 shadow-[0_0_15px_-3px_rgba(34,211,238,0.2)]" : "bg-neuro-bg/80 border-neuro-border/60", | ||
| isError && "border-rose-500/80 shadow-[0_0_10px_rgba(244,63,94,0.3)]" | ||
| 'rounded-xl border px-4 py-3 bg-neuro-bg/70 text-sm', | ||
| status?.state === 'executing' && 'border-emerald-400/40', | ||
| status?.state === 'planning' && 'border-cyan-400/40', | ||
| status?.state === 'error' && 'border-rose-400/50' | ||
| )} | ||
| style={{ left: `${pos.x}%`, top: `${pos.y}%` }} | ||
| > | ||
| <div className="text-[10px] uppercase tracking-widest text-neuro-text-muted mb-1">{name}</div> | ||
|
|
||
| {/* Status Indicator */} | ||
| <div className="flex items-center gap-2"> | ||
| <div className={cn( | ||
| "w-1.5 h-1.5 rounded-full", | ||
| status.state === 'executing' && "bg-emerald-400 animate-pulse", | ||
| status.state === 'planning' && "bg-cyan-400 animate-pulse", | ||
| status.state === 'error' && "bg-rose-500", | ||
| status.state === 'idle' && "bg-slate-600" | ||
| )} /> | ||
| <div className="flex items-center justify-between"> | ||
| <span className="text-xs uppercase tracking-[0.3em] text-neuro-text-muted">{agent}</span> | ||
| <span className={cn( | ||
| "text-xs font-medium", | ||
| isActive ? "text-neuro-text-primary" : "text-neuro-text-muted", | ||
| isError && "text-rose-300" | ||
| 'text-[10px] uppercase tracking-[0.2em]', | ||
| status?.state === 'executing' && 'text-emerald-200', | ||
| status?.state === 'planning' && 'text-cyan-200', | ||
| status?.state === 'error' && 'text-rose-200', | ||
| status?.state === 'idle' && 'text-neuro-text-muted' | ||
| )}> | ||
| {status.state} | ||
| {status?.state || 'idle'} | ||
| </span> | ||
| </div> | ||
|
|
||
| {/* Hover Info (Introspection) */} | ||
| <div className="absolute top-full mt-2 w-48 opacity-0 hover:opacity-100 transition-opacity pointer-events-none bg-black/90 border border-neuro-border text-[10px] p-2 rounded shadow-xl z-20 text-neuro-text-secondary"> | ||
| <div>Task: {status.current_task || 'Idle'}</div> | ||
| <div>Queue: {status.queue_depth}</div> | ||
| <div>Signal: {(status.signal_strength || 0).toFixed(2)}</div> | ||
| <p className="mt-2 text-neuro-text-primary text-sm"> | ||
| {status?.current_task || 'Awaiting orchestration signals.'} | ||
| </p> | ||
| <div className="mt-3 flex items-center justify-between text-[10px] uppercase tracking-[0.2em] text-neuro-text-muted"> | ||
| <span>Queue {status?.queue_depth ?? 0}</span> | ||
| <span>{new Date(status?.last_update ?? Date.now()).toLocaleTimeString()}</span> | ||
| </div> |
There was a problem hiding this comment.
Guard against invalid last_update timestamps.
new Date(invalid).toLocaleTimeString() can throw and break rendering. Add a safe fallback.
🛡️ Suggested fix
- return (
+ const lastUpdate = status?.last_update ? new Date(status.last_update) : null;
+ const lastUpdateLabel =
+ lastUpdate && !Number.isNaN(lastUpdate.getTime())
+ ? lastUpdate.toLocaleTimeString()
+ : '—';
+ return (
@@
- <span>{new Date(status?.last_update ?? Date.now()).toLocaleTimeString()}</span>
+ <span>{lastUpdateLabel}</span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {agentOrder.map(agent => { | |
| const status = agents[agent]; | |
| return ( | |
| <div | |
| key={name} | |
| key={agent} | |
| className={cn( | |
| "absolute transform -translate-x-1/2 -translate-y-1/2 transition-all duration-500", | |
| "flex flex-col items-center justify-center p-2 rounded-lg border backdrop-blur-sm cursor-help z-10", | |
| isActive ? "bg-neuro-surface/90 border-cyan-400/50 shadow-[0_0_15px_-3px_rgba(34,211,238,0.2)]" : "bg-neuro-bg/80 border-neuro-border/60", | |
| isError && "border-rose-500/80 shadow-[0_0_10px_rgba(244,63,94,0.3)]" | |
| 'rounded-xl border px-4 py-3 bg-neuro-bg/70 text-sm', | |
| status?.state === 'executing' && 'border-emerald-400/40', | |
| status?.state === 'planning' && 'border-cyan-400/40', | |
| status?.state === 'error' && 'border-rose-400/50' | |
| )} | |
| style={{ left: `${pos.x}%`, top: `${pos.y}%` }} | |
| > | |
| <div className="text-[10px] uppercase tracking-widest text-neuro-text-muted mb-1">{name}</div> | |
| {/* Status Indicator */} | |
| <div className="flex items-center gap-2"> | |
| <div className={cn( | |
| "w-1.5 h-1.5 rounded-full", | |
| status.state === 'executing' && "bg-emerald-400 animate-pulse", | |
| status.state === 'planning' && "bg-cyan-400 animate-pulse", | |
| status.state === 'error' && "bg-rose-500", | |
| status.state === 'idle' && "bg-slate-600" | |
| )} /> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs uppercase tracking-[0.3em] text-neuro-text-muted">{agent}</span> | |
| <span className={cn( | |
| "text-xs font-medium", | |
| isActive ? "text-neuro-text-primary" : "text-neuro-text-muted", | |
| isError && "text-rose-300" | |
| 'text-[10px] uppercase tracking-[0.2em]', | |
| status?.state === 'executing' && 'text-emerald-200', | |
| status?.state === 'planning' && 'text-cyan-200', | |
| status?.state === 'error' && 'text-rose-200', | |
| status?.state === 'idle' && 'text-neuro-text-muted' | |
| )}> | |
| {status.state} | |
| {status?.state || 'idle'} | |
| </span> | |
| </div> | |
| {/* Hover Info (Introspection) */} | |
| <div className="absolute top-full mt-2 w-48 opacity-0 hover:opacity-100 transition-opacity pointer-events-none bg-black/90 border border-neuro-border text-[10px] p-2 rounded shadow-xl z-20 text-neuro-text-secondary"> | |
| <div>Task: {status.current_task || 'Idle'}</div> | |
| <div>Queue: {status.queue_depth}</div> | |
| <div>Signal: {(status.signal_strength || 0).toFixed(2)}</div> | |
| <p className="mt-2 text-neuro-text-primary text-sm"> | |
| {status?.current_task || 'Awaiting orchestration signals.'} | |
| </p> | |
| <div className="mt-3 flex items-center justify-between text-[10px] uppercase tracking-[0.2em] text-neuro-text-muted"> | |
| <span>Queue {status?.queue_depth ?? 0}</span> | |
| <span>{new Date(status?.last_update ?? Date.now()).toLocaleTimeString()}</span> | |
| </div> | |
| {agentOrder.map(agent => { | |
| const status = agents[agent]; | |
| const lastUpdate = status?.last_update ? new Date(status.last_update) : null; | |
| const lastUpdateLabel = | |
| lastUpdate && !Number.isNaN(lastUpdate.getTime()) | |
| ? lastUpdate.toLocaleTimeString() | |
| : '—'; | |
| return ( | |
| <div | |
| key={agent} | |
| className={cn( | |
| 'rounded-xl border px-4 py-3 bg-neuro-bg/70 text-sm', | |
| status?.state === 'executing' && 'border-emerald-400/40', | |
| status?.state === 'planning' && 'border-cyan-400/40', | |
| status?.state === 'error' && 'border-rose-400/50' | |
| )} | |
| > | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs uppercase tracking-[0.3em] text-neuro-text-muted">{agent}</span> | |
| <span className={cn( | |
| 'text-[10px] uppercase tracking-[0.2em]', | |
| status?.state === 'executing' && 'text-emerald-200', | |
| status?.state === 'planning' && 'text-cyan-200', | |
| status?.state === 'error' && 'text-rose-200', | |
| status?.state === 'idle' && 'text-neuro-text-muted' | |
| )}> | |
| {status?.state || 'idle'} | |
| </span> | |
| </div> | |
| <p className="mt-2 text-neuro-text-primary text-sm"> | |
| {status?.current_task || 'Awaiting orchestration signals.'} | |
| </p> | |
| <div className="mt-3 flex items-center justify-between text-[10px] uppercase tracking-[0.2em] text-neuro-text-muted"> | |
| <span>Queue {status?.queue_depth ?? 0}</span> | |
| <span>{lastUpdateLabel}</span> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/components/webmode/panels/AgentGraphPanel.tsx` around lines 13 -
43, The rendering can throw when status?.last_update is invalid; update the
AgentGraphPanel mapping (the agentOrder.map block) to defensively parse/validate
status?.last_update before calling toLocaleTimeString: check that
status?.last_update is a finite timestamp or valid Date (e.g.,
Number.isFinite(Date.parse(...)) or create a Date and test
isNaN(date.getTime())), and if invalid use Date.now() or a formatted "N/A"
fallback; replace the direct new Date(status?.last_update ??
Date.now()).toLocaleTimeString() call with this safe logic so rendering cannot
crash on bad timestamps.
| <span className="px-2 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60">Approve</span> | ||
| <span className="px-2 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60">Reject</span> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's look at the full PermissionsPanel.tsx file
echo "=== Full PermissionsPanel.tsx content ==="
cat -n web-ui/src/components/webmode/panels/PermissionsPanel.tsxRepository: demonking369/NeuroRift
Length of output: 1865
🏁 Script executed:
#!/bin/bash
# Search for useNeuroRift hook implementation
echo "=== Searching for useNeuroRift hook definition ==="
rg -n "useNeuroRift" --type=ts --type=tsx -A 5Repository: demonking369/NeuroRift
Length of output: 161
🏁 Script executed:
#!/bin/bash
# Search for approve/reject action handlers in the codebase
echo "=== Searching for approve/reject patterns ==="
rg -n "(approveTask|rejectTask|approve.*action|reject.*action)" --type=ts --type=tsxRepository: demonking369/NeuroRift
Length of output: 156
🏁 Script executed:
#!/bin/bash
# Search for useNeuroRift hook definition (retry without problematic file type)
echo "=== Searching for useNeuroRift hook ==="
find . -name "*.ts" -o -name "*.tsx" | xargs rg -n "export.*useNeuroRift" -A 10 2>/dev/null | head -100Repository: demonking369/NeuroRift
Length of output: 1014
🏁 Script executed:
#!/bin/bash
# Search for approve/reject patterns more broadly
echo "=== Searching for approve/reject action patterns ==="
rg -n "(approveTask|rejectTask|handleApprove|handleReject|approve.*=|reject.*=)" --max-count=20 2>/dev/nullRepository: demonking369/NeuroRift
Length of output: 3832
🏁 Script executed:
#!/bin/bash
# Find and list the hooks file
echo "=== Finding lib/hooks files ==="
find . -path "*lib/hooks*" -type f 2>/dev/null | head -20Repository: demonking369/NeuroRift
Length of output: 123
🏁 Script executed:
#!/bin/bash
# Get the full useNeuroRift hook to see what it returns
wc -l web-ui/src/lib/hooks.tsRepository: demonking369/NeuroRift
Length of output: 93
🏁 Script executed:
#!/bin/bash
# Read the useNeuroRift hook return statement
tail -100 web-ui/src/lib/hooks.tsRepository: demonking369/NeuroRift
Length of output: 4592
Approve/Reject controls are non-functional — approval flow is completely broken.
Both "Approve" and "Reject" are static <span> elements with no onClick handlers. No action dispatchers exist in useNeuroRift() to handle approvals — the hook returns only { session, agents, tasks, approvals, torConnected, systemHealth, browserActive, metrics, setTorConnected }. This means clicking either control does nothing, and there is no mechanism to send an approval response back to the backend.
Beyond correctness, <span> elements are not keyboard-focusable and carry no implicit button role, creating an accessibility violation for keyboard and assistive-technology users.
These should be <button> elements wired to dispatch functions (e.g., approveTask / rejectTask) that must be added to useNeuroRift() and sent to the WebSocket backend.
🐛 Required changes
- Extend
useNeuroRift()to export approval action dispatchers:
return {
session,
agents,
tasks,
approvals,
torConnected,
systemHealth,
browserActive,
metrics,
setTorConnected,
+ approveTask,
+ rejectTask,
};- Implement the action functions to send approval responses via WebSocket:
+const approveTask = (approvalId: string) => {
+ const ws = getWebSocket();
+ ws.send({ type: 'approval_response', approval_id: approvalId, approved: true });
+};
+
+const rejectTask = (approvalId: string) => {
+ const ws = getWebSocket();
+ ws.send({ type: 'approval_response', approval_id: approvalId, approved: false });
+};- Update PermissionsPanel to use semantic
<button>elements and wire the handlers:
-const { approvals } = useNeuroRift();
+const { approvals, approveTask, rejectTask } = useNeuroRift();
-<span className="px-2 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60">Approve</span>
-<span className="px-2 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60">Reject</span>
+<button onClick={() => approveTask(approval.id)} className="px-2 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60 hover:bg-emerald-900/40 cursor-pointer">Approve</button>
+<button onClick={() => rejectTask(approval.id)} className="px-2 py-1 rounded-full bg-neuro-surface/70 border border-neuro-border/60 hover:bg-rose-900/40 cursor-pointer">Reject</button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/components/webmode/panels/PermissionsPanel.tsx` around lines 25 -
26, PermissionsPanel currently renders non-interactive <span> elements for
Approve/Reject; update it to render semantic <button> elements and attach
onClick handlers that call new action dispatchers exported from useNeuroRift
(e.g., approveTask and rejectTask). Add approveTask(taskId, optionalMeta) and
rejectTask(taskId, reason) to the useNeuroRift hook implementation so they send
the appropriate approval response over the existing WebSocket connection (reuse
the hook's socket/message sender) and update local approvals state; ensure these
functions are returned from useNeuroRift alongside { session, agents, tasks,
approvals, torConnected, systemHealth, browserActive, metrics, setTorConnected
}. In PermissionsPanel.tsx wire the buttons to call approveTask/rejectTask with
the relevant task id, add accessible attributes (type="button", aria-label) and
keyboard focusability, and handle optimistic UI updates or error handling based
on the hook's promise/result.
| function normalizeMode(mode?: string) { | ||
| if (mode?.toUpperCase() === 'DEFENSIVE') return 'DEFENSIVE'; | ||
| return 'OFFENSIVE'; | ||
| } |
There was a problem hiding this comment.
normalizeMode drops STEALTH sessions.
Any STEALTH mode gets coerced to OFFENSIVE, which can enable tools that should be disallowed.
✅ Suggested fix
-function normalizeMode(mode?: string) {
- if (mode?.toUpperCase() === 'DEFENSIVE') return 'DEFENSIVE';
- return 'OFFENSIVE';
-}
+function normalizeMode(mode?: string): SessionState['mode'] {
+ const value = mode?.toUpperCase();
+ if (value === 'DEFENSIVE') return 'DEFENSIVE';
+ if (value === 'STEALTH') return 'STEALTH';
+ return 'OFFENSIVE';
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function normalizeMode(mode?: string) { | |
| if (mode?.toUpperCase() === 'DEFENSIVE') return 'DEFENSIVE'; | |
| return 'OFFENSIVE'; | |
| } | |
| function normalizeMode(mode?: string): SessionState['mode'] { | |
| const value = mode?.toUpperCase(); | |
| if (value === 'DEFENSIVE') return 'DEFENSIVE'; | |
| if (value === 'STEALTH') return 'STEALTH'; | |
| return 'OFFENSIVE'; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/lib/hooks.ts` around lines 7 - 10, The function normalizeMode
currently maps any non-DEFENSIVE string (including "STEALTH") to "OFFENSIVE";
update normalizeMode to explicitly recognize allowed modes (at least
"DEFENSIVE", "OFFENSIVE", and "STEALTH") and return the uppercased input when it
matches one of those, otherwise fall back to the safe default ("OFFENSIVE") —
locate the normalizeMode function and replace the simple toUpperCase check with
a whitelist check against these mode names so STEALTH is preserved instead of
coerced.
| connect() { | ||
| if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { | ||
| return; | ||
| } | ||
|
|
||
| const endpoint = process.env.NEXT_PUBLIC_NEURORIFT_WS_URL ?? 'ws://127.0.0.1:8765'; | ||
| this.ws = new WebSocket(endpoint); | ||
|
|
||
| this.ws.onopen = () => { | ||
| const pending = [...this.queue]; | ||
| this.queue = []; | ||
| pending.forEach(payload => this.send(payload)); | ||
| window.dispatchEvent(new CustomEvent('neurorift:connection', { detail: { connected: true } })); | ||
| }; | ||
|
|
||
| this.ws.onmessage = event => { | ||
| try { | ||
| const payload = JSON.parse(event.data); | ||
| this.callbacks.forEach(callback => callback(payload)); | ||
| window.dispatchEvent(new CustomEvent('neurorift:event', { detail: payload })); | ||
|
|
||
| if (payload.type === 'chat_response') { | ||
| window.dispatchEvent(new CustomEvent('neurorift:chat_response', { detail: payload })); | ||
| } | ||
| if (payload.type === 'session_list') { | ||
| window.dispatchEvent(new CustomEvent('neurorift:session_list', { detail: payload })); | ||
| } | ||
| } catch (error) { | ||
| console.error('Failed to parse websocket event', error); | ||
| } | ||
| }; | ||
|
|
||
| this.ws.onerror = () => { | ||
| window.dispatchEvent(new CustomEvent('neurorift:connection', { detail: { connected: false } })); | ||
| }; | ||
|
|
||
| this.ws.onclose = () => { | ||
| window.dispatchEvent(new CustomEvent('neurorift:connection', { detail: { connected: false } })); | ||
| this.ws = null; | ||
| if (!this.intentionallyClosed) { | ||
| this.reconnectTimer = window.setTimeout(() => this.connect(), 1200); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| on(event: string, handler: (payload: any) => void) { | ||
| this.listeners.set(event, handler); | ||
| close() { | ||
| this.intentionallyClosed = true; | ||
| if (this.reconnectTimer) { | ||
| window.clearTimeout(this.reconnectTimer); | ||
| } | ||
| this.ws?.close(); | ||
| this.ws = null; | ||
| } | ||
|
|
||
| send(payload: WebSocketPayload) { | ||
| this.connect(); | ||
|
|
||
| if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { | ||
| this.queue.push(payload); | ||
| return; | ||
| } | ||
|
|
||
| this.ws.send(JSON.stringify(payload)); | ||
| } | ||
| } | ||
|
|
||
| const sampleSessions = [ | ||
| { | ||
| id: 'session-aurora', | ||
| name: 'Aurora Lattice Sweep', | ||
| mode: 'DEFENSIVE', | ||
| status: 'active', | ||
| updated_at: new Date(Date.now() - 1000 * 60 * 12).toISOString(), | ||
| findings: [], | ||
| artifacts: [], | ||
| metadata: { description: 'Continuous defense posture validation.' } | ||
| }, | ||
| { | ||
| id: 'session-echo', | ||
| name: 'Echo Vector Audit', | ||
| mode: 'OFFENSIVE', | ||
| status: 'paused', | ||
| updated_at: new Date(Date.now() - 1000 * 60 * 120).toISOString(), | ||
| findings: [], | ||
| artifacts: [], | ||
| metadata: { description: 'Red-team simulation across agent mesh.' } | ||
| subscribe(callback: EventCallback) { | ||
| this.callbacks.add(callback); | ||
| return () => { | ||
| this.callbacks.delete(callback); | ||
| }; | ||
| } | ||
| ]; | ||
| } | ||
|
|
||
| let instance: MockWebSocket | null = null; | ||
| let instance: NeuroRiftSocket | null = null; | ||
|
|
||
| export function getWebSocket() { | ||
| if (!instance) { | ||
| instance = new MockWebSocket(); | ||
| instance = new NeuroRiftSocket(); | ||
| instance.connect(); | ||
| } |
There was a problem hiding this comment.
Reconnect can get stuck after a manual close.
close() marks intentionallyClosed = true, but connect() never resets it and getWebSocket() won’t reconnect if the instance exists with a null socket. That can leave the client permanently disconnected unless a send occurs, and it disables auto‑reconnect for later sessions.
🐛 Proposed fix
connect() {
+ this.intentionallyClosed = false;
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
return;
}
@@
export function getWebSocket() {
if (!instance) {
instance = new NeuroRiftSocket();
instance.connect();
+ } else if (!instance['ws']) {
+ instance.connect();
}
return instance;
}🧰 Tools
🪛 Biome (2.4.4)
[error] 23-23: This callback passed to forEach() iterable method should not return a value.
(lint/suspicious/useIterableCallbackReturn)
[error] 30-30: This callback passed to forEach() iterable method should not return a value.
(lint/suspicious/useIterableCallbackReturn)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/lib/websocket.ts` around lines 12 - 91, The instance can remain
permanently disconnected because close() sets
NeuroRiftSocket.intentionallyClosed = true and getWebSocket() doesn't trigger
reconnect when an instance exists with a null ws; fix by resetting
intentionallyClosed at the start of connect() (e.g., this.intentionallyClosed =
false inside NeuroRiftSocket.connect) and also update getWebSocket() to call
instance.connect() when instance exists but instance.ws is null (so an existing
instance will attempt reconnection instead of being left inert).
User description
Motivation
Description
web-ui/src/lib/websocket.tsand rewritingweb-ui/src/lib/hooks.ts, and rewired the Tools page to queue/cancel tasks and render live output (web-ui/src/app/tools/page.tsx).web-ui/src/components/webmode/*and supporting types, utils and config schema underweb-ui/src/lib/webmode/*andweb-ui/src/lib/*.core/neurorift-core/src/websocket/events.rs) and core orchestration (core/neurorift-core/src/lib.rs,core/neurorift-core/src/main.rs) to return task IDs, broadcasttask_output/task_cancelled/task_failedevents, supportcancel_task, and perform asyncexecute_taskvia the Python bridge with automaticsave_sessionon completion.core/neurorift-core/src/python_bridge/mod.rs) to enforce{success,data,error}semantics, upgradedmodules/web/bridge_server.pyto: check Ollama availability/models, persist per-session artifacts/audit under~/.neurorift/sessions/..., provide/startup_checks, and record AI/tool audit entries; Ollama base URL set tohttp://127.0.0.1:11434.self_evolutionconfig toconfigs/neurorift_config.jsonand a frontend constantweb-ui/src/lib/evolution.tsto surface the proposal/approval sandbox policy.Testing
cargo check -p neurorift-coreand it completed successfully (Rust compile checks passed). (Succeeded)python -m py_compile modules/web/bridge_server.pywith no syntax errors. (Succeeded)npm run devand used Playwright to load/toolsand capture a screenshot artifactartifacts/tools-operational.pngto verify UI wiring and live streaming behavior. (Succeeded)npm run buildwhich failed in this environment due tonext/fontfetching Google Fonts over TLS (environmental TLS/fetch issue), andnpm run lintfailed due to project lint path configuration; these are tooling/environment issues and do not affect the runtime wiring implemented here. (Build: environmental failure; Lint: project script issue)Codex Task
CodeAnt-AI Description
Enable real Web Mode with live backend wiring, Ollama checks, session persistence, and actionable tool execution
What Changed
Impact
✅ Can queue and cancel tool executions from the UI✅ Local session artifacts and audit logs persisted per session✅ Clearer AI availability: UI blocks generation and shows startup checks when Ollama is missing💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.
Summary by CodeRabbit
New Features
Bug Fixes
Refactor