mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
3290bc9ca9422602540552f726cbbcee88047721
885
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3290bc9ca9 |
feat(pi): add 'max' thinking level (#1032)
* feat(pi): add 'max' thinking level Pi's --thinking flag accepts 7 levels: off, minimal, low, medium, high, xhigh, max. The shared constant and UI only exposed 6 levels (missing max). Add 'max' to PI_THINKING_LEVELS and PI_THINKING_LEVEL_LABELS. Like xhigh, max requires explicit opt-in via the model's thinkingLevelMap — models that support it will include max in their map and the UI will show it accordingly. * fix(pi): close max thinking-level branch |
||
|
|
53406b2e97 |
fix(web): sync browser tab title with session (#1034)
* test: reproduce issue #712 * fix: sync browser title with session (closes #712) |
||
|
|
553b3492f1 |
fix(codex): wait for manual compaction to finish (#1038)
* test: reproduce issue #982 * fix: wait for Codex manual compaction (closes #982) * test: cover Codex compaction turn completion |
||
|
|
c87720ab4d |
fix(cli): load extra headers from settings (#1041)
* test: reproduce issue #786 * fix: load extra headers from settings (closes #786) * test: cover extra header precedence and redaction * fix: redact persisted extra headers in diagnostics * test: cover runner extra header identity * fix: restart runner when extra headers change |
||
|
|
e87e875d72 |
feat(opencode): accept -s/--session as resume aliases (#1042)
Map OpenCode-native session flags to resumeSessionId so `hapi opencode -s <id>` restores the session like --resume. |
||
|
|
87f5c78ab2 |
fix(codex): scan transcripts incrementally (#1031)
Co-authored-by: zj1123581321 <zj1123581321@users.noreply.github.com> |
||
|
|
1c834607a2 | Release version 0.22.3 | ||
|
|
8ee04500b9 |
fix(hub,cli): coerce null session activeAt so resume cannot 500 (#1026)
Legacy rows and inserts left sessions.active_at NULL while SessionSchema required a number, so CLI GET /cli/sessions/:id failed Zod and resume surfaced HTTP 500. Persist active_at on insert, harden hub read coerce, and nullish-transform activeAt in SessionSchema (output stays number). Fixes #1025 Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f6ad345339 | feat(web): redesign responsive settings navigation (#1027) | ||
|
|
b9eed7c071 |
feat: add Grok Build support (#1030)
* test: define Grok Build integration behavior * feat: add Grok Build agent integration * test: cover Grok permissions and resume paths * docs: add Grok Build setup guide * fix: scope Grok ACP discovery to session cwd * fix: align Grok permission UI semantics * docs: clarify Grok runner setup * test: require Grok create model and effort options * feat: add Grok create model and effort pickers * test: define Grok runtime parity behavior * feat: add Grok runtime ACP controls and discovery * fix: tighten Grok runtime controls * fix: suppress nonfatal Grok title quota errors * feat: support Grok Auto permission mode * feat: forward ACP native session titles for Grok * fix: guard Grok Windows shell arguments |
||
|
|
de07643828 |
fix(codex): improve local session import compatibility (#995)
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp> |
||
|
|
d97b270ba8 |
fix(codex): bridge MCP elicitation through user input (#1008)
* fix(codex): bridge MCP elicitation through user input * fix(codex): allow MCP elicitation in yolo mode * fix(codex): preserve MCP form semantics * fix(codex): accept implicit MCP form mode * fix(codex): harden MCP elicitation prompts * fix(codex): require valid MCP choice answers * fix(codex): round-trip MCP array elicitation * fix(web): require explicit MCP URL confirmation * fix(codex): preserve MCP array item types * fix(codex): support multi-select MCP elicitation * fix(codex): allow MCP elicitation in read-only mode * fix(codex): route MCP tool approvals through permissions |
||
|
|
474db94136 | test(cli): stabilize runner integration timeouts (#1011) | ||
|
|
73584e925a |
feat(cursor): multitask slash, autoReview mode, native worktree/add-dir (#1014)
* feat(cursor): multitask slash, autoReview mode, native worktree/add-dir Close the highest-value Cursor Agent gaps for remote HAPI: expand ACP-safe slash pass-through (/multitask, worktree, add-dir, …), add autoReview permission mode (--auto-review spawn + mid-session slash), and route Cursor New Session worktrees through agent --worktree instead of HAPI sibling trees. Fixes #1013 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): accept --mode autoReview for hapi cursor Align --mode parsing with CURSOR_PERMISSION_MODES so documented `hapi cursor --mode autoReview` enables Smart Auto instead of silently falling back to default. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
b104c38f3a |
fix(web): show Codex reasoning effort in SessionHeader (#1016)
Surface the same reasoning label already shown in the composer StatusBar in the top SessionHeader for codex/opencode sessions. Also show an explicit Fast badge only when serviceTier is fast (#1004-aligned). Closes #1015 (header display portion). Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
d0c8391261 | fix(opencode): preserve ACP text deltas (#1023) | ||
|
|
d9ea9db507 |
fix(codex): preserve native safety behavior (#1024)
* test: reproduce issue #1020 * fix: preserve Codex safety behavior (closes #1020) * test: cover Codex safety buffering dismissal * test: cover Codex safety retry edge cases * fix: keep dismissed safety buffering prompts hidden |
||
|
|
d160203bb2 |
fix(codex): support dynamic reasoning efforts (#1012)
* fix(codex): support model-reported reasoning efforts * fix(web): prevent service worker edge caching * ci: retrigger stuck Actions run * fix(codex): accept dynamic reasoning effort values * fix(web): restore reasoning effort on model switch failure |
||
|
|
4c76668a6c | refine message actions and metadata | ||
|
|
8782b8a110 |
fix(codex): align local transcript message projection
Use semantic events as the visible text source and preserve completed plans as ordered plan proposal cards. |
||
|
|
942a1dfff8 | Release version 0.21.0 | ||
|
|
5a377e38b9 | fix(codex): defer session persistence until user activity | ||
|
|
65e1708c78 |
feat(web): mermaid diagram lightbox on click (#741)
* feat(web): mermaid diagram lightbox on click Click rendered mermaid blocks in chat to open a zoomable full-screen viewer. Re-renders from source in the modal with the current theme. Closes #737. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to viewport on open Auto-scale diagrams to fill the viewer instead of opening at intrinsic mermaid size. Reset returns to fit; zoom label is relative to fit (100%). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to device screen not inner panel Use visualViewport for fit scale, full-screen pan layer, and a floating toolbar so the diagram can use the whole display. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): show mermaid lightbox by reusing inline SVG Second mermaid.render on open often left a 0×0 SVG while fit scale was computed from the loading placeholder. Reuse the inline SVG in the modal and measure viewBox with retried fit-to-screen. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): uniquify mermaid SVG ids in lightbox clone Inlining the same mermaid markup twice duplicates element ids and breaks url(#ref) resolution in the modal copy. Prefix ids and hrefs for lightbox only. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): give mermaid lightbox SVG explicit dimensions Mermaid emits width="100%" with max-width in px; that collapses to 0×0 inside the centered lightbox layer. Derive width/height from viewBox for the uniquified lightbox clone. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): render mermaid lightbox via isolated SVG data URL String id rewrites broke mermaid's embedded CSS so only labels appeared zoomed. Rasterize the inline SVG to a data-URL img instead of duplicating markup in the DOM. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): lightbox re-renders SVG for sequence diagrams Data-URL images drop or blank some mermaid diagram types (sequence). Re-render with a modal-specific id into inline SVG on a code-bg panel, and add sequence theme variables for dark/light. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): mermaid lightbox uses inline SVG in shadow DOM Reuse the inline render in an isolated shadow root so sequence CSS stays intact, and fit the viewport from viewBox dimensions instead of the loading placeholder or width="100%" layout. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright lightbox coverage per mermaid diagram type Add e2e harness and a script that opens the lightbox for each diagram kind (flowchart through kanban). Fit uses inline getBBox() so compact charts like gitGraph fill the viewport. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): bounded Playwright via webServer, fix gantt fit sizing Playwright owns Vite lifecycle (no agent-spawned dev server). Fit uses viewBox unless viewBox padding is excessive (gitGraph); wide charts use width-based coverage in e2e. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): gitignore Playwright test-results Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): address PR 741 bot feedback (typecheck, fit floor, gitignore) Guard lightbox open when svg is null; allow fit scale down to 0.01 while keeping 0.25 minimum for manual zoom; ignore Playwright test-results/ correctly. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright asserts click expands diagram vs inline Measure inline vs lightbox bounding box after click; require visible growth (area ratio or max dimension) plus dialog + shadow SVG content. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright against live HAPI session for mermaid lightbox Add seed script for a dedicated chat session, live hub Playwright suite (HAPI_LIVE=1), and dogfood doc. Live tests fail until driver serves shadow-DOM lightbox (catches gray-box regression on stale bundles). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): undo wrapper transform in lightbox fit; carry fit floor in zoom Resolves PR #741 review threads (HAPI Bot Major): 1. measureSvgIntrinsicSize / measureContentSize prefer intrinsic dimensions (viewBox -> width/height attrs -> img.naturalSize) before getBoundingClientRect. When the rect is the only signal, divide by scaleRef.current so the 50/200ms refit retries stop compounding with the wrapper's scale(...) transform. Large diagrams no longer jump tiny or oversize after async render completes. 2. Interactive zoom (wheel/keys/buttons/pinch) now clamps with Math.min(MIN_SCALE, baseScaleRef.current). A diagram fitted below the normal 25% floor stays reachable instead of snapping back to 25% and clipping. Zoom-out button disabled threshold uses the same min. 3. Add Vitest coverage for both helpers (intrinsic precedence, scale-aware rect fallback, divide-by-zero guard) so regressions surface without needing the full Playwright stack. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): mermaid seed refuses to wipe non-fixture sessions HAPI Bot Major (PR #741): SESSION_ID is documented as overridable, and the script unconditionally deletes every message for the target session before seeding fixtures. If pointed at a real session id, that's silent data loss. Refuse to proceed when an existing session id has a tag other than 'mermaid-lightbox-e2e'. New ids and the canonical fixture session still seed normally; real sessions throw before any DELETE runs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): normalize mermaid svg for lightbox shadow root Mermaid emits width="100%" on every diagram. Inside a shadow root whose host has no explicit size, that collapses to zero in Chromium for most diagram types - only ones that ship pixel attrs (e.g. journey) happen to render. Operator confirmed on the live driver: every diagram except journey opened to a grey rounded square. MermaidLightboxSvg now runs normalizeMermaidSvgForStandaloneDisplay before injecting (strips width/height="100%", bakes viewBox dims as pixels) and sets :host{display:inline-block} so the host sizes to the SVG. Inline svg in chat is unchanged - only the lightbox copy is normalized. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep mermaid lightbox content below the toolbar Operator screenshot showed the diagram top (e.g. pie 'Pets' title) clipped behind the toolbar bar. Two causes: 1. getScreenFitSize used the full viewport height, so the fit scale sized the diagram to fill an area the toolbar overlapped. 2. The viewport (drag/zoom area) was inset-0; content centered on the full viewport center, not the visible region's center, pushing the top behind the toolbar. Measure the toolbar with a ResizeObserver, subtract its height from the fit calculation (clamped at zero), and start the viewport region below the toolbar (top: toolbarHeight). Fit scale recomputes whenever toolbar height changes. Adds Vitest coverage for getScreenFitSize reserved-top math. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): guard ResizeObserver before constructing it HAPI Bot Major (PR #741): Vitest jsdom does not polyfill ResizeObserver, so the toolbar measure effect throws ReferenceError when the existing mermaid-diagram React tests open the lightbox. Same code path is also brittle in any browser/webview without the API. Fall back to plain window 'resize' listener when ResizeObserver is absent. Toolbar height won't auto-update on element resize without it, but the lightbox still renders and the resize listener catches the common viewport-rotation case. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): live mermaid playwright wrapper runs from repo root HAPI Bot Minor (PR #741): the wrapper sets cwd to scripts/, but the test:mermaid-lightbox:live npm script lives in the repo-root package.json, so spawning npm there exited before Playwright started. Switch cwd to the repo root and drop the unused WEB_DIR constant. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): accept signed viewBox values in mermaid lightbox normalize HAPI Bot Minor (PR #741): the viewBox regex only matched digits, dots, and spaces, so a valid viewBox with negative origin (e.g. '-8 -8 640 480') returned null. normalizeMermaidSvgForStandaloneDisplay then became a no-op and left width='100%', re-introducing the zero-sized lightbox render this PR is meant to fix for the affected diagrams. Switch to the bot's suggested regex (signed numbers, single or double quotes, comma or space separators) and reject NaN parts. Adds Vitest coverage for signed origins, single quotes, comma separators, the malformed/no-viewBox null paths, and an end-to-end normalize test that fails against the old regex. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align @playwright/test on 1.60.0 across workspaces HAPI Bot Major (PR #741): web/package.json pinned @playwright/test at 1.49.1 while the root workspace and bun.lock were on 1.60.0. The mismatch surfaced after rebasing onto upstream/main, where the root had already moved to 1.60.0 while my web devDependency lagged from an older commit. A frozen install would reject the lockfile and the new web e2e script could resolve a different Playwright than root scripts. Bump the web devDependency to 1.60.0 and regenerate bun.lock so all workspaces share one Playwright version. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): move mermaid playwright fixtures out of public HAPI Bot Minor (PR #741): the e2e and smoke fixtures lived under web/public, so Vite copied them verbatim into web/dist and the hub asset generator embedded them in production bundles. Both pages import Vite dev-only paths (/@react-refresh and /src/dev/...), so the production /mermaid-lightbox-{e2e,smoke}.html routes would 404 on those imports. Move both fixtures to web/e2e-fixtures/ to match the existing scratchlist-fixture pattern (relative ../src/dev import, served by Vite at /e2e-fixtures/...) and update the Playwright spec to hit the new path. Build now ships 112 PWA precache entries instead of 114 (both fixtures excluded from dist). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
a82dd49049 |
feat(web): markdown Source | Preview toggle in session file pane (#957)
* feat(web): markdown Source | Preview toggle in session file pane Add Source | Preview toggle for .md/.mdx files in the session file route, defaulting to preview with localStorage persistence. Reuse chat markdown pipeline via MarkdownRenderer standalone mode (no assistant-ui thread). Includes unit tests, Playwright smoke, and e2e fixture. Closes tiann/hapi#954 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): cast standalone MarkdownRenderer components for react-markdown Soup verify gate: defaultComponents merge type is wider than react-markdown Components; standalone file-pane path needs explicit cast. * fix(web): route file-pane markdown fences through SyntaxHighlighter Standalone file preview now mirrors chat code-block rendering: fenced blocks use SyntaxHighlighter and MARKDOWN_COMPONENTS_BY_LANGUAGE (mermaid included) without requiring ThreadPrimitive context. Addresses HAPI Bot Major on tiann/hapi#957. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): detect fenced vs inline code in standalone markdown preview Move block detection to the pre override (react-markdown v10 does not pass inline to custom code components). Add inline-code regression test. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
018bcb8eaa | fix(web): clean up React tests globally (#993) | ||
|
|
58cfc330f9 | fix(web): show standard for untouched Codex service tier (#1010) | ||
|
|
a2465c782b |
[codex] fix Qwen realtime compatibility (#977)
* fix: improve Qwen realtime compatibility * fix: preserve Qwen endpoint query parameters |
||
|
|
afdcd92fc6 | fix: normalize Windows drive roots (#979) | ||
|
|
43e7b6bef7 |
fix(cli): compute macOS machine-health memory from vm_stat (#990)
* fix(cli): add darwin vm_stat memory percent parser Add readDarwinMemoryUsedPercent, a pure parser that computes macOS used memory as App Memory + Wired + Compressed (anonymous + wired-down + occupied-by-compressor pages), matching Activity Monitor's "Memory Used" figure. Page size is parsed from the vm_stat header rather than hardcoded, since it differs between Apple Silicon (16KB) and Intel (4KB) Macs. Not wired up yet; covered by unit tests, including a verbatim vm_stat capture from a 16GB Mac mini where the pre-fix total - freemem() path reported 99% (counting reclaimable cache as used) while App+Wired+ Compressed is 79% — the number a user sees in Activity Monitor. * fix(cli): wire darwin memory percent into computeMemoryPercent Add a platform() === 'darwin' branch that shells out to vm_stat (1s timeout, guarded by try/catch) and feeds its output to readDarwinMemoryUsedPercent. On any failure or undefined result it falls through to the existing total - freemem() fallback, matching the Linux branch's structure. This fixes the Machine capacity tooltip showing a stuck ~99% "High pressure" warning on macOS runners: os.freemem() there counts reclaimable file cache as used, so it reports near-total usage. Summing only App Memory + Wired + Compressed reports the same figure Activity Monitor shows. |
||
|
|
e45fde51e9 |
fix(claude): stop 1M/200k context-window flicker in the status bar (#992)
* refactor(claude): thread session's selected model into SDKToLogConverter
Adds an optional selectedModel field to the converter's context, wired
from session.getModel() in the launcher, so a later commit can seed the
turn-1 contextWindow estimate for presets whose system/init model
arrives without the "[1m]" suffix. No behavior change yet.
* fix(claude): key contextWindow cache by model to stop 1M/200k flicker
The remote launcher re-emits system/init on every turn for the same
converter instance. Its init-time estimate only checked whether the
model string ended in "[1m]", but current claude CLI versions strip
that suffix from system/init for some 1M presets (fable[1m] arrives as
"claude-fable-5"), so the estimate guessed 200k for them. The one
authoritative value is result.modelUsage[<model>].contextWindow, which
arrives after the heuristic has already injected 200k into that turn's
assistant message and then gets clobbered back to 200k by the very
next turn's init - producing the observed 200k<->1M oscillation in the
web status bar.
Cache the authoritative contextWindow per model id instead of a single
session-wide number, and only let system/init seed a heuristic guess
for a model that has no cached value yet, so a same-model re-init no
longer downgrades an already-learned value.
Two observed facts about the CLI's model ids drive the design:
system/init.model and the result.modelUsage keys always agree with
each other within a session (both bare for plain/fable[1m], both
suffixed for opus[1m]/sonnet[1m]), while each per-turn assistant
message reports its model bare and thus can't distinguish a 200k plain
preset from its 1M "[1m]" variant on tiers where they share a base id.
So the cache is keyed on the raw id (init/result agree, no
normalization) and assistant injection looks the value up via
resolvedModel (the last init id) rather than the lossy message.model.
Keying raw keeps a plain preset and its [1m] variant on distinct
entries; looking up via resolvedModel also means sidechain (Task
subagent) messages carry the main session window rather than the
subagent's own, since the web status bar picks the most recent usage
message without filtering sidechains and would otherwise flicker to
the subagent's smaller window while it runs.
For presets whose init model arrives bare even though they are 1M
(fable[1m]), the originally-selected preset - which preserves the
"[1m]" suffix - seeds the turn-1 estimate, kept live across mid-session
model switches via updateSelectedModel() (called from the launcher on
every turn) so it never goes stale.
* fix(web): recognize [1m] suffix on full Claude model ids in budget fallback
getContextBudgetTokens already special-cased "[1m]" for short preset
values (e.g. "opus[1m]") but fell through to the default 200k budget
for full model ids (e.g. "claude-opus-4-8[1m]"), which is what the CLI
now reports once context_window isn't available and this fallback is
consulted. Check the suffix on that branch too so it stays a correct
last-resort even without a session-provided context_window.
* refactor(web): merge duplicate Claude context-budget branches
isClaudeModelPreset(trimmedModel) and the startsWith('claude-') branch
below it had become byte-for-byte identical bodies after the [1m]
suffix check was added to both. Merge them into one condition; no
behavior change.
* fix(claude): distinguish fable from fable[1m] when the CLI reports both bare
The per-model contextWindow cache keyed on the raw system/init model id,
on the assumption that a 1M preset and its plain form always land on
distinct ids. That holds for opus[1m]/sonnet[1m] (the CLI reports the
"[1m]" suffix on their init and result ids) but not for fable: the CLI
reports both "fable" and "fable[1m]" with the same bare id
"claude-fable-5". So the "seed only if not already cached" guard would
skip re-seeding when switching fable[1m] -> fable, leaving the stale 1M
in place until fable's own result arrived - the same switch flicker this
change set out to remove, just for fable specifically.
Fold the selected preset's "[1m]" back into the cache key
(computeContextWindowKey): when the init model arrives bare but the
session selected an "[1m]" preset, key the entry as "<id>[1m]" so the 1M
and plain variants stay distinct; ids the CLI already suffixed are left
as-is. Seeding, lookups, and the current-model result entry all use this
resolved key. Subagent result entries (e.g. haiku) keep their own raw id
so the session's "[1m]" is never folded onto a model that isn't the
selected one.
|
||
|
|
b44885ae67 |
feat(gemini): remove launchable Gemini CLI agent, keep old sessions readable (#953)
* feat(gemini): remove launchable Gemini CLI agent, keep sessions readable Google sunset the consumer Gemini CLI (Pro/Ultra/free tiers stopped serving requests 2026-06-18). This removes the ability to launch/create Gemini CLI sessions while keeping existing stored Gemini sessions fully readable in the web UI. Removed (no longer launchable): - cli/src/gemini/ runtime (runGemini, loop, local/remote launchers, session, ACP backend, config, scanner) + GeminiDisplay ink view - `hapi gemini` command + registry entry + usage line - runner spawn branch & buildCliArgs mapping now reject gemini with a clear error; resume dispatch throws a clear "no longer supported" error - gemini dropped from the new-session agent selector via new CREATABLE_AGENT_FLAVORS, and from preferred-agent defaults Kept (read path — existing sessions still validate, load, render): - `gemini` in AGENT_FLAVORS / AgentFlavorSchema, FLAVOR_CAPS / FLAVOR_LABELS - AgentFlavorIcon badge, model-option labels, ACP message normalization, metadata.geminiSessionId, hub session dedup/resume-id Note: the Gemini *Live voice* backend is a separate feature and is untouched. Adds read-guarantee tests (stored gemini validates; excluded from creatable). typecheck + full suite green. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): reject gemini resume before handoff (#953 review) HAPI Bot [Major]: `hapi resume <active-gemini-session>` called handoffSessionToLocal() — which tells the running remote agent to exit — before reaching the gemini-unsupported throw in dispatchLocalResume, so it could stop the live/readable session and then fail locally. Move the gemini guard into resumeCommand.run before the handoff, so an active Gemini session is left running/readable instead of being stopped. Keep the dispatch-layer guard as defense-in-depth. Adds a regression test asserting handoffSessionToLocal is not called for an active gemini target. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): harden against stale gemini input (#953 review) Two [Minor] follow-ups from HAPI Bot: - newSessionFormDraft: coerce a restored browse draft's agent to a creatable flavor, so a pre-removal 'gemini' draft cannot submit agent:'gemini' even though the selector no longer offers it. - buildCliArgs: reject 'gemini' explicitly instead of silently falling through to the 'claude' command if the exported helper is reused outside the guarded spawnSession path. Updated the buildCliArgs precedence test to a creatable agent and added a test asserting buildCliArgs('gemini') throws. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): reset dependent draft fields when coercing stale agent (#953 review) Follow-up [Minor]: coercing a stale gemini draft's agent to claude left model/base/effort untouched, so a { agent:'gemini', model:'gemini-2.5-pro' } draft restored as claude *with* a Gemini model, which handleCreate() then sent to the runner. Now reset model / cursorSelectedBase / effort / modelReasoningEffort to defaults whenever the agent is coerced. Adds a regression test. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): tombstone `hapi gemini` so it errors clearly (#953 review) HAPI Bot [Major]: after removing geminiCommand from the registry, resolveCommand() treats `gemini` as an unknown subcommand and falls through to the default Claude command (forwarding "gemini" as an arg), so `hapi gemini` silently started Claude instead of reporting the sunset. Add an explicit tombstone `gemini` command that prints the sunset error and exits 1. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(web): assert AgentSelector hides the sunset Gemini agent (#953) Render regression test confirming the new-session AgentSelector offers exactly CREATABLE_AGENT_FLAVORS and never shows a Gemini radio. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
26a24bb6ce |
feat(web,hub,cli): show machine health in session sidebar (#962)
* feat(web,hub,cli): show machine load in session sidebar Runners attach OS health snapshots to machine-alive heartbeats; the hub caches them and the web session list renders load or CPU between the machine label and session count. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web,cli): show CPU and RAM pressure in machine health badge Sidebar label now combines CPU and RAM percentages for overload signaling; load stays in the tooltip on Unix. Prime CPU sampling so the first heartbeat includes usage, not just memory. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): visual machine health meters with tooltip Replace bare CPU/RAM text with labeled mini bar gauges, chip border tint by severity, and a HoverTooltip explaining capacity and overload guidance. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): widen machine health tooltip with horizontal layout Allow a generous popover width and lay CPU/RAM/load out side by side so the capacity tooltip reads wider and less tall than the chip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): anchor machine health tooltip to row left edge Wide tooltip was align=end on the chip, so it grew left off-screen. Use row-span positioning on the machine tile button instead. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): machine host card with OS label and inline health Turn the session sidebar machine row into a bordered host panel with OS metadata and side-by-side CPU/RAM meters embedded in the tile instead of a flat label line matching project rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep machine host tile single-row height Collapse the machine header back to one py-1.5 row with OS and compact inline health beside the name, and restore the original project indent without the extra nested rail or second header line. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): show CPU core count in machine health tooltip When the runner reports cpuCount, the tooltip reads "CPU across all 6 cores" instead of the generic all-cores label. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: add machine health sidebar screenshots Dogfood captures for the session sidebar machine tile and capacity tooltip, for upstream PR review. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): clear machine-alive priming timeout on disconnect Track the 50ms CPU priming setTimeout and clear it in stopKeepAlive so disconnect/shutdown during the delay cannot leave a stray interval alive. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop dogfood screenshots from upstream PR diff Review evidence lives in the PR discussion only; no need to ship PNGs in the repo long-term. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): truncate long machine OS/host metadata in sidebar row Bound the metadata span so a long hostname cannot push the health chip or session count off-screen in narrow sidebars. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): reveal machine health tooltip on keyboard row focus Wire MACHINE_ROW_TOOLTIP_FOCUS_CLASS and aria-describedby on the machine header button so keyboard users can read the health tooltip like session rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): use MemAvailable for Linux RAM pressure on Bun Bun's os.freemem() reflects MemFree (~1% on cache-heavy hosts), which made sidebar RAM read ~99% while btop showed ~40% used. Parse /proc/meminfo MemAvailable instead so used percent matches operator tools. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web,cli): show machine uptime in sidebar tiles and tooltip Collect os.uptime() as uptimeSeconds on keepalive and render compact up 1h 54m in the machine meta row plus an Uptime line in the health tooltip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): anchor machine health tooltip to chip not row align=row positioned the tooltip below the full machine header button, so the collapsible project panel painted over it on hover. Use align=end with a min-width panel so mouse and keyboard tooltips stay visible. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
5ade952218 |
fix(cursor): support ACP parameterized model picker (#969)
* test: reproduce issue #968 * fix: support Cursor parameterized model picker (closes #968) |
||
|
|
8493ec92f4 |
fix(cli): Pi RPC parsers compatible with Zod 4 optional fields (#973)
asStrOrDef/asOpt* helpers now treat missing keys as undefined so get_available_models/get_commands parsing works again. safeParse success checked explicitly. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f51e06e8f3 |
fix(web): stop pinning resolved permission cards to the bottom of the chat (#974)
agentState keeps an answered permission request in completedRequests. When its tool_use message is not in the loaded window, the permission-only synthesis appended a card to the end of the timeline — and there is no chronological re-sort, so the card stays pinned above the composer as a stale "answered" card that never moves to its place in history. With several answered asks this piles up at the bottom of the chat. Synthesize a card only for a *pending* request (the case that needs an answerable card when its message hasn't loaded). A resolved request is history and renders only via its own message when that message is in the window. |
||
|
|
2ab3b39887 |
fix(hub,cli): four hub-restart-cascade cleanup bugs (#913 #914 #916 #919) (#923)
* fix(hub,cli): four hub-restart-cascade cleanup bugs (#913 #914 #916 #919) These four contained bugs were uncovered by a 2026-06-15 hub-restart incident where `hapi-restart-hub` SIGTERMed 23 cursor ACP sessions. Each fix lands independently of the architectural #915 (hub-restart cascade-archive) and the hypothesis-pending #917 (reopen creates dead session); audit-trail correctness and idempotency wins stand on their own. Fresh ACP sessions could be SIGTERMed during the async `update-metadata` ACK round-trip, stranding the on-disk ACP store with no DB handle. Add `ApiSessionClient.flushMetadata()` and await it after `onSessionFoundWithProtocol` on the fresh-session branch. Resume-path pre-registration (PR #834) is unchanged. Hub-restart-cascade SIGTERMs went through the same path as web-UI Archive clicks, both writing archiveReason='User terminated'. New default is 'Hub restart'; the KillSession RPC handler (the authoritative user-archive signal) now explicitly stamps 'User terminated' before cleanupAndExit. SIGINT (local-terminal Ctrl-C) keeps the 'User terminated' label too. `rpcGateway.killSession` threw a generic Error when no target socket was registered, and the archive route surfaced that as 500. Add typed `RpcTargetMissingError`, narrow on it in `syncEngine.archiveSession`, fall back to a hub-side `markSessionArchivedFromHub` write so lifecycleState still flips to 'archived'. Drop the requireActive guard on the route and 2xx-noop for already-archived rows. without refresh, producing forever-409 on rename/reopen until an unrelated event triggered a cache refresh. `renameSession`, `clearSessionArchiveMetadata`, `restoreSessionArchiveMetadata` now retry-with-refresh (5 attempts, then throw) mirroring the existing good pattern in `mergeSessions`. Refs tiann/hapi#913 Refs tiann/hapi#914 Refs tiann/hapi#916 Refs tiann/hapi#919 AI disclosure: implementation by Claude Sonnet 4.5 (Cursor agent peer) under operator supervision. Issue triage by a sibling discovery agent. Per CONTRIBUTING.md AI-assisted contributions policy. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): runner-spawned children use 'Stopped by runner' as default archive reason Addresses bot review of #923: with the #914 default-archiveReason flip to 'Hub restart', runner-driven SIGTERM paths (`hapi runner stop-session`, webhook-timeout cleanup at run.ts:587, orphan-cleanup at run.ts:267) all mislabel as 'Hub restart' which is also inaccurate audit-trail noise. Smallest defensible change: parameterise the lifecycle default via HAPI_DEFAULT_ARCHIVE_REASON env, and have the runner set 'Stopped by runner' on spawn. Terminal-launched sessions (no runner parent, no env var) still default to 'Hub restart' since hub-restart cascade documented at #915 is the most plausible SIGTERM source for those. Explicit overrides via setArchiveReason (KillSession RPC, SIGINT Ctrl-C, markCrash uncaught exception) still win. Two new unit tests cover the env-var default and the override precedence. Refs tiann/hapi#914. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): markSessionArchivedFromHub surfaces persistence failures as 5xx Addresses second-round bot review of #923 (Major): `markSessionArchivedFromHub` silently returned on DB write errors and on exhausted version-retry attempts, which would let `/archive` claim 200 OK while the row stayed unarchived. That regresses the #916 acceptance criterion that non-RPC errors during archive must still propagate as 5xx. Both fall-through paths now throw, matching the contract of the sibling writers in this file (renameSession, mergeSessions). The sessionModel test suite gains two cases that spy on `store.sessions.updateSessionMetadata` to force `error` and `version-mismatch` shapes and asserts the helper throws. The existing route test at `hub/src/web/routes/sessions.test.ts:1015` already covers the route-level 500 propagation for any error thrown out of `archiveSession`, so no new route test is needed. Imports `spyOn` from `bun:test` to match this test file's runtime (the rest of the hub package uses bun:test, not vitest). Refs tiann/hapi#916. Co-authored-by: Cursor <cursoragent@cursor.com> * revert(cli): drop HAPI_DEFAULT_ARCHIVE_REASON env override Reverts `1c8972a3`. Bot review round 3 surfaced that the env-on-spawn approach (the bot's own round-1 suggestion shape) mislabels hub-restart-cascade SIGTERMs against runner-spawned children: systemd killcgroup on `hapi-runner.service` stop sends SIGTERM to all runner-children directly, and those would archive as 'Stopped by runner' instead of 'Hub restart'. The two suggestions are mutually incompatible without adding an IPC channel (stdio: 'ipc' on spawn) so the runner can stamp setArchiveReason via childProcess.send() before SIGTERMing. That is a refactor, not a smallest-defensible change. Going back to the simple shape: SIGTERM default is 'Hub restart' for everyone, runner-internal stop paths share that label. The audit-trail-correctness criterion from the #914 issue is met (SIGTERM no longer falsely labels as 'User terminated'). Finer attribution between cascade vs runner-stop is deferred as a follow-up. Refs tiann/hapi#914. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): clean completions get 'Session completed', not 'Hub restart' Addresses bot review round 4 of #923 (Major): every agent runner (runClaude, runCodex, runCursor, runGemini, runKimi, runOpencode) calls setSessionEndReason('completed') on the natural exit path without touching archiveReason. With the SIGTERM default flipped to 'Hub restart', clean completions were now archived as restart cascades. Fix: setSessionEndReason flips archiveReason to 'Session completed' when it transitions to 'completed' AND no caller has already overridden the archive reason. This covers all six agent runners with a single setter change (no per-runner edits). Two new tests cover the natural-completion default and the override precedence (explicit setArchiveReason still wins). Refs tiann/hapi#914. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): restore inactive-session guard on /archive except split-brain Addresses post-rebase bot review Major on #923: dropping requireActive entirely let normal inactive non-archived rows (completed stubs, UI Delete/Reopen targets) fall through to archiveSession, which could stamp archivedBy=hub on sessions that were never active. Restore the 409 for inactive rows unless metadata.lifecycleState is still 'running' (hub-restart split-brain cleanup case from #916). Two route tests cover the guard and the exception. Refs tiann/hapi#916. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): merge runnerLifecycle tests after upstream rebase Post-rebase fix: Session completed tests referenced makeFakeSession which was renamed to createMockApiSessionWithMetadataCapture when merging upstream hasExplicitSessionEndReason tests with #914 archive reason coverage. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): pass lifecycle object to KillSession handler in Pi runner Upstream #862 (Pi agent) landed after this branch was cut. runPi.ts still registered the legacy bare cleanupAndExit callback, so web Archive for Pi sessions would persist archiveReason: Hub restart instead of User terminated. One-line fix matching the other six agent runners. Refs tiann/hapi#914. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
a0259b531e |
feat(web): drag-and-drop files onto chat panel to add as attachments (#936)
* feat(web): drag-and-drop files onto chat panel to add as attachments Closes #935 Adds a `useDragOver` hook that detects when a file is being dragged over the browser window and suppresses the browser's default file-open behaviour for drops outside the accept zone. A new `DragDropZone` component wraps the inner `AssistantRuntimeProvider` content in `SessionChat`. It shows a semi-transparent overlay (dashed border + "Drop to attach" label) on the right-side chat panel as soon as any file drag is detected — regardless of where the pointer is on the page. Dropping on the right panel adds the files as composer attachments via the existing `api.composer().addAttachment()` path. Drops on the left sidebar are suppressed (no navigation, no attachment). via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): disable drag-drop zone when pendingSchedule is active The backend rejects requests with both scheduledAt and attachments. DragDropZone now respects pendingSchedule the same way paste and the attach button do — disabled=true suppresses the overlay, sets dropEffect='none', and skips addAttachment on drop. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): harden drag-drop default-action handling Address HAPI Bot review on #936: - useDragOver: cancel the browser's default file-open/navigation on the document-level `drop` event for file payloads, not only on `dragover`. Preventing default on `dragover` alone still lets the browser open a file dropped outside any zone (e.g. the sidebar), which could unload the app. - DragDropZone: only preventDefault when the drop payload actually contains files, so non-file drops (e.g. dragging selected text into the composer) keep their default browser behaviour. Add regression tests for both. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(web): use Simplified Chinese for composer.dropToAttach in zh-CN Address HAPI Bot review on #936: the new zh-CN string used Traditional Chinese forms (放開以附加檔案) in the Simplified Chinese locale, which is inconsistent with neighbouring keys (e.g. composer.attach = 添加文件). Use 松开以添加文件 to match. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b1910b6b2e |
feat(web): session header files and outline view toggles (#952)
* feat(web): session header files and outline view toggles Files and outline icons in SessionHeader act as depressed toggles; files view shares the session header and places refresh beside the search box. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): add Playwright handoff script for session view toggles Supports new-feature-intake visual gate: files toggle pressed + refresh beside search. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev): handoff script avoid networkidle on live hub SSE HAPI keeps connections open on :3006; domcontentloaded is the correct wait. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): place filesystem refresh outside search field Refresh is a sibling of the search pill, not inside it, so the control is visually and structurally separate from file search. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
02a0aa6733 |
fix(cursor): surface agent errors with warning styling in web UI (#871)
* test: reproduce issue #864 Assert Cursor error paths emit agent error payloads and web UI renders them as warning-styled events instead of neutral session messages. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): surface agent errors with warning styling in web UI (closes #864) Route Cursor stderr, init, prompt, and legacy exit failures through sendAgentMessage({ type: 'error' }) and teach the web chat layer to render error events with a warning icon instead of neutral info text. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bfd0f4a376 | fix(web): keep Codex restart control clear of close button (#880) | ||
|
|
d1a686f8d0 |
fix(cursor): map base-only CLI sku to fast=false to avoid silent variant no-op (#887)
Without an explicit -fast suffix, inferSkuParamHints returned no fast hint, so matchCliSkuToAcpWireId tied between fast=true and fast=false wires and kept the first one. For composer-2.5 that meant the picker's "non-fast" sku silently resolved to composer-2.5[fast=true] — the same wire the fast sku resolves to — producing the "selected but no response" symptom in #883. Treat absence of -fast as fast=false so base-only skus pick the slow variant and round-trip back to the matching radio. Fixes #883 via [HAPI](https://hapi.run) Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
8f3ea10df7 |
fix(web): mechanical repair of GFM tables with off-by-one separator rows (#902)
* feat(web): mechanical repair of GFM tables with off-by-one separator rows Adds a remark plugin (remarkRepairTables) that runs after remark-gfm and silently fixes the dominant broken-table pattern seen in agent output: the separator row has fewer pipe-delimited cells than the header row. remark-gfm follows the GFM spec and silently truncates the table to the separator column count, dropping header and data cells. This plugin reads the original source via file.value position data, detects the mismatch, pads the separator row, and re-parses the corrected block so all columns are preserved. Analysis of 7 days of session data: 975 apparent table blocks, 879 flagged broken. Of those, 744 (84.6%) were false positives (inline pipes in prose and shell commands). The separator off-by-one pattern accounted for the majority of genuine failures (~94 of 135 real broken tables). The plugin is wired into MARKDOWN_PLUGINS and MARKDOWN_PLUGINS_WITH_BREAKS, immediately after remarkGfm where position data is available. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test(web): strengthen remarkRepairTables test suite - Remove unused parseTableCols helper - Assert alignment markers (:-- / --:) are preserved in repaired separator - Add header-only table test (header + broken separator, no data rows) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): remove dead repairCount + add structural column-count assertion - Drop repairCount from visitTables — increment was never read at call site - Add per-row cell count assertion to the 3-column repair test to catch structural regressions that content-presence checks would miss via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test(web): add structural column-count assertions to remaining repair tests Off-by-N (4-column), alignment-hints, and header-only tests now verify each output row has the correct number of cells, not just content presence. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): skip escaped pipes in countSourceCells to prevent false repairs \| inside a GFM table cell is a literal pipe character, not a cell delimiter. The previous split('|') approach miscounted cells in headers like | A \| B | C |, treating a valid 2-column table as 3-column and padding the separator unnecessarily. Replaces the split with a character-scan that tracks escape state. Adds a test asserting the separator column count stays at 2 for tables with escaped pipes in the header. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): re-parse repaired table with main processor to preserve inline extensions parseTableBlock() previously created a bare remarkParse+remarkGfm processor, so inline math (or other pipeline extensions) inside a repaired table cell was parsed as plain text and lost after repair. Fix: use this (the Processor instance unified passes to the plugin factory) to re-parse the repaired block, so all registered extensions apply. Removes the now-unused remarkParse/remarkGfm/unified imports. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): rewrite repair plugin as string preprocessor The previous implementation visited `table` AST nodes after remark-gfm parsed the source. But remark-gfm 4.x degrades a mismatched-separator table (separator row has fewer cells than the header row) to a paragraph node entirely — no `table` node is ever produced, so the visitor never triggered and the repair was a no-op. New approach: scan `file.value` for broken separator rows BEFORE the AST is built, pad them in-place, then re-parse the corrected source so remark-gfm produces proper table nodes. Export `repairMarkdownTables` as a named function for direct testing. Update the unit tests to actually discriminate between a repaired table (stringified lines start with `|`) and the old broken paragraph output (stringified lines start with `\|`, escaped by remark-stringify). via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): skip fenced code blocks and preserve indentation in repair scan The string-level scanner was modifying table-like lines inside fenced code blocks (``` / ~~~) — a bug reported in PR review (Major). Also preserves original leading whitespace when replacing a separator line so indented tables are not affected. Add tests for fenced-code skip, ~~~ variant, and correct repair after a fence closes. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): harden remark-repair-tables against code-span pipes and mixed fences - countSourceCells: strip backtick code spans before counting column boundaries — a header like | `a | b` | c | is 2 columns, not 3 - repairMarkdownTables: track fenceChar ('`'|'~'|null) instead of a boolean toggle so ``` inside ~~~ no longer incorrectly flips fence state - add 2 tests: code-span-with-pipe in header, backtick inside tilde fence - fix stale comment in markdown-text.tsx (plugin reads file.value, not AST nodes) - drop no-op .trimStart() (padSeparatorLine already returns a trimmed string) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): handle double-backtick code spans and preserve tree root on re-parse - countSourceCells: use /`+[^`]*?`+/g so double-backtick spans like `` `a | b` `` are also stripped before counting column boundaries - remarkRepairTables: Object.assign(tree, newTree) instead of only copying children, so position/data from the root node are preserved via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): require closing fence to match opener length (GFM fence rule) A ```` fence must not be closed by ``` — GFM specifies the closer must use the same marker character AND be at least as long as the opener sequence. Track fenceLength alongside fenceChar so longer-backtick fences stay open until a closer of equal or greater length arrives. Also tighten the fence-match regex from /^\s*/ to /^ {0,3}/ to match the GFM spec (fences are valid with up to 3 spaces of indentation, not arbitrary whitespace). Adds a regression test for the ```` / ``` case. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): closing fence must have only whitespace after the marker (GFM rule) GFM §4.5: a fence closing sequence may only be followed by optional spaces. A content line like \`\`\`ts inside a code block is not a valid closer, so we must not clear fenceChar when the remainder of the line is non-whitespace. Captures rest after the marker and guards the close branch with /^\s*$/. Opening fences are unaffected (info strings on openers remain valid). Adds a regression test: ``` opener, ```ts content line, ``` closer. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
f5c0ef245b |
fix: active-only session filter + paginated "Show N more" (closes #901) (#903)
* test: reproduce issue #901 (active-only filter + paginated show more) * fix: active-only session filter + paginated 'Show N more' (closes #901) Add a persisted 'Active sessions only' toggle in Settings -> Display that hides inactive sessions in the sidebar while keeping the selected session visible. Change 'Show N more' to reveal one batch (preview-limit size) per click instead of expanding every hidden session at once, with 'Show less' to collapse back to the initial preview. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
22bf7e04d2 |
fix(web): persist file explorer expanded tree and scroll position across navigation (#911)
* fix(web): persist file explorer expanded tree and scroll position across navigation Expanded folder state and scroll position in the Directories tab were stored only in local React state, so navigating to a file and back would reset the tree to the root and scroll to top. Now both are saved to sessionStorage (keyed by sessionId) on every change and restored on remount, so the explorer resumes exactly where the user left off. Closes #910 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): key DirectoryTree by sessionId to prevent stale expanded state across sessions When navigating between sessions, React can reuse the same DirectoryTree instance. The useState lazy initializer only runs on first mount, so the tree would hydrate with the wrong session's expanded set and then overwrite the new session's storage key. Adding key={sessionId} forces a fresh mount per session. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
a8e08e8014 |
feat(web): add download button to file viewer (#926)
Adds a download icon button to the file viewer toolbar (next to copy-path). Clicking it decodes the existing base64 file content into a Blob and triggers a browser download — no new backend endpoint required. Works for text, binary, and image files. Button is hidden until the file has loaded successfully. Closes #924 via [HAPI](https://hapi.run) Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
2643f17840 |
feat(web): Web Share Target -> Android system share sheet integration (#933)
* feat(web): Web Share Target -> composer attachment preload
PWA manifest now declares a `share_target` so Android Chrome surfaces
HAPI in the system share sheet for any app (Photos, Files, browser).
Pipeline on share:
1. Service worker intercepts POST /share, parses the multipart payload
(title/text/url + N files), persists it in IndexedDB under a
transfer id, and 303-redirects to /share?id=<id>. The 303 forces
Chrome to convert the POST into a GET so the SPA route mounts.
2. New /share route loads the transfer, previews the content, and
lets the user pick a recent active session (top 5 by activeAt) or
a "+ New session". Tapping a session stashes the transfer id in
sessionStorage and navigates to /sessions/:id.
3. SessionChat mounts a ShareSeedConsumer once the AssistantRuntime
is up; it consumes the pending transfer once per mount, seeds
composer text + per-file attachments via the existing
attachmentAdapter, then deletes the IDB row so a refresh of the
session page does not replay the upload.
The whole feature reuses the existing /sessions/:id/upload endpoint;
no hub or shared changes.
Limitations (also disclosed in the PR body):
- PWA must be installed; Android Chrome only registers share_target
on install. iOS Safari ignores the manifest field entirely.
- File MIME accept list is broad (`*/*` fallback); some Chrome
versions still filter despite this.
Tests:
- shareTransfer.test.ts (8) covers payload parse, multi-file order,
type fallback, ingest redirect shape and error propagation.
- sharePendingState.test.ts (3) covers atomic consume + overwrite.
Closes: pending upstream issue (filed before PR per intake doc).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web/share): freeze picker session list at mount, sort by updatedAt, drop top-5 cap
The /share picker was visually re-shuffling under the operator's finger
as SSE events rolled in: every session metadata patch refreshed the
React Query cache, the useMemo recomputed, and items reordered (often
within a second of opening the share sheet). Sort key was activeAt,
which heartbeats every few seconds while a session is connected,
making the noise floor even higher.
Three changes:
- Snapshot the active-session list once when sessions finish loading
via useState + a deferred useEffect. The picker is a one-shot
interaction; closing the share sheet and re-sharing produces a
fresh snapshot, so freezing for the duration of the picker view is
the right trade.
- Sort by updatedAt desc to match SessionList's canonical "most
recent interaction first" order. updatedAt only moves on
user-meaningful events, not heartbeats.
- Drop the TOP_SESSIONS=5 cap. The picker is already inside an
app-scroll-y container, so showing all active sessions and letting
the operator scroll matches the operator's mental model better
than an arbitrary truncation.
Per operator dogfood report: "list of recent sessions is constantly
updating; should be just a scrollable list, from most recent
interaction to not."
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web/share): base-aware share_target paths for subpath PWA deploys
Manifest share_target.action, SW POST matching, and ingest 303 redirects
were hard-coded to /share. Standalone builds with --base /<repo>/ put
scope/start_url under the subpath but left the share action at origin
root, so Chrome posted outside the SW scope and the handler never ran.
Extract shareTargetPathnameFromBase() (used at build time in
vite.config.ts and at runtime via import.meta.env.BASE_URL in sw.ts and
shareTransfer.ts). Normalizes base to a trailing slash before URL
resolution so /repo and /repo/ both resolve to /repo/share.
Addresses upstream PR #933 review (Major).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web/share): defer sessionStorage arm until new session spawn succeeds
The "+ New session" picker path called setSharePendingTransfer before a
session existed. Cancel, spawn failure, or backing out left a stale id in
sessionStorage that the next unrelated SessionChat mount would consume.
Pass shareTransferId via /sessions/new search params instead; arm the
consumer only in handleSuccess after spawn, and delete the IDB row on
cancel.
Addresses upstream PR #933 review (Major).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web/share): append share text to existing composer draft
ShareSeedConsumer called setText(seedText) unconditionally, clobbering
per-session drafts restored by useComposerDraft from sessionStorage.
Merge share title/text/url after any in-composer text or saved draft,
joined with a blank line. Pass sessionId into ShareSeedConsumer so
getDraft() can be consulted when the composer is still empty.
Addresses upstream PR #933 review (Major).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web/share): preserve shareTransferId through /browse detour
New-session spawn from the share picker could lose shareTransferId when
the operator opened /browse to pick a folder: handleChooseFolder and
BrowsePage handleStartSession dropped the search param, so handleSuccess
never armed the composer consumer.
Thread shareTransferId through browseRoute search validation and both
navigation hops.
Addresses upstream PR #933 review (Major).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web/share): consume pending transfer in effect for StrictMode
ShareSeedConsumer called consumeSharePendingTransfer during render.
React.StrictMode double-invokes render in dev; the discarded pass
deleted the sessionStorage key before the committed render seeded.
Move consume into a mount-only useEffect and gate the seed effect on
transferReady.
Addresses upstream PR #933 review (Minor).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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> |
||
|
|
ce67823fc3 |
feat(web,hub): rich hover tooltips on session-list attention indicators (#941)
* feat(web): rich hover tooltips on session-list attention indicators
The session-row attention dots and the future-scheduled clock icon used
plain `title=""` attributes which gave only a one-word label ("Permission
required"). Replace those with hover/focus-revealed tooltips that name
*which* tools are blocking, count background tasks, surface the
"updated Nm ago" timestamp, and explain the pending schedule.
To make per-tool copy possible without an extra round trip,
`SessionSummary` now carries a structured slice of the pending tool
requests, capped at `PENDING_REQUEST_SUMMARY_CAP = 5` oldest-first:
pendingRequests: Array<{ id; kind; tool; since }>
`pendingRequestsCount` remains the authoritative total;
`pendingRequestKinds` is still derived from the FULL request set so a
single `'input'` request beyond the cap still surfaces its kind on the
session row.
The tooltip primitive (`HoverTooltip`) is a CSS-driven reveal — no
portal, no positioning JS — so it composes cheaply inside the existing
session-row `<button>` and stays out of the way on touch devices, which
keep getting the same `aria-label` the old `title=""` attribute provided
to screen readers.
Test coverage: shared derivation + cap + tie-break + full-set kind
behaviour; web tooltip render across all four attention kinds plus
mixed-kind overflow suppression and aria-label exposure.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): opaque tooltip surface; drop redundant 'updated Nm ago' body
Two operator-feedback fixes on the new session-list HoverTooltip:
1. Tooltip background was bg-[var(--app-bg)] - the same variable as the
session row underneath - so the tooltip looked translucent and the row
text bled through. Switch to bg-[var(--app-secondary-bg)] (#2C2C2E
dark / #f3f4f6 light, both opaque) and bump shadow-md -> shadow-lg.
Telegram-themed clients still pick up tg-theme-secondary-bg-color so
the tooltip stays on-theme.
2. The 'unread' attention dot tooltip rendered 'New activity / Updated 5m
ago', but the relative-time pill ('5m ago') is already on the right
edge of the same session row. The tooltip body just duplicated info.
Render only the title for the unread case; drop the
session.tooltip.unread.body i18n key from en + zh-CN.
The other tooltip kinds (permission/input list tools, background lists
task count) keep their bodies - those facts are not visible elsewhere on
the row.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web,hub): show scheduled fire time in session-list clock tooltip
The schedule clock tooltip previously said only "Will fire when due."
while the row already showed a relative updated-at pill. Extend the
session-list API with nextScheduledAt (MIN future scheduled_at per
session, same filter as futureScheduledMessageCount) and render:
- single scheduled: "Fires in 5m · Jun 16, 1:45 PM"
- multiple: "Next in 5m · Jun 16, 1:45 PM · +2 more"
Extract formatScheduledTime from QueuedMessagesBar into web/lib/
scheduledTime.ts alongside formatFutureRelativeTime and the tooltip
composer. SSE upsert preserves nextScheduledAt until the list refetch
that already runs on schedule-related events.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): wire session-row keyboard focus to HoverTooltip a11y
Address PR #941 Major review: aria-describedby and tooltip visibility
were on a non-focusable inner span, so keyboard users tabbing the session
row button never received the rich tooltip description and
group-focus-within never matched.
- Session row button owns aria-describedby (attention + schedule ids)
- Add group/session-row + SESSION_ROW_TOOLTIP_FOCUS_CLASS reveal on
:focus-visible
- HoverTooltip takes required id; drop inner aria-label/describedby
- useSessionRowTooltipIds helper composes stable row tooltip ids
- Tests for id wiring and parent-focus reveal classes
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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> |
||
|
|
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> |