mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
b2a30c2e390c58b111b0ccec757c2eb15a0c3591
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2a30c2e39 | feat(hub,web): support scheduling messages for future delivery (#590) | ||
|
|
7d55bc1456 |
feat(web): float queued messages above composer until invocation (#542)
* refactor: add invoked_at column and propagate via messages-consumed - Bump hub schema to V8: add `invoked_at INTEGER` to messages table - Add `migrateFromV7ToV8` (idempotent ALTER TABLE ADD COLUMN) - Add migration chain entries for V4/V5/V6/V7 → V8 - Expose `StoredMessage.invokedAt: number | null` and `markMessagesInvoked` - Record server-side `Date.now()` in hub on `messages-consumed` socket event - Propagate `invokedAt` through SSE (`messages-consumed` payload) - Update `markMessagesConsumed` in web store to accept and store `invokedAt` - Preserve optimistic `invokedAt` in `mergeMessages` (server echo path) - Add migration unit tests (fresh V8, V7→V8 ALTER, markMessagesInvoked) * feat(web): float queued messages above composer until invocation Show queued (uninvoked) user messages in a dedicated floating bar above the composer instead of inline in the thread timeline. Once the CLI acks the batch via messages-consumed, the bar disappears and the messages appear in the thread at their invocation position (invokedAt ordering). - Add QueuedMessagesBar component: subscribes to message-window-store, filters user messages with invokedAt==null, shows clock icon + text preview; disappears when all messages are invoked - Filter queued messages from thread (visibleMessages), sort by invokedAt ?? createdAt so invoked messages land at the right position - Extend markMessagesConsumed to update server-loaded messages (status undefined) in addition to optimistic (status 'queued'), enabling multi-device and post-refresh scenarios - Remove opacity-60 from UserMessage: queued messages no longer appear in the thread so the dimming branch is unreachable - Include invokedAt in getMessagesPage/getMessagesAfter API responses so the web client can restore floating-bar state after page refresh - Add invokedAt field to DecryptedMessageSchema for shared protocol type * fix(hub,web): make sort use invokedAt and V8 backfill idempotent - compareMessages: prioritize invokedAt/createdAt over seq so invoked messages land at their invocation position rather than their send-time seq position - migrateFromV7ToV8: move backfill outside the ALTER guard so it re-runs if a previous attempt crashed between ALTER and UPDATE before the user_version bump (idempotent WHERE invoked_at IS NULL) * fix(hub,web): cover localId-less messages and live-ack invokedAt - addMessage: messages without a localId have no ack path (markMessagesInvoked matches by localId). Treat them as already-invoked at insert time so they land in the thread instead of sitting in the queued floating bar forever. - markMessagesConsumed: apply the ack even when the message is already 'sent' optimistically, so the live window receives invokedAt instead of waiting until a full refetch. * fix(hub): propagate invokedAt in live message-received SSE payload The SSE `message-received` event omitted `invokedAt` while REST pagination included it, so localId-less CLI/local user messages arrived on the live wire as queued (`invokedAt == null`) and stayed in the floating bar until a full refetch replaced them with the stored row. * fix(hub): propagate invokedAt in CLI socket message-received handler The CLI socket 'message' handler fans out to web via a separate `onWebappEvent` publisher; the previous fix only touched the `MessageService` publisher. Aligns the live SSE payload shape with the REST/page-load shape so localId-less CLI/local user messages with `invokedAt = createdAt` (set in addMessage) reach web filters with the field already populated, instead of being misclassified as queued until a full refetch. * fix(hub,web): add byPosition pagination to fix long-session queued message loss Pagination used seq-based windows, so queued messages with low seq but late invokedAt fell outside the visible window on refresh. Fix by adding a V8 byPosition mode that orders by COALESCE(invoked_at, created_at) DESC, seq DESC with a composite cursor, while keeping the V7 seq path fully intact for backward compatibility. - hub/store/index: add idx_messages_session_position (createSchema + V7→V8 migration) - hub/store/messages: add getMessagesByPosition with composite cursor SQL - hub/store/messageStore: delegate getMessagesByPosition - hub/sync/messageService: add getMessagesPageByPosition with nextBeforeAt response - hub/sync/syncEngine: expose getMessagesPageByPosition - hub/web/routes/messages: byPosition=1 query param dispatches to V8 path - web/types/api: MessagesResponse.page gains optional nextBeforeAt - web/api/client: getMessages gains byPosition + beforeAt options - web/lib/message-window-store: fetchLatestMessages/fetchOlderMessages use V8 composite cursor; fallback to seq cursor when hub returns no nextBeforeAt - hub/store/migration-v8.test: 7 new tests covering position sort, composite cursor pagination, long-session scenario, V7 compat, and index existence * fix(hub,web): re-sort on consume and use position cursor for next fetch - markMessagesConsumed: re-merge with empty list to re-sort by position key after invokedAt is set. A queued user message becomes visible with the consume event; without re-sort it stays at its send-time array slot until the next fetch overwrites it. - getMessagesPageByPosition: pick the cursor from stored[0] (oldest in position order) instead of scanning for minimum seq. With the page already in ascending position order, scanning for min seq could land on a low-seq, late-invoked row that is actually the newest in the page, causing the next older fetch to overlap. * fix(web): trust invokedAt as the only invocation signal and pin cursor pair - visibleMessages predicate (SessionChat + QueuedMessagesBar): drop the status === 'sent' check. status='sent' only means the REST write returned, not that the CLI consumed the message; an optimistic 'sent' with no invokedAt is still queued. invokedAt is the single source of truth for invocation. - byPosition cursor: track oldestPositionSeq alongside oldestPositionAt so the server's cursor pair travels through the next older fetch unchanged. Recomputing beforeSeq from the local window's minimum seq could combine it with a server beforeAt that referred to a different row, causing the SQL cursor to skip or overlap. * fix(hub): include uninvoked local messages in latest page Long sessions can push a queued user message (invokedAt = null, sort key = createdAt) outside the latest position-ordered page once the agent emits more than `limit` later rows. A refresh or secondary client then never receives the row, the floating bar stays empty, and the later `messages-consumed` event only carries localIds — there is no way to materialize the missing row at invocation time. Pin uninvoked local user messages to every latest-page response out-of-band. The pagination cursor still anchors to the position-ordered page rows, so older-page fetches are unaffected. * fix(web): preserve queued messages across trimVisible The visible-window trim drops the oldest entries beyond VISIBLE_WINDOW_SIZE, but a queued user message (invokedAt = null) sorts by send time and is the oldest item. Once a long agent stream pushes it past the window the row is gone from the client store, and the `messages-consumed` SSE carries only localIds — there is no way to restore or reposition the dropped row without a full refetch. Pull queued rows out before slicing the regular budget, then merge them back in. Queued rows are bounded by composer/CLI queue depth and do not meaningfully grow the window. * fix(web): use strict null for queued check and fall back invokedAt - Optimistic message sets invokedAt: null explicitly so the strict-null queued check matches the local opt-in. Pre-V8 hub responses that omit the field (`undefined`) are treated as already-invoked and stay in the thread instead of being misclassified as queued. - markMessagesConsumed: when the consume SyncEvent omits invokedAt (older hub) fall back to client time, otherwise a message that receives an ack with no server timestamp stays queued forever under the new strict-null filter. The persisted server value is still authoritative on next fetch. * fix: comprehensive invokedAt propagation hardening (review feedback batch) Bot review surfaced 11 propagation bugs incrementally; this batch fixes 9 additional adjacent issues found by hostile-review to break the incremental discovery cycle: - legacy DB (user_version=0 with HAPI tables): step ladder runs V1→V8 before createSchema so pre-existing tables get all later columns/indexes - step ladder includes V1/V2/V3 entries; previously V1-V3 DBs threw - mergeSessionMessages collision branch forces invoked_at = created_at so unmergeable rows can't strand in the floating bar - session-end auto-invokes still-queued user messages and broadcasts messages-consumed; the floating bar no longer pins ghost rows after the CLI is gone - trimPending preserves queued rows symmetrically with trimVisible - markMessagesInvoked is first-write-wins; duplicate acks are no-ops rather than re-stamping invoked_at and reordering the thread - markMessagesConsumed migrates just-acked pending entries into the visible thread so non-at-bottom users see their own messages without scrolling - mergeMessages dedup window compares by position key (invokedAt ?? createdAt) instead of createdAt only, so late-invoked optimistic copies don't duplicate the server echo - isQueuedForInvocation centralized in lib/messages.ts (single predicate used by SessionChat, QueuedMessagesBar, and the store) * fix(web): mirror hub's first-write-wins on markMessagesConsumed The hub's markMessagesInvoked is first-write-wins, but the web store was still overwriting any non-null invokedAt with the latest messages-consumed timestamp. A duplicate ack (CLI re-emit) would leave the SQLite row at the original timestamp while live clients moved the message to the duplicate ack time, diverging until refetch. Mirror the guard: only set invokedAt when it is null. * fix: in-scope hostile-review polish Web: - fetchLatestMessages: persist the V8 composite cursor pair on the non-at-bottom branch too. Without this, a refresh while scrolled up dropped the cursor and the next loadMore fell back to V7 seq mode against a V8 hub — same asymmetric class of bug commit 30df6b2 fixed for the at-bottom path. - markMessagesConsumed: tighten the loose-null check on invokedAt to strict null, consistent with isQueuedForInvocation and the rest of the file. The idSet filter already shields V7-stamped rows from this path, but the strict-null contract should not vary by call site. - messages: drop the upsertMessagesInCache export. It has no callers (verified with grep) and is the only user of the InfiniteData / MessagesResponse imports, so the imports go with it. Hub tests: - migration-v8.test.ts: add a session-end auto-invoke test (getUninvokedLocalMessages + markMessagesInvoked clears every queued row and stamps them all with the same invokedAt) and two byPosition union tests covering (1) a low-position queued row pushed out of the latest page is still surfaced via the uninvoked set, and (2) pageRows[0] is the oldest row in the page so the web client can safely anchor the next-older cursor on it. * fix(hub,web): bot-13 polish — atomic SSE on DB success and attachment chip text - sessionHandlers messages-consumed: emit messages-consumed only after markMessagesInvoked succeeds. Otherwise a transient SQLite failure would broadcast an invokedAt that was never persisted; live clients would hide the queued rows while a refresh / secondary client would see them as queued again, diverging the state. - QueuedMessagesBar: fall back to attachment filenames when the message text is empty. The composer / POST /messages allow attachment-only sends; without the fallback those queued messages rendered as blank chips until invocation. |
||
|
|
32755f9056 | feat(web): show queued status for messages pending inference (#492) | ||
|
|
c32378b3ba |
feat(web): persist composer draft across session switches (#438)
* feat(web): persist composer draft across session switches Switching between sessions now preserves the text typed in the composer. Drafts are stored per-session in sessionStorage and restored when the user navigates back. - Add composer-drafts utility (sessionStorage, in-memory cache) - Restore draft on HappyComposer mount, save on unmount - Clear draft on message send - Evict oldest drafts when exceeding 50 entries - Add unit tests for composer-drafts Fixes #231 * fix(web): add key prop to HappyComposer for explicit remount on session switch * fix(web): move clearDraft to SessionChat after send validation Prevents draft loss when Codex rejects an unsupported slash command. * fix(web): remove explicit clearDraft, rely on unmount save Successful sends clear the composer text, so the unmount save naturally persists an empty string which deletes the draft entry. This avoids clearing the draft when the send is blocked or fails. * fix(web): clear draft on successful send via onSuccess callback Move draft clearing to the send-success path so drafts are only removed after the message is actually accepted by the server. * fix(web): pass session ID to onSuccess to clear correct draft The previous version used the current route's sessionId, which could clear the wrong draft if the user switched sessions before the send completed. * test(web): add useSendMessage onSuccess callback tests Verify that onSuccess receives the correct session ID (including resolved IDs), and is not called on send failure or block. * refactor(web): extract useComposerDraft hook with unit tests Extract the draft save/restore logic from HappyComposer into a dedicated useComposerDraft hook. Adds 6 unit tests covering: - mount: restores saved draft via rAF - mount: skips restore if composer already has text - mount: skips restore if no saved draft - unmount: saves current text after rAF has fired - unmount: skips save before rAF (draftReady guard) - no-op when sessionId is undefined * fix(web): clear both route and resolved session drafts after send When resolveSessionId swaps the session (e.g. inactive → resumed), the sent ID differs from the route's session ID. Extract clearDraftsAfterSend so both are cleared and unit-testable. * fix(web): refresh eviction order when updating an existing draft Delete the key before re-inserting so Object.keys() reflects the most recent write, preventing a recently edited draft from being evicted first. |
||
|
|
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 |
||
|
|
87c79c3b94 |
feat: add session resume capability with automatic continuation
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 |
||
|
|
53e4de6684 | feat: add file upload support | ||
|
|
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. |
||
|
|
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. |