Add: just commands for release build

This commit is contained in:
DaZuo0122
2026-02-13 11:22:46 +08:00
parent 3c3ca342c9
commit 55fe53235d
21 changed files with 253 additions and 97 deletions

View File

@@ -26,6 +26,7 @@ use tracing::{error, info, warn};
const APP_NAME: &str = "sprimo";
const DEFAULT_PACK: &str = "default";
const WINDOW_PADDING: f32 = 16.0;
const STARTUP_WINDOW_SIZE: f32 = 416.0;
const MAGENTA_KEY: [u8; 3] = [255, 0, 255];
const CHROMA_KEY_TOLERANCE: u8 = 24;
const CHROMA_KEY_FORCE_RATIO: f32 = 0.15;
@@ -180,7 +181,13 @@ fn main() -> Result<(), AppError> {
.compact()
.init();
let (config_path, config) = sprimo_config::load_or_create(APP_NAME)?;
let (config_path, mut config) = sprimo_config::load_or_create(APP_NAME)?;
if config.window.click_through {
config.window.click_through = false;
if let Err(err) = save(&config_path, &config) {
warn!(%err, "failed to persist click-through disable at startup");
}
}
let platform: Arc<dyn PlatformAdapter> = create_adapter().into();
let capabilities = platform.capabilities();
@@ -188,7 +195,7 @@ fn main() -> Result<(), AppError> {
snapshot.x = config.window.x;
snapshot.y = config.window.y;
snapshot.scale = config.window.scale;
snapshot.flags.click_through = config.window.click_through;
snapshot.flags.click_through = false;
snapshot.flags.always_on_top = config.window.always_on_top;
snapshot.flags.visible = config.window.visible;
snapshot.active_sprite_pack = config.sprite.selected_pack.clone();
@@ -223,7 +230,10 @@ fn main() -> Result<(), AppError> {
decorations: false,
resizable: false,
window_level: WindowLevel::AlwaysOnTop,
resolution: bevy::window::WindowResolution::new(544.0, 544.0),
resolution: bevy::window::WindowResolution::new(
STARTUP_WINDOW_SIZE,
STARTUP_WINDOW_SIZE,
),
position: WindowPosition::At(IVec2::new(
config.window.x.round() as i32,
config.window.y.round() as i32,
@@ -721,7 +731,6 @@ fn attach_window_handle_once(
.read()
.expect("frontend snapshot lock poisoned");
let _ = platform.0.set_always_on_top(guard.flags.always_on_top);
let _ = platform.0.set_click_through(guard.flags.click_through);
let _ = platform.0.set_visible(guard.flags.visible);
let _ = platform.0.set_window_position(guard.x, guard.y);
*attached = true;
@@ -746,14 +755,14 @@ fn poll_hotkey_recovery(
) {
while ingress.0.try_recv().is_ok() {
info!("recovery hotkey received");
let _ = platform.0.set_click_through(false);
let _ = platform.0.set_always_on_top(true);
let _ = platform.0.set_visible(true);
if let Ok(mut visibility) = pet_query.get_single_mut() {
*visibility = Visibility::Visible;
}
config.config.window.click_through = false;
config.config.window.always_on_top = true;
config.config.window.visible = true;
if let Err(err) = save(&config.path, &config.config) {
warn!(%err, "failed to persist config after hotkey recovery");
@@ -761,6 +770,7 @@ fn poll_hotkey_recovery(
if let Ok(mut guard) = snapshot.0.write() {
guard.flags.click_through = false;
guard.flags.always_on_top = true;
guard.flags.visible = true;
guard.last_error = None;
}
@@ -927,9 +937,8 @@ fn poll_backend_commands(
always_on_top,
visible,
} => {
if let Some(value) = click_through {
let _ = platform.0.set_click_through(value);
config.config.window.click_through = value;
if click_through.is_some() {
config.config.window.click_through = false;
}
if let Some(value) = always_on_top {
let _ = platform.0.set_always_on_top(value);
@@ -949,9 +958,7 @@ fn poll_backend_commands(
warn!(%err, "failed to persist flag config");
}
if let Ok(mut guard) = snapshot.0.write() {
if let Some(value) = click_through {
guard.flags.click_through = value;
}
guard.flags.click_through = false;
if let Some(value) = always_on_top {
guard.flags.always_on_top = value;
}

View File

@@ -171,7 +171,7 @@ mod windows {
impl PlatformAdapter for WindowsAdapter {
fn capabilities(&self) -> CapabilityFlags {
CapabilityFlags {
supports_click_through: true,
supports_click_through: false,
supports_transparency: true,
supports_tray: false,
supports_global_hotkey: true,

View File

@@ -35,14 +35,16 @@ impl RuntimeCore {
pub fn new_with_config(
config_path: PathBuf,
config_value: AppConfig,
mut config_value: AppConfig,
capabilities: CapabilityFlags,
) -> Result<Self, RuntimeCoreError> {
let click_through_was_enabled = config_value.window.click_through;
config_value.window.click_through = false;
let mut snapshot = FrontendStateSnapshot::idle(capabilities);
snapshot.x = config_value.window.x;
snapshot.y = config_value.window.y;
snapshot.scale = config_value.window.scale;
snapshot.flags.click_through = config_value.window.click_through;
snapshot.flags.click_through = false;
snapshot.flags.always_on_top = config_value.window.always_on_top;
snapshot.flags.visible = config_value.window.visible;
snapshot.active_sprite_pack = config_value.sprite.selected_pack.clone();
@@ -50,14 +52,18 @@ impl RuntimeCore {
let api_config = ApiConfig::default_with_token(config_value.api.auth_token.clone());
let (command_tx, command_rx) = mpsc::channel(1_024);
Ok(Self {
let core = Self {
config_path,
config: Arc::new(RwLock::new(config_value)),
snapshot: Arc::new(RwLock::new(snapshot)),
api_config,
command_tx,
command_rx: Arc::new(Mutex::new(command_rx)),
})
};
if click_through_was_enabled {
core.persist_config()?;
}
Ok(core)
}
pub fn snapshot(&self) -> Arc<RwLock<FrontendStateSnapshot>> {
@@ -169,7 +175,7 @@ impl RuntimeCore {
self.persist_config()?;
}
FrontendCommand::SetFlags {
click_through,
click_through: _click_through,
always_on_top,
visible,
} => {
@@ -178,9 +184,7 @@ impl RuntimeCore {
.snapshot
.write()
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
if let Some(value) = click_through {
snapshot.flags.click_through = *value;
}
snapshot.flags.click_through = false;
if let Some(value) = always_on_top {
snapshot.flags.always_on_top = *value;
}
@@ -194,9 +198,7 @@ impl RuntimeCore {
.config
.write()
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
if let Some(value) = click_through {
config.window.click_through = *value;
}
config.window.click_through = false;
if let Some(value) = always_on_top {
config.window.always_on_top = *value;
}
@@ -259,4 +261,26 @@ mod tests {
assert_eq!(snapshot.state, FrontendState::Active);
assert_eq!(snapshot.current_animation, "active");
}
#[test]
fn click_through_flag_is_ignored_and_forced_false() {
let temp = TempDir::new().expect("tempdir");
let path = temp.path().join("config.toml");
let mut config = AppConfig::default();
config.window.click_through = true;
let core = RuntimeCore::new_with_config(path, config, CapabilityFlags::default())
.expect("core init");
core.apply_command(&FrontendCommand::SetFlags {
click_through: Some(true),
always_on_top: None,
visible: None,
})
.expect("apply");
let snapshot = core.snapshot().read().expect("snapshot lock").clone();
assert!(!snapshot.flags.click_through);
let config = core.config().read().expect("config lock").clone();
assert!(!config.window.click_through);
}
}

View File

@@ -5,6 +5,7 @@
"windows": ["*"],
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:event:allow-listen",
"core:event:allow-unlisten"
]

View File

@@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:event:allow-listen","core:event:allow-unlisten"]}}
{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:window:allow-start-dragging","core:event:allow-listen","core:event:allow-unlisten"]}}

View File

@@ -153,6 +153,8 @@ fn main() -> Result<(), AppError> {
}
let command_rx = runtime_core.command_receiver();
let runtime_core_for_commands = Arc::clone(&runtime_core);
let app_handle_for_commands = app_handle.clone();
runtime.spawn(async move {
loop {
let next = {
@@ -163,24 +165,45 @@ fn main() -> Result<(), AppError> {
break;
};
if let Err(err) = runtime_core.apply_command(&envelope.command) {
if let Err(err) = runtime_core_for_commands.apply_command(&envelope.command) {
warn!(%err, "failed to apply command in tauri runtime");
continue;
}
let payload = {
let snapshot = runtime_core.snapshot();
let snapshot = runtime_core_for_commands.snapshot();
match snapshot.read() {
Ok(s) => Some(to_ui_snapshot(&s)),
Err(_) => None,
}
};
if let Some(value) = payload {
let _ = app_handle.emit("runtime:snapshot", value);
let _ = app_handle_for_commands.emit("runtime:snapshot", value);
}
}
});
if let Some(window) = app.get_webview_window("main") {
let runtime_core = Arc::clone(&runtime_core);
let app_handle = app_handle.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::Moved(position) = event {
let command = sprimo_protocol::v1::FrontendCommand::SetTransform {
x: Some(position.x as f32),
y: Some(position.y as f32),
anchor: None,
scale: None,
opacity: None,
};
if runtime_core.apply_command(&command).is_ok() {
if let Ok(snapshot) = runtime_core.snapshot().read() {
let _ = app_handle.emit("runtime:snapshot", to_ui_snapshot(&snapshot));
}
}
}
});
}
let _ = app;
Ok(())
})

View File

@@ -12,8 +12,8 @@
"windows": [
{
"title": "sprimo-tauri",
"width": 640,
"height": 640,
"width": 416,
"height": 416,
"decorations": false,
"transparent": true,
"alwaysOnTop": true,

View File

@@ -18,9 +18,9 @@ Auth: `Authorization: Bearer <token>` required on all endpoints except `/v1/heal
"uptime_seconds": 12,
"active_sprite_pack": "default",
"capabilities": {
"supports_click_through": true,
"supports_click_through": false,
"supports_transparency": true,
"supports_tray": true,
"supports_tray": false,
"supports_global_hotkey": true,
"supports_skip_taskbar": true
}
@@ -70,6 +70,11 @@ Auth: `Authorization: Bearer <token>` required on all endpoints except `/v1/heal
}
```
## Compatibility Notes
- `SetFlags.click_through` is deprecated for compatibility and ignored at runtime.
- Frontend state always reports `flags.click_through = false`.
## Error Response
```json

View File

@@ -45,5 +45,6 @@ backend = "bevy"
- `auth_token` is generated on first run if config does not exist.
- `window.x`, `window.y`, `window.scale`, and flag fields are persisted after matching commands.
- On Windows, `recovery_hotkey` provides click-through recovery even when the window is non-interactive.
- `window.click_through` is deprecated and ignored at runtime; it is always forced to `false`.
- On Windows, `recovery_hotkey` now forces `visible = true` and `always_on_top = true` for recovery.
- `frontend.backend` selects runtime frontend implementation (`bevy` or `tauri`).

View File

@@ -10,7 +10,7 @@
# 1. Overview
The frontend is a **desktop overlay pet renderer** implemented in Rust (Bevy). It presents an animated character in a **transparent, borderless window** that can be **always-on-top** and optionally **click-through**. It receives control instructions from a local backend process via **localhost REST API**, applies them to its animation/state machine, and persists user preferences locally.
The frontend is a **desktop overlay pet renderer** implemented in Rust (Bevy). It presents an animated character in a **transparent, borderless window** that can be **always-on-top**. It receives control instructions from a local backend process via **localhost REST API**, applies them to its animation/state machine, and persists user preferences locally.
The frontend must be able to run standalone (idle animation) even if the backend is not running.
@@ -22,7 +22,7 @@ The frontend must be able to run standalone (idle animation) even if the backend
1. **Render a cute animated character overlay** with smooth sprite animation.
2. Provide a **stable command interface** (REST) for backend control.
3. Offer **essential user controls** (tray/menu + hotkeys optional) to avoid “locking” the pet in click-through mode.
3. Offer **essential user controls** (tray/menu + hotkeys optional) to keep the pet recoverable and visible.
4. Persist **window position, scale, sprite pack choice, and flags**.
5. Be **cross-platform** (Windows/macOS/Linux) with documented degradations, especially on Linux Wayland.
@@ -43,7 +43,6 @@ The frontend must be able to run standalone (idle animation) even if the backend
* As a user, I can see the pet on my desktop immediately after launch.
* As a user, I can drag the pet to a preferred location.
* As a user, I can toggle click-through so the pet doesnt block my mouse.
* As a user, I can toggle always-on-top so the pet stays visible.
* As a user, I can change the character (sprite pack).
@@ -55,7 +54,7 @@ The frontend must be able to run standalone (idle animation) even if the backend
## Safety
* As a user, I can always recover control of the pet even if click-through is enabled (hotkey/tray item).
* As a user, I can always recover the pet visibility/interaction state via hotkey or tray item.
---
@@ -82,22 +81,19 @@ The frontend must be able to run standalone (idle animation) even if the backend
* When ON, window stays above normal windows.
### FR-FW-3 Click-through (mouse pass-through)
### FR-FW-3 Interaction model
* Support enabling/disabling click-through:
* ON: mouse events pass to windows underneath.
* OFF: pet receives mouse input (drag, context menu).
* Must provide a **failsafe** mechanism to disable click-through without clicking the pet.
* Click-through is not required.
* Pet remains interactive while visible.
* Must provide a **failsafe** mechanism to recover visibility and interaction state.
**Acceptance**
* With click-through enabled, user can click apps behind pet.
* User can disable click-through via tray or hotkey reliably.
* Recovery hotkey/tray action restores visible, interactive pet state reliably.
### FR-FW-4 Dragging & anchoring
* When click-through is OFF, user can drag the pet.
* User can drag the pet.
* Dragging updates persisted position in config.
* Optional: snapping to screen edges.
@@ -130,12 +126,12 @@ The frontend must be able to run standalone (idle animation) even if the backend
### Platform notes (requirements)
* **Windows:** click-through uses extended window styles (WS_EX_TRANSPARENT / layered), always-on-top via SetWindowPos.
* **Windows:** always-on-top via SetWindowPos.
* **macOS:** NSWindow level + ignoresMouseEvents.
* **Linux:** best effort:
* X11: possible with shape/input region.
* Wayland: click-through may be unavailable; document limitation.
* Wayland: overlay behavior limitations may apply; document limitation.
---
@@ -273,7 +269,7 @@ Each state maps to a default animation (configurable by sprite pack):
* `PlayAnimation { name, priority, duration_ms?, interrupt? }`
* `SetSpritePack { pack_id_or_path }`
* `SetTransform { x?, y?, anchor?, scale?, opacity? }`
* `SetFlags { click_through?, always_on_top?, visible? }`
* `SetFlags { click_through?, always_on_top?, visible? }` (`click_through` is deprecated/ignored)
* `Toast { text, ttl_ms? }` (optional but recommended)
### FR-API-4 Idempotency & dedupe
@@ -303,7 +299,6 @@ Each state maps to a default animation (configurable by sprite pack):
Provide tray/menu bar items:
* Show/Hide
* Toggle Click-through
* Toggle Always-on-top
* Sprite Pack selection (at least “Default” + “Open sprite folder…”)
* Reload sprite packs
@@ -315,7 +310,7 @@ If tray is too hard on Linux in v0.1, provide a fallback (hotkey + config).
At minimum one global hotkey:
* Toggle click-through OR “enter interactive mode
* Force visible + interactive recovery mode
Example default:
@@ -323,7 +318,7 @@ Example default:
**Acceptance**
* User can recover control even if pet is click-through and cannot be clicked.
* User can recover visibility and interaction state even when the pet was hidden or misplaced.
### FR-CTL-3 Context menu (optional)
@@ -348,7 +343,7 @@ Right click pet (when interactive) to open a minimal menu.
* position (x,y) + monitor id (best-effort)
* scale
* always_on_top
* click_through
* click_through (deprecated/ignored; always false)
* visible
* animation:
@@ -432,10 +427,10 @@ Frontend must expose in logs (and optionally `/v1/health`) capability flags:
Example:
* Windows: all true
* macOS: all true
* Windows: click-through false; others vary by implementation status
* macOS: click-through false; others vary by implementation status
* Linux X11: most true
* Linux Wayland: click-through likely false, skip-taskbar variable
* Linux Wayland: skip-taskbar variable and overlay behavior limitations
---
@@ -444,8 +439,8 @@ Example:
## Window
1. Launch: window appears borderless & transparent.
2. Drag: with click-through OFF, drag updates position; restart restores.
3. Click-through: toggle via hotkey; pet becomes non-interactive; toggle back works.
2. Drag: drag updates position; restart restores.
3. Recovery: hotkey restores visible + always-on-top behavior reliably.
4. Always-on-top: verify staying above typical apps.
## Animation

View File

@@ -11,9 +11,9 @@ Date: 2026-02-12
| Command/state pipeline | Implemented | Command queue, state snapshot updates, transient state TTL rollback |
| Config persistence | Implemented | `config.toml` bootstrap/load/save with generated token |
| Sprite pack contract | Implemented | `manifest.json` loader and selected->default fallback |
| Platform abstraction | Implemented | Windows adapter now applies click-through/top-most/visibility/position using Win32 APIs |
| Platform abstraction | Implemented | Windows adapter applies top-most/visibility/position using Win32 APIs; click-through is disabled by current requirements |
| Overlay rendering | Implemented (MVP) | Bevy runtime with transparent undecorated window, sprite playback, command bridge |
| Global failsafe | Implemented (Windows) | Global recovery hotkey `Ctrl+Alt+P` disables click-through and forces visibility |
| Global failsafe | Implemented (Windows) | Global recovery hotkey `Ctrl+Alt+P` forces visibility and top-most recovery |
| Embedded default pack | Implemented | Bundled under `assets/sprite-packs/default/` using `sprite.png` (8x7, 512x512 frames) |
| Build/package automation | Implemented (Windows) | `justfile` and `scripts/package_windows.py` generate portable ZIP + SHA256 |
| QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` |

View File

@@ -14,7 +14,7 @@
- [x] `SetState` updates state and default animation mapping.
- [x] transient state with `ttl_ms` returns to durable state.
- [x] `SetTransform` persists x/y/scale.
- [x] `SetFlags` persists click-through/always-on-top/visible.
- [x] `SetFlags` persists always-on-top/visible and ignores deprecated click-through.
## Config

View File

@@ -4,7 +4,7 @@ Date: 2026-02-12
| Capability | Windows | Linux X11 | Linux Wayland | macOS |
|------------|---------|-----------|---------------|-------|
| `supports_click_through` | true (implemented) | false (current) | false | false (current) |
| `supports_click_through` | false (disabled by product requirement) | false | false | false |
| `supports_transparency` | true | true | true | true |
| `supports_tray` | false (current) | false (current) | false (current) | false (current) |
| `supports_global_hotkey` | true (implemented) | false (current) | false (current) | false (current) |
@@ -12,6 +12,7 @@ Date: 2026-02-12
## Notes
- Current code applies real Win32 operations for click-through, visibility, top-most, and positioning.
- Current code applies real Win32 operations for visibility, top-most, and positioning.
- Click-through is intentionally disabled by current product requirements.
- Non-Windows targets currently use a no-op adapter with conservative flags.
- Wayland limitations remain an expected degradation in v0.1.

View File

@@ -23,13 +23,24 @@ Use `just` for command entry:
```powershell
just check
just test
just build-release
just package-win
just smoke-win
just build-release-bevy
just package-win-bevy
just smoke-win-bevy
just build-release-tauri
just package-win-tauri
just smoke-win-tauri
```
`just package-win` calls `scripts/package_windows.py package`.
`just smoke-win` calls `scripts/package_windows.py smoke`.
Compatibility aliases:
- `just build-release` -> Bevy release build.
- `just package-win` -> Bevy package.
- `just smoke-win` -> Bevy smoke package check.
Packaging script target selection:
- Bevy: `python scripts/package_windows.py package --frontend bevy`
- Tauri: `python scripts/package_windows.py package --frontend tauri`
## Behavior Test Checklist (Packaged App)
@@ -37,8 +48,8 @@ Run tests from an unpacked ZIP folder, not from the workspace run.
1. Launch `sprimo-app.exe`; verify default sprite renders.
2. Verify no terminal window appears when launching release build by double-click.
3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces interactive mode.
4. Verify click-through and always-on-top toggles via API commands.
3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces visibility and top-most recovery.
4. Verify `SetFlags` applies always-on-top and visibility via API commands.
5. Verify `/v1/health` and `/v1/state` behavior with auth.
6. Verify `SetSpritePack`:
- valid pack switches runtime visuals
@@ -46,6 +57,7 @@ Run tests from an unpacked ZIP folder, not from the workspace run.
7. Restart app and verify persisted config behavior.
8. Confirm overlay background is transparent (desktop visible behind non-sprite pixels).
9. Confirm no magenta matte remains around sprite in default pack.
10. Confirm default startup window footprint is reduced (416x416 before runtime pack resize).
## Test Log Template
@@ -90,6 +102,10 @@ Authoritative workflow: `docs/TAURI_RUNTIME_TESTING.md`.
- `/v1/state` with auth
- `/v1/command`
- `/v1/commands`
7. Verify tauri frameless drag:
- left-mouse drag moves window
- window remains non-resizable
- moved position updates runtime snapshot `x/y` and persists after restart
### Packaged Mode (Required Once Tauri Packaging Exists)

View File

@@ -50,6 +50,9 @@ Frontend:
- `runtime:snapshot` event after command application.
- React/Vite frontend now renders sprite atlas frames with PixiJS and updates animation/scale
from runtime snapshot events.
- Tauri window drag is implemented for undecorated mode:
- left-mouse drag starts native window dragging
- moved position is synced into runtime-core snapshot/config state.
- Bevy frontend remains intact.
- Runtime QA workflow is defined in `docs/TAURI_RUNTIME_TESTING.md`.

View File

@@ -68,6 +68,10 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
7. Verify sprite-pack loading:
- valid selected pack loads correctly
- invalid pack path failure is surfaced and runtime remains alive
8. Verify frameless window drag behavior:
- left-mouse drag moves the window
- window remains non-resizable
- moved position is reflected in runtime snapshot state (`x`, `y`) and persists after restart
## API + Runtime Contract Checklist

View File

@@ -2,11 +2,10 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { PixiPetRenderer, type UiSpritePack, type UiSnapshot } from "./renderer/pixi_pet";
import "./styles.css";
const UI_BUILD_MARKER = "issue2-fix3";
function App(): JSX.Element {
const [snapshot, setSnapshot] = React.useState<UiSnapshot | null>(null);
const [error, setError] = React.useState<string | null>(null);
@@ -56,12 +55,20 @@ function App(): JSX.Element {
};
}, []);
const onMouseDown = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
if (event.button !== 0) {
return;
}
void getCurrentWindow().startDragging().catch((err) => {
setError(String(err));
});
}, []);
return (
<main className="app">
<main className="app" onMouseDown={onMouseDown}>
<div className="canvas-host" ref={hostRef} />
<section className="debug-panel">
<h1>sprimo-tauri</h1>
<p>ui build: {UI_BUILD_MARKER}</p>
{error !== null ? <p className="error">{error}</p> : null}
{snapshot === null ? (
<p>Loading snapshot...</p>

View File

@@ -13,6 +13,7 @@ body {
width: 100vw;
height: 100vh;
position: relative;
user-select: none;
}
.canvas-host {

View File

@@ -63,7 +63,7 @@ This breaks sprite presentation and leaves the UI in an error state.
Implemented:
1. `frontend/tauri-ui/package.json`
- Added `@pixi/assets` dependency.
- Removed temporary `@pixi/assets` dependency after switching loader strategy.
2. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
- Switched atlas loading to `Assets.load<Texture>(pack.atlas_data_url)`.
@@ -85,6 +85,7 @@ Implemented:
6. `frontend/tauri-ui/src/main.tsx`
- Added visible UI build marker (`issue2-fix3`) to detect stale embedded frontend artifacts.
- Removed temporary UI build marker after verification passed.
7. `crates/sprimo-tauri/capabilities/default.json`
- Added default capability with:
@@ -102,6 +103,9 @@ Implemented:
### Commands Run
- [x] `cargo check --workspace`
- [x] `cargo test --workspace`
- [x] `just qa-validate`
- [x] `cargo check -p sprimo-tauri`
- [x] `cargo check -p sprimo-runtime-core`
- [x] `just build-tauri-ui`
@@ -128,8 +132,8 @@ Implemented:
### Result
- Current Status: `Fix Implemented`
- Notes: build/check/qa validation passed; manual runtime visual verification still required.
- Current Status: `Verification Passed`
- Notes: reporter confirmed fix works on runtime behavior; closure gate evidence still to be completed.
## Status History
@@ -143,10 +147,12 @@ Implemented:
- `2026-02-13 15:05` - codex - `In Progress` - added explicit UI build marker to detect stale executable/frontend embedding.
- `2026-02-13 15:20` - reporter - `In Progress` - provided `issue2-after-fix3` screenshot; stale-build issue resolved, permission + chroma-key defects observed.
- `2026-02-13 15:35` - codex - `Fix Implemented` - added tauri capability permission file and tauri-side chroma-key conversion.
- `2026-02-13 15:50` - reporter - `Verification Passed` - confirmed the runtime fix works.
- `2026-02-13 16:05` - codex - `Verification Passed` - completed workspace check/test checklist and normalized issue record.
## Closure
- Current Status: `Fix Implemented`
- Current Status: `Verification Passed`
- Close Date:
- Owner:
- Linked PR/commit:

View File

@@ -18,6 +18,27 @@ package-win:
smoke-win:
{{python}} scripts/package_windows.py smoke
build-release-bevy:
cargo build --release -p sprimo-app
build-release-tauri:
just build-tauri-ui
cargo build --release -p sprimo-tauri
package-win-bevy:
{{python}} scripts/package_windows.py package --frontend bevy
smoke-win-bevy:
{{python}} scripts/package_windows.py smoke --frontend bevy
package-win-tauri:
just build-tauri-ui
{{python}} scripts/package_windows.py package --frontend tauri
smoke-win-tauri:
just build-tauri-ui
{{python}} scripts/package_windows.py smoke --frontend tauri
qa-validate:
{{python}} scripts/qa_validate.py

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Build and package a portable Windows ZIP for sprimo-app."""
"""Build and package portable Windows ZIPs for Bevy/Tauri frontends."""
from __future__ import annotations
@@ -18,7 +18,6 @@ from typing import Iterable
ROOT = Path(__file__).resolve().parents[1]
DIST = ROOT / "dist"
BIN_REL = ROOT / "target" / "release" / "sprimo-app.exe"
ASSETS_REL = ROOT / "assets"
@@ -26,8 +25,36 @@ class PackagingError(RuntimeError):
"""Raised when packaging preconditions are not met."""
@dataclass(frozen=True)
class FrontendLayout:
id: str
crate: str
binary_name: str
artifact_name: str
readme_run: str
FRONTENDS: dict[str, FrontendLayout] = {
"bevy": FrontendLayout(
id="bevy",
crate="sprimo-app",
binary_name="sprimo-app.exe",
artifact_name="sprimo-windows-x64",
readme_run="sprimo-app.exe",
),
"tauri": FrontendLayout(
id="tauri",
crate="sprimo-tauri",
binary_name="sprimo-tauri.exe",
artifact_name="sprimo-tauri-windows-x64",
readme_run="sprimo-tauri.exe",
),
}
@dataclass(frozen=True)
class PackageLayout:
frontend: str
version: str
zip_path: Path
checksum_path: Path
@@ -72,12 +99,13 @@ def read_version() -> str:
raise PackagingError("could not determine version")
def ensure_release_binary() -> Path:
if not BIN_REL.exists():
run(["cargo", "build", "--release", "-p", "sprimo-app"])
if not BIN_REL.exists():
raise PackagingError(f"release binary missing: {BIN_REL}")
return BIN_REL
def ensure_release_binary(frontend: FrontendLayout) -> Path:
binary_path = ROOT / "target" / "release" / frontend.binary_name
if not binary_path.exists():
run(["cargo", "build", "--release", "-p", frontend.crate])
if not binary_path.exists():
raise PackagingError(f"release binary missing: {binary_path}")
return binary_path
def ensure_assets() -> None:
@@ -108,13 +136,13 @@ def sha256_file(path: Path) -> str:
return digest.hexdigest()
def package() -> PackageLayout:
def package(frontend: FrontendLayout) -> PackageLayout:
version = read_version()
ensure_assets()
binary = ensure_release_binary()
binary = ensure_release_binary(frontend)
DIST.mkdir(parents=True, exist_ok=True)
artifact_name = f"sprimo-windows-x64-v{version}"
artifact_name = f"{frontend.artifact_name}-v{version}"
zip_path = DIST / f"{artifact_name}.zip"
checksum_path = DIST / f"{artifact_name}.zip.sha256"
@@ -122,13 +150,14 @@ def package() -> PackageLayout:
stage = Path(temp_dir) / artifact_name
stage.mkdir(parents=True, exist_ok=True)
shutil.copy2(binary, stage / "sprimo-app.exe")
shutil.copy2(binary, stage / frontend.binary_name)
shutil.copytree(ASSETS_REL, stage / "assets", dirs_exist_ok=True)
readme = stage / "README.txt"
readme.write_text(
"Sprimo portable package\n"
"Run: sprimo-app.exe\n"
f"Frontend: {frontend.id}\n"
f"Run: {frontend.readme_run}\n"
"Assets are expected at ./assets relative to the executable.\n",
encoding="utf-8",
)
@@ -143,11 +172,16 @@ def package() -> PackageLayout:
checksum = sha256_file(zip_path)
checksum_path.write_text(f"{checksum} {zip_path.name}\n", encoding="utf-8")
return PackageLayout(version=version, zip_path=zip_path, checksum_path=checksum_path)
return PackageLayout(
frontend=frontend.id,
version=version,
zip_path=zip_path,
checksum_path=checksum_path,
)
def smoke() -> None:
layout = package()
def smoke(frontend: FrontendLayout) -> None:
layout = package(frontend)
print(f"package created: {layout.zip_path}")
print(f"checksum file: {layout.checksum_path}")
@@ -160,7 +194,7 @@ def smoke() -> None:
pkg_root = root_candidates[0]
required = [
pkg_root / "sprimo-app.exe",
pkg_root / frontend.binary_name,
pkg_root / "assets" / "sprite-packs" / "default" / "manifest.json",
pkg_root / "assets" / "sprite-packs" / "default" / "sprite.png",
]
@@ -175,18 +209,25 @@ def smoke() -> None:
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Sprimo Windows packaging helper")
parser.add_argument("command", choices=["package", "smoke"], help="action to execute")
parser.add_argument(
"--frontend",
choices=sorted(FRONTENDS.keys()),
default="bevy",
help="frontend package target",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
frontend = FRONTENDS[args.frontend]
try:
if args.command == "package":
layout = package()
layout = package(frontend)
print(f"created: {layout.zip_path}")
print(f"sha256: {layout.checksum_path}")
else:
smoke()
smoke(frontend)
return 0
except PackagingError as exc:
print(f"error: {exc}", file=sys.stderr)