Skip to content

feat: connect a Dot. Quote/0 as a third display type - #1

Merged
CLCK0622 merged 8 commits into
mainfrom
feat/quote0-display
Sep 23, 2026
Merged

CLCK0622 merged 8 commits into
mainfrom
feat/quote0-display

Conversation

@CLCK0622

Copy link
Copy Markdown
Member

这是什么

macOS 端的 Quote/0 支持,对应后端 inklethq/backend#41。在 New Display 的 Hardware / Virtual 之外新增第三张卡片 Quote/0:输入 Dot. API key 和面板序列号,调用 POST /api/devices/quote0,之后它就是一台普通设备(transport: "dot_cloud"),由后端经 Dot. 云推图。不刷固件、不用 NFC。

改了什么

  • 模型:DeviceDTO 增加 transport / cloudDeviceId / cloudModel / cloudDeliveryError(_At);Device 增加 kind(.inklet / .quote0)、identifier(序列号或硬件 ID)、modelName、symbol。没有 transport 的旧后端仍按 inklet 硬件处理。
  • 连接表单(NewDisplayView):三步前置说明(Dot. App 里给面板加 Image API 循环项、创建 key、复制序列号),SecureField 输入 key,本地先校验 dot_app_ 前缀,成功后清空 key 并跳到该设备详情。错误按后端 code 映射成可读句子,未知码回退到后端的 message。
  • 详情页:型号显示 "Quote/0",标识行改为 "Serial",网络行改为 "Dot. cloud · Reachable/Unreachable",预览下方显示 cloudDeliveryError(云端拒绝时唯一能告诉用户的信息),"Sent" 提示文案区分两种传输,解绑对话框说明会忘掉 key、面板保留现有画面。
  • 预览框架:DisplayFrame 按 kind 绘制——Quote/0 用 296×152 的裸面板加细边框,不再套 D1 边框。
  • 侧栏 / 首页卡片 / 编辑器目标选择:Quote/0 用 cloud 图标;首页卡片有投递错误时直接显示。
  • 文档:README 后端依赖、docs/virtual-displays.md 的 Quote/0 说明。

验证

  • swift build --product InkletMac 通过;swift test(Xcode 26 工具链)41 个测试全绿,新增 6 个 Quote/0 测试(解码、命名、回退、错误映射、绑定响应形状)。
  • 未做:对真实后端与真实 Quote/0 的联调。需要生产注入 DEVICE_CLOUD_CREDENTIAL_KEYS 后,用一把 Dot. key 和一台加了 Image API 循环项的面板走一遍:连接 → 详情页 → Push Here → 面板刷新。

没做的

iOS 端(wire 类型与规则相同,可平移)与 Portal 的绑定 UI。

🤖 Generated with Claude Code

