From 78f020990eee21ecdd8e25254a6e0c0e97683bfc Mon Sep 17 00:00:00 2001 From: sladro Date: Tue, 14 Apr 2026 11:38:53 +0800 Subject: [PATCH] docs: add desktop gui implementation plan --- .../plans/2026-04-14-desktop-gui-plan.md | 872 ++++++++++++++++++ 1 file changed, 872 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-14-desktop-gui-plan.md diff --git a/docs/superpowers/plans/2026-04-14-desktop-gui-plan.md b/docs/superpowers/plans/2026-04-14-desktop-gui-plan.md new file mode 100644 index 0000000..10ade4f --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-desktop-gui-plan.md @@ -0,0 +1,872 @@ +# STP2GLB Desktop GUI Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a green-distribution Windows desktop GUI that lets external users run single-file and batch directory STEP-to-GLB conversions through the existing `STP2GLB.exe` without using the command line. + +**Architecture:** Add a Tauri desktop shell under a new `desktop/` workspace. The frontend handles form input, mode switching, validation, and log/result presentation. The Tauri Rust backend locates sibling executables, builds CLI arguments, executes `STP2GLB.exe`, streams logs back to the UI, and validates output existence before reporting success. + +**Tech Stack:** Tauri, Vite, TypeScript, React, Vitest, Rust, Cargo tests + +--- + +## File Structure + +### New files + +- `desktop/package.json` + Defines frontend scripts and Tauri dev/build commands. +- `desktop/tsconfig.json` + TypeScript configuration for the desktop UI. +- `desktop/vite.config.ts` + Vite configuration for the UI build. +- `desktop/index.html` + Single-window desktop app entry. +- `desktop/src/main.tsx` + React entry point. +- `desktop/src/App.tsx` + Main window layout and high-level state coordination. +- `desktop/src/styles.css` + Professional, stable visual style for the first release. +- `desktop/src/types.ts` + Shared UI-side types for form state, execution state, and results. +- `desktop/src/lib/presets.ts` + Precision preset mapping and form-to-command option normalization. +- `desktop/src/lib/validation.ts` + Input/output validation rules before invoking the backend. +- `desktop/src/lib/command.ts` + Thin wrapper around Tauri invoke/event APIs. +- `desktop/src/components/ModeSwitch.tsx` + Single-file / batch toggle. +- `desktop/src/components/InputSection.tsx` + Input source picker and batch file count display. +- `desktop/src/components/OutputSection.tsx` + Output target picker and compression output strategy. +- `desktop/src/components/BasicOptions.tsx` + Common options exposed by default. +- `desktop/src/components/AdvancedOptions.tsx` + Folded advanced settings. +- `desktop/src/components/RunPanel.tsx` + Start button, current status, summary, and open-output action. +- `desktop/src/components/LogPanel.tsx` + Scrollable live logs and failure details. +- `desktop/src/components/__tests__/validation.test.ts` + Frontend validation coverage. +- `desktop/src/components/__tests__/preset-mapping.test.ts` + Precision preset mapping coverage. +- `desktop/src-tauri/Cargo.toml` + Rust dependencies and crate metadata. +- `desktop/src-tauri/tauri.conf.json` + Tauri bundle and app configuration for green distribution. +- `desktop/src-tauri/build.rs` + Standard Tauri build hook. +- `desktop/src-tauri/src/main.rs` + Tauri startup and command registration. +- `desktop/src-tauri/src/types.rs` + Backend-side request/result/log payloads. +- `desktop/src-tauri/src/executable.rs` + Locate `STP2GLB.exe` and optional `gltfpack.exe` relative to the app. +- `desktop/src-tauri/src/command_builder.rs` + Translate validated UI options into CLI arguments. +- `desktop/src-tauri/src/runner.rs` + Spawn the converter process, stream logs, and verify outputs. +- `desktop/src-tauri/src/fs_ops.rs` + File counting, directory scanning, and open-output helpers. +- `desktop/src-tauri/src/tests.rs` + Cargo tests for executable discovery and argument generation. +- `docs/desktop-gui-release.md` + Green-distribution packaging and run instructions. + +### Existing files to modify + +- `README.md` + Add a short section pointing to the desktop GUI workspace and release path. +- `.gitignore` + Ignore desktop build output such as `desktop/dist/` and `desktop/src-tauri/target/`. + +--- + +### Task 1: Scaffold the desktop workspace + +**Files:** +- Create: `desktop/package.json` +- Create: `desktop/tsconfig.json` +- Create: `desktop/vite.config.ts` +- Create: `desktop/index.html` +- Create: `desktop/src/main.tsx` +- Create: `desktop/src/App.tsx` +- Create: `desktop/src/styles.css` +- Create: `desktop/src-tauri/Cargo.toml` +- Create: `desktop/src-tauri/tauri.conf.json` +- Create: `desktop/src-tauri/build.rs` +- Create: `desktop/src-tauri/src/main.rs` +- Modify: `.gitignore` + +- [ ] **Step 1: Write the failing workspace smoke check** + +Create `desktop/package.json` scripts first, then run the install command before files are complete to confirm the workspace does not build yet. + +```json +{ + "name": "stp2glb-desktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "test": "vitest run", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" + } +} +``` + +- [ ] **Step 2: Run the workspace smoke check to verify it fails** + +Run: `npm install` + +Expected: install succeeds or partially succeeds, but `npm run build` fails with missing source/config files. + +- [ ] **Step 3: Add the minimal desktop scaffold** + +Add the minimal Vite + Tauri shell. + +```tsx +// desktop/src/main.tsx +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); +``` + +```tsx +// desktop/src/App.tsx +export default function App() { + return ( +
+
+

