Commit graph

2812 commits

Author SHA1 Message Date
Kayshen-X 8654c04b00 feat(collab): replace public invites with 10-char region-tagged pairing codes
The ~500-char opc1_ fragment is retired from every production surface; a
relay session now shares one 10-char Crockford code (1 region char + 9
random, 45 bits). The full invite is sealed with a blake3 encrypt-then-MAC
under keys derived from the code and stored on the locator control plane
under an independent code_id; guests claim from exactly the region the
code names, so neither the id nor the bearer ticket reaches uninvolved
regions. The store tombstones exhausted claim budgets (an id can never be
re-published under a burned code), caps codes per device key, and keeps
per-route ingress body limits. Connect failures now distinguish invalid,
expired, and relay-not-configured across all 15 locales, and the threat
model documents the operator-grindable 45-bit residual risk.
2026-08-02 08:31:13 +08:00
Kayshen-X 25eb2f0cc1 fix(editor): make the collab join field paste-replace, selectable and clearable
A pasted invite code now replaces the whole field instead of appending to
stale content, Cmd/Ctrl+A selects the field so Backspace/Delete clears it
in one stroke, and a clear button sits inside the input. IME commits keep
insert semantics; only clipboard paths replace. Every blur and navigation
arm drops the whole-field selection so no destructive replace-on-type
state survives a click.
2026-08-02 08:30:12 +08:00
Kayshen-X 45d962938b feat(collab): add isolated PKCS11 locator signer 2026-08-01 16:35:59 +08:00
Kayshen-X c715972f51 feat(collab): require recovery epochs in union policy v2 2026-08-01 16:30:38 +08:00
Kayshen-X 5f04e26c1a chore(rust): align workspace checks 2026-08-01 16:19:26 +08:00
Kayshen-X 6212b486e3 feat(collab): minimize the relay credential and confirm the owner on LAN
Two halves of the same problem: the relay learned who collaborates with
whom, and a guest could not safely join a stranger's session without an
invite.

Claim-minimized relay bearer. The relay authenticated each WSS connection
with the full collaboration ticket, whose claims carry the account subject,
device id, and optional display name and avatar. Now that collaboration is
cross-account, that let a relay operator reconstruct a social graph. The
relay reads exactly one field out of that ticket — the expiry it clamps the
session deadline to — and its authorization output, (route, role, expiry),
comes from the signed locator and the route capability, not from identity.
The disclosure was gratuitous.

A separate audience-scoped token now carries only issuer, audience,
version, scope, the channel binding to the caller's X25519 key, and the
time bounds. `VerifiedRelayTokenClaims` deliberately exposes no identity
accessor at all, so the relay cannot regress into reading one. No route or
role claim was added: route authorization already comes from the locator
plus the capability secret, and putting route ids in the token would move
the graph to the issuer, which also knows the account.

Scope of the guarantee, stated in the code so nobody over-reads it: this
defends against a third-party or regional relay operator. It does not
defend against the first party, who runs both the issuer and the relay and
can rejoin on the channel-binding key and the issuance time. It also
de-identifies rather than making the view unlinkable — the device's X25519
static is persistent and in the clear in every hello, so the operator still
builds a device graph, it just cannot name the nodes or join them to the
account namespace.

The two token types are domain-separated by both JWS `typ` and `aud`,
strictly compared, with `deny_unknown_fields` on disjoint claim structs, so
each is structurally invalid against the other's parser. That property is
what makes sharing one signing key defensible, so it is tested in both
directions. The relay dual-accepts during migration, discriminating on
`typ` before claim parsing, behind an env flag. The client never retries a
rejected minimized token with the full ticket — that would be a downgrade
any curious relay could trigger at will.

Guest owner confirmation. A guest joining over unpinned LAN discovery still
required the same account, because it has no approval prompt of its own and
mDNS names nobody. It now gets the explicit decision the owner already had:
the verified owner identity is surfaced and confirmed before the peer is
authorized, so nothing from the session — snapshot, presence, session name
— exists before the user decides. With that gate in place the unpinned LAN
path admits any issued account too.

Display name and avatar are attacker-chosen, so the projection separates
them from the account subject and device id at the type level, strips
invisible and bidi-control characters, and labels them as claimed. A
display name cannot occupy an authoritative row; the test uses another
account's UUID as the display name to prove it.

