Commit Graph
1263 Commits
Author SHA1 Message Date
weishu b09dd67268 feat(android): protocol wire models, catalogs, session patching (B-M1a)
:core:protocol, package app.hapi.protocol — pure-JVM foundations for the
Android client, mirrored from shared/src/schemas.ts + sessionSummary.ts and
docs/api/client-contract/{sse,pagination,messages}.md:

- wire/: shared HapiJson config; DecryptedMessage with tri-state invokedAt
  (custom serializer — explicit null = queued vs absent = legacy-invoked);
  AttachmentMetadata; AgentState + (completed) requests; Session verified
  field-by-field against SessionSchema; typed-subset SessionMetadata;
  SessionSummary/PendingRequest; SessionPatch with VersionedValue wrappers
  and a strict SessionPatches.parse (unknown key / empty / mistyped -> null,
  replicating SessionPatchSchema.strict()); Machine family; the 13-type
  SyncEvent union behind total SyncEvents.parse (unknown/malformed ->
  Unknown, never throws; machine-updated data kept tri-state for the
  removed-vs-refetch distinction); MessagesPage + REST envelopes.
- catalog/: AgentFlavor (known + Other(raw)) with capabilities/labels from
  flavors.ts; PermissionMode/tone table, per-flavor mode lists, codex
  collaboration + copilot agent modes incl. legacy 'fleet' normalization.
- patch/SessionPatching.kt: exact port of web/src/lib/sessionPatch.ts —
  strict > version gates, max-monotonic updatedAt, activeAt <60s
  render-irrelevance, and deliberately NOT applying activeTurnStartedAt /
  scratchlistUpdatedAt (the TS reference never assigns them).