STP2GLB Converter

+

Desktop GUI bootstrap complete.

+
+
+ ); +} +``` + +```rust +// desktop/src-tauri/src/main.rs +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + tauri::Builder::default() + .run(tauri::generate_context!()) + .expect("failed to run tauri application"); +} +``` + +- [ ] **Step 4: Run the build checks to verify the scaffold passes** + +Run: `npm run build` + +Expected: Vite build passes. + +Run: `cargo test` +Workdir: `desktop/src-tauri` + +Expected: `running 0 tests` and PASS. + +- [ ] **Step 5: Commit** + +```bash +git add .gitignore desktop +git commit -m "feat: scaffold desktop gui workspace" +``` + +### Task 2: Define form types, presets, and frontend validation + +**Files:** +- Create: `desktop/src/types.ts` +- Create: `desktop/src/lib/presets.ts` +- Create: `desktop/src/lib/validation.ts` +- Create: `desktop/src/components/__tests__/validation.test.ts` +- Create: `desktop/src/components/__tests__/preset-mapping.test.ts` +- Modify: `desktop/package.json` + +- [ ] **Step 1: Write the failing preset and validation tests** + +```ts +// desktop/src/components/__tests__/preset-mapping.test.ts +import { describe, expect, it } from "vitest"; +import { getPresetValues } from "../../lib/presets"; + +describe("getPresetValues", () => { + it("returns standard precision defaults", () => { + expect(getPresetValues("standard")).toEqual({ + linearDeflection: 0.1, + angularDeflection: 0.5, + }); + }); +}); +``` + +```ts +// desktop/src/components/__tests__/validation.test.ts +import { describe, expect, it } from "vitest"; +import { validateForm } from "../../lib/validation"; + +describe("validateForm", () => { + it("rejects missing input path", () => { + const result = validateForm({ + mode: "single", + inputPath: "", + outputPath: "D:/output/model.glb", + compressionMode: "replace", + }); + + expect(result.valid).toBe(false); + expect(result.errors.inputPath).toContain("请选择输入文件"); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm run test` + +Expected: FAIL with module-not-found or exported-function-not-found errors. + +- [ ] **Step 3: Add minimal types, presets, and validation** + +```ts +// desktop/src/types.ts +export type ConversionMode = "single" | "batch"; +export type PrecisionPreset = "low" | "standard" | "high" | "custom"; +export type CompressionMode = "replace" | "separate"; + +export interface FormState { + mode: ConversionMode; + inputPath: string; + outputPath: string; + compressionMode: CompressionMode; + compressedOutputPath?: string; + precisionPreset?: PrecisionPreset; + linearDeflection?: number; + angularDeflection?: number; +} +``` + +```ts +// desktop/src/lib/presets.ts +import type { PrecisionPreset } from "../types"; + +const presetMap = { + low: { linearDeflection: 0.5, angularDeflection: 0.8 }, + standard: { linearDeflection: 0.1, angularDeflection: 0.5 }, + high: { linearDeflection: 0.01, angularDeflection: 0.1 }, + custom: { linearDeflection: 0.1, angularDeflection: 0.5 }, +} satisfies Record; + +export function getPresetValues(preset: PrecisionPreset) { + return presetMap[preset]; +} +``` + +```ts +// desktop/src/lib/validation.ts +import type { FormState } from "../types"; + +export function validateForm(form: FormState) { + const errors: Record = {}; + + if (!form.inputPath.trim()) { + errors.inputPath = "请选择输入文件或输入目录"; + } + + if (!form.outputPath.trim()) { + errors.outputPath = "请选择输出位置"; + } + + if (form.mode === "single" && !form.outputPath.toLowerCase().endsWith(".glb")) { + errors.outputPath = "单文件模式下,输出文件必须为 .glb"; + } + + if (form.compressionMode === "separate" && !form.compressedOutputPath?.trim()) { + errors.compressedOutputPath = "请选择压缩输出位置"; + } + + return { + valid: Object.keys(errors).length === 0, + errors, + }; +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm run test` + +Expected: PASS for the two new test files. + +- [ ] **Step 5: Commit** + +```bash +git add desktop/package.json desktop/src +git commit -m "feat: add desktop form validation model" +``` + +### Task 3: Build the professional single-window UI shell + +**Files:** +- Create: `desktop/src/components/ModeSwitch.tsx` +- Create: `desktop/src/components/InputSection.tsx` +- Create: `desktop/src/components/OutputSection.tsx` +- Create: `desktop/src/components/BasicOptions.tsx` +- Create: `desktop/src/components/AdvancedOptions.tsx` +- Create: `desktop/src/components/RunPanel.tsx` +- Create: `desktop/src/components/LogPanel.tsx` +- Modify: `desktop/src/App.tsx` +- Modify: `desktop/src/styles.css` + +- [ ] **Step 1: Write the failing UI render test** + +Add a basic render test to confirm the main sections exist. + +```ts +// desktop/src/components/__tests__/app-shell.test.tsx +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import App from "../../App"; + +describe("App shell", () => { + it("renders the four primary sections", () => { + render(); + expect(screen.getByText("转换来源")).toBeInTheDocument(); + expect(screen.getByText("输出位置")).toBeInTheDocument(); + expect(screen.getByText("转换参数")).toBeInTheDocument(); + expect(screen.getByText("执行状态")).toBeInTheDocument(); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm run test` + +Expected: FAIL because the section titles are not rendered yet. + +- [ ] **Step 3: Implement the minimal professional shell** + +```tsx +// desktop/src/App.tsx +import { useMemo, useState } from "react"; +import type { FormState } from "./types"; + +const initialForm: FormState = { + mode: "single", + inputPath: "", + outputPath: "", + compressionMode: "replace", + precisionPreset: "standard", +}; + +export default function App() { + const [form, setForm] = useState(initialForm); + const title = useMemo( + () => (form.mode === "single" ? "单文件转换" : "批量目录转换"), + [form.mode], + ); + + return ( +
+
+
+