Both halves are pinned in the boundary gate: losing either asymmetry is
silent, because the code still compiles and every other check still passes
while nothing authenticates the peer.
2026-08-01 14:24:46 +08:00
Kayshen-X 0feab42b4c feat(collab): allow cross-account collaboration
Peer admission required the remote ticket's subject to equal the local
account on both sides, so only devices of one account could pair. That
made the product multi-device sync rather than collaboration.

The subject equality was the authorization, so it is replaced rather than
deleted. `PeerIdentityPolicy` states which accounts a peer may belong to,
and the two sides get different answers because they do not have the same
ability to tell who the peer is:

- The owner accepting a guest admits any issued account. Nothing at this
  layer decides whether the guest joins — a human does, from the approval
  prompt, which is shown the verified identity and which the admission
  state machine makes unskippable (`Active` is reachable only through
  `OwnerAuthorized`).
- A guest joining by invite or relay admits any issued account, because
  the invite's signed locator already pinned the owner's Noise static key
  and that pin is checked before admission runs. The device is
  authenticated whatever account is behind it, which is what makes joining
  a stranger's session safe.
- A guest joining over an unpinned LAN discovery still requires the same
  account. A guest has no approval prompt — whatever it accepts, it
  accepts silently — and nothing else names the peer there: mDNS is
  spoofable and no key is known in advance. Relaxing it would let anyone
  on the segment holding any valid ticket pose as the owner, undetected.
  Opening this needs a way for the guest to confirm who it is joining,
  which is a user-facing decision, not a protocol change.

Relaxing the account relaxes nothing else: issuer, expiry, and the binding
to the observed Noise static key are unchanged, and renewal still refuses
any mid-session change of issuer, subject, device id, or key. Tests cover
that a foreign subject is admitted while each of those still rejects, and
the unpinned-LAN case has its own regression guard.

Also fixes two things this uncovered. The live MCP tests in op-host-desktop
drove the endpoint with raw HTTP and no token, so they failed with 401
after bc75c765b authenticated it — they now send the instance token, which
is also a live check that the authentication works. And two constant
relations in the transport config were runtime asserts that clippy rejects
as constant-valued; they are compile-time asserts now, which is what a
relation between constants should have been.
2026-08-01 13:01:24 +08:00
Kayshen-X cbbf2db568 refactor(desktop): split collab conflict files under the 800-line cap
Pure code motion restoring the workspace file-size cap after the
conflict-stash feature pushed four files over it:

- op-editor-core: notice kinds and the discarded-edit projection move from
  collab_ui_state into a new collab_notice_ui module; collab_ui_state
  re-exports them so import paths stay stable.
- op-host-desktop: the local-edit gestures (begin/finish/reapply) move to
  collab_runtime/local_edit; the discarded-edit stash handling moves to
  collab_runtime/discarded_edit; the fail-closed paths and failure-notice
  mappings move to collab_runtime/failure; the owner-retaining guest test
  harness and the conflict e2e test move to collab_runtime/conflict_tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-01 12:53:13 +08:00
Kayshen-X 7c4f38cd4e fix(mcp): authenticate the live endpoint and degrade the bootstrap cache
The live MCP endpoint on 127.0.0.1 was a bypass of the collaboration
admission model. Its per-instance token authenticated only the ping probe
and shutdown, so document reads and writes were available to any local
process, and nothing validated Origin or Host — a page in a browser on the
same machine could reach it by DNS rebinding. During a session that is the
shared document, not just this user's file.

Every stateful call now requires the instance token, compared without an
early exit. Host must be a numeric loopback literal on the bound port, and
an Origin, when present, must match it; a request with no Origin still
works, which is what real CLI clients send. OPTIONS, initialize, and ping
stay tokenless so CLI discovery keeps working, and CollabGatePolicy is
untouched — this sits in front of it.

The `op` CLI did not send the token, so authenticating tool calls would
have returned 401 for every `op` invocation against a live editor. The
token was already in the port file next to the port; it is now resolved
with the port and travels as a header. Ping and shutdown keep their
existing tokenless wire contract.

Bootstrap cache: reads now degrade like writes already did. An unreadable
or corrupt cache leaves this start with no anti-rollback generation floor,
which is the position an absent cache has always left it in, and which the
threat model already accepts because deleting the file achieves the same
thing with no more privilege than corrupting it. Refusing bought no
security and cost the ability to collaborate at all. The tests state the
price plainly: with no floor the lower-generation document is accepted,
and `rollback_floor_armed` has to report it.

