* feat(opencode): support plan mode
* feat(opencode): support reasoning effort
* feat(opencode): surface context usage in web
Bridge OpenCode ACP usage updates into the existing token-count pipeline so the web status bar can show live context and cache information without a separate UI path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(opencode): restrict plan mode to remote, rollback reasoning effort on failure
- Block local OpenCode plan startup (tools not enforced in local path)
- Allow remote OpenCode plan only (ACP permission handler denies tools)
- Guard web /permission-mode endpoint for local OpenCode plan sessions
- Rollback session reasoning effort when OpenCode rejects set_config_option
- Wire rollback callback through opencodeLoop to runOpencode closure
- Add tests: local plan rejected, remote plan allowed, web guard, effort rollback
* fix(web): auto-retry OpenCode models query to populate model selector without refresh
- Retry early failures (RPC may still be registering on new sessions)
- Poll briefly until availableModels is non-empty
- Stop polling once model options are discovered
- Add tests for retry/poll/stop policy
* fix(opencode): cap model discovery polling
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Emit throttled ACP reasoning snapshots with stable stream ids so OpenCode reasoning updates render live without one row per token.
Collapse matching reasoning snapshots in the web timeline and avoid stale session 404 redirects during refetch.
Tests: targeted Vitest suite and bun typecheck.
Co-authored-by: twshe <twshe@outlook.com>
* test(codex): add web event rendering harness
* fix(codex): surface plan updates in web
* fix(codex): render MCP tool calls in web
* fix(codex): improve terminal and context display
* fix(codex): format token usage events
* fix(codex): show status context in web
* fix(codex): preserve tool result errors
The CLI wraps array-content user messages as agent output because
isExternalUserMessage rejects non-string content. On the web side,
detect text-only arrays in non-sidechain user output and emit them as
role:'user' so they display in the user lane.
Also handle sidechain user messages with mixed array content (e.g.
tool_result + text) by extracting text parts into a sidechain block.
Closes#407's original scope on top of the #402 base.
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): drop "No response requested." assistant messages
When Claude Code injects system messages (task notifications, system
reminders) as user turns, Claude responds with "No response requested."
In the HAPI web UI this appears as a reply to the user's message,
making it look like Claude is ignoring their input.
Filter these out in isSkippableAgentContent() (catches the fallback
path in normalize.ts) and in normalizeAssistantOutput() (catches the
primary path). Both checks verify the assistant message contains only
the text "No response requested." with no tool calls.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): filter text block instead of dropping entire message
Address bot review: dropping the whole normalized record breaks
sidechain UUID threading (parentUUID chain orphans).
Instead of returning null, suppress only the "No response requested."
text block during content extraction. The message record (uuid,
parentUUID, usage) is preserved so the tracer's sidechain grouping
continues to work.
Also remove the isSkippableAgentContent check since we no longer
need to drop the message at that layer.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): move "No response requested." filter to reducer layer
Address bot review: filtering in normalizeAssistantOutput() produced
empty content arrays, breaking traceMessages() which reads uuid and
parentUUID from content[0]. Sidechain child messages whose parentUUID
pointed to the filtered message became orphaned.
Fix: revert the normalizer to always emit the text block (preserving
the UUID chain for the tracer), and filter the sentinel text in
reducerTimeline.ts where text blocks become visible AgentTextBlocks.
At this point tracing is already complete.
Also adds reducer-level tests for the filter and updates the
normalize test to verify the text block is preserved.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): scope sentinel filter to single-block assistant messages only
Address bot review: the previous filter suppressed any text block
matching "No response requested.", which could hide legitimate replies.
Now the filter only triggers when the message has exactly one content
block (msg.content.length === 1) — i.e., the assistant response is
purely the sentinel text with no tool calls or reasoning blocks.
This prevents false positives while still catching the system-injection
auto-reply case.
Add test for the multi-block case (text + tool call) to verify the
sentinel text is preserved when other content exists.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): add parentUUID structural check to sentinel filter
Address bot review: raw text match alone could theoretically suppress
a legitimate reply. Add c.parentUUID !== null as a structural guard:
- Sentinel auto-replies always follow a prior assistant turn, so their
parentUUID is set (pointing to the previous message in the chain).
- A first message in a conversation has parentUUID: null and will
never be filtered.
Combined conditions: msg.content.length === 1 (sole block, no tool
calls) AND c.parentUUID !== null (not the first reply) AND exact text
match.
Add tests for the parentUUID=null escape hatch.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): use injected-turn UUID tracking for sentinel filter
Address review: use structural markers instead of broad text matching.
1. Pre-scan collects UUIDs from sidechain content blocks (system-
injected user turns). The sentinel filter now only triggers when
parentUUID points to one of these known injected turns.
2. Move task-notification event extraction from normalizer to reducer.
Previously, task-notifications with summary were normalized as
role:'event', losing their uuid. Now they stay as sidechain (uuid
preserved for pre-scan), and the reducer extracts the summary as
an agent-event block.
3. Remove redundant 'uuid' in c guard (always present on sidechain type).
False positive analysis: a legitimate reply is only suppressed when ALL
of: (a) sole content block, (b) parentUUID matches a sidechain-injected
turn, (c) exact sentinel text. This combination cannot occur for real
user-facing content.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): add parentUUID to sidechain content type for tracer linkage
The sidechain content block was missing parentUUID, so traceMessages()
could not chain system-injected user turns (task notifications, system
reminders) inside a Task sidechain back to their parent. This caused
later sidechain messages pointing to the injected turn's UUID to become
orphaned and disappear from the Task card.
Add parentUUID to the sidechain type definition and propagate it from
normalizeUserOutput() in both the isSidechain and non-sidechain paths.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): handle array-content sidechain user messages to prevent prompt leak
Sidechain user messages can arrive with either string content or array
content ([{type:'text', text:'...'}]) depending on how Claude Code
serialises them. The previous fix only handled the string case, causing
intermittent prompt leaks when array format was used.
Now normalizeUserOutput extracts text from array-content sidechain
messages and emits them as sidechain blocks, so the tracer can match
them to their parent Task tool call.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* test(web): verify parentUUID propagation from assistant output data
Add integration tests confirming normalizeAssistantOutput correctly
maps data.parentUuid to text block parentUUID (used by the reducer's
sentinel detection). Tests cover both present and absent parentUuid.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): filter system-injected XML tags from rendering as raw text
Claude Code injects internal messages (<task-notification>, <system-reminder>,
<command-name>, <local-command-caveat>) as user-role messages. The web UI was
rendering these as raw XML text visible to users.
- Parse <task-notification> and display as agent-event with summary text
- Silently drop <system-reminder>, <command-name>, <local-command-caveat>
- Add tests covering all injection prefixes and edge cases
* fix(web): scope system injection filtering to Claude sessions only
Address review feedback: the XML tag filtering was applied at the
generic timeline layer, which could incorrectly hide legitimate user
messages in Codex/Gemini sessions.
- Add isClaudeSession flag threaded from Session.metadata.claudeSessionId
- Only filter system-injected tags when isClaudeSession is true
- Add tests verifying non-Claude sessions pass through all messages
* fix(web): treat all string user output as sidechain to prevent prompt leaks
Restores the fix from 3cf96ab that was accidentally reverted in 2205e04.
In normalizeUserOutput(), string-content user messages arriving through
the agent output path are never real user input (real user text goes
through normalizeUserRecord). Previously, non-sidechain string messages
were emitted as role:'user', causing subagent prompts and system-injected
messages to render as user text in the web UI.
Now all string-content user messages in this path are:
- <task-notification> with summary → converted to role:'event'
- Everything else → marked as sidechain (matched to parent Task tool
call by the tracer, or harmlessly skipped by the reducer)
This provides a root-level fix that prevents ANY string user message
from the agent output path from leaking as visible user text.
* ci: retrigger CI
* fix(web): remove superseded return-null filter from upstream PR #372
The upstream `return null` filter for <task-notification> and
<system-reminder> (from PR #372) is now superseded by the comprehensive
sidechain upgrade logic. Remove it to avoid short-circuiting the new
task-notification → event conversion.
* refactor(web): remove reducer-side system injection filtering
System-injected messages are now fully handled in normalizeUserOutput()
(normalize layer), so the redundant filtering in reduceTimeline() is no
longer needed. Removing it also eliminates the risk of accidentally
hiding legitimate user messages that happen to start with XML tags.
- Remove SYSTEM_INJECTION_PREFIXES, isSystemInjectedMessage,
parseTaskNotificationSummary from reducerTimeline.ts
- Remove isClaudeSession plumbing from reducer.ts and SessionChat.tsx
- Simplify reducerTimeline.test.ts to only test pass-through behavior