Commit Graph
44 Commits
Author SHA1 Message Date
Shawn TianandGitHub d809fca433 fix: reconcile stale queued messages (#1063)
Recover missed messages-consumed events from authoritative Hub state after session SSE reconnects.
2026-07-18 12:18:47 +08:00
dfb1805fd6 feat(web): OLED Black theme + per-appearance custom colors (#937)
* test: reproduce issue #866

* feat(web): OLED Black theme + per-appearance custom colors (closes #866)

Add an explicit OLED Black appearance (true #000 canvas, border-based
elevation) alongside system/dark/light, and a curated "key color"
customizer. Each key color (background, surface, text, hint, accent,
border, user bubble) cascades to its --app-* tokens and is stored per
appearance so a color tuned for light never leaks onto pure black.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: HAPI <noreply@hapi.run>
2026-06-18 10:15:34 +08:00
5f27abddd4 feat(web): in-app PWA update prompt when new service worker is available (#946)
* feat(web): in-app PWA update prompt when new service worker is available (closes #938)

User-controlled reload with a persistent banner, visibility-triggered SW
checks, and an expandable rationale. Switches registerType to prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): align vite.config with soup layers for clean driver merge

Keeps registerType prompt while matching garden IWER stubs and PWA
share_target shape expected by feat/pwa-share-target in the manifest.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Revert "fix(web): align vite.config with soup layers for clean driver merge"

This reverts commit 6f0915b0884d029a2413d8819a4dfe81d7c4e595.

* fix(web): make PWA reload apply waiting service worker updates

Handle SKIP_WAITING in injectManifest sw.ts and reload via controllerchange
with a timed fallback when vite-plugin-pwa prompt mode does not navigate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): satisfy setTimeout mock typing in PWA reload tests

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): register PWA service worker before auth gates

Mount PwaUpdateProvider at app root and show the update banner on login
and error screens so registerSW runs for logged-out users too.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): offset PWA update banner below top status banners

Reserve top-12 when syncing or reconnecting so the reload prompt stays
visible above SyncingBanner and ReconnectingBanner.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): offset PWA update banner below voice error banner

