* 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>
* 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>
* 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>
* 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>
* 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
* 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.
* 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
* feat(web): add About section to settings page
- Add website link to hapi.run
- Display app version from CLI package
- Display protocol version from shared module
- Add Vitest testing setup with settings page tests
🤖 Generated with Claude Code
* test(web): add tests for website link and i18n key usage
Address residual risks mentioned in PR review:
- Test website link URL and security attributes (target, rel)
- Verify correct i18n keys are used for About section via spy
Simplify test setup by using real I18nProvider and en locale.
🤖 Generated with Claude Code