mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
d3473f9ed42e8a1d64fc3d71433b0507d98306f9
35
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d3473f9ed4 |
fix(cli): forward Claude API error records with null service_tier (#1857) (#2087)
Claude Code writes failed API calls ("Prompt is too long", usage limits,
logged out) as synthetic assistant records whose usage carries
`service_tier: null`. The schema only accepted undefined, so the local
session scanner dropped every one and the web session just looked stalled.
- Accept null for the usage fields the Messages API types as nullable.
- Keep the top-level `error`/`isApiErrorMessage` on assistant records in
local mode, and `error` in the SDK converter for remote mode.
- Render assistant records carrying `error` as the existing error event,
so the all-zero synthetic usage no longer resets the context gauge.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
2b402d24d9 |
fix(web): show Codex round usage metadata (#1685)
* fix(web): show Codex round usage metadata * fix(web): skip imported Codex round duration |
||
|
|
661e9b4eb7 | feat(web): show Claude round usage metadata (#1655) | ||
|
|
5c81ed69e2 |
fix(runner) preserve native instructions (#1557)
* fix(codex): preserve native instructions Keep HAPI guidance in the developer layer so app-server retains Codex persistence rules. Align long-running goal states with the Codex 0.147 protocol. * fix(codex): filter new goal statuses |
||
|
|
1f2fbcb142 |
fix(web): map codex-enveloped compact-summary to the chat block (#1582)
* fix(web): map codex-enveloped compact-summary to the chat block The merged #1570 renders Pi compaction summaries as a dedicated chat block via the event envelope. A compact-summary arriving in the codex payload envelope (older import paths, future producers) would still be silently dropped by the codex-content filter; map it to the same agent-event the live pi wrapper emits, mirroring the existing context_compacted handling. Adds a normalizeAgent regression test and tightens the /compact thinking-state test to assert the last keepAlive flips true (RPC outstanding) then false (settled). Verified: bun typecheck clean; web normalizeAgent 12/12, cli runPi 57/57 in an isolated TMPDIR. * fix(web): remove duplicate showSessionSummaryInChat in markdown-a fixture Regression from #1530: the fixture object literal sets the key twice, which breaks `bun typecheck` on upstream/main (Test workflow failing on push). One-line cleanup; no behavior change. Verified: bun typecheck exit 0 across cli/web/hub. |
||
|
|
b7f52f58ca |
feat(media): add audio and file display (#1405)
* feat(cli): cross-flavor inline image display via MCP and ACP Share display_image prompt across MCP-bridge flavors (Cursor, Gemini, Kimi, Codex, Claude, OpenCode), auto-approve the tool in buildHapiMcpBridge, handle ACP image content blocks, and harden generated-image registration with content sniffing. Closes tiann/hapi#956 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): render generated-image cards reliably in chat Keep object URLs stable across refetch, upscale tiny inline images, fetch generated-image bytes with cache no-store (avoid empty 304 bodies), and load hapiMcpUrl from per-session API in hapi-display-image tooling. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(cli+web): display_video MCP for inline mp4/webm (#956) Add display_video alongside display_image, video MIME sniffing with avif guard, web GeneratedImageCard video player, and hapi-display-image auto-routing. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(cli+web): cross-flavor display_video parity with images (#956) Share display_video prompts across MCP-bridge flavors, auto-approve the tool, register mp4/webm via path sniffing, render inline video in web on the existing generated-image RPC path, and restore robust media card fetch. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): ACP image ordering and inline media source provenance Flush buffered assistant text before async generated_image emit from ACP image blocks (PR #958 review Major). Add optional source metadata on generated-image wire messages (ingress, flavor, toolCallId, toolName) for MCP, ACP, and Codex tool-result paths. Seeds artifact-event follow-up #966. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli+web): address PR #958 review Majors on media order and stale blobs Queue ACP session updates and await async image registration before later events; clear GeneratedImageCard blob state when imageId changes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): await ACP queue after late-drain before turn_complete Straggler session/update during drainLateBuffers can queue async image registration; re-await sessionUpdateQueue so generated_image is not emitted after turn_complete. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): route AVIF ftyp brands to display_image in helper Match server-side detectImageMimeType so .avif files are not sent to display_video and rejected as unsupported video. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(#956): agent inline-media doctor and discovery fixes - hapi doctor inline-media: probe bridges, print per-session inline commands - Expose hapiMcpUrl on session list summaries (stops false "no MCP" scans) - Helper script: match cursorSessionId prefixes; HAPI_SESSION_ID path-only mode - ACP bridge prompt: shell fallback + HAPI session id vs agent id rule Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): allow immutable cache for generated media blobs Drop cache: no-store on generated-image fetch so browser can reuse hub immutable responses; on 304 re-read via force-cache (#927, PR review). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(#956): Cursor native MCP overlay; drop user-turn bridge prepend Cursor ACP ignores session/new mcpServers. Write .cursor/mcp.json and run agent mcp enable hapi instead. Remove HAPI_MCP_BRIDGE_PROMPT from user turns on ACP remotes; enrich MCP tool descriptions for discovery. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): preserve non-HAPI mcp.json keys on Cursor overlay cleanup Cleanup only removes or restores the hapi MCP entry instead of rewriting the full pre-session snapshot, so concurrent edits to other servers survive. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): handle generated_image in Grok ACP launcher switch Upstream Grok launcher exhaustiveness broke after AgentMessage gained generated_image for cross-flavor inline media. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): rebase fallout for display_video + OpenCode skill lookup Gate display_video in the STDIO bridge, restore OpenCode first-prompt TITLE_INSTRUCTION (skill_lookup), and update tool-list test expectations. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): leave user-owned hapi MCP entry alone on overlay cleanup Only undo mcpServers.hapi when it still matches the exact entry this session installed; concurrent Cursor/user edits of that key survive. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): exact-match auto-approve for display_image and display_video Move media tools off substring name/id hints onto the exact-name set so forged lookalike tools are not approved in default permission mode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): drop dead Cursor bridge prompt; put guidance in MCP descriptions Cursor must not get a user-turn media prepend (prompt-taint). Remove unused HAPI_MCP_BRIDGE_PROMPT_CURSOR and embed DISPLAY_*_PROMPT_CURSOR in the display_image/display_video MCP tool descriptions instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): require user approval for display_image and display_video Those tools read arbitrary local paths into chat; keep them on MCP approval_mode prompt and out of default-mode auto-approve exact names. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: re-trigger Codex PR review after provider 503 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): ignore URI-only ACP image blocks that read local disk Passive ACP agentMessageChunk handling must not load file:// or bare paths; local media goes through prompt-gated display_image/display_video. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): allowlist MP4 ftyp brands for video sniffing Reject HEIC/HEIF and other non-video ISO-BMFF containers instead of treating every non-AVIF ftyp as video/mp4. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep Cursor ACP startup if MCP overlay fails Wrap installCursorMcpOverlay so a malformed project .cursor/mcp.json cannot abort the session; continue without inline media tools. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail doctor inline-media when checks fail Exit non-zero whenever required checks fail, even if an active hapiMcpUrl bridge is present. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): expect display_video when change_title is disabled Native ACP title mode still exposes display_image and display_video; update startHappyServer test after rebase onto 0.23.4. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep ACP title sync synchronous outside media queue session_info_update title forwarding (#1028) must not wait on the async message-handler queue used for inline media ordering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): sniff media headers only; advertise OpenCode display_video Read 16 bytes for detectMediaTool instead of the whole file, and include hapi_display_video in OPENCODE_NATIVE_TOOL_INSTRUCTION for remote ACP. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): show Cursor generate_image inline in HAPI chat cursor/generate_image only emitted a tool card; register filePath or base64 imageData into generatedImages and emit generated_image so the web chat card renders (issue #956 / swear01 report). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): ignore path-only Cursor generate_image reads Path-only filePath registration bypassed permission-gated display_image / display_video MCP tools. Keep base64 imageData only; local paths must go through MCP approval. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): gate inline media base64 length before decode Reject oversized ACP/Cursor base64 payloads by character count so the CLI never allocates past the 25 MB generated-image cap. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): require EBML DocType webm for inline video sniff Bare EBML magic matches Matroska/MKV too; only accept DocType webm. Also restore annotated Playwright cursor in annotatedVideoUseOption. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): read 128-byte header for WebM DocType sniff detectMediaTool only loaded 16 bytes, so EBML DocType webm was often missing and valid WebM files fell through to display_image. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): compare generated-image source by value in reconcile Wire normalization allocates a fresh source object each pass; reference equality forced media-card recomputation on every reload/SSE refresh. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): inject Cursor MCP enable for overlay unit tests installCursorMcpOverlay always spawned `agent mcp enable`; tests now pass a noop so the suite never shells out to a real Cursor binary. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): shell-quote doctor inline-media helper command Paths and session prefixes with spaces/metacharacters broke the copied snippet; JSON.stringify each interpolated argument. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): fix doctor inline-media quote path expectation Repo root from scriptPath is three levels up (cli/), not the parent of cli. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli/web): per-session Cursor MCP overlay id and bound tiny-image scale Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): redact generate_image base64 from logs and fix doctor MCP ids Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): harden inline-media doctor for packaged installs and hub headers Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): lock Cursor mcp.json updates and bound ACP media filenames Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): preserve mcp.json mode and token-scoped overlay locks Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): publish Cursor MCP lock owners via link(2) and treat EPERM as alive Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail closed on stale MCP locks; keep concurrent mcp.json top-level keys Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): roll back Cursor MCP overlay when agent mcp enable fails Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): await ACP session queue in suppressUpdatesDuring tests #958 queues handleUpdate for media registration; upstream compact tests assumed sync delivery after restore. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): capture ACP handler at enqueue; keep display_video manual Close two Major review findings on #958: suppress queue leak after restore, and Claude --allowedTools auto-approving local-path video. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: write through symlinked mcp.json; lazy-load inline video Preserve user Cursor MCP symlinks on atomic overlay writes, and require explicit Load video before fetching large generated-video blobs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): use valid TerminalToolDisplayMode in media card test Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): require unique session prefix in display helper Reject ambiguous prefix matches so images/videos cannot land in the wrong HAPI chat when multiple agent session ids share a prefix. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): always cleanup Cursor MCP overlay on teardown Run overlay cleanup in finally so cancelAll/disconnect failures cannot leave a dead hapi-<sessionId> entry in .cursor/mcp.json. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): handle Copilot generated_image; refuse MCP symlinks Unblock typecheck after Antigravity/Copilot merge, and fail closed when .cursor/mcp.json or .cursor is a project-controlled symlink. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: abort restore deliveryMode; prune dead Cursor MCP overlays Unblock web typecheck after steer merge, and recover orphaned hapi-* mcp.json entries via HAPI_MCP_OVERLAY_PID ownership stamps. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): recover dead-PID Cursor MCP overlay locks Token-matched unlock so a crash mid-lock no longer permanently disables inline media; keep live-owner waits identity-safe. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): serialize Cursor MCP stale-lock recovery Acquire an exclusive recovery lock before token-matched unlink so two recoverers cannot remove a successor's live mcp.json lock. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail closed on stale Cursor MCP overlay locks Withdraw racy auto-recovery: pathname check-then-unlink/rename can steal a successor lock. Stale locks throw with an explicit rm hint instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): drop duplicate deliveryMode on abort restore Merge left both steer and queue; keep queue so retries after abort do not re-bind to a later Pi turn. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): bound inline media reads on open fd Close TOCTOU between pathname size check and readFile for display_image / display_video and registerGeneratedImageFromPath. Also preserve non-PID env edits on Cursor MCP overlay cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): repair happyMcpStdioBridge test syntax after merge Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): write Cursor MCP overlay to ~/.cursor, not the project Keep ephemeral hapi-<sessionId> bridges out of the checked-out tree so agents cannot git-add a live loopback URL. Tests inject mcpConfigDir. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): point Cursor MCP diagnostics at ~/.cursor/mcp.json Co-authored-by: Cursor <cursoragent@cursor.com> * feat(media): add audio and file display --------- Co-authored-by: HeavyGee <133152184+heavygee@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse> |
||
|
|
021b5c194b |
feat(pi): complete RPC parity, native steer, and history controls (#1353)
* feat(pi): complete RPC interaction parity * feat(pi): integrate native conversation history * fix(pi): harden RPC lifecycle boundaries * fix(pi): address review lifecycle and upload boundaries * fix(pi): release history transaction on rollback deadline * fix(pi): isolate preflight and timed-out mutations * fix(pi): preserve retry and editor boundaries * fix(pi): disable unavailable history synchronization * fix(pi): gate fallback readiness on history baseline * fix(pi): bind uploads and retire extension requests * fix(pi): preserve canceled and legacy stream boundaries * fix(pi): preserve native fork runtime state * fix(pi): persist dialogs and preserve select values * fix(pi): keep upload authorization path-stable * feat(pi): preserve native steer semantics Route ordinary sends during an active Pi main turn through native steer while keeping explicit queue delivery on the existing composer gestures. Persist the delivery contract across Hub replay and Web retries, and guard stale steer dispatch with streaming generations and ordered prompt fallback. * fix(pi): queue deferred steer deliveries Keep native steer only for the initial live emit. Reconnect replay, CLI backfill, clear-gate release, and mature delivery now downgrade turn-scoped steer intent to the durable HAPI queue without mutating stored provenance. * fix(pi): retain abort guard through preflight miss Treat an immediate no-active abort rejection as a possible async-preflight race. Keep the existing abort boundary alive so a late agent_start receives the compensating abort before queued work is released. * fix(pi): queue stale steer retries A failed send no longer reuses turn-scoped steer intent after its original Pi generation is lost. Text restoration, attachment retry, and legacy retry provenance all enter the durable HAPI queue while fresh ordinary sends retain native steer behavior. * fix(pi): invalidate rejected abort generation After a no-active preflight abort waits through late-start compensation, mark the target stream idle while the runtime mutation lease is still held. Waiting native steers therefore fall back instead of entering the aborted generation. * fix(pi): queue idempotent steer retries Track whether a localId insert created a new row. Initial inserts may retain live Pi steer, while duplicate-localId retries deliver a queue-safe view of the stored row without overwriting its original provenance. * fix(pi): sync command-only history before fallback Read the Pi append log before retiring a successful prompt that produced no agent lifecycle. Preserve FIFO history associations across missing entry events, and fail the wrapper closed if that mandatory synchronization cannot be completed. |
||
|
|
e35c06b36a | feat(agy): add Antigravity as an interactive PTY agent (#1320) | ||
|
|
deb5d63695 | fix(cli,web): group orphaned subagent trace by parentToolUseId (#1175) | ||
|
|
6bedd0d924 | feat(tooling): preserve native tool titles (#1133) | ||
|
|
289c9f2218 |
feat(cli,web): show Claude Code's away recap in local-mode chat (#1089)
* feat(shared,cli): whitelist away_summary so auto recap reaches the hub Claude Code's local TUI writes an automatic away-summary recap to the session transcript on window blur/focus (5min+ idle), but VISIBLE_CLAUDE_SYSTEM_SUBTYPES dropped it before it ever reached the hub. Add it to the whitelist so the local launcher forwards it like the other system subtypes, and cover the forwarding + Zod passthrough of the recap `content` field with tests. * feat(web): render Claude Code's automatic away recap in the chat Once away_summary reaches the hub (previous commit), the web chat still dropped it silently: normalizeAgent had no branch for the subtype, so it fell through to `return null`. Add a `recap` AgentEvent, a normalizeAgent branch mirroring the existing turn_duration/compact subtype branches, and a presentation entry that prefixes the text with `recap:` so it reads distinctly from the manual /recap assistant bubble (which already renders as a normal message). No new render component needed: it flows through the existing generic system-event row (SystemMessage.tsx + getEventPresentation) that every other system subtype already uses. * fix(web): drop inaccurate manual-/recap comparison from recap comments |
||
|
|
2ce6d3ef3a |
feat(web): show tool call duration in the detail dialog (#1036)
* refactor(web): export formatDuration for reuse * feat(web): show tool call duration in the detail dialog Show a completed tool's execution duration at the top of its detail dialog. The value is derived from the Claude entry's own timestamps (the execution machine's wall clock) rather than the hub's message-receive time, and is used only when both the tool_use and tool_result entries carry a real timestamp — otherwise it falls back to the hub receive times on both sides, so the two clocks are never mixed. Running/pending tools show nothing, the running-state live timer is unchanged, and clock skew is guarded against. Reuses the existing formatDuration formatter. No schema changes. * fix(web): backfill hub startedAt on reorder so duration isn't 0.0s When a tool_result entry is reduced before its tool_use, the tool block is created from the result, so the hub startedAt is the result receive time. The tool_use path only lowered the exec start, not the hub startedAt, so a timestamp-less pair (no exec duration available) fell back to startedAt === completedAt and the detail dialog showed 0.0s. Lower the hub startedAt to the earlier tool_use receive time as well. |
||
|
|
02a0aa6733 |
fix(cursor): surface agent errors with warning styling in web UI (#871)
* test: reproduce issue #864 Assert Cursor error paths emit agent error payloads and web UI renders them as warning-styled events instead of neutral session messages. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): surface agent errors with warning styling in web UI (closes #864) Route Cursor stderr, init, prompt, and legacy exit failures through sendAgentMessage({ type: 'error' }) and teach the web chat layer to render error events with a warning icon instead of neutral info text. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
3a8693f380 |
feat(cursor): migrate remote sessions to ACP with model/variant pickers (#799)
* feat(cli,web,hub): migrate Cursor remote sessions to ACP with model/effort pickers Move stream-json remote launcher to legacy path and add ACP launcher with set_config_option model/mode sync, optimistic keepalive on config changes, and shared catalog caching. Web gets dual base/effort Cursor pickers for session and new-session flows; hide composer status bar when Cursor sends no usage_update. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli,web,shared): Cursor model picker — ACP wires + CLI sku variants Enrich the web/mobile picker with agent --list-models SKUs grouped under ACP wire bases, fix session-open base highlight, and keep catalog discovery safe while the ACP transport holds the CLI lock. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor-acp): apply ACP default model when web resets to Default Web sends model: null for Default; push session/set_config_option with the ACP default[] wire so Cursor backend matches hub state. Regression tests for setModel(null) and applyModelConfig(null). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp): clear stale agent-acp lock when owning process is gone Check lock pid with signal 0; remove orphaned lock dirs after SIGKILL or crash so listCursorModels can run cold probes again. Regression tests for guard and catalog discovery. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cursor): use live pid for ACP lock handler tests Stale-lock cleanup clears dead pids; handler tests must simulate an active lock with the current process pid to avoid cold probes/timeouts. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp): scope agent CLI lock guard to Cursor agent command only Gemini/OpenCode/Kimi ACP sessions must not register agent-acp-active; that blocked listCursorModels while unrelated backends were running. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,web): reject Cursor model changes for local sessions Hub returns 409 when controlledByUser is set, matching Codex. Web hides model and variant pickers for local Cursor sessions so users do not hit a dead RPC path. Document pre-push-review in AGENTS.md. Verified: bun typecheck; bun run test (919 cli + 243 hub + 768 web + 46 shared). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): send stable ids for Cursor ask_question replies Parse and submit question.id and option.id so ACP receives keys like { approach: ['a'] } instead of index/label. Verified: bun typecheck && bun run test. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
485bb46db8 |
fix(claude): propagate real contextWindow from SDK result to web (#720)
The "Default" model in NewSession sends no --model flag, so Claude CLI picks its own default (e.g. Opus 4.7 [1m] on Pro accounts). The web status bar then falls back to 200K - 10K headroom = 190K because the Claude SDK path never plumbs the real per-model contextWindow through to the wire-level `modelContextWindow`, unlike the ACP/Codex backends. Fix the gap in three places: - cli/src/claude/sdk/types.ts: declare optional `modelUsage` on SDKResultMessage to surface what Claude CLI already emits (`modelUsage[<model>].contextWindow`). - cli/src/claude/utils/sdkToLogConverter.ts: on system.init, capture the resolved model name (full form with `[1m]` suffix) and derive an initial contextWindow from the suffix. On every assistant message, inject the cached contextWindow into `usage.context_window` when absent. On result, refine the cache with the authoritative value from `modelUsage`. - web/src/chat/normalizeAgent.ts: forward `context_window` through the assistant usage normalization, so the existing reducer path (reducer.ts:175 → StatusBar.tsx:175) can render the real window. Closes #719. |
||
|
|
6f2bb7d32b |
feat(opencode): add plan mode, reasoning effort, and status telemetry (#688)
* 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> |
||
|
|
b06251bd53 |
fix: stream opencode reasoning updates (#661)
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> |
||
|
|
0e594da84d | Render Codex review messages | ||
|
|
86780e94d0 | feat(codex): preview generated images in chat (#630) | ||
|
|
089ddad476 | feat: support Codex goal slash command | ||
|
|
841b7cc035 |
Add Codex multi-agent timeline support (#588)
* checkpoint codex multiagent UI state * fix codex multiagent event scoping * fix: stabilize codex subagent timeline * chore: remove codex subagent nesting prompt * test(web): stabilize tool result rendering tests * fix: collapse codex agent trace rows by default * fix(web): keep chat scrolled to bottom * fix(web): backfill agent-run-heavy message loads * fix(codex): fail stuck subagent spawns * fix(codex): preserve wrapped child event scope * fix(codex): surface agent tool completions |
||
|
|
d9d7ed6699 |
feat(web): show message metadata (invoke time, duration, model) on click (#555)
* feat(web): show message metadata (invoke time, duration, model) on click
* fix(cli): preserve model field on assistant messages forwarded to hub
`RawMessageSchema` validates the `message` object in Claude Code session
JSONL lines before the cli forwards each message to the hub. Zod's default
parse mode strips fields that the schema does not declare, so the
`message.model` value (e.g. `claude-sonnet-4-6`) was silently removed
before the message reached the hub. The web normalizer reads
`data.message.model` to label assistant blocks, so without this field
every assistant message fell back to a generic "AI Model" label —
defeating the per-message model attribution this PR adds.
Add `model` to `RawMessageSchema` so it survives parse and reaches the
hub intact.
* fix(web): drop dead model shorthand in result envelope normalize
The `result/success` branch in `normalizeAgentRecord` referenced a `model`
identifier that was never declared in the function scope, breaking
`bun typecheck`. The reducer that consumes the resulting `turn-duration`
event does not look at `model` on the event itself, so the shorthand was
dead code. Remove it to restore typecheck.
* refactor(web): simplify turn-duration matcher with findLastIndex
Replace the imperative reverse-scan loops in the `turn-duration` reducer
branch with `findLastIndex`. The previous fallback also had an awkward
double-loop that mutated the matched block in place; using an index plus
a single immutable update keeps the block reference clean and makes the
match priority (id-prefix > tool-call id > last assistant-like) explicit.
Behaviour is unchanged — existing reducer tests cover both the messageId
match and the fallback paths.
* fix(web): preserve per-message model across mid-session model switches
The metadata footer fell back to `Session.model` from chat context when a
message did not carry its own `model`. That session value mutates when
the user switches models mid-session, so older messages were relabeled
with the latest model — including Codex/local assistant paths
(`AGENT_MESSAGE_PAYLOAD_TYPE`) that don't populate `msg.model`.
Drop the mutable-context fallback: pass `messageModel ?? null` to
`MessageMetadata` and let it omit the model line when no per-message
value is available. This is correct behaviour for messages whose
producer didn't record a model, and avoids ever attributing a message
to a model that didn't generate it.
Also remove the now-unused `useHappyChatContext` import in this file.
Add reducer invariants to lock in the data flow:
- `preserves per-message model across mid-session model switches`
- `leaves model undefined when message lacks per-message model`
* fix(web): keep tool-block reference identity when applying turn-duration
`ensureToolBlock` stores the same `ToolCallBlock` instance in both
`toolBlocksById` and `blocks`. The earlier refactor cloned the matched
block via `blocks[foundIndex] = { ...b, durationMs }`, which left the
map pointing at the stale original. A subsequent permission/result
mutation through `ensureToolBlock` would then update the stale map
object while the rendered `blocks` entry never sees the completion or
result, causing tool cards to miss state transitions.
Mutate the matched block in place instead — same in-place pattern the
reducer used before — and gate the assignment on the kinds that carry a
`durationMs` field so TypeScript narrows correctly.
Add an invariant test that fires a `turn-duration` event at a tool-call
block and asserts the rendered block and `toolBlocksById.get(...)`
remain the same object reference.
* fix(web): do not render service_tier as the model id
`MessageMetadata` previously fell back to `usage.service_tier` as the
"model" when no per-message `model` was available, so messages without
their own model id could surface labels like `Model: standard_only` —
service_tier is tier metadata, not a model.
Render the model line only when a real `model` is present; if a
non-`standard` `service_tier` is the only signal, surface it as a
separate `Tier: <tier>` label so it is not mistaken for the model.
The standard tier is the implicit default and is never rendered alone.
Extract the label-building logic into `buildMessageMetadataLabels` so
it can be unit-tested without a DOM. Add tests covering: model present,
model missing with non-standard tier, default standard tier, model with
non-standard tier appended, and the empty-input case.
* fix(web): metadata toggle ignores clicks on nested interactive controls
The bubble-level click handler that opens the metadata footer wraps
interactive descendants — tool-card buttons, retry buttons, dialog
triggers (Radix `role="button"`), and the Markdown code-copy button.
Clicking any of those flips the metadata footer as a side effect, even
when the descendant is the actual target of the user's intent.
Extract the closest-ancestor check into a small `metadataToggle` helper
and route both `AssistantMessage` and `UserMessage` click paths through
it. The toggle bails out when the click target sits inside any
`button`, `a`, `input`, `textarea`, `select`, or `[role="button"]`
ancestor; plain message-body text still toggles as before.
Add unit tests covering: button target, nested span inside a button,
`role="button"` Radix-style trigger, anchor/input/textarea/select form
controls, plain message-body text (no toggle), and a non-HTMLElement
target.
* fix(cli): preserve messageId on system/turn_duration record
`web/src/chat/normalizeAgent.ts` matches each `turn-duration` event to
the assistant block carrying the same `data.messageId`. Claude code
emits that field on the `system/turn_duration` record, but
`RawJSONLinesSchema`'s system branch did not declare `messageId`, so
Zod stripped it before the cli forwarded the record to the hub. The
matcher then fell back to "the last visible block", which is wrong for
interleaved/tool-heavy turns and silently attaches the duration to the
wrong assistant block.
Add `messageId: z.string().optional()` to the system schema so the id
survives parse and reaches the web reducer. Tests cover the preserved
case, the legacy case without `messageId`, and the previously-fixed
`message.model` case so Zod strip regressions on adjacent fields stay
locked in.
* fix(web): metadata toggle accepts SVG event targets
`isClickOnNestedControl` only walked up via `closest` when the click
target was an `HTMLElement`. The copy / retry / Markdown code-copy
buttons render SVG icons, so clicking the icon makes the event target
an `SVGElement` (not an `HTMLElement`) — the guard returned false and
the bubble-level click flipped the metadata footer anyway.
Widen the type check to `Element`, which is the common super-class of
both `HTMLElement` and `SVGElement` and also exposes `closest`. Plain
text targets and non-Element targets still behave as before.
Add a regression test that mounts an icon-only button (`<button><svg>
<path/></svg></button>`) and asserts both the `<svg>` and `<path>`
targets walk up to the enclosing button.
* refactor(cli): rely on Zod passthrough for jsonl envelopes
`RawMessageSchema` and the `system` branch of `RawJSONLinesSchema` were
declared with Zod's default `strip` mode, so any field the cli did not
explicitly enumerate was silently dropped before the hub forwarded the
record. The metadata pipeline lost `message.model` and
`system/turn_duration.messageId` exactly that way, and each gap took a
separate fix.
Switch both schemas to `.passthrough()` so undeclared fields survive
parse and reach the web reducer verbatim. Future SDK additions no
longer require another schema patch.
Add tests asserting that unknown keys on assistant messages and
unknown keys on system records (alongside the existing `messageId`
case) are preserved end-to-end through the schema.
* refactor(web): clean up dead metadata propagation surface
Several knobs were added to thread metadata through the chat tree but
ended up unused or redundant; consolidate them so the data flow has a
single canonical path.
- Drop the unreachable `data.type === 'result' && data.subtype ===
'success'` branch in `normalizeAgentRecord`. Claude's `result`
records are consumed by `claudeRemote` as session-completion signals
and never forwarded to the hub; the cli `RawJSONLinesSchema`
discriminator does not include `result`, so these records are
rejected before they reach `normalizeAgentRecord` either way.
- Stop threading `invokedAt` through the inner `normalizeAssistantOutput`
/ `normalizeUserOutput` / `normalizeAgentRecord` calls. Every caller
in `normalizeDecryptedMessage` already overwrites it via the outer
spread, so the inner copies were dead writes. Set `invokedAt` only at
the outer boundary.
- Remove the `model?: string | null` field from `HappyChatContextValue`
and the `model` prop on `HappyThread` / `SessionChat`. Its only
consumer (`AssistantMessage` mutable-fallback) was removed when the
per-message model attribution fix landed; the prop has no readers
now.
- Match the existing `as Partial<HappyChatMessageMetadata> | undefined`
cast pattern in `AssistantMessage` and `UserMessage` instead of the
non-`Partial` cast that pretended every field was present even when
`custom` is undefined.
- Rename `AgentEvent.turn-duration.messageId` to `targetMessageId` so a
reader does not confuse the duration's target with the surrounding
envelope id; the wire field on Claude's `system/turn_duration` record
stays `messageId` (vendor name) and is mapped at the normalize
boundary.
No behaviour change. All existing tests pass.
* fix(web): turn-duration matcher and cli-output merge precedence
Two reducer-level metadata-correctness bugs surfaced during a hostile
self-review.
1. Turn-duration matcher silently dropped the duration when
`targetMessageId` resolved to a non-duration-bearing block. The
existing pipeline did `findLastIndex(b => b.id === targetId || ...)`
first; if that hit an `agent-event` or `user-text` block (id-prefix
collision), the kind guard at the assignment site failed and the
duration was never attached. The fallback search ran only when the
first pass returned -1, not when the kind check rejected the match.
Fold the kind filter into every search predicate via a typed
`isDurationTarget` helper so the priority `target-bearing match >
tool-call id > last duration-bearing block` is exhaustive.
2. `mergeCliOutputBlocks` had asymmetric metadata precedence between
the command-name block (`prev`) and the stdout follow-up (`block`):
`invokedAt` and `model` preferred prev, but `durationMs` and `usage`
preferred block. Only the command-name block carries first-class
metadata; the stdout follow-up is a synthetic split. Use prev as the
primary source uniformly and fall back to block only when prev is
missing the field.
Tests cover the fallback path on the matcher and both precedence
scenarios on the merger.
* fix(web): preserve tool-call invokedAt across tool-result update
`ensureToolBlock` is called twice for the same tool: first with the
seed from the assistant's tool-use block, then with the seed from the
matching tool-result message. The second call's `seed.invokedAt` came
from the tool-result message and was unconditionally overwriting the
tool-call's original invokedAt. The rendered "Invoke" timestamp on a
tool card therefore showed when the result was processed, contradicting
the column header.
Guard the assignment so the timestamp survives the second call —
`existing.invokedAt ??= seed.invokedAt` semantics — while still letting
the first call set the value when the tool is created. `durationMs`,
`usage`, and `model` continue to overwrite because their values come
from the result message's usage block and are intentionally newer.
Add a regression test that fires a tool-use followed by a tool-result
with a later invokedAt and asserts the tool block keeps the original.
* fix(web): metadata footer UX, accessibility, and label hardening
Bundle the remaining UI surface fixes for the metadata footer.
- Make the bubble interactive only when there is metadata to disclose.
Without the guard, every non-Claude session bubble (Codex / Cursor /
Gemini, none of which populate `model`/`usage`/`durationMs` in the web
layer) showed a pointer cursor and reacted to clicks even though
`MessageMetadata` rendered nothing — false-positive interactivity.
- Add keyboard support: when the bubble is interactive it now exposes
`role="button"`, `tabIndex=0`, `aria-expanded`, and an `Enter`/`Space`
key handler so screen readers and keyboard-only users can disclose
the footer the same way mouse users do.
- Fix nullish-vs-falsy bugs in the label builder: a 0 ms turn or a 0
unix-epoch invokedAt no longer hides their lines. Use explicit
`!= null` / `>= 0` checks.
- Rename the token total to "billable tokens" so the explicit exclusion
of cache I/O is signalled in the label rather than implied by the
number alone.
- Tag the queued/sending status spans with `role="status"` (and an
accessible label) so they are announced by AT and so the metadata
toggle's `closest('button, ..., [role="status"]')` filter does not
accidentally fire when a user clicks a status icon.
- Add the native `<summary>` element and `[role="status"]` to the
toggle's nested-control selector. Tool cards already render their
expandable bodies as `<details><summary>` — clicking the summary now
expands the disclosure without also flipping the metadata footer.
Tests cover: native `<summary>` target, `role="status"` target, the
billable label, durationMs=0 surfaced, invokedAt=0 surfaced,
invokedAt=null/undefined hidden.
* fix(web): expose cli-output metadata via dedicated toggle button
CliOutputBlock renders the entire card as a Dialog trigger <button>, so
the bubble-level click handler on the cli-output branch never opened the
metadata footer by mouse — every click landed inside that button and
isClickOnNestedControl bailed out. The wrapping div with role="button"
was also a nested-interactive a11y anti-pattern.
Drop the wrapper's role/onClick/tabIndex/keyDown on the cli-output
branch and render an explicit "Show metadata" / "Hide metadata" button
beneath the card. The dialog trigger keeps its full hit area; the
metadata footer is now reachable by both mouse and keyboard.
* fix(web): exclude toggle wrapper from nested-control guard
The bubble-level toggle wrappers in AssistantMessage / UserMessage
carry role="button" for keyboard accessibility. Without excluding
currentTarget, closest('[role="button"]') from any inner click matches
the wrapper itself and the toggle bails out — making the metadata
footer unreachable for mouse users (keyboard Enter/Space still worked,
which is why unit tests and bot review missed it).
Walk currentTarget out of the match: a nested control is one whose
closest matching ancestor is *not* the wrapper itself.
* fix(web): apply nested-control guard on keyboard activation too
The mouse path bailed via isClickOnNestedControl, but the keyboard path
on the metadata-toggle wrapper did not. Pressing Enter or Space on a
focused descendant control (e.g. Markdown code-copy button) bubbled the
keydown up and the wrapper toggled metadata alongside the descendant's
own activation.
Generalize the helper to isNestedInteractiveEvent over both
MouseEvent and KeyboardEvent and call it from onMetadataKeyDown in
AssistantMessage and UserMessage.
|
||
|
|
e76738aa5a |
fix(codex): improve web rendering for Codex events (#544)
* 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 |
||
|
|
e7ba48c761 |
fix(web): normalize non-sidechain text-only array user output as user message (#409)
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> |
||
|
|
73fa846df3 |
fix(web): drop "No response requested." assistant messages (#402)
* 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> |
||
|
|
36022a0a1a |
fix(web): filter system-injected XML tags from rendering as raw text (#387)
* 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 |
||
|
|
2c20b04bec | fix(web): suppress Task tool prompt text from leaking into chat (#372) | ||
|
|
a02f908dc5 | fix test | ||
|
|
d76b1a6ac0 | refactor: introduce model-agnostic agent interfaces (#323) | ||
|
|
7a8f1c3e87 |
feat: add compact_boundary event support in web UI
https://github.com/tiann/hapi/issues/108#issuecomment-3799439152 |
||
|
|
8137ce4f23 |
feat: add microcompact_boundary event handling in web UI
Adds support for Claude Code's microcompact_boundary system message type to display context microcompaction events with token savings information in the chat UI. Shows a package icon with formatted token count (e.g., "Context compacted (saved 32K tokens)"). close #108 |
||
|
|
9922588c6f |
refactor: consolidate utility functions into shared package
Move duplicate isObject, asString, asNumber, and safeStringify functions from multiple modules into a centralized shared/src/utils.ts module and update imports across cli, server, and web packages. This eliminates code duplication and improves maintainability. |
||
|
|
940d7b8233 | feat(web): add turn-duration event display | ||
|
|
f7d161e6cf |
feat: add api-error event support with folding for retry messages
Introduces support for displaying API error system messages in the web UI with automatic folding of consecutive retry messages to show only the latest state. |
||
|
|
34cfe49f87 |
refactor: extract chat normalization and reducer logic, cleanup legacy credentials
- Split normalize.ts into focused modules: normalizeAgent, normalizeUser, normalizeUtils - Extract reducer.ts into specialized modules: reducerCliOutput, reducerEvents, reducerTimeline, reducerTools - Remove legacy credentials support (zod schema, readCredentials, writeCredentialsLegacy) - Refactor NewSession component from monolithic file into modular subcomponents - Improves code organization and maintainability across chat and authentication layers |