mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-06 01:11:37 +00:00
* feat(android): composer @ mentions with session chips and file hits Mirror the web composer @-mention autocomplete: an active `@word` at the composer tail opens a dropdown that merges two sources like the web's `[...sessionHits, ...fileHits]`: - Session rows from the cached session list via `matchSessionsForMention` (web `sessionReference.ts` port): sidebar dedupe by `flavor:agentSessionId`, `hasConversationContent == true` gate so empty stubs are never reference targets, current session excluded, score tiers (title exact/prefix/contains, id prefix/contains, other-field) with active +50 / archived -25, Long-scored with updatedAt as tiebreak, limit 20. Insert text is Copy-reference prose (`See session "<title>" (/sessions/<id>) for context.` + steer suffix) with JSON-quote escaping, whitespace normalization, and 120-grapheme truncation via BreakIterator. - File rows via `GET /api/sessions/:id/files` (limit 50) for codex / copilot flavors only: copilot inserts `@<fullPath>`, codex inserts `@"<fullPath>"` with quote/backslash escaping; RPC failure degrades silently to session-only rows (web parity), CancellationException rethrown. Trigger detection ports `findActiveWord.ts` byte-for-byte (hard-stop scan, `@` boundary rule, `/`+`.` path continuation forward-only, bare `@` popup, no code-fence exemption), evaluated at the end of the text (the Android composer is a cursor-less String field - full cursor-aware mirror kept as `Mentions.findActiveWord`). Selection swaps the whole word and appends a trailing space (`applySuggestion.ts`). The `hasConversationContent` gate field already exists on this lineage as a non-nullable Boolean (absent / JSON null decodes to `false` via HapiJson coercion, asserted in a new SessionDecodingTest case); the `searchSessionFiles` seam moves onto `ChatSessionApi` with a defaulted `limit` parameter. MentionDropdown reuses the SlashCommandDropdown chrome; new string `chat_mention_suggestions` lands at identical key positions in en/zh-rCN/ru, and three zh keys a prior merge had parked at the file tail move back to their en-mirrored positions so all three locales key-sync exactly. Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Align mention prefix scan and title whitespace with the web - findActiveWord default prefixes now mirror the web trigger set ['@', '/', '$'] (HappyComposer.tsx): a boundary-legal '/' or '$' tail word stops the backward scan, so "@ref /tmp" yields the "/tmp" word (routed to the slash/skill domain) instead of joining a multi-word mention query whose whole-word swap would swallow the typed "/tmp" on selection. - sanitizeSessionReferenceTitle now collapses the JS /\s+/ Unicode whitespace set (NBSP, U+3000, U+2028/2029, U+FEFF, ...) so CJK titles normalize byte-identically to the web. Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Mentions: caret-aware detection + stale-row invalidation (review fixes) Consolidates the two review fixes from #2155 into this implementation: caret-position — detection and insertion now evaluate at the composer caret instead of the text tail. Mentions.mentionQueryOf gains a selection-aware overload; the VM tracks text + caret as one atomic ComposerDraft (mirroring the field's TextFieldValue), ChatComposer reports text/selection in a single callback and adopts external text at the reported caret, and applySuggestion rests the cursor after the insertion. A selected range suppresses suggestions (web rule). stale-suggestion — the file leg emits an empty batch before awaiting each RPC, so the previous query's rows never linger while a new query's request is in flight (delayed-response regression test included). Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Fix androidTest lambda arity after composer callback consolidation CodexPlanActionsTest renders ChatComposer with onTextChange = {}, which no longer compiles now that the callback reports (text, selectionStart, selectionEnd) atomically. Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * feat(android): composer scheduled send with preset and specific picks The composer clock entry (web ScheduleTimePicker twin): the exact web preset set (+5m/+30m/+1h/+4h, resolved at the send instant) plus a specific-datetime tab with the web's 30 s past-grace and 7-day ceiling (ScheduleSend pure-logic object). A pending pick arms ChatViewModel.pendingSchedule, expires absolute picks on the clock, downgrades steer long-presses to queue (the wire forbids scheduledAt + steer), rides the send RPC as epoch-millis scheduledAt, and the queued bar's Edit re-arms a future fire time as an absolute pick (computeEditPendingSchedule). Queue rows now label with the web formatScheduledTime ("Oct 25, 2:30 PM", the year only when it differs from the current one). Declared divergences from web/src (all also in KDoc): - Paste gate: the web drops file pastes while a schedule is pending; the Android composer has no clipboard-file paste path, so the illegal combination is made unreachable instead — the attach button disables while a pick is pending and the clock button disables with attachments on the tray (the same mutual exclusion the web enforces). - Pending-pick lifetime: the web clears the pick only after the send mutation is accepted and restores it on failure; Android clears at dispatch because the failed row already carries wire scheduledAt and retryFailedMessage re-fires with it — the retry is the restore. - Panel chrome: the web anchors a fixed panel above the clock button (bottom sheet on touch viewports) and closes on outside-pointer-down; Android renders the panel above the input card like the slash/mention dropdown precedent and closes on the clock toggle or a selection. The specific tab splits the web's single datetime-local box into date + time fields for touch keyboards; the web's min/max input attributes are browser affordances, not mirrored — the pure validator is the gate, as on the web. Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Guard scheduled-send edge paths found in adversarial review - editQueuedMessage no longer re-arms the schedule restore while the attachment tray is non-empty: the wire forbids scheduledAt + attachments and the clock button is disabled with attachments, so an armed pick would be invisible, un-clearable, and crash the send on the invariant check. The drop surfaces a notice instead. - The restore is now guarded by a pick snapshot taken at tap time: a schedule re-picked during the cancel round-trip wins over the restore (web snapshots text + pendingSchedule together in onEdit). - The /clear|/new concurrent shortcut now requires scheduledAt == null like the web — a scheduled clear queues as a scheduled message. - The pending-schedule expiry job is an AtomicReference: set/clear raced across the main thread and the edit-restore coroutine could leak a stale job that clears a newer pick at its deadline. - formatScheduledTime follows the locale's hour cycle and date order like web Intl ("25 окт., 14:30" / "10月25日 14:30" / "Oct 25, 2:30 PM"); the zh queued-scheduled label drops its full-width colon. Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Fix preview lambda arity after composer callback consolidation Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Reject scheduled sends over attachments and serialize schedule transitions The wire contract excludes attachments from scheduled sends, but only the clock button's enabled state enforced it — the picker panel stays open across an attachment add and the share-import path can drop a file into the tray while a pick is armed. Three layers now hold: - ViewModel transition gate: arming a pick over a non-empty tray is rejected in applyPendingScheduleLocked (clears always allowed); the open picker panel also closes the moment a chip appears (ChatComposer). - Send guard: sendMessage resolves the schedule BEFORE the draft clears or the tray consumes and bails with a ScheduleAttachmentConflict notice, so both sides stay recoverable in the UI. - performSend's invariant tripwire stays as the last net. Same commit, the two flagged Minors from review: - Replacement/expiry serialization: the pick value swap and the expiry registration become one critical section (scheduleMutex), replacing the AtomicReference job handle whose getAndSet still left a window where a restore published A, a callback picked B, and A's timer cleared B at A's deadline. Uncontended transitions stay synchronous via tryLock; the timer body re-checks the revision inside the lock so an obsolete timer can never clear a newer pick. - Queued-edit restore now snapshots the schedule revision (web pendingScheduleRevisionAtEdit) instead of value equality, so a pick - or a pick plus explicit clear - re-made while the cancel round-trip is in flight wins over the restore even when the slot's value lands back where it started. Interaction tests: rejection while chips sit in the tray, pre-consume send guard, consecutive absolute picks keeping only the newest expiry, a preset surviving a restored pick's deadline, and edit-restore losing to both a mid-flight pick+clear and a clear+same-preset reselection. Co-Authored-By: Claude Code <noreply@anthropic.com> * Mask specific date/time entry for digit keyboards The Specific-tab fields kept KeyboardType.Number, but a numeric soft keyboard has no `-` or `:` key, so the required YYYY-MM-DD / HH:mm shapes were literally untypeable. The fields now auto-insert the separators as digits arrive (maskDateInput / maskTimeInput): digits only, capped at the wire shape, idempotent on already-formatted text (a pasted "2026-11-05" stays put; deleting just re-masks the rest). A half-typed pair (non-blank, so Submit is tappable) used to fail the tap silently: parseSpecificInput returns null and onSubmit did nothing. It now surfaces an inline invalid-shape error (SpecificError.Invalid, Android-only — the web's datetime-local box cannot hold a malformed shape), joining Past/TooFar and clearing on the next edit like them. Pure-logic tests cover keystroke-by-keystroke separator growth for both fields, mask idempotence, non-digit/overflow capping, and that mask output feeds the strict parser unchanged (partial masks parse to null, never a silently wrong instant). Test tags (schedule-date-input / schedule-time-input / schedule-submit) added for the instrumentation tests. Co-Authored-By: Claude Code <noreply@anthropic.com> * Add schedule composer instrumentation tests Device-level state transitions for the two review-flagged surfaces, one setContent per test against the 320dp CI viewport: - Mutual exclusion: an open picker panel dismisses the moment an attachment chip lands, and the clock button reports disabled with the non-empty tray. - Preset flow: tapping +30m delivers Preset("+30m") and closes the panel. - Digit-IME entry: performTextInput injects through the IME channel (the soft keyboard's path), so raw digits typed into the Specific fields must come back masked ("20261105"→"2026-11-05", "1430"→"14:30") and the submitted pick resolves to the typed wall time in the device zone, asserted against the exact epoch ms. - Partial input ("20261"/"143") submits to the inline invalid error string, not a silent no-op. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: HAPI <noreply@hapi.run>