Repository navigation
native: Swift 警告清零并转为错误;Mac 后端安装向导;iOS 可访问性与推送通道 - #371
Conversation
b2680eb to
bb01dc4
Compare
4158b0b to
2f26d4f
Compare
The Mac shell carried the same six warning sites across several releases (14 warning lines per clean build). Fix each at the root and pin the count at zero with -warnings-as-errors on the Lisa target, so they cannot creep back — Swift 6 strict concurrency would have turned them into hard errors anyway. - BackendController.restart: the one-shot NotificationCenter observer was captured by a Sendable closure and then mutated. Replace it with a waiter list on the @mainactor controller that post() drains right after the status broadcast; the termination handler hops back via Task { @mainactor } instead of a bare asyncAfter. - WebContent / IslandContent: `config.processPool = WKProcessPool()` has been a no-op since macOS 12 and is deprecated. The real seam for the chat window and the island pill sharing cookies / localStorage is the website data store — set `websiteDataStore = .default()` explicitly on both, with the intent documented. - HotkeyManager: `hotKeyID` was never mutated; `let`, and drop the `_ = hotKeyID` workaround. - Package.swift: `-warnings-as-errors` via swiftSettings.unsafeFlags. This is a root-only package (swift build, build.sh, build-mac-apps.sh), never a dependency, so unsafeFlags is fine and covers every build path. Verified: `swift build -c debug`, `-c release`, and the universal `-c release --arch arm64 --arch x86_64` that build.sh runs — all 0 warnings / 0 errors from a clean .build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit e564fc58b4f48bb31a7eb279e08ba50e818b37cd)
The Mac download is advertised as one click, but Lisa.app is only the window — the backend is a separate npm package. Until now the app assumed `npm install -g @oratis/lisa` had already been run in a terminal: when it hadn't, start() spawned a shell that failed silently and the user watched "Starting…" for 20 s before a hint appeared. That's the review's UX-7. This adds a wizard that closes the gap end to end: - Detection runs on a zsh LOGIN shell plus an explicit PATH prelude (~/.npm-global, Volta, Homebrew, nvm.sh, fnm). A GUI app launched from Finder reads ~/.zprofile but not ~/.zshrc, which is exactly where nvm and fnm live — so a Node that "works in Terminal" was invisible to us. Detect, install and `lisa serve` now all resolve tools the same way. - The decision is a pure state machine in a new AppKit-free LisaSetup target — ready / nodeMissing / nodeTooOld / cliMissing — with 20 unit tests in a new test target (Tests/LisaSetupTests). An installed `lisa` (or a serve-command.txt override) wins over an odd-looking Node, because `lisa` resolving is the thing that actually has to be true. - nodeMissing shows both doors: `brew install node` with a Copy button and a nodejs.org link, plus Re-check. cliMissing shows one button that runs the install with live, ANSI-stripped npm output and a spinner, then starts `lisa serve --web` exactly as before. - A failed install is classified instead of dumped. EACCES/EPERM gets the standard no-sudo fix (npm prefix → ~/.npm-global, PATH into ~/.zprofile) shown verbatim with "Apply fix and retry"; network gets a retry; EBADENGINE routes back to the Node step. - BackendController.start() now gates on `command -v lisa` and reports a LISA_MISSING marker, so a missing CLI opens the wizard immediately rather than timing out a 25-attempt poll. start(completion:) lets the wizard follow the outcome; the waiter list carries the note too. The terminal path stays first class: the offline splash keeps its `npm install -g` / `lisa serve --web` block, and the wizard's own footer repeats it. Three ways in — Lisa ▸ Set Up Backend…, "Set Up…" in the menu-bar popover, and "Set up backend…" on the offline splash. Verified: swift build -c debug and -c release clean (warnings are errors), swift test 20/20. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 058d71ae40a329eb0639d382ecb6c32d33a6601a)
Review §D of REVIEW_IOS_APP_v1.0 (still open per the v0.24.0 UX review, UX-12):
status was carried by colour alone, several rows read as disjoint fragments, and
the icon-only controls were both unlabeled and under the 44pt floor.
The root fix is a shared vocabulary rather than per-view strings:
GlanceColors.phrase(_:) turns an agent state into a word, and it lives in
Shared/ because the widget and Live Activity targets can't see the app's copy —
so "waiting on you" reads identically in the roster, the Lock Screen banner, the
Dynamic Island and the home Widget. statusPhrase(_:) is its roster-side twin and
mirrors stateColor's precedence (a pending permission outranks the raw state) so
the pip's colour and its spoken label can't disagree; a test pins that pair.
StatusDot now takes an optional label: passing one makes the pip speak, omitting
one hides it from VoiceOver. That second half matters — where the row already
says the state, an unlabeled dot was about to be read as "circle".
- One element per row: RosterRow (already), NeedsYouCard's header, the LISA
session rows, dispatch entries, StatCell ("Needs you: 2"), onboarding choice
cards (with .isSelected instead of a stray checkmark image).
- Labels + hints on the icon-only controls: Send, Stop, Open, Unlock, Copy,
quick chips, the agents card, the proactive Toggle (which had no name at all).
- 44pt targets via minHeight, never a fixed height, so Dynamic Type still grows
the label: OnboardingSecondaryButton, "Not now", the scanner's manual escape
hatch, SetupBanner, NeedsYouCard's Open, LockView's Unlock.
- The camera viewfinder is an empty rectangle to VoiceOver; it now announces
"Camera is live" with a value that tracks the scan note and a hint naming the
manual path.
- Text beside colour where colour was load-bearing: mail importance reads
"‼ Urgent" / "! Important", dispatch rows say "exited" rather than only
greying the pip, a stale widget snapshot says so instead of just dimming, and
the allowance bar exposes "$x of $y left" as its value.
Verified: ./build.sh test 'platform=iOS Simulator,name=iPhone 17 Pro' — 31
tests pass (29 before, +2 new), 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 394b2909b21d5f0ef7eb575a0b7924b73457efff)
src/web/push.ts has supported ntfy and APNs since day one, but the app buried a topic field under an "Enable push notifications" button that reported "Push registered (APNs)" regardless — so on a Mac with no Apple key (the default: apnsConfigFromEnv() returns null and delivery logs a stub) the user was told push worked and then nothing ever arrived. That's UX-12 / review §B. Settings → Notifications is now a transport picker: - ntfy: topic + an optional server field (blank = ntfy.sh, matching the server's own default), registered through /api/push/register with `server` — a field the client never sent, so a self-hosted ntfy was unreachable from the phone. - APNs: unchanged registration, but the status copy no longer claims delivery. The state line under the picker is derived from GET /api/push/list — what the Mac says it will publish to — via a pure PushSettings.stateLine, so it can say "your Mac is sending to a different topic" instead of a falsely reassuring "registered", and the APNs line names LISA_APNS_* and admits the app cannot check it from here (/api/push/* reports no APNs capability; adding one would be a server change, outside this stream's ownership). "Send a test notification" is ntfy-only, and posts straight from the phone to the topic rather than through the Mac. Publishing to ntfy is an unauthenticated POST, so this proves the topic and the user's ntfy subscription end to end even while the Mac is asleep — and there is no APNs equivalent to offer: no server test endpoint exists and a device can't make Apple push to itself. The button reports the real HTTP outcome, including "a self-hosted server may need auth". Preferences stopped being write-only: the toggles are seeded from the registered subscription, show "Unsaved — these apply once you tap Save preferences" when they diverge, and save through /api/push/prefs on the existing id rather than re-registering (which would churn a registered APNs token). PushPrefs gains `brief` — the server has always had it and the app silently shipped its default — and a tolerant decoder so a Mac that predates a preference doesn't discard the whole subscription. The topic/server/transport now persist in UserDefaults; they lived in @State and vanished when you left the tab. Verified: ./build.sh test 'platform=iOS Simulator,name=iPhone 17 Pro' — 39 tests pass (31 before, +8 new), 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 235f498ca960c24cfd2c5d13a0e88c3714bb2d2e)
Watching a build from the phone meant tapping "Load output" over and over: the detail view called /api/agents/pty/<id>/output once per tap, even though the server has had an attach stream all along (a `snapshot` frame with the current tail, a `chunk` per burst, `end` when the agent finishes). PTYLiveOutput follows that stream for a running session. The key invariant is that a snapshot REPLACES the buffer rather than appending: after a dropped connection the server re-sends the whole tail, so appending would duplicate everything the reconnect was meant to recover. Reconnect is the roster's backoff (1,2,4…30s) with a "Reconnect now" escape, and it stops for good on `end`, on a 403 (this Mac's control policy refuses remote attach — retrying would just hammer it) and on a 404. The buffer is bounded at 60k characters, trimmed on a line boundary when one is close, so a chatty agent can't grow it without limit — the phone only ever shows the tail. That trimming is pure and pinned by tests, as is the status phrase, so "live" / "retrying in 4s" / "finished" can't drift into a permanent optimistic "live" the way a bare spinner would. Long lines scroll horizontally instead of wrapping: the text opts out of wrapping inside a two-axis ScrollView, and the view follows the tail on each append the way a terminal does. Backgrounding suspends SSE on iOS, so returning to the foreground reattaches unless the agent already ended. A finished session keeps the one-shot pull — there is nothing left to stream, and an idle SSE socket for a done agent is pure cost. Verified: ./build.sh test 'platform=iOS Simulator,name=iPhone 17 Pro' — 44 tests pass (39 before, +5 new), 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 51b5f7b108863073586f4aaa8a3b00ba696a0ed0)
Delivers on the iOS half of the commitment made reviewing #365: the ledger learned to record an observed exit and DispatchView now carries it (status / exitCode / exitSignal / exitedAt), but the roster still rendered "alive" vs "exited" — so a crash, an OOM kill and a clean finish were one grey dot and one word. - DispatchKind (running | ok | failed | unknown) maps the server's `status`, with an explicit fallback for a pre-status server: no `status` and `alive: false` is **unknown**, never `ok`. The agent is spawned detached, so a dead pid alone never means success. - The row spells the state out in text and gives each state its own dot colour; colour is never the only carrier. - The detail row names the exit code or the signal ("failed (exit 127)", "killed by SIGKILL") — "failed" alone does not say whether the agent errored out or was killed. - Each state gets a distinct VoiceOver label, since the dot's colour carries nothing for VoiceOver. A test pins all four as distinct and non-empty. Also: HTTP 403 said "Server returned HTTP 403." Hosted Lisa denies the machine-level routes (push, dispatch, mail, consent, devices — the deny-list in src/web/capabilities.ts, which #365 extended to /api/push), and push registration is the route an iPhone hits first, so the generic message left "registered, but no notifications ever arrive" unexplained. It now names the hosted/local split and what to do about it. Verified: `./build.sh test` — 49 tests, 0 failures (44 existing + 5 new). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bb01dc4 to
1ebdc58
Compare
评审 — 构建承诺实测通过;补齐 iOS 退出状态(兑现 #365 的承诺)已 rebase 到 main,base 改为 warnings-as-errors 实测本机有 Xcode 26.6 / Swift 6.3.3,所以这条承诺是可以真跑的,不用只看描述:
也把 main 的 mac-client 单独导出来做了基线计数:干净构建 12 行警告 = 6 个不同警告,分别是
安装向导的命令执行面 — 查过,没问题新增的
已补:iOS 的 dispatch 退出状态评审 #365 时我把"UI 消费留给各自表面 PR"写成了明确承诺,这里兑现 iOS 那一半。 #365 让账本记录了观察到的退出、 补上:
顺带修了一处 #365 刚刚让它变得真实可达的问题:HTTP 403 只显示 "Server returned HTTP 403."。托管版 Lisa 拒绝所有机器级路由(push / dispatch / mail / consent / devices——
其余核对
验证macOS: 🤖 Generated with Claude Code |
`lisa upgrade` printed npm's first line and stopped. EACCES on a global install is the single most common way this command fails — it is the whole reason `sudo npm install -g` is folklore — and #371 had already taught the Mac wizard the no-sudo prefix fix. The same user got a dead end from the CLI and a working fix from the app. classifyUpgradeFailure() mirrors classifyInstallFailure() in packaging/mac-client/Sources/LisaSetup/BackendSetup.swift, on the same needles, so the two surfaces cannot drift into contradicting each other about one shared `npm install -g`. The permissions branch prints that wizard's commands verbatim. Classification reads the whole error message, not its first line: runCmd rejects with `npm exited 1: <stderr>` and the needle is almost never on line one — the reason the old first-line-only output was uninformative. Tests cover each class, the multi-line real-world shape, that no branch hands out a bare `sudo npm install` (that is what creates the root-owned folder in the first place), and that MIN_NODE_MAJOR tracks package.json `engines.node`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last of the three surfaces, and the second half of the commitment made reviewing #365 (iOS landed in #371). The dashboard rendered `d.alive ? 'Running' : 'Done'`, so a crash, an OOM kill, `exit 127` and a clean finish were one green "Done" — the card actively asserted a success the server never observed. The ledger has recorded a classified exit since #365 and DispatchView carries it; nothing consumed it here. dispatchState() maps the server's `status` to a label and a colour class: running, Done, `Failed · exit 127`, `Killed · SIGKILL`, or `Exited · status unknown`. A pre-status backend degrades via `alive`, and an exited dispatch there is **unknown, never Done** — a dead pid is not evidence of success when the child is detached. An unrecognised status lands in the same place rather than falling through to green. Tested by slicing dispatchState out of the exact bytes the browser is served and running it in a vm, the same technique this PR's i18n tests use — so the test cannot pass against a version of the function the page doesn't ship. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
embed-runtime.sh 把 dist + 生产依赖 + 校验过的官方 Node 打进 bundle(--omit=optional 去掉唯一原生模块,保持纯 JS);启动优先级 serve-command.txt → 内置后端 → PATH;后端瞬崩时从 backend.log 读出真实原因并按启动路径分类。离线横幅从压住标题栏的顶部红条改为主题化的底部 toast(info/error 音调 + 可复制命令 chip)。 评审补: - toast 移植到 #375 之后的 main.js/main.css;README 段落移到 GUIDE。 - BackendController 与 #371 安装向导集成:换回主 actor waiter(原写法触发 -warnings-as-errors 的 sendable 捕获错误)、恢复 start(completion:) 让向导拿到分类后的失败原因。 - build.sh 从仓库根调用会失败(cd 后重新解析 $0),顺带修好 app 图标 fallback。 - 内置 Node 的哈希钉进版本库(sums 文件与 tarball 同源,无法自证)。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
第 3 / 8 条,实现 T-11 与 UX-7、UX-12。基于 #370。
macOS
14 个警告 → 0,并开启 warnings-as-errors。 修掉
BackendController里被 Sendable 闭包捕获后又改写的 observer、IslandContent/WebContent两处已弃用的WKProcessPool(改用共享WKWebsiteDataStore达到同样的会话共享效果)、以及一个从未重新赋值的 var。Swift 6 的严格并发检查下这些会变成错误,现在提前挡住回归。debug 与 release 两种配置均验证通过。后端安装向导(UX-7)。 官网承诺一键下载,实际上下载 DMG 之后还要用户自己跑
npm install -g @oratis/lisa。现在检测 Node(经/bin/zsh -lc以便解析 nvm/brew 的 PATH)与 lisa CLI,提供一键安装并在面板内实时显示输出;Node 缺失时给出 Homebrew 命令(带复制按钮)和官网链接以及重新检测按钮;权限失败时直接给出修复命令。决策逻辑抽成纯函数并有测试。iOS
可访问性(UX-12)。 之前全应用几乎没有 accessibility 标注:状态点只用颜色区分、roster 行被读成互不相关的碎片、图标按钮无标签、扫码区域没有说明。现在状态一律配文字、每行合并为单个可访问元素、图标按钮全部有标签、进度点读作"第 N 步 / 共 M 步",Send/Copy 等控件补足 44pt 触控区。
推送通道。 服务端一直支持 ntfy,客户端却只接 APNs——没有 Apple 推送密钥时界面显示"已注册"但永远收不到通知。现在 Settings 里可选传输方式,并按服务端实际返回如实说明投递状态。
PTY 实时输出。 会话详情改为订阅
/api/agents/pty/<id>/stream的 SSE,替代原来的一次性拉取按钮,断线自动重连。验证
swift build -c debug与-c release均 0 警告 0 错误;iOS 模拟器测试 44 通过 / 0 失败(此前 29)。🤖 Generated with Claude Code