Commit Graph
1685 Commits
Author SHA1 Message Date
AnanovoandGitHub f29be07bb9 fix(codex,web): preserve late command result identity and timing (#2239) 2026-10-29 22:12:27 +08:00
weishu 9384ce031b feat(cli): add Codex-aware auto peer delivery
Default peer CLI and MCP messages to auto, steering eligible Codex turns and queueing otherwise. Preserve explicit delivery modes and uncertain-send safety, and clarify steer guidance for important time-sensitive findings.
2026-10-29 22:02:43 +08:00
AnanovoandGitHub f48a6696cf fix(web): reveal directory tails and support touch scrolling (#2237) 2026-10-29 21:22:31 +08:00
weishu 01cdec1179 feat(android): share files and text to sessions 2026-10-29 21:17:19 +08:00
weishu 730dfcb22e Release version 0.33.2 2026-10-29 13:24:52 +08:00
weishu 4b3f92ea84 fix(android): keep session ordering timestamps consistently Long 2026-10-29 13:01:38 +08:00
AnanovoandGitHub bb51684339 fix(web): hide reorder handles for single composer attachments (#2235) 2026-10-29 12:49:12 +08:00
weishu 01dabad204 fix(sessions): stabilize ordering by latest user input
Persist and backfill the last user-message timestamp, and use it for deterministic session ordering across web and native clients while preserving manual pins.

Remove the fixed-order setting and guard ordering against transcript echoes, replayed messages, and stale REST/SSE updates. Add regression coverage and regenerate the SSE fixture.
2026-10-29 12:47:52 +08:00
weishu 5fd1179d2d fix(hub): repair v24 migration test fixture 2026-10-29 11:20:01 +08:00
6ccde08548 feat(android): render settings pickers as expressive choice dialogs (#2227)
* feat(android): anchor the Material type scale on the shared reading tokens

MaterialTheme was built with only a colorScheme, leaving HapiTypography's
body/code/caption tokens an island that never influenced Material
typography. Derive the app-wide Material type scale from those tokens so
Material widgets and direct token consumers share one type system: the
body token drives bodyLarge (the chat reading size) and the caption token
drives bodySmall (the metadata size), while every other slot keeps its
Material platform default so existing MaterialTheme.typography call sites
stay pixel-identical. Only size and leading come from the tokens; weight,
tracking and family stay Material's. HapiTheme now accepts a typography
argument (defaulting to the shared scale) and passes it through, and the
monospace code token stays a direct-consumer token because Material has
no code slot.

Co-Authored-By: Claude Code <noreply@anthropic.com>
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>

* feat(android): render settings pickers as expressive choice dialogs

The theme and language pickers were the last bare AlertDialog + radio-row
surfaces in the app: plain stacked radio buttons with no visual anchor for
what each option means, and two focus targets per row (the row and the
RadioButton each carried their own click handler). Give small exclusive
pickers a shared expressive form: each option renders as a full-width
choice card whose selected state is a secondaryContainer fill plus a
trailing check, and options can carry a fixed preview swatch.

The theme modes show two-tone palette chips — follow-system spans the
light and dark palettes, OLED is outlined pure black, dark uses the dark
canvas rather than black so the two stay distinguishable at a glance.
Swatch colors are fixed values depicting the mode they name, not derived
from the active scheme, mirroring how the web color-theme cards hardcode
their previews: a "light" chip must stay light even inside a dark dialog.

Rows keep radio semantics through Modifier.selectable with Role.RadioButton
inside a selectableGroup, so TalkBack still announces the standard
"selected, n of m" picker phrasing while each option becomes a single
focus target. Tapping an option still applies it immediately. Language
options skip the swatch slot; the component stays generic over the value.

Co-Authored-By: Claude Code <noreply@anthropic.com>
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-29 10:57:55 +08:00
2a75984b75 feat(cli): add peer delivery modes for CLI and MCP (#2197)
* feat(cli): select queue or steer for peer messages

* fix(peer): respect per-agent steering availability

---------

Co-authored-by: NightWatcher <hitszzzf@gmail.com>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-29 10:55:58 +08:00
weishu a6cf8fd078 Release version 0.33.1 2026-10-29 10:13:33 +08:00
weishu 47aad06866 fix(release): upgrade Bun and reject mismatched platform binaries
Pin the build toolchain to Bun 1.4.4 so CI includes the darwin-arm64 signing fix required by macOS 27.

Reject stale optional platform packages before launching, document clean reinstall recovery, and add launcher regression coverage.
2026-10-29 09:45:46 +08:00
weishu ee273464f4 Release version 0.33.0 2026-10-29 08:51:11 +08:00
weishuandClaude Opus 5.5 23122ccbc0 feat(hub): persist agent turns and report them through hapi usage
Whether a session's agent is thinking lived only in the session cache,
so once a turn ended nothing could say how long the agent had worked,
and a hub restart forgot even the turn in progress. Tools that track
time (Weft, for one) had no way to see when agents on other machines
were busy.

Schema v28 adds session_turns: a row opens when a session starts
thinking (a prompt queued through the hub, the agent reporting it,
or a patch) and closes when it stops, the session ends, or its
keepalives expire, at the last keepalive. Rows are only written on
those transitions, never per keepalive. Turns move with a session
when it is merged away.

At startup the hub closes turns left open by the previous process at
the session's newest message. If the agent carries on thinking within
10 minutes of the restart, the same turn continues, so the work between
that message and the restart isn't lost. The deadline counts from the
restart, and a keepalive or patch reporting idle, the session ending,
or a new prompt ends the chance: those start a new turn.

GET /api/activity returns, for the caller's namespace, each session's
turns in a window (default 24 h, at most 31 days) with what identifies
it (flavor, agent session id, machine, path, title) and the permission
prompts and questions it waited on, taken from agent_state.

`hapi usage [--since] [--until] [--json]` prints a summary or a
hapi.activity.v1 report for other tools, with distinct exit codes for
bad arguments (2), missing or refused credentials (3) and a hub too
old to answer (4). The summary counts only the part of each turn
inside the window and subtracts overlapping waits once. It reuses the
peer commands' token exchange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:48:06 +08:00
b4be7c2d93 fix(cursor): stop acknowledging soft steers that an abort cancels (#2229)
Abort sends session/cancel, and ACP answers the pending soft-steer
session/prompt with stopReason "cancelled". The completion handler
ignored the stopReason and acknowledged the row as steered, so a
message Cursor may never have run was shown as delivered.

Surface the stopReason from beginSoftSteerPrompt. On "cancelled" the
row is held as indeterminate (the same path a transport failure takes),
because Cursor may or may not have folded it into the cancelled turn:
the user resolves it with Retry or Cancel instead of a silent claim or
an automatic re-run after Stop.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:07:28 +08:00
Junmo KimandGitHub ba7494c5b5 fix(web): show Codex cached input percentage in response metadata (#2230) 2026-10-29 08:07:11 +08:00
weishu 0161b45cb3 fix(ios): isolate pairing test state and account for safe areas 2026-10-28 21:06:55 +08:00
weishu 25ab6bb842 fix(ios): handle pairing item provider pastes explicitly 2026-10-28 20:25:17 +08:00
weishuandClaude Opus 5.5 243466da04 fix(ios): keep transcript rows in sync with environment changes on iOS 27
On iOS 27, SwiftUI re-runs a representable's update only when an
environment value read during it changes; even @Environment(\.self) is
tracked per key. AnchoredTranscriptList copied context.environment
without reading keys, and compared metrics later in a drain that is
often deferred. A theme switch or a system text size change therefore
never reached the hosted rows: text kept its old size and colors.

updateUIViewController now reads every runtime-changing value the rows
follow (Dynamic Type, bold text, typography, locale, direction, color
scheme, theme, link action, enabled state). Rows still receive the
complete environment snapshot.

The tests drive Dynamic Type through UIKit traits, as the system does.
On iOS 27 a SwiftUI dynamicTypeSize override does not reach a UIKit
representable's environment, and the app never sets one above the
list. A new test checks that theme-only and text-size-only changes
reach visible rows without reconfiguring cells; it fails without the
fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:25:18 +08:00
weishuandClaude Opus 5.5 4b7a1cff90 ci(ios): build and test with Xcode 27
The iPhone Duo layout uses UIHingeInteraction, which only exists in
the iOS 27.1 SDK. CI ran Xcode 16.4 (iOS 18.5 SDK) on macos-15, so the
app build failed: #available is a runtime check and cannot hide a
symbol the SDK lacks. ChatView's iOS 26/27 modifiers would have failed
next.

Both jobs now run on the xcode-27 runner image (Xcode 27.1 by default),
the toolchain releases are built with. The READMEs state Xcode 27.1 as
the minimum.

Swift 6.4 also timed out type-checking a long `+` chain in
MultipartEncoderTests, so `swift test` failed to build; the expected
body is now joined from an array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:25:18 +08:00
3ndetzandGitHub e733dffe10 Prevent duplicate replay fanout and bound stalled SSE writes (#2223)
* Prevent replay fanout and bound stalled SSE writes

* Handle streams aborted during replay

* Preserve repeated consumption acknowledgements

* Bound SSE queues by size while allowing short bursts
2026-10-28 18:00:43 +08:00
weishuandClaude Opus 5.5 132d256171 feat(ios): fork, rewind and export conversations
The hub's fork, rewind and export routes had no iOS UI. The chat's
menu now offers:

- Fork Conversation: when the agent advertises forkCurrent, the
  session is live and no Codex turn is running (the web gate). The new
  session opens in place of this chat.
- Rewind or Fork…: a sheet listing loaded user messages that have a
  native point in metadata.conversationHistoryPoints. Each row offers a
  confirmed Fork from Here or Rewind to Here, gated like web: forks
  need an idle turn except on Codex, and rewinds always need one. The
  actions live in a sheet, not on the transcript bubbles, because those
  keep text selection and cached row heights. After a rewind the tail
  resyncs with a history reset. An ambiguous native boundary (409
  ambiguous_native_boundary_fork_safe) offers a fork from the same
  point instead.
- Export Conversation…: saves the hub's lossless JSON payload through
  the Files save panel, using the web download's filename. A large
  export asks before retrying with force; a 413 explains the limit.
  Markdown export stays web-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 423c5f51df feat(ios): schedule sends from the composer
iOS showed scheduled messages but could not create them. A clock
button now sits next to the composer's attachment button. It offers
the web presets (in 5 min, 30 min, 1 h, 4 h) or a specific time up to 7
days ahead. A chip above the input shows the pending time; its × sends
normally again.

Presets resolve when the user taps Send, so "in 5 minutes" counts from
the send. Validation follows the web: a 30 s grace period for past
times and a 7-day maximum.

A scheduled send is text-only and queued. The hub rejects scheduledAt
with attachments or steer, so the clock is disabled while attachments
are present and the steer menu is hidden. The schedule clears once the
send dispatches. Sending reuses the existing optimistic path, and a
retry keeps the row's scheduledAt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 f7722fcd65 feat(ios): suggest slash commands in the composer
Typing / in the iOS composer offered nothing; web and Android show the
session's commands. While the input is a lone /token, a list above the
card shows matching commands. A tap inserts "/name " so the user can
add arguments.

As on Android, the sources are metadata.slashCommands plus the
/slash-commands RPC. The RPC is fetched once, when the input first
enters slash mode, and its entries override bare names. A failed fetch
retries on the next /. There is no copy of the shared built-in table,
so the CLI stays the source of truth. Ranking follows web: exact,
prefix, contains, then near-miss typos. The list is hidden in
Scratchlist mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 64e062fb84 feat(ios): search sessions, unread-only lens and mark all read
The iOS session list could only filter by machine. Web also has text
search, an unread-only filter and Mark All as Read.

- Search: a .searchable field ranks matches with the web relevance
  rules, ported from sessionListSearch.ts via Android as
  SessionListSearch. It searches title, worktree, summary, agent,
  machine, path and id; every word must match, with */? wildcards.
  Results keep the global/project pin sections, so the Pinned header
  still applies.
- Filters menu:
  - It now holds an Unread Only toggle that keeps the iPad's selected
    chat listed.
  - The machine section appears only with two or more machines, as
    before; the menu itself shows whenever the hub has sessions.
  - Mark All as Read advances the local last-seen watermarks after a
    confirmation, as on web.
- Empty list: an empty search or filter result gets its own empty state
  instead of "No sessions yet".

getSessionTitle moves into HapiKit so search and the rows share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 1efcc3183d feat(ios): switch models for every agent in session settings
The settings sheet only had model pickers for Claude and Codex; other
agents could not change models after creation. Live sessions now load
/sessions/:id/<flavor>-models. Agy uses its machine catalog, since it
has no session route. As on web, grok and copilot skip
terminal-controlled sessions.

Rows follow web getModelOptionsForFlavor:
- Copilot, dsh, grok and kimi offer a Default or Auto row that clears
  the model.
- Cursor's Auto is the explicit 'auto' wire id.
- Opencode, agy and pi only switch between listed models.
- An unlisted current model stays visible.

Pi resolves provider/modelId from metadata.piSelectedModel and switches
with a {provider, modelId} reference.

Efforts:
- Grok and OpenCode options come from their *-reasoning-effort-options
  routes and reload after a model switch. OpenCode options that still
  describe the previous model stay hidden.
- Pi shows the current model's thinking levels.

The sheet's load-failure flag now covers every catalog, not just Codex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 d01f03eef7 feat(ios): pick models for every agent when creating a session
The create form only offered models for Claude and Codex, so every
other agent spawned on its default model. The form now loads the
catalogs web uses. Agy, cursor, dsh and pi read
/machines/:id/<flavor>-models. Copilot, grok, kimi and opencode probe
the same route with cwd once the directory exists; the view refetches
when the typed path turns out to exist.

Option rows follow the web builders:
- Copilot and cursor label the default row "Auto".
- Kimi and pi prefix the provider; pi values are provider/modelId.
- OpenCode marks the agent's own default.
- Cursor shows a flat list of ACP ids, without the variant drill-down.

Grok efforts and pi thinking levels follow the selected model, and an
OpenCode variant becomes the reasoning effort. Grok's auto permission
mode is hidden when the probe reports it unsupported. A restored
selection the catalog no longer offers falls back to Default, and
Create waits while a non-default selection is still being validated.

The spawn body sends model for all of these agents, and cursor always
sends it, with 'auto' as the default. Grok and pi send effort; opencode
sends modelReasoningEffort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 b534a96f2f feat(ios): open notification taps on the owning hub
Push payloads do not name a hub, so a tap always opened the session
against the active hub. With several paired hubs, a session from
another hub then failed to load until the user switched hubs by hand.

When the active hub's list does not know the session, the tap now
looks it up with GET /api/sessions/:id. Hubs are tried in the same
active-first order, and 404/403 have the same meaning, as for the
notification actions. If another hub owns the session, the app
switches to it first; the new home screen then opens the chat. A miss
or an unreachable hub opens against the active hub, as before.
Single-hub installs skip the lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 5942b427a9 feat(ios): rename, delete and reopen sessions
The API wrappers existed but iOS had no UI for these actions, while
web and Android did. The session row's context menu and the chat's …
menu now offer Rename for every session. Active sessions keep Archive.
Inactive sessions get Reopen and a confirmed Delete instead of
Archive, which the hub rejects with 409 for them.

The store applies renames and deletes optimistically. A failed rename
refetches server state; a failed delete restores the row and the
cached detail. Reopen marks the returned session active right away.
When reopen returns a superseding id, the chat seeds the new window,
moves the draft and navigates to it, as send-resume already does. A
reopen from the list also moves the draft and opens the session. A
422 shows the hub's message with the missing fields.

On iPhone, an authoritative removal (delete, or archive elsewhere) now
pops that chat, matching the iPad split, which already clears the
selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
64f464190f feat(android): refactor settings into a hub with category subpages (#2218)
Replace the single-column settings screen with a category directory
whose rows come from a declarative catalog (SettingsHubCatalog: one
entry = route + title + summary + icon + visibility lambda), so adding
a category later (Display, Chat, Voice, Machines) is a list element,
not layout surgery. Mirrors the web settings IA at the category level.

- General subpage (settings/general) inherits the uncategorized
  sections: Appearance (theme + Material You), Language, About
  (versions + hub health). Zero behavior change per item.
- The owner-only Insights section dissolves into hub-level rows:
  Usage/Storage render only for the owner (same ns=default gate) and
  route to their existing dashboards, matching the web's owner-only
  nav categories (SettingsNav OWNER_ONLY_CATEGORIES).
- Flat settings/... route pattern with per-hub ViewModelHolder, same
  as the existing usage/storage precedents; back from a subpage lands
  on the hub. The hub's holder passes probeHealth=false so opening the
  hub itself fires no /health request; the General holder keeps it.
- Previews: hub owner/non-owner gating + General body. JVM tests pin
  catalog order, the owner gate, route uniqueness, and row summaries.
- Strings: settings_category_general(+_summary) in en/ru/zh; relocate
  three zh keys (chat_tool_resolved, chat_plan_implement,
  chat_plan_continue) that had drifted to the file tail and broke the
  three-language key order (KEYSYNC) — pure moves, values untouched.


via [HAPI](https://hapi.run)

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-28 15:20:53 +08:00
weishuandGitHub 7c715843bb ci: merge owner-approved PRs automatically after checks pass (#2217)
* ci: merge owner-approved PRs automatically after checks pass

Approval by the repo owner on a PR should be the only manual step before
merge. The auto-merge workflow squash-merges open PRs the owner approved
at their current head once every required check's latest run on that
head has passed (test, integration, windows-codex-mcp).

Fork PR review events receive no secrets, so a no-op signal workflow
runs on pull_request_review and its completion triggers the privileged
workflow, which runs default-branch code. Merges use AUTO_MERGE_TOKEN, a
user token, so the resulting pushes to main trigger push workflows; the
workflow refuses to merge if that secret is missing.

* ci: load the auto-merge script from main on every trigger

workflow_dispatch checks out the ref chosen by the caller, so a manual
run from a topic branch would execute unmerged merge logic with the
owner's token. Pin the checkout to main.
2026-10-28 13:02:05 +08:00
e0b4aac78c fix(web): do not claim Agents are missing when no machine is connected (#1978)
New Session treated a disabled availability query (empty agents, not
loading) as "this machine has no CLIs." That painted the same string as
a real empty probe, next to "No machines available."

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 12:18:57 +08:00
ShinnandGitHub d730996e7b fix(web): dismiss composer settings before Escape aborts a turn (#1957)
* fix(web): dismiss composer settings before Escape aborts a turn

* fix(web): preserve IME Escape in composer settings
2026-10-28 12:18:40 +08:00
bce61e5048 hub: extend offline-detection grace for thinking sessions (#1877)
A session's keepalive is emitted over a volatile socket channel, so any
transport outage longer than the 30s expiry window flips the session
offline even though the CLI process is alive mid-turn and its outbound
messages are buffered for delivery on reconnect — the session comes back
the instant the socket reconnects, which surfaces to clients as a
spurious "offline during thinking" state.

Give sessions with thinking=true a 5-minute expiry grace instead. The
grace is bounded, resets to the plain 30s timeout as soon as a keepalive
reports thinking=false, and genuinely dead processes still expire on the
extended timeout.

via [HAPI](https://hapi.run)

Co-authored-by: HAPI <noreply@hapi.run>
2026-10-28 12:18:22 +08:00
AnanovoandGitHub 8f1318ed8e fix(web): allow directory collapse during session search (#1797)
* fix(web): allow directory collapse during session search

* fix(web): preserve directory collapse after filtering
2026-10-28 12:17:59 +08:00
Junmo KimandGitHub 2a94782412 fix(web): make composer session mention pills ellipsize (#1678) 2026-10-28 12:17:41 +08:00
a23d78f416 fix(web): avoid session misclicks during live reordering (#1656)
Co-authored-by: NPUlrk <21106497+NPUlrk@users.noreply.github.com>
2026-10-28 12:17:28 +08:00
Junmo KimandGitHub a4a5779829 fix: render opencode edit/write/read tool calls through the shared views (#1644)
* feat(cli): add diff-shape tool input canonicalization helper

* fix(cli): canonicalize ACP diff-shaped tool inputs for the Edit/Write views

* fix(opencode): normalize local-mode edit/write tool parts to the diff view shapes

* fix(opencode): preserve replaceAll and fix id-less hook pairing

- canonicalizeDiffToolInput now carries OpenCode's optional replaceAll through
  to the canonical Edit input as replace_all, so persisted history keeps
  whether an edit replaced one occurrence or all.
- opencodeLocalLauncher computes the execute-hook pairing signature from the
  raw name/input BEFORE canonicalizing, so an id-less 'before' with empty or
  partial args shares a queue key with the full 'after'.

* fix(opencode): preserve snake_case replace_all in edit canonicalization

canonicalizeDiffToolInput accepts already-canonical snake_case edit inputs
(file_path/old_string/new_string), but only read camelCase replaceAll. Read
replace_all too so the execution argument survives every call site.

* fix(opencode): upgrade partial local edit/write calls when full args arrive

A partial native before ({filePath}) was emitted as-is and recorded as sent,
so the later canonical full input was deduped and the web Edit/Write view never
received the complete arguments. Track the emitted input per callId and, when
a partial native edit/write call is superseded by a canonical full input, emit
a replacement tool-call (same callId) before the result. Applies to both the
message.part.updated and tool.execute.* local event paths.

* fix(opencode): release completed tool inputs from upgrade tracking

emittedToolInputs retained the full emitted Write content and Edit old/new
strings for the whole launcher lifetime. Once a call is completed
(sentToolResults), later upgrades are already blocked, so drop the stored
payload in both result paths. Add a lifecycle test that a stale part for a
completed callId neither re-emits nor emits a duplicate result.

* style(opencode): restore 4-space indentation on local launcher tracking decls

The per-session tracking declarations (sentTextParts, sentToolCalls,
sentToolResults, emittedToolInputs) were flush-left inside opencodeLocalLauncher,
violating the repo's 4-space indentation rule.

* fix(opencode): gate diff canonicalization on edit/write semantics

canonicalizeDiffToolInput now takes the tool's semantic name/kind and only
normalizes known edit/write aliases (edit, write, write_file, replace, modify,
file_edit). The Edit shape (path + both old/new) is unambiguous on its own, but
Write is just {path, content} — a shape many non-edit MCP tools also accept —
so without the gate those tools were renamed to Write and their extra args
dropped. OpenCode maps both edit and write to kind 'edit', which stays in the
allow-list, so the native-shape cases keep working. ACP callers pass the update
kind (falling back to the derived name); local callers pass the tool name.

* fix(opencode): re-emit canonical edit/write when the input changes

shouldEmit only handled the null->canonical transition, so a shape-complete
Write/Edit that already canonicalizes (e.g. {content: ""}) was suppressed
when the final content arrived, leaving the web diff with stale input. Emit on
first usable input and re-emit whenever the canonical input value changes,
across both message.part.updated and tool.execute.* paths.

* test(opencode): cover changed-input re-emit on the message.part.updated path

The execute-hook path already had a regression for canonical inputs changing
from empty to final content; add the matching case for message.part.updated so
both local emit paths are covered.

* fix(web): render opencode native read output as a file content code block

OpenCode's lowercase read tool fell through to the generic markdown
result view, collapsing the whole file into a soft-wrapped paragraph
and dropping the <path>/<type>/<content> markup. Register the read
alias, unwrap the opencode payload (including the trailing
"(End of file ...)" trailer), parse its 1-indexed line numbering into
the code block gutter, and resolve the camelCase filePath key in the
card title.

* fix(web): only unwrap complete opencode read envelopes

Anchor the envelope match to the full <path>/<type>file/<content>
structure so plain Read results whose file text contains a literal
<content> element are left untouched, capture the body greedily so an
embedded </content> survives, and stop trimming real trailing
whitespace.
2026-10-28 12:17:14 +08:00
Junmo KimandGitHub 83088aae73 fix(agy): report the failure reason instead of echoing the answer (#1642)
A failed `result` envelope still carries the turn's answer in `response` —
agy decodes that field from the complete buffer regardless of status. The
driver used it as the failure text, so a failed turn delivered the whole
answer a second time as an error event, right under the streamed copy.

Read the reason from `error` instead, which is what agy actually reports
there (e.g. "timeout waiting for response"), and keep the existing
`agy turn failed: <status>` fallback when the envelope carries none.
2026-10-28 12:17:00 +08:00
AnanovoandGitHub 8d94a745b5 fix(web): prevent session navigation on right-click (#1636) 2026-10-28 12:16:47 +08:00
AnanovoandGitHub 21b0ff30f3 fix(web): add exact PWA notification icon sizes (#1625) 2026-10-28 12:16:25 +08:00
AnanovoandGitHub aa0172e6aa fix(web): align fork and rewind confirmations (#1603) 2026-10-28 12:16:12 +08:00
AnanovoandGitHub 7765fd3378 fix(web): make long file errors expandable (#1422)
* fix(web): make long file errors expandable

* fix(web): count graphemes in expandable errors

* fix(web): reset expanded errors when messages change

* fix(web): reset expansion when error text changes

* fix(web): avoid duplicate file error alerts
2026-10-28 12:15:58 +08:00
AnanovoandGitHub f6bc583471 feat(web): open a session in a separate window (#2192)
* feat(web): open a session in a separate window

* test(web): cover session sidebar restore control

* fix(web): preserve hidden sidebar across session navigation

* fix(web): retain hidden sidebar on session routes
2026-10-28 12:15:22 +08:00
AnanovoandGitHub 009e6d7799 feat(codex): allow historical forks while a turn is running (#2185)
* feat(codex): allow historical forks while a turn is running

* fix(web): fork user-only completed turns during generation
2026-10-28 12:15:11 +08:00
AnanovoandGitHub 575e995480 feat(hub): generate fallback titles after the first completed turn (#2075)
* feat(hub): generate fallback titles after the first completed turn

* fix(hub): recognize batched first-turn title prompts

* fix(hub): track heartbeat-only first turns for title fallback

* fix(hub): preserve title turn identity across restarts
2026-10-28 12:14:59 +08:00
AnanovoandGitHub 577ab7546b feat(sessions): add explicit per-session title locks (#2072)
* feat(sessions): add explicit per-session title locks

* fix(sessions): preserve title locks across rename entry points
2026-10-28 12:14:48 +08:00
Junmo KimandGitHub 328ddb0ee3 feat(cli): show Claude compaction as a summary card with token delta (#1691)
* feat(cli): report claude compact completion with token delta

Replace the plain 'Compaction completed' line with a pi-style
'📦 Compacted (<before> → <after> tokens)' fallback built from the
system/compact_boundary metadata, and prefix the start notice with 📦.
The SDK stream carries no summary body (measured), so the delta line is
the terminal state of the completion event.

* fix(cli): hide local-command tag user messages from chat

Claude Code echoes slash-command bookkeeping back over the SDK as plain
user-role messages wrapped in <local-command-*> tags. Flag them isMeta so
the existing downstream filters drop them instead of leaking raw stdout.

* feat(web): collapse compact summary card by default

Keep the header (title, token delta, timestamp) always visible and hide
the summary body behind a chevron toggle. Toggle state is component-local
and non-persistent.

* feat(cli): promote claude compact summary from the local transcript

* fix(cli): don't double-render the manual /compact boundary as a status line

* fix(cli): detect later-turn /compact and ignore stale transcript summaries

* fix(cli): establish the compact transcript baseline before the result can race it

* fix(cli): narrow the local-command meta filter to the compact stdout echo

* fix(cli): range-read only the transcript tail when polling for the compact summary

* fix(cli): complete the transcript tail read before parsing the compact summary

* fix(cli): refresh the context bar with post-compaction tokens after /compact

* fix(cli): scope the Compacted stdout suppression to the active compact turn

* fix(cli): make compact lifecycle ownership race-safe

* fix(cli): publish compact outcome before accepting the next turn

* fix(cli): cancel compact completion with its failed stream attempt

* fix(cli): propagate deferred compact completion failures

* refactor(cli): coordinate compact completion in the response loop

* fix(cli): correlate compact completion with its stream signal

* fix(cli): commit compact ownership when its result arrives

* refactor(cli): name the manual compact stream suppression predicates

* fix(cli): suppress the compact echo even when hook output is appended

* fix(cli): carry the resolved context window into the compact usage refresh
2026-10-28 12:14:34 +08:00
Junmo KimandGitHub ae147e85db feat(opencode): import local session history (#1677)
* feat(cli): list local OpenCode sessions over machine RPC

* feat(hub): import OpenCode sessions into HAPI

* feat(web): import OpenCode history from new-session picker

* fix(cli): derive OpenCode imported message ids from part ids

Random ids made every scan differ, so delta classification flagged
unchanged transcripts as diverged on re-import.

* fix(cli): surface unreadable OpenCode db as rpc error

A locked or unopenable opencode.db now fails the listing instead of
reporting an empty success; summaries normalize timestamps and the web
import toast counts only created or actually-appended sessions.

* fix(hub): fail closed on opencode re-import past live HAPI continuation

Imported entries must remain a strict prefix of the native transcript.
Insertions ahead of the imported boundary or new native messages after
the session continued in HAPI now fail closed instead of appending
duplicated or reordered history.

* fix(web,hub): apply selected launch config when importing OpenCode history

Pass the model, reasoning effort, and permission mode chosen in the
new-session picker through the import request and persist them on the
imported HAPI session via applySessionConfig, so reopen launches with
the visible selection instead of stale or default values.

* fix(hub,cli): preserve explicit launch resets and skip streaming parts on import

- Launch config keys are applied by presence, so explicit null/default
  selections from the picker must reach applySessionConfig to clear
  stale model, effort, or permission values on re-import. Requests that
  omit the fields entirely still leave persisted config untouched.
- Assistant text parts are mutable while a response streams. Mirror the
  live scanner gate and import only finished (time.end set), synthetic,
  or user text so an in-progress row can never be stored and later
  finalized as transcript_diverged.

* test(hub): fix launch-config reset assertions to satisfy strict types

* fix(cli): fail opencode transcript reads on query errors instead of partial snapshots

Swallowed SQLite errors could return a truncated message list that the
hub later rejects as transcript_diverged with no repair path. Full
transcript reads now propagate query failures; summary lastUserMessage
extraction stays resilient per message.

* test(cli): cover unreadable-part-table transcript read stays non-partial

* fix(hub): scope launch-config failures to the per-session import result

A thrown applySessionConfig call used to escape the bulk handler as an
unstructured 500 even though transcripts were already persisted. Catch
it per session and report it as a config_failed result so remaining
sessions in a bulk request still import.

Also cover the mid-transcript part-query failure with an injected
opener that rejects instead of returning a holey transcript.

* fix(cli): read opencode transcripts inside a deferred snapshot transaction

Messages and their parts were read in separate statements, so a
concurrent OpenCode write could leave a hole in the imported sequence
that positional re-import checks then reject forever. Wrap each full
transcript read in BEGIN DEFERRED/COMMIT with rollback on failure.

* fix(cli): follow machine path policy for OpenCode imports

* fix(web): use unified permission mode for OpenCode imports

* fix(opencode): harden session import consistency

* fix(hub): preserve metadata on rejected OpenCode imports

* fix(hub): refresh OpenCode import state under lock

* fix(hub): resolve OpenCode imports after locking

* fix(opencode): bind imported resume to source machine
2026-10-28 12:14:18 +08:00