New Display gains a Quote/0 card beside Hardware and Virtual. The form
takes a Dot. API key and the panel's serial number and calls the new
POST /api/devices/quote0 (inklet-backend#41); the backend proves the key
against the Dot. cloud, seals it, and from then on pushes every rendered
picture through the Dot. image API. The app never keeps the key: it
lives in the form's state for the length of one request.

The device model now reads `transport`: a `dot_cloud` row is a Quote/0,
named by its serial (nickname still wins), shown with a cloud glyph in
the sidebar, the composer's target picker and the Home card, drawn as a
bare 296×152 panel instead of inside the D1 bezel, and described on the
detail page as "Quote/0" with a Serial row and a "Dot. cloud" reachability
row. `cloudDeliveryError` — the one thing a cloud panel can say about a
picture that did not land — is shown under the preview and on the Home
card. The unbind copy says what actually happens (inklet forgets the
key; the panel keeps what it has). A backend that sends no `transport`
still reads as inklet hardware.

Bind refusals map the backend's codes to sentences the form shows
(bad key, serial not in that Dot. account, owned by another inklet
account, Dot. rate limited or down, feature off on the server), with
the backend's own message as the fallback for a code this build does
not know.

Tests: decoding a dot_cloud row (kind, name, identifier, model, glyph,
delivery error), nickname precedence, the no-transport fallback, an
empty delivery error reading as none, the bind response shape, and the
error-code mapping.

Not in this change: the iOS app (same wire types, same rules to apply)
and any Portal UI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 20, 2026 04:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CLCK0622 and others added 4 commits September 20, 2026 02:35
A Quote/0 fits a picture that is not its own 2:1 onto white; the
preview used scaledToFill and cropped it, so the app and the panel
disagreed about the same frame. Fit, like the panel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
26 was what the machine it was written on had, not what the app needs. The
one 26-only API in use is ToolbarSpacer, at three sites; each is now gated
on #available(macOS 26.0, *), so on 15 the toolbar just has no grouping
gaps and the Create button takes the trailing placement on its own.
Everything else — @observable, desktop widgets, the presentation cache —
already runs on 15. 14 would cost nine more sites; 13 has no desktop
widgets.

Building still needs Xcode 26 for the widget extension.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…explaining

Return used to pick "Just upload" instead of sending: the .defaultAction
shortcut sat on the Menu, and a shortcut on a Menu propagates to its
items. The key equivalents now live on hidden buttons of their own — ⌘↩ to
send, ⌥1…⌥4 to choose the mode — so they fire with the menu closed and
never land on an item. The items keep their ⌥n so the menu draws the hint.

The labels are "Just upload", "Push to device", "Recent summary" and
"Show as-is". What each one costs (an AI run, or none) moved into the
tooltip; the face of a split button is not the place for a sentence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Portal's Analyses page, in the app: every run inklet has made for the
account, newest first, filtered by state and by who started it, and for
any one of them the public event timeline — what the agent read, which
layout it chose, whether the plan came back, and when the panel showed
the picture. Knowledge only says a note was "organized"; this is where.

- InkletAPI: analyses (cursor pages), analysis, analysisEvents.
- HistoryModel: the paged list and, for the open run, a poll that keeps
  going while the run is live and for a while after it ends — render and
  delivery happen after the terminal state and never come with it.
- AnalysisCopy: the Portal's analysis-copy and analysis-timeline ported
  to Swift, so both clients tell the same story: activities merge into one
  row that changes in place, retries group by attempt, delivery rows name
  the panel, unknown event types fall back to their summary.
- Ink.warn / Ink.working for the two tones the timeline needed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@CLCK0622

Copy link
Copy Markdown
Member Author

Three more commits on this branch, released as v0.2.4-alpha.2 for testing:

  • Runs on macOS 15+ (cfbe326). The only 26-only API in use was ToolbarSpacer at three sites, now gated on #available(macOS 26.0, *). Building still needs Xcode 26.
  • Composer keys and labels (2624e55). ⌥1/⌥2/⌥3 (and ⌥4 when "Show as-is" is offered) pick the mode, ⌘↩ sends. Return no longer picks "Just upload" — the .defaultAction shortcut sat on the Menu, and a shortcut there propagates to its items; the key equivalents now live on hidden buttons. Labels are "Just upload / Push to device / Recent summary / Show as-is", with the cost of each in the tooltip.
  • History page (6694bdb). The Portal's Analyses page in the sidebar: every run, filtered by state and by who started it, and for any run its event timeline (activities merge into one row that updates in place, retries group by attempt, delivery rows name the panel). AnalysisCopy.swift is a port of the Portal's analysis-copy.ts / analysis-timeline.ts, with 8 tests.

Full suite: 49 tests green.

CLCK0622 and others added 3 commits September 20, 2026 15:31
A cloud glyph said nothing about whose cloud. The card now carries Dot.'s
own mark, drawn as a Shape from their single_logo.svg — one filled path —
so it takes the foreground style and scales like the SF Symbols beside it.

"Nothing to flash, nothing to tap" is gone from the description; it was
a sales line, and the card is a button.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… connected

The sidebar row and the composer's display picker showed a cloud for a
Quote/0; they now carry Dot.'s mark, the same shape the New Display card
draws. Device.icon is an Image rather than a symbol name so the rows
AppKit draws — menus, pickers — can show it too; the mark is a template
NSImage drawn by a handler, so it is crisp at any scale.

Connecting a Quote/0 asks for a name. A serial number tells the agent
nothing about where the panel is or what it is for, and "let inklet
choose" has to choose by something. Blank keeps the serial. The bind
route takes no name, so it is a second request; a name that fails to
stick is a rename away, the bind is the part that must not be lost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Knowledge says a note was organized and a display says a card arrived;
the run in between — what the agent read, which layout it chose, when
the panel showed it — lives on the History page, and there was no way
from one to the other. Each history row on a display page now opens its
run: click, the Run button on hover, or the context menu. A run the
History list does not hold (another filter, an older page) is read on
its own, so the jump always lands.

The status "Confirmed" reads "Showing": that is what the panel said,
and the backend expires the previous picture the moment a newer one
takes its place, so only one row ever says it. The timeline's
delivery.confirmed line says the same.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@CLCK0622
CLCK0622 merged commit ebe8792 into main Sep 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants