The copy button was absolutely positioned (right-0 top-0) over the
entire message content area. When assistant messages contain both text
and tool calls (e.g. TodoWrite), the button overlapped the tool card UI.
Move the button from absolute positioning inside the content wrapper to
an inline flex layout after the content. This places it at the bottom-
right of the message, below all content (text + tool cards), so it never
overlaps anything. The hover-to-reveal behavior is preserved.
Also restores getAssistantCopyText to its original logic so mixed
text+tool messages remain copyable (the previous fix of suppressing the
button entirely for mixed messages was too aggressive).
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* fix(hub): raise maxRequestBodySize so file uploads work
The Bun server inherited maxRequestBodySize from Socket.IO's default
maxHttpBufferSize (1 MB). The upload endpoint sends files as base64
in JSON, so any image > ~750 KB was silently rejected before reaching
the route handler. The frontend allows 50 MB uploads.
Raise the limit to at least 100 MB to accommodate 50 MB files with
base64 encoding overhead (~33%).
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(hub,web): fix file uploads — raise body limit, lower max size, show errors
Three changes:
1. hub/server.ts: Bun's maxRequestBodySize inherited Socket.IO's 1 MB
default, silently rejecting any upload. Raise to 10 MB.
2. hub/routes + web/attachmentAdapter: lower MAX_UPLOAD_BYTES from
50 MB to 5 MB (realistic for images; 5 MB base64 ≈ 6.7 MB body,
fits within the 10 MB server limit).
3. web/AttachmentItem: show "Upload failed" text and strike-through
filename on error, instead of just a tiny icon.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(hub): keep 50MB upload limit, size maxRequestBodySize to match
Bot review correctly flagged that lowering MAX_UPLOAD_BYTES to 5 MB
regresses the documented 50 MB limit. Revert to 50 MB and calculate
maxRequestBodySize properly: 50 MB × 4/3 (base64) + 1 MB (JSON
overhead) ≈ 68 MB.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): reconnect SSE immediately when tab becomes visible
The SSE watchdog skips heartbeat checks while the tab is hidden. If the
connection dies in the background, the user sees stale messages after
switching back and has to wait up to 10 s for the next watchdog tick.
Add a visibilitychange listener that checks heartbeat staleness
immediately when the tab becomes visible and reconnects if stale.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): map visibility-recovery reason in reconnecting banner
The new 'visibility-recovery' reconnect reason was not mapped in
getReasonLabel(), so the raw string would appear in the UI banner.
Add localized labels for both en and zh-CN.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): disable indented code blocks in markdown rendering
In CommonMark, text indented by 4+ spaces is treated as a code block.
LLM responses frequently have indented content inside numbered lists or
quoted text, causing large chunks to render as a single code block
instead of formatted markdown.
Add a remark plugin that disables the codeIndented tokenizer. Fenced
code blocks (``` … ```) continue to work normally.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): fix typecheck for remark plugin this binding
Use `as any` cast for the unified processor `this` context instead of
an explicit type annotation that conflicts with the Processor type.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): use this:unknown annotation to satisfy noImplicitThis
The previous `as any` cast on `this` still triggers noImplicitThis in
strict mode. Annotate the parameter as `this: unknown` and cast to
the required shape inside the function body. Also use the key-based
`data(key, value)` API instead of mutating the returned object.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* feat(web): add copy button to user messages
Add a small copy button to user message bubbles for easy text copying,
especially useful on mobile where selecting text is difficult.
- Mobile: button always visible (opacity-60)
- Desktop: button appears on hover
- Uses existing useCopyToClipboard hook with haptic feedback
- Conditionally rendered to avoid empty container spacing
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): use valid CSS property in copy button transition
transition-[opacity,colors] is invalid because 'colors' is not a CSS
property (only Tailwind's utility class 'transition-colors' expands it).
Use 'background-color' instead so the hover background transition
actually works.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* feat: improve spawn error handling and reporting across full stack
- Return error result instead of throwing in apiMachine spawn handler
- Add lastSpawnError field to RunnerState for persistent error tracking
- Add error awaiter system for early process exit/error detection before webhook
- Build detailed webhook failure messages with exit code, signal, and stderr tail
- Report spawn outcomes to hub via runner state updates
- Handle more spawn result types in rpcGateway with better error messages
- Display runner last spawn error in web UI (NewSession & SpawnSession)
- Extract shared formatRunnerSpawnError utility to avoid duplication
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cli): narrow spawnResult type check to fix TS2339 error
Use `type === 'error'` instead of `type !== 'success'` to properly
narrow the discriminated union, allowing TypeScript to infer errorMessage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Add Claude Code Agent Teams support
- Add TeamState schemas and types for team collaboration
- Extract team state from TeamCreate, SendMessage, Task tools
- Add database migration V3→V4 for team_state storage
- Add TeamPanel component to display team members, tasks, messages
- Add team tool icons and presentation rules
- Support vite proxy configuration via VITE_HUB_PROXY env var
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix: Add timestamp protection for team_state updates
Prevent old messages from overwriting newer team state by checking
team_state_updated_at before updating.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix: Extract team tasks from Task/TaskCreate/TaskUpdate tools
- Enhance processTaskToolWithTeam to also generate task entries from
the Task tool's description field when spawning teammates
- Add processTaskCreate handler for TaskCreate tool calls
- Add processTaskUpdate handler for TaskUpdate tool calls
- Register both new tools in the extraction switch statement
This fixes the gap where the Tasks section in TeamPanel could never
populate because team task data was not being extracted from the
message stream.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix: Skip orphan TaskUpdate without title to prevent schema validation failure
When TaskUpdate arrives before TaskCreate (message ordering), skip inserting
incomplete tasks that lack required title field, preventing entire teamState
from being dropped by schema validation.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* test: Add unit tests for orphan TaskUpdate handling
Verify that applyTeamStateDelta correctly skips inserting tasks without
title field (orphan TaskUpdate) while still allowing normal task creation
and updates to existing tasks.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: tfq <tfq@gmail.com>
Co-authored-by: HAPI <noreply@hapi.run>
Replace hardcoded English strings with translation function calls
in SpawnSession.tsx and router.tsx. Add newSession.title key for
the NewSessionPage header to match the newSession.* namespace.
* 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
On iPadOS with a hardware keyboard, `isTouch` is true (via pointer:coarse)
so submitOnEnter is disabled and there's no keyboard-based way to send.
Add Shift+Enter as a send shortcut that works on all platforms without
changing existing behavior — desktop Enter-to-send is preserved.
Closes#175
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(web): add built-in Nerd Font for terminal icon support
Add MesloLGLDZ Nerd Font as a built-in web font to ensure Powerlevel10k
and other Nerd Font-dependent prompts render correctly in the web terminal,
even on devices without local Nerd Fonts installed (e.g., mobile phones).
Changes:
- Add MesloLGLDZNerdFontMono-Regular.woff2 to web/public/fonts/
- Load font via FontFace API with BASE_URL support for subpath deployments
- Merge font loading and terminal creation in same useEffect to ensure
font is ready before terminal renders
Fixes#121
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: use CDN for Nerd Font instead of bundled file
Load MesloLGLDZ Nerd Font from jsDelivr CDN instead of bundling the 1.2MB
font file. This reduces the bundle size significantly while still ensuring
Nerd Font icons display correctly on all devices.
CDN URL: https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/MesloLGLDZNerdFontMono-Regular.woff2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): prevent message ordering issues when switching sessions
When switching between sessions or loading paginated messages, old tool
blocks from agentState.requests were being mixed with current messages,
causing display order issues.
Root cause: reduceChatBlocks creates tool blocks for all permissions in
agentState, including those older than the current message page. These
old blocks (with earlier createdAt) would appear mixed with newer messages.
Fix:
1. In reducer.ts: Skip creating permission-only tool blocks if their
createdAt is older than the oldest message in the current view
2. In SessionChat.tsx: Clear caches synchronously in useMemo when session
changes, since useEffect runs after render
Closes#148
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: tfq <tfq@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
- Add ReconnectingBanner component to show "Reconnecting..." status
- Track SSE connection state in App.tsx with onDisconnect handler
- Add onBlocked callback to useSendMessage for handling blocked send attempts
- Provide haptic feedback when send is blocked due to no API or session
- Show toast notification when message cannot be sent due to server disconnection
- Add translation keys for reconnecting message and send blocked feedback in en/zh-CN
close#125
- Add Settings > Display > Font Size selector (80/90/100/110/120%)
- Persist preference (hapi-font-scale) and apply globally via --app-font-scale
- Normalize main UI typography to match Settings
Move getInputString, getInputStringAny, and truncate functions from
individual ToolCard components into a dedicated toolInputUtils module
to eliminate duplication across PermissionFooter, ToolCard, and
knownTools components.
Add ability to resume inactive sessions and automatically continue with new
session ID. Includes message merging for preserving conversation history,
UI improvements to allow sending during inactive state, and database schema
migration. close#87