Commit Graph
64 Commits
Author SHA1 Message Date
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
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
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 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
ffd950a220 fix(codex): drop safe-yolo from Codex permission modes (#2183)
* fix(codex): drop safe-yolo from Codex permission modes

Every Codex session runs on the native shared runtime (runCodex is
runSharedCodex), which rejects safe-yolo. The mode stayed in
CODEX_PERMISSION_MODES and six filters hid it again in the web picker,
composer, hub permission-mode route, hapi set-mode and both native
apps. API callers that validate against the flavor catalog (e.g. a
spawn with permissionMode: 'safe-yolo') still passed it through and
only failed inside the runner.

Remove it from the Codex catalog (shared, iOS, Android, fixture) and
delete the filters. autoReview is the native replacement. Safe Yolo
stays for Gemini, Kimi and Copilot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ios): Codex option list now has four modes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:55 +08:00
fe2fbb881d fix: list Claude background tasks from the snapshot instead of guessing (#2177)
* fix: list Claude background tasks from the snapshot instead of guessing

The status panel only had a count from `background_tasks_changed` and
reconstructed commands by regex-matching Bash results in loaded chat
history, so tasks whose start message was unloaded, auto-backgrounded
("moved to the background (ID: …)"), or non-Bash showed
"N running · exact command unavailable".

The hub now keeps the snapshot's tasks (id, type, description) on the
session alongside the count. The web panel lists them directly, using
the exact task ID to look up the Bash command when loaded and the
snapshot description otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): only attribute a Bash command from its background launch result

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native): accept backgroundTasks in strict session patch decoders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:41 +08:00
878ca2058c feat(codex): expose Approve for me as a permission mode (#2016) (#2090)
* feat(codex): expose Approve for me as a permission mode (#2016)

Add autoReview to the Codex permission catalog. It keeps the
workspace-write sandbox with on-request approvals and routes escalations
to Codex's native reviewer (approvals_reviewer = "auto_review"), the same
config as `hapi codex --approve-for-me`. It works at launch and mid-session
via thread/settings/update; explicit modes now reset the reviewer to user,
and sessions report autoReview from the native reviewer setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(native): expect Auto-review in the Codex permission lists (#2016)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(modes): label autoReview as Auto, matching Claude's auto mode (#2016)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:59:47 +08:00
d3d8bf2620 feat(ios): add Russian locale (#1964)
* feat(ios): add Russian locale

Add Russian translations to Localizable.xcstrings for all 591 catalog
entries (reusing matching Android translations and translating the rest);
zh-Hans and format placeholders are preserved.

* fix(ios): make Russian selectable and use correct plural forms

- add AppLanguage.russian ("ru"), its display name, the AppleLanguages
  override, and register ru in the Xcode project's knownRegions so the
  language picker can actually select Russian
- add Russian one/few/many/other plural variations for %lld sessions,
  %lld attachments and %@, %lld sessions
- use count-agnostic phrasing for the other dynamic counts (tools, edits,
  lines, chars, findings, notes, queued messages, questions, ...)
- correct Input/Output to Ввод/Вывод
- cover the new language case in ThemePrefsTests

* fix(ios): use Russian plural forms for the remaining count labels

* fix(ios): pass the step count to the plural localization

* fix(ios): pass tool counts to the plural localization

* fix(ios): keep the singular English tool-group labels

---------

Co-authored-by: mrudy <mrudy@local>
2026-10-05 17:12:57 +08:00
weishu 16ba8fdaa9 feat(ios): add adaptive iPad navigation 2026-09-14 18:10:59 +08:00
weishu 5280a3b165 fix(chat): dismiss Codex plan actions when continuing planning 2026-09-14 14:30:08 +08:00
weishu ee46cd6fea feat(ios): integrate scratchlist with the chat composer 2026-09-14 11:55:46 +08:00
AnanovoandGitHub b41b9293e9 fix(web): render cold sessions from a small initial page (#1809)
* fix(web): speed up cold session first paint

* test(web): update pagination fixtures for cold page

* test(web): wait for cold page before loading history

* feat(native): match cold session pagination

* test(native): cover cached re-entry page size
2026-09-14 09:07:14 +08:00
weishu 40b6ac462c fix: align directory browsing and iOS session creation 2026-09-14 08:44:02 +08:00
weishu 93e9c5ac65 fix: restore Codex titles and iOS collaboration controls 2026-09-14 08:44:02 +08:00
weishu 0a9f15a9a7 fix: align request_user_input other answers across clients 2026-09-13 19:48:14 +08:00
weishu a6880fe592 fix(native): add Codex plan proposal actions
Add current-proposal decoding, dedicated implementation requests and composer-only continuation on iOS and Android. Keep pending/errors across row recycling, reject stale actions and prevent duplicate submissions.

Include native regression tests and localized plan controls.
2026-09-13 19:48:14 +08:00
weishu e0db9c6815 fix(notifications): distinguish user questions from permission requests 2026-09-13 12:41:38 +08:00
weishu 2e37419a7d fix(ios): reduce back-to-latest button sensitivity 2026-09-13 11:46:10 +08:00
weishu 0c4abcb3d1 feat(codex): share sessions across terminal and web
Use one native app-server for terminal, Web and phone clients while retaining the existing CLI and Runner lifecycle.

Synchronize native queues, permissions, question history and steering state; preserve explicit permission precedence and per-turn usage models. Resume inactive clear commands through Runner and reject independent child cold resumes.

Add shared-runtime regression tests, generated protocol fixtures and lifecycle documentation.
2026-09-12 10:59:17 +08:00
AnanovoandGitHub ebe2c6bd9c fix(ios): isolate typography spacing test on main actor (#1812) 2026-09-11 14:19:13 +08:00
weishu 10042f79c6 feat(ios): refine chat typography and reading layout
Use unified 16pt Dynamic Type body text, clearer Markdown spacing, and a shared reading column. Preserve transcript anchors across typography changes and bound tool command previews to two lines. Add layout and typography regression tests.
2026-09-10 16:22:36 +08:00
weishu c2e3d16b7e feat(native): improve anchored chat scrolling and history loading
Add viewport-driven paging with layout acknowledgements, bounded retries, cancellation gates, and epoch-safe history retention.

Preserve transcript anchors and expansion state, fix tool-group identity collisions, and serialize Android history coordination on Main.

Reduce per-scroll composition and layout work; add native regression tests, CI coverage, and profiling guidance.
2026-09-10 16:22:36 +08:00
weishu d0b7df5f30 fix(native): hide dictation when no transcription provider is configured 2026-09-09 16:45:19 +08:00
Junmo KimandGitHub f27e58741b feat(web,ios,android): let Claude pick a permission mode when creating a session (#1751)
* refactor(web): route create-form permission control through one native-select predicate

Extract usesNativePermissionSelect(flavor) (grok || codex-family, matching
the existing iOS/Android predicate of the same name) and route
PermissionField's select-vs-toggle gate through it instead of an inline
condition. Rename the codex-family-only state codexFamilyPermissionMode to
nativePermissionMode since it now backs a shared predicate, not just the
codex family. Behavior is unchanged: any stale sessionStorage draft written
under the old codexFamilyPermissionMode key has no value under the new key
and falls back to 'default', which only matters within a single browser
tab's lifetime.

* feat(web): let Claude pick a permission mode when creating a session

Claude was the only create-form flavor still on the global HAPI YOLO toggle
while grok and the codex family got the native permission select, so there was
no way to start a session in Plan Mode without creating it first and switching
the mode from the composer. usesNativePermissionSelect now gates the control
for claude as well, and the spawn body carries permissionMode (including
'default') instead of yolo, which is the shape the other native-select flavors
already send.

The stored hapi:newSession:yolo preference is bridged into the select rather
than dropped, but only for the flavors that have actually moved onto it
(LEGACY_YOLO_BRIDGE_AGENTS = codex, claude). copilot, gemini, kimi and opencode
moved earlier and settled on 'default'; re-enabling Yolo for them now would
widen permissions rather than migrate a preference. This narrows the
sessionStorage draft bridge too, which until now fired for the whole codex
family with no allow-list, so their draft restores yield 'default' instead of
'yolo' — same-tab-lifetime state only.

Claude and the codex family share one nativePermissionMode state and their mode
sets do not overlap, so the existing agent-change reset plus the flavor filters
in the draft loader and the stored launch settings are what keep a codex mode
out of a Claude spawn. Adds the regression test that pins it: pick a mode under
codex, switch to Claude, create, assert the payload carries 'default'.

* feat(ios): let Claude pick a permission mode when creating a session

Extend usesNativePermissionSelect to include claude alongside grok and the
codex family, matching the web change. buildSpawnRequest now derives both
yolo and permissionMode from that single predicate instead of two separate
local flags, so claude sends permissionMode (including 'default') and no
longer sends yolo. Unlike web, iOS carries no persistent YOLO preference
across sessions to migrate — the toggle only lives in the in-memory form or
a draft deleted on success — so there is no bridging logic to add here.

* feat(android): let Claude pick a permission mode when creating a session

Extend usesNativePermissionSelect to include claude alongside grok and the
codex family, matching the web and iOS changes. buildSpawnRequest derives
both yolo and permissionMode from that single predicate, so claude sends
permissionMode (including 'default') and no longer sends yolo. Unlike web,
Android has no persistent YOLO preference to migrate: the toggle only lives
in the form draft, which is deleted once a session is created.

The agent-switch test asserted claude renders the YOLO toggle; it now checks
the native select for claude and keeps the toggle assertion on cursor, which
still carries it.
2026-09-09 09:30:42 +08:00
weishu 9048bfa4f1 fix(ios): prevent reconnect notices from shifting chat 2026-09-08 20:05:05 +08:00
weishu 6a81d19509 fix(ios): stabilize session transcript scrolling
Constrain transcript rows to the viewport and replace competing scroll anchors with measured tail corrections. Preserve following through media resizing and reset scroll state when the transcript remounts.

Gate history paging on reader gestures and cover tail-following behavior with seven regression tests.
2026-09-08 13:57:16 +08:00
3873e58496 fix(web): keep streamed reasoning/text block ids stable across snapshot rows (#1741)
* fix(web): keep streamed reasoning/text block ids stable across snapshot rows

Streaming snapshots of one stream (pi/codex reasoning and text) arrive as
separate message rows, and the window store retires older rows as newer
snapshots land. The timeline derived the block id from whichever row was
first seen, so the id (and the threadMessageId built from it) churned on
every snapshot, remounting the rendered reasoning panel mid-stream and
replaying its open animation — the panel visibly flashed/re-rendered on
every snapshot tick.

Derive the block id from the stream id when present (unique per stream,
stable across snapshot rows) so the block is updated in place and the
smooth streaming keeps appending to the previous text. Row-derived ids
remain the fallback for content without a stream id.

Also rerun gen:fixtures to refresh the two golden fixtures affected by
the new id shape.

* fix(ios,android): mirror stream-stable block ids in native chat ports

The native HapiKit (Swift) and protocol (Kotlin) chat pipelines are ports
of the web reducerTimeline and are pinned by the same golden fixtures in
shared/fixtures/chat. After the web-side change to derive streamed
reasoning/text block ids from the stream id, the ports still produced
row-derived ids, so the iOS/Android fixture conformance suites went red
on the two refreshed fixtures.

Apply the same streamId-first id derivation (row-derived fallback kept)
to both ports so all three pipelines project identical block ids.

* fix(web,ios,android): reject blank stream ids as block identity

Blank ('' or whitespace-only) stream ids are not streams per the wire
semantics in shared/src/messages.ts (readReasoningStreamId trims before
accepting). The previous nullish fallback let accepted payloads carrying
blank ids through, so every such row shared one empty block id: the
merge maps collided and assistant-ui occurrence suffixes churned with
list position, reintroducing remounts.

Normalize with a trim guard in all three pipelines (web, HapiKit,
protocol) and add a web regression test covering both empty and
whitespace-only ids.

* fix(ios): use normalized stream id for block construction identity

The blank-id guard was applied to lookup and map insertion but block
construction still read the raw optional, so accepted payloads carrying
blank/whitespace ids produced blocks sharing one blank SwiftUI identity
instead of falling back to row-derived ids (web/Android already used the
normalized local). Hoist the nonBlankStreamId result and reuse it for
lookup, block identity, and insertion in both the text and reasoning
branches.

Also add native coverage for stream identity: stream-id derivation for
text/reasoning plus blank ('' and whitespace-only) fallbacks, which the
golden fixtures do not exercise.

* fix(web): pin blank stream-id identity contract in golden fixtures

Update the two stale fixture descriptions (stream-keyed blocks are now
keyed by the stream id, not the first message) and add a generated
conformance fixture covering empty and whitespace-only codex data.id
values for both reasoning and text: blank ids are not stream identities,
so each payload keeps its own row-derived block id instead of collapsing
onto a shared blank identity. Web, iOS, and Android all run this same
golden fixture.

* feat(hub): make title provider max_tokens and timeout env-tunable

Reasoning models used as title providers (e.g. GLM thinking models) need
more than 64 completion tokens and more than the hardcoded 10s timeout to
emit a title, and the only workaround was patching the compiled binary
after every install.

Expose both knobs via HAPI_TITLE_PROVIDER_MAX_TOKENS and
HAPI_TITLE_PROVIDER_TIMEOUT_MS, following the existing
HAPI_TITLE_SUGGESTION_RATE_LIMIT pattern; defaults are unchanged.

* docs(hub): document title provider max_tokens/timeout env knobs

Add the two new HAPI_TITLE_PROVIDER_* variables to the title-provider
configuration table in the installation guide, and extend the provider
test to cover the timeout abort path (the signal fires and rejects the
in-flight request).

---------

Co-authored-by: HongChenGG <HongChenGG@users.noreply.github.com>
2026-09-06 14:20:19 +08:00
weishu e5a8212f4a feat(session): validate agents and browse workspace directories 2026-08-25 16:12:29 +08:00
SSU-WEI HUANGandGitHub be1ef2a2e4 feat(dsh): integrate DeepSeek Harness through ACP (#1632)
* feat(dsh): add DeepSeek Harness ACP flavor

* fix(dsh): update mobile flavor catalogs

* fix(dsh): keep mobile spawn policy managed

* fix(dsh): keep managed policy and prompt retry

* fix(dsh): suppress unsupported runner policy flags

* fix(dsh): align native managed-policy UX
2026-08-22 12:37:28 +08:00
weishu 8ae402d5b4 test(ios): avoid nested require macros 2026-08-18 21:42:47 +08:00
weishu e289a0a776 feat(ios): push — APNs registration, E2E notification service extension, actions (P3) 2026-08-18 21:07:15 +08:00
weishu 8417c8b33a fix(ios): decode fractional fs-mtime epoch fields (device-feedback parity)
Machine cli mtimes and file/directory modified are Double on the wire
(fs.stat mtimeMs carries sub-ms precision); integer decode threw on real
hub data. Mirrors the Android LenientEpochMs fix; 461 package tests green
in the Linux harness.
2026-08-18 16:27:32 +08:00
weishu 6066f20c20 feat(ios): zh-CN localization + catalog dedupe (A-M5)
Part 1 — Simplified Chinese localization of the iOS app layer:
- ios/Hapi/Resources/Localizable.xcstrings: hand-authored String Catalog
  (395 keys, zh-Hans; en implicit as source). Terminology mined from
  web/src/lib/locales/zh-CN.ts (会话/新建会话/权限模式/允许/拒绝/工作树/
  机器/语音输入/用量/草稿夹 …).
- Mechanical edits only: merged multi-part string concatenations into
  single LocalizedStringKey literals, converted ternary/plain-String user
  copy to String(localized:), switched helper params (detailRow,
  DashboardCard, optionPicker, notice) to LocalizedStringKey.
- LocalizedNoticeMapper (app layer): display-point translation of the 27
  known HapiKit-emitted strings (ChatInteractor notices, dictation errors,
  window-sync warnings, files fallbacks, worktree-name validation) with
  verbatim passthrough for server-originated text; package stays
  language-free.
- Deliberately untranslated (web parity / non-copy): event rows
  (EventPresentation — web renders presentation.ts verbatim), catalog
  option labels (Default/Auto/Sonnet/permission modes — web shows them
  verbatim), tool names, code-like titles (grep(pattern:), MCP:, Skill,
  Task), unit suffixes (B/KB/MB, m/h/d/w), decorative separators.
- Language row wired: AppLanguage gains .system (follow system, new
  default); explicit picks write the AppleLanguages override, Follow
  system removes it; footer notes a relaunch applies it (no supported
  in-place SwiftUI locale swap).
- project.pbxproj: zh-Hans added to knownRegions.

Part 2 — #39 catalog dedupe (HapiProtocol):
- NewSessionCatalogs.claudeModels/claudeEfforts now derive from
  ClaudeModels/ClaudeEfforts (single source shared with ModelCatalog);
  codexReasoningEfforts stays own data (web CODEX_REASONING_EFFORT_OPTIONS
  minus max); effortLabel delegates to ModelCatalog.capitalizedFirst.
- New CatalogTests lock test for the derived option lists.

Gate: bash ios/scripts/linux-test.sh — 459 tests green.
2026-08-18 16:04:03 +08:00
weishu 3510a0f4f1 fix(ios): Linux compile + full package test round — fixtures green locally (A-CI-local)
ios/scripts/linux-test.sh stages HapiKit + shared/fixtures at repo depth
into a persistent tmp dir and runs swift test in swift:6.1-noble; the
manifest drops HapiUI (SwiftUI/swift-markdown/Highlightr) under
#if os(Linux). 458 tests green, including all 48 chat and 11 pagination
golden fixtures.

Fixes that fell out of the first real compile of the blind-written port:
- ChatTypes.CodexReview.wireValue + SummaryPatching legacy id fallback:
  split expressions that exceeded the Swift 6 type-checker budget
- FileEndpointsTests: raw string containing "# terminated the literal
  early (never compiled anywhere) — now ##-delimited
- SSEClient.backoffSleep: Task { try? ... } inferred Task<()?, Never>
- Darwin gates: Security/Keychain behind canImport(Security) (tests use
  InMemoryCredentialStore via CredentialStoring), CryptoKit digest with
  FNV-1a filename fallback, FoundationNetworking imports for URLSession
  types, corelibs URLCache diskPath: initializer, get-only
  waitsForConnectivity, delegate-based SSE transport where
  URLSession.bytes(for:) does not exist
2026-08-18 11:51:57 +08:00
weishu 5773ca2d93 merge: A-M4b iOS scratchlist
# Conflicts:
#	ios/Hapi/Features/Chat/ChatModel.swift
#	ios/Hapi/Features/Chat/ChatView.swift
#	ios/README.md
2026-08-18 11:40:58 +08:00
weishu 6c7746d2b0 feat(ios): scratchlist (A-M4b)
Per-session parked notes mirroring the Android B-M4d feature:

- HapiProtocol Models/ScratchlistApi.swift: wire types for entries CRUD
  (idempotent create via client entryId+createdAt), attachment metadata,
  limits (defaults from shared/src/scratchlistAttachments.ts), upload
  envelope, and the typed error codes (scratchlist_at_cap,
  scratchlist_attachment_too_large, scratchlist_attachment_in_use, ...).
- Endpoints/ScratchlistEndpoints.swift: GET/POST/PUT/DELETE entries,
  limits, base64 upload, raw-bytes attachment fetch, attachment delete.
- Stores/ScratchlistStore.swift: @MainActor @Observable per-session cache
  behind the SessionScratchlistStoring seam - open/release observation,
  16 ms-coalesced refetch on the scratchlistUpdatedAt SSE signal,
  optimistic create/update/delete with surgical entryId reconcile +
  rollback (refresh preserves in-flight optimistic creates), 200-entry cap
  pre-check + hub 409 verdict, uploadsInFlight, cached limits with
  offline defaults, UTF-16 text clamp at 10000.
- Stores/ScratchlistAttachmentGuard.swift: pure Fits/Downscale/Reject
  budget verdicts ported verbatim.
- SessionListStore: onScratchlistInvalidation callback fired when a
  session patch carries scratchlistUpdatedAt (the seam the M4b comment
  reserved); HubSession wires it into the store and injects the store
  into ChatInteractor.
- ChatInteractor (additive tail section): scratchlist store property,
  scratchlistCount badge seam, insertComposerText, parkComposerDraft
  (composer clears only after the hub accepts; at-cap/failed keep the
  draft).
- Features/Scratchlist/: sheet off the chat toolbar's note icon with
  count badge - entry cards (4-line preview, relative age, authed
  thumbnails via NSCache loader, filename chips), edit sheet
  (PhotosPicker -> guard -> JPEG downscale -> upload spinner tile,
  remove, delete/save), full-screen viewer (GeneratedImage pattern),
  per-entry To composer, and Park current draft in the screen header (a
  deliberate placement divergence from Android's composer button - the
  composer UI is owned by the concurrent attachments package). iOS-only
  import step transcodes disallowed-but-decodable rasters (HEIC) to JPEG
  before the guard.
- Tests (36, all transcribed from or mirroring the Android suites): store
  CRUD optimistic/rollback/at-cap/invalidation/upload/limits with
  canonical wire-body asserts + the SessionListStore seam test, 9 guard
  verdicts, 9 interactor park/insert/badge seam tests over a fake store.

Verified on Linux via a dockerized Swift 6.0 scratch copy (FIFO HTTP
performer): full HapiClient build under strict concurrency, 36/36 new
tests green, SessionListStore 17/17 and ChatInteractor 29/29 suites
green; app-side screen model + loader typechecked against stub
frameworks; SwiftUI views and the CG-based import parse-checked (macOS
CI compiles them).
2026-08-18 11:38:37 +08:00
weishu b9c4d092aa merge: A-M4a iOS files/git browser + file viewer
# Conflicts:
#	ios/README.md
2026-08-18 11:33:36 +08:00
weishu a520552ef9 feat(ios): files/git browser + file viewer (A-M4a) 2026-08-18 11:32:14 +08:00
weishu 50a42ecd6d merge: A-M3f iOS composer attachments + dictation
# Conflicts:
#	ios/README.md
2026-08-18 11:28:35 +08:00
weishu 05d063cc33 feat(ios): composer attachments + dictation (A-M3f)
Attachments (Android B-M3f semantics ported verbatim):
- HapiClient/Attachments/AttachmentPolicy — pure plan matrix (>4 MB
  recompressible image -> 2048 px JPEG q85 with .jpg rename, 50 MB hard
  reject, 192 MB image read cap, 512 px q80 previewUrl data-URL thumbs,
  data-URL parse/round-trip).
- HapiClient/Attachments/ComposerAttachments — upload-on-pick tray over an
  AttachmentUploading seam (APIClient conforms): uploading/ready/failed
  chips, retained payload for retry, remove -> best-effort delete,
  mid-upload removal deletes the orphan on completion, consume() ->
  AttachmentMetadata with JPEG data-URL previewUrl, discardAllDetached +
  deinit orphan cleanup (Android onCleared analogue).
- ChatInteractor: tray ownership, unsettled chips refuse the send with a
  notice, attachments-only sends post empty text, optimistic rows carry
  the metadata, appendDictatedText/postNotice/discardAttachments.
- App: AttachmentPreparer (capped security-scoped reads, ImageIO
  downscale/encode with EXIF transform, HEIC-undecodable fallback),
  PhotosPicker multi (videos via FileRepresentation temp files),
  UIImagePickerController camera capture, fileImporter; composer chip row
  (thumb/spinner/tap-to-retry/remove) with attachment-aware send gating;
  user bubbles upgrade chips to off-main-decoded previewUrl thumbnails
  (web-sent attachments included).

Dictation (Android B-M3ce port):
- HapiProtocol/Models/VoiceApi — TranscriptionResponse,
  TranscriptionProvidersResponse, TranscriptionProviderInfo.
- HapiClient/Endpoints/VoiceEndpoints — GET
  /api/voice/transcription/providers + multipart POST
  /api/voice/transcription (file/provider/mode/language, Android part
  order) over MultipartFormData; DictationTranscribing conformance.
- HapiClient/Voice/DictationController — idle/starting/recording/
  transcribing, transcribed/noProvider/error events, provider memoized
  (first standard-capable entry), appendTranscript port.
- App: AVAudioRecorderDictation (m4a/AAC mono 44.1 kHz 96 kbps, session
  activate/deactivate), mic button + recording chip (elapsed + cancel),
  record-permission request via AVAudioApplication.

Info.plist gains NSMicrophoneUsageDescription; the camera string now
covers attachment capture (modern PhotosPicker needs no photo-library
permission). Tests: policy matrix, tray over the real client with exact
base64 upload bodies + gated in-flight scenarios, dictation controller
suite with fake recorder/transport, voice endpoint request shapes, and
the interactor attachment-send matrix transcribed from the Android VM
tests (wire bodies byte-for-byte).
2026-08-18 11:27:28 +08:00
weishu 8795bab4da feat(ios): usage/storage dashboards + settings (A-M4de) 2026-08-18 11:18:50 +08:00
weishu ed310fcb9b merge: A-M3ab iOS composer + permission actions + session config
# Conflicts:
#	ios/Packages/HapiKit/Sources/HapiProtocol/Models/ApiResponses.swift
#	ios/README.md
2026-08-18 10:35:36 +08:00
weishu eed3dddb94 feat(ios): composer, permission actions, session config (A-M3ab)
Interaction layer turning the read-only chat into a working remote control,
mirroring the merged Android B-M3ab feature-for-feature (web authority where
the ports disagreed):

- ChatInteractor (HapiClient, fully swift-test covered): optimistic composer
  sends (appendOptimistic -> POST -> status settle), queue-by-default with a
  long-press Send&Steer intent while a turn is active, tap-to-retry on failed
  rows (steer retries degrade to queue), per-session drafts
  (UserDefaultsChatDrafts, hub-scoped keys, debounced + flushed on close).
- session_inactive (409) recovery: one POST /resume (current permissionMode)
  then retry; a superseding session id seeds the new window
  (MessageWindowControllers.seed), migrates the draft, retargets the
  optimistic row and emits sessionSuperseded -- ChatView/HomeView replace the
  navigation entry in place.
- Queued bar: uninvoked sends in web sort order with Cancel (optimistic
  DELETE; invoked-race ingests the authoritative row as sent), Edit
  (cancel + composer prefill, newer-draft guard) and Steer (invoked answers
  reconcile a missed consume); single-flight per-row op guard.
  reconcileQueuedState now runs on chat open and on session-pipe gap.
- Permission actions: flavor-exact bodies mirroring PermissionFooter.tsx --
  claude {} / allowTools (Bash(cmd)) / mode:acceptEdits, codex-family
  decision approved / approved_for_session / abort via deny -- plus
  AskUserQuestion flat answers (option cards, Other free text, no-questions
  fallback, cursor stable ids) and request_user_input nested answers
  (user_note suffix, required validation); optimistic Resolving /
  AlreadyHandled (404/409) overrides settled by the agentState patch.
  ChatPipeline now re-attaches the window row's client status so failed
  user rows actually render the retry affordance (web normalize.ts parity;
  the Android reference misses this overlay).
- Session config sheet (toolbar gear): catalog-driven permission-mode picker
  with tones, claude static model/effort catalogs (ModelCatalog port), codex
  models via new GET /sessions/:id/codex-models endpoint + wire types with
  per-model reasoning efforts; optimistic detail updates
  (SessionListStore.updateDetailLocal, new) rolled forward to server truth
  on error.
- Lifecycle: VisibilityReporter posts POST /api/visibility per tracked
  handshake subscriptionId on scene-phase flips (404 prunes); ChatSession
  exposes its subscriptionId and feeds the reporter; the global SSE pipe was
  already HubSession-lifetime on iOS.
- Tests: ChatInteractorTests transcribes the Android interaction suite
  against the real APIClient/AuthManager/SessionListStore/window registry
  with only HTTP scripted -- canonical JSON bodies asserted byte-for-byte
  (send/approve/deny/config), optimistic send happy/fail/retry, 409 resume
  both id paths, cancel invoked-race, steer reconcile, edit prefill,
  override lifecycle, config optimistic + rollback, drafts, abort.
2026-08-18 10:33:04 +08:00
weishu fa91f06662 feat(ios): new session flow (A-M3c)
Port of the tested Android NEW SESSION reference (B-M3d) to iOS:

- HapiProtocol: NewSessionCatalogs (static claude models/efforts + codex
  reasoning-effort fallback, exact Android data) in Catalog/; CodexModelSummary
  + CodexModelsResponse wire types (shared/src/apiTypes.ts).
- HapiClient: machineCodexModels endpoint (GET /api/machines/:id/codex-models,
  rpc_target_missing surfaces as APIError); NewSession/NewSessionForm.swift —
  typed Codable draft (tolerant decode) + NewSessionLogic: exact spawn body
  per SpawnSessionRequestSchema (yolo incl. false for non-grok/non-codex-family,
  permissionMode incl. 'default' for grok+codex-family, sessionType always,
  trimmed-or-absent worktreeName, serviceTier only while fast tier visible,
  collaborationMode only when plan, model only claude/codex), parent-path
  derivation, suggestion filtering, recent-path LRU(8), worktree-name
  validation, codex catalog helpers, draft sanitization.
- App: Features/NewSession (NewSessionModel @Observable orchestration —
  machine preselect last-used, 250 ms debounced list-directory autocomplete
  with per-parent cache, exists probe with worktree-blocking / simple
  two-tap-create, codex catalog fetch + selection reconcile, UserDefaults
  draft/prefs per hub; NewSessionView Form UI with per-flavor option matrix);
  session-list "+" toolbar button on HomeView presents the sheet, success
  dismisses and pushes the chat.
- Tests: spawn-body exactness (4 configs, canonical JSON), parent query,
  suggestions, LRU, worktree validation, fast-tier detection, reasoning-effort
  normalization, draft sanitize + tolerant decode; codex-models endpoint
  request/error construction.
2026-08-18 10:03:05 +08:00
weishu a4355d1837 feat(ios): read-only chat assembly (A-M2f)
Wires the merged M2 pieces into a usable pair -> list -> live chat flow:

- pbxproj: link HapiUI into the app target (UUIDs C7/C8, mirroring the
  existing HapiProtocol/HapiClient product references; only project change).
- HubSession: MessageWindowControllers registry + per-chat ChatSession
  factory with in-memory per-session SSE resume cursors and scene-phase
  forwarding to the active chat.
- ChatSession (app): session-scope SSEClient while the chat is open;
  window opened/activated before subscribing; a single consume task awaits
  every event into the window actor, giving Android-channel-equivalent
  arrival-order ingestion; session-updated and friends route through the
  shared SyncEventRouter (detail patching), session-removed clears the
  window, a gap handshake triggers full resync + detail refetch + catch-up
  tail sync; stop hands the cursor back for seamless reopen.
- ChatPipeline (HapiClient): actor running the reduction path off-main -
  queued-row filter, normalization memoized by row instance identity,
  reduce + buildVisibleChatBlocks with previousGroups-stable group ids.
- ChatModel: window state + detail agentState + machine labels -> serial
  ~100 ms-coalesced pipeline loop (first run immediate, publishes strictly
  ordered), header title cascade, loading/empty/error states, loadOlder /
  retry, last-seen stamping on every update.
- ChatView: bottom-anchored ScrollView/LazyVStack (defaultScrollAnchor +
  scrollPosition(id:)), auto-stick at bottom, new-messages pill, top
  sentinel paging with documented scroll re-anchoring, degraded banners.
- Block views: user bubble w/ attachment chips, agent markdown, collapsed
  reasoning, tool cards (knownTools.tsx-parity presentation, status chip,
  read-only permission row w/ pending banner, per-tool bodies: terminal,
  before/after edits, write content, unified-diff detection -> DiffTextView,
  checklists, read-only questions, pretty-JSON fallback, children rail),
  tool groups, centered event rows (presentation.ts port), cli output,
  generated images (authed bytes + in-memory cache + full-screen viewer),
  codex review verdict card.
- Navigation/links: session list now pushes ChatView; app-level
  \.hapiOpenURL handler (https/http -> SFSafariViewController, custom
  schemes confirm first, hapi-file:// -> M4 placeholder) + theme injection.
- Tests: ChatPipelineTests drive the runner with fixture-derived windows
  (stable ids, memo-stable recomputes, queued filter, group-id stability
  across an older-page arrival).
- README: M2f app-layer description.
2026-08-17 20:55:36 +08:00