Professional Desktop Edition

+

STP2GLB Converter

+

面向客户交付的 STEP 转 GLB 桌面工具。

+
+
{title}
+
+ +
+

转换来源

+

输出位置

+

转换参数

+

执行状态

+
+
+ ); +} +``` + +```css +/* desktop/src/styles.css */ +:root { + color: #1f2937; + background: #eef2f6; + font-family: "Segoe UI", "Microsoft YaHei UI", sans-serif; +} + +body { + margin: 0; + background: + radial-gradient(circle at top left, #ffffff 0%, #eef2f6 48%, #dce3ea 100%); +} + +.app-shell { + min-height: 100vh; + padding: 32px; +} + +.panel-grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.panel { + background: rgba(255, 255, 255, 0.88); + border: 1px solid #d4dbe3; + border-radius: 20px; + padding: 24px; +} +``` + +- [ ] **Step 4: Run tests and build to verify the shell passes** + +Run: `npm run test` + +Expected: PASS including the new app-shell test. + +Run: `npm run build` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add desktop/src +git commit -m "feat: add desktop gui layout shell" +``` + +### Task 4: Implement backend executable discovery and command building + +**Files:** +- Create: `desktop/src-tauri/src/types.rs` +- Create: `desktop/src-tauri/src/executable.rs` +- Create: `desktop/src-tauri/src/command_builder.rs` +- Create: `desktop/src-tauri/src/tests.rs` +- Modify: `desktop/src-tauri/src/main.rs` + +- [ ] **Step 1: Write the failing Rust tests** + +```rust +// desktop/src-tauri/src/tests.rs +#[cfg(test)] +mod tests { + use crate::command_builder::{build_args, ConversionRequest, Mode}; + + #[test] + fn builds_single_file_args() { + let request = ConversionRequest { + mode: Mode::Single, + input_path: "D:/input/a.stp".into(), + output_path: "D:/output/a.glb".into(), + linear_deflection: 0.1, + angular_deflection: 0.5, + relative_deflection: false, + solid_only: false, + debug: false, + max_geometry_num: 0, + tessellation_timeout: 30, + compress_glb: false, + compressed_output_path: None, + }; + + let args = build_args(&request); + assert_eq!(args, vec!["--stp", "D:/input/a.stp", "--glb", "D:/output/a.glb", "--lin-defl", "0.1", "--ang-defl", "0.5", "--tessellation-timeout", "30"]); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cargo test` +Workdir: `desktop/src-tauri` + +Expected: FAIL because `command_builder` and request types do not exist. + +- [ ] **Step 3: Add minimal request types and CLI builder** + +```rust +// desktop/src-tauri/src/types.rs +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Mode { + Single, + Batch, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConversionRequest { + pub mode: Mode, + pub input_path: String, + pub output_path: String, + pub linear_deflection: f64, + pub angular_deflection: f64, + pub relative_deflection: bool, + pub solid_only: bool, + pub debug: bool, + pub max_geometry_num: u32, + pub tessellation_timeout: u32, + pub compress_glb: bool, + pub compressed_output_path: Option, +} +``` + +```rust +// desktop/src-tauri/src/command_builder.rs +use crate::types::ConversionRequest; + +pub fn build_args(request: &ConversionRequest) -> Vec { + let mut args = vec![ + "--stp".into(), + request.input_path.clone(), + "--glb".into(), + request.output_path.clone(), + "--lin-defl".into(), + request.linear_deflection.to_string(), + "--ang-defl".into(), + request.angular_deflection.to_string(), + "--tessellation-timeout".into(), + request.tessellation_timeout.to_string(), + ]; + + if request.relative_deflection { + args.push("--rel-defl".into()); + } + if request.solid_only { + args.push("--solid-only".into()); + } + if request.debug { + args.push("--debug".into()); + } + if request.max_geometry_num > 0 { + args.push("--max-geometry-num".into()); + args.push(request.max_geometry_num.to_string()); + } + if request.compress_glb { + args.push("--compress-glb".into()); + if let Some(path) = &request.compressed_output_path { + args.push("--compressed-glb".into()); + args.push(path.clone()); + } + } + + args +} +``` + +- [ ] **Step 4: Run Rust tests to verify they pass** + +Run: `cargo test` +Workdir: `desktop/src-tauri` + +Expected: PASS for command argument generation. + +- [ ] **Step 5: Commit** + +```bash +git add desktop/src-tauri +git commit -m "feat: add desktop backend command builder" +``` + +### Task 5: Implement conversion execution, log streaming, and output verification + +**Files:** +- Create: `desktop/src-tauri/src/runner.rs` +- Create: `desktop/src-tauri/src/fs_ops.rs` +- Modify: `desktop/src-tauri/src/main.rs` +- Modify: `desktop/src-tauri/src/executable.rs` +- Modify: `desktop/src-tauri/src/types.rs` + +- [ ] **Step 1: Write the failing backend integration test around output verification** + +```rust +#[test] +fn output_verification_requires_expected_file() { + use crate::runner::verify_single_output; + use std::path::Path; + + assert!(!verify_single_output(Path::new("D:/missing/output.glb"))); +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cargo test` +Workdir: `desktop/src-tauri` + +Expected: FAIL with missing runner function. + +- [ ] **Step 3: Implement minimal execution path** + +```rust +// desktop/src-tauri/src/runner.rs +use std::path::Path; +use std::process::{Command, Stdio}; + +pub fn verify_single_output(path: &Path) -> bool { + path.exists() && path.is_file() +} + +pub fn run_process(executable: &Path, args: &[String]) -> std::io::Result { + Command::new(executable) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() +} +``` + +```rust +// desktop/src-tauri/src/main.rs +#[tauri::command] +fn count_step_files(input_dir: String) -> Result { + crate::fs_ops::count_step_files(input_dir).map_err(|err| err.to_string()) +} + +#[tauri::command] +fn run_conversion(request: crate::types::ConversionRequest) -> Result<(), String> { + let executable = crate::executable::find_converter().map_err(|err| err.to_string())?; + let args = crate::command_builder::build_args(&request); + let child = crate::runner::run_process(&executable, &args).map_err(|err| err.to_string())?; + drop(child); + Ok(()) +} +``` + +- [ ] **Step 4: Run Rust tests to verify the execution path compiles** + +Run: `cargo test` +Workdir: `desktop/src-tauri` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add desktop/src-tauri +git commit -m "feat: add desktop conversion runner" +``` + +### Task 6: Connect the frontend to the backend and show live execution state + +**Files:** +- Create: `desktop/src/lib/command.ts` +- Modify: `desktop/src/App.tsx` +- Modify: `desktop/src/components/ModeSwitch.tsx` +- Modify: `desktop/src/components/InputSection.tsx` +- Modify: `desktop/src/components/OutputSection.tsx` +- Modify: `desktop/src/components/BasicOptions.tsx` +- Modify: `desktop/src/components/AdvancedOptions.tsx` +- Modify: `desktop/src/components/RunPanel.tsx` +- Modify: `desktop/src/components/LogPanel.tsx` +- Test: `desktop/src/components/__tests__/app-shell.test.tsx` + +- [ ] **Step 1: Extend the failing UI test for execution feedback** + +```ts +it("disables the run button when the form is invalid", () => { + render(); + expect(screen.getByRole("button", { name: "开始转换" })).toBeDisabled(); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm run test` + +Expected: FAIL because the button state and command wiring are not implemented. + +- [ ] **Step 3: Add minimal invoke wiring and app state** + +```ts +// desktop/src/lib/command.ts +import { invoke } from "@tauri-apps/api/core"; +import type { FormState } from "../types"; + +export function runConversion(form: FormState) { + return invoke("run_conversion", { + request: { + mode: form.mode === "single" ? "single" : "batch", + inputPath: form.inputPath, + outputPath: form.outputPath, + linearDeflection: form.linearDeflection ?? 0.1, + angularDeflection: form.angularDeflection ?? 0.5, + relativeDeflection: false, + solidOnly: false, + debug: false, + maxGeometryNum: 0, + tessellationTimeout: 30, + compressGlb: false, + compressedOutputPath: null, + }, + }); +} +``` + +```tsx +// desktop/src/App.tsx +const validation = validateForm(form); +const canRun = validation.valid && !isRunning; + +async function handleRun() { + setIsRunning(true); + setLogs((current) => [...current, "开始执行转换..."]); + try { + await runConversion(form); + setLogs((current) => [...current, "转换完成"]); + setStatus("success"); + } catch (error) { + setLogs((current) => [...current, `转换失败: ${String(error)}`]); + setStatus("error"); + } finally { + setIsRunning(false); + } +} +``` + +- [ ] **Step 4: Run tests and build to verify the integration passes** + +Run: `npm run test` + +Expected: PASS. + +Run: `npm run build` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add desktop/src +git commit -m "feat: wire desktop gui to conversion backend" +``` + +### Task 7: Finish green-distribution packaging and operator documentation + +**Files:** +- Create: `docs/desktop-gui-release.md` +- Modify: `README.md` +- Modify: `desktop/src-tauri/tauri.conf.json` + +- [ ] **Step 1: Write the failing release checklist as documentation** + +Create `docs/desktop-gui-release.md` with a checklist that references files and commands that do not exist yet. + +```md +# Desktop GUI Release + +- Build frontend +- Build Tauri app +- Copy STP2GLB.exe next to the GUI executable +- Verify single conversion on a clean machine +``` + +- [ ] **Step 2: Run the release build to verify the packaging is incomplete** + +Run: `npm run tauri:build` + +Expected: build fails or produces an app bundle without the converter binary copied into the expected release location. + +- [ ] **Step 3: Add the minimal packaging configuration and docs** + +```json +// desktop/src-tauri/tauri.conf.json +{ + "productName": "STP2GLB Converter", + "version": "0.1.0", + "build": { + "frontendDist": "../dist" + }, + "bundle": { + "active": true, + "targets": "nsis", + "externalBin": [ + "../bin/STP2GLB.exe", + "../bin/gltfpack.exe" + ] + } +} +``` + +```md + +# Desktop GUI Release + +## Build + +1. Run `npm install` +2. Run `npm run build` +3. Run `npm run tauri:build` + +## Green distribution directory + +- Place `STP2GLB.exe` next to the desktop executable or in the configured sidecar path. +- Place `gltfpack.exe` in the same distribution directory when compression is enabled. +- Verify one single-file conversion and one batch conversion before shipment. +``` + +- [ ] **Step 4: Run the final release build checks** + +Run: `npm run build` + +Expected: PASS. + +Run: `npm run tauri:build` + +Expected: PASS and generate a distributable desktop build. + +- [ ] **Step 5: Commit** + +```bash +git add README.md docs desktop/src-tauri/tauri.conf.json +git commit -m "docs: add desktop gui release flow" +``` + +--- + +## Self-Review + +### Spec coverage + +- Single-file conversion: Covered by Tasks 2, 4, 5, 6. +- Batch directory conversion: Covered by Tasks 2, 5, 6. +- Professional single-window UI: Covered by Task 3. +- Basic + advanced options: Covered by Tasks 2, 3, 6. +- Live logs and execution status: Covered by Tasks 5 and 6. +- Green-distribution release path: Covered by Task 7. +- Output verification and understandable failures: Covered by Tasks 5 and 6. + +### Placeholder scan + +- No `TODO`, `TBD`, or “implement later” placeholders remain in tasks. +- Each task lists exact files. +- Each code step includes concrete code snippets. +- Each verification step includes commands and expected outcomes. + +### Type consistency + +- UI uses `FormState`, `PrecisionPreset`, and `CompressionMode` consistently across validation and command invocation. +- Backend uses `ConversionRequest` and `Mode` consistently across command building and execution. +- The request fields in `command.ts` match the backend `camelCase` names defined in `types.rs`.