Threat model: correct an overstatement. Peer admission requires the remote
ticket's subject to equal the local account, so the product pairs only
devices of one account today. A relay operator reconstructs which devices
of an account sync and when — not a cross-account collaboration graph.
Also records that the relay reads exactly one field out of the ticket it
verifies, the expiry, which makes the identity disclosure gratuitous
rather than load-bearing, and states what a minimized credential would and
would not buy.
2026-08-01 12:23:09 +08:00
Kayshen-X 4c5f200b3d feat(editor): surface collab conflict details and replay discarded edits
A guest edit that loses a collaboration conflict is no longer silently
dropped behind a generic toast. The cancellation now carries the dropped
EditChanges end-to-end: the conflict notice names the discarded nodes and
fields (dedicated EditConflictDiscarded notice kind so the detail can never
attach to an unrelated conflict toast), and the dropped property intent is
stashed so the collab panel can resubmit it on request through the new
op_collab::reapply_property_changes API. The stash is created only for
genuine concurrency losses (property conflict / precondition failure) —
policy, permission, and size rejections map to their own notices — and is
cleared on every session-Ended path so it cannot outlive its session.

Addresses one blocker (replay consumed the stash before acquiring the edit
lane), four concerns, and one nit from external review. Verified by
op-host-desktop collab (136), op-editor-core (19 collab), op-editor-ui
(52 collab), op-collab, and op-i18n suites.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-01 12:15:30 +08:00
Kayshen-X b98154df15 fix(collab): close review follow-ups and align the reauth budget test
Follow-up to 0649cf40, which split renewal concurrency away from initial
authentication so an unauthenticated flood cannot starve live tunnels.
`online_challenge_timeout_keeps_its_auth_concurrency_permit` encoded the
old coupling as its contract — it saturated `max_auth_in_flight` to make a
second online challenge contend — so after the split nothing contended and
the test hung the whole suite. The property it covers (a timed-out online
challenge neither leaks its permit nor spawns another blocking task) is
unchanged; it now saturates the renewal budget that actually governs that
path. Its initial-path sibling was already passing and is untouched.

Roster: add `release_disconnected_peer`. A departed guest is retained for
same-epoch resume, but that retention is unbounded and counts against
`max_participants`, so a session that churns through guests eventually
admits nobody and each retained peer holds its result window until the
epoch ends. Releasing is refused for the owner and for a still-connected
peer, and it does not weaken id-space safety: a namespace can only be
reissued when the document carries none of its ids, which is checked
against the document rather than the roster.

Tickets: replace the derived `PartialEq` on `OpaqueTicket` with a
constant-time comparison. Nothing compares a ticket against a stored
secret today — verification lives in the host auth layer — but the derive
inherited `String`'s early-exit compare, so any future `==` on
attacker-supplied input would have been a timing oracle.

Bootstrap cache: a failed write now degrades instead of failing the
bootstrap. The document was already fully verified; what a failed write
costs is the anti-rollback generation floor for the next start, and the
threat model already accepts a missing floor because deleting the cache
has the same effect. An unwritable configuration directory must not mean
"cannot collaborate". The degradation travels with the document rather
than passing silently. Reads stay fail-closed: an absent cache reads as
`Ok(None)`, so only a cache that exists and is corrupt or unreadable
takes that path, and that is an anomaly rather than a first run.

Threat model: document what the relay operator actually sees. The bearer
ticket it verifies carries the account subject, device id, and optional
profile claims, so it learns who collaborates with whom — the asset table
previously implied those reached session participants only. Also adds the
invite-as-bearer-capability lifecycle and the local MCP endpoint, which
during a session reads and writes the shared document outside the
admission story. The relay-client doc comment no longer claims the relay
"sees only bytes".
2026-08-01 11:42:10 +08:00
Kayshen-X f9f9c8574a fix(collab): harden p2p collaboration against resource exhaustion
Addresses a security review of the collaboration subsystem. No auth
bypass, key leak, or document-plaintext exposure was found; every
finding below is availability or trust-boundary hardening.

Landed as one commit because the pieces are not separable: the
inbound-direction ceiling spans op-collab, op-collab-transport, and the
desktop host atomically, the guarded accept spans transport, smoke, and
the desktop host, and the boundary-gate rules only hold against the
final state. Splitting would produce commits that fail to build or fail
the gate.

Relay server (public, internet-facing):
- Charge pre-pairing capacity per source address. The auth-concurrency
  semaphore was taken before the WebSocket upgrade and the peer address
  was discarded, so one host could pin every permit by connecting and
  going silent.
- Give renewals their own budget. Reauthentication competed for the same
  semaphore, so an unauthenticated flood progressively closed live
  tunnels with a policy error.
- Release the pair registration when the ready status fails to send; the
  counterpart only reclaims it if it reads its pairing notice.
- Require the X25519 key file to be owned by the running user; mode bits
  alone do not establish trust.
- Summarise capacity rejections instead of logging one line each.

Locator service:
- Rate-limit publishes per client instead of process-wide. One
  unauthenticated caller could consume the whole budget and 429 every
  tenant's invite issuance.

Collaboration protocol:
- Size the inbound envelope ceiling from the authenticated remote role
  rather than sharing the 64 MiB snapshot ceiling in both directions, so
  an admitted guest cannot force a 64 MiB JSON parse per frame. The
  ceiling is applied before the discriminator and before the generic
  value decode; a peer-declared snapshot kind cannot raise it.
- Reject display names carrying Unicode format characters, which render
  identically to an existing participant's name.
- Reject avatar URLs pointing at non-globally-routable addresses.

Transport:
- Reclaim a pending-handshake seat from a peer that has not produced a
  valid first handshake message, and raise the global ceiling. Sixteen
  seats held for the full handshake window let four addresses deny every
  join.
- Put inbound reassembly under an aggregate budget; only the outbound
  aggregate was bounded.
- Stop heartbeats from refreshing the idle deadline in receive_transfer.
- Filter IPv4 link-local discovery advertisements, matching IPv6.

Relay client and trust roots:
- Bound server-initiated reauthentication per connection by count and
  minimum interval, sized from the protocol's own cadence.
- Close the policy-file TOCTOU window by identity-checking the opened
  file, and reject group/world-writable or foreign-owned policy files.
- Stop discarding bootstrap cache-write failures, which silently
  disabled the anti-rollback generation floor.
2026-08-01 09:48:23 +08:00
Fini 22e1a038c7 feat(panels): round out the prompt center with web, dashboard, component and modify entries
The catalog shipped with the mobile-app and freeform galleries only, so
four of the six categories opened empty — a filter chip that leads
nowhere reads as a broken panel, not an empty one.

Adds eight entries covering the remaining categories (two web pages, two
dashboards, two component systems, two modify instructions), each with
its bilingual body, a generated preview, and a 15-locale title key. The
catalog-size constant moves 1255 -> 1263 accordingly.

Starter prompts gain previews too: they were the first thing a new user
sees and were the only category rendering as bare text.

The native raster test now decodes four of the new previews instead of a
single hard-coded one, so a re-generated asset that regresses decoding
is caught for more than one image.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 21:06:56 +08:00
Fini 188afc99d1 feat(agent): add a fixed-input modify mode to the smoke harness
The harness could only drive fresh generation, so the modify-category
prompts had no way to produce before/after thumbnails: their whole point
is what changes about an EXISTING document.

`OPENPENCIL_SMOKE_MODIFY_INPUT=<baseline.op>` runs a real document
through the same `build_modify_plan` -> `run_modify_turn` -> scoped host
apply path the desktop uses, so a thumbnail reflects the shipping code
rather than a harness-only shortcut.

The baseline is never overwritten — the output must go to a distinct
`OPENPENCIL_SMOKE_OUT` — and both files are SHA-256 addressed in the
summary, so a thumbnail set can prove every modify prompt started from
the identical input.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 21:06:35 +08:00
Fini 833bf59135 refactor(desktop): retry exhausted stock-image searches from a dedicated arm
A node whose stock search came back empty was parked in a terminal
failure state with no way back, so a design shipped with placeholder
slots even when a second attempt would have found art. Split the retry
policy out of `image_enrich_cli` into its own `retry` module and give
the session an explicit `retry_search_failures` entry point that
re-admits those nodes for a bounded, caller-managed retry.

Only Search/Auto nodes are re-admitted: an explicit Generate target that
failed is never silently converted into a stock search, since that would
substitute different art than the design asked for. `image_request_mode`
makes that distinction a property of the node rather than something each
call site re-derives.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 21:06:11 +08:00
Fini d709f2c572 fix(agent): stop landing pages from scaffolding as sidebar dashboards
`plan_is_sidebar_dashboard` accepted a sidebar signal from ANY subtask,
so a landing page whose plan happens to carry a nav/menu section was
built on the two-column dashboard scaffold — a sidebar rail down the
left of a page that should be a full-width hero stack.

Require the signal to come from the FIRST subtask (a real sidebar is the
leading section, not an incidental one), and let a plan's landing-page
anatomy veto an ambiguous signal via `plan_has_landing_anatomy`. An
explicit landing-page request now vetoes every dashboard signal, while
an explicit dashboard or admin-console request still wins ahead of the
structural check, so the unambiguous cases are decided by what the user
asked for rather than by section keywords.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 21:06:11 +08:00
Fini 72fd294514 feat(agent): honor root dimensions requested in the prompt
A prompt that names its canvas size ("1200x800", "390 宽") had no path
to the root frame: planning always applied the desktop/mobile defaults,
and cleanup was free to grow the root past whatever was asked for.

Parse the request once (`request_dimensions`), apply it during plan
normalization, state it in the compact prompt so the model builds to the
same number, and carry a `preserve_requested_root_height` flag into
cleanup through an explicit `CleanupPolicy`. The policy defaults to the
historical behavior — only the fresh-root orchestrator path opts in — so
append and modify runs are untouched.

Cleanup needs the RESOLVED height to honor that flag without collapsing
real content, so `geometry_validation` grows `resolved_node_height`,
measuring the laid-out subtree against the node's own top edge rather
than trusting the declared value.

Planning corpus follows: the desktop sizes are labelled "Desktop
default", so an explicit request reads as an override rather than a
contradiction.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 21:05:55 +08:00
Fini 5a0eb98a2b fix(mcp): tolerate a trailing separator on a program DSL line
The line grammar anchors every pattern on `\)$`, so an operation
written as `img=G(...),` was rejected as unparsable. A trailing `;` was
already stripped; a `,` was not — and the comma is the costlier miss,
because a model reaching for a list separator writes it on EVERY line.
All lines fail, the transaction rolls back, and nothing lands.

Measured 2026-07-31: five `G(...)` image fills rejected for one trailing
comma each. `Cannot parse operation` echoes the line without saying what
is wrong, so the model mis-diagnosed it as an ARGUMENT separator
problem, then started deleting and rebuilding subtrees it had already
committed — chasing node ids that were never stale, since ids are string
identities and a delete renumbers nothing. The design ended up worse
than before the failing batch. One rejected line is a retry; a rejected
batch is a demolition.

Only the line's own tail is trimmed, so a comma inside an argument body
is untouched: every real operation ends on `)`.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 21:00:50 +08:00
Fini 4eba419042 fix(agent): keep a customised process PATH ahead of the login shell
`effective_path_env` always let the login shell's PATH lead. That is
right for a Dock/Finder launch, where the process inherits launchd's
stock PATH and the login shell is the only place the user's toolchain
exists — but wrong whenever the process PATH was customised, because
then it *is* the user's live intent and we were silently overriding
which binary gets resolved.

Measured: with two `codex` installs (an old npm global under homebrew
and a current one under nvm) and a `.zshrc` ordering homebrew first, the
app resolved the old binary while the user's terminal resolved the new
one. It reported a stale model catalog and, worse, rewrote the shared
`~/.codex/models_cache.json` with its own outdated list, so even the
cache fallback went backwards.

Decide the merge direction on a fact rather than a guess: if every entry
of the process PATH is a stock system directory it carries no intent and
the login shell leads; otherwise the process PATH leads. Login-only
entries are still appended either way, so nothing that used to be
reachable stops being reachable.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 20:49:39 +08:00
Fini 9b1451a094 fix(agent): honor thinking-disable for every reasoning family
`model_profile` declares `deepseek-v4-pro { thinking_disabled: true }`,
but the wire layer decided whether to actually send
`thinking:{"type":"disabled"}` from a model-name allowlist that only
covered MiniMax and GLM. DeepSeek matched neither, so the declaration
was silently dropped and every agent-loop turn leaked reasoning until
`max_tokens` ran out — which truncates a `batch_design` mid-JSON while
leaving the short read-only tool calls intact, so the transcript shows a
run of green tool calls and then simply stops.

The same list lived in three places (single-shot body, agent loop,
headless harness) and had already drifted: the harness matched GLM with
`starts_with` where production used `contains`, so a vendor-prefixed id
benchmarked with thinking on and shipped with it off. Collapse all three
onto `op_orchestrator::accepts_thinking_body_field`, next to the profile
table that states the intent, and add a guard test asserting every model
whose profile asks for thinking off can express that on the wire.

DeepSeek's field shape and its `effort=high` default are documented at
https://api-docs.deepseek.com/guides/thinking_mode/ — it is the same
`{"thinking":{"type":...}}` MiniMax and GLM take. Sending it
unconditionally is still wrong: a builtin provider may point at an
endpoint that rejects unknown body fields, so the table stays the
boundary and a new family is one line in one place.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 20:49:39 +08:00
Fini 4f33b8e44f fix(agent): drop hidden models from the codex app-server list
The cache parser filters on `visibility`, but the app-server parser
ignored the protocol's twin field `hidden`, so the two paths that feed
the same picker disagreed on what counts as a listable model. Today the
server withholds internal entries (`codex-auto-review`) from
`model/list` on its own, so this changes nothing against the current
build — it keeps a server that starts sending them from leaking an
unusable model into the picker.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 20:48:31 +08:00
Kayshen-X 54dac3944a test(agent): harden cli timeout probes 2026-07-31 00:57:30 +08:00
Fini 0641cb3f3e fix(desktop): reject conflicting headless modes 2026-07-31 00:32:56 +08:00
Fini 87dbb21d8f test(native): derive settings toggle target from layout 2026-07-31 00:29:34 +08:00
Fini f7004e34bc fix(agent): harden generated mobile geometry 2026-07-31 00:29:22 +08:00
Fini fd627e1654 feat(agent): add prompt asset generation pipeline 2026-07-31 00:29:07 +08:00
Fini 51017a1f7d feat(ai): add visual prompt previews 2026-07-31 00:28:54 +08:00
Kayshen-X 957316dcf2 docs(readme): add star history visualization 2026-07-30 23:06:47 +08:00
Kayshen-X 4b3d3334ba feat(desktop): load signed relay bootstrap 2026-07-30 22:51:48 +08:00
Fini 7639ac2c0f feat(ai): add prompt center 2026-07-30 09:00:40 +08:00
Fini e29ea4229c fix(agent): backfill generated screen interactions 2026-07-30 08:59:39 +08:00
Fini ee44d9b2c0 fix(agent): default ACP agents to basic capability tier 2026-07-30 08:57:17 +08:00
Fini 6932081068 feat(agent): present custom ACP agents as local integrations 2026-07-30 08:57:16 +08:00
Fini ddac26e23a fix(agent): make ACP connection probes generation-safe 2026-07-30 08:57:15 +08:00
Fini 9b01720850 feat(agent): refresh cli model catalogs on picker open 2026-07-30 08:57:14 +08:00
Fini bcce4e7071 fix(panels): fix color variable picker interaction routing 2026-07-30 08:57:13 +08:00
Kayshen-X 76af87930f fix(desktop): show update check status 2026-07-30 02:14:54 +08:00
Kayshen-X 9a09d3cdbe fix(desktop): remove native batch export item 2026-07-30 01:52:44 +08:00
Kayshen-X 7f953dfdf7 fix(host): gate login shell probe on windows 2026-07-30 00:47:21 +08:00
Kayshen-X 7c4d715005 test(relay): pump delayed reauth controls 2026-07-30 00:10:09 +08:00
Kayshen-X 7c2ce58aa3 test(collab): gate unix key store helpers 2026-07-29 23:51:50 +08:00
Kayshen-X d0d0e5b866 test(editor): isolate selection overlay indicators 2026-07-29 23:14:25 +08:00
Kayshen-X 9ac8428799 fix(locator): isolate unix hsm support 2026-07-29 23:14:15 +08:00
Kayshen-X 4a95bd374c test(collab): await rebased smoke acknowledgement 2026-07-29 23:12:27 +08:00
Kayshen-X 0324a61a83 test(collab): await final smoke acknowledgement 2026-07-29 22:55:17 +08:00
Kayshen-X af812daa77 chore(deps): update anyhow to 1.0.103 2026-07-29 22:39:41 +08:00
Kayshen-X bc9993b189 fix(relay): gate development-only host import 2026-07-29 22:39:32 +08:00
Kayshen-X abfebfea87 test(collab): wait for retired worker slot 2026-07-29 22:39:21 +08:00
Kayshen-X c766338fef fix(web): preserve shortcut input ownership 2026-07-29 22:39:08 +08:00
Kayshen-X dc92e63019 fix(auth): isolate bundled runtime symbol 2026-07-29 22:38:54 +08:00