From d580834c9ed6c92f5a299665e1bb3cc988122b37 Mon Sep 17 00:00:00 2001 From: Richard Abrich <774615+abrichr@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:45:25 -0400 Subject: [PATCH 1/3] feat(approval): show who decided each save Needs you's technical details now list who answered each save of an item: a person in Run it or on their phone (signed in or not), or a named agent, rule or system with the first characters of the grant a person signed. A delegate row without a grant hash, on a person's route, or a phone answer without AAL2 reads as unverified, and rows that aren't answers are left out. Display only: what counts as approved is unchanged. --- .../openadapt_desktop/approval_mode.py | 77 ++++++++++++ packages/desktop/src/lib/approvalMode.test.ts | 40 +++++++ packages/desktop/src/lib/approvalMode.ts | 31 +++++ packages/desktop/src/lib/types.ts | 16 +++ packages/desktop/src/screens/NeedsYou.tsx | 13 ++ .../tests/test_engine/test_item_approvers.py | 111 ++++++++++++++++++ 6 files changed, 288 insertions(+) create mode 100644 packages/desktop/tests/test_engine/test_item_approvers.py diff --git a/packages/desktop/openadapt_desktop/approval_mode.py b/packages/desktop/openadapt_desktop/approval_mode.py index c76afee17..6fb5db18b 100644 --- a/packages/desktop/openadapt_desktop/approval_mode.py +++ b/packages/desktop/openadapt_desktop/approval_mode.py @@ -608,6 +608,81 @@ def _check(value: Any) -> dict[str, Any] | None: ) +#: How an answer reached the run: a person in Desktop's Run it or on their +#: phone (a Cloud-signed decision), or a delegate by its own route. +PERSON_ROUTES = frozenset({"desktop_run_it", "cloud_phone"}) +DELEGATE_ROUTES = frozenset({"local_cli", "webhook", "mcp"}) +DELEGATE_KINDS = frozenset({"rule", "agent", "system"}) + + +def approvers(raw: Any) -> list[dict[str, Any]]: + """Who decided each save of an item, from Flow's ``approvals`` rows. + + Closed codes and opaque ids only. A person row names the route and, when + signed in, the Cloud user id. A delegate row names the agent, rule or + system and the ``sha256:`` of the person-signed grant it acted under; a + delegate row without one reads as ``unverified``. A row Desktop can't + read is left out, never shown as a person. + """ + + out: list[dict[str, Any]] = [] + rows = raw if isinstance(raw, list) else [] + for row in rows[:64]: + if not isinstance(row, dict): + continue + decision = row.get("decision") + if decision not in ("approved", "declined"): + continue + route = row.get("route", "desktop_run_it") + decided_by = row.get("decided_by", "human") + entry: dict[str, Any] = { + "decision": decision, + "route": route if route in PERSON_ROUTES | DELEGATE_ROUTES else "unknown", + "kind": "person", + "id": None, + "policy_ref": None, + "signed_in": False, + "verified": True, + } + if decided_by == "automation": + approver = row.get("approver") + approver = approver if isinstance(approver, dict) else {} + kind = approver.get("kind") + approver_id = approver.get("id") + policy_ref = approver.get("policy_ref") + entry.update( + kind=kind if kind in DELEGATE_KINDS else "unknown", + id=approver_id + if isinstance(approver_id, str) and _OPAQUE_ID.fullmatch(approver_id) + else None, + policy_ref=policy_ref + if isinstance(policy_ref, str) and _PREFIXED_SHA256.fullmatch(policy_ref) + else None, + ) + entry["verified"] = ( + entry["kind"] != "unknown" + and entry["id"] is not None + and entry["policy_ref"] is not None + and route in DELEGATE_ROUTES + ) + elif decided_by == "human": + approver_id = row.get("approver_id") + entry["signed_in"] = row.get("approver_kind") == "cloud_user" + entry["id"] = ( + approver_id + if isinstance(approver_id, str) and _OPAQUE_ID.fullmatch(approver_id) + else None + ) + entry["verified"] = route in PERSON_ROUTES and ( + route != "cloud_phone" + or (entry["signed_in"] and row.get("auth_method") == "cloud_aal2") + ) + else: + continue + out.append(entry) + return out + + def _unconfirmed(approval: Mapping[str, Any], reason: str) -> dict[str, Any]: """The same block, now needing a person and never counting as without one.""" @@ -720,6 +795,7 @@ def every_check_passed(group: str, required: frozenset[str]) -> bool: if isinstance(block.get("saves_without_person"), int) else 0 ), + "approvers": approvers(block.get("approvals")), } @@ -806,6 +882,7 @@ def parse_stored(value: Any) -> dict[str, Any] | None: "SwitchApprovalError", "SwitchEnvelope", "accept_switch", + "approvers", "blockers", "confirm_no_person", "flow_switch_api", diff --git a/packages/desktop/src/lib/approvalMode.test.ts b/packages/desktop/src/lib/approvalMode.test.ts index bab54a1af..d24defcfe 100644 --- a/packages/desktop/src/lib/approvalMode.test.ts +++ b/packages/desktop/src/lib/approvalMode.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { + approverSentence, confirmedButNeedsLook, notAvailableLine, offAutomatically, @@ -155,3 +156,42 @@ describe("shared switch cases (openadapt_desktop/work_items.py reads the same fi ).toBe(item.confirmed_but_needs_look); }); }); + +describe("who decided each save", () => { + const person = { + decision: "approved" as const, + route: "desktop_run_it" as const, + kind: "person" as const, + id: "aaaaaaaa-0000-4000-8000-000000000003", + policy_ref: null, + signed_in: true, + verified: true, + }; + + it("names a person in Run it or on their phone", () => { + expect(approverSentence(person)).toBe("Approved by a signed-in person in Run it"); + expect(approverSentence({ ...person, route: "cloud_phone" })).toBe( + "Approved by a signed-in person on their phone", + ); + expect(approverSentence({ ...person, decision: "declined", signed_in: false })).toBe( + "Declined by a person signed out of Cloud in Run it", + ); + }); + + it("names a delegate and the grant a person signed", () => { + const agent = { + ...person, + route: "local_cli" as const, + kind: "agent" as const, + id: "agent:approver-bot", + policy_ref: `sha256:${"e".repeat(64)}`, + signed_in: false, + }; + expect(approverSentence(agent)).toBe( + "Approved by agent agent:approver-bot, under a grant a person signed (eeeeeeeeeeee…)", + ); + expect(approverSentence({ ...agent, policy_ref: null, verified: false })).toMatch( + /can't verify, so a person should look/, + ); + }); +}); diff --git a/packages/desktop/src/lib/approvalMode.ts b/packages/desktop/src/lib/approvalMode.ts index 365f5f2fd..5a1efe6fd 100644 --- a/packages/desktop/src/lib/approvalMode.ts +++ b/packages/desktop/src/lib/approvalMode.ts @@ -9,6 +9,7 @@ import type { ApprovalModeState, ItemApproval, + ItemApprover, OffReason, RequirementResult, SwitchState, @@ -274,3 +275,33 @@ export function whyItCameToYou( } return null; } + +/** The first characters of a grant hash, enough to match the grant a person signed. */ +export function shortGrant(policyRef: string | null | undefined): string | null { + const match = /^sha256:([0-9a-f]{64})$/.exec(String(policyRef ?? "")); + return match ? match[1].slice(0, 12) : null; +} + +/** + * Who decided one save, in plain words: a person in Run it or on their + * phone, or a named agent, rule or system and the grant a person signed. + */ +export function approverSentence(approver: ItemApprover): string { + const verb = approver.decision === "approved" ? "Approved" : "Declined"; + if (approver.kind === "person") { + const where = + approver.route === "cloud_phone" + ? "on their phone" + : approver.route === "desktop_run_it" + ? "in Run it" + : "by an unknown route"; + const who = approver.signed_in ? "a signed-in person" : "a person signed out of Cloud"; + return `${verb} by ${who} ${where}`; + } + const grant = shortGrant(approver.policy_ref); + if (!approver.verified || !approver.id || !grant) { + return `${verb} by a delegate this computer can't verify, so a person should look`; + } + const kind = approver.kind === "rule" ? "rule" : approver.kind === "system" ? "system" : "agent"; + return `${verb} by ${kind} ${approver.id}, under a grant a person signed (${grant}…)`; +} diff --git a/packages/desktop/src/lib/types.ts b/packages/desktop/src/lib/types.ts index abf40e899..2b3c1de55 100644 --- a/packages/desktop/src/lib/types.ts +++ b/packages/desktop/src/lib/types.ts @@ -221,6 +221,22 @@ export interface ItemApproval { failed_check: { code: string; passed: boolean; detail: string } | null; post_checks: { verified: boolean; whole_run_clean: boolean }; saves_without_person: number; + /** Who decided each save, oldest first (absent from older engines). */ + approvers?: ItemApprover[]; +} + +/** One answer to a save: a person, or a delegate a person named in a signed grant. */ +export interface ItemApprover { + decision: "approved" | "declined"; + route: "desktop_run_it" | "cloud_phone" | "local_cli" | "webhook" | "mcp" | "unknown"; + kind: "person" | "agent" | "rule" | "system" | "unknown"; + /** A Cloud user id for a signed-in person, or the delegate's opaque id. */ + id: string | null; + /** sha256: of the person-signed grant a delegate acted under. */ + policy_ref: string | null; + signed_in: boolean; + /** False when the row doesn't hold together (a delegate without a grant, say). */ + verified: boolean; } /** Why "Only ask a person about problems" turned off (design 7.6). */ diff --git a/packages/desktop/src/screens/NeedsYou.tsx b/packages/desktop/src/screens/NeedsYou.tsx index 13c3bdadf..95df31537 100644 --- a/packages/desktop/src/screens/NeedsYou.tsx +++ b/packages/desktop/src/screens/NeedsYou.tsx @@ -9,6 +9,7 @@ import { CMD, engineInvoke, engineTry } from "../lib/engine"; import { attentionCopy } from "../lib/attention"; import { SWITCH_LABEL, + approverSentence, confirmedButNeedsLook, offAutomatically, offReasonSentence, @@ -235,6 +236,18 @@ function AttentionCard({ .filter(Boolean) .join(" · ")} + {(item.approval.approvers ?? []).length > 0 && ( + <> +
Decided by
+
+ +
+ + )} )} {item.halt_step && ( diff --git a/packages/desktop/tests/test_engine/test_item_approvers.py b/packages/desktop/tests/test_engine/test_item_approvers.py new file mode 100644 index 000000000..224ba17ad --- /dev/null +++ b/packages/desktop/tests/test_engine/test_item_approvers.py @@ -0,0 +1,111 @@ +"""Who decided each save: the Desktop projection of Flow's approval rows. + +A person in Run it or on their phone, or a delegate (an agent, rule or +system) named in a grant a person signed. A row that doesn't hold together +reads as unverified, and a row that isn't an answer is left out. +""" + +from __future__ import annotations + +from openadapt_desktop.approval_mode import approvers, item_approval + +_GRANT = "sha256:" + "e" * 64 + + +def _row(**extra: object) -> dict: + row = { + "step_id": "s4", + "request_sha256": "sha256:" + "a" * 64, + "answer_sha256": "sha256:" + "b" * 64, + "decision": "approved", + "decline_reason": None, + "decided_by": "human", + "route": "desktop_run_it", + "approver_kind": "cloud_user", + "approver_id": "aaaaaaaa-0000-4000-8000-000000000003", + "auth_method": "cloud_aal2", + } + row.update(extra) + return row + + +_DELEGATE = { + "decided_by": "automation", + "route": "local_cli", + "approver_kind": "unauthenticated", + "approver_id": None, + "auth_method": "none", + "approver": { + "schema_version": "openadapt.approver-identity/v1", + "kind": "agent", + "auth": "local_cli", + "id": "agent:approver-bot", + "auth_method": "none", + "policy_ref": _GRANT, + "on_behalf_of": None, + }, +} + + +def test_the_record_names_who_decided_each_save() -> None: + rows = approvers( + [ + _row(), + _row(route="cloud_phone"), + _row(**_DELEGATE), + _row( + decision="declined", + decline_reason="wrong_values", + approver_kind="unauthenticated", + approver_id=None, + auth_method="none", + ), + ] + ) + assert [r["route"] for r in rows] == [ + "desktop_run_it", + "cloud_phone", + "local_cli", + "desktop_run_it", + ] + assert rows[0] == { + "decision": "approved", + "route": "desktop_run_it", + "kind": "person", + "id": "aaaaaaaa-0000-4000-8000-000000000003", + "policy_ref": None, + "signed_in": True, + "verified": True, + } + assert rows[1]["verified"] is True + assert rows[2]["kind"] == "agent" + assert rows[2]["id"] == "agent:approver-bot" + assert rows[2]["policy_ref"] == _GRANT + assert rows[2]["verified"] is True + assert rows[3]["signed_in"] is False + + +def test_a_delegate_without_a_signed_grant_or_on_a_persons_route_is_not_verified() -> None: + no_grant = dict(_DELEGATE, approver=dict(_DELEGATE["approver"], policy_ref=None)) + on_person_route = dict(_DELEGATE, route="desktop_run_it") + phone_without_aal2 = _row(route="cloud_phone", auth_method="cloud_aal1") + rows = approvers([_row(**no_grant), _row(**on_person_route), phone_without_aal2]) + assert [r["verified"] for r in rows] == [False, False, False] + # Rows that aren't answers at all are left out, never shown as a person. + assert approvers([{"decision": "maybe"}, "x", _row(decided_by="software")]) == [] + assert approvers(None) == [] + + +def test_the_item_projection_carries_the_approvers() -> None: + block = { + "schema_version": "openadapt.approval-mode-item/v1", + "mode": "each_item", + "decision": "approved", + "needs_person": False, + "pre_checks": [{"code": "each_item", "passed": True, "detail": "approved_by_person"}], + "approvals": [_row()], + } + projected = item_approval(block) + assert projected is not None + assert projected["approvers"][0]["kind"] == "person" + assert projected["approvers"][0]["route"] == "desktop_run_it" From af7e35654c42f2e8e0fd79e02fcada0e2aefcb03 Mon Sep 17 00:00:00 2001 From: Richard Abrich <774615+abrichr@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:17:23 -0400 Subject: [PATCH 2/3] fix(approvers): the phone route is the decision portal, not a Cloud relay --- packages/desktop/openadapt_desktop/approval_mode.py | 9 +++------ packages/desktop/src/lib/approvalMode.test.ts | 2 +- packages/desktop/src/lib/approvalMode.ts | 2 +- packages/desktop/src/lib/types.ts | 2 +- .../tests/test_engine/test_item_approvers.py | 13 +++++++++---- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/desktop/openadapt_desktop/approval_mode.py b/packages/desktop/openadapt_desktop/approval_mode.py index 6fb5db18b..b7d39e8b3 100644 --- a/packages/desktop/openadapt_desktop/approval_mode.py +++ b/packages/desktop/openadapt_desktop/approval_mode.py @@ -609,8 +609,8 @@ def _check(value: Any) -> dict[str, Any] | None: #: How an answer reached the run: a person in Desktop's Run it or on their -#: phone (a Cloud-signed decision), or a delegate by its own route. -PERSON_ROUTES = frozenset({"desktop_run_it", "cloud_phone"}) +#: phone (the runner's decision portal), or a delegate by its own route. +PERSON_ROUTES = frozenset({"desktop_run_it", "decision_portal"}) DELEGATE_ROUTES = frozenset({"local_cli", "webhook", "mcp"}) DELEGATE_KINDS = frozenset({"rule", "agent", "system"}) @@ -673,10 +673,7 @@ def approvers(raw: Any) -> list[dict[str, Any]]: if isinstance(approver_id, str) and _OPAQUE_ID.fullmatch(approver_id) else None ) - entry["verified"] = route in PERSON_ROUTES and ( - route != "cloud_phone" - or (entry["signed_in"] and row.get("auth_method") == "cloud_aal2") - ) + entry["verified"] = route in PERSON_ROUTES else: continue out.append(entry) diff --git a/packages/desktop/src/lib/approvalMode.test.ts b/packages/desktop/src/lib/approvalMode.test.ts index d24defcfe..21fdf5895 100644 --- a/packages/desktop/src/lib/approvalMode.test.ts +++ b/packages/desktop/src/lib/approvalMode.test.ts @@ -170,7 +170,7 @@ describe("who decided each save", () => { it("names a person in Run it or on their phone", () => { expect(approverSentence(person)).toBe("Approved by a signed-in person in Run it"); - expect(approverSentence({ ...person, route: "cloud_phone" })).toBe( + expect(approverSentence({ ...person, route: "decision_portal" })).toBe( "Approved by a signed-in person on their phone", ); expect(approverSentence({ ...person, decision: "declined", signed_in: false })).toBe( diff --git a/packages/desktop/src/lib/approvalMode.ts b/packages/desktop/src/lib/approvalMode.ts index 5a1efe6fd..f3e851527 100644 --- a/packages/desktop/src/lib/approvalMode.ts +++ b/packages/desktop/src/lib/approvalMode.ts @@ -290,7 +290,7 @@ export function approverSentence(approver: ItemApprover): string { const verb = approver.decision === "approved" ? "Approved" : "Declined"; if (approver.kind === "person") { const where = - approver.route === "cloud_phone" + approver.route === "decision_portal" ? "on their phone" : approver.route === "desktop_run_it" ? "in Run it" diff --git a/packages/desktop/src/lib/types.ts b/packages/desktop/src/lib/types.ts index 2b3c1de55..deead620c 100644 --- a/packages/desktop/src/lib/types.ts +++ b/packages/desktop/src/lib/types.ts @@ -228,7 +228,7 @@ export interface ItemApproval { /** One answer to a save: a person, or a delegate a person named in a signed grant. */ export interface ItemApprover { decision: "approved" | "declined"; - route: "desktop_run_it" | "cloud_phone" | "local_cli" | "webhook" | "mcp" | "unknown"; + route: "desktop_run_it" | "decision_portal" | "local_cli" | "webhook" | "mcp" | "unknown"; kind: "person" | "agent" | "rule" | "system" | "unknown"; /** A Cloud user id for a signed-in person, or the delegate's opaque id. */ id: string | null; diff --git a/packages/desktop/tests/test_engine/test_item_approvers.py b/packages/desktop/tests/test_engine/test_item_approvers.py index 224ba17ad..60b613509 100644 --- a/packages/desktop/tests/test_engine/test_item_approvers.py +++ b/packages/desktop/tests/test_engine/test_item_approvers.py @@ -51,7 +51,12 @@ def test_the_record_names_who_decided_each_save() -> None: rows = approvers( [ _row(), - _row(route="cloud_phone"), + _row( + route="decision_portal", + approver_kind="unauthenticated", + approver_id=None, + auth_method="none", + ), _row(**_DELEGATE), _row( decision="declined", @@ -64,7 +69,7 @@ def test_the_record_names_who_decided_each_save() -> None: ) assert [r["route"] for r in rows] == [ "desktop_run_it", - "cloud_phone", + "decision_portal", "local_cli", "desktop_run_it", ] @@ -88,8 +93,8 @@ def test_the_record_names_who_decided_each_save() -> None: def test_a_delegate_without_a_signed_grant_or_on_a_persons_route_is_not_verified() -> None: no_grant = dict(_DELEGATE, approver=dict(_DELEGATE["approver"], policy_ref=None)) on_person_route = dict(_DELEGATE, route="desktop_run_it") - phone_without_aal2 = _row(route="cloud_phone", auth_method="cloud_aal1") - rows = approvers([_row(**no_grant), _row(**on_person_route), phone_without_aal2]) + unknown_route = _row(route="cloud_phone") + rows = approvers([_row(**no_grant), _row(**on_person_route), unknown_route]) assert [r["verified"] for r in rows] == [False, False, False] # Rows that aren't answers at all are left out, never shown as a person. assert approvers([{"decision": "maybe"}, "x", _row(decided_by="software")]) == [] From 525113cb2d6b4fe051d1439841dc7f4b597d913e Mon Sep 17 00:00:00 2001 From: abrichr Date: Sun, 18 Oct 2026 18:22:27 -0400 Subject: [PATCH 3/3] chore: release 0.21.0 Co-Authored-By: Claude Opus 5.5 --- packages/desktop/CHANGELOG.md | 14 ++++++++++++++ packages/desktop/openadapt_desktop/__init__.py | 2 +- packages/desktop/package-lock.json | 4 ++-- packages/desktop/package.json | 2 +- packages/desktop/pyproject.toml | 2 +- packages/desktop/src-tauri/Cargo.lock | 2 +- packages/desktop/src-tauri/Cargo.toml | 2 +- packages/desktop/src-tauri/tauri.conf.json | 2 +- packages/desktop/uv.lock | 2 +- 9 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 5a4fdd4d4..9ebd38c1c 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -1,6 +1,20 @@ # CHANGELOG +## v0.21.0 (2026-10-18) + +### Features + +- **approval**: show who decided each save + +### Bug Fixes + +- **approvers**: the phone route is the decision portal, not a Cloud relay +- **desktop**: release only on a commit whose desktop jobs passed +- **desktop**: price an S3 endpoint as R2 by its host; scope JS/Rust CodeQL + + + ## v0.20.0 (2026-10-18) ### Documentation diff --git a/packages/desktop/openadapt_desktop/__init__.py b/packages/desktop/openadapt_desktop/__init__.py index 694600fdf..601b30681 100644 --- a/packages/desktop/openadapt_desktop/__init__.py +++ b/packages/desktop/openadapt_desktop/__init__.py @@ -21,4 +21,4 @@ +-- backends/ Storage backend plugins """ -__version__ = "0.20.0" +__version__ = "0.21.0" diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json index 524e0a5cd..33d7accac 100644 --- a/packages/desktop/package-lock.json +++ b/packages/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "openadapt-desktop", - "version": "0.20.0", + "version": "0.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openadapt-desktop", - "version": "0.20.0", + "version": "0.21.0", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-notification": "^2", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 08cecdd2c..360ff12e6 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "openadapt-desktop", - "version": "0.20.0", + "version": "0.21.0", "description": "Installed companion for OpenAdapt authoring, teaching, and local pairing", "private": true, "type": "module", diff --git a/packages/desktop/pyproject.toml b/packages/desktop/pyproject.toml index ef0f15f54..29faee9cc 100644 --- a/packages/desktop/pyproject.toml +++ b/packages/desktop/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "openadapt-desktop" -version = "0.20.0" +version = "0.21.0" description = "The OpenAdapt desktop app and local engine: record a data-entry task, run it on a list, and check each saved record" readme = "README.md" requires-python = ">=3.11" diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index cecc29d04..36c76d231 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2449,7 +2449,7 @@ dependencies = [ [[package]] name = "openadapt-desktop" -version = "0.20.0" +version = "0.21.0" dependencies = [ "hex", "reqwest", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index b39416e0d..24a4da70f 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openadapt-desktop" -version = "0.20.0" +version = "0.21.0" description = "Installed companion for OpenAdapt authoring, teaching, and local pairing" authors = ["OpenAdapt AI"] license = "MIT" diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index 822ae9561..aea2772db 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicoverbruggen/tauri-v2-schema/main/schema.json", "productName": "OpenAdapt Desktop", - "version": "0.20.0", + "version": "0.21.0", "identifier": "ai.openadapt.desktop", "build": { "frontendDist": "../dist", diff --git a/packages/desktop/uv.lock b/packages/desktop/uv.lock index fdb884233..307424ab4 100644 --- a/packages/desktop/uv.lock +++ b/packages/desktop/uv.lock @@ -1159,7 +1159,7 @@ wheels = [ [[package]] name = "openadapt-desktop" -version = "0.20.0" +version = "0.21.0" source = { editable = "." } dependencies = [ { name = "cryptography" },