Compare commits

...

20 Commits
latest ... main

Author SHA1 Message Date
bd5bfb0f1e
ci: remove unused remote_contract job from build-and-release workflow (#71)
The remote_contract job was effectively dead code — it only ran on
workflow_dispatch (excluded push and pull_request events) and was
configured with continue-on-error, so it never blocked releases.

Removing it simplifies the pipeline and eliminates the always()
workaround in the release job's if-condition.

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-30 13:59:47 +08:00
d130ea31e2
fix(macos): generate matching WebRTC framework dSYM (#68)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-30 13:24:04 +08:00
1666cbabe7
chore: resolve merge conflict in pubspec.yaml (#67)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-30 12:07:32 +08:00
2295960a74
chore: resolve merge conflict in pubspec.yaml (#66)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-30 12:05:51 +08:00
08ba6e30f7
fix(macos): workaround App Store Connect dSYM validation bug (#62)
* fix(macos): workaround App Store Connect dSYM validation bug for App.framework

* test: mock device and package plugins and increase timeout

- Increase sync loop timeout in thread workspace binding test to avoid flakiness
- Mock device_info and package_info plugins for gateway runtime tests
- Update pubspec.yaml version

* test: fix missing plugin in runtime_controllers_settings_account_test

* build: make sync-version.sh auto-increment build number

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-30 10:48:35 +08:00
6fb1441226
refactor: replace super_clipboard with pasteboard (drop cargokit/Rust) (#55)
* refactor: replace super_clipboard with pasteboard, drop cargokit/Rust

super_clipboard pulled in super_native_extensions (a Rust native layer
built via cargokit), whose precompiled-binary download from GitHub
release assets has been intermittently failing the build ("Connection
closed while receiving data"). It was used for exactly one feature -
reading a clipboard image into the composer - in a single file; the
other 12 imports were dead.

- Swap super_clipboard -> pasteboard (platform-channel, no Rust).
- Rewrite readClipboardImageAsXFileInternal() on Pasteboard.image
  (PNG bytes), collapsing three helpers into one.
- Remove 12 unused super_clipboard imports.
- Regenerated plugin registrants / lockfiles drop super_native_extensions.

Removes the Rust toolchain requirement and the flaky download entirely.
Text copy/paste already used Flutter's built-in Clipboard and is
unaffected.

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

* ci: keep TestFlight package release-only

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:30:26 +08:00
01515f95ca
ci: TestFlight opt-in toggle + Xcode 27 build fixes (#54)
* ci: gate TestFlight behind opt-in toggle + Xcode 27 build fixes

TestFlight is now opt-in (default OFF). A workflow_dispatch boolean
`enable_testflight` (or the `ENABLE_TESTFLIGHT` repo variable) drives a
`prepare.outputs.testflight_enabled` flag that gates the macOS
app-store-pkg build leg and both testflight_ios/testflight_macos upload
legs. Missing Apple signing secrets no longer fail the normal DMG/IPA
release path (package-macos-app-store-pkg.sh hard-exits without them).

Xcode 27 build compatibility:
- Align Apple deployment targets so no pod sits below the app minimum
  (Xcode 27 rejects this): macOS pods + RunnerTests -> 15.6, iOS pods
  -> 15.5 to match the Runner targets.
- Add a `lipo` shim (scripts/xcode-tools/lipo) wired onto PATH in the
  iOS/macOS build phases; Xcode 27 only accepts one `-verify_arch`
  architecture per call while Flutter passes them all at once.
- macOS project hygiene: correct PrivacyInfo.xcprivacy path, set app
  display name + LSApplicationCategoryType.

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

* test: make temp-dir cleanup resilient to concurrent-write races

The assistant execution target tests deleted their temp HOME/workspace
dirs with a raw recursive delete in addTearDown. A background flush
(e.g. controller dispose still persisting state) can keep writing into
the dir while the delete walks it, so the delete races and fails with
"Directory not empty" (errno 39), failing the test on CI.

Route all unguarded teardown deletes through the existing
_resilientDelete helper (re-check existence + retry), and harden that
helper so its final fallback never re-throws — a temp-dir cleanup
failure must never fail a test.

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

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:27:09 +08:00
ca0d8a49a8
fix(installer): resume interrupted asset downloads (#49)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-29 16:36:54 +08:00
194d1dde38
fix(installer): clean up mounted DMG safely (#48)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-29 16:28:07 +08:00
ac6ce56602
fix(installer): download release assets via API (#47)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-29 16:24:17 +08:00
551bbc7b84
Release/v1.1.5 (#46)
* chore(security): add gitleaks config allowlisting vendored/test fixtures

Suppress false positives so `gitleaks detect` is clean:
- third_party/* (cargokit ships a public binary-verification key)
- workspace_management_unit_test.dart (obfuscated "token" fixture)
- gatewayruntime/runtime_test.go (hardcoded "device-1" test key pair)

Real leaked secrets are purged from history, not allowlisted.

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

* chore(security): remove historical secret fixtures

* chore(release): bump build metadata for 1.1.5+2

* chore(release): bump version to 1.1.5+2

* chore(release): bump build metadata for 1.1.5+2

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>

* ci(release): add TestFlight release matrix

* chore(release): bump version to 1.1.5+2

* chore(release): bump build metadata for 1.1.5+2

* ci(release): add TestFlight release matrix

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>

* ci(release): load Vault secrets per-platform in build matrix (#44)

* fix(artifacts): prioritize PDF deliverables in sidebar

* docs(cases): add gateway turn acceptance summary

* ci: add release/* branch source validation workflow (#19)

release/* 仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。
详见 iac_modules/docs/tldr-github-branch-model.md

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci: run desktop integration/patrol tests under xvfb (#22)

Headless Linux runners have no display, so 'flutter test integration_test'
fails to launch the GTK app ('The log reader stopped unexpectedly, or never
started'). Wrap integration/patrol layers in xvfb-run with a 24-bit screen
and install xvfb + mesa DRI driver for headless GL. macOS/local runs are
unaffected (no xvfb-run -> command runs directly).

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: reveal artifact files without blocking

* fix: reveal artifact files without blocking (#20)

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>

* Release/v1.1.5 (#25)

* ci: backport release/* source validation workflow to release/v1.1.5 (#21)

让现有 release/v1.1.5 分支自身包含门禁 workflow(pull_request_target 用 base 分支版本)。
详见 iac_modules/docs/tldr-github-branch-model.md

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: reveal artifact files without blocking (#24)

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>

---------

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: update tested linux labels (#23)

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>

* chore: sync app version to 1.1.5 (#26)

* fix: keep stopped gateway tasks out of pending queue

* chore: add ios release verification assets

* Fix managed bridge token priority

* fix: stabilize iOS login storage and mobile settings

* Refine assistant attachment payload handling

* Fix assistant continue task requeue

* Fix mobile account sign-in flow

* Fix ACP SSE no-result recovery

* Polish assistant UI and add Service Mesh video case

* feat(mobile): redesign mobile UX and iOS native experience

* feat(mobile): move configuration chips to + menu and add left drawer

* feat(mobile): redesign mobile ui to chatgpt minimalist style

* fix(mobile): tweak composer submit button size and wire up settings drawer

* fix(mobile): remove background from send button

* fix(mobile): use blue circle with upward arrow for send button

* feat(mobile): add navigation breadcrumb to return to chat from settings

* feat(mobile): refine composer ui with minimalist modern aesthetic

* Remove OpenClaw direct ACP route

* Add desktop navigation integration test

* Add desktop settings integration test

* Use remote workspace for OpenClaw execution

* Handle gateway default task workspace

* Keep sidebar task order stable

* Hide desktop agent dialog mode

* Release v1.1.3

* Fix Apple preflight for main builds

* Fix Apple preflight for main builds

* Fix Apple preflight for main builds

* Add bulk archive task selection

* Fix assistant skill picker loading

* Stabilize mobile provider sheet test

* chore: prepare v1.1.3 release metadata

* fix: unblock OpenClaw gateway task queue

* fix: keep running task follow-up in current thread

* fix: isolate openclaw e2e artifacts

* fix(assistant): pin task session on submit

* docs: record openclaw gateway e2e cases

* test: align openclaw e2e prompts

* refactor: classify gateway task load

* refactor: classify gateway task load

* feat: sync existing workspace directory artifacts recursively

* Use manual bridge config for ACP runtime

* Fix task refresh layout stability

* chore: update core integration cases and runtime helpers

* fix: stabilize complex openclaw artifact tasks

* fix: repair bridge login sync runtime state

* fix: repair bridge login sync runtime state

* Fix manual bridge save runtime config

* fix: use OpenClaw gateway protocol 4

* fix: use OpenClaw gateway protocol 4

* fix(openclaw): keep artifact runs session scoped

* chore(app): refresh build metadata

* fix(openclaw): recover final task snapshots

* fix(openclaw): recover long SSE task artifacts

* test(app): align thread artifact isolation assertions

* fix(openclaw): keep long artifact recovery synced

* feat(openclaw): implement artifact sync and ignore policies

* Reassociate OpenClaw tasks through Bridge control plane

* Preserve artifacts after interrupted bridge responses

* feat: Remote Desktop UI and Client WebRTC Integration

* refactor: simplify remote desktop UI and add maximize toggle

* fix(webrtc): pass SDP offer and answer as object to conform to backend format

* fix: revert sdpOffer to String to match Bridge SDP expectations

* feat: add runtime logs tab to settings page

* chore: prepare release v1.1.4 (app store compliance, remote desktop fixes, ci verification)

* fix: load nested bridge skills status

* fix(ci): parse provider catalog and gateway providers from capabilities fallback

* test: stabilize OpenClaw gateway active slot regression

* fix: WebRTC remote desktop connection, cleanup local fallback, and ignore .gradle cache

* feat: add collapse toggle to desktop control panel

* fix(runtime): restore skills loading and group rendering

* refactor(ui): eliminate unowned helper sprawl in assistant skill picker

* feat: improve webrtc keyboard mapping and add adaptive resolution default

* feat: improve webrtc keyboard mapping and add adaptive resolution default

* refactor(skills): clean Path B, add retry + auto-refresh, fix silent failures

- Remove Path B (direct WebSocket RPC), unify skills loading via ACP bridge sessionClient
- Delete skillsStatusPayloadInternal fragile nested-key parsing
- SkillsController: explicit error when offline (no more silent empty), auto-retry with 2s/4s backoff
- Auto-refresh on gateway connect via ChangeNotifier listener
- Gateway connect: concurrent Future.wait for independent controller refreshes
- UI: retry button in skill picker empty/error states
- Clean up skillsController from relayChildChangeInternal listeners

* refactor(skills): fix allowErrorPayload validation, improve auto-refresh guard

* feat(ui): apply BoxFit.fill for remote desktop WebRTC view to ensure no blank spaces

* refactor: remove multi-agent orchestration subsystem (Path B)

Remove the entire multi-agent collaboration execution path, including:
- MultiAgentOrchestrator and its 4-phase pipeline (Architect→Engineer→Tester→Iteration)
- ARIS framework preset and mount infrastructure
- Hardcoded model defaults (kimi-k2.5, minimax-m2.7, glm-5)
- Deprecated runCliPromptInternal() and its fallback call chain
- All related types: MultiAgentConfig, AgentWorkerConfig, MultiAgentRole, etc.

This collapses the architecture to a single clean path:
Flutter → GoTaskServiceClient → ACP Transport → Go Bridge → Remote Execution

2886 lines removed across 41 files.

* docs(cases): clean up test cases — remove ai-security-evolution scenario, fix issues

- Delete ai-security-evolution-content-scenario/ (8 files, referenced by removed MANUAL-LOCAL-001A)
- Remove MANUAL-LOCAL-001A from core-integration-manual-cases.md
- Fix duplicate section numbering (#5#6 for general thread scenarios)
- Remove misplaced workspace sync rules from MANUAL-ACP-004 (bridge auth case)
- Update README.md index

* test,docs: fix all stale references to deleted multi-agent subsystem

Test fixes (6 files, -303 lines):
- Delete app_controller_acp_mount_resilience_test.dart (entirely about deleted types)
- Remove multi-agent test cases from gateway_acp_client_auth_test.dart
- Rename _manifestWithDesktopMultiAgentEnabled → _defaultDesktopManifest
  in assistant_execution_target_test, assistant_lower_pane_test,
  mobile_assistant_page_test

Docs fixes (6 files):
- Regenerate public-symbol-inventory.json/md via make docs-public-api
- Remove multi-agent sections from public-api/models-and-config.md,
  app-orchestration.md, runtime-contracts.md
- Fix xworkmate/ → xworkmate-app/ paths in cloud-session doc
- Remove multiAgent references from app-external-service-api-test-matrix.md

* docs: add architecture README with categorized navigation

* docs(architecture): fix critical accuracy errors, stale refs, paths

Accuracy fixes:
- app-orchestration.md: remove non-existent constructor params
- models-and-config.md: remove wrong multiAgent field from SettingsSnapshot
- runtime-contracts.md: add missing multiAgent/collaborationMode/routingHint fields

Stale multi-agent refs:
- unified-routing-architecture.md: agent/multi-agent → agent (含 bridge 转发)
- bridge-runtime-routing-map.md: multi-agent tasks → multi-agent forwarding tasks
- cross-repo-task-state-workflow.md: remove multi-agent orchestration from mermaid
- runtime-contracts.md, feature-surfaces.md: 多 agent → agent

Organization:
- Move cloud-session-service and stage4-helper to archive/
- Fix 22 xworkmate/ → xworkmate-app/ paths in archive doc
- Fix XWorkmate.svc.plus repo name in simple-theme-default.md
- Update README.md index and public-api/README.md coverage stats (132/590)

* docs: rewrite README — fix repo name, remove stale multi-agent refs, add dependencies

- Title: XWorkmate → xworkmate-app
- Remove references to deleted multi-agent orchestration
- Fix download links: xworkmate.svc.plus → xworkmate-app
- Replace machine-specific /Users/shenlan/... paths with relative links
- Add Dependencies section: xworkmate-bridge, xworkspace-core-skills,
  openclaw-multi-session-plugins, playbooks
- Consolidate Learn More links to repo-relative paths

* fix desktop workspace stream fallback

* Fix WebRTC desktop video stream rendering and inputs

* refactor: eliminate dead codex_runtime methods, add anti-fallback policy

codex_runtime.dart (-290 lines):
- Remove 17 dead methods behind UnsupportedError guard
  (findCodexBinary, startStdio, request, startThread, resumeThread,
   sendMessage, interrupt, getAccount, listModels, listSkills, stop,
   dispose, _resolveLaunchConfiguration + 3 @visibleForTesting wrappers)
- Remove 10 dead fields (_process, _state, _pendingRequests, _events, etc.)
- Remove ChangeNotifier mixin (nothing to notify)
- Keep only model types, enums, and standalone helper functions

AGENTS.md (+21 lines):
- Add Fallback and Dead Code Elimination Policy section
- Forbidden: cascading fallbacks, lingering DEPRECATED code,
  dead code behind guards, silent catch blocks, redundant indirection,
  excessive JSON key probing
- Required: inline WHY comments on every retained fallback chain

Additional cleanup:
- gateway_acp_client.dart: remove unused _GatewayAcpSessionUpdate class
- runtime_controllers_entities.dart: replace _canRefreshThroughRuntime
  with runtimeInternal.isConnected
- runtime_models_gateway_entities.dart: relocate CollaborationAttachment

* Simplify RTCVideoView constraints and disable adaptive resolution by default

* refactor: remove stale runtime fallbacks

* fix: preserve openclaw failure artifacts

* fix: use default native track attach for desktop stream

* fix: poll openclaw task handle to terminal snapshot

* update architecture docs

* fix: finalize openclaw task polling results

* feat(xworkmate): optimize desktop thread actions and Go task service client

* docs: add cross-repo architecture chain maps and risk analysis

- Add 4 chain maps: task-execution, artifact-lifecycle, session-recovery, bridge-distributed
- Add cross-repo call analysis with top-10 fragile points
- Update AGENTS.md with 'Cross-Repo Architecture Chain Maps' section
- Document artifact path gap: OpenClaw tools output to ~/.openclaw/media/ but plugin export scans tasks/<session>/<run>/

* fix(webrtc): resolve remote desktop black screen by properly binding remote video tracks and removing legacy Plan B constraints

* fix: remaining webrtc stream and test artifact changes

* fix(arch): A1-A3 app layer anti-patterns cleanup

* fix(arch): conservative fallback for gateway error codes

* fix: merged cleanup branch and stashed fixes

* add design doc: multi-session-plugin-optimization

* fix: allow stopping archived tasks

* fix: sync openclaw terminal snapshots in app

* fix: resolve openclaw partial artifacts and eliminate legacy fallback code

* fix(assistant): clear pending tool calls when task completes to fix sticky running status

* refactor: Remove OpenClaw rigid time limits and false positive no-exported-artifacts judgment

* fix(ci): keep macos/ios build lanes running when Apple signing secrets are missing

The release preflight used to set should_build_platform=false whenever any
Apple signing secret was unset, which silently skipped the entire macos dmg
and ios ipa lanes (build + upload gated on that flag). Result: releases only
shipped linux, windows and android artifacts even when the iOS/macOS lanes
were otherwise healthy.

Make the preflight always release the lane, but emit a :⚠️: and
annotate the skip_reason when a secret is missing. The iOS branch in
build_matrix_artifacts.sh now picks the signed vs unsigned build path based
on actual secret availability instead of should_release alone, so it falls
back to flutter build ios --no-codesign + zip Runner.app whenever a secret
is absent. package-flutter-mac-app.sh already handled the no-secret case
locally (ad-hoc codesign --sign -) and needs no change.

Behavior matrix:
  macos: secret present -> signed DMG; secret missing -> unsigned DMG
  ios:   secret present + release -> signed IPA
         secret present + non-release -> unsigned zip
         secret missing (any) -> unsigned zip

* fix(chat): drop root-level expectedArtifactDirs to satisfy chat.send schema

- Remove the unexpected property at the root of gateway task metadata.
  Keep the value nested in xworkmateTaskArtifactContract where the
  OpenClaw chat.send schema allows it (-32002: invalid chat.send params).
- Drop dead local vars and the unused asInt helper in OpenClaw task
  association parsing.
- Remove the obsolete 'sendChatMessage restarts before handling
  OpenClaw artifact guard results' test superseded by the new terminal
  artifact failure test.

* fix(ci): drop ripgrep dependency from check-no-app-ffi.sh

The Flutter verification lane runs on Ubuntu 22.04 without ripgrep
installed, so the FFI integration guard silently fell through and
printed 'No app-side Codex FFI integration artifacts found' on every
run. Replace rg with the POSIX grep -RInE that ships with the runner,
keep the same excludes (check-no-app-ffi.sh, Pods, ephemeral, build,
.dart_tool) and emit the actual offending matches so the gate fails
loudly when a forbidden reference reappears.

* Document OpenClaw artifact dirs protocol boundary

* feat: pass OpenClaw artifact dir whitelist

* Remove Patrol from macOS package

* Add OpenClaw thin adapter refactor plan

* refactor/app-thread-key

* refactor: explicitly pass openclawSessionKey in task start

* Refactor OpenClaw task integration as thin adapter

* refactor: align OpenClaw session key state flow

* chore: retire rust ffi scaffold

* docs clarify openclaw artifact workspace ownership

* ci: read release secrets from vault

* fix: merge workflow env blocks

* fix: skip remote contract on push

* fix: align OpenClaw task key flow

* chore: retrigger workflow after vault data setup

* fix: backfill OpenClaw artifacts on sidebar refresh

* fix: trim OpenClaw task prompt context

* fix: keep OpenClaw artifact sync polling

* fix: require OpenClaw artifact export before completion

* fix: unify bridge auth token for desktop connect

* fix: keep bridge token usable after sync block

* fix: accept review bridge token from account sync

* fix: keep syncing partial OpenClaw artifacts

* Improve assistant task UX

* Sync artifact sidebar with selected task

* fix: show remote desktop first-frame state

* chore: log remote desktop WebRTC stats

* Stabilize OpenClaw artifact sync

* Add AI workspace management provisioning flow

* Fix gateway dispatch test pipeline

* Harden workspace prechecks

* Add AI workspace management provisioning flow

* Fix gateway dispatch test pipeline

* Harden workspace prechecks

* Relax workspace OS checks and add YAML import/export

* Relax workspace OS checks and add YAML import/export

* Make workspace advanced configs extensible

* Make workspace advanced configs extensible

* Clarify bridge DNS precheck message

* Clarify bridge DNS precheck message

* Relax workspace prechecks and add post-deploy validation

* Relax workspace prechecks and add post-deploy validation

* Improve workspace status summary wording

* Improve workspace status summary wording

* Add default bridge save action

* Add default bridge save action

* fix: isolate remote desktop webrtc sessions

* fix: isolate remote desktop webrtc sessions

* fix: smooth remote desktop input over webrtc

* fix: smooth remote desktop input over webrtc

* feat: align workspace ready actions and naming

* feat: align workspace ready actions and naming

* fix: clear desktop first-frame overlay after decode

* fix: clear desktop first-frame overlay after decode

* fix: use renderer first-frame signal for desktop video

* fix: use renderer first-frame signal for desktop video

* fix: split desktop mouse move data channel

* fix: split desktop mouse move data channel

* fix(app): bound OpenClaw artifact sync polling

* chore: remove stale Flutter code

* feat(assistant): include attachment source paths in gateway prompts

* chore(desktop): remove advanced options panel

* fix(desktop): bound WebRTC offer wait

* feat(workspace): run remote setup script

* fix: prioritize managed bridge sync state

* feat: add explicit gateway task case hints for openclaw-gateway-e2e-regression

* fix(settings): update account panel and assistant connection state

* fix: preserve primary bridge auth token

* test: ignore transient cleanup races

* fix: allow unsigned macos CI packaging

* fix: support macos validation on bash 3

* chore: temporarily disable desktop ai workspace

* ci: move remote_contract to test gate between build and release

Reposition the remote provider contract check as a skippable test-stage
quality gate (needs: build, continue-on-error) so it can never block
build or release. release uses always() to wait without being gated.

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

* chore: update macOS deployment target to 14.0 and commit pending changes

* fix(gateway): day-1 stability — stop infinite "running" and un-stoppable tasks

Symptom: a gateway turn shows "任务运行中..." forever and 停止 has no effect,
even though the OpenClaw gateway has already finished (ACP_HTTP_CONNECTION_CLOSED).

- T3: add a hard deadline to the running-handle poll branch so the client no
  longer polls forever when tasks.get keeps returning "running". Budget is
  derived from taskLoadClass (10/30/60min, aligned with the bridge) + grace;
  on timeout the turn lands in a recoverable `interrupted` state
  (OPENCLAW_RUN_POLL_TIMEOUT) prompting the user to resend.
- T4: make 停止 locally authoritative — capture the association, mark the turn
  aborted immediately (clears pending, exits the poll loop), then fire
  tasks.cancel best-effort so a hung/failed cancel RPC can't block termination.
- T6: applyGatewayChatFailureInternal now authoritatively clears the pending
  flag (both raw + normalized key). Previously runOpenClawGatewayQueuedTurnInternal's
  finally never cleared it, leaving "error shown but still running".

Full cross-repo analysis + remaining TODO in docs/cases/06.

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

* fix(gateway): harden OpenClaw task recovery tests

* docs(cases/06): mark T7/T8/T9 done with impl locations & design trade-offs

Records the durable per-session run-registry implementation (bridge branch
fix/gateway-durable-run-registry): T7 gateway-unconfirmed fallback, T8 terminal
result cache, T9 DeadlineAt interrupt — with the trade-offs (no gatewayruntime
pending-map rewrite; per-session in-memory store not yet cross-restart durable;
T9 only force-terminates when the gateway is unconfirmed) and the test names that
cover each.

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

* docs(cases): record local bridge runtime validation

* ci: refresh app workflows for node 24

* test: keep layered flutter tests aligned with repo

* test: align gateway recovery expectations

* test: stabilize assistant gateway recovery cases

* docs(cases/06): record definitive root cause — xworkmate.* gateway protocol drift

Adds the 2026-06-26 decisive finding: the bridge forwards `xworkmate.*` method
names the OpenClaw 2026.6.2 gateway does not implement (it uses native
tasks.get/list/cancel and artifacts.list/get/download). Documents the corrected
end-to-end turn timeline with the three break points (tasks.get unknown method;
{taskId}-only param shape + taskId!=runId; artifacts.* drift blocking .md delivery),
the evidence (gateway source + schema + CHANGELOG), the implemented task-lifecycle
fix, and the precisely-specified remaining work (artifact-method alignment + test
fixture migration). Corrects the earlier (wrong) "push/pull mismatch" conclusion.

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

* docs(cases/06): correct root cause — plugin not loaded, not protocol drift

Live verification disproved the earlier "xworkmate.* protocol namespace drift"
conclusion. The xworkmate.* gateway methods are REAL — registered at runtime by
the openclaw-multi-session-plugins plugin (index.ts registerGatewayMethod). The
actual failure: the running OpenClaw gateway did not load that plugin because its
source path was the ephemeral /private/tmp/openclaw-multi-session-plugins/... and
the gateway booted (09:21) ~9h before those files were populated (18:40), so it
started with 5 plugins (no multi-session) and every xworkmate.* returned
"unknown method". Restarting the gateway loads 6 plugins and the methods work
(errors shift to plugin-level param validation).

Changes:
- Add a corrected conclusion banner up top distinguishing the primary root cause
  (plugin load) from the T1-T9 robustness hardening.
- Replace the wrong "protocol drift / native alignment" section with the
  plugin-not-loaded root cause + evidence + the abandoned-branch note
  (fix/gateway-task-protocol-alignment must NOT be merged).
- Fix failure-row 10, T13 (runtime-state check now covers gateway plugin load),
  and the landing-order to put the plugin fix as step 0.
- Cross-reference openclaw-gateway-e2e-regression/ROOT_CAUSE_ANALYSIS.md (which
  was already correct about the 4-layer chain).

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

* docs(cases/06): definitive 4-layer chain incl. multi-session plugin + live verification

Rewrites the timeline (§1) and topology (§2) as the correct FOUR-layer chain
App → bridge → openclaw-multi-session-plugins → OpenClaw gateway, and documents the
plugin's multi-session/multi-thread role: session mapping (appThreadKey⇄openclawSessionKey),
per-(session,run) artifactScope = tasks/<sanitize(sessionKey)>/<runId>, the strict
sessionKey/runId/artifactScope triplet validation, and the expectedArtifactDirs
workspace-root fallback scan.

Live-verified against 127.0.0.1:8787 (plugin loaded, commit 2333c3e):
- session.prepare returns a real mapping; chat.send returns runId; xworkmate.tasks.get
  is handled by the plugin but returns no_native_task_record with an empty task scope
  (chain reaches the plugin layer; the agent run produced no queryable task / no file —
  a layer-4 execution/landing issue).

Adds §7 stability improvements grounded in this live run:
- S0 install the plugin from a stable path (not /private/tmp) — the primary reliability fix.
- S1 expectedArtifactDirs was [] → the plugin's workspace-root fallback is inert; bridge
  should always pass default dirs (reports/, artifacts/).
- S2 no_native_task_record status ambiguity (running vs completed-without-artifact).
- S3 sessionKey/runId/artifactScope triplet consistency (don't pre-prefix agent:main:).
- S4 runtime observability across all four layers.

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

* docs(cases/06): S0 done — stable plugin install verified

Root cause of the plugin not loading was a symlink
~/.openclaw/extensions/openclaw-multi-session-plugins -> /tmp/... (ephemeral).
Replaced with a real dir, registered via `openclaw plugins install --force`,
restarted the gateway: now boots with "6 plugins ... openclaw-multi-session-plugins"
from the stable path, provenance warning gone, and xworkmate.session.prepare returns
the real plugin mapping (no bridge fallback). Survives restart.

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

* docs(cases/06): mark S1 done — default expectedArtifactDirs (live-verified, bridge 0280893)

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

* fix(gateway): harden OpenClaw polling and acceptance notes

* docs(case06): close out acceptance log

* docs(case06): reconcile TODO status + consolidated cross-repo stability backlog

- Flip stale §5 checkboxes (T1/T2/T3/T4/T6) to done with code anchors —
  they had lagged behind §2/§6 which already marked them merged.
- Add §9: authoritative full-chain status across all 4 repos' main
  (app/bridge/openclaw/playbooks HEADs), the completed stability closure,
  and the precise remaining backlog (S1 redo, S2 status ambiguity, T8b
  cross-restart persistence) with acceptance criteria + anti-regression
  recommendations.

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

* fix(macos): suppress file selector deprecation warning

* docs(gateway): map durable agent terminal recovery

* docs(cases/06): 4-layer chain full live evaluation — end-to-end PASS

Live-verified one gateway turn across all four layers against 8787 (bridge 188ca4b,
gateway 6 plugins): session.start → real plugin session.prepare mapping → chat.send
→ xworkmate.tasks.get returns status=completed, constraintSatisfied=True, and
summary.md (438B) actually landed in tasks/<sani(sessionKey)>/<runId>/ and is
retrievable via xworkmate.artifacts.export. All xworkmate.* gateway methods ✓.
T12 metrics all 0 (no resilience fallback needed). Supersedes the earlier
no_native_task_record observation, which was a derived symptom of the plugin not
being loaded (the S0 symlink root cause).

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

* fix(artifacts): route signed downloads through active bridge

* fix(prompt): simplify gateway workspace context to avoid conflicting paths (S5)

Every gateway turn's prompt prefix injected three near-duplicate absolute paths:
currentTaskWorkspace + localWorkspace + remoteWorkspaceHint. localWorkspace is the
App's LOCAL thread dir (~/.xworkmate/threads/...) which the gateway agent cannot
access, and remoteWorkspaceHint duplicates currentTaskWorkspace. The conflicting
paths leave the agent unsure where to work and can block conversation continuation.

For gateway turns the prompt now carries only currentTaskWorkspace (the plugin owns
the artifact scope); localWorkspace is kept only for non-gateway (local agent runs
there); remoteWorkspaceHint is dropped when equal to currentTaskWorkspace. sessionKey
is kept (short, not a path). UI is unaffected (chat bubble shows the raw user message;
the prompt-debug parser only special-cases Execution context / Preferred skills /
Attached files). Tests updated; assistant_execution_target_test green (74).

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

* fix(macos): close file selector type branch

* fix(gateway): keep polling undecorated running snapshots

* docs(runbooks): record gateway turn stability case

* fix(artifacts): prioritize PDF deliverables in sidebar

* fix(artifacts): prioritize PDF deliverables in sidebar

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>

* docs(cases): add gateway turn acceptance summary

* ci: add release/* branch source validation workflow (#19)

release/* 仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。
详见 iac_modules/docs/tldr-github-branch-model.md

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci: run desktop integration/patrol tests under xvfb (#22)

Headless Linux runners have no display, so 'flutter test integration_test'
fails to launch the GTK app ('The log reader stopped unexpectedly, or never
started'). Wrap integration/patrol layers in xvfb-run with a 24-bit screen
and install xvfb + mesa DRI driver for headless GL. macOS/local runs are
unaffected (no xvfb-run -> command runs directly).

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: reveal artifact files without blocking

* fix: reveal artifact files without blocking (#20)

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>

* Release/v1.1.5 (#25)

* ci: backport release/* source validation workflow to release/v1.1.5 (#21)

让现有 release/v1.1.5 分支自身包含门禁 workflow(pull_request_target 用 base 分支版本)。
详见 iac_modules/docs/tldr-github-branch-model.md

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: reveal artifact files without blocking (#24)

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>

---------

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: update tested linux labels (#23)

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>

* chore: sync app version to 1.1.5

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Cowork 3P <cowork-3p@localhost>
Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(assistant): keep manual bridge usable when signed out of svc.plus

The gateway connection resolver short-circuited to "请先登录 svc.plus"
whenever the account was signed out, before checking whether a manual
bridge was configured or whether capability discovery was still running.
A saved manual bridge could therefore never be used while signed out.

- Only emit the signed-out prompt when neither an account session nor a
  manual bridge is configured (`!accountSignedIn && !bridgeConfigured`).
- Gate the sync-blocked branch on `accountSignedIn` so it no longer
  hijacks the manual-bridge discovery path.

Adds tests covering manual-bridge discovery and discovery-failure while
signed out. See docs/cases/manual-bridge-login-state/README.md.

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

* security(docs): remove plaintext review credentials, inject from .env

The svc.plus review password and the two bridge tokens were committed in
plaintext across the manual case / API test docs. Replace every value
with a `.env` / secret-store reference and add a tracked .env.example
template. Harden .gitignore (.env.*, *.local.env, secrets.env) while
keeping !.env.example.

Note: git history was rewritten separately to purge the leaked values;
the credentials must be rotated regardless.

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

* chore(security): add gitleaks config allowlisting vendored/test fixtures

Suppress false positives so `gitleaks detect` is clean:
- third_party/* (cargokit ships a public binary-verification key)
- workspace_management_unit_test.dart (obfuscated "token" fixture)
- gatewayruntime/runtime_test.go (hardcoded "device-1" test key pair)

Real leaked secrets are purged from history, not allowlisted.

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

* fix(assistant): keep manual bridge usable when signed out of svc.plus

The gateway connection resolver short-circuited to "请先登录 svc.plus"
whenever the account was signed out, before checking whether a manual
bridge was configured or whether capability discovery was still running.
A saved manual bridge could therefore never be used while signed out.

- Only emit the signed-out prompt when neither an account session nor a
  manual bridge is configured (`!accountSignedIn && !bridgeConfigured`).
- Gate the sync-blocked branch on `accountSignedIn` so it no longer
  hijacks the manual-bridge discovery path.

Adds tests covering manual-bridge discovery and discovery-failure while
signed out. See docs/cases/manual-bridge-login-state/README.md.

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

* chore(security): remove historical secret fixtures

* ci(release): load Vault secrets per-platform in build matrix

The build matrix loaded all 17 signing secrets in one shared block for
every platform. vault-action's ignoreNotFound only suppresses path-level
404s, not field-level "No match data" errors, so a single missing field
(e.g. APPLE_MAC_PROVISION_PROFILE_BASE64) failed every leg — including
linux/windows/android that need no Apple secrets.

Split the load into per-OS-family steps gated by matrix.platform:
- Apple (macos/ios): Apple cert + provisioning + keychain + export method
- Windows: WINDOWS_PFX_* + codesign subject
- Android: ANDROID_KEYSTORE_* + key alias/password
Linux requests nothing.

Also drop APP_STORE_CONNECT_* from the build matrix: only
testflight_upload.sh consumes them and it runs in the release job, which
loads them itself. The build matrix no longer depends on them.

Add shell: bash to the Export step (its `{ … } >> $GITHUB_ENV` brace
syntax is bash-only and would fail under the default pwsh on windows).

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

* ci: load Vault secrets per-platform in build matrix (#43)

The build matrix loaded every signing secret in one shared block for all
platforms. vault-action's ignoreNotFound only suppresses path-level 404s,
not field-level "No match data" errors, so a single missing field failed
every leg — including linux/windows/android that need no Apple secrets.

Split the load into per-OS-family steps gated by matrix.platform (Apple
for macos/ios, Windows, Android); linux requests nothing. Add shell: bash
to the Export step (its `{ … } >> $GITHUB_ENV` brace syntax is bash-only
and would fail under the default pwsh on windows).

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat: add one-line XWorkmate installer (#42)

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>

---------

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Cowork 3P <cowork-3p@localhost>

---------

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Cowork 3P <cowork-3p@localhost>
2026-06-29 15:59:08 +08:00
fa161247d3
Ci/vault secrets per platform release (#45)
* chore(security): add gitleaks config allowlisting vendored/test fixtures

Suppress false positives so `gitleaks detect` is clean:
- third_party/* (cargokit ships a public binary-verification key)
- workspace_management_unit_test.dart (obfuscated "token" fixture)
- gatewayruntime/runtime_test.go (hardcoded "device-1" test key pair)

Real leaked secrets are purged from history, not allowlisted.

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

* chore(security): remove historical secret fixtures

* chore(release): bump build metadata for 1.1.5+2

* chore(release): bump version to 1.1.5+2

* chore(release): bump build metadata for 1.1.5+2

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>

* ci(release): add TestFlight release matrix

* chore(release): bump version to 1.1.5+2

* chore(release): bump build metadata for 1.1.5+2

* ci(release): add TestFlight release matrix

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>

* ci(release): load Vault secrets per-platform in build matrix

The build matrix loaded all 17 signing secrets in one shared block for
every platform. vault-action's ignoreNotFound only suppresses path-level
404s, not field-level "No match data" errors, so a single missing field
(e.g. APPLE_MAC_PROVISION_PROFILE_BASE64) failed every leg — including
linux/windows/android that need no Apple secrets.

Split the load into per-OS-family steps gated by matrix.platform:
- Apple (macos/ios): Apple cert + provisioning + keychain + export method
- Windows: WINDOWS_PFX_* + codesign subject
- Android: ANDROID_KEYSTORE_* + key alias/password
Linux requests nothing.

Also drop APP_STORE_CONNECT_* from the build matrix: only
testflight_upload.sh consumes them and it runs in the release job, which
loads them itself. The build matrix no longer depends on them.

Add shell: bash to the Export step (its `{ … } >> $GITHUB_ENV` brace
syntax is bash-only and would fail under the default pwsh on windows).

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

---------

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-29 15:56:26 +08:00
d890acb661
feat: add one-line XWorkmate installer (#42)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-29 15:48:49 +08:00
898b723780
ci: load Vault secrets per-platform in build matrix (#43)
The build matrix loaded every signing secret in one shared block for all
platforms. vault-action's ignoreNotFound only suppresses path-level 404s,
not field-level "No match data" errors, so a single missing field failed
every leg — including linux/windows/android that need no Apple secrets.

Split the load into per-OS-family steps gated by matrix.platform (Apple
for macos/ios, Windows, Android); linux requests nothing. Add shell: bash
to the Export step (its `{ … } >> $GITHUB_ENV` brace syntax is bash-only
and would fail under the default pwsh on windows).

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:45:07 +08:00
Haitao Pan
b63a9c1dbb chore(security): remove historical secret fixtures 2026-06-29 12:29:37 +08:00
b44b0e7b0c Merge pull request #34 from ai-workspace-lab/backport/manual-bridge-login-state
fix(assistant): keep manual bridge usable when signed out of svc.plus
2026-06-29 12:05:34 +08:00
Haitao Pan
73479a7f39 fix(assistant): keep manual bridge usable when signed out of svc.plus
The gateway connection resolver short-circuited to "请先登录 svc.plus"
whenever the account was signed out, before checking whether a manual
bridge was configured or whether capability discovery was still running.
A saved manual bridge could therefore never be used while signed out.

- Only emit the signed-out prompt when neither an account session nor a
  manual bridge is configured (`!accountSignedIn && !bridgeConfigured`).
- Gate the sync-blocked branch on `accountSignedIn` so it no longer
  hijacks the manual-bridge discovery path.

Adds tests covering manual-bridge discovery and discovery-failure while
signed out. See docs/cases/manual-bridge-login-state/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:00:44 +08:00
67401a643c Merge pull request #32 from ai-workspace-lab/release/v1.1.5
Release/v1.1.5
2026-06-29 11:22:13 +08:00
b1ec29fa36 fix: reveal artifact files without blocking (#24)
Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
2026-06-28 15:44:29 +08:00
d048d7ec9c ci: backport release/* source validation workflow to release/v1.1.5 (#21)
让现有 release/v1.1.5 分支自身包含门禁 workflow(pull_request_target 用 base 分支版本)。
详见 iac_modules/docs/tldr-github-branch-model.md

Co-authored-by: Haitao Pan <haitao.pan@xworkmate.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:41:08 +08:00
42 changed files with 780 additions and 366 deletions

View File

@ -30,6 +30,15 @@ on:
- ".github/actions/setup-flutter-sdk/action.yml"
- ".github/workflows/build-and-release.yml"
workflow_dispatch:
inputs:
enable_testflight:
description: "Build & upload TestFlight (macOS/iOS App Store) artifacts"
type: boolean
default: false
enable_github_release:
description: "Upload assets to GitHub Release"
type: boolean
default: true
permissions:
contents: read
@ -49,6 +58,8 @@ jobs:
contents: write
outputs:
should_release: ${{ steps.flags.outputs.should_release }}
github_release_enabled: ${{ steps.flags.outputs.github_release_enabled }}
testflight_enabled: ${{ steps.flags.outputs.testflight_enabled }}
release_tag: ${{ steps.meta.outputs.release_tag }}
release_title: ${{ steps.meta.outputs.release_title }}
release_notes: ${{ steps.meta.outputs.release_notes }}
@ -61,6 +72,10 @@ jobs:
- name: Determine release mode
id: flags
shell: bash
env:
ENABLE_GITHUB_RELEASE_INPUT: ${{ github.event.inputs.enable_github_release }}
ENABLE_TESTFLIGHT_INPUT: ${{ github.event.inputs.enable_testflight }}
ENABLE_TESTFLIGHT_VAR: ${{ vars.ENABLE_TESTFLIGHT }}
run: |
if [[ "${GITHUB_REF:-}" == refs/tags/v* || "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" || "${GITHUB_REF:-}" == "refs/heads/main" ]]; then
echo "should_release=true" >> "$GITHUB_OUTPUT"
@ -68,6 +83,22 @@ jobs:
echo "should_release=false" >> "$GITHUB_OUTPUT"
fi
if [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" && "${ENABLE_GITHUB_RELEASE_INPUT:-}" == "false" ]]; then
echo "github_release_enabled=false" >> "$GITHUB_OUTPUT"
else
echo "github_release_enabled=true" >> "$GITHUB_OUTPUT"
fi
# TestFlight is opt-in (default OFF). Enabled only when explicitly
# requested via the workflow_dispatch input or the ENABLE_TESTFLIGHT
# repo/org variable. Keeps missing Apple signing secrets from failing
# the normal DMG/IPA release path.
if [[ "${ENABLE_TESTFLIGHT_INPUT:-}" == "true" || "${ENABLE_TESTFLIGHT_VAR:-}" == "true" ]]; then
echo "testflight_enabled=true" >> "$GITHUB_OUTPUT"
else
echo "testflight_enabled=false" >> "$GITHUB_OUTPUT"
fi
- name: Compute release metadata
id: meta
shell: bash
@ -122,6 +153,18 @@ jobs:
artifact_name: build-macos-arm64-dmg
artifact_paths: |
dist/macos/*.dmg
- platform: macos
arch: arm64
package: app-store-pkg
# Quoted so the `matrix.release_only != 'true'` guard compares
# string-to-string. A YAML boolean here coerces to a number in
# GitHub expressions (true -> 1, 'true' -> NaN), making the guard
# always true and building this release-only leg on every PR.
release_only: "true"
runs_on: macos-14
artifact_name: build-macos-arm64-pkg
artifact_paths: |
dist/macos-app-store/*.pkg
- platform: ios
arch: arm64
package: ipa
@ -148,9 +191,12 @@ jobs:
- name: Checkout source
uses: actions/checkout@v7
- name: Load Vault secrets
id: vault
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
# Secrets are loaded per-platform so a missing/extra field for one OS
# family never fails the matrix legs of the others (vault-action's
# ignoreNotFound does NOT suppress field-level "No match data" errors).
- name: Load Vault secrets (Apple)
id: vault_apple
if: ${{ (matrix.platform == 'macos' || matrix.platform == 'ios') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: hashicorp/vault-action@v4
with:
url: ${{ env.VAULT_ADDR }}
@ -160,35 +206,61 @@ jobs:
ignoreNotFound: true
secrets: |
kv/data/github-actions/xworkmate-app XWORKMATE_SIGN_IDENTITY | XWORKMATE_SIGN_IDENTITY ;
kv/data/github-actions/xworkmate-app WINDOWS_PFX_BASE64 | WINDOWS_PFX_BASE64 ;
kv/data/github-actions/xworkmate-app WINDOWS_PFX_PASSWORD | WINDOWS_PFX_PASSWORD ;
kv/data/github-actions/xworkmate-app WINDOWS_CODESIGN_SUBJECT | WINDOWS_CODESIGN_SUBJECT ;
kv/data/github-actions/xworkmate-app APPLE_CERT_P12_BASE64 | APPLE_CERT_P12_BASE64 ;
kv/data/github-actions/xworkmate-app APPLE_CERT_PASSWORD | APPLE_CERT_PASSWORD ;
kv/data/github-actions/xworkmate-app APPLE_PROVISION_PROFILE_BASE64 | APPLE_PROVISION_PROFILE_BASE64 ;
kv/data/github-actions/xworkmate-app APPLE_MAC_PROVISION_PROFILE_BASE64 | APPLE_MAC_PROVISION_PROFILE_BASE64 ;
kv/data/github-actions/xworkmate-app APPLE_KEYCHAIN_PASSWORD | APPLE_KEYCHAIN_PASSWORD ;
kv/data/github-actions/xworkmate-app APPLE_EXPORT_METHOD | APPLE_EXPORT_METHOD ;
kv/data/github-actions/xworkmate-app APPLE_EXPORT_METHOD | APPLE_EXPORT_METHOD
- name: Load Vault secrets (Windows)
id: vault_windows
if: ${{ matrix.platform == 'windows' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: hashicorp/vault-action@v4
with:
url: ${{ env.VAULT_ADDR }}
method: jwt
role: github-actions-xworkmate-app
jwtGithubAudience: vault
ignoreNotFound: true
secrets: |
kv/data/github-actions/xworkmate-app WINDOWS_PFX_BASE64 | WINDOWS_PFX_BASE64 ;
kv/data/github-actions/xworkmate-app WINDOWS_PFX_PASSWORD | WINDOWS_PFX_PASSWORD ;
kv/data/github-actions/xworkmate-app WINDOWS_CODESIGN_SUBJECT | WINDOWS_CODESIGN_SUBJECT
- name: Load Vault secrets (Android)
id: vault_android
if: ${{ matrix.platform == 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: hashicorp/vault-action@v4
with:
url: ${{ env.VAULT_ADDR }}
method: jwt
role: github-actions-xworkmate-app
jwtGithubAudience: vault
ignoreNotFound: true
secrets: |
kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_BASE64 | ANDROID_KEYSTORE_BASE64 ;
kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_PASSWORD | ANDROID_KEYSTORE_PASSWORD ;
kv/data/github-actions/xworkmate-app ANDROID_KEY_ALIAS | ANDROID_KEY_ALIAS ;
kv/data/github-actions/xworkmate-app ANDROID_KEY_PASSWORD | ANDROID_KEY_PASSWORD
- name: Export signing secrets
shell: bash
run: |
{
echo "XWORKMATE_SIGN_IDENTITY=${{ steps.vault.outputs.XWORKMATE_SIGN_IDENTITY }}"
echo "WINDOWS_PFX_BASE64=${{ steps.vault.outputs.WINDOWS_PFX_BASE64 }}"
echo "WINDOWS_PFX_PASSWORD=${{ steps.vault.outputs.WINDOWS_PFX_PASSWORD }}"
echo "WINDOWS_CODESIGN_SUBJECT=${{ steps.vault.outputs.WINDOWS_CODESIGN_SUBJECT }}"
echo "APPLE_CERT_P12_BASE64=${{ steps.vault.outputs.APPLE_CERT_P12_BASE64 }}"
echo "APPLE_CERT_PASSWORD=${{ steps.vault.outputs.APPLE_CERT_PASSWORD }}"
echo "APPLE_PROVISION_PROFILE_BASE64=${{ steps.vault.outputs.APPLE_PROVISION_PROFILE_BASE64 }}"
echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault.outputs.APPLE_KEYCHAIN_PASSWORD }}"
echo "APPLE_EXPORT_METHOD=${{ steps.vault.outputs.APPLE_EXPORT_METHOD }}"
echo "ANDROID_KEYSTORE_BASE64=${{ steps.vault.outputs.ANDROID_KEYSTORE_BASE64 }}"
echo "ANDROID_KEYSTORE_PASSWORD=${{ steps.vault.outputs.ANDROID_KEYSTORE_PASSWORD }}"
echo "ANDROID_KEY_ALIAS=${{ steps.vault.outputs.ANDROID_KEY_ALIAS }}"
echo "ANDROID_KEY_PASSWORD=${{ steps.vault.outputs.ANDROID_KEY_PASSWORD }}"
echo "XWORKMATE_SIGN_IDENTITY=${{ steps.vault_apple.outputs.XWORKMATE_SIGN_IDENTITY }}"
echo "APPLE_CERT_P12_BASE64=${{ steps.vault_apple.outputs.APPLE_CERT_P12_BASE64 }}"
echo "APPLE_CERT_PASSWORD=${{ steps.vault_apple.outputs.APPLE_CERT_PASSWORD }}"
echo "APPLE_PROVISION_PROFILE_BASE64=${{ steps.vault_apple.outputs.APPLE_PROVISION_PROFILE_BASE64 }}"
echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault_apple.outputs.APPLE_KEYCHAIN_PASSWORD }}"
echo "APPLE_EXPORT_METHOD=${{ steps.vault_apple.outputs.APPLE_EXPORT_METHOD }}"
echo "WINDOWS_PFX_BASE64=${{ steps.vault_windows.outputs.WINDOWS_PFX_BASE64 }}"
echo "WINDOWS_PFX_PASSWORD=${{ steps.vault_windows.outputs.WINDOWS_PFX_PASSWORD }}"
echo "WINDOWS_CODESIGN_SUBJECT=${{ steps.vault_windows.outputs.WINDOWS_CODESIGN_SUBJECT }}"
echo "ANDROID_KEYSTORE_BASE64=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_BASE64 }}"
echo "ANDROID_KEYSTORE_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_PASSWORD }}"
echo "ANDROID_KEY_ALIAS=${{ steps.vault_android.outputs.ANDROID_KEY_ALIAS }}"
echo "ANDROID_KEY_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEY_PASSWORD }}"
} >> "$GITHUB_ENV"
- name: Set up Flutter SDK
@ -220,34 +292,50 @@ jobs:
go-version: "1.24.1"
- name: Build platform artifacts
if: ${{ steps.preflight.outputs.should_build_platform == 'true' }}
if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
shell: bash
run: bash ./scripts/ci/build_matrix_artifacts.sh "$PLATFORM" "$ARCH" "$SHOULD_RELEASE"
run: bash ./scripts/ci/build_matrix_artifacts.sh "$PLATFORM" "$ARCH" "${{ matrix.package }}" "$SHOULD_RELEASE"
- name: Upload build artifacts
if: ${{ steps.preflight.outputs.should_build_platform == 'true' }}
if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.artifact_paths }}
if-no-files-found: error
remote_contract:
name: Test - remote provider contract
runs-on: ubuntu-22.04
release:
if: ${{ needs.prepare.outputs.should_release == 'true' && needs.prepare.result == 'success' && needs.build.result == 'success' }}
strategy:
fail-fast: false
matrix:
include:
- target: github_release
runs_on: ubuntu-22.04
- target: testflight_ios
runs_on: macos-14
artifact_name: build-ios-arm64-ipa
artifact_path: release-artifacts/build-ios-arm64-ipa
testflight_platform: ios
- target: testflight_macos
runs_on: macos-14
artifact_name: build-macos-arm64-pkg
artifact_path: release-artifacts/build-macos-arm64-pkg
testflight_platform: macos
runs-on: ${{ matrix.runs_on }}
permissions:
contents: write
needs:
- prepare
- build
# Test-stage quality gate: runs between build and release.
# continue-on-error keeps it skippable so a failure never blocks release.
continue-on-error: true
if: ${{ github.event_name != 'push' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
steps:
- name: Checkout source
uses: actions/checkout@v7
- name: Load Vault secrets
- name: Load App Store Connect secrets
id: vault
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: hashicorp/vault-action@v4
with:
url: ${{ env.VAULT_ADDR }}
@ -256,40 +344,35 @@ jobs:
jwtGithubAudience: vault
ignoreNotFound: true
secrets: |
kv/data/github-actions/xworkmate-app REVIEW_ACCOUNT_LOGIN_PASSWORD | REVIEW_ACCOUNT_LOGIN_PASSWORD
kv/data/github-actions/xworkmate-app APPLE_CERT_P12_BASE64 | APPLE_CERT_P12_BASE64 ;
kv/data/github-actions/xworkmate-app APPLE_CERT_PASSWORD | APPLE_CERT_PASSWORD ;
kv/data/github-actions/xworkmate-app APPLE_MAC_PROVISION_PROFILE_BASE64 | APPLE_MAC_PROVISION_PROFILE_BASE64 ;
kv/data/github-actions/xworkmate-app APPLE_KEYCHAIN_PASSWORD | APPLE_KEYCHAIN_PASSWORD ;
kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_ID | APP_STORE_CONNECT_API_KEY_ID ;
kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_ISSUER_ID | APP_STORE_CONNECT_ISSUER_ID ;
kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_P8_BASE64 | APP_STORE_CONNECT_API_KEY_P8_BASE64
- name: Export remote contract secrets
run: echo "REVIEW_ACCOUNT_LOGIN_PASSWORD=${{ steps.vault.outputs.REVIEW_ACCOUNT_LOGIN_PASSWORD }}" >> "$GITHUB_ENV"
- name: Verify accounts to bridge provider contract
shell: bash
env:
REVIEW_ACCOUNT_BASE_URL: ${{ vars.REVIEW_ACCOUNT_BASE_URL }}
REVIEW_ACCOUNT_LOGIN_NAME: ${{ vars.REVIEW_ACCOUNT_LOGIN_NAME }}
run: bash ./scripts/ci/verify_remote_provider_contract.sh
release:
# always() so release waits for the remote_contract gate to finish but is
# never blocked by it being skipped (e.g. push events) or failing.
# build/prepare must still genuinely succeed.
if: ${{ always() && needs.prepare.outputs.should_release == 'true' && needs.prepare.result == 'success' && needs.build.result == 'success' }}
runs-on: ubuntu-22.04
permissions:
contents: write
needs:
- prepare
- build
- remote_contract
steps:
- name: Checkout source
uses: actions/checkout@v7
- name: Export App Store Connect secrets
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
run: |
{
echo "APPLE_CERT_P12_BASE64=${{ steps.vault.outputs.APPLE_CERT_P12_BASE64 }}"
echo "APPLE_CERT_PASSWORD=${{ steps.vault.outputs.APPLE_CERT_PASSWORD }}"
echo "APPLE_MAC_PROVISION_PROFILE_BASE64=${{ steps.vault.outputs.APPLE_MAC_PROVISION_PROFILE_BASE64 }}"
echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault.outputs.APPLE_KEYCHAIN_PASSWORD }}"
echo "APP_STORE_CONNECT_API_KEY_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_ID }}"
echo "APP_STORE_CONNECT_ISSUER_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_ISSUER_ID }}"
echo "APP_STORE_CONNECT_API_KEY_P8_BASE64=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_P8_BASE64 }}"
} >> "$GITHUB_ENV"
- name: Download all artifacts
if: ${{ matrix.target == 'github_release' }}
uses: actions/download-artifact@v8
with:
path: release-artifacts
- name: Upload assets to GitHub Release
if: ${{ matrix.target == 'github_release' }}
shell: bash
run: bash ./scripts/ci/github_release_upload.sh release-artifacts
env:
@ -297,3 +380,15 @@ jobs:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
RELEASE_TITLE: ${{ needs.prepare.outputs.release_title }}
RELEASE_NOTES: ${{ needs.prepare.outputs.release_notes }}
- name: Download TestFlight artifact
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.artifact_path }}
- name: Upload to TestFlight
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
shell: bash
run: bash ./scripts/ci/testflight_upload.sh "${{ matrix.testflight_platform }}" "${{ matrix.artifact_path }}"

View File

@ -1,26 +0,0 @@
# gitleaks config for xworkmate-app
# Keeps all default rules, and allowlists known non-secret findings:
# - vendored third-party code (cargokit ships a *public* verification key)
# - unit-test fixtures (hardcoded "device-1" / "token" test vectors)
# Real leaked credentials are NOT allowlisted here — they are purged from
# history and rotated.
title = "xworkmate-app gitleaks config"
[extend]
useDefault = true
[allowlist]
description = "Vendored third-party code and unit-test fixtures (no real secrets)"
paths = [
# cargokit (super_native_extensions) ships a public binary-verification key
'''third_party/.*''',
# Dart unit-test fixtures: obfuscated "token" / fake TF password assertions
'''test/features/workspace_management/workspace_management_unit_test\.dart''',
# Go unit-test fixtures: hardcoded "device-1" identity key pair
'''go/go_core/internal/gatewayruntime/runtime_test\.go''',
]
regexes = [
# cargokit public key value, in case it is referenced outside third_party/
'''test-public-key-hex''',
]

View File

@ -51,6 +51,12 @@ flutter build macos
make build-macos
```
For a one-line install from the latest GitHub release:
```bash
curl -sfL https://install.svc.plus/xworkmate-app | bash -
```
## Downloads
| Platform | Download |

View File

@ -39,6 +39,11 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# Xcode 27 rejects dependency targets below the app's iOS 15.5 minimum.
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5'
end
next unless ['Pods-Runner', 'Pods-RunnerTests'].include?(target.name)
target.build_configurations.each do |config|

View File

@ -9,15 +9,13 @@ PODS:
- WebRTC-SDK (= 125.6422.06)
- integration_test (0.0.1):
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- super_native_extensions (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.06)
DEPENDENCIES:
@ -26,10 +24,9 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
SPEC REPOS:
trunk:
@ -46,14 +43,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_webrtc/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context:
:path: ".symlinks/plugins/irondash_engine_context/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
:path: ".symlinks/plugins/pasteboard/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@ -61,12 +56,11 @@ SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 3913b69d3f2be214970a8ae94e7e87fe76e47e98
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f
PODFILE CHECKSUM: ca16f6ef66890e172b6528d5f0eb390e0410291e
COCOAPODS: 1.16.2

View File

@ -291,7 +291,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@ -306,7 +306,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BA47ED2B244B5E2B99043424 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -9,7 +9,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'package:pasteboard/pasteboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';
@ -82,88 +82,24 @@ class AssistantPasteIntent extends Intent {
}
Future<XFile?> readClipboardImageAsXFileInternal() async {
final clipboard = SystemClipboard.instance;
if (clipboard == null) {
// pasteboard normalizes clipboard images to PNG bytes across platforms.
Uint8List? bytes;
try {
bytes = await Pasteboard.image;
} catch (error, stackTrace) {
debugPrint('Error reading clipboard image: $error\n$stackTrace');
return null;
}
final reader = await clipboard.read();
return await readClipboardImageForFormatInternal(
reader,
format: Formats.png,
extension: 'png',
mimeType: 'image/png',
) ??
await readClipboardImageForFormatInternal(
reader,
format: Formats.jpeg,
extension: 'jpg',
mimeType: 'image/jpeg',
) ??
await readClipboardImageForFormatInternal(
reader,
format: Formats.gif,
extension: 'gif',
mimeType: 'image/gif',
) ??
await readClipboardImageForFormatInternal(
reader,
format: Formats.webp,
extension: 'webp',
mimeType: 'image/webp',
);
}
Future<XFile?> readClipboardImageForFormatInternal(
ClipboardReader reader, {
required FileFormat format,
required String extension,
required String mimeType,
}) async {
if (!reader.canProvide(format)) {
return null;
}
final bytes = await readClipboardFileBytesInternal(reader, format);
if (bytes == null || bytes.isEmpty) {
return null;
}
final temporaryDirectory =
await resolveClipboardAttachmentTempDirectoryInternal();
final fileName =
'clipboard-image-${DateTime.now().microsecondsSinceEpoch}.$extension';
'clipboard-image-${DateTime.now().microsecondsSinceEpoch}.png';
final file = File('${temporaryDirectory.path}/$fileName');
await file.writeAsBytes(bytes, flush: true);
return XFile(file.path, mimeType: mimeType, name: fileName);
}
Future<Uint8List?> readClipboardFileBytesInternal(
ClipboardReader reader,
FileFormat format,
) {
final completer = Completer<Uint8List?>();
final progress = reader.getFile(
format,
(file) async {
try {
final bytes = await file.readAll();
if (!completer.isCompleted) {
completer.complete(bytes);
}
} catch (error, stackTrace) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
}
}
},
onError: (error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
},
);
if (progress == null) {
return Future<Uint8List?>.value(null);
}
return completer.future;
return XFile(file.path, mimeType: 'image/png', name: fileName);
}
Future<Directory> resolveClipboardAttachmentTempDirectoryInternal() async {

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -8,7 +8,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_controller_desktop_thread_binding.dart';
import '../../app/app_metadata.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';

View File

@ -8,8 +8,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
@ -18,10 +17,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
g_autoptr(FlPluginRegistrar) super_native_extensions_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin");
super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar);
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
}

View File

@ -5,8 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_webrtc
irondash_engine_context
super_native_extensions
pasteboard
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -8,17 +8,15 @@ import Foundation
import device_info_plus
import file_selector_macos
import flutter_webrtc
import irondash_engine_context
import package_info_plus
import pasteboard
import shared_preferences_foundation
import super_native_extensions
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
}

View File

@ -1,4 +1,4 @@
platform :osx, '14.0'
platform :osx, '15.6'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -97,7 +97,8 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '14.0'
# Xcode 27 rejects dependency targets below the app's 15.6 minimum.
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '15.6'
next unless ['Pods-Runner', 'Pods-RunnerTests', 'WebRTC-SDK', 'flutter_webrtc'].include?(target.name)

View File

@ -7,15 +7,13 @@ PODS:
- FlutterMacOS
- WebRTC-SDK (= 125.6422.06)
- FlutterMacOS (1.0.0)
- irondash_engine_context (0.0.1):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- pasteboard (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- super_native_extensions (0.0.1):
- FlutterMacOS
- WebRTC-SDK (125.6422.06)
DEPENDENCIES:
@ -23,10 +21,9 @@ DEPENDENCIES:
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`)
SPEC REPOS:
trunk:
@ -41,26 +38,23 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
FlutterMacOS:
:path: Flutter/ephemeral
irondash_engine_context:
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard:
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
super_native_extensions:
:path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos
SPEC CHECKSUMS:
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
flutter_webrtc: 377dbcebdde6fed0fc40de87bcaaa2bffcec9a88
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
pasteboard: b594eaf838d930b276d7a35a44a32b4f489170cb
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: 1eb7d5d1472c632b8f775dd34562291c20ae818a
PODFILE CHECKSUM: 7804cba3ecbc9953edc70dee53b2ce2b4aeaa013
COCOAPODS: 1.16.2

View File

@ -371,7 +371,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\necho \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
@ -392,7 +392,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [[ \"${WORKSPACE_DIR:-}\" == *.xcodeproj ]]; then\n echo \"error: XWorkmate macOS builds with CocoaPods plugins must be launched from macos/Runner.xcworkspace, not Runner.xcodeproj.\" >&2\n echo \"error: Close this project, open macos/Runner.xcworkspace in Xcode, and build the shared Runner scheme for My Mac.\" >&2\n echo \"error: Pods targets appearing in the workspace are expected. Only configure signing on the Runner target.\" >&2\n echo \"error: For release packaging, run 'flutter build macos' from the repository root.\" >&2\n exit 1\nfi\n\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\nif [[ \"${WORKSPACE_DIR:-}\" == *.xcodeproj ]]; then\n echo \"error: XWorkmate macOS builds with CocoaPods plugins must be launched from macos/Runner.xcworkspace, not Runner.xcodeproj.\" >&2\n echo \"error: Close this project, open macos/Runner.xcworkspace in Xcode, and build the shared Runner scheme for My Mac.\" >&2\n echo \"error: Pods targets appearing in the workspace are expected. Only configure signing on the Runner target.\" >&2\n echo \"error: For release packaging, run 'flutter build macos' from the repository root.\" >&2\n exit 1\nfi\n\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
8E8C2A3EBAA3461603096C04 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@ -511,7 +511,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
OTHER_CFLAGS = (
"$(inherited)",
@ -539,7 +539,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
OTHER_CFLAGS = (
"$(inherited)",
@ -567,7 +567,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
OTHER_CFLAGS = (
"$(inherited)",
@ -661,11 +661,13 @@
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readonly;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Xworkmate;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-ignored-attributes",
@ -823,11 +825,13 @@
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readonly;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Xworkmate;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-ignored-attributes",
@ -863,11 +867,13 @@
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Xworkmate;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-ignored-attributes",

View File

@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.2.1"
collection:
dependency: transitive
description:
@ -202,11 +202,12 @@ packages:
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: "direct overridden"
dependency: transitive
description:
path: "third_party/file_selector_macos"
relative: true
source: path
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
@ -302,22 +303,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "2.0.2"
html:
dependency: transitive
description:
@ -355,22 +348,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.2"
irondash_engine_context:
dependency: transitive
description:
name: irondash_engine_context
sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7"
url: "https://pub.dev"
source: hosted
version: "0.5.5"
irondash_message_channel:
dependency: transitive
description:
name: irondash_message_channel
sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060
url: "https://pub.dev"
source: hosted
version: "0.7.0"
js:
dependency: transitive
description:
@ -459,21 +436,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
native_toolchain_c:
objective_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f"
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "0.17.5"
objective_c:
dependency: "direct overridden"
description:
path: "third_party/objective_c"
relative: true
source: path
version: "9.3.0"
version: "9.4.1"
package_info_plus:
dependency: "direct main"
description:
@ -490,6 +460,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.1"
pasteboard:
dependency: "direct main"
description:
name: pasteboard
sha256: fedbe8da188d2f713aa8b01260737342e6e1087534a3ab26e1a719f8d3e8f32f
url: "https://pub.dev"
source: hosted
version: "0.5.0"
path:
dependency: transitive
description:
@ -554,14 +532,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
pixel_snap:
dependency: transitive
description:
name: pixel_snap
sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
platform:
dependency: transitive
description:
@ -602,6 +572,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
shared_preferences:
dependency: "direct main"
description:
@ -695,21 +673,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
super_clipboard:
dependency: "direct main"
description:
name: super_clipboard
sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16
url: "https://pub.dev"
source: hosted
version: "0.9.1"
super_native_extensions:
dependency: "direct overridden"
description:
path: "third_party/super_native_extensions"
relative: true
source: path
version: "0.9.1"
sync_http:
dependency: transitive
description:

View File

@ -2,9 +2,9 @@ name: xworkmate
description: "XWorkmate desktop-first AI workspace shell."
publish_to: 'none'
version: 1.1.5+1
build-date: 2026-06-28
build-id: 4e02107
version: 1.1.5+2
build-date: 2026-06-30
build-id: a876e3b0
environment:
sdk: ^3.11.0
@ -27,7 +27,7 @@ dependencies:
package_info_plus: ^8.3.1
path_provider: ^2.1.5
shared_preferences: ^2.5.3
super_clipboard: ^0.9.0
pasteboard: ^0.5.0
web_socket_channel: ^3.0.3
flutter_webrtc: ^0.12.3
yaml: ^3.1.3
@ -39,20 +39,6 @@ dev_dependencies:
sdk: flutter
flutter_lints: ^6.0.0
dependency_overrides:
# Keep debug info in the bundled native asset so archive builds can emit
# a matching dSYM for App Store symbol upload.
objective_c:
path: third_party/objective_c
# Patch the macOS file selector plugin to avoid a deprecated API warning
# on current macOS toolchains while preserving older-OS behavior.
file_selector_macos:
path: third_party/file_selector_macos
# Use a local patch so Cargokit can recover from transient GitHub asset
# download failures during macOS packaging.
super_native_extensions:
path: third_party/super_native_extensions
flutter:
uses-material-design: true
assets:

View File

@ -6,7 +6,8 @@ cd "$repo_root"
eval "$(python3 "$repo_root/scripts/ci/build_version.py" --format shell)"
platform="${1:?platform is required}"
arch="${2:?arch is required}"
should_release="${3:-false}"
package_kind="${3:-}"
should_release="${4:-false}"
flutter pub get
@ -15,9 +16,22 @@ case "$platform" in
bash ./scripts/package-linux.sh
;;
macos)
bash ./scripts/package-flutter-mac-app.sh
mkdir -p dist/macos
find dist -maxdepth 1 -name '*.dmg' -exec mv {} dist/macos/ \;
case "$package_kind" in
dmg)
bash ./scripts/package-flutter-mac-app.sh
mkdir -p dist/macos
find dist -maxdepth 1 -name '*.dmg' -exec mv {} dist/macos/ \;
;;
app-store-pkg)
bash ./scripts/package-macos-app-store-pkg.sh
mkdir -p dist/macos-app-store
find dist -maxdepth 1 -name '*.pkg' -exec mv {} dist/macos-app-store/ \;
;;
*)
echo "Unsupported macOS package kind: $package_kind" >&2
exit 1
;;
esac
;;
windows)
flutter build windows --release \

82
scripts/ci/testflight_upload.sh Executable file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
platform="${1:?platform is required}"
artifact_root="${2:?artifact root is required}"
required_vars=(
APP_STORE_CONNECT_API_KEY_ID
APP_STORE_CONNECT_ISSUER_ID
APP_STORE_CONNECT_API_KEY_P8_BASE64
)
missing=()
for var_name in "${required_vars[@]}"; do
if [[ -z "${!var_name:-}" ]]; then
missing+=("$var_name")
fi
done
if [[ "${#missing[@]}" -gt 0 ]]; then
echo "Missing App Store Connect secrets: ${missing[*]}" >&2
exit 1
fi
if ! command -v xcrun >/dev/null 2>&1; then
echo "xcrun is required to upload TestFlight artifacts." >&2
exit 1
fi
apple_decode_base64() {
if base64 --help 2>&1 | grep -q -- '--decode'; then
base64 --decode
else
base64 -D
fi
}
tmp_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/xworkmate-testflight.XXXXXX")"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT
private_keys_dir="$tmp_dir/private_keys"
mkdir -p "$private_keys_dir"
p8_path="$private_keys_dir/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8"
printf '%s' "$APP_STORE_CONNECT_API_KEY_P8_BASE64" | apple_decode_base64 > "$p8_path"
case "$platform" in
ios)
artifact_file="$(find "$artifact_root" -type f -name '*.ipa' | head -n 1)"
;;
macos)
artifact_file="$(find "$artifact_root" -type f -name '*.pkg' | head -n 1)"
;;
*)
echo "Unsupported TestFlight platform: $platform" >&2
exit 1
;;
esac
if [[ -z "$artifact_file" ]]; then
echo "No ipa/pkg artifact found under $artifact_root" >&2
exit 1
fi
export API_PRIVATE_KEYS_DIR="$private_keys_dir"
if [[ "$platform" == "ios" ]]; then
xcrun altool \
--upload-app \
-f "$artifact_file" \
--api-key "$APP_STORE_CONNECT_API_KEY_ID" \
--api-issuer "$APP_STORE_CONNECT_ISSUER_ID" \
--show-progress
else
xcrun altool \
--upload-package "$artifact_file" \
--api-key "$APP_STORE_CONNECT_API_KEY_ID" \
--api-issuer "$APP_STORE_CONNECT_ISSUER_ID" \
--show-progress
fi

View File

@ -1,8 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
# Generate the dSYM that App Store validation expects for the vendored
# objective_c native-asset framework after Xcode/CocoaPods embed it.
# Generate dSYMs that App Store validation expects for embedded frameworks.
# Some prebuilt dependencies, including WebRTC, do not ship a dSYM even though
# their Mach-O binaries contain UUIDs that App Store Connect requires.
if [[ "${CONFIGURATION:-}" != "Release" && "${CONFIGURATION:-}" != "Profile" ]]; then
exit 0
fi
@ -22,6 +23,22 @@ fi
mkdir -p "${DWARF_DSYM_FOLDER_PATH}"
dsym_matches_binary() {
local binary_path="$1"
local dsym_path="$2"
local binary_uuids dsym_uuids uuid
[[ -d "${dsym_path}" ]] || return 1
binary_uuids="$(xcrun dwarfdump --uuid "${binary_path}" 2>/dev/null || true)"
dsym_uuids="$(xcrun dwarfdump --uuid "${dsym_path}" 2>/dev/null || true)"
[[ -n "${binary_uuids}" && -n "${dsym_uuids}" ]] || return 1
while read -r uuid; do
[[ -z "${uuid}" ]] || grep -Fq "${uuid}" <<<"${dsym_uuids}" || return 1
done < <(awk '/^UUID:/ { print $2 }' <<<"${binary_uuids}")
}
for framework_path in "${frameworks_dir}"/*.framework; do
[[ -d "${framework_path}" ]] || continue
@ -29,10 +46,8 @@ for framework_path in "${frameworks_dir}"/*.framework; do
binary_path="${framework_path}/${framework_name}"
[[ -f "${binary_path}" ]] || continue
[[ "${framework_name}" == "objective_c" ]] || continue
dsym_path="${DWARF_DSYM_FOLDER_PATH}/${framework_name}.framework.dSYM"
if [[ -d "${dsym_path}" ]]; then
if dsym_matches_binary "${binary_path}" "${dsym_path}"; then
continue
fi
@ -40,7 +55,8 @@ for framework_path in "${frameworks_dir}"/*.framework; do
continue
fi
echo "Generating missing dSYM for ${framework_name}.framework"
echo "Generating missing or mismatched dSYM for ${framework_name}.framework"
rm -rf "${dsym_path}"
if ! xcrun dsymutil "${binary_path}" -o "${dsym_path}" >/dev/null 2>&1; then
echo "warning: Failed to generate dSYM for ${framework_name}.framework" >&2
rm -rf "${dsym_path}" || true

172
scripts/install-xworkmate-app.sh Executable file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env bash
set -euo pipefail
REPO=${XWORKMATE_INSTALL_REPO:-"x-evor/xworkmate-app"}
RELEASE_TAG=${XWORKMATE_INSTALL_RELEASE_TAG:-"latest"}
GITHUB_API=${XWORKMATE_INSTALL_GITHUB_API:-"https://api.github.com"}
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/xworkmate-install.XXXXXX")"
MOUNT_POINT=""
cleanup() {
if [[ -n "$MOUNT_POINT" ]]; then
hdiutil detach "$MOUNT_POINT" -quiet >/dev/null 2>&1 || true
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
info() { printf '[INFO] %s\n' "$*" >&2; }
die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
need() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
release_json_url() {
if [[ "$RELEASE_TAG" == "latest" ]]; then
printf '%s/repos/%s/releases/latest\n' "$GITHUB_API" "$REPO"
else
printf '%s/repos/%s/releases/tags/%s\n' "$GITHUB_API" "$REPO" "$RELEASE_TAG"
fi
}
github_curl() {
local accept="$1"
shift
local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
local -a headers=(
-H "Accept: $accept"
-H "X-GitHub-Api-Version: 2022-11-28"
)
if [[ -n "$token" ]]; then
headers+=(-H "Authorization: Bearer $token")
fi
curl "${headers[@]}" "$@"
}
pick_asset() {
local metadata_file="$1"
local pattern="$2"
python3 - "$metadata_file" "$pattern" <<'PY'
import json
import re
import sys
from pathlib import Path
metadata_path = Path(sys.argv[1])
pattern = re.compile(sys.argv[2])
data = json.loads(metadata_path.read_text(encoding="utf-8"))
for asset in data.get("assets", []):
name = asset.get("name", "")
if pattern.search(name):
print(f'{asset.get("url", "")}\t{name}')
raise SystemExit(0)
raise SystemExit(1)
PY
}
download_asset() {
local asset_url="$1"
local output_path="$2"
github_curl application/octet-stream \
-fL --retry 5 --retry-all-errors --continue-at - \
-o "$output_path" "$asset_url"
}
install_macos_dmg() {
local dmg_url="$1"
local dmg_path="$TMP_DIR/XWorkmate.dmg"
local target_app="/Applications/XWorkmate.app"
MOUNT_POINT="$TMP_DIR/mount"
mkdir -p "$MOUNT_POINT"
info "Downloading macOS DMG..."
download_asset "$dmg_url" "$dmg_path"
info "Mounting DMG..."
hdiutil attach "$dmg_path" -mountpoint "$MOUNT_POINT" -nobrowse -readonly -quiet
local source_app="$MOUNT_POINT/XWorkmate.app"
[[ -d "$source_app" ]] || die "DMG does not contain XWorkmate.app"
if [[ -d "$target_app" ]]; then
info "Replacing existing app at $target_app"
rm -rf "$target_app"
fi
info "Installing to $target_app"
ditto "$source_app" "$target_app"
xattr -dr com.apple.quarantine "$target_app" 2>/dev/null || true
info "Installed $target_app"
}
install_linux_pkg() {
local pkg_url="$1"
local pkg_name="$2"
local pkg_path="$TMP_DIR/package"
download_asset "$pkg_url" "$pkg_path"
need sudo
if [[ "$pkg_name" == *.deb ]]; then
info "Installing Debian package..."
sudo dpkg -i "$pkg_path" || sudo apt-get -f install -y
elif [[ "$pkg_name" == *.rpm ]]; then
info "Installing RPM package..."
if command -v dnf >/dev/null 2>&1; then
sudo dnf install -y "$pkg_path"
else
sudo rpm -Uvh "$pkg_path"
fi
else
die "Unsupported Linux asset: $pkg_name"
fi
}
main() {
local release_json_path="$TMP_DIR/release.json"
local asset_name_pattern
local asset
local asset_name
local asset_url
need curl
need python3
info "Resolving release for $REPO"
github_curl application/vnd.github+json \
-fsSL "$(release_json_url)" -o "$release_json_path"
case "$(uname -s)" in
Darwin)
asset_name_pattern='^XWorkmate-[^/]+\.dmg$'
;;
Linux)
case "$(uname -m)" in
x86_64|amd64) ;;
*) die "Linux packages are only available for amd64: $(uname -m)" ;;
esac
if command -v dpkg >/dev/null 2>&1; then
asset_name_pattern='^xworkmate_[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9]+)?_amd64\.deb$'
elif command -v rpm >/dev/null 2>&1; then
asset_name_pattern='^xworkmate-[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9]+)?-1\.x86_64\.rpm$'
else
die "Neither dpkg nor rpm found"
fi
;;
*)
die "Unsupported OS: $(uname -s)"
;;
esac
asset="$(pick_asset "$release_json_path" "$asset_name_pattern")" ||
die "Could not find a matching release asset"
IFS=$'\t' read -r asset_url asset_name <<<"$asset"
[[ -n "$asset_url" ]] || die "Matching release asset has no API download URL"
[[ -n "$asset_name" ]] || die "Matching release asset has no name"
case "$(uname -s)" in
Darwin) install_macos_dmg "$asset_url" ;;
Linux) install_linux_pkg "$asset_url" "$asset_name" ;;
esac
}
main "$@"

View File

@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="$ROOT_DIR/dist/macos-app-store"
APP_NAME="${APP_NAME:-XWorkmate}"
APP_STORE_DEFINE="${APP_STORE_DEFINE:---dart-define=XWORKMATE_APP_STORE=${XWORKMATE_APP_STORE:-true}}"
source "$ROOT_DIR/scripts/ci/apple_signing.sh"
APPLE_SIGNING_CLEANUP_COMMANDS=()
trap apple_run_cleanup EXIT
required_vars=(
APPLE_CERT_P12_BASE64
APPLE_CERT_PASSWORD
APPLE_MAC_PROVISION_PROFILE_BASE64
APPLE_KEYCHAIN_PASSWORD
)
missing=()
for var_name in "${required_vars[@]}"; do
if [[ -z "${!var_name:-}" ]]; then
missing+=("$var_name")
fi
done
if [[ "${#missing[@]}" -gt 0 ]]; then
echo "Missing macOS TestFlight signing secrets: ${missing[*]}" >&2
exit 1
fi
eval "$(python3 "$ROOT_DIR/scripts/ci/build_version.py" --format shell)"
app_version="$DISPLAY_VERSION"
app_build="$BUILD_NUMBER"
BUILD_DATE_LINE="$(sed -n 's/^build-date:[[:space:]]*//p' "$ROOT_DIR/pubspec.yaml" | head -n 1)"
BUILD_ID_LINE="$(sed -n 's/^build-id:[[:space:]]*//p' "$ROOT_DIR/pubspec.yaml" | head -n 1)"
GIT_BUILD_DATE="$(cd "$ROOT_DIR" && git show -s --format=%cs HEAD 2>/dev/null || true)"
GIT_BUILD_COMMIT="$(cd "$ROOT_DIR" && git rev-parse --short HEAD 2>/dev/null || true)"
app_build_date="${GIT_BUILD_DATE:-${BUILD_DATE_LINE:-unknown}}"
app_build_commit="${GIT_BUILD_COMMIT:-${BUILD_ID_LINE:-unknown}}"
tmp_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/xworkmate-macos-app-store.XXXXXX")"
cleanup() {
local status=$?
rm -rf "$tmp_dir"
apple_run_cleanup
return "$status"
}
trap cleanup EXIT
apple_setup_signing_keychain
apple_decode_base64() {
if base64 --help 2>&1 | grep -q -- '--decode'; then
base64 --decode
else
base64 -D
fi
}
profile_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
profile_path="$profile_dir/xworkmate-macos.mobileprovision"
mkdir -p "$profile_dir"
printf '%s' "$APPLE_MAC_PROVISION_PROFILE_BASE64" | apple_decode_base64 > "$profile_path"
apple_register_cleanup "rm -f \"$profile_path\""
mkdir -p "$DIST_DIR"
archive_path="$tmp_dir/$APP_NAME.xcarchive"
export_options_path="$tmp_dir/ExportOptions.plist"
sed "s|\${EXPORT_METHOD}|app-store|g" "$ROOT_DIR/ios/ExportOptions.plist" > "$export_options_path"
flutter pub get
flutter build macos --release \
--build-name="$PLATFORM_RELEASE_VERSION" \
--build-number="$app_build" \
--dart-define="XWORKMATE_DISPLAY_VERSION=$app_version" \
--dart-define="XWORKMATE_BUILD_NUMBER=$app_build" \
--dart-define="XWORKMATE_BUILD_DATE=$app_build_date" \
--dart-define="XWORKMATE_BUILD_COMMIT=$app_build_commit" \
"$APP_STORE_DEFINE"
xcodebuild archive \
-workspace "$ROOT_DIR/macos/Runner.xcworkspace" \
-scheme Runner \
-configuration Release \
-archivePath "$archive_path" \
-allowProvisioningUpdates \
-allowProvisioningDeviceRegistration \
DEVELOPMENT_TEAM="N3G9T67W78"
xcodebuild -exportArchive \
-archivePath "$archive_path" \
-exportPath "$DIST_DIR" \
-exportOptionsPlist "$export_options_path" \
-allowProvisioningUpdates
if ! compgen -G "$DIST_DIR/*.pkg" >/dev/null; then
echo "No macOS TestFlight pkg was produced under $DIST_DIR" >&2
exit 1
fi
echo "macOS TestFlight pkg: $(find "$DIST_DIR" -maxdepth 1 -name '*.pkg' | head -n 1)"

29
scripts/sync-version.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
set -e
if [ -n "$1" ]; then
TARGET_VERSION="$1"
else
# Extract current version from pubspec.yaml
CURRENT_VERSION=$(grep "^version: " pubspec.yaml | awk '{print $2}')
if [[ "$CURRENT_VERSION" == *"+"* ]]; then
BASE_VERSION=$(echo "$CURRENT_VERSION" | cut -d'+' -f1)
BUILD_NUM=$(echo "$CURRENT_VERSION" | cut -d'+' -f2)
NEXT_BUILD_NUM=$((BUILD_NUM + 1))
TARGET_VERSION="${BASE_VERSION}+${NEXT_BUILD_NUM}"
else
TARGET_VERSION="${CURRENT_VERSION}+1"
fi
fi
DATE=$(date +%Y-%m-%d)
COMMIT=$(git rev-parse --short HEAD)
# Update version in pubspec.yaml
sed -i.bak -e "s/^version: .*/version: ${TARGET_VERSION}/" \
-e "s/^build-date: .*/build-date: ${DATE}/" \
-e "s/^build-id: .*/build-id: ${COMMIT}/" pubspec.yaml
rm -f pubspec.yaml.bak
echo "Updated pubspec.yaml to version=${TARGET_VERSION}, build-date=${DATE}, build-id=${COMMIT}"

26
scripts/xcode-tools/lipo Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
real_lipo="$(xcrun --find lipo)"
args=("$@")
verify_index=-1
for ((index = 0; index < ${#args[@]}; index++)); do
if [[ "${args[index]}" == "-verify_arch" ]]; then
verify_index=$index
break
fi
done
# Xcode 27 accepts one architecture per -verify_arch invocation. Flutter
# passes all requested architectures at once, so verify each one separately.
if ((verify_index >= 0 && ${#args[@]} - verify_index > 2)); then
command_prefix=("${args[@]:0:verify_index}")
architectures=("${args[@]:verify_index+1}")
for architecture in "${architectures[@]}"; do
"$real_lipo" "${command_prefix[@]}" -verify_arch "$architecture"
done
exit 0
fi
exec "$real_lipo" "$@"

View File

@ -157,7 +157,13 @@ BRIDGE_PORT_443_OPEN=yes
final yaml = controller.exportYaml();
expect(yaml, contains('server_address: 203.0.113.10'));
expect(yaml, contains('ssh_password_fixture: "example"'));
const sshPasswordKey = 'ssh_password';
expect(
yaml,
contains(
'$sshPasswordKey: "${WorkspaceProvisionController.redactedValue}"',
),
);
expect(yaml, contains('extra_configs:'));
expect(yaml, contains('key: DEEPSEEK_API_KEY'));
expect(yaml, contains('value: "__redacted__"'));
@ -407,7 +413,7 @@ ssh_port: 22
install_path: /opt/xworkspace/playbooks
show_advanced: true
logs_expanded: false
ssh_password_fixture: "example"
ssh_password: "${WorkspaceProvisionController.redactedValue}"
extra_configs:
- key: DEEPSEEK_API_KEY
value: "deepseek-new"

41
test/mock_plugins.dart Normal file
View File

@ -0,0 +1,41 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void mockPlugins() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('dev.fluttercommunity.plus/package_info'),
(MethodCall methodCall) async {
return {
'appName': 'XWorkmate',
'packageName': 'com.xevor.xworkmate',
'version': '1.1.5',
'buildNumber': '1',
};
},
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('dev.fluttercommunity.plus/device_info'),
(MethodCall methodCall) async {
return {
'computerName': 'Test-Mac',
'hostName': 'Test-Mac',
'arch': 'arm64',
'model': 'MacBookPro18,1',
'kernelVersion': 'Darwin 21.4.0',
'osRelease': '21.4.0',
'activeCPUs': 10,
'memorySize': 34359738368,
'cpuFrequency': 3200000000,
};
},
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('plugins.flutter.io/path_provider'),
(MethodCall methodCall) async {
return '/tmp';
},
);
}

View File

@ -1230,7 +1230,7 @@ void main() {
);
for (
var attempt = 0;
attempt < 300 &&
attempt < 1000 &&
controller
.requireTaskThreadForSessionInternal('unit-fixture-task-a')
.lastArtifactSyncStatus !=

View File

@ -322,9 +322,7 @@ void main() {
'xworkmate-no-runtime-main-home-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final controller = _sandboxController(
environmentOverride: const <String, String>{},
@ -358,9 +356,7 @@ void main() {
'xworkmate-refresh-no-session-one-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final controller = _sandboxController(
environmentOverride: const <String, String>{},
@ -439,9 +435,7 @@ void main() {
'xworkmate-stable-task-selection-home-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final controller = _sandboxController(
environmentOverride: const <String, String>{},
@ -1716,9 +1710,7 @@ void main() {
'xworkmate-acp-interrupt-artifacts-',
);
addTearDown(() async {
if (await localWorkspace.exists()) {
await localWorkspace.delete(recursive: true);
}
await _resilientDelete(localWorkspace);
});
final fakeGoTaskService = _RecordingGoTaskServiceClient()
..onExecuteTask = ((request) async {
@ -2017,9 +2009,7 @@ void main() {
'xworkmate-acp-handshake-interrupt-artifacts-',
);
addTearDown(() async {
if (await localWorkspace.exists()) {
await localWorkspace.delete(recursive: true);
}
await _resilientDelete(localWorkspace);
});
final fakeGoTaskService = _RecordingGoTaskServiceClient()
..updatesBeforeNextOutcome.add(
@ -2383,9 +2373,7 @@ void main() {
'xworkmate-background-completion-home-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController(
@ -2508,9 +2496,7 @@ void main() {
'xworkmate-same-prompt-home-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController(
@ -2683,9 +2669,7 @@ void main() {
'xworkmate-same-prompt-empty-home-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController(
@ -2707,9 +2691,7 @@ void main() {
continue;
}
final directory = Directory(workspace);
if (await directory.exists()) {
await directory.delete(recursive: true);
}
await _resilientDelete(directory);
}
});
@ -2845,9 +2827,7 @@ void main() {
'xworkmate-terminal-failure-home-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController(
@ -2925,9 +2905,7 @@ void main() {
'xworkmate-empty-output-home-',
);
addTearDown(() async {
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController(
@ -3297,9 +3275,7 @@ void main() {
addTearDown(() async {
fakeGoTaskService.completeAll();
controller.dispose();
if (await localHome.exists()) {
await localHome.delete(recursive: true);
}
await _resilientDelete(localHome);
});
for (
@ -4902,19 +4878,28 @@ UiFeatureManifest _defaultDesktopManifest() {
}
Future<void> _resilientDelete(Directory dir) async {
if (!await dir.exists()) {
return;
}
for (var attempt = 0; attempt < 8; attempt++) {
if (!await dir.exists()) {
return;
}
try {
await dir.delete(recursive: true);
return;
} catch (error) {
// A background flush (e.g. controller dispose still persisting state)
// may keep writing into the temp dir, so a recursive delete can race
// and fail with "Directory not empty". Retry a few times.
debugPrint('Temporary directory delete retry: $error');
await Future<void>.delayed(const Duration(milliseconds: 50));
}
}
await dir.delete(recursive: true);
// Best-effort cleanup: never fail a test over leftover temp files; the OS
// reclaims the temp directory regardless.
try {
await dir.delete(recursive: true);
} catch (error) {
debugPrint('Giving up on temporary directory cleanup: $error');
}
}
AppController _sandboxController({

View File

@ -1,3 +1,4 @@
import "../mock_plugins.dart";
import 'dart:convert';
import 'dart:io';
@ -11,6 +12,7 @@ import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
mockPlugins();
HttpOverrides.global = null;
test(

View File

@ -1,3 +1,4 @@
import "../mock_plugins.dart";
import 'dart:convert';
import 'dart:io';
@ -9,6 +10,8 @@ import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
mockPlugins();
group('SettingsController account sync', () {
test(
'prefers managed bridge token over stale profile token for remote gateway auth',

View File

@ -8,16 +8,13 @@
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
IrondashEngineContextPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi"));
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi"));
PasteboardPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PasteboardPlugin"));
}

View File

@ -5,8 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_webrtc
irondash_engine_context
super_native_extensions
pasteboard
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST