The Codex import button used the same circular-arrow SVG as the
session-list refresh button, making the two adjacent actions look
identical. Switch the import affordance to a download-into-tray icon
to match its 'import sessions' semantics.
Closes#1135
* fix codex session import merge (#1123)
修复 Codex 会话导入合并后列表为空的问题。
Fix Codex session import merge so a session is not detected as a duplicate of itself and deleted during merge.
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
* fix(web): honor explicit older-history loads
Clear the initial scroll-settling window before Load older, outline
load-more, and outline target fetches so a user click is not swallowed
during the first-scroll-to-bottom settle period.
Automatic top-sentinel loads still respect settling.
Fixes#1067
---------
Co-authored-by: Himehane <36065996+Himehane@users.noreply.github.com>
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
* fix codex session import merge (#1123)
修复 Codex 会话导入合并后列表为空的问题。
Fix Codex session import merge so a session is not detected as a duplicate of itself and deleted during merge.
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
* fix(web): keep overlays below PWA status bar
Account for env(safe-area-inset-top) on fixed top banners, image
preview chrome, and session action menus so installed PWA mode no
longer draws controls under the OS status bar.
Fixes#1066
---------
Co-authored-by: Himehane <36065996+Himehane@users.noreply.github.com>
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
Use @lobehub/icons (already a dependency) for per-agent SVG logos in the
session list, session header, and new-session agent selector.
- Color variants for claude/codex/gemini; Mono (currentColor) for
cursor/grok/opencode so glyphs track the theme text color
- kimi uses Mono as well: KimiColor's main glyph is hard-coded #fff and
vanishes on the light theme
- pi and unknown flavors keep the letter-badge fallback (no logo shipped)
- Deep component imports avoid the package root's ./features re-export,
which pulls uninstalled peer deps (antd, @lobehub/ui)
* feat(shared,cli): whitelist away_summary so auto recap reaches the hub
Claude Code's local TUI writes an automatic away-summary recap to the
session transcript on window blur/focus (5min+ idle), but
VISIBLE_CLAUDE_SYSTEM_SUBTYPES dropped it before it ever reached the
hub. Add it to the whitelist so the local launcher forwards it like
the other system subtypes, and cover the forwarding + Zod passthrough
of the recap `content` field with tests.
* feat(web): render Claude Code's automatic away recap in the chat
Once away_summary reaches the hub (previous commit), the web chat
still dropped it silently: normalizeAgent had no branch for the
subtype, so it fell through to `return null`. Add a `recap` AgentEvent,
a normalizeAgent branch mirroring the existing turn_duration/compact
subtype branches, and a presentation entry that prefixes the text with
`recap:` so it reads distinctly from the manual /recap assistant
bubble (which already renders as a normal message). No new render
component needed: it flows through the existing generic system-event
row (SystemMessage.tsx + getEventPresentation) that every other system
subtype already uses.
* fix(web): drop inaccurate manual-/recap comparison from recap comments
* fix(web): improve machine health sidebar UX
Keep machine names visible, align health metrics, and make nested health tooltips usable with touch and keyboard input.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): preserve tooltip focus reveal groups
Keep the unnamed group used by existing focus reveal classes while retaining named hover groups for nested machine health tooltips.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): add exec* timestamps to ToolCard test fixture
Unblocks typecheck after #1036 made execStartedAt/execCompletedAt
required on ChatToolCall; fixture was missing both fields.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): align machine health status with meters
Right-align the capacity status with the utilization meter edge to balance the tooltip header without shortening the bars.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): restore machine health disclosure semantics
Expose the machine group's expanded state on its toggle and describe the health trigger with the tooltip body for assistive technologies.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
MessageInfoPopover's trigger button carried the same
happy-message-actions-desktop-only class as the other hover-reveal
actions from 4c76668a, so it never rendered on touch-only viewports
(no hover: hover match). Switch it to the always-visible flex pattern
the sibling copy button already uses, matching desktop's hover-reveal
opacity animation on the parent row.
Also widen the action row's desktop-only-row guard to stay reachable
when a tool-only response (no copyable text) still carries model/
duration metadata from its first tool block, so the info popover isn't
hidden behind an empty row on mobile.
* feat(web): show subagent's executed model in Task/Agent card header
Task/Agent trace cards previously gave no indication of which model a
subagent actually ran under, even though the model can differ from the
calling session's (e.g. main session on opus, subagent on haiku) and
can even change mid-run when --fallback-model kicks in under overload.
The data already reaches the frontend: each child block produced from
a subagent's own sidechain carries the model of the assistant message
it came from. Derive it in getSubagentModel() from the tool call's own
children (not the parent ToolCallBlock.model, which reflects the
calling session and would misattribute the model), collecting distinct
raw values in first-seen order and joining them the same way
aggregateResponseGroups already does for top-level multi-turn message
metadata.
Full SDK model ids (e.g. claude-sonnet-4-5-20250929) are long and not
great for a compact label, so formatSubagentModelLabel() extends this
repo's existing "friendly label, else raw fallback" idiom
(getClaudeModelLabel(model) ?? model in claudeModelOptions.ts, which
only covers the short preset aliases) with a narrow second fallback
that extracts just the name and version from the SDK id shape and
drops the date suffix (-> "Sonnet 4.5"). Anything else is left as-is.
Renders the result as a small chip in the header's existing right-side
meta cluster (next to ElapsedView/status icon), always visible without
opening the detail dialog.
* fix(web): cap subagent model badge width to avoid squeezing the card header
Addresses HAPI Bot review on #1045: formatSubagentModelLabel() returns
unrecognized model ids (Gemini, Codex, future formats) unchanged, and
those can be long. Bound the chip with max-w + truncate so it can't
push the title/status area off narrow cards, with a title attribute
so the full value is still reachable on hover.
* refactor(web): export formatDuration for reuse
* feat(web): show tool call duration in the detail dialog
Show a completed tool's execution duration at the top of its detail
dialog. The value is derived from the Claude entry's own timestamps
(the execution machine's wall clock) rather than the hub's
message-receive time, and is used only when both the tool_use and
tool_result entries carry a real timestamp — otherwise it falls back to
the hub receive times on both sides, so the two clocks are never mixed.
Running/pending tools show nothing, the running-state live timer is
unchanged, and clock skew is guarded against. Reuses the existing
formatDuration formatter. No schema changes.
* fix(web): backfill hub startedAt on reorder so duration isn't 0.0s
When a tool_result entry is reduced before its tool_use, the tool block
is created from the result, so the hub startedAt is the result receive
time. The tool_use path only lowered the exec start, not the hub
startedAt, so a timestamp-less pair (no exec duration available) fell
back to startedAt === completedAt and the detail dialog showed 0.0s.
Lower the hub startedAt to the earlier tool_use receive time as well.
Use provider-qualified model lookup (provider + modelId) to resolve the
correct context window for the Pi status bar, falling back to legacy
modelId when selected-model metadata is absent. This prevents showing
the wrong context window when two providers share the same modelId.
* 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
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>
* 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>
* 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.
* 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>
* 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>
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.
* 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>
* 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>
* 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>
* 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>
* 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>