mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
44390af35c1bdc683ee09e4e85ed2e40b002e080
117
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8eac26726b | Release version 0.23.4 | ||
|
|
782b523fb0 | Release version 0.23.3 | ||
|
|
db1444fe2e | Release version 0.23.2 | ||
|
|
b74a11ecc3 | Release version 0.23.1 | ||
|
|
2211888f04 | Release version 0.23.0 | ||
|
|
1c834607a2 | Release version 0.22.3 | ||
|
|
4c76668a6c | refine message actions and metadata | ||
|
|
942a1dfff8 | Release version 0.21.0 | ||
|
|
65e1708c78 |
feat(web): mermaid diagram lightbox on click (#741)
* feat(web): mermaid diagram lightbox on click Click rendered mermaid blocks in chat to open a zoomable full-screen viewer. Re-renders from source in the modal with the current theme. Closes #737. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to viewport on open Auto-scale diagrams to fill the viewer instead of opening at intrinsic mermaid size. Reset returns to fit; zoom label is relative to fit (100%). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to device screen not inner panel Use visualViewport for fit scale, full-screen pan layer, and a floating toolbar so the diagram can use the whole display. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): show mermaid lightbox by reusing inline SVG Second mermaid.render on open often left a 0×0 SVG while fit scale was computed from the loading placeholder. Reuse the inline SVG in the modal and measure viewBox with retried fit-to-screen. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): uniquify mermaid SVG ids in lightbox clone Inlining the same mermaid markup twice duplicates element ids and breaks url(#ref) resolution in the modal copy. Prefix ids and hrefs for lightbox only. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): give mermaid lightbox SVG explicit dimensions Mermaid emits width="100%" with max-width in px; that collapses to 0×0 inside the centered lightbox layer. Derive width/height from viewBox for the uniquified lightbox clone. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): render mermaid lightbox via isolated SVG data URL String id rewrites broke mermaid's embedded CSS so only labels appeared zoomed. Rasterize the inline SVG to a data-URL img instead of duplicating markup in the DOM. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): lightbox re-renders SVG for sequence diagrams Data-URL images drop or blank some mermaid diagram types (sequence). Re-render with a modal-specific id into inline SVG on a code-bg panel, and add sequence theme variables for dark/light. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): mermaid lightbox uses inline SVG in shadow DOM Reuse the inline render in an isolated shadow root so sequence CSS stays intact, and fit the viewport from viewBox dimensions instead of the loading placeholder or width="100%" layout. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright lightbox coverage per mermaid diagram type Add e2e harness and a script that opens the lightbox for each diagram kind (flowchart through kanban). Fit uses inline getBBox() so compact charts like gitGraph fill the viewport. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): bounded Playwright via webServer, fix gantt fit sizing Playwright owns Vite lifecycle (no agent-spawned dev server). Fit uses viewBox unless viewBox padding is excessive (gitGraph); wide charts use width-based coverage in e2e. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): gitignore Playwright test-results Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): address PR 741 bot feedback (typecheck, fit floor, gitignore) Guard lightbox open when svg is null; allow fit scale down to 0.01 while keeping 0.25 minimum for manual zoom; ignore Playwright test-results/ correctly. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright asserts click expands diagram vs inline Measure inline vs lightbox bounding box after click; require visible growth (area ratio or max dimension) plus dialog + shadow SVG content. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright against live HAPI session for mermaid lightbox Add seed script for a dedicated chat session, live hub Playwright suite (HAPI_LIVE=1), and dogfood doc. Live tests fail until driver serves shadow-DOM lightbox (catches gray-box regression on stale bundles). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): undo wrapper transform in lightbox fit; carry fit floor in zoom Resolves PR #741 review threads (HAPI Bot Major): 1. measureSvgIntrinsicSize / measureContentSize prefer intrinsic dimensions (viewBox -> width/height attrs -> img.naturalSize) before getBoundingClientRect. When the rect is the only signal, divide by scaleRef.current so the 50/200ms refit retries stop compounding with the wrapper's scale(...) transform. Large diagrams no longer jump tiny or oversize after async render completes. 2. Interactive zoom (wheel/keys/buttons/pinch) now clamps with Math.min(MIN_SCALE, baseScaleRef.current). A diagram fitted below the normal 25% floor stays reachable instead of snapping back to 25% and clipping. Zoom-out button disabled threshold uses the same min. 3. Add Vitest coverage for both helpers (intrinsic precedence, scale-aware rect fallback, divide-by-zero guard) so regressions surface without needing the full Playwright stack. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): mermaid seed refuses to wipe non-fixture sessions HAPI Bot Major (PR #741): SESSION_ID is documented as overridable, and the script unconditionally deletes every message for the target session before seeding fixtures. If pointed at a real session id, that's silent data loss. Refuse to proceed when an existing session id has a tag other than 'mermaid-lightbox-e2e'. New ids and the canonical fixture session still seed normally; real sessions throw before any DELETE runs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): normalize mermaid svg for lightbox shadow root Mermaid emits width="100%" on every diagram. Inside a shadow root whose host has no explicit size, that collapses to zero in Chromium for most diagram types - only ones that ship pixel attrs (e.g. journey) happen to render. Operator confirmed on the live driver: every diagram except journey opened to a grey rounded square. MermaidLightboxSvg now runs normalizeMermaidSvgForStandaloneDisplay before injecting (strips width/height="100%", bakes viewBox dims as pixels) and sets :host{display:inline-block} so the host sizes to the SVG. Inline svg in chat is unchanged - only the lightbox copy is normalized. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep mermaid lightbox content below the toolbar Operator screenshot showed the diagram top (e.g. pie 'Pets' title) clipped behind the toolbar bar. Two causes: 1. getScreenFitSize used the full viewport height, so the fit scale sized the diagram to fill an area the toolbar overlapped. 2. The viewport (drag/zoom area) was inset-0; content centered on the full viewport center, not the visible region's center, pushing the top behind the toolbar. Measure the toolbar with a ResizeObserver, subtract its height from the fit calculation (clamped at zero), and start the viewport region below the toolbar (top: toolbarHeight). Fit scale recomputes whenever toolbar height changes. Adds Vitest coverage for getScreenFitSize reserved-top math. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): guard ResizeObserver before constructing it HAPI Bot Major (PR #741): Vitest jsdom does not polyfill ResizeObserver, so the toolbar measure effect throws ReferenceError when the existing mermaid-diagram React tests open the lightbox. Same code path is also brittle in any browser/webview without the API. Fall back to plain window 'resize' listener when ResizeObserver is absent. Toolbar height won't auto-update on element resize without it, but the lightbox still renders and the resize listener catches the common viewport-rotation case. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): live mermaid playwright wrapper runs from repo root HAPI Bot Minor (PR #741): the wrapper sets cwd to scripts/, but the test:mermaid-lightbox:live npm script lives in the repo-root package.json, so spawning npm there exited before Playwright started. Switch cwd to the repo root and drop the unused WEB_DIR constant. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): accept signed viewBox values in mermaid lightbox normalize HAPI Bot Minor (PR #741): the viewBox regex only matched digits, dots, and spaces, so a valid viewBox with negative origin (e.g. '-8 -8 640 480') returned null. normalizeMermaidSvgForStandaloneDisplay then became a no-op and left width='100%', re-introducing the zero-sized lightbox render this PR is meant to fix for the affected diagrams. Switch to the bot's suggested regex (signed numbers, single or double quotes, comma or space separators) and reject NaN parts. Adds Vitest coverage for signed origins, single quotes, comma separators, the malformed/no-viewBox null paths, and an end-to-end normalize test that fails against the old regex. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align @playwright/test on 1.60.0 across workspaces HAPI Bot Major (PR #741): web/package.json pinned @playwright/test at 1.49.1 while the root workspace and bun.lock were on 1.60.0. The mismatch surfaced after rebasing onto upstream/main, where the root had already moved to 1.60.0 while my web devDependency lagged from an older commit. A frozen install would reject the lockfile and the new web e2e script could resolve a different Playwright than root scripts. Bump the web devDependency to 1.60.0 and regenerate bun.lock so all workspaces share one Playwright version. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): move mermaid playwright fixtures out of public HAPI Bot Minor (PR #741): the e2e and smoke fixtures lived under web/public, so Vite copied them verbatim into web/dist and the hub asset generator embedded them in production bundles. Both pages import Vite dev-only paths (/@react-refresh and /src/dev/...), so the production /mermaid-lightbox-{e2e,smoke}.html routes would 404 on those imports. Move both fixtures to web/e2e-fixtures/ to match the existing scratchlist-fixture pattern (relative ../src/dev import, served by Vite at /e2e-fixtures/...) and update the Playwright spec to hit the new path. Build now ships 112 PWA precache entries instead of 114 (both fixtures excluded from dist). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
e23ae1b265 |
feat: add Pi Coding Agent support (#862)
* docs: spec for hapi-pi-agent-backend
* docs: spec retrospect for hapi-pi-agent-backend
* docs: plan for hapi-pi-agent-backend
* docs: plan retrospect for hapi-pi-agent-backend
* feat(pi): add hapi pi command with JSONL transport and event converter
- PiTransport: spawn pi --mode rpc, JSONL stdio, ENOENT/EPIPE handling
- PiEventConverter: Pi AgentEvent → HAPI AgentMessage conversion
- runPi: session lifecycle, dual-track event routing, model switching
- pi command: CLI registration with PI_PERMISSION_MODES
- Shared: add 'pi' to AGENT_FLAVORS, FLAVOR_CAPS, FLAVOR_LABELS
30 tests passing (15 transport + 15 converter)
* fix(pi): add Pi RPC types, fix double-cleanup/double-start/converter safety net
- Add cli/src/pi/types.ts with PiAgentEvent/PiResponseEvent discriminated unions
- PiTransport: constructor uses options object, double-start guard, drop log
- PiEventConverter: typed events via type assertions, top-level try/catch
- runPi: safeCleanup guard prevents double-cleanup race, sendAgentMessage
for converted events, keepAlive() for session pings
- 33 tests passing
* docs: dev phase reviews and test results for hapi-pi-agent-backend
- Business logic review: pass (0 must_fix)
- Standards review: pass (0 must_fix)
- Taste review: P0 types issue fixed in code
- Robustness review v2: pass (v1 3 MUST_FIX all fixed)
- Integration review: pass (0 must_fix)
- Test results: 33 passing, all type errors resolved
* docs: taste review v2 pass after type definition fixes
* docs: dev retrospect for hapi-pi-agent-backend
* test: test execution for hapi-pi-agent-backend (20/20 pass)
* fix: add taste_review symlink for gate pattern match
* docs: test retrospect for hapi-pi-agent-backend
* fix(web): add pi to MODEL_OPTIONS Record type
* ci: PR and CI evidence for hapi-pi-agent-backend
* docs: overall retrospect for hapi-pi-agent-backend (all 5 phases)
* test(pi): add buffer split, missing fields, and handleResponse tests
- PiTransport: buffer cross-chunk reassembly test
- PiEventConverter: tool_execution_end with missing result/toolCallId
- handleResponse: 10 tests covering all branches (error, get_state,
set_model, new_session, abort, prompt, unknown command)
- Extract handleResponse to accept onUpdate callback for testability
- Total: 46 tests passing (was 33)
* fix(pi): set requiresRuntimeAssets to false — pi runs as subprocess, no native tools needed
* refactor(cli): lazy import ensureRuntimeAssets to reduce startup overhead
* docs: add 15 manual E2E protocol test cases (TC-4-xx) based on real Pi RPC capture
- TC-4-01 to TC-4-15: manual tests covering tool execution, thinking
lifecycle, multi-turn, abort, error scenarios, model switch, cleanup
- Priority: P0 (tool fields, failure, thinking, multi-turn, abort)
> P1 (basic conversation, write tool, model switch, usage) > P2 (edge cases)
- Includes actual Pi RPC event sequence from live capture as reference
- e2e-test-plan.md updated with test environment setup instructions
- Total test cases: 35 (6 unit + 14 integration + 15 manual)
* test: E2E protocol test results for hapi-pi (11/15 pass)
P0/P1 automated tests (8/8 pass):
- TC-4-01: Basic text conversation ✓
- TC-4-02: Tool read (field names verified) ✓
- TC-4-03: Tool write (file created) ✓
- TC-4-04: Tool failure (isError=true) ✓
- TC-4-05: Thinking lifecycle + usage ✓
- TC-4-06: Multi-turn context retention ✓
- TC-4-07: Abort generation ✓
- TC-4-14: Token count ✓
- TC-4-15: Extension UI events ignored ✓
P2 results:
- TC-4-10: Invalid token → 401 ✓
- TC-4-12: Ctrl+C cleanup, no orphans ✓
- TC-4-08: ENOENT (harness issue, exit code correct)
- TC-4-11: set_model not supported by Pi (success=false)
- TC-4-13: Pi crash (harness output capture issue)
* test: fix TC-4-11 result — Pi set_model works with correct provider/modelId
Previous test used invalid provider='' + modelId='deepseek-chat'.
Re-tested with provider='deepseek' + modelId='deepseek-v4-flash':
- set_model success=true
- model switched glm-5.1 → deepseek-v4-flash
- subsequent prompt confirmed working
Final E2E results: 12/15 PASS, 2 FAIL (test harness), 1 SKIP
* chore: remove .xyz-harness/ from git tracking, add to .gitignore
Local harness workflow artifacts should not be tracked in the repo.
* fix(pi): resolve web UI bugs for hapi-pi integration
Five bugs fixed for end-to-end pi session via hapi web UI:
1. runner buildCliArgs: add 'pi' branch to spawn correct command
(was falling back to 'claude', launching wrong agent)
2. runPi: implement real keep-alive (2s interval) to prevent hub
30s timeout marking session inactive
3. runPi: bump keep-alive to active state during agent/turn_start
4. sessionResume: add 'pi' to flavor switch and resume condition
(was returning undefined, causing 'cannotResume' on inactive session)
5. PiEventConverter: emit codex-compatible {type:'message',message:...}
/{type:'reasoning',message:...} with streamId; dedup by skipping
text_start/text_end (only send deltas) to avoid triple-rendered text
6. PiTransport: fallback to stdout 'end' event when child process
close event doesn't fire (bun spawn quirk)
Verified end-to-end: web UI shows pi reasoning + reply correctly,
session stays online, no duplicate text.
* fix(pi): address 4 web UI display bugs in hapi-pi integration
Three of four follow-up bugs reported after the initial fix (6c28949):
1. Stuck in 'queued' status — fix
Pi's runner doesn't use MessageQueue2, so the base session's
onBatchConsumed hook never fires. Add a FIFO of pending localIds
in runPi and emit messages-consumed on agent_start. turn_start
is intentionally skipped (it can fire multiple times per agent
run after tool calls). A prompt rejection from Pi also consumes
the localId so the next prompt isn't poisoned.
2. AI thinking only displays ':' — fix
Pi emits pure incremental deltas (text_delta / thinking_delta)
per token. The web reducer dedupes reasoning by streamId WITHIN
one message's content array only — separate wire messages
produce separate renders. Without accumulation, 50 deltas = 50
reasoning renders, of which the reducer keeps only the last
delta (a single character like ':').
3. Output text on separate lines — fix
Same root cause as #2 but for text: the reducer appends each
text AgentMessage as a new agent-text block (no dedup), so 50
deltas become a 50-row character-by-character column.
4. Tool call execution status (in_progress -> completed)
The tool result wire CodexMessage type is 'tool-call-result'
(with callId + is_error?); the internal AgentMessage 'tool_result'
is converted to that. Status mapping is preserved.
Implementation: extract a PiMessageAccumulator class (testable in
isolation) that mirrors codex's ReasoningProcessor pattern:
- message_start resets state and streamId
- text_delta / thinking_delta append to internal text / reasoning
- text_start/thinking_start/text_end/thinking_end ignored (they
carry full partial state — would duplicate)
- message_end flushes (max 1 reasoning + 1 text message, in order)
- turn_end safety net flushes if active
- flushIfActive() exposed for transport close / crash
The converter now routes AgentMessage through convertAgentMessage
so the wire format is codex-shaped (matches opencode/gemini/kimi
path). AgentMessage 'text' and CodexMessage 'message' both gain
optional id; convertAgentMessage preserves caller-provided id for
streamId-based dedup on the web side.
Tests: 16 new PiMessageAccumulator tests + 5 updated
PiEventConverter tests + 4 messageConverter tests, all passing.
Full suite: 909/910 (1 unrelated macOS path normalization). tsc
clean.
* fix(pi): review round 1 - 1 must-fix issue
The web session-resume helper referenced metadata.piSessionId, but the
shared MetadataSchema does not define the field, and the back-end has no
path to populate it (Pi session resume is out of scope per spec.md).
This caused web typecheck to fail and would also have produced a
runtime 'resume_unavailable' from the hub if a user tried to resume a Pi
session that had any user messages (the stale 'flavor === pi' branch in
inactiveSessionCanResume claimed resume was supported).
Revert the two early Pi branches from the web resume helper. Add a
comment pointing at the spec and noting what to undo when back-end
resume ships (re-add 'case pi' + 'piSessionId' on MetadataSchema +
extend hub resolveAgentResumeId).
* fix(pi): review round 2 - 4 must-fix issues
1. cli/src/runner/run.ts buildCliArgs: stop forwarding --resume to the pi
binary. Pi session resume is out of scope (no piSessionId on
Metadata), so forwarding would create an orphan session the hub can't
track. Hub already returns null from resolveAgentResumeId for
flavor='pi' and falls through to fresh spawn; this just hardens the
runner layer to match.
2. cli/src/pi/runPi.ts: cache currentProvider from get_state and use it
for subsequent set_model RPCs. Pi's set_model requires both provider
and modelId, but the bootstrap-time code emitted provider: '' which
Pi rejects. The bootstrap-time model is still applied by Pi at
startup, so suppressing set_model until get_state arrives is a no-op
for same-model configs rather than a wrong-model emit.
3. web/src/components/AssistantChat/modelOptions.ts: add explicit pi
branches to getModelOptionsForFlavor and getNextModelForFlavor.
Without them, Pi sessions fell through to the Claude preset cycler,
which would push sonnet/opus ids into a Pi session via
set-session-config. Mirrors the opencode handling introduced earlier.
Tests added/updated: buildCliArgs covers pi + claude resume; handleResponse
mirror test covers provider caching; modelOptions tests cover pi
no-fallback behavior for both option list and cycler.
* fix(pi): add session resume support and fix review issues
- Add piSessionId to MetadataSchema (shared/src/schemas.ts)
- Persist piSessionId from get_state response to metadata (cli/src/pi/runPi.ts)
- Pass --session-id to Pi spawn on resume (cli/src/pi/runPi.ts)
- Add pi branch to resolveAgentResumeId (hub/src/sync/syncEngine.ts)
- Add case 'pi' to resolveAgentSessionIdFromMetadata (web/src/lib/sessionResume.ts)
- Replace pi resume skip guard with --session-id forwarding (cli/src/runner/run.ts)
- Preserve piSessionId in pickExistingSessionMetadata (cli/src/agent/sessionFactory.ts)
- Add pi badge to AgentFlavorIcon (web/src/components/AgentFlavorIcon.tsx)
- Fix transport.onClose crash-marking on normal shutdown (cli/src/pi/runPi.ts)
* fix(pi): review round 1 - 3 must-fix issues
- resume.ts: add pi branch to dispatchLocalResume() so hapi resume
dispatches to runPi instead of falling through to cursor
- runPi.ts: accept existingSessionId and use bootstrapExistingSession
when resuming, matching other agents' pattern
- agentCommandOptions.ts: parse --session-id in addition to --resume
so runner-spawned pi resume actually forwards the session ID
- types.ts: export PiPermissionMode alongside other agent permission
mode types for consistent import convention
* fix(pi): review round 2 - 2 must-fix issues
* refactor(workflow): improve pi-adaptation-review-loop robustness
- Switch from structured output to file-based JSON output for reliability
- Replace per-round file limit (20→30) with clear wording (remove misleading split-commits instruction)
- Return { data, error } from readResultFile() to surface parse/validation failures in abortReason
- Fix lastMustFix sentinel: initialize to null, use ?? for explicit N/A reporting
- Add getAgentDirs() to dynamically discover agent dirs from cli/src/
- Document rollbackTo() atomic-round design intent
- Add isValidIssue() validation, runFinalCleanup() helper, git repo pre-check
* test(pi): add coverage for pi flavor across shared, cli, and web
- shared/flavors.test.ts: pi/kimi capability, label, known, supports
- shared/modes.test.ts: PI_PERMISSION_MODES contract, per-mode checks
(7-mode allowed/denied matrix)
- web/AssistantChat/modelOptions.test.ts: pi shortcut vs Claude
cycler, normalize filter (auto/default/whitespace), kimi/cursor/
opencode cross-flavor consistency
- web/lib/sessionResume.test.ts: piSessionId resolver, cross-flavor
stale-id protection, inactiveSessionCanResume for pi, regression
coverage for all 6 other flavors
- web/components/AgentFlavorIcon.test.tsx: pi badge styling
(bg-[#5b21b6]), Un fallback, case/whitespace normalize,
className override
- cli/commands/agentCommandOptions.test.ts: --session-id
(pi-specific flag), --resume alias, PI mode validation,
--yolo vs explicit-mode priority
137 new test cases, all passing. Full suite: 96 files / 933 tests
green (unrelated apiMachine.test.ts macOS /private/var path issue
remains as documented in handoff).
* feat(pi): implement P0 — context budget bar + dynamic model discovery
P0-1: Context Budget Bar
- Add pi branch to modelConfig.ts getContextBudgetTokens()
- Conservative 200K default context window for Pi sessions
P0-2: CLI-side model discovery
- Add get_available_models to PiRpcCommand type
- Auto-send get_available_models after get_state in runPi.ts
- Cache model list and push to session metadata
- Register ListPiModels RPC handler with promise-based transport query
P0-3: Hub-side routing
- Add listPiModelsForSession to rpcGateway and syncEngine
- Add REST endpoint GET /sessions/:id/pi-models (pi sessions only)
P0-4: Web-side rendering
- Add PiModelSummary type to shared apiTypes
- Add usePiModels hook (TanStack Query, stale 60s)
- Add getSessionPiModels to API client
- Add sessionPiModels query key
- Wire piModelOptions into SessionChat availableModelOptions
- Model dropdown renders discovered models or falls back to Default
* fix(pi): address code review findings + pre-existing test issue
Review fixes:
- Fix race condition in sendPiRpcAndWait: use incremental id as key
instead of command type, preventing resolver overwrite on concurrent
calls (e.g. auto-discovery + ListPiModels RPC)
- Extract parsePiModels() to eliminate duplicated model parsing logic
between handleResponse and ListPiModels RPC handler (DRY)
- Add resolvePendingRpc() call in error response path to prevent
promise leaks when Pi rejects an RPC with an id
- Add piModelsState.error guard to onModelChange in SessionChat,
matching the pattern used by codex and cursor flavors
Pre-existing fix:
- Fix apiMachine.test.ts symlink assertion on macOS (/var vs
/private/var) by applying realpathSync to the expected path
* feat(pi): P1 — session rename sync, thinking level UI, skills/commands
P1-1: Session Rename → Pi notification
- Add set_session_name to PiRpcCommand
- Register RenamePiSession RPC handler in CLI
- Hub syncEngine.renameSession now forwards to Pi CLI for active sessions
- Hub rpcGateway + REST endpoint added
P1-2: Thinking Level support
- Add Pi thinking level constants to shared/src/piThinkingLevel.ts
(off/minimal/low/medium/high/xhigh)
- Add ThinkingLevel capability to Pi flavor in flavors.ts
- sessionConfigRpc now supports effortMode for Pi thinking level
- runPi captures thinkingLevel from get_state and forwards via
set_thinking_level
- Hub effort endpoint accepts pi sessions (was claude-only)
- Web: piThinkingLevelOptions.ts + HappyComposer renders Pi options
when flavor=pi
P1-3: Skills/Commands discovery
- Add get_commands to PiRpcCommand, auto-discover after get_state
- Register ListPiCommands + ListSlashCommands RPC handlers in CLI
(maps Pi commands to HAPI SlashCommand format)
- Hub: listPiCommandsForSession + REST GET /sessions/:id/pi-commands
- Web: usePiCommands hook + api client + query keys
Also fixes:
- Pre-existing ZodError.errors → ZodError.issues in hub/socket/server.ts
- Updated test expectation for effort endpoint error message
* feat(pi): implement P2 features — steer, queue modes, history, native images
P2-1: Steer/Follow-up
- Track piIsStreaming state from agent_start/turn_start/turn_end/agent_end
- When streaming, onUserMessage sends steer instead of prompt
- Added PiSteer/PiFollowUp RPC methods + hub routing + REST endpoints
P2-2: Queue modes
- Added set_steering_mode/set_follow_up_mode to PiRpcCommand
- CLI RPC handlers with mode state tracking
- Hub routing + REST POST endpoints
- Web API client methods
P2-3: History replay
- Added get_messages to PiRpcCommand
- CLI handler converts Pi AgentMessage to PiMessageEntry format
- Hub RPC routing + REST GET /sessions/:id/pi-messages
- Web usePiMessages hook + query key
P2-4: Native image passing
- Added PiImageContent type for base64 image data
- extractPiImages() helper reads attachment files as base64
- prompt/steer commands now include images field
- Falls back to @path text reference for non-image/unreadable files
* feat(pi): implement P3 advanced features — compact, fork, clone, switch, stats, export
P3 features for Pi agent integration:
- Compact: compact RPC with custom instructions, set_auto_compaction toggle
- Fork: fork at entry ID, get_fork_messages for fork context
- Clone: clone current Pi session
- Switch Session: switch Pi to a different session by path
- Session Stats: get token counts, message counts, cost
- HTML Export: export session as HTML file
All features follow existing P2 pattern:
- CLI: RPC handlers in runPi.ts with sendPiRpcAndWait
- Hub: rpcGateway + syncEngine routing + REST endpoints
- Web: API client methods + query keys + type exports + hooks (stats, fork messages)
Total: 8 new REST endpoints, 9 RPC handlers, 6 web API methods
Typecheck: all 3 packages pass (cli+hub+web)
Tests: 1155 pass (263 hub + 803 web + 89 shared), 0 failures
* refactor(pi): clean up runPi.ts imports and readability
- Replace require('fs') with top-level import { readFileSync } from 'fs'
- Extract handleGetState() as standalone function from handleResponse
switch case (get_state case: 35 lines → 4 lines dispatch)
Typecheck: all 3 packages pass
Tests: 1066 pass (263 hub + 803 web), 0 failures
* fix(pi): remove native image passing, fix version pollution
- Remove extractPiImages helper and PiImageContent type: all
attachments now use @path text references via
formatMessageWithAttachments, consistent with every other agent
- Remove images field from prompt/steer/follow_up RPC commands
- Remove unused readFileSync import
- Restore cli/package.json version from test pollution
(0.0.0-integration-test-should-be-auto-cleaned-up-51369 → 0.20.0)
Typecheck: all 3 packages pass
Tests: 1286 pass, 0 failures
* refactor(pi): extract hub helper, unify web hooks, fix import style
- Hub: extract withPiSession helper eliminating boilerplate across 15
Pi REST endpoints (~400 lines → ~150 lines)
- Web: unify usePiForkMessages and usePiSessionStats to return
destructured typed fields matching usePiModels/usePiCommands pattern
- Web: move 15 Pi response types from inline import() to top-level
named imports in api/client.ts
- CLI: remove duplicate PiCommandSummary/PiCommandsResponse from
types.ts, re-export from @hapi/protocol/apiTypes
Typecheck: all 3 packages pass
Tests: 1286 pass, 0 failures
* chore: untrack .agents/skills and .pi, fix .xyz-harness in gitignore
* refactor: remove unused text message id from converter layer, update gitignore
* fix: update tests for pi resume support and text id removal
* fix: restore cursor resume branch in buildCliArgs
* refactor: remove pi-specific rename from syncEngine, align with other agents
* refactor: remove effort field from sessionConfigRpc, Pi self-handles RPC
Pi agent now self-handles SetSessionConfig RPC (like Claude) using
the existing field, instead of adding a parallel
field to the shared sessionConfigRpc helper which only knows about
.
- Remove effort/effortMode from sessionConfigRpc types and logic
- runPi.ts: self-register RPC handler with PiThinkingLevel validation
- Reuse resolveSessionConfigPermissionMode from sessionConfigRpc
* refactor: consolidate Pi RPC layer from 36 methods to 3 generics
rpcGateway: 12 methods → callPiRpc<T>
syncEngine: 12 passthroughs → callPiRpc<T> delegate
web client: 12 methods → callPiEndpoint<T>
routes: use engine.callPiRpc with RPC_METHODS constants
hooks: use callPiEndpoint, add missing type imports
* chore: revert unrelated apiMachine test change
* refactor: remove unused ThinkingLevel capability from flavors
Pi's thinking level is an effort variant, not a separate capability.
The ThinkingLevel constant and supportsThinkingLevel() had zero callers
— the frontend uses flavor-based branching for effort option rendering.
* refactor: drop Pi prefix from generic RPC method names
* refactor: remove 13 Pi RPC methods with no UI consumers
Steer: already handled by onUserMessage auto-routing
Follow-up: redundant with HAPI message queue
ListPiCommands/GetMessages/ForkMessages/SessionStats: no UI
Compact/SetAutoCompaction/Fork/Clone/SwitchSession/ExportHtml: no UI
SetSteeringMode/SetFollowUpMode: no UI
Kept: ListPiModels (has UI), SetSessionConfig, ListSlashCommands, Abort, Switch
Deleted: 4 web hooks, 13 RPC handlers, 12 REST routes, 13 rpcMethods entries
Net: -730 lines
* refactor: extract session.ts and loop.ts from runPi.ts
Restructure Pi agent following Codex pattern (without Local/Remote
splitting since Pi only has remote mode):
- session.ts: PiSession class managing state + hub communication
- loop.ts: response parsing, RPC resolver, transport event wiring
- runPi.ts: thin entry (bootstrap, RPC handlers, lifecycle)
Changes from review:
- Encapsulate RPC resolver in PiRpcResolver class (session-scoped,
not module-level singleton)
- Remove unused extractTextFromPiMessage export
- Fix inline import('./types') → top-level import
* refactor: normalize Pi file naming and improve test coverage
- Rename PiTransport.ts → piTransport.ts, PiEventConverter.ts →
piEventConverter.ts, PiMessageAccumulator.ts → piMessageAccumulator.ts
(match project-wide camelCase convention)
- Delete handleResponse.test.ts (tested stale copy of inline function)
- Add loop.test.ts with 20 tests covering parsePiModels,
parsePiCommands, wireTransportEvents integration, and sendPiRpcAndWait
- Total Pi tests: 73 (was 53)
* test: add E2E harness with 4 core helpers and integration specs
Helper functions in e2e/harness.ts capture the four non-obvious
interactions discovered during the 2026-06-09 retest:
- longPress: SessionActionMenu is triggered by 500ms press, not click
- mockOffline: useOnlineStatus hook listens to navigator.onLine +
window offline event, not CDP Network.emulateNetworkConditions
- pollForText: thinking indicator flickers in <1s, 3s polling misses
- isVisible: element.offsetParent returns null for position:fixed
dialogs even when visible; use getBoundingClientRect
Plus Chrome lifecycle (startChrome/stopChrome, never pkill chrome)
and hub API helpers (loginWithToken, listSessions).
5 integration specs (e2e/integration/) cover:
- yolo-permission: toggle + localStorage persistence (4 cases)
- codex-dialog: pre-flight check + dialog render (3 cases)
- stress: 10 concurrent + invalid JWT + malformed + unknown
endpoint (5 cases, all PASS)
All 12 integration cases pass. Full E2E results in
.xzy-harness/2026-06-09-full-e2e-retest/ (67 cases, 0 functional
bugs found).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: resolve Pi model selection and thinking level issues
- Fix PiModelPanel: use provider+modelId composite for selection check
and React key, preventing duplicate highlights for same-name models
across different providers
- Fix PiThinkingLevelPanel: unify thinkingLevelMap filtering logic by
extracting shared isThinkingLevelSupported utility
- Fix HappyComposer: auto-reset effort to highest supported level when
switching models, update label to reflect effective level
* refactor: remove 29 dead exports from feat-pi-support
Remove unused types, methods, and re-exports identified by dead code audit:
shared/src/apiTypes.ts (19):
- SessionModelIdentifier, ListPiCommandsResponse
- PiSteeringMode, PiFollowUpMode, PiSteerResponse, PiFollowUpResponse
- PiQueueModeResponse, PiMessageEntry, PiMessagesResponse
- PiCompactResponse, PiSetAutoCompactionResponse
- PiForkResponse, PiForkMessageEntry, PiForkMessagesResponse
- PiCloneResponse, PiSwitchSessionResponse
- PiSessionStats, PiSessionStatsResponse, PiExportHtmlResponse
cli/src/pi/types.ts (6):
- PiSessionStats, PiCompactionResult, PiForkMessageEntry (dead local duplicates)
- PiCommandsResponse, PI_THINKING_LEVELS, PI_THINKING_LEVEL_LABELS (dead re-exports)
cli/src/pi/piMessageAccumulator.ts (1):
- flushIfActive() method (comment claimed runPi calls it, but it doesn't)
cli/src/pi/piTransport.ts (1):
- isRunning() method (never called in production code)
web/ (2):
- ProviderGroup, PiThinkingLevelOption (unnecessary exports, made local)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: resolve 7 PR review issues in Pi support
#3 Remove duplicated PI_THINKING_LEVELS in schemas.ts, import from @hapi/protocol
#2 Add piAvailableModels field to MetadataSchema (schema-runtime consistency)
#6 Replace hardcoded flavor names with supportsEffort() in effort route
#1 Move PiRpcResolver from module-level singleton to PiSession instance
#4 Add piCachedModels fallback in piModelOptions useMemo
#7 Merge message_update dead branch into unified not-converted case
#10 Fix misleading Pi model list comments in modelOptions.ts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: normalize Pi model object to string in hub sessionCache (#5), remove extra blank line in rpcGateway (#8)
#5: applySessionConfig now extracts modelId from { provider, modelId }
before passing to setSessionModel / session.model, preventing
[object Object] from being stored in SQLite when Pi switches models.
#8: Remove double blank line before RpcGateway class declaration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pi): preserve piAvailableModels on resume, document SetSessionConfig divergence
- sessionFactory: preserve piAvailableModels in pickExistingSessionMetadata
so web shows cached models on inactive-session view without RPC round-trip
- sessionConfigRpc: extend resolveNullableSessionModel to accept
{provider, modelId} object form for schema consistency
- runPi: document why Pi manually registers SetSessionConfig instead of
reusing registerSessionConfigRpc (wire protocol needs separate fields)
- package.json: restore version to 0.20.0
* refactor: remove unused Pi types, extract JsonLineParser, clean up review findings
- Remove 13 unused PiRpcCommand variants and PiStreamingBehavior type (YAGNI)
- Remove unnecessary exports on 3 internal Zod schemas in pi/schemas.ts
- Extract JsonLineParser base class to utils/, shared by PiTransport,
CodexAppServerClient, and AcpStdioTransport (eliminates 3x duplicate
handleStdout buffer logic)
- Remove DEV-only duplicate session ID detection from SessionList.tsx
(debug code unrelated to Pi support scope)
- Add comments explaining key prefix rationale in SessionChat.tsx
* chore: remove unrelated E2E test harness from Pi support PR
E2E harness (codex-dialog, stress, yolo-permission, scratchlist specs)
was introduced in this branch but tests generic HAPI behavior unrelated
to Pi agent support. Should live in a separate PR.
* fix: wrap cursor model change handler for union type compatibility
* fix: apply startup --model to Pi and remove duplicate lockfile entry
1. --model startup bug:
- Add initialModel to PiSession to preserve startup model
- handleGetState preserves initialModel instead of overwriting with Pi default
- get_available_models handler resolves provider from cached models and sends set_model
2. bun.lock duplicate key:
- Remove duplicate @twsxtd/hapi-win32-x64@0.20.0 entry
- Fixes CI lockfile regeneration that caused hono type errors
* fix: update test expectation for effort endpoint error message
* fix(pi): resolve 8 link-review defects + abort session termination
- W1C-D-1: hasSameAgentSessionIds missing piSessionId/kimiSessionId
+ extractAgentSessionId also needs piSessionId recognition
- D-1: dispatchLocalResume pi branch missing effort param
- W1B-1-01: buildCliArgs only passes --effort for claude, not pi
- W2B-D-2: effort=null does not send set_thinking_level to Pi
- D-3: turn_start does not consume pendingLocalIds
- D-7: keep_alive falls into default case in convertPiEvent
- D-9: finally overwrites sessionEndReason set by Switch/Abort
- W2B-D-3: ListPiModels RPC does not update metadata
- Abort handler: remove cleanupAndExit, only cancel current turn
Also: Switch handler returns { success: true } for consistency
Test coverage: 13 new test cases across 5 files
* fix: restore cli version from integration test placeholder
* fix(pi): send restored thinking level to Pi subprocess on startup
opts.effort was stored in piSession.currentThinkingLevel but never
forwarded via set_thinking_level during the startup sequence, causing
runner-spawned and resumed sessions to show the restored effort in
HAPI while Pi kept its default.
* fix: restore cli package version from integration test residue
* fix(pi): switch-to-remote handler preserves session instead of terminating
Replace lifecycle.cleanupAndExit() with createModeChangeHandler + keepAlive
in the Switch RPC handler. Pi runs as a single long-lived subprocess
without BaseLocalLauncher's restart loop, so cleanupAndExit() permanently
destroyed the session on mode switch. The web handoff button now correctly
changes control mode while keeping Pi alive.
* fix(pi): remove permission mode selector (Pi RPC has no runtime switching)
Pi's --mode rpc is non-interactive and auto-approves all tool execution;
there is no set_permission_mode command in the protocol. The selector
reported success without changing Pi's behavior, misleading users.
Remove the concept across all four packages:
- shared: getPermissionModesForFlavor('pi') returns [] (cascades to
hub 400 + web UI auto-hide via length===0 guards); drop
PI_PERMISSION_MODES / PiPermissionMode
- cli: strip permissionMode from PiSession/runPi/pi command/resume;
drop the no-op SetSessionConfig permission branch that stored state
without forwarding to the subprocess
- web: delete PiPermissionPanel.tsx; remove panel block + imports
from HappyComposer
* fix(cli): realpath workspace root in apiMachine test assertion
The handler realpaths the cwd as a symlink-escape guard, so on macOS
/var/folders/... resolves to /private/var/folders/... The test compared
against the un-resolved path and failed on macOS. Use realpathSync on
the expected value for cross-platform consistency (no-op on Linux where
/tmp has no symlink prefix).
* fix(pi): keepalive reads current mode instead of constructor-time startingMode
The Switch handler updated controlledByUser but PiSession.pushKeepAlive()
still emitted the readonly startingMode every 2s, so a runner-started
session switched to local would flip back to remote on the next keepalive.
Replace readonly startingMode with a mutable mode field; add setMode()
that updates it and re-pushes keepAlive immediately. The Switch RPC
handler now calls setMode() before handleModeChange.
* fix(pi): runner no longer passes permission flags to Pi subprocess
After removing the Pi permission selector, the Pi command parser rejects
--permission-mode and ignores --yolo. But the shared buildCliArgs tail in
the runner still appended these flags for Pi sessions, making runner-
spawned Pi children exit before registering a session.
Guard the permission/yolo append with agent !== 'pi'.
* fix(pi): preserve provider identity when persisting selected Pi model
The hub's applySessionConfig normalized Pi's { provider, modelId } object
down to a plain modelId string for the shared session.model field, losing
the provider. On reload or next render, web's selectedPiModel lookup
matched by modelId alone — if two providers share a modelId, the wrong
one was highlighted, and subsequent model/thinking-level changes sent the
wrong provider to the Pi subprocess.
Add a provider-qualified piSelectedModel field to session metadata
(schema + persistPiSelectedModel mirroring persistPreferredPermissionMode).
Web's selectedPiModel now prefers the provider-qualified match and only
falls back to modelId-only matching when absent.
* fix(pi): model picker checkmark follows provider-qualified selection
selectedPiModel already resolves via provider+modelId, but the model
panel's currentPiModel still matched by modelId alone — so with two
providers sharing a modelId the checkmark pointed at the wrong row.
Reuse selectedPiModel directly.
* fix(pi): steer messages consumed immediately, not queued in pendingLocalIds
onUserMessage unconditionally pushed localId into pendingLocalIds, but a
steer (sent while piIsStreaming) does not start a new turn — so the
steer's localId was never drained by turn_start. The next normal prompt's
turn_start would consume the stale steer localId instead, leaving the
new prompt's bubble stuck in the queued bar.
Only queue localId for the prompt path. Steer path emits
messages-consumed immediately.
* fix(pi): clear stale thinking level when switching to non-reasoning model
The model-change effect early-returned when selectedPiModel.reasoning ===
false, leaving the previously-set effort (e.g. 'high') persisted on the
session. The UI hid the thinking picker for the non-reasoning model, but
the hub still forwarded the stale effort as set_thinking_level — with no
visible control to clear it.
Call onEffortChange(null) for non-reasoning models.
* fix(pi): return provider-qualified model in SetSessionConfig applied
The CLI handler returned only currentModel (bare string), so the hub's
applySessionConfig saw a non-object model and cleared
metadata.piSelectedModel via persistPiSelectedModel(session, null) —
undoing the provider that was just stored on the inbound config.
Return { provider, modelId } when both are known so the hub keeps the
provider-qualified metadata intact across active model changes.
* fix(pi): preserve piSelectedModel in bootstrapExistingSession metadata
The metadata whitelist rebuild kept piAvailableModels but omitted
piSelectedModel, so the first resume/local-handoff update dropped the
provider identity — after which web fell back to modelId-only matching
and could select the wrong provider for duplicate modelIds.
* fix(pi): await Pi confirmation before reporting model/effort applied
SetSessionConfig was fire-and-forget — transport.send wrote JSONL to
stdin and returned immediately. If Pi rejected an invalid provider/model
or thinking level, the hub still persisted the new value and the UI
reported success while Pi kept the old runtime state.
Use sendPiRpcAndWait so a failed set_model/set_thinking_level rejects
the web request and leaves the session config unchanged.
* fix(pi): resolve set_model RPC so awaited model switch does not time out
SetSessionConfig awaits sendPiRpcAndWait(set_model) before reporting the
model applied, but handleResponse's set_model branch updated state and
fell through without calling resolvePendingRpc. The pending RPC promise
then waited the full 10s timeout and rejected, making /sessions/:id/model
return 409 even though Pi accepted the change. Mirror every other branch
by resolving the pending RPC after updating currentModel/currentProvider.
* fix(pi): drain pending localId on turn_start only; throw when set_model suppressed
- loop.ts: split agent_start/turn_start branches. Pi emits both per prompt;
draining on both popped the FIFO twice and shipped an undefined localId to
the hub. agent_start now only sets thinking state; turn_start drains.
- runPi.ts: when set_model is suppressed (provider unknown), throw instead of
silently returning applied, so the hub returns 409 rather than persisting a
piSelectedModel Pi never received.
- loop.test.ts: assert agent_start does not drain; add regression test that a
single turn drains exactly one real localId.
* fix(pi): exclude Pi from generic Ctrl/Cmd+M model cycler
SessionChat fed piModelOptions into HappyComposer.availableModelOptions,
so the global Ctrl/Cmd+M shortcut ran getNextModelForFlavor over the Pi
list and called onModelChange with a bare modelId string. Pi needs
{ provider, modelId } to disambiguate duplicate model IDs across
providers; a bare string made runPi fall back to the first cached
provider match (wrong provider) or throw when the provider was unknown.
Drop the piModelOptions useMemo and pass undefined for Pi, mirroring
modelOptions.ts where the Pi branch already returns the current model
unchanged (no-op) when no custom options are supplied. Pi model changes
now go only through the dedicated provider-qualified picker (piModels).
* fix(pi): commit PiSession config only after Pi confirms the RPC
SetSessionConfig previously mutated piSession.currentModel /
currentProvider / currentThinkingLevel BEFORE awaiting
sendPiRpcAndWait(set_model / set_thinking_level). When Pi rejected the
value or the RPC timed out, the handler threw and the route returned
409, but PiSession kept the unconfirmed values; the 2s keepalive then
reported them back to the hub, where handleSessionAlive persisted a
model/effort Pi never accepted.
Resolve the requested model/effort into locals first, send the RPCs,
and only commit to PiSession after each await resolves. The null
(clear-model) path needs no RPC so it still commits immediately; the
unknown-provider path still throws without committing.
* fix(pi): apply startup model only after Pi confirms set_model
Two startup paths persisted the requested --model before Pi confirmed it:
1. handleGetState set session.currentModel = session.initialModel as soon
as get_state returned, using the unconfirmed startup model instead of
Pi's actual default. If the model was unavailable or rejected, the 2s
keepAlive reported it to the hub, which persisted/showed a model Pi
never accepted.
2. get_available_models then sent set_model fire-and-forget, so a Pi
rejection was never observed and currentModel stayed on the bad value.
Fix: handleGetState now reports Pi's real current model (newModel) while
a startup model is merely requested. get_available_models resolves the
provider from the cached list, awaits set_model, and commits
currentModel/currentProvider only on success — on rejection it logs and
keeps Pi's default. The await is fired detached so the
get_available_models RPC itself still resolves for ListPiModels.
* fix(pi): do not persist startup model before Pi confirms set_model
The startup --model still reached the hub unconfirmed via two paths the
previous Fix #13 left open:
1. bootstrapSession({ model: opts.model }) seeded the hub session model
at creation time, and SessionCache.handleSessionAlive persists every
non-undefined keepAlive model — so an unavailable/rejected model was
stored and shown before get_available_models/set_model ran.
2. PiSession constructor set this.currentModel = opts.model, so the very
first keepAlive (sent by startKeepAlive before any RPC confirms the
model) reported the unconfirmed value.
Pass model: undefined to bootstrapSession and start PiSession.currentModel
at null; opts.model is still captured as initialModel and applied/committed
only after get_available_models confirms it exists and set_model succeeds
(Fix #13). The hub now sees Pi's real current model from the first
get_state keepAlive and switches to the requested model only once accepted.
Also add sendPiRpcAndWait contract tests pinning the await<->resolve
symmetry (Fix #10): set_model/set_thinking_level/get_available_models must
resolve before timeout on a success response, and reject on a Pi error.
* fix(pi): apply startup effort only after Pi confirms set_thinking_level
runPi restored opts.effort straight into piSession.currentThinkingLevel
before startKeepAlive ran, and pushKeepAlive persists effort — so a
resumed/runner-spawned session could store/show a thinking level Pi
rejected or ignored. This is the effort analog of the startup-model
confirmation contract (Fix #13/#14).
Capture the requested effort into a local startupThinkingLevel instead of
mutating currentThinkingLevel up front. After transport.start() and the
get_state/get_available_models/get_commands sends, await set_thinking_level
and commit currentThinkingLevel + push a keepAlive only on success; on
rejection keep Pi's default (already reported by get_state). The await is
detached so the run loop is not blocked, and get_state is sent before the
set so its authoritative baseline lands first and cannot clobber the
confirmed value.
* fix(pi): omit unknown runtime config from keepalive, don't clear persisted state
Fix #14 changed PiSession.currentModel to start at null so the startup
--model was not leaked before confirmation. But the hub treats keepAlive
model:null as an explicit clear (sessionCache.ts only skips when the
field is undefined), so the first heartbeat (startKeepAlive runs before
get_state) now erased a resumed Pi session's persisted model/effort
before Pi reported its real state.
Distinguish "unknown" from "clear": currentModel/currentThinkingLevel
start undefined and keepAlive omits undefined fields (via
getKeepAliveRuntime), so the hub leaves persisted values alone until Pi
confirms. null remains an explicit clear and is still forwarded. Once
get_state/set_model/set_thinking_level confirm a value it is set and
reported normally.
* fix(pi): disable Ctrl/Cmd+M model cycler for Pi entirely
Fix #11 removed piModelOptions from availableModelOptions, assuming
getNextModelForFlavor('pi', model, undefined) was a no-op. It is not:
the Pi branch returns normalizeCurrentModel(model), i.e. the current
modelId as a bare string, so the shortcut still called onModelChange with
a bare modelId. That loses the provider and can pick the wrong cached
match, clear the model when session.model is empty, or hit 'provider is
not yet known'. Short-circuit the handler for Pi so model changes go only
through the dedicated provider-qualified PiModelPanel.
* fix(pi): persist piSelectedModel from get_state and startup set_model paths
Pi stores session.model as the bare modelId and relies on
metadata.piSelectedModel ({ provider, modelId }) to disambiguate
duplicate modelId values across providers in the web picker and
thinking-level filtering. But piSelectedModel was only written by the web
/sessions/:id/model path (hub persistPiSelectedModel). The runtime paths
that set currentModel/currentProvider — get_state, the startup
get_available_models set_model, and the set_model response — only
keepAlive'd the bare modelId, so a Pi session on Pi's default model,
resumed from CLI, or started with --model had no provider identity in
metadata and could render/filter against the wrong provider.
Add persistSelectedPiModel(session) (no-op unless both fields are known)
and call it after get_state, after a successful startup set_model, and
after the set_model response updates the fields. This mirrors what the
web picker already does.
* fix(pi): default startingMode to remote — Pi has no local TUI path
A terminal `hapi pi` launch defaulted to startingMode 'local' and marked
the session controlledByUser, but Pi only runs as `pi --mode rpc` with
piped stdio — there is no local terminal/TUI input path like Claude/Codex
have. The terminal user could not drive the session and the web treated
it as local-controlled, so the first terminal Pi session was stuck until
manually switched from the web.
Default to 'remote' so the session is immediately drivable from the web.
An explicit opts.startingMode (runner path) still takes precedence.
* fix(pi): resume with remote startingMode — no local TUI path
The previous Fix #19 changed the `hapi pi` default to remote, but
`hapi resume` still passed startingMode: 'local' into runPi for Pi
sessions, re-introducing the same unsupported local-control state on the
resume path: setControlledByUser publishes controlledByUser while Pi has
no terminal/TUI input, hiding/rejecting remote-only controls until a web
switch. Pass 'remote' here too and update the resume test accordingly.
* fix: restore e2e/scratchlist.spec.ts deleted from main by mistake
The earlier "remove unrelated E2E harness" commit (d1e5b4c) deleted the
whole e2e/ directory this branch had added, but scratchlist.spec.ts is a
main-branch Playwright spec (the only file under playwright testDir
./e2e). Its removal left `bun run test:e2e` with no tests to run while
the script and playwright.config.ts still point at that directory.
Restore scratchlist.spec.ts from main; the unrelated harness files
(HARNESS.md, harness.*, integration/*.mts) that were genuinely
branch-only additions stay removed.
---------
Co-authored-by: pi <pi@local>
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
d464651870 | Release version 0.20.2 | ||
|
|
55d1bbb7bd |
feat(cursor): invisible sync-on-open migrator from legacy stream-json to ACP (#844)
* feat(cursor): invisible sync-on-open migrator from legacy stream-json to ACP Closes #824 When the operator reopens a legacy stream-json Cursor session in HAPI, the hub now transparently transplants its `~/.cursor/chats/<wsh>/<uuid>/store.db` into `~/.cursor/acp-sessions/<uuid>/`, verifies it loads via `agent acp`, flips `metadata.cursorSessionProtocol = 'acp'`, and removes the legacy source - all before `resumeSession` returns. Subsequent opens are pure ACP. The primary justification is safety, not feature parity. #784 (`cursor-agent` fabricates `Questions skipped by the user` responses in legacy stream-json mode) still fires regularly in dogfood despite #801's mitigation: the agent ships destructive side effects against fabricated consent. Migration to ACP closes the protocol-level door because the `AskQuestion` tool does not exist on the ACP side, so there is nothing to fabricate. working. That tradeoff was reasonable at the time. The accumulated #784 evidence makes legacy sessions actively unsafe; this PR makes the upgrade path invisible enough that users stop avoiding it. A pre-PR spike established that legacy and ACP `store.db` files use the identical SQLite schema; only the directory layout differs. The migrator therefore: 1. Sanity-checks the source store and pre-flips state (`session.active`, `lifecycleState`, on-disk presence, target collision) 2. Optionally archives a stale-running row (`forceArchiveRunning: true` is the default for the auto-migrate path because the caller already verified `session.active === false`) 3. Atomically creates `~/.cursor/acp-sessions/<uuid>/` with mode `0o700` 4. Copies `store.db` and chmods to `0o600` (multi-user-host hardening) 5. Writes a minimal `meta.json` sidecar (`schemaVersion`, `cwd`, optional `title`) with mode `0o600` 6. Spawns `agent acp` under HAPI_HOME isolation and verifies the session loads via `session/load`. On long histories the verify also drives a trivial single-turn prompt; on short ones load-only is enough 7. Flips `cursorSessionProtocol = 'acp'` AND clears the `cursorMigrationState` banner flag in a SINGLE metadata write 8. Removes the legacy source store (only after verify succeeded and the protocol flip committed). The legacy `~/.cursor/chats` parent dir is left as-is Every failure leaves the legacy state intact. No `rm` fires without a verify success AND a committed protocol flip. The transplant takes 15-20s on long histories (copy a multi-hundred-MB store, spawn `agent acp`, replay thousands of notifications, tear down the probe). Without a progress indicator the wait reads as "broken" to a fresh reviewer. A minimal banner ships alongside the migrator: - Hub sets `metadata.cursorMigrationState = 'in_progress'` BEFORE the long-running transplant. The session-cache refresh emits the existing `session-updated` SSE event (no new event type), so the web client picks it up in milliseconds. No client-side polling needed. - Hub clears the flag in the SAME metadata write that flips `cursorSessionProtocol` to `'acp'` on success, so the banner disappears in the same render tick the chat re-renders as ACP - no flicker window. - Hub clears the flag explicitly in the auto-migrate helper's `finally` on failure/exception, so the banner never gets stuck if migration falls back to the legacy launcher. - Web renders an accessible (role=status, aria-live=polite) banner with an indeterminate spinner. Deliberately no fake percentage - we do not have phase data and a fake progress bar would lie. This PR is intentionally sequenced AFTER swear01's three ACP mop-up PRs (merged today as |
||
|
|
1f92a31b12 | Release version 0.20.1 | ||
|
|
c87f88f02f |
fix(web): disable single-dollar inline math in remark-math (#805)
* fix(web/markdown): disable single-dollar inline math in remark-math Default remark-math configuration treats $...$ as inline LaTeX, which turns any prose containing two currency amounts (e.g. "save $400 vs the $200 plan") into a KaTeX block — paragraphs collapse, whitespace is stripped, the running text is re-rendered as math symbols. Pass `singleDollarTextMath: false` to remarkMath so single $ is plain text. Block math `$$...$$` (on its own line) still renders, matching GitHub-flavored markdown semantics. Single source of truth: MARKDOWN_PLUGINS is shared by MarkdownText, Reasoning, and MarkdownRenderer — fix lands in all three surfaces. Adds 3 regression tests that drive the unified pipeline end-to-end: prose with multiple "$N" amounts produces no `class="katex"` and no `<math>` element; `$$...$$` block math still does. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): declare unified/remark-parse/remark-rehype/hast-util-to-html The new markdown-text regression test imports these directly to drive the unified pipeline end-to-end. They were resolving via transitive deps from remark-math and rehype-katex, which is fragile — a future dep upgrade can remove the transitives and break the test. Declare them explicitly under devDependencies. No code change; lockfile records the same versions that were already installed transitively (unified@11.0.5, remark-parse@11.0.0, remark-rehype@11.1.2, hast-util-to-html@9.0.5). Addresses the HAPI Bot review finding on PR #805. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
d82ff6127d | Release version 0.20.0 | ||
|
|
d09168778c |
fix(web): preserve user prompt line breaks (#804)
* test: reproduce issue #794 * fix: preserve user prompt line breaks (closes #794) |
||
|
|
18bcb522e1 |
feat(web): per-session scratchlist (workbench) panel (#772)
* feat(web): per-session scratchlist (workbench) panel Adds a per-session "scratchlist" panel above the composer for parking notes / drafts / parking-lot ideas that are explicitly held — never auto-sent. This is distinct from the existing queue (QueuedMessagesBar): - Queue = conveyor belt: messages auto-fire once the agent is idle. - Scratchlist = workbench: held until the operator promotes them. The amber accent and "held — not sent" pill make the visual distinction obvious so operators don't mistake one for the other. Features: - Collapsible per-session panel (collapsed by default, persisted in localStorage). - Add (Enter) / delete / reorder (up/down) entries. - Promote-to-composer copies into the composer for editing (entry stays — copy semantics). - Promote-to-queue routes through the existing onSend path so the entry shows up in QueuedMessagesBar; entry is removed only on accepted send. - Entries persist per session under hapi.scratchlist.v1.<sessionId>. - Confirm-on-delete only for entries longer than 100 chars. - Ctrl/Cmd+Shift+S focuses the add-input. - en + zh-CN strings. v1 scope: localStorage-only. Hub-sync deferred to v2 to keep the diff small and reviewable. Test coverage: - web/src/lib/scratchlist.test.ts — 21 tests (storage round-trip, add/delete/reorder/cap, malformed-JSON resilience, confirm threshold). - web/src/components/AssistantChat/ScratchlistPanel.test.tsx — 13 tests (collapse persistence, hydration, add/delete/reorder UI, promote-to-composer copy semantics, promote-to-queue accepted / rejected paths, per-session isolation). Closes #11 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): block focus into collapsed panel via inert Upstream review (tiann/hapi#772, codex bot) flagged that the collapsed scratchlist body was visually hidden via CSS only - the textarea and action buttons stayed mounted, focusable, and clickable while their ancestor was aria-hidden. Tab into invisible controls + a hidden subtree with focusable descendants is an a11y violation. Apply `inert` to the inner content, gated on the collapsed state. This removes the subtree from the focus, pointer, and accessibility trees while keeping the grid-template-rows expand animation intact (no conditional remount, so the open/close transition still runs). Add a regression test that asserts `inert` is present while collapsed and removed (or empty) while expanded, so a future revert of the fix trips immediately. Co-authored-by: Cursor <cursoragent@cursor.com> * test(scratchlist): add Playwright e2e + isolated fixture page The unit suite under jsdom can't verify the parts of the scratchlist that actually live in the browser: - `inert` blocks focus (jsdom ignores `inert`) - the grid-template-rows collapse animation - localStorage surviving a full page reload - per-session keying surviving cross-route navigation - Ctrl/Cmd+Shift+S firing the global expand+focus shortcut Add a Playwright config + spec that drives a real Chromium against a new Vite-served fixture (`web/e2e-fixtures/scratchlist-fixture.html`). The fixture mounts the production `ScratchlistPanel` in isolation inside an `I18nProvider` and exposes the promote callbacks on `window.__scratchlistE2E` so the spec can assert that promote-to- composer and promote-to-queue receive the right text without having to spin up the hub, auth, or socket layer. Nine specs cover: 1. starts collapsed, toggles 2. collapsed inner is `inert` and refuses focus / pointer 3. add: entry appears, draft clears, count updates 4. persistence across full page reload 5. promote-to-composer fires callback (entry stays - copy semantics) 6. promote-to-queue success path (entry removed) 7. promote-to-queue failure path (entry retained for retry) 8. Ctrl+Shift+S expands + focuses input 9. per-session isolation across navigation Wires `bun run test:e2e` and `test:e2e:ui` at the repo root and documents the harness in `web/README.md`. Bumps `playwright` 1.49.1 -> 1.60.0 alongside the new `@playwright/test` dep so the bundled chromium-headless-shell-1223 (Chrome 148) is used; the older 131 binary SIGTRAPs on this kernel during launch. Adds `test-results/` and `playwright-report/` to `.gitignore`. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): key host by session.id to prevent cross-session leak Upstream review (tiann/hapi#772, codex bot follow-up) flagged a state leak across same-route session switches. ScratchlistPanel reads `sessionId` once via `useState(() => readScratchlist(sessionId))` and rehydrates in a `useEffect`. SessionChat stays mounted when the operator switches sessions on the same `/sessions/$sessionId` route, so the panel sees a new `sessionId` prop without unmounting. Effect order during the prop change: 1. render with sessionId=B but stale entries=[A's items] 2. rehydrate effect: setEntries(read(B)) -> queues correction 3. persist effect (deps [sessionId, entries] both changed): persistScratchlist(B, [A's items]) -> writes A into B 4. re-render with sessionId=B, entries=B's items 5. persist effect: persistScratchlist(B, B's items) -> overwrites the bug write The bug is transient (step 3's write is corrected by step 5) but real: any read between steps 3 and 5 (another tab, a SW prefetch, manual inspection) sees A's data under B's key. Fix is one line: `key={props.session.id}` on `<ScratchlistHost>`. React unmounts and remounts the host when the key changes, so the new mount's useState initializer reads B's storage from scratch and never touches B's key with A's data. This is the React-canonical "reset state on prop change" pattern; cleaner than chasing the race inside the panel. Add an e2e regression test that: - installs a `localStorage.setItem` spy in `addInitScript` - mounts the fixture under session A and adds an entry - clears the spy, then switches to session B in-place via `window.__scratchlistE2E.setSessionId('leak-B')` (no page reload) - asserts no recorded write to `hapi.scratchlist.v1.leak-B` contained A's text (catches the transient corrupting write deterministically, before the correction overwrites it) - round-trips back to A to confirm A's storage is intact The fixture grows a `?key=0` mode that drops the host's `key=` prop. Verified red/green: with `key=0` the regression test fails on the spy-detected corrupting write; with the fix in place (default), all 10 e2e specs pass. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
4aee1e4f84 | Release version 0.19.0 | ||
|
|
27e58570c3 |
chore(web): upgrade @tanstack/react-router to ^1.170.8 (#722)
* chore(web): upgrade @tanstack/react-router to ^1.170.8 Fixes QuotaExceededError in scroll restoration: upstream @tanstack/react-router >=1.145.6 wraps sessionStorage.setItem with try-catch, preventing the crash when scroll restoration cache exceeds quota. Refs: #683, #716, #721 * fix(web): adapt scrollStorageGuard to @tanstack/router-core >=1.145.6 API `scrollRestorationCache` was removed from the public exports; replace with `storageKey` import and simplify `hardResetScrollRestorationPersistedState` to a plain `removeItem`. Remove the now-stale in-memory cache sync path and its associated tests. Upstream try-catch (>=1.145.6) covers crash prevention; this guard continues to proactively prune sessionStorage. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
763f45acdd |
feat: add support for Kimi Code CLI and fixed some bugs (#659)
* Add Kimi agent support via ACP protocol Add full integration for the Kimi Code CLI agent using the standard Agent Client Protocol (ACP). Includes: - kimi command and CLI registry wiring - Local launcher spawning kimi directly - Remote launcher with ACP stdio transport via AcpSdkBackend - Session management with resume support - Permission handler supporting all Kimi permission modes - Terminal UI display component - Runtime config resolving model from env and ~/.kimi/config.toml via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Fix Kimi ACP tool call input decoding on web Kimi streams tool arguments as JSON text inside the content array (e.g. {\"command\": \"df -h\"}) instead of rawInput/kind. The handler now extracts input from three sources in priority order: 1. rawInput (Claude/Codex path) 2. kind + title fallback (Gemini path) 3. content JSON text (Kimi path) Also handles: - rawInput: null no longer blocks the kind+title fallback - Title prefixes like \"Shell: free -h\" are stripped to extract args - Stale placeholder inputs are re-derived when the title updates - Normalized kind aliases (shell, run, read_file, write, etc.) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Add kimi support to web UI * Fix some bugs * fix(kimi): dedupe repeated tool_call display in terminal UI * fix(web): keep tool block immutable so React detects input/state changes * fix(web): recognise Kimi subagent titles like 'Agent: ...' as subagent tools * fix(web): allow-for-session for ACP agents (kimi, cursor) PermissionFooter treated all non-codex sessions as Claude, sending Claude-specific acceptEdits/allowTools to ACP agents that don't support them. Hub rejected acceptEdits for kimi, and the ACP PermissionAdapter ignored allowTools. - Only show 'allow all edits' for Claude sessions - Send decision: approved_for_session for non-Claude ACP agents - Update status display to check decision field * fix(web): lookup subagent sidechains by tool-call id instead of msg id * fix(web): don't trim newest messages when loading older history fetchOlderMessages was using trimVisible(merged, 'prepend') which kept the oldest 400 messages and dropped the newest ones. This caused: 1. Latest messages to disappear when user loaded older history 2. User to see no visible change when new old messages were drowned in the 400-message window. Remove the incorrect trim so all fetched older messages are retained alongside the current window. Subsequent ingestIncomingMessages (append mode) will naturally keep the window bounded when new agent messages arrive. * fix(cli): route Kimi session resume to runKimi instead of runCursor Kimi was present in AGENT_FLAVORS but dispatchLocalResume had no branch for it, so resuming a Kimi session fell through to the Cursor launcher. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(cli): pass selected model to Kimi ACP backend via KIMI_MODEL env createKimiBackend was ignoring opts.model and only setting KIMI_PROJECT_DIR. Use buildKimiEnv so the selected model reaches the subprocess as KIMI_MODEL. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): bound message window on older loads with dedicated larger cap fetchOlderMessages was keeping all messages unbounded, causing sessionStorage bloat on repeated pagination. Reintroduce trimming with OLDER_LOAD_WINDOW_SIZE (800) so growth is capped while the newest messages are still preserved for far longer than before. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): revert sidechain lookup to message id, matching tracer/grouping pipeline tracer.ts sets sidechainId to the parent message id, and reducer.ts groups by sidechainId. A prior commit changed reducerTimeline.ts to look up by tool-call id (c.id), which broke sidechain attachment. Revert to msg.id so the lookup matches the actual grouping key end-to-end. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(cli): gate ACP title prefix stripping to known tool-kind labels extractTitleArgument stripped at the first colon unconditionally, corrupting commands/paths like curl http://localhost:3000 or Windows paths. Now it only strips when the prefix normalizes to the same tool kind as the event, verified via regex. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(shared): include kimi in isCodexFamilyFlavor for ACP permission UI Kimi is an ACP-style agent that supports the abort decision, but isCodexFamilyFlavor excluded it, so PermissionFooter rendered the non-Codex Allow/Deny UI without the Abort button. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
6aa7274851 | Release version 0.18.4 | ||
|
|
15113668cc | Release version 0.18.3 | ||
|
|
1954920753 | Release version 0.18.2 | ||
|
|
83795c0630 | Clean up cross-package build coupling | ||
|
|
ce2e76a42e | Add Windows remote terminal support (#642) | ||
|
|
ff866c2069 | Release version 0.18.1 | ||
|
|
c07b3a2ed2 | Release version 0.18.0 | ||
|
|
088a712f1e |
Stop active Codex child agents on abort (#615)
* fix(cli): stop active codex child agents * chore: refresh bun lockfile for deploy * fix(web): enable stop for active codex child agents * test(cli): cover aborting active codex child agents |
||
|
|
752a505973 | Release version 0.17.4 | ||
|
|
0db303d9f8 | Fix release lockfile platform packages | ||
|
|
47c408c84f | Release version 0.17.3 | ||
|
|
f7a40bd573 |
feat(web): polish chat rendering and fix remote session interactions (#567)
* feat(web): polish chat rendering Refresh the web chat presentation across user messages, tool cards, code blocks, diffs, reasoning, and Mermaid diagrams.\n\nAdd focused regression coverage for bubble/status behavior, code and diff rendering, clipboard output, Mermaid theming, and message-window updates. * fix(web): stabilize chat tool rendering Preserve manual scroll anchors while older messages and tool dialogs update, and align code, diff, and tool result rendering with chat typography. Add chat font-weight settings, ignore local Playwright CLI artifacts, document the Angular commit-message convention, and cover the scroll, result, code, diff, and settings behavior with focused tests. Constraint: User requested committing all current workspace diffs with Angular-style commit messaging Tested: bun run typecheck:web && bun run test:web && git diff --check Co-authored-by: OmX <omx@oh-my-codex.dev> * style(tool-card): polish question and permission card styles Align AskUserQuestion option surfaces and permission action hierarchy with the existing tool card visual language while preserving interaction logic. Extract shared option presentation helpers and theme-driven hover/muted colors to reduce duplication. Constraint: Frontend style-only polish; preserve existing permission and answer submission behavior Rejected: Keep screenshot artifacts in the repo | they are local visual review output, not source Confidence: high Scope-risk: narrow Tested: git diff --check; bun run typecheck:web; bun run test:web; bun run build:web Not-tested: manual cross-browser visual QA beyond local Playwright inspection Co-authored-by: OmX <omx@oh-my-codex.dev> * fix(cli): keep Claude remote plan prompts actionable Handle Claude remote /plan locally so HAPI switches plan permission mode before forwarding any prompt text. This avoids Claude Code treating /plan as an unknown skill and ending with only a ready event. Constraint: Claude SDK result messages are not conversation log entries, so command handling must happen before the prompt reaches Claude.\nRejected: surfacing SDK result summaries in web chat | would expose transport-level summaries broadly instead of fixing the slash-command path.\nConfidence: high\nScope-risk: narrow\nDirective: Keep Claude remote slash commands that alter runtime mode in the CLI special-command parser.\nTested: bun test cli/src/parsers/specialCommands.test.ts; bun typecheck; git diff --check\nNot-tested: Manual GitHub-hosted runner deployment. * fix(web): polish tool result rendering * fix(web): preserve collapsed session order * fix(chat): settle initial thread scroll * fix(settings): remove chat font weight option * fix(web): remove font weight bootstrap code * chore: remove unrelated branch artifacts * test(web): update consumed message invocation test * fix(chat): cancel initial scroll settling on manual scroll --------- Co-authored-by: huhaoyu.hahahu <huhaoyu.hahahu@bytedance.com> Co-authored-by: OmX <omx@oh-my-codex.dev> |
||
|
|
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 |
||
|
|
c9ff7f9de3 | Release version 0.17.2 | ||
|
|
be47ad06b9 | Release version 0.17.1 | ||
|
|
83e5053156 | Release version 0.17.0 | ||
|
|
938c897ed9 | Release version 0.16.8 | ||
|
|
5bb27ab8ba | Release version 0.16.7 | ||
|
|
813ac7fdda |
feat(web): add LaTeX math formula rendering with KaTeX (#436)
* feat(web): add LaTeX math formula rendering with KaTeX Add remark-math + rehype-katex to the markdown rendering pipeline so inline ($...$) and display ($$...$$) math formulas are rendered as proper KaTeX output in chat messages and tool results. Closes #237 * fix(web): disable single-dollar math parsing and add KaTeX to reasoning - Set singleDollarTextMath: false to prevent $HOME, $PATH etc from being misinterpreted as math formulas. Only $$...$$ (display) is parsed; inline math requires explicit \(...\) or $$...$$. - Add rehypePlugins to the reasoning renderer so math formulas render consistently across chat, tool results, and reasoning blocks. * refactor(web): use satisfies for type-safe plugin exports Replace any[] with satisfies NonNullable<MarkdownTextPrimitiveProps[...]> to preserve type safety on the shared plugin lists without needing eslint suppressions. * fix(web): enable single-dollar inline math syntax Re-enable $...$ parsing (remark-math default) so inline formulas like $E=mc^2$ render correctly. Shell variables like $HOME typically appear inside code spans/blocks which remark-math does not parse, so false positives are minimal in practice. |
||
|
|
ef87e30727 | feat(web): redesign sidebar with resizable width and 3-level hierarchy (#427) | ||
|
|
3ef7b8c7cd | Release version 0.16.6 | ||
|
|
fa0ce1ca94 | Release version 0.16.5 | ||
|
|
7da6f7c5c5 | Release version 0.16.4 | ||
|
|
32f05d99a0 | Release version 0.16.3 | ||
|
|
b4b73d4405 | Release version 0.16.1 | ||
|
|
5ceab49630 | Release version 0.16.0 | ||
|
|
c9be2894ac |
feat(cursor): add support for Cursor Agent CLI integration (#236)
* feat(cursor): add support for Cursor Agent CLI integration - Introduced new command `hapi cursor` to start Cursor Agent sessions. - Added functionality for resuming sessions and managing permission modes. - Updated documentation to include Cursor Agent usage and installation instructions. - Enhanced existing codebase to accommodate Cursor as a recognized agent flavor. - Implemented local and remote session handling for Cursor Agent. This update expands HAPI's capabilities by integrating support for the Cursor Agent, allowing users to leverage its features alongside existing agents. * Remove TODO.md file as it is no longer needed following the integration of Cursor Agent CLI support. This cleanup helps streamline project documentation and reflects the completion of the associated tasks. * feat(cursor): implement remote mode and fix --hapi-starting-mode - Consume --hapi-starting-mode in cursor command (do not forward to agent) - Implement cursorRemoteLauncher: spawn agent -p with stream-json, --trust - Add cursorEventConverter for NDJSON parsing (system/assistant/tool_call/result) - Multi-turn via --resume session_id - Update docs: cursor supports both local and remote modes Made-with: Cursor * fix: type error * fix(cursor): address PR review - model UI, sessionId metadata, duplicate flags - HappyComposer: use isClaudeFlavor for model mode (cursor has no model modes) - cursorLocalLauncher: call onSessionFound for resume so cursorSessionId in metadata - cursorCommand: do not forward parsed flags to cursorArgs (avoid duplicates) Made-with: Cursor |
||
|
|
2bdae8fe2d | Release version 0.15.4 | ||
|
|
a74dcc9477 | Release version 0.15.3 | ||
|
|
eb8e749946 | Release version 0.15.2 |