Commit Graph
739 Commits
Author SHA1 Message Date
b06251bd53 fix: stream opencode reasoning updates (#661)
Emit throttled ACP reasoning snapshots with stable stream ids so OpenCode reasoning updates render live without one row per token.

Collapse matching reasoning snapshots in the web timeline and avoid stale session 404 redirects during refetch.

Tests: targeted Vitest suite and bun typecheck.

Co-authored-by: twshe <twshe@outlook.com>
2026-05-22 08:57:50 +08:00
CoColateandGitHub 5b90a48b33 fix(web): respect manual session path collapse (#656) 2026-05-21 21:18:27 +08:00
CoColateandGitHub 611c4eec49 fix(codex): honor yolo for MCP elicitation (#655)
Route Codex app-server MCP elicitation decisions through the live session permission mode. HAPI bridge elicitation remains accepted, non-HAPI elicitation stays cancelled in non-yolo modes, and yolo accepts future non-HAPI prompts after mode changes.\n\nAdds adapter and launcher seam tests covering default -> yolo -> default behavior.\n\nTests:\n- bunx vitest run src/codex/utils/appServerPermissionAdapter.test.ts src/codex/codexRemoteLauncher.test.ts\n- bun run typecheck
2026-05-21 17:09:10 +08:00
weishu 8e3bba9303 refactor: share REST mutation schemas 2026-05-21 14:49:51 +08:00
weishu e66f403cb6 refactor: derive new session model options 2026-05-21 14:45:59 +08:00
weishu eed7c9b0b3 refactor: rely on tool view registry for inputs 2026-05-21 14:44:30 +08:00
weishu 1284385d38 refactor: share remote agent command parsing 2026-05-21 14:42:40 +08:00
weishu b0a3397601 refactor: share session config RPC handling 2026-05-21 14:40:00 +08:00
weishu e88a9075df refactor: share core REST payload types 2026-05-21 14:35:47 +08:00
weishu 7c26c1e749 refactor: reuse shared machine socket types 2026-05-21 14:32:28 +08:00
weishu 9704227ce2 refactor: share machine runner schemas 2026-05-21 14:29:42 +08:00
weishu 7d5a5ee919 chore: remove obsolete spawn leftovers 2026-05-21 14:25:10 +08:00
weishu b79f50f815 Share RPC method constants 2026-05-21 10:53:31 +08:00
weishu 41f6b37d69 Structure SSE update patches 2026-05-21 10:44:46 +08:00
weishu 2e1e2d39db Share slash command definitions 2026-05-21 10:36:25 +08:00
weishu d6f97065c1 Share REST and RPC response types 2026-05-21 10:31:50 +08:00
weishu 9698aa9f4c Unify agent flavor definitions 2026-05-21 10:23:57 +08:00
weishu 15113668cc Release version 0.18.3 2026-05-20 20:50:49 +08:00
weishu c53ee0ed90 fix web vitest config loading 2026-05-20 20:47:16 +08:00
weishu 64d1a4de1c Cap resume picker redraw height 2026-05-20 20:44:12 +08:00
weishu 30c9d34cf8 Size resume picker rows by terminal height 2026-05-20 20:43:04 +08:00
weishu e90ef52078 Show more resume picker rows 2026-05-20 20:41:57 +08:00
weishu f41be3a420 Reduce resume picker redraw flicker 2026-05-20 20:41:05 +08:00
weishu 5264599908 Recover first prompt for resume sessions 2026-05-20 20:35:03 +08:00
weishu 856af6d8b2 Show first user message in resume picker 2026-05-20 20:31:40 +08:00
weishu 2ef90f84fb Move resume picker directory to status bar 2026-05-20 20:26:38 +08:00
weishu 62ac4e7b0a Show relative time in resume picker 2026-05-20 20:24:46 +08:00
weishu 1bd0bb2cf7 Add interactive resume session picker 2026-05-20 20:20:27 +08:00
weishu 1954920753 Release version 0.18.2 2026-05-20 20:06:33 +08:00
weishu c8c122812f Fix mobile schedule picker layout 2026-05-20 20:04:07 +08:00
weishu fbd7527cf4 Stop hub cleanly from CLI command 2026-05-20 19:55:40 +08:00
weishu 83795c0630 Clean up cross-package build coupling 2026-05-20 19:29:43 +08:00
weishu 6759cf4657 Remove old protocol compatibility layers 2026-05-20 17:45:28 +08:00
weishu 9324598cd5 chore: remove dead UI and agent entrypoints 2026-05-20 17:32:08 +08:00
SmallSpiderandGitHub 74e40b8a1a fix(codex): stabilize goal status UI events (#652) 2026-05-20 17:26:10 +08:00
SmallSpiderandGitHub 25631d971c fix(codex): stabilize goal status updates (#651) 2026-05-20 11:31:28 +08:00
SmallSpiderandGitHub 2aaae25d0a fix(codex): dedupe repeated goal updates (#649) 2026-05-20 10:00:51 +08:00
lekoandGitHub 197f327590 feat: add hapi resume command (#647) 2026-05-20 06:18:42 +08:00
SmallSpiderandGitHub 79d919675e fix(codex): handle subagent spawn startup failures (#648) 2026-05-19 21:35:50 +08:00
weishu c070cdef28 Fix probe failure 2026-05-19 17:07:15 +08:00
lekoandGitHub ce2e76a42e Add Windows remote terminal support (#642) 2026-05-19 07:54:12 +08:00
lekoandGitHub bb04247127 Fix CLI tests on Windows paths (#643) 2026-05-19 07:53:28 +08:00
lekoandGitHub 7cb8ccccbe Fix hub SQLite cleanup on Windows (#644) 2026-05-19 07:53:03 +08:00
weishu ff866c2069 Release version 0.18.1 2026-05-18 11:06:32 +08:00
weishu f31c591f33 Improve grouped tool card labels 2026-05-18 11:02:07 +08:00
Junmo KimandGitHub d1c2051f28 fix(web): aggregate per-response metadata so multi-turn cards show total usage (#637)
* refactor(web): extend MessageMetadata to accept aggregated turnCount

Add an optional `turnCount` prop to MessageMetadata so the same builder
can render an aggregated response-group footer when the caller has
already summed usage and dedup-joined model ids. The label set switches
to `Models` / `Total` / `N turns` only when `turnCount >= 2`, leaving
single-turn footers byte-identical with the existing
`Invoke · Model · Usage` output.

Also expose `turnCount?: number` on `HappyChatMessageMetadata` so a
later commit can inject the aggregated metadata through the library's
ThreadMessageLike payload without widening the type at the same time.

No call site passes `turnCount` yet, so this commit is behavior-neutral
on all existing surfaces (proof-of-invariance test included).

* feat(web): aggregate per-response metadata so multi-turn cards show total usage

The `@assistant-ui/react` converter joins adjacent assistant messages
into one card but only preserves `metadata.custom` from the first
block, so multi-turn responses currently show the first turn's usage
and model only.

Compute response-group aggregates in `useHappyRuntime` and inject the
sum on each group's first visible block, where the library will keep
them. Per group: usage tokens are summed across distinct turns,
model ids are dedup-joined in first-seen order, and the invoke time
is the first turn's so the footer keeps showing when the response
started (regression-guarded by unit test). `durationMs` is explicitly
cleared on aggregated blocks because the first turn's value would
otherwise leak through the join.

Turn identity prefers the CLI-stamped `localId`. When that is null
(claude code spawn sessions today emit `localId=null` on every chunk)
the aggregator falls back to a fingerprint built from `model` plus the
shared `usage` totals — every block emitted within one Claude SDK
message carries an identical usage object, so the fingerprint dedups
those chunks without merging distinct turns whose token counts
naturally differ. Tool-result chunks with no model or usage are
skipped so they cannot inflate the turn count.

Single-turn responses get no aggregate entry, so their footers stay
byte-identical with the existing behavior.

Test plan
- `assistant-runtime.test.ts` covers the six grouping scenarios spelled
  out in the design note (localId-based + null-localId fingerprint
  fallback) plus two defensive cases for tool_result chunks and cache
  token preservation.

* fix(web): preserve explicit zero sums and count tool-group turns in response aggregator

Two correctness gaps in aggregateResponseGroups:

- addUsage folded `0 + 0` through `|| undefined`, dropping an
  explicit-zero cache token sum from the aggregated metadata.
  Replace the falsy fold with sumOptional(): undefined only when
  both operands are absent, otherwise (a ?? 0) + (b ?? 0).
- turnSourceFromBlock returned null for tool-group blocks, so a
  card whose visible-first block is a tool-group dropped its
  turn entirely. Read the first underlying tool-call instead;
  degrade to null only when the group somehow holds zero tools.

Unit tests cover both regressions: tool-group as the first visible
block in a response group, explicit-zero cache sums preserved, and
the empty-tool-group degrade-to-null path.

* fix(web): dedup response-group turns by adjacency rather than set membership

The fingerprint fallback (used when localId is null) compared each
turn key against a Set of every key seen in the group. A response
group whose first and third turns happened to carry the same
(model, usage) fingerprint would collapse the third turn into the
first, under-counting the visible turn count.

Switch to ordering-based dedup: each block's turn key only collides
with the immediately previous turn. Adjacent blocks within one SDK
message still collapse (their usage object is identical), but
non-adjacent fingerprint matches across separate turns stay
distinct. Behavior under localId-stamped flows is unchanged because
distinct turns always carry distinct localIds.

Unit test covers a three-turn group whose first and third turns
share a fingerprint with a different middle turn between them.

* fix(web): aggregate every tool-call in a tool-group and dedup by createdAt fingerprint

`buildVisibleChatBlocks` merges adjacent eligible tool-calls into a single
`tool-group` without checking that they share a turn. Reading only the
first underlying tool would drop every later tool turn from the aggregate,
so each tool-call in the group now contributes its own turn source.

The fingerprint fallback (used when the CLI does not stamp `localId`)
gains `createdAt` as a third axis. The reducer copies `msg.createdAt`
onto every derived ChatBlock, so blocks from one SDK message still
collapse to one turn, while two adjacent turns that happen to coincide
on `(model, usage)` no longer dedup against each other. Same wall-clock
millisecond collisions remain theoretically possible but are bounded by
the hub stamp resolution.

Helper layer consolidates: `turnSourceFromBlock` (single-or-null) is
gone, replaced by `turnSourcesFromBlock` returning the array directly.
Test renames clarify the contract — the existing tool-group test now
documents the same-turn collapse case — and one new test pins the
fingerprint coincidence case.

* fix(web): make tool-only response cards expose aggregate metadata

`aggregateResponseGroups` keys aggregate metadata onto a response group's
first visible block, which can be a `tool-group` when the assistant turn
starts with tools. The `toolOnly` render branch did not wire the click
toggle that the default/codex branches use, so the new Models/Total/N-turns
footer stayed unreachable for those cards.

Wrap the toolOnly content with the same cursor-pointer div used in the
sibling branches (toggleMetadata, onMetadataKeyDown, role=button,
aria-expanded). Carry `min-w-0` on the wrapper so long tool labels keep
clipping under the existing `overflow-x-hidden` on MessagePrimitive.Root.

The shared `isNestedInteractiveEvent` guard prevents the wrapper toggle
from firing when nested tool buttons or disclosures are clicked.
2026-05-18 10:40:51 +08:00
Junmo KimandGitHub 6e32b20524 feat(web): linkify custom URI schemes with a confirm prompt (#633)
* feat(web): add UriConfirmDialog component

Add a Radix Dialog-based confirmation modal for custom URI scheme
navigation. Follows the RenameSessionDialog pattern.

- UriConfirmDialog: shows URI, scheme label, Cancel/Open/Always-allow buttons
- i18n keys: dialog.uri.{title,description,open,alwaysAllow}

* feat(web): autolink non-https URI schemes in markdown

Add a remark plugin that converts raw `scheme://...` text nodes into
link nodes for non-http(s) schemes. GFM already handles http/https;
this plugin handles the remainder (obsidian://, vscode://, slack://, etc.).

- No scheme allowlist: every `scheme://` pattern is converted; the
  sanitize layer (urlTransform) and onClick layer (classifyScheme) handle
  blocking/confirmation downstream.
- Runs before remarkStripCjkAutolink so the CJK-strip plugin sees the
  new link nodes and can trim trailing CJK punctuation from them.
- Trailing punctuation (.,;!?) stripped from matched URIs.
- Unit tests: conversion, partial-match, escape, explicit link bypass,
  code-block bypass, trailing-punct trimming.

* feat(web): linkify custom URI schemes via markdown <a> handler

Wire up 4-layer URI security policy in the markdown renderer:

1. URL sanitize (deny-only): urlTransform strips javascript:/data:/vbscript:/file:
   using classifyScheme as single source of truth (handles percent-encoding,
   case-insensitive, whitespace-prefix bypass patterns).

2. onClick intercept: custom <A> component classifies each href —
   - IANA safe (https/http/irc/ircs/mailto/xmpp): navigate directly.
   - Deny (javascript/data/vbscript/file): preventDefault silently.
   - Custom (obsidian/vscode/slack/…): preventDefault + open UriConfirmDialog.

3. UriConfirmProvider: one dialog lifted to each markdown root (MarkdownText,
   Reasoning, MarkdownRenderer). Shared isAllowed state across all <a> tags in
   the subtree — "Always allow" click updates every link in one React commit.

4. Intra-tab cross-provider sync (P7e.1): module-level schemeListeners Set so
   sibling UriConfirmProviders (MarkdownText + Reasoning in AssistantMessage)
   receive allowed-scheme updates synchronously without waiting for the window
   storage event (which only fires in other tabs). Cross-tab sync continues via
   the existing window storage event listener.

5. "Always allow" persisted to localStorage (hapi-allowed-schemes). Custom
   schemes once allowed navigate directly on subsequent clicks, no dialog gate.
   href="#" in DOM for unallowed custom schemes prevents middle-click bypass.
   Deny-scheme href="" prevents any navigation even if localStorage tampered.

Security: classifyScheme decodes percent-encoding before scheme extraction,
blocking %6Aavascript:, jav%61script:, javascript%3A (single-encoded colon)
and double-encoded variants. DENY_SCHEMES checked after localStorage lookup so
tampered allowed-list cannot promote deny schemes.

Tests: classifyScheme 6-axis security bypass, denyOnlyTransform, localStorage
roundtrip, cross-tab storage event, <A> click handler cases.

* fix(web): block control-char-spliced deny schemes in classifyScheme

Browsers silently strip ASCII control characters (\t, \n, \r) and
whitespace from URL scheme names during navigation. A scheme like
`java\nscript:alert(1)` was navigated as `javascript:` while our
literal string comparison classified it as 'custom', allowing it
past the deny list and into window.open().

Introduce normalizedScheme() that:
- applies 2 rounds of decodeURIComponent so double-encoded schemes
  (javascript%253A → javascript%3A → javascript:) are fully unwrapped
  before comparison
- strips [\x00-\x1F\x7F\s] from the extracted scheme name, matching
  the browser's own normalization

classifyScheme() now delegates to normalizedScheme() so both the
denyOnlyTransform (urlTransform) path and the <A> onClick path benefit
from the same normalization.

Tests added for \n / \t / \r / space spliced into scheme, and verify
that double-encoded colon is now caught via scheme-match (not just
the no-colon fallback).

* fix(web): preserve relative markdown links from being blocked

Relative / no-scheme hrefs (/settings, ./foo, #section, ?q=1) were
silently preventDefault'd in <A>'s onClick handler. denyOnlyTransform
correctly passed them through (no colon → not a scheme URL), but the
click handler called classifyScheme(href) which returned 'deny' for
any input with no valid scheme separator — then the deny branch fired.

Add hasScheme(href): checks whether the first ':' appears before any
path/query/fragment boundary ('/', '?', '#'). When hasScheme is false
the href is treated as 'iana' so the browser or SPA router can navigate
normally with no dialog and no preventDefault.

Also wrap renderA() with <I18nProvider> so the UriConfirmDialog that
UriConfirmProvider may render does not throw outside its translation
context during tests.

Fixes a regression that broke all relative-path markdown links once the
custom-URI-scheme onClick handler was added.

* test(web): cover percent-encoded scheme control char + protocol-relative href

Round-5 internal hostile review noted two coverage gaps on the bot-fixup commits:

- `java%0Ascript:alert(1)` (percent-encoded newline in the scheme name) takes the
  same decode→strip code path as the literal `java\nscript:` case but was only
  tested literally. Add an explicit test so a future refactor that drops the
  decode-then-strip ordering would be caught.
- Protocol-relative URLs (`//host/path`) have no colon, so `hasScheme` returns
  false and `<A>` treats them as scheme-less — browsers then navigate them as
  the current origin's protocol. Existing relative-href tests covered absolute
  paths, hashes, queries, and colon-in-path, but not the protocol-relative
  variant. Add one assertion.

Also extend the `hasScheme` JSDoc to note that protocol-relative URLs are
intentionally treated as scheme-less.

* fix(web): preserve balanced parens/brackets in autolinked URIs

The trailing-punctuation strip used to drop every `)` / `]` from the end
of a matched URI, even when the URL body had an unmatched opener. So a
URI like `obsidian://open?file=Note(1)` was rendered with href
`obsidian://open?file=Note(1` plus a separate `)` text node, opening a
broken deep link.

Match the GFM autolink-literal behaviour: when the trailing character is
`)` or `]`, keep it iff the URL body has more opening counterparts than
closers (so the trailing closer balances an earlier opener and belongs
to the URL). Other trailing punctuation (`.,;!?:>'"`) and unmatched
closers still strip as before.

Add tests for the balanced cases (`Note(1)`, `Note[1]`, nested
`(a(b)c)`), the "balanced URL followed by a period" case, and a
regression test that an unmatched `).` after a URL is still stripped.
2026-05-18 10:40:32 +08:00
Junmo KimandGitHub 5512890a4c fix(web): bound scroll restoration cache by collapsing keys to pathname (#632) 2026-05-18 09:09:54 +08:00
Junmo KimandGitHub b2a30c2e39 feat(hub,web): support scheduling messages for future delivery (#590) 2026-05-18 09:09:17 +08:00
weishu 2e96992dd4 Fix mobile status bar 2026-05-17 23:23:28 +08:00