Tests (85 total, all green): golden-fixture decoding over shared/fixtures/
chat/* (fails loudly on zero files or unsupported VERSION), version-gate /
keep-alive cases ported from useSSE.test.ts, full-session + list + messages
page decode, SyncEvent union coverage, catalog pins (TODO(K6): switch to
fixtures/catalogs/modes.json once generated). :app:assembleDebug verified.
2026-08-17 12:06:32 +08:00
weishu 81146e68c5 docs(api): drop temporary dead-link ignores now that all contract pages exist 2026-08-17 11:28:14 +08:00
weishu 3885f65ad5 merge: B-M0 Android scaffold 2026-08-17 11:26:09 +08:00
weishu 2ffaf7fb82 merge: A-M0 iOS scaffold 2026-08-17 11:25:53 +08:00
weishu 659fe258ff merge: K4+K5 golden fixtures 2026-08-17 11:25:46 +08:00
weishu 1a40db3b2d merge: K2 client contract docs (sse/pagination/messages) 2026-08-17 11:25:46 +08:00
weishu 68f4f92e48 merge: K1 client contract docs (auth/rest/errors) 2026-08-17 11:25:46 +08:00
weishu d87ce4206e merge: K3 pairing label 2026-08-17 11:25:46 +08:00
weishu 9479f667b5 feat(fixtures): golden chat fixtures generated from web pipeline (K4+K5) 2026-08-17 11:23:34 +08:00
weishu 83bd25aa3b feat(android): scaffold Compose app + core modules + CI (B-M0) 2026-08-17 11:22:35 +08:00
weishu e0e7be39c3 feat(ios): scaffold SwiftUI app + HapiKit package + CI (A-M0) 2026-08-17 11:18:50 +08:00
weishu 1b9ca34fb5 docs(api): add native client contract (sse, pagination, messages) 2026-08-17 11:18:14 +08:00
weishu 16ebba12e8 docs(api): add native client contract (auth, rest, errors) 2026-08-17 11:15:00 +08:00
weishu 4f671453c9 chore(hub): platform-neutral companion pairing label 2026-08-17 11:08:04 +08:00
weishu 12847ddd0f Release version 0.28.0 2026-08-17 10:03:11 +08:00
AnanovoandGitHub d644d4f30f feat(cli): follow conversation language in status prompt (#1584) 2026-08-16 23:06:06 +08:00
AnanovoandGitHub 76e00f0091 feat(hub): add background-only ServerChan fallback (#1608)
* feat(hub): add background-only ServerChan fallback

* fix(hub): validate ServerChan background setting type

* fix(web): preserve pending visibility transitions

* fix(web): guard visibility reports across subscriptions
2026-08-16 23:04:03 +08:00
SSU-WEI HUANGandGitHub 6c6f4b4929 feat(agy): replace fragile PTY/TUI wrapper with headless print-mode transport (#1591)
Replace the Antigravity (agy) integration — a PTY wrapping the TUI with
output-marker scraping ('? for shortcuts', 'Generating', trust dialogs,
/model picker navigation, quota-screen regex) — with a headless print-mode
transport: every user turn spawns `agy -p <msg> --conversation <uuid>
--output-format stream-json`, and NDJSON events (init/step_update/result)
map onto the existing transcript-entry channel (sendAgySessionMessage), so
hub/web rendering is unchanged. ~8.3k LOC (incl. tests) removed.

Fixes #1588. Design: docs/design/agy-headless-transport.md.

CLI:
- new cli/src/agy/headless/: agyNdjsonParser (pure functions, malformed-line
  tolerance, step conversation-id adoption), AgyPlannerAccumulator (per-step
  delta accumulation with settling retries), AgyHeadlessDriver (per-turn
  spawn/kill loop, NDJSON chunk buffering, authoritative delivery ack via
  user_input/result, interrupt + retry + shutdown lifecycle with consume/
  restore, process-tree termination, SSH agent preserved, prompt log
  redaction, per-turn model snapshot with conversation-DB fallback)
- runAgy/loop/session rewired; agy is remote-only (no PTY, no local mode,
  no local-switch action); queued batches snapshot model/effort/mode
- deleted agyPty, agyPtyLauncher, agyHookCarrier(+scope cache), agyModelKeys,
  agyQuestionKeys, agyAskQuestion, agySessionScanner, agyPermissionHandler
  (+tests); buildAgyHooksJson removed; startHookServer agy-pre-invocation
  route → 200 no-op
- runner: agy reopen/resume via generic --existing-session-id; commands/
  agy.ts defaults remote; resume rejects ACTIVE agy sessions (remote-only,
  in-flight turns cannot hand off)
- MCP stays user-managed (agy reads ~/.gemini/config/mcp_config.json and
  workspace .agents/mcp_config.json natively in headless — verified)

Hub/web:
- machines.ts drops agy→pty forcing and rejects non-remote startingMode
- NewSession drops agy startingMode='pty'; terminal toggle disappears
  automatically; RemoteModeDisplay hides the local-switch hint when absent
- docs/guide/agents.md updated: headless print mode, no PTY/hooks, MCP via
  user's own mcp_config.json

Tests: 47 parser+driver tests (fake-binary e2e, chunk-split NDJSON, delivery
ack semantics, interrupt/retry/shutdown races, model attribution, EOF
framing, malformed envelopes); full suite green (cli ~2340, hub 1093,
web 2474, shared 262). Real-binary smoke on agy 1.1.13: single turn exit 0,
--conversation resume keeps the same conversation_id.
2026-08-16 22:44:27 +08:00
SSU-WEI HUANGandGitHub a6feb6e8ba feat: unified agent configuration descriptors (session config consolidation) (#1469)
* feat(config): add agent config descriptor protocol and advertise via runner capability

Introduce shared agent configuration descriptors covering model, effort,
permission, and secondary settings per agent flavor, plus the canonical
HAPI YOLO -> native permission mode mapping. Runners advertise the
builtin descriptors through the runner-state capability so hubs and web
can render configuration without hardcoded flavor branches.

Migrate the OpenCode create-session model picker from a bespoke radio
list to the shared SelectControl combobox.

* feat(web): render create-session permission from agent config descriptor

Replace the flavor-branched Grok/Codex-family/YOLO permission block with
a descriptor-driven PermissionField. Pi now reports permission as managed
instead of silently ignoring the YOLO toggle, and YOLO-only flavors show
the native permission mode the preference maps to.

Removes the superseded GrokPermissionModeSelector and
CodexFamilyPermissionModeSelector components.

* ci: retry flaky claudeRemote 5s-timeout failure

* fix(web): persist explicit OpenCode Default selection instead of restoring a concrete model

The parent initialization effect treated every null selected model as
'uninitialized' and auto-picked a concrete advertised model, clobbering
the user's explicit Default choice (and a restored Default preference).
null now means explicit Default and is preserved; only undefined (no
choice made yet) triggers probe-based initialization. Add parent-level
regression tests for Default persistence and remembered-model restore.

* fix(web): accept undefined selected model in OpencodeModelSelector props

* feat(web+cli+hub): unify create-session model/effort fields and add Pi model/effort support

Pi's agent config descriptor now advertises model (machine) and effort
(static thinking levels) for create AND session availability:
- cli: ListPiModelsForMachine RPC runs 'pi --list-models' (cached, inflight
  deduped) and parses the provider/model table; startup model match accepts
  provider-qualified ids
- hub: GET /api/machines/:id/pi-models route + rpcGateway/syncEngine passthrough
- web: NewSession renders Pi models grouped by provider through the generic
  ModelSelector and a new descriptor-driven EffortField (replaces the
  per-flavor LaunchEffortSelector/ReasoningEffortSelector pair); launch payload
  forwards Pi model + thinking-level effort (runner already supported --model/
  --effort for pi)

* fix(web): render Pi provider groups in ModelSelector and scope Grok availability warning

- ModelSelector now renders grouped options as <optgroup> (Pi models are
  provider-grouped; identical modelIds from different providers stay distinct)
- PermissionField only receives autoPermissionModeSupported for Grok — a
  cached Grok probe result no longer leaks the Grok warning onto other agents

Addresses HAPI Bot Minor findings on #1469.

* fix(web): drop Object.groupBy from ModelSelector; revalidate restored Pi models against the catalog

- ModelSelector buckets options with a reduce instead of Object.groupBy
  (Safari < 17.4 has no polyfill — New Session would throw on those clients)
- Pi restored model/effort are cleared when the value is absent from the live
  machine catalog, and Create waits for the catalog while a non-default Pi
  choice is being validated (mirrors Codex/Grok/Copilot handling)

Addresses HAPI Bot findings on #1469.

* fix(pi+web): serialize startup model before thinking level; hide Pi launch controls during history import

- PiSession gains startupModelSettled; the startup set_thinking_level waits for
  the requested model's set_model attempt to settle first, so a level the
  default model rejects is not lost before the requested model is confirmed
  (set_model and set_thinking_level were already serialized by the runtime
  mutation lock; this pins the model-first ordering)
- Create Session hides Pi model/effort controls while a Pi history import is
  selected — the import reopens the native session as-is and would silently
  ignore launch-only model/effort values

Addresses HAPI Bot findings on #1469.

* fix(pi): settle startup-model gate when model discovery fails or returns no models

A failed or empty get_available_models response would leave the
startupModelSettled gate unresolved, stranding a requested startup effort
indefinitely. Resolve the gate on the error path and the empty-models path;
adds regression tests for both.
2026-08-16 22:43:39 +08:00
SSU-WEI HUANGandGitHub 79ffe9aa0e feat(web): composer model/effort value buttons and first-class permission (part of #1438) (#1475)
* feat(web): composer model/effort value buttons and settings order

Wide composers now show [model] and [effort] value buttons for non-Pi
flavors (labels from the current session values), opening the settings
sheet on click. Narrow viewports collapse to the settings button only
via a new useNarrowViewport hook. The settings sheet reorders to
Model -> Effort -> Permission -> other settings (Fast mode,
collaboration, Copilot agent mode) so permission is first-class.

Toolbar customization gains 'model'/'effort' items with settings labels.
Pi keeps its dedicated model/thinking panels unchanged (unified
descriptor-driven sheet is a follow-up).

* fix(web): satisfy strict types in composer value-button test harness

* fix(web): address review findings on composer model/effort value buttons

- Normalize null/'auto'/'default' model wire values onto the value:null
  option so default-model sessions keep a localized label button (Major)
- Exempt model/effort value buttons from the settings outside-click
  dismissal so a second click closes the sheet instead of reopening it
- Read matchMedia synchronously in useNarrowViewport so narrow first
  paints never flash the wide toolbar
- Add regression tests: model=null/'auto' labels, toggle-close behavior,
  and initial narrow-viewport render

* feat(web): fold Pi into the generic composer model/effort value buttons

Pi sessions previously exposed model/effort twice: dedicated 'Pi model' /
'Pi thinking level' toolbar buttons (PiModelPanel/PiThinkingLevelPanel)
AND the settings sheet's generic Model/Effort sections. Consolidate so Pi
looks exactly like every other flavor:

- Pi now uses the generic model/effort value buttons; labels resolve from
  the provider-qualified piModels catalog (name -> modelId -> session id).
- The settings sheet's Model section already renders provider-grouped Pi
  rows and Effort renders Pi thinking levels, so the dedicated panels and
  their toolbar slots are deleted.
- Keep Pi's mid-turn control affordance (#1442): Pi turns hold
  thread.isDisabled for minutes, so Pi model/effort controls stay enabled
  while a turn is running (configurationControlsDisabled instead of
  controlsDisabled), including the sheet rows.
- Drop 'piModel'/'piThinking' toolbar layout items; persisted layouts
  normalize them away automatically.

Tests: pi value-button label/sheet tests, mid-turn model selection via the
unified sheet, toolbar layout defaults.

* fix(web): keep a session-settings trigger on narrow viewports and mid-turn Pi

Address the HAPI review bot's two Minor findings on the Pi consolidation:

- Narrow viewports collapse the model/effort value buttons into the
  settings sheet, so a persisted toolbar layout hiding the gear left no
  session-settings trigger at all. ComposerButtons now forces the gear
  back into the rendered layout on narrow viewports (wide layouts keep
  honoring the user's hidden choice).
- The Pi mid-turn live-control rule only reached the value buttons and
  sheet rows; with those buttons gone on narrow, the gear was still
  disabled by controlsDisabled for the whole (minutes-long) Pi turn.
  HappyComposer now passes settingsDisabled={modelEffortControlsDisabled}
  so the gear stays clickable mid-turn for Pi exactly like the buttons.

Tests: narrow + hidden-gear layout keeps Settings; narrow Pi mid-turn
gear stays enabled and opens the provider-grouped sheet.

* fix(web): address HAPI Bot Pi settings-sheet findings

Three Minor findings from the review bot on the unified Pi sheet:

- Provider-qualified selection: rows compared only modelId, so duplicate
  model IDs across providers all looked selected. Compare against
  piSelectedModel's provider+modelId when available.
- Thinking-level reset: the removed Pi panel toggled the current level
  back to null; the unified effort rows only submitted concrete values.
  Re-clicking the selected effort row now clears it for Pi.
- Memo staleness: the settings-sheet memo did not depend on
  modelEffortControlsDisabled, so a Pi disabled-state transition while
  the sheet was open left rows enabled from the prior render.

Tests: colliding model IDs highlight only the matching provider row,
re-clicking the selected effort row sends null, and a rerender with
active=false disables the open sheet's rows.

* fix(web): include piSelectedModel in settings-sheet memo deps

The overlays memo reads piSelectedModel for provider-qualified row
highlighting but only declared the derived selectedPiModel. When
piSelectedModel hydrates from absent to a qualifier that resolves to
the same catalog object, the memo is reused and duplicate model IDs
stay highlighted across providers. Add the raw prop to the dep array.

* fix(web): gate Pi model rows on catalog; reset drill-down via value button

Address the HAPI Bot review on the unified composer settings sheet:

- Pi no longer falls back to the generic synthesized modelOptions rows
  when its provider catalog is empty/loading. Selecting one of those
  would post a bare model id that runPi cannot resolve to a provider
  (first cached match or 409). The Model section now only renders for
  Pi when piModelGroups exists, and renders grouped rows exclusively.
- Closing the sheet through the model/effort value button now goes
  through handleSettingsToggle, so a Cursor variant drill-down resets
  to the base model list on reopen (previously only the gear and
  outside-click paths cleared it).

Tests: empty Pi catalog hides Model section; value-button close resets
Cursor drill-down.

* fix(web): hide Pi effort controls until the selected model resolves

Address the HAPI Bot review: with the catalog still loading/failed there
is no selectedPiModel to derive a capability map from, but the unified
effort control stayed enabled (mid-turn Pi controls are intentionally
live). Selecting a level would send set_thinking_level for a model that
may not support reasoning, and the RPC can be rejected after the sheet
closed. The old dedicated panel guarded this state.

showEffortSettings now requires a resolved, reasoning-capable Pi model;
the effort value button hides the same way. With an empty catalog Pi
exposes no settings trigger at all, matching the old control states.

Tests: unresolved Pi catalog mid-turn exposes no effort action.

* fix(web): hide the Pi model trigger until the catalog resolves

Address the HAPI Bot Minor finding: with an empty/loading Pi catalog the
model value button fell back to the bare session model id and rendered
an enabled trigger that opens no Model section. Show the button only
once the provider-qualified catalog entry resolves.
2026-08-16 22:43:10 +08:00
SSU-WEI HUANGandGitHub ca6a6f0939 fix(cursor): retry transient ACP connection errors (#1541)
* fix(cursor): retry transient ACP connection errors

* fix(cursor): keep retry classification conservative

* fix(cursor): avoid retrying completed tool effects

* fix(cursor): require terminal retry failures

* fix(cursor): track retry activity across extension events

* fix(cursor): honor permission abort before retry
2026-08-16 22:42:10 +08:00
AnanovoandGitHub 7909c46fff feat(search): support wildcard patterns across search fields (#1571)
* feat(search): add wildcard matching to search fields

* fix(search): harden wildcard matching and file globs

* fix(search): align file matching with shared wildcard semantics

* fix(search): bound file wildcard search in runner

* fix(search): normalize outline queries through shared matcher

* fix(web): remove duplicate markdown test context field
2026-08-16 22:41:24 +08:00
AnanovoandGitHub 8dc4a50fee fix(web): prevent replaying historical assistant output (#1572)
* fix(web): prevent replaying historical assistant output

* fix(web): handle history pagination during typing handoff

* fix(web): preserve streaming handoff across history windows

* fix(web): distinguish hydrated active runs from new output

* fix(web): preserve handoff across tail hydration

* fix(web): preserve first output in user-only turns
2026-08-16 22:41:08 +08:00
KorenKritaandGitHub bf6bae1924 fix(pi): settle autonomous agent lifecycles instead of swallowing them (#1563)
* fix(pi): settle autonomous agent lifecycles instead of swallowing them

When Pi starts an agent lifecycle on its own — a subagent completion
wake-up, scheduled work — no HAPI prompt is in flight, so the previous
prompt lifecycle has already delivered its settlement and
deliveredSettlement is still true. agent_start unconditionally set
thinking=true, but every settlement path (agent_settled delivery, the
legacy agent_end grace, the prompt-lifecycle fallback) was gated shut by
that stale flag, so the autonomous turn's completion was swallowed:
thinking stayed true forever, the FIFO pump stayed blocked
(piIsStreaming), new messages queued without ever being sent, and abort
waited on a settlement that could never arrive. The only escape was
killing the session.

Open a fresh settlement cycle when agent_start/turn_start arrives with
deliveredSettlement still true: reset deliveredSettlement,
agentEndObserved and activeAgentSettledSeen so the existing settlement
paths (direct agent_settled, legacy agent_end grace) apply to the
autonomous lifecycle. Prompt-driven lifecycles are unaffected because
beginPromptLifecycle has already reset the flag before their agent_start
arrives; mid-cycle retries are unaffected because their cycle has not
settled yet.

Evidence: hapi log 2026-08-13-18-39-10-pid-85758.log — 19:24:07
agent_start (no prompt accepted) → 19:25:40 agent_end + agent_settled
both swallowed → session stuck thinking=true for 27+ minutes until
killed.

* fix(pi): generation-scope the settled callback against autonomous lifecycle races

Review follow-up (HAPI Bot, Major): deliverSettlement() notifies
onAgentSettled only after an async conversationHistory.syncEntries().
An autonomous lifecycle can begin in that window; the stale finally
callback would then mark the new lifecycle's abort boundary as settled
before it emits agent_settled.

Capture lifecycleGeneration at settlement time and skip the
notification when it no longer matches, and advance the generation when
agent_start reopens a settlement cycle for an autonomous lifecycle so
the in-flight callback turns stale.

New regression test holds syncEntries open across the autonomous
agent_start and asserts the stale callback does not settle the new
boundary (fails without the fix).
2026-08-16 22:40:15 +08:00
SSU-WEI HUANGandGitHub 44aff5a924 fix(codex): cache codex model list to avoid spawning app-server per request (#1534)
* fix(codex): cache codex model list to avoid spawning app-server per request

listCodexModels() spawned a fresh `codex app-server` subprocess on every
request: exec a version probe, boot the app-server, validate the ChatGPT
session (token refresh over the network when needed), list models, then
kill the process. Fleet measurement showed 0.5-4.4s per call on healthy
machines and 33s (initialize timeout) on a machine with a slow OpenAI
network path, and the web refetches on every session open / dialog mount
(staleTime 30s).

Mirror the opencode model cache: cache successful non-empty lists for 5
minutes and coalesce concurrent requests into a single app-server spawn.
Failures and empty results are never cached, so a broken machine retries
on the next request.

Fixes #1533

* test(codex): cover TTL expiration of the model list cache
2026-08-16 22:39:57 +08:00
AnanovoandGitHub ec19699418 fix(web): preserve session sidebar scroll during navigation (#1552)
* fix(web): preserve session sidebar scroll during navigation

* fix(web): preserve nested session pane scroll

* fix(web): avoid repeated file scroll restoration

* fix(web): preserve scroll when leaving chat file previews

* fix(web): preserve scroll after session resolution
2026-08-16 22:39:07 +08:00
AnanovoandGitHub daa10ea2c8 fix(hub): retry Cursor legacy SQLite cleanup on Windows (#1561)
* fix(hub): retry Cursor SQLite cleanup on Windows

* test(hub): keep cleanup retry regression Windows-specific

* test(hub): exercise Windows cleanup retry in CI
2026-08-16 22:38:49 +08:00
Shawn TianandGitHub 5c81ed69e2 fix(runner) preserve native instructions (#1557)
* fix(codex): preserve native instructions

Keep HAPI guidance in the developer layer so app-server retains Codex persistence rules. Align long-running goal states with the Codex 0.147 protocol.

* fix(codex): filter new goal statuses
2026-08-16 22:38:15 +08:00
AnanovoandGitHub e32334fbb2 fix(web): prioritize latest tail on session re-entry (#1560)
* fix(web): prioritize latest tail on session re-entry

* fix(web): start latest sync immediately on re-entry
2026-08-16 22:37:08 +08:00
AnanovoandGitHub 767e40a71f fix(web): preserve current Fork action while reading history (#1573)
* fix(web): preserve current Fork action while reading history

* fix(web): invalidate stale Fork boundary during history updates

* fix(web): invalidate Fork boundary after message consumption
2026-08-16 22:36:37 +08:00
SSU-WEI HUANGandGitHub 30504252b9 feat(web): keep quiet active sessions in the pinned top section — Running / Active / Inactive tiers (#1590)
* feat(web): keep quiet active sessions in the pinned top section (#1589)

When pin-in-progress is on, any connected session now floats above the
project folders instead of dropping down the moment it finishes
executing. The sidebar reads as three tiers:

1. In progress - running and pending work (unchanged)
2. Active sessions - quiet but connected (finished executing, operator
   usually continues the conversation)
3. Inactive - everything else, in directory groups

Also remove the duplicate showSessionSummaryInChat fixture property in
markdown-a.test.tsx that fails web typecheck on upstream main.

* test(web): cover Active section collapse, keyboard toggle, and search expansion

* fix(web): keep action-only project headers when every row floated

A directory whose sessions all floated to the pinned sections lost its
header entirely, taking copy-path and new-session-in-directory with it.
Render an action-only header for fully-floated projects so the directory
actions stay available.
2026-08-16 22:36:20 +08:00
SSU-WEI HUANGandGitHub 72483229c7 fix(web): restore Ctrl+A select-all on the chat page (#1595)
* fix(web): restore Ctrl+A select-all on the chat page

Chromium's SelectAll collapses to an empty caret when the page contains
a contenteditable (the rich composer) but focus is outside it, so
Ctrl+A + Ctrl+C on the Happy page copied nothing. Take over Ctrl/Cmd+A
at window scope when focus is outside the composer/inputs and select
the message thread manually.

Also removes a duplicate property in markdown-a.test.tsx that broke
`bun typecheck` on upstream/main.

* fix(web): restrict select-all takeover to unshifted Ctrl/Cmd+A; wire e2e spec into CI

Address review findings:
- leave Ctrl+Shift+A to the browser (matches native Chromium, where
  the shift variant is unbound)
- run the composer-copy Chromium regression spec in CI alongside
  terminal-wrap-fidelity.spec.ts
- move the spec to the root e2e/ dir so the root playwright config
  picks it up
2026-08-16 22:35:53 +08:00
SSU-WEI HUANGandGitHub 1f2fbcb142 fix(web): map codex-enveloped compact-summary to the chat block (#1582)
* fix(web): map codex-enveloped compact-summary to the chat block

The merged #1570 renders Pi compaction summaries as a dedicated chat
block via the event envelope. A compact-summary arriving in the codex
payload envelope (older import paths, future producers) would still be
silently dropped by the codex-content filter; map it to the same
agent-event the live pi wrapper emits, mirroring the existing
context_compacted handling. Adds a normalizeAgent regression test and
tightens the /compact thinking-state test to assert the last keepAlive
flips true (RPC outstanding) then false (settled).

Verified: bun typecheck clean; web normalizeAgent 12/12, cli runPi
57/57 in an isolated TMPDIR.

* fix(web): remove duplicate showSessionSummaryInChat in markdown-a fixture

Regression from #1530: the fixture object literal sets the key twice,
which breaks `bun typecheck` on upstream/main (Test workflow failing on
push). One-line cleanup; no behavior change.

Verified: bun typecheck exit 0 across cli/web/hub.
2026-08-16 22:35:31 +08:00
901f17d0ca feat(pi): support Pi slash commands from HAPI web (compact/session/model/help) (#1570)
* feat(pi): support Pi slash commands from HAPI web (compact/session/model/help)

Pi runs as 'pi --mode rpc' over piped stdio, so TUI slash commands typed in
web chat previously fell through to the LLM as plain text and silently did
nothing (notably /compact).

- shared: add Pi builtin slash command list (help/compact/session/model) so
  the web / menu exposes them; web test updated to match
- cli: intercept Pi builtin commands in runPi's user-message path
  * /compact [instructions] -> Pi compact RPC (120s timeout, works while
    streaming; summary + token delta reported back as chat messages)
  * /session -> get_session_stats formatted stats
  * /model [modelId] -> list/switch via set_model
  * /help -> supported-commands list
  * other Pi TUI builtins (/tree, /export, /reload, ...) -> explicit
    terminal-only notice instead of silent LLM pass-through
  * unknown slash text still passes through (extension commands, skills,
    templates keep working)
- gate the prompt pump with piCompactInFlight so queued prompts are not
  rejected by Pi mid-compaction; buffer commands until ready like prompts
- ListSlashCommands RPC merges HAPI builtins with Pi extension commands
- tests: parser unit tests + runPi integration tests (compact execution,
  streaming steer interception, failure reporting, FIFO blocking, model
  switch, unsupported commands, slash list merge)
- docs: document Pi slash command support in docs/guide/agents.md

* fix(pi): address review findings on slash command lifecycle

- compact timeout: fail the session (indeterminate outcome, runtime lease
  poisoned) instead of reopening the prompt FIFO into a possibly-compacting
  Pi; pump only when cleanup has not been initiated
- special commands: release the cancellation reservation before executing so
  a cancel landing mid-command is not acknowledged (hub would delete the
  queued row while the command still runs)
- tests: drop the duplicated slash-command describe block; add focused tests
  for compaction timeout with a queued prompt and cancellation during an
  in-flight special command

* fix(pi): route slash commands through the prompt FIFO and reject ambiguous models

- slash commands now share the prompt FIFO with ordinary messages: a
  /compact or /model typed after a queued prompt dispatches only after it
  (and after the active turn settles), instead of jumping the queue from
  the preparation chain
- the pump dispatches special entries out-of-band while piSpecialCommandInFlight
  keeps the FIFO blocked; steer promotion refuses slash commands
- /model <id> prefers an exact provider/modelId match and reports bare IDs
  shared by multiple providers as ambiguous instead of picking the first
- tests: FIFO ordering (queued prompt before /compact), steer-delivered
  /compact queued until settle, ambiguous/qualified model selection

* fix(pi): keep /compact interruptible, honor extension precedence, require token boundary

- head-of-line /compact dispatches even while Pi is streaming (Pi's
  compact() aborts the active generation itself); every other queued item
  still waits for the stream to settle, preserving FIFO order
- discovered extension commands / prompt templates override same-name
  builtins at message time, matching the slash-list merge precedence
- parsePiSpecialCommand requires a command-token boundary, so path-like
  text such as /compact.md or /model/config stays an ordinary prompt
- tests: interrupt rule, extension collision, reserved-name path prefixes,
  non-compact commands waiting for stream settle

* fix(pi): honor cancellation acknowledged during slash-command discovery

A cancel arriving while the chain awaits get_commands (cold cache) was
acknowledged via the preparing reservation but never re-checked, so a
canceled /compact could still execute. Re-check the cancellation marker
after discovery and drop the message before dispatch.

* fix(pi): qualify /model selectors and report failed slash RPCs once

- /model lists provider-qualified selectors (openai/gpt-5.2) so duplicate
  bare IDs remain usable and copy-pasteable; current model is qualified too
- compact/set_model failures are owned by the awaited slash/config handlers:
  the common response handler no longer emits the raw Pi error a second time
- tests: qualified listing with duplicate providers, single-message failure
  reporting for rejected /compact and /model

* fix(pi): consume slash-command queue row at dispatch

Special commands (/compact, /session, /model, /help) are executed by HAPI
itself and never delivered to Pi as prompts. Consumption was deferred until
the command finished, so a /compact run — an LLM summarization pass that can
take minutes — left the row stuck in the web queued bar for its whole
duration, then surfaced as a sent message. Consume the row the moment
dispatch starts; failures still surface via the explicit event message.

* fix(pi): guard special-command dispatch against unexpected rejections

* ci: retry Codex PR Review after infra failure (proxy 503)

* fix(pi): keep session queued-thinking grace during /compact dispatch

The queued-thinking grace is session-scoped, so clearing it while
acknowledging a dispatch-time /compact row also drops the grace for any
prompt queued behind it. /compact keeps running for minutes without
toggling Pi thinking state, which would leave the web session looking idle
while compaction and the following prompt are still pending. Only the
fast, synchronous commands (/session, /model, /help) clear the grace.

* fix(pi): render compaction summary as a dedicated chat block

The manual /compact RPC result was reported as two plain message
events ("📦 Compaction completed (tokens: …)" + "📦 Compaction
summary: …"), which the web chat renders as tiny centered status
lines — unusable for a real summary payload. Emit a structured
compact-summary event instead (summary + token delta) and render
it as an independent block: header with the delta and the summary
markdown in a scrollable panel.

Also emit the same structured event when importing Pi session
files (compaction entries), and queue the event lossless like
other user-visible messages so a disconnect cannot drop it.

Verified: bun typecheck clean; bun run test exit 0 (cli 2481
passed, web 2451 passed, hub/shared clean); runPi/loop/apiSession/
piSessions/presentation suites green.

* fix(pi): address HAPI Bot findings on compact dispatch and import

- Track compaction as thinking for its whole duration: /compact runs for
  minutes without a Pi streaming event, so the 15s queued-thinking grace
  alone left the web session looking idle while compaction and any queued
  prompts were still pending (updateThinkingState around the compact RPC).
- Imported Pi compaction summaries must use the event envelope
  (content.type: 'event') like the live wrapper's compact RPC result; the
  codex payload envelope is dropped by the web normalizer. Extend
  CodexImportedMessageSchema with the event variant.

* fix(pi): /model retries discovery when the model cache is empty

Startup model discovery can be late or fail once; using only the cached
catalog made /model report valid models as unknown. getPiModels() falls
back to the get_available_models RPC on an empty cache, used for both
listing and switching.

* fix(pi): interrupt in-flight /compact on Abort; surface startup model rejection

- The Abort action no longer waits on the runtime-mutation lease when a
  manual /compact is in flight (compaction can hold it for up to 120s,
  blowing the 25s abort deadline and failing closed). It sends the abort
  RPC directly so Pi cancels its compaction AbortController; the compact
  RPC's 'Compaction cancelled' error is not double-reported as a failure
  since Pi already emits the compaction_end(aborted) lifecycle event.
- A rejected detached startup set_model now emits a visible ⚠️ event into
  chat instead of only a debug log, restoring the pre-existing behavior.

* fix(pi): close the Abort race when /compact is queued on the mutation lock

Abort previously assumed an in-flight /compact always had its RPC issued;
the command is marked active at queue dispatch, but the compact RPC is sent
only after the runtime-mutation lock is acquired. An Abort landing in that
gap acknowledged success while the compact RPC still ran afterwards.
Track the compact's rpcStarted/cancelled state: Abort cancels a not-yet-
started compact in place (the queued callback skips it), and interrupts a
started one via the abort RPC as before.

* fix(pi): persist provider-qualified selection after /model switch

The success path updated currentModel/currentProvider and keepalive with a
bare model ID, leaving metadata.piSelectedModel on the previous provider.
The web picker prefers that metadata for selection, context-window
resolution, and effort options, so a switch like openai/gpt-5.2 ->
azure/gpt-5.2 was invisible. Persist piSelectedModel with the full
provider/modelId pair on every confirmed switch.

* fix(pi): retire pending extension UI requests when /compact interrupts a turn

The streaming-interrupt path sent the compact RPC without cancelling
pending extension UI requests first, unlike the Abort path. Editor
requests have no timeout, so the web could stay stuck on a stale
input/permission card and a later answer could be routed to the aborted
turn. Cancel all pending requests (with a response) before compacting.

* fix(pi): fail closed when the direct compact-abort RPC times out

The in-flight /compact abort branch awaited the abort RPC without the
ordinary Abort path's timeout handling: an unanswered abort left the
compaction outcome indeterminate (the compact RPC keeps the mutation
lease for up to 120s) while the wrapper still looked live. Fail the
session on PiRpcTimeoutError, mirroring the standard abort fail-closed
path.

---------

Co-authored-by: swear01 <swear01@users.noreply.github.com>
2026-08-15 11:21:45 +08:00
Kong ZhiyangandGitHub 339607bf3e fix(web): keep single tildes literal in markdown (#1471) 2026-08-15 11:18:12 +08:00
AnanovoandGitHub c35b320a40 fix(web): normalize Windows file search query separators (#1490) 2026-08-15 11:17:58 +08:00
AnanovoandGitHub 386ee4121b fix(web): hide chat viewport focus outline after Home/End (#1495)
* fix(web): hide chat scroll focus outline

* fix(web): keep chat viewport keyboard focus visible

* test(web): cover chat viewport keyboard focus
2026-08-15 11:17:43 +08:00
AnanovoandGitHub 0e60697895 fix(web): prevent Rewind crashes when message history resets (#1530) 2026-08-15 11:17:25 +08:00
AnanovoandGitHub e83dbdd40a fix(web): add clear action to collapsed session search (#1544)
* fix(web): add clear action to collapsed session search

* fix(web): improve light theme clear icon contrast
2026-08-15 11:17:10 +08:00
AnanovoandGitHub 44703d0153 fix(web): localize Fork and Rewind labels in Simplified Chinese (#1546)
* fix(web): localize Fork and Rewind labels in Chinese

* test(web): cover localized history confirmation dialogs
2026-08-15 11:16:59 +08:00
AnanovoandGitHub f803967e5d fix(web): stabilize autocomplete dropdown layout (#1547) 2026-08-15 11:16:48 +08:00
AnanovoandGitHub b2ae6b90a3 fix(web): keep share actions visible during generation (#1550)
* fix(web): keep share actions visible during generation

* test(web): cover sharing during generation
2026-08-15 11:16:32 +08:00
SSU-WEI HUANGandGitHub a794be0811 fix(web): recover silently-dead SSE connections quickly, stop banner noise (#1562)
A suspended mobile tab can lose its SSE connection without the browser
ever noticing (no FIN/RST arrives). On resume the client used to wait
for the 90s heartbeat watchdog or lean on the native EventSource retry,
which can hang on a dead pooled socket - leaving a persistent
"Reconnecting... (stream error)" banner while the UI keeps working.

- useSSE: on visibility resume, distrust the connection after one missed
  heartbeat interval (45s) instead of 90s
- useSSE: abandon connection attempts that don't open within 10s (hung
  on a dead pooled socket after resume) instead of waiting for the
  watchdog; force bypasses the one-shot reconnect guard for the new
  attempt cycle
- useSSE: first reconnect attempt is immediate; exponential backoff
  starts from the second attempt
- ReconnectingBanner: localize connect-timeout and transport-error
  reasons (transport-error is already emitted by current code but fell
  through unlabeled)

Fixes #1559. Reimplements the reconnect half of #989 (closed unmerged)
on top of the current scheduler (hidden-tab deferral, slow backoff,
replay cursor).
2026-08-15 11:16:19 +08:00
AnanovoandGitHub 235d6dd6fd fix(web): clarify displayed media labels (#1565) 2026-08-15 11:15:42 +08:00
AnanovoandGitHub effc505a07 fix(mcp): clarify display_image user-output semantics (#1568) 2026-08-15 11:15:27 +08:00
AnanovoandGitHub 7be4babfbd fix(web): shorten Codex sync label and size action menu to content (#1569)
* fix(web): refine Codex sync action menu

* test(web): cover action menu trigger anchoring
2026-08-15 11:14:45 +08:00
AnanovoandGitHub e314522982 fix(web): lower context warning thresholds (#1576) 2026-08-15 11:14:31 +08:00
AnanovoandGitHub e0354b09c0 fix(web): align new-session and settings controls (#1578) 2026-08-15 11:14:12 +08:00
AnanovoandGitHub ad72229923 feat(sessions): add on-demand AI title suggestions (#1577)
* feat(sessions): add on-demand AI title suggestions

* fix(sessions): address title suggestion review feedback

* fix(web): ignore stale title generation results
2026-08-15 11:13:49 +08:00
AnanovoandGitHub febbf8ff58 fix(web): improve session search UI (#1545)
* fix(web): improve session search UI

* fix(web): reserve space for search clear control
2026-08-13 10:14:55 +08:00