Use PwaUpdateBannerWithStatusOffset inside VoiceProvider so voice errors
share the same top-12 reservation as sync and reconnect banners.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 10:15:10 +08:00
78155a9d27 perf(web): add staleTime to useSession to suppress focus/mount refetches (#884) (#885)
* perf(web): suppress useSession refetch storm (closes #884)

Two compounding behaviours in the React client were producing a sustained
~100 req/sec stream of GET /api/sessions/<uuid> to the hub on installs with
a moderately-sized session fleet:

1. `useSession` had no `staleTime`, so window-focus and remount each
   triggered a fresh REST round-trip even though SSE was already pushing
   the same data via `patchSessionDetail`.

2. `useSSE`'s `session-added`/`session-updated` handler unconditionally
   queued a per-session `invalidateQueries({queryKey: ['session', id]})`
   whenever the incoming SSE payload was not a structured patch, fanning
   out a refetch to every still-observed `useSession` regardless of
   whether the user was currently viewing that session detail.

Fixes:

- `useSession` now sets `staleTime: 30_000` (exported as
  `SESSION_DETAIL_STALE_TIME_MS` for testability/tuning).  SSE remains
  authoritative for freshness; the REST endpoint is a cold-start path.

- `useSSE` only queues per-session detail invalidation when an active
  observer is mounted for that session.  The new
  `hasActiveSessionDetailObserver` helper checks the TanStack query cache
  for `getObserversCount() > 0`.  List-summary invalidation is unchanged
  (sidebar still updates).

No behaviour change for the patch-path: structured `SessionPatch` events
still flow through `patchSessionDetail` + `patchSessionSummary` and update
in place.  The fallback path is what we are taming.

Measured on the reporter's box pre-fix: 31,944 GET /api/sessions/<uuid>
hits over a 5-minute idle window across 132 distinct session UUIDs
(~106 req/sec).  Expected post-fix: ~0 for sessions whose detail page
is not currently open, gated by `staleTime` for navigation thrash.

Tests:
- `useSession.test.ts` asserts `SESSION_DETAIL_STALE_TIME_MS` is set.
- `useSSE.test.ts` adds 4 cases for `hasActiveSessionDetailObserver`
  covering no-cache, cache-without-observer, mounted-observer, and
  cross-session isolation.

* fix(web): revert observer-gating in useSSE (address PR #885 review)

The Codex review on #885 correctly flagged that
`hasActiveSessionDetailObserver`-gating around the two
`queueSessionDetailInvalidation` fallback paths broke an important
correctness invariant: with `staleTime: 30_000` in place, skipping
the invalidation entirely (instead of letting TanStack mark the
cache stale) means a subsequent remount within 30s will serve the
stale cached detail without a REST recovery fetch.

This regressed real backend code paths.  Hub emits
`session-updated` events with no structured `data` field on todos /
teamState / metadata / agentState changes (see
`hub/src/socket/handlers/cli/sessionHandlers.ts:117,128,216,263`),
which hit the gated `else` branch.

Root cause of the over-correction was a misunderstanding of TanStack
v5 semantics: `invalidateQueries` with the default
`refetchType: 'active'` is *already* a network no-op for unobserved
queries — it just marks them stale.  The manual observer-count check
was structurally redundant *and* incorrectly suppressed the stale
marking.

Revert: restore the original unconditional
`queueSessionDetailInvalidation` calls on both fallback branches.
Drop the `hasActiveSessionDetailObserver` helper export and its 4
unit-test cases.  Keep Fix A (`staleTime: 30_000` on `useSession`)
intact — that change is independently safe and addresses the
focus-refetch / remount-refetch class of redundant requests.

* docs(web): correct staleTime rationale in useSession (#884)

Stand-in cold review on PR #885 caught that the comment overstated the
fix's reach.  `web/src/lib/query-client.ts:7` already sets the global
default `refetchOnWindowFocus: false` and `staleTime: 5_000`, so the
per-query `staleTime: 30_000` does NOT cut focus-refetches (there were
none) and only extends the remount/reconnect-no-refetch window from
5s to 30s.

Rewrite the comment to be accurate about scope: the change suppresses
remount refetches within a 30s window, and explicit `invalidateQueries`
(SSE fallback path, reconnect-recovery in `App.tsx`) still refetches
active observers — so live updates and recovery flows are preserved.

No code behaviour change; comment-only edit.

* fix(web): invalidate all cached session details on SSE reconnect

Codex review on PR #885 caught a real regression introduced by the
`SESSION_DETAIL_STALE_TIME_MS = 30_000` change: the reconnect-recovery
handler in `App.tsx` only invalidated the *currently-selected* session's
detail.  With per-query staleTime extended from 5s (global default) to
30s, a previously-viewed but non-selected session whose cache was still
within the freshness window could serve stale data after the SSE channel
missed updates during the disconnect.

Scenario:
1. User views session A → cache populated, fresh.
2. User switches to session B → A's observer unmounts, cache lingers
   (gcTime: 5min).
3. SSE disconnects.  Session A receives updates server-side that no
   patch event reaches the client.
4. SSE reconnects.  Old `handleSseConnect` only invalidated
   `session(selectedSessionId=B)`, NOT A.
5. User navigates back to A within 30s → useSession remounts → cache
   is still considered fresh by staleTime → no REST recovery fetch →
   user sees stale A data.

Fix: broaden the per-session invalidation in `handleSseConnect` from
`['session', selectedSessionId]` to the prefix `['session']`, which
matches every cached session-detail entry.  Active observers refetch
(same as before — only the selected session was active), inactive
cached entries get marked stale so the next remount refetches.

Performance impact: zero new fetches on reconnect (the selected
session is still the only one with an active observer in practice).
Marking inactive entries stale is metadata-only, free.

This restores the pre-staleTime invariant where every cached session
detail was either fresh (just fetched) or actively re-fetched on
reconnect, and matches the documented contract that SSE is the
authoritative freshness signal while REST is the cold-start /
reconnect-recovery path.

---------

Co-authored-by: heavygee <heavygee@users.noreply.github.com>
2026-06-18 10:13:36 +08:00
9af6696a68 fix(web): keep global SSE alive for session list status updates (#694)
When a session is open, the web app now keeps an always-on all:true SSE
connection for sidebar session-updated events while using a second
session-scoped stream for message delivery. Also bump session activity on
hub sendMessage so web-originated sends refresh list timestamps.

Fixes tiann/hapi#693

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 14:45:43 +08:00
junesandGitHub 60af9835b4 feat(web): 优化聚合 tool use 展示与聊天背景设置 (#619) 2026-05-13 13:04:31 +08:00
fad5dbbc30 fix(web): localize toast messages and keep full session counts (#573)
Normalize hub toast text in the web client for i18n coverage (including Ready for input notifications) and stop deduplicating session rows by agentSessionId so outline/group counts reflect user-visible sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 09:13:02 +08:00
f097f10716 Preserve history when deduplicating agent sessions (#471)
* fix(hub): merge histories for duplicate agent sessions

* fix(hub,web): refresh active duplicate history merges

* fix(hub): avoid active-active history merges

* fix(web): reset message window on history invalidation

---------

Co-authored-by: Liu-KM <Liu-KM@users.noreply.github.com>
2026-04-21 13:56:50 +08:00
Haoqing WangandGitHub 1c8cb5c90d fix(web): prevent mobile keyboard from covering chat input (#403) 2026-04-06 05:39:40 +08:00
QihanandGitHub 2b133feec5 fix(web): keep mobile views scrollable and new-session actions reachable (#364) 2026-03-26 08:04:58 +08:00
LihengwannaflyandGitHub 1942f088ff fix(sync): add SSE heartbeat and alive status (#223) 2026-02-28 01:46:21 +08:00
Mark LandGitHub 4a59d4896f fix: prevent /sessions/new from matching as dynamic sessionId route (#164) 2026-02-07 22:33:08 +08:00
weishu cc9b9279bd fix(web): require hub URL without clearing input 2026-02-06 17:03:46 +08:00
weishu 5b27f6b41e feat(web): Add reconnecting feedback when SSE connection is lost (#125)
- 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
2026-02-01 19:40:43 +08:00
weishu 37e10a831b feat: rename server package to hub
Rename the `server/` directory to `hub/` and update all references
across CLI, docs, web, and workspace configuration.
2026-01-27 19:51:21 +08:00
weishu a928e82db2 Add error hint 2026-01-19 19:03:01 +08:00
weishu 8f5b773b23 feat: voice assistant 2026-01-19 18:16:57 +08:00
weishu 7003bda706 fix: use baseUrl for Socket.IO connection to support separate frontend hosting
Previously, useTerminalSocket used a relative path `/terminal` for Socket.IO,
which would connect to the frontend host instead of the actual HAPI server.
Now it uses the baseUrl from context to connect to the correct server, enabling
scenarios like hosting the frontend on GitHub Pages while the server runs elsewhere.
2026-01-14 11:27:35 +08:00
weishu 5d1899616c fix: typecheck 2026-01-14 08:59:55 +08:00
weishu 2449d783f6 refactor: use useNavigate for URL parameter cleanup in AppInner 2026-01-14 08:39:36 +08:00
weishu 5defb6dbfc feat: add wireguard relay integration for public server access with End-to-End Encryption
Integrates tunwg (WireGuard tunnel) to enable optional public access
to the hapi server. Tunnel is disabled by default and enabled via
--relay flag or HAPI_TUNNEL=true environment variable.

Users can now run 'hapi server --relay' and get a direct link like:
https://app.hapi.run/?server=https://xxx.relay.hapi.run&token=xxx
2026-01-13 20:18:11 +08:00
ChenandGitHub aaaad33e61 feat: add i18n support and enhance session UI (#50) 2026-01-10 22:07:22 +08:00
weishu 803c2f65a9 feat: add notification optimization with visibility tracking and toast messages
Implement a visibility-aware notification system that delivers toast messages via SSE to visible browser tabs instead of sending push notifications, reducing unnecessary push requests. Includes VisibilityTracker for monitoring connection visibility, toast UI components, and enhanced SSEManager with toast delivery capability.
2026-01-07 18:58:31 +08:00
weishu 0f29ee182c refactor: replace React Query message cache with windowed message store
Introduces a new message-window-store module to manage message state with
automatic windowing of visible and pending messages. This replaces manual
React Query cache operations with a centralized, observable state system.
close #39

Key changes:
- New MessageWindowState tracks visible/pending messages with size limits
- Automatic trimming of message windows (400 visible, 200 pending messages)
- Pending message buffering when user scrolls away from bottom
- Centralized status updates for optimistic messages
- Thread component simplified with forwarded scroll and pending callbacks
- Removes message count tracking from components

This improves performance for chats with many messages and provides a
cleaner separation of concerns between UI and state management.
2026-01-07 16:59:12 +08:00
weishu 43519621f3 feat: add web push notifications support
Implement push notification system with VAPID keys, client service worker integration, and push subscription management. Includes server-side PushService for sending notifications and PushNotifier for reactive event handling, plus client-side usePushNotifications hook and service worker support.
2026-01-02 19:19:40 +08:00
weishu 8f7dcf848b refactor: Add Telegram binding storage and auth 2025-12-31 17:43:57 +08:00
weishu e758efb0ec feat: unify app name 2025-12-29 14:36:24 +08:00
weishu 0be023b23b feat: add configurable server URL for standalone web hosting
Adds support for hosting the web UI separately from the hapi server on static hosts (GitHub Pages, Cloudflare Pages). Users can now set a custom server origin via a dialog on the login screen, with the ability to return to same-origin behavior.

Changes include:
- New useServerUrl hook for managing server URL configuration and storage
- Updated API client to support baseUrl parameter for all requests
- Enhanced login UI with server picker dialog (top-right button)
- Auth system now keys tokens per baseUrl to support multiple servers
- SSE connection updated to use configured baseUrl
- Documentation updates for standalone hosting setup
2025-12-25 17:42:30 +08:00
weishu d15eafba4c fix: prevent LoginPrompt flash on page refresh with stored token
Add additional condition to check for the intermediate auth state where
authSource exists but token hasn't been fetched yet. This prevents the
LoginPrompt from briefly flashing when the page is refreshed and the user
already has a stored access token.

Changes the condition from just `isAuthLoading` to also cover the gap
before the useAuth effect starts fetching the token, displaying the
loading state instead of the login prompt.
2025-12-24 18:16:23 +08:00
weishu 7f2a36117c feat: add Spinner and LoadingState components for unified loading UX
Introduces two reusable loading components to standardize loading states:
- Spinner: Base spinner with accessibility support (role, aria-label)
- LoadingState: Semantic loading indicator combining spinner and label

Updates loading patterns across the app:
- Full-screen/block loading now uses LoadingState (App, router, files, file)
- Inline button loading states show spinner + text with aria-busy
- Message list loading replaced with MessageSkeleton component
- Removes duplicate SpinnerIcon definition from PermissionFooter

Improves UX and accessibility:
- Loading screens centered and full-height
- Consistent ellipsis character (…)
- Semantic accessibility (role="status", aria-live, aria-busy)
- Adds CSS variables for banner styling
2025-12-24 17:23:39 +08:00
weishu baaf02daf5 fix: prevent sync banner from flashing on session switches
Added isFirstConnectRef to distinguish between first SSE connection
(page load) and subsequent reconnects (session switches). Only the
first connection forces the banner to show; subsequent connects use
non-forced mode which only displays the banner when returning from
background within 30s, eliminating unwanted banner flashes on
session changes.
2025-12-24 15:10:00 +08:00
weishu 6d401fb9ec feat: add syncing banner and smart scroll behavior for chat thread
Implement two key UX improvements:

1. Syncing banner with visibility tracking:
   - New useSyncingState hook manages syncing state with safety timeout
   - Banner shows when SSE connects/reconnects
   - Auto-hides after 10s to prevent stuck spinner
   - Smart visibility detection to suppress banner when returning from background

2. Smart scroll behavior for chat thread:
   - Only auto-scrolls when user is near bottom (<120px threshold)
   - Shows "X new messages" indicator when user is reading history
   - Smooth scroll to bottom with indicator button
   - Resets state on session change

Files:
- New: useSyncingState hook for syncing state management
- New: SyncingBanner component for non-blocking indicator
- Modified: App.tsx integrates syncing with SSE handling
- Modified: HappyThread implements smart scroll with dynamic autoScroll
- Modified: index.css adds spinner and bounce-in animations
2025-12-24 14:05:55 +08:00
weishu 999ee64852 refactor(web): unify session creation into single page with path history 2025-12-22 21:45:11 +08:00
weishu 3bac5c7bc6 feat(web): integrate TanStack React Router for client-side routing
Replace state-based screen navigation with proper URL-based routing. This includes:
- New router configuration with routes for sessions, machines, and spawn pages
- App context provider to share API and token across the app
- useAppGoBack hook for handling browser and Telegram back navigation
- Refactored App component to render outlet and use router hooks
- Memory history for Telegram app, browser history for web
2025-12-20 16:49:10 +08:00
weishu d2ad977394 server: add SSE events stream for webapp
web: switch webapp updates to SSE
2025-12-19 21:28:53 +08:00
weishu 3a7272d03d feat(web): integrate TanStack Query for state management
Replace manual state management with TanStack Query (React Query) for more robust server state handling. This refactoring introduces:

- New hooks for queries: useSessions, useSession, useMessages, useMachines
- New hooks for mutations: useSendMessage, useSessionActions, useSpawnSession
- Centralized query client with optimized configuration (5s staleTime, disabled window focus refetch)
- Query key factory for consistent cache invalidation
- Improved message synchronization via socket events with cache updates
- Optimistic updates for message sending with retry capability
- Simplified App.tsx by removing manual state management logic
- Integrated React Query devtools in development mode

This enables automatic cache management, better error handling, and a foundation for more sophisticated data fetching patterns.
2025-12-19 18:25:13 +08:00
weishu 4606ba23d9 feat(web): implement conditional Telegram SDK loading
Add lazy loading of Telegram SDK only when running inside Telegram Mini App
environment instead of unconditionally loading in all browsers. Includes:

- New environment detection functions: isTelegramEnvironment(), isTelegramApp()
- Dynamic SDK loading with 3s timeout via loadTelegramSdk()
- Removed static script tag from index.html
- Made haptic feedback lazy (SDK check on each call)
- Made theme listeners lazy (attached in initializeTheme())
- Updated all components to use unified detection functions

This prevents SDK load time overhead in regular browser environments while
ensuring the app works correctly both in Telegram and browser contexts.
2025-12-18 22:54:40 +08:00
weishu 2a097d0cdf fix(web): prevent Android Chrome keyboard overlap 2025-12-18 19:05:01 +08:00
weishu f95350d245 fix: integrate History API for browser back button navigation
Syncs React routing state with browser History API. Clicking the browser
back button now navigates within the app instead of exiting. Telegram
BackButton behavior is preserved by checking for initData.
2025-12-18 16:06:10 +08:00
weishu be00343289 feat: add iOS Safari install guide and improve PWA UX with neutral theme
- Add iOS Safari install prompt with step-by-step modal guide
- Fix Telegram environment detection (check initData, not just SDK presence)
- Fix install prompt re-entrancy issue (clear deferredPrompt immediately)
- Extract icon components to separate file (web/src/components/icons.tsx)
- Rename "Happy App" to "Hapi" throughout the UI and manifests
- Change button/icon colors from blue (--app-button) to neutral theme (--app-fg/--app-bg)
- Add install dismiss state to prevent repeated prompts
- Improve iOS Safari detection with robust user agent parsing
2025-12-18 14:05:50 +08:00
weishu ea9f0f3b1a feat: implement progressive web app support with offline capabilities
Add complete PWA implementation including service worker registration,
offline support, and installation prompts:

- Add vite-plugin-pwa and workbox-window dependencies for PWA tooling
- Configure VitePWA plugin with web app manifest and app metadata
- Set up Workbox caching strategies for API endpoints and CDN assets
- Implement service worker auto-update with user-triggered refresh
- Create usePWAInstall hook to handle beforeinstallprompt events
- Create useOnlineStatus hook for monitoring network connectivity
- Add InstallPrompt component with haptic feedback integration
- Add OfflineBanner component to notify users of offline status
- Configure PWA icons and assets (64x64, 192x192, 512x512 variants)
- Add TypeScript type declarations for virtual PWA register module
- Integrate PWA components and service worker into App.tsx and main.tsx
- Add PWA meta tags and viewport configuration to index.html
2025-12-18 13:40:36 +08:00
weishu cf2b96b566 feat: add browser environment support with access token authentication
Enable the web client to run in plain browser environments alongside Telegram Mini App support:

- Server: Add CLI_API_TOKEN authentication path as alternative to Telegram initData
- Client: Add useAuthSource hook to detect and manage Telegram vs browser auth sources
- Client: Add usePlatform hook for platform abstraction with graceful haptic feedback degradation
- Client: Add LoginPrompt component for browser access token login
- Client: Extend useTheme to fall back to system prefers-color-scheme in browser
- Client: Migrate all direct HapticFeedback calls to use usePlatform hook
2025-12-18 12:45:49 +08:00
weishu 0485065b87 refactor: improve message synchronization with sequence-based pagination and merging 2025-12-16 21:07:20 +08:00
weishu b4654acb92 init 2025-12-16 15:03:50 +08:00