diff --git a/.gitignore b/.gitignore index 2c165dcf..fbcb249e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ data/ # Libretto runtime state. Keep config/skills if setup creates them. .libretto/sessions/ .libretto/profiles/ + +# Local design prototypes and visual QA artifacts. +/design-qa.md +/prototypes/ +/qa/ diff --git a/docs/superpowers/plans/2026-07-18-automation-logging-variants.md b/docs/superpowers/plans/2026-07-18-automation-logging-variants.md new file mode 100644 index 00000000..d640ff1b --- /dev/null +++ b/docs/superpowers/plans/2026-07-18-automation-logging-variants.md @@ -0,0 +1,161 @@ +# Automation Logging Variants 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:** Simplify parallel sync launch and provide three directly comparable live-log presentations. + +**Architecture:** Keep one React app and one parallel-run state. Select `drawer`, `inline`, or `console` from `?logging=`; each presentation renders the same deterministic log event list, so only layout changes between links. + +**Tech Stack:** React 19, Vite 6, Phosphor Icons, native CSS, Node test runner, Sites hosting. + +## Global Constraints + +- Reuse the existing task data, timer, dialogs, sidebar, and three-stage workflow. +- Do not add routes, packages, backend calls, persistence, scheduling, queues, or retry logic. +- Keep Traditional Chinese product copy. +- Keep the existing `.openai/hosting.json` project and workspace-wide access. + +--- + +### Task 1: Shared live-log events + +**Files:** +- Modify: `prototypes/automation-guided-flow/src/task-state.test.mjs` +- Modify: `prototypes/automation-guided-flow/src/task-state.mjs` + +**Interfaces:** +- Produces: `buildRunLogs(progress: number, elapsedSeconds: number): Array<{time: string, task: string, message: string, level: "info" | "success" | "error"}>` + +- [ ] **Step 1: Write the failing test** + +Add a test that imports `buildRunLogs`, checks that progress `4` returns only the dispatch event, and checks that progress `60` includes the failed 渣打 event with `level: "error"`. + +```js +test("buildRunLogs reveals chronological task events as progress advances", () => { + assert.equal(buildRunLogs(4, 0).length, 1); + const logs = buildRunLogs(60, 18); + assert.equal(logs.at(-1).task, "渣打銀行對帳單"); + assert.equal(logs.at(-1).level, "error"); +}); +``` + +- [ ] **Step 2: Verify the test fails for the missing export** + +Run: `node --test src/task-state.test.mjs` + +Expected: FAIL because `buildRunLogs` is not exported. + +- [ ] **Step 3: Implement the smallest deterministic event builder** + +Use one ordered event array with progress thresholds and return only reached events. Format time from `elapsedSeconds` without timers or I/O. + +```js +export function buildRunLogs(progress, elapsedSeconds) { + const now = (offset) => `00:${String(Math.max(0, elapsedSeconds - offset)).padStart(2, "0")}`; + return [ + [0, "系統", "已送出 14 個同步任務", "info", 0], + [10, "富邦全部對帳單", "已連線銀行網站", "info", 3], + [24, "玉山信用卡對帳單", "登入完成,開始下載", "info", 7], + [42, "富邦全部對帳單", "已下載 5 個檔案", "success", 12], + [58, "渣打銀行對帳單", "登入逾時,需要重新執行", "error", 18], + ].filter(([threshold]) => progress >= threshold).map(([, task, message, level, offset]) => ({ time: now(offset), task, message, level })); +} +``` + +- [ ] **Step 4: Verify all state tests pass** + +Run: `node --test src/task-state.test.mjs` + +Expected: 5 tests pass. + +### Task 2: Simplified launch and three presentations + +**Files:** +- Modify: `prototypes/automation-guided-flow/src/App.jsx` +- Modify: `prototypes/automation-guided-flow/src/styles.css` + +**Interfaces:** +- Consumes: `buildRunLogs(progress, elapsedSeconds)` from Task 1. +- Produces: direct variants at `?logging=drawer`, `?logging=inline`, and `?logging=console`. + +- [ ] **Step 1: Simplify the idle hero and confirmation sheet** + +Change the hero to one heading and one `同步全部` button. Remove summary counts and `調整任務`. Trim `PreflightSheet` to credential readiness, selected tasks, `取消`, and `開始同步`; delete warning and acknowledgement state/props. + +```jsx +
+

{count} 個任務可以同步執行

+ +
+``` + +- [ ] **Step 2: Select the presentation from the query parameter** + +Normalize once at module load and default invalid values to `drawer`. + +```js +const requestedLoggingMode = new URLSearchParams(window.location.search).get("logging"); +const loggingMode = ["drawer", "inline", "console"].includes(requestedLoggingMode) ? requestedLoggingMode : "drawer"; +``` + +- [ ] **Step 3: Add one shared log renderer** + +Render time, task, message, and error/success color from `buildRunLogs`. Keep the markup reusable in all three surfaces. + +```jsx +function LogEntries({ logs }) { + return
    {logs.map((log) =>
  1. {log.task}{log.message}
  2. )}
; +} +``` + +- [ ] **Step 4: Add drawer, inline, and console wrappers** + +- Drawer: fixed right panel with aggregate progress, `LogEntries`, close, and stop-all. +- Inline: add a unique `日誌` button per included task row and expand the selected row directly beneath it. +- Console: fixed bottom panel with chronological `LogEntries`, collapse, and stop-all. + +All three wrappers receive only `run`, `logs`, visibility callbacks, and `onStop`; they do not own run state. + +- [ ] **Step 5: Connect launch and visibility** + +On confirm, start the existing parallel run, close confirmation, then open the selected log surface. During a run, the hero contains `查看日誌` and `停止全部`; `查看日誌` reopens the relevant surface. + +- [ ] **Step 6: Style only the new surfaces and delete obsolete rules** + +Remove `.preflight-warning`, `.acknowledge`, and unused multi-check spacing. Add `.live-log-drawer`, `.inline-log`, `.log-console`, and shared `.log-entries` rules using existing colors and borders. Keep the drawer responsive and the console above the 64px sidebar breakpoint. + +- [ ] **Step 7: Build and verify** + +Run: `node --test src/task-state.test.mjs && npm run build && git diff --check` + +Expected: tests pass, Vite emits `dist/server/index.js`, and diff check is clean. + +### Task 3: Browser QA and existing Sites deployment + +**Files:** +- Modify: `prototypes/automation-guided-flow/design-qa.md` +- Generated build output only: `prototypes/automation-guided-flow/dist/**` + +**Interfaces:** +- Consumes the three query-parameter variants from Task 2. +- Produces an updated version at the existing Sites URL. + +- [ ] **Step 1: Verify all three local variants** + +For each query parameter, launch sync and confirm the expected logging surface appears. Check that removed copy and controls are absent, `同步全部` is present, and the browser console has no fresh errors. + +- [ ] **Step 2: Verify responsive layout** + +At the existing narrow browser viewport, confirm no document-level horizontal overflow and that the active logging surface can be closed and reopened. + +- [ ] **Step 3: Update design QA** + +Record the simplified launch flow, three tested variants, interaction checks, browser-console result, and `final result: passed` in `design-qa.md`. + +- [ ] **Step 4: Publish the validated source** + +Push the exact source state to the existing Sites source repository, package the current `dist`, save version 2 with the pushed commit SHA, deploy it, poll to `succeeded`, and retain `workspace_all` access. + +- [ ] **Step 5: Return three direct links** + +Return the same deployed base URL with `?logging=drawer`, `?logging=inline`, and `?logging=console` so the user can compare without a new route or selector UI. diff --git a/docs/superpowers/plans/2026-07-19-automation-active-task-jump.md b/docs/superpowers/plans/2026-07-19-automation-active-task-jump.md new file mode 100644 index 00000000..252527eb --- /dev/null +++ b/docs/superpowers/plans/2026-07-19-automation-active-task-jump.md @@ -0,0 +1,53 @@ +# Automation Active Task Jump Prototype Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build one isolated Automation prototype where active tasks in the running summary navigate to their inline logs. + +**Architecture:** Create one self-contained HTML prototype with realistic mock data and native browser behavior. Keep all presentation and interaction local to the prototype; add one source-level Node check for the required interaction contract. + +**Tech Stack:** HTML, CSS, browser JavaScript, Node.js assertions. + +## Global Constraints + +- Do not modify the production app. +- Remove the 「查看日誌」 action from the prototype. +- Task start and polling never cause automatic expansion or scrolling. +- Preserve other manually opened inline logs. +- Use no new dependencies. + +--- + +### Task 1: Active-task summary and inline-log navigation + +**Files:** +- Create: `prototypes/automation-active-task-jump/index.html` +- Create: `prototypes/automation-active-task-jump/active-task-jump.check.mjs` + +**Interfaces:** +- Consumes: browser `scrollIntoView`, `focus`, `matchMedia`, and DOM events. +- Produces: `revealTaskLog(taskId)` in the prototype script. + +- [ ] **Step 1: Write the failing source contract check** + +Assert that the prototype contains active-task buttons, no 「查看日誌」 button, `revealTaskLog`, `scrollIntoView`, focus transfer, reduced-motion handling, and multiple independently open inline logs. + +- [ ] **Step 2: Run the check to verify it fails** + +Run: `node prototypes/automation-active-task-jump/active-task-jump.check.mjs` +Expected: FAIL because `index.html` does not exist. + +- [ ] **Step 3: Implement the self-contained prototype** + +Build the existing Automation shell, running summary task buttons, three workflow stages, and inline log panels. Selecting an active task opens its stage and log, scrolls to it, focuses its heading, and applies a temporary highlight. + +- [ ] **Step 4: Run the source check** + +Run: `node prototypes/automation-active-task-jump/active-task-jump.check.mjs` +Expected: PASS. + +- [ ] **Step 5: Render and visually verify** + +Render at 1600 × 1000. Confirm typography, layout, active-task scanability, target highlight, and narrow responsiveness. Start a local-only static server and provide the prototype URL. + +**Repository note:** Keep the existing dirty worktree uncommitted and preserve unrelated user changes. diff --git a/docs/superpowers/plans/2026-07-19-automation-disclosure-motion.md b/docs/superpowers/plans/2026-07-19-automation-disclosure-motion.md new file mode 100644 index 00000000..d8ae797d --- /dev/null +++ b/docs/superpowers/plans/2026-07-19-automation-disclosure-motion.md @@ -0,0 +1,86 @@ +# Automation Disclosure Motion 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:** Animate automation stage, inline-log, and show-all disclosures without changing their layout or state model. + +**Architecture:** Reuse Svelte's installed `slide` transition through one local reduced-motion-aware wrapper in `AutomationDashboard.svelte`. Apply it to the three existing conditional surfaces and add a CSS transition to the existing caret. + +**Tech Stack:** Svelte 5, TypeScript, native CSS, Node assertion checks, Electron CDP. + +## Global Constraints + +- No new dependency, route, data state, or visual redesign. +- Expansion and collapse duration is 220 ms; caret rotation is 180 ms. +- `prefers-reduced-motion: reduce` changes disclosure duration to 0 ms. +- Preserve table semantics, fixed column widths, labels, and controls. + +--- + +### Task 1: Disclosure transitions + +**Files:** +- Modify: `src/lib/automation/AutomationDashboard.check.ts` +- Modify: `src/lib/automation/AutomationDashboard.svelte` + +**Interfaces:** +- Consumes: Svelte `slide(node, { duration })`. +- Produces: `disclosureSlide(node: Element)` transition used by stage bodies, inline logs, and task rows. + +- [ ] **Step 1: Write the failing source regression checks** + +Add assertions for the `slide` import, reduced-motion wrapper, three transition directives, and caret transition: + +```ts +assert.match(source, /import \{ slide \} from "svelte\/transition"/); +assert.match(source, /function disclosureSlide\(node: Element\)/); +assert.match(source, /matchMedia\("\(prefers-reduced-motion: reduce\)"\)\.matches \? 0 : 220/); +assert.match(source, /class="stage-body"[^>]*transition:disclosureSlide/); +assert.match(source, /class="task-row"[^>]*transition:disclosureSlide/); +assert.match(source, /class="inline-log-panel"[^>]*transition:disclosureSlide/); +assert.match(source, /transition: transform 180ms ease/); +``` + +- [ ] **Step 2: Run the focused check and confirm the new assertions fail** + +Run: `node --no-warnings --experimental-strip-types --test src/lib/automation/AutomationDashboard.check.ts` + +Expected: FAIL on the missing `svelte/transition` import. + +- [ ] **Step 3: Add the smallest implementation** + +Import `slide`, add the local wrapper, apply `transition:disclosureSlide` to the existing stage body, keyed task rows, and inline log panel, and add `transition: transform 180ms ease` to `.stage-caret`. + +```svelte + +``` + +- [ ] **Step 4: Run focused and project verification** + +Run: `node --no-warnings --experimental-strip-types --test src/lib/automation/AutomationDashboard.check.ts` + +Expected: PASS. + +Run: `npm run typecheck` + +Expected: exit 0 with no Svelte or TypeScript errors. + +Run: `npm run build` + +Expected: exit 0 and renderer/electron bundles emitted. + +- [ ] **Step 5: Verify the live Electron interactions** + +Connect to `http://127.0.0.1:9222`, reload the built page, and verify stage collapse/expand, inline-log collapse/expand, and show-all/collapse. Capture the implementation at the same desktop viewport and confirm there are no fresh console errors. + +- [ ] **Step 6: Record design QA** + +Update `design-qa.md` with the source screenshot paths, implementation screenshot path, viewport/state, the three interactions, console result, focused comparison, and `final result: passed` only when no actionable P0/P1/P2 mismatch remains. diff --git a/docs/superpowers/plans/2026-07-19-automation-log-redesign-options.md b/docs/superpowers/plans/2026-07-19-automation-log-redesign-options.md new file mode 100644 index 00000000..1946d4bb --- /dev/null +++ b/docs/superpowers/plans/2026-07-19-automation-log-redesign-options.md @@ -0,0 +1,88 @@ +# Automation Log Redesign Options — Implementation Plan + +> **For Codex:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Remove the aggregate progress bar from the production Automation running summary and deliver three interactive, directly comparable log-viewer prototypes. + +**Architecture:** Keep the production change narrowly scoped to `AutomationDashboard.svelte` and its source-level regression check. Build the three visual directions as isolated, self-contained HTML prototypes under `prototypes/automation-log-options/`, using the existing Automation shell, Traditional Chinese labels, realistic concurrent task data, and no Electron or banking side effects. + +**Tech Stack:** Svelte 5 source checks for production; standalone HTML/CSS/JavaScript for prototypes. + +--- + +### Task 1: Remove the production aggregate progress bar + +**Files:** +- Modify: `src/lib/automation/AutomationDashboard.check.ts` +- Modify: `src/lib/automation/AutomationDashboard.svelte` + +**Step 1: Write the failing regression check** + +- Replace the assertions for `combinedTaskProgress` with assertions that the running-summary aggregate progress markup and state are absent. +- Keep assertions proving per-task progress UI remains available. + +**Step 2: Run the focused check to verify RED** + +Run: `npx tsx src/lib/automation/AutomationDashboard.check.ts` +Expected: FAIL because the aggregate progress state/function/markup still exists. + +**Step 3: Implement the minimal production change** + +- Remove `aggregateProgress` reactive state. +- Remove `combinedTaskProgress`. +- Remove the hero `aggregate-progress` block only. +- Preserve running task count, 「查看日誌」, 「停止全部」, and per-task progress/status. + +**Step 4: Run the focused check to verify GREEN** + +Run: `npx tsx src/lib/automation/AutomationDashboard.check.ts` +Expected: PASS. + +### Task 2: Build three isolated interactive log prototypes + +**Files:** +- Create: `prototypes/automation-log-options/task-stack.html` +- Create: `prototypes/automation-log-options/console-matrix.html` +- Create: `prototypes/automation-log-options/merged-stream.html` +- Create: `prototypes/automation-log-options/index.html` + +**Step 1: Build the three directions in parallel** + +- Option 1 — Task stack: full-width stacked log sections with sticky task headers and independent collapse. +- Option 2 — Console matrix: equal responsive consoles with independent scroll and follow-tail controls. +- Option 3 — Merged stream: one chronological stream with task tags, filter/search, error-only, and pause/follow controls. +- In every option, one modal must show multiple active task logs simultaneously. +- Do not include the aggregate hero progress bar. + +**Step 2: Add the comparison hub** + +- Add a simple index page linking all three options and summarizing their operational trade-offs. +- Keep every option directly addressable by URL. + +**Step 3: Render all three at one viewport** + +- Produce same-size preview screenshots for direct visual comparison. +- Check modal fit, content density, controls, overflow, and task-state legibility. + +### Task 3: Verify implementation and hand off for selection + +**Files:** +- Modify: `design-qa.md` + +**Step 1: Verify production code** + +Run the focused dashboard check, typecheck, and build. Record any unrelated pre-existing failures without changing their scope. + +**Step 2: Verify prototype interactions** + +- Confirm open/close and Escape behavior. +- Confirm each option displays at least four active tasks at once. +- Confirm option-specific controls work. +- Confirm no option auto-opens on simulated task start. + +**Step 3: Record visual QA and share links** + +- Append a concise comparison result to `design-qa.md`. +- Return all three prototype links and ask the user to choose 1, 2, or 3 before production log UI implementation. + +**Repository note:** Keep the current dirty worktree uncommitted; do not include unrelated user changes in a commit. diff --git a/docs/superpowers/specs/2026-07-18-automation-logging-variants-design.md b/docs/superpowers/specs/2026-07-18-automation-logging-variants-design.md new file mode 100644 index 00000000..8b4f19bc --- /dev/null +++ b/docs/superpowers/specs/2026-07-18-automation-logging-variants-design.md @@ -0,0 +1,33 @@ +# Automation Logging Variants + +## Goal + +Make parallel sync easier to start, then compare three logging presentations without changing the underlying task flow. + +## Shared launch flow + +- Hero shows only `14 個任務可以同步執行` and one `同步全部` action. +- Remove the summary counts and `調整任務` action. +- The confirmation sheet keeps only its title, credential readiness, selected task list, `取消`, and `開始同步`. +- Remove the running-task check, dependency check, downstream-unlock check, multi-window warning, and acknowledgement checkbox. + +## Logging variants + +All variants use the same live run state and realistic timestamped events. The selected mode is controlled by the `logging` query parameter so each version has a direct link. + +1. `drawer`: after launch, the right sheet becomes `即時日誌`, with aggregate progress, latest events, error emphasis, and stop-all. +2. `inline`: each running task row exposes a `日誌` control; the selected task expands beneath its row with recent events. +3. `console`: a bottom panel shows a dense chronological stream across all tasks, with a collapse control. + +## Behavior and error handling + +- Starting sync launches every selected task with one click. +- Logs advance from the same timer as progress, so all three presentations stay consistent. +- A simulated failure event is visually distinct and identifies the task; no backend, persistence, scheduling, or retry system is added. +- Existing credential and run-history dialogs remain available. + +## Verification + +- Unit-test log event generation and existing parallel-run state. +- Verify launch and log visibility for all three query-parameter variants. +- Check responsive behavior, browser console, production build, and update the existing Sites deployment. diff --git a/docs/superpowers/specs/2026-07-19-automation-active-task-jump-design.md b/docs/superpowers/specs/2026-07-19-automation-active-task-jump-design.md new file mode 100644 index 00000000..984e1ebe --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-automation-active-task-jump-design.md @@ -0,0 +1,57 @@ +# Automation Active Task Jump — Design Spec + +## Goal + +Replace the running-summary 「查看日誌」 action with a compact list of active tasks. Selecting a task reveals and scrolls to that task's inline log on the Automation page. + +## Scope + +- This specification now describes the production implementation. +- Reuse the current Automation visual system and page structure. +- Keep 「停止全部」 as the only summary-level action. +- Do not add a log modal, filters, progress aggregation, or new task-management behavior. + +## Running Summary + +- The heading continues to show the number of concurrently running tasks. +- Below the heading, render active, waiting-for-human, and failed tasks as circular icon buttons in a horizontally scrollable layer. +- Hover and keyboard focus reveal a tooltip with the task name, stage, state, and timestamp. +- The former 「查看日誌」 button is absent. + +## Selection Interaction + +When an active task is selected: + +1. Open the workflow stage containing that task if it is collapsed. +2. Reveal that task's inline log if it is collapsed. +3. Close any previously expanded inline log so only the selected task remains open. +4. Smooth-scroll the selected task row into view with enough top offset for the fixed application header. +5. Apply a short blue focus highlight to the target log container, then remove it automatically. +6. Move keyboard focus to the inline log panel without triggering a second scroll. + +Starting or polling a task must not automatically expand logs or move the viewport. + +## Production State + +- Show every active, waiting-for-human, and failed task in the summary. +- Tasks can appear across workflow stages; selecting one opens its collapsed stage. +- Labels, statuses, timestamps, and log output come from the current page model. + +## Accessibility + +- Active-task items use buttons, not generic clickable containers. +- Each button names both the task and the action, for example 「前往玉山信用卡對帳單日誌」. +- Each button's accessible name includes both the log action and task name. +- The target log panel receives programmatic focus. +- Smooth scrolling is disabled when `prefers-reduced-motion: reduce` is active; the focus highlight remains. +- Status is conveyed by text, not color alone. + +## Verification + +- The page initially shows no inline logs expanded. +- Selecting each summary task opens the correct stage and log. +- Opening another log closes the previously expanded log. +- Repeated selection of the same task still repositions and highlights it. +- Simulated task start does not expand or scroll. +- Escape has no special behavior because this design uses no modal. +- Verify desktop layout at 1600 × 1000 and a narrow responsive state. diff --git a/docs/superpowers/specs/2026-07-19-automation-disclosure-motion-design.md b/docs/superpowers/specs/2026-07-19-automation-disclosure-motion-design.md new file mode 100644 index 00000000..781b4bef --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-automation-disclosure-motion-design.md @@ -0,0 +1,25 @@ +# Automation Disclosure Motion Design + +## Goal + +Add a consistent expanding and collapsing motion to the automation workflow disclosures shown in the approved screenshots. + +## Interaction + +- Stage bodies expand and collapse vertically in 220 ms. +- Inline task logs use the same vertical motion beneath the selected task. +- Additional collect-stage task rows animate when `顯示全部任務` is toggled. +- Stage carets rotate over 180 ms so the control state changes with the content. +- Reduced-motion users receive an immediate state change. + +## Constraints + +- Reuse Svelte's installed `slide` transition and the existing component state. +- Do not add packages for the disclosure motion, or add routes, data state, or new visual styling. +- Preserve table semantics, fixed column widths, labels, and existing controls. + +## Verification + +- Add a source-level regression check for the shared transition and each disclosure surface. +- Run the focused check, typecheck, and production build. +- Use the running Electron app over CDP to open and close the three disclosure surfaces and inspect console errors. diff --git a/docs/superpowers/specs/2026-07-19-automation-log-redesign-options-design.md b/docs/superpowers/specs/2026-07-19-automation-log-redesign-options-design.md new file mode 100644 index 00000000..ea89c7f8 --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-automation-log-redesign-options-design.md @@ -0,0 +1,53 @@ +# Automation Log Redesign Options + +## Goal + +Create three interactive HTML prototypes for the shared modal that displays every currently running task's live log, then let the user choose one before production implementation. Independently remove the aggregate progress bar from the production Automation hero. + +## Shared constraints + +- Keep the existing Automation visual system, Traditional Chinese copy, modal backdrop, task data, semantic states, and desktop density. +- The modal shows all currently running tasks at the same time; it is not a one-task switcher. +- Opening the modal must not change task execution. Starting a task must not open the modal or an inline log. +- Each prototype uses realistic raw log text, long paths, success/running/error states, and at least four concurrent tasks. +- The close action, Escape key, scrolling, and visible task-state updates must work. +- Prototype work remains isolated from the production app until an option is selected. + +## Direction 1: Task stack + +- One full-width task section per running task in a single modal scroll area. +- Sticky task header contains name, state, elapsed time, and collapse control. +- Every task is expanded initially; users may collapse noisy tasks while leaving several logs visible. +- Best for readability, raw-log fidelity, and the smallest production implementation. + +## Direction 2: Console matrix + +- Responsive two-column grid of equal task consoles, collapsing to one column at narrow widths. +- Each console has an independent vertical scroll, follow-tail toggle, and compact state header. +- The modal itself keeps a stable header and footer while consoles use the available height. +- Best for operators comparing several tasks side by side. + +## Direction 3: Merged event stream + +- One chronological stream combines entries from every running task. +- Each entry includes timestamp, task label, level, and message; task chips filter the shared stream without hiding the fact that multiple tasks are running. +- Search, pause-follow, and error-only controls are interactive in the prototype. +- Best for quickly locating failures, but production implementation would require reliable per-line timestamps or ingestion metadata. + +## Production hero change + +- Remove the aggregate progress bar and percentage from the active Automation hero. +- Keep the running-task count on the left, show the active-task icon strip beneath it for inline-log jumps, and keep `停止全部` on the right. +- Preserve per-task status/progress in the task table; only the hero aggregate is removed. + +## Prototype delivery + +- Deliver one local prototype with three directly addressable variants and an in-prototype option switcher. +- Use mock-only state; do not call Electron automation APIs or launch banking workflows. +- Verify all primary controls, responsive layout, and console output before handoff. + +## Verification + +- Add a focused production source check proving the hero no longer renders the aggregate progress block. +- Run the focused check, typecheck, production build, and `git diff --check`. +- Capture each prototype at the same desktop viewport and record a design-QA comparison against the supplied modal screenshots. diff --git a/electron/ipc.ts b/electron/ipc.ts index 91d9fe0f..5c217755 100644 --- a/electron/ipc.ts +++ b/electron/ipc.ts @@ -4,6 +4,7 @@ import { automationCancel, automationResume, automationRun, + automationRunMany, automationRunHistory, automationSaveCredentials, loadAutomationDesktopModel, @@ -81,6 +82,7 @@ export function registerOctopusBeakIpc({ (_event, updates: Record) => automationSaveCredentials(updates), ); ipcMain.handle("automation:run", (_event, taskId: string) => automationRun(taskId)); + ipcMain.handle("automation:runMany", (_event, taskIds: string[]) => automationRunMany(taskIds)); ipcMain.handle("automation:resume", (_event, taskId: string) => automationResume(taskId)); ipcMain.handle("automation:cancel", (_event, taskId: string) => automationCancel(taskId)); ipcMain.handle("automation:runHistory", () => automationRunHistory()); diff --git a/electron/main.check.ts b/electron/main.check.ts new file mode 100644 index 00000000..0fb9cab0 --- /dev/null +++ b/electron/main.check.ts @@ -0,0 +1,9 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const source = readFileSync(new URL("./main.ts", import.meta.url), "utf8"); + +assert.match( + source, + /try\s*{\s*prepareLibrettoRunCdpPatch\(\);\s*}\s*catch\s*\(error\)\s*{\s*console\.warn\("libretto-run-cdp-patch-failed", error\);\s*}/, +); diff --git a/electron/main.ts b/electron/main.ts index 8dc72a3a..61d7cd1c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -3,6 +3,7 @@ import { pathToFileURL } from "node:url"; import { app, BrowserWindow, dialog } from "electron"; import { activeAutomationTaskIds, + prepareLibrettoRunCdpPatch, recoverAbandonedAutomationSessions, shutdownAutomationSessions, startAutomationTask, @@ -154,6 +155,11 @@ async function start() { electronPath: process.execPath, })); process.chdir(userData); + try { + prepareLibrettoRunCdpPatch(); + } catch (error) { + console.warn("libretto-run-cdp-patch-failed", error); + } migrateLedgerBeforeWindow(); await recoverAbandonedAutomationSessions().catch((error) => { console.warn("automation-session-startup-recovery-failed", error); diff --git a/electron/preload.check.ts b/electron/preload.check.ts index 3fdfdc52..c8f8dc1a 100644 --- a/electron/preload.check.ts +++ b/electron/preload.check.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { octopusBeakApiChannels } from "../src/lib/desktop/api.ts"; assert.equal(octopusBeakApiChannels.includes("automation:run"), true); +assert.equal(octopusBeakApiChannels.includes("automation:runMany"), true); assert.equal(octopusBeakApiChannels.includes("automation:cancel"), true); assert.equal(octopusBeakApiChannels.includes("automation:runHistory"), true); assert.equal(octopusBeakApiChannels.includes("automation:viewerScreenshot"), true); diff --git a/electron/preload.ts b/electron/preload.ts index a4410e94..3faceef9 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -33,6 +33,7 @@ const api: OctopusBeakApi = { load: () => ipcRenderer.invoke("automation:load"), saveCredentials: (updates) => ipcRenderer.invoke("automation:saveCredentials", updates), run: (taskId) => ipcRenderer.invoke("automation:run", taskId), + runMany: (taskIds) => ipcRenderer.invoke("automation:runMany", taskIds), resume: (taskId) => ipcRenderer.invoke("automation:resume", taskId), cancel: (taskId) => ipcRenderer.invoke("automation:cancel", taskId), runHistory: () => ipcRenderer.invoke("automation:runHistory"), diff --git a/package-lock.json b/package-lock.json index 8aa7e94f..c29d03e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@electron-forge/cli": "^7.11.2", "@electron-forge/maker-dmg": "^7.11.2", "@electron-forge/maker-zip": "^7.11.2", + "@lucide/svelte": "^1.25.0", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.68.0", "@types/node": "^26.0.0", @@ -1717,6 +1718,16 @@ "node": ">=18" } }, + "node_modules/@lucide/svelte": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.25.0.tgz", + "integrity": "sha512-v9m+dD68jxVnqkU3K59mG/RSRFlPGzmKCGSyMfnXcaGv9jODDQMyQkcp1CGvk3Y/cUj9v7f8rw1n//K0B53xGQ==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", diff --git a/package.json b/package.json index 2aab3103..abf53c06 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@electron-forge/cli": "^7.11.2", "@electron-forge/maker-dmg": "^7.11.2", "@electron-forge/maker-zip": "^7.11.2", + "@lucide/svelte": "^1.25.0", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.68.0", "@types/node": "^26.0.0", diff --git a/scripts/spending-chart-alternatives.check.mjs b/scripts/spending-chart-alternatives.check.mjs index 68d9c5dc..76c8098a 100644 --- a/scripts/spending-chart-alternatives.check.mjs +++ b/scripts/spending-chart-alternatives.check.mjs @@ -88,8 +88,8 @@ try { assert.equal(await chart.locator("[data-selection-outline]").count(), 0); assert.equal(await chart.locator("canvas.lc-layout-canvas").count(), 1); assert.equal(await chart.locator("svg.lc-layout-svg").count() > 0, true); - assert.equal(await chart.getAttribute("data-rendered-months"), "20"); - assert.equal(await chart.getAttribute("data-rendered-buckets"), "40"); + assert.equal(await chart.getAttribute("data-rendered-months"), "30"); + assert.equal(await chart.getAttribute("data-rendered-buckets"), "60"); const confirmedToggle = page.locator('[data-chart-state="confirmed"]'); const pendingToggle = page.locator('[data-chart-state="pending"]'); assert.equal(await confirmedToggle.getAttribute("aria-pressed"), "true"); @@ -98,10 +98,10 @@ try { await pendingToggle.click(); assert.equal(await pendingToggle.getAttribute("aria-pressed"), "true"); assert.equal(await chart.getAttribute("data-show-pending"), "true"); - assert.equal(await chart.getAttribute("data-rendered-buckets"), "40"); + assert.equal(await chart.getAttribute("data-rendered-buckets"), "60"); assert.match(await chart.locator(".spending-row-summary").nth(24).textContent(), /Pending.*TWD 8,400/u); assert.match(await chart.locator(".spending-row-summary").nth(24).textContent(), new RegExp(`Confirmed total.*TWD ${monthlyRows[24].total.toLocaleString("en-US")}`, "u")); - assert.equal(await chart.getAttribute("data-rounded-bars"), "38"); + assert.equal(await chart.getAttribute("data-rounded-bars"), "58"); assert.equal(await chart.locator("[data-spending-bar]").count(), 0); const initialScale = Number(await chart.getAttribute("data-initial-scale")); const initialTranslateX = Number(await chart.getAttribute("data-initial-translate-x")); @@ -158,8 +158,7 @@ try { const selectedBandAfterDrag = await chart.locator("[data-selected-period]").boundingBox(); assert.ok(selectedBandAfterDrag); assert.notEqual(selectedBandAfterDrag.x, selectedBandBeforeDrag.x); - const renderedMonthsAfterDrag = Number(await chart.getAttribute("data-rendered-months")); - assert.ok(renderedMonthsAfterDrag <= 23, `rendered ${renderedMonthsAfterDrag} months after drag`); + assert.equal(await chart.getAttribute("data-rendered-months"), "30"); assert.equal(await page.evaluate(() => window.__spendingLoadCount), loadCountBeforeDrag); assert.equal(await chart.locator('[data-action="reset"]').count(), 1); const paintedBounds = await chart.locator('canvas[data-spending-bars-canvas]').evaluate((canvas) => { diff --git a/src/lib/automation/AutomationDashboard.check.ts b/src/lib/automation/AutomationDashboard.check.ts new file mode 100644 index 00000000..a19cff31 --- /dev/null +++ b/src/lib/automation/AutomationDashboard.check.ts @@ -0,0 +1,112 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const source = readFileSync(new URL("./AutomationDashboard.svelte", import.meta.url), "utf8"); +const runTaskSource = source.slice(source.indexOf("async function runTask"), source.indexOf("async function runParallelTasks")); +const runParallelTasksSource = source.slice(source.indexOf("async function runParallelTasks"), source.indexOf("async function stopAllTasks")); + +assert.doesNotMatch(runTaskSource, /expandedLogTaskId\s*=/); +assert.doesNotMatch(runParallelTasksSource, /expandedLogTaskId\s*=/); +assert.match(runParallelTasksSource, /automation\.runMany\(tasks\.map\(\(task\) => task\.id\)\)/); +assert.doesNotMatch(runParallelTasksSource, /Promise\.allSettled/); +assert.match(source, /import \{ slide \} from "svelte\/transition"/); +assert.match(source, /function disclosureSlide\(node: Element\)/); +assert.match(source, /matchMedia\("\(prefers-reduced-motion: reduce\)"\)\.matches \? 0 : 220/); +assert.match(source, /class="stage-body"[^>]*transition:disclosureSlide/); +assert.match(source, /class="inline-log-panel"[^>]*transition:disclosureSlide/); +assert.match(source, /async function toggleCollectTasks\(event: MouseEvent\)/); +assert.match(source, /class="table-reveal"/); +assert.match(source, /container\.animate\(/); +assert.doesNotMatch(source, /class="task-row"[^>]*transition:disclosureSlide/); +assert.match(source, /transition: transform 180ms ease/); +assert.match(source, /class="inline-task-log"/); +assert.doesNotMatch(source, /activeLogsOpen/); +assert.doesNotMatch(source, /openActiveLogs/); +assert.doesNotMatch(source, /aria-labelledby="active-logs-title"/); +assert.doesNotMatch(source, /\$t\.automation\.viewLogs/); +assert.match(source, /from "@lucide\/svelte"/); +assert.match(source, /ArrowLeftRight/); +assert.match(source, /CircleEllipsis/); +assert.match(source, /CloudDownload/); +assert.match(source, /Import as ImportIcon/); +assert.match(source, /Landmark/); +assert.match(source, /class="active-task-jump-list"/); +assert.match(source, /class="active-task-filter"/); +assert.match(source, /\$: iconTasks = automation\.tasks\.filter\([\s\S]*?task\.status === "failed"/); +assert.match(source, /\{#if iconTasks\.length\}/); +assert.match(source, /\{#each iconTasks as task \(task\.id\)\}/); +assert.match(source, /class="active-task-jump"/); +assert.match(source, /class:failed=\{task\.status === "failed"\}/); +assert.match(source, /aria-label=\{`\$\{\$t\.automation\.logs\} · \$\{taskLabel\(task, \$t\)\}`\}/); +assert.match(source, /title=\{taskLabel\(task, \$t\)\}/); +assert.match(source, /onclick=\{\(\) => handleActiveTaskClick\(task\)\}/); +assert.match(source, /function handleActiveTaskClick\(task: AutomationTaskRow\)/); +assert.match(source, /task\.status === "waiting_for_human" && task\.humanSession/); +assert.match(source, /openHumanViewer\(task\)/); +assert.match(source, /async function revealTaskLog\(task: AutomationTaskRow\)/); +assert.match(source, /expandedLogTaskId = task\.id/); +assert.match(source, /showAllCollectTasks = true/); +assert.match(source, /]*id=\{`\$\{task\.id\}-task-row`\}/); +assert.match(source, /getElementById\(`\$\{task\.id\}-task-row`\)/); +assert.match(source, /scrollIntoView\(\{ behavior: reducedMotion \? "auto" : "smooth", block: "start" \}\)/); +assert.match(source, /\.task-row\s*\{\s*scroll-margin-top: 88px;/); +assert.match(source, /focus\(\{ preventScroll: true \}\)/); +assert.match(source, /overflow-x:\s*auto/); +assert.match(source, /\.active-task-filter\s*\{[\s\S]*?border: 1px solid var\(--border\)/); +assert.match(source, /\.active-task-jump-list\s*\{[\s\S]*?flex-wrap: nowrap/); +assert.match(source, /scrollbar-width: none/); +assert.match(source, /onwheel=\{scrollActiveTasks\}/); +assert.match(source, /function scrollActiveTasks\(event: WheelEvent\)/); +assert.match(source, /class="active-task-tooltip"/); +assert.match(source, /role="tooltip"/); +assert.match(source, /onpointerenter=\{\(event\) => showTaskTooltip\(task, event\)\}/); +assert.match(source, /onpointerleave=\{hideTaskTooltip\}/); +assert.match(source, /\$t\.automation\.statusLabels\[hoveredTask\.status\]/); +assert.match(source, /border-radius: 50%/); +assert.match(source, /\.active-task-jump\.failed\s*\{[\s\S]*?var\(--danger\)/); +const resumeHumanViewerSource = source.slice(source.indexOf("async function resumeHumanViewer"), source.indexOf("function pointerPoint")); +assert.match(resumeHumanViewerSource, /automation\.resume\(task\.id\)/); +assert.doesNotMatch(resumeHumanViewerSource, /runTask\(task\)/); +assert.doesNotMatch(source, /aggregateProgress/); +assert.doesNotMatch(source, /combinedTaskProgress/); +assert.doesNotMatch(source, /class="aggregate-progress"/); +assert.match(source, /class="progress-cell"/); +assert.match(source, /\$: activeTasks = automation\.tasks\.filter\(\(task\) => task\.isActive\);/); +assert.match(source, /task\.status === "waiting_for_human"[\s\S]*?automation\.forceQuit\(task\.id\)/); +assert.match(source, /historyTaskCount\(historyRows\.length\)/); +assert.match(source, /class="stage-toggle-action"/); +assert.match(source, /aria-expanded=\{stageOpen\[stage\.id\]\}/); +assert.doesNotMatch(source, /
openSyncSheet\(stage\.tasks\)\}/); +assert.match(source, /class:muted=\{!stageRunnableTasks\(stage\.tasks\)\.length\}/); +assert.doesNotMatch(source, /stage\.description/); +assert.match(source, /\$t\.automation\.startImportHeading/); +assert.match(source, /grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/); +assert.match(source, /:global\(html\) \{\s*overflow-y: scroll;/); +assert.match(source, /class="card workflow-card"/); +assert.match(source, /class="sync-sheet"/); +assert.doesNotMatch(source, /\$t\.automation\.commandId/); +assert.doesNotMatch(source, /class="task-command"/); +assert.match(source, /[\s\S]*width: 32%[\s\S]*width: 14%[\s\S]*width: 22%[\s\S]*width: 12%[\s\S]*width: 20%[\s\S]*<\/colgroup>/); +assert.match(source, /\.automation-table\s*\{\s*table-layout: fixed;/); +assert.match(source, /colspan="5"/); +assert.match(source, /class="modal-body credential-layout"/); +assert.match(source, /class="credential-provider-list"/); +assert.match(source, /class:selected=\{group\.id === selectedCredentialGroupId\}/); +assert.match(source, /async function updateCredentialSearch\(event: Event\)/); +assert.match(source, /selectedCredentialGroupId = visibleCredentialGroups\[0\]\?\.id \?\? ""/); +assert.match(source, /value=\{credentialSearch\} oninput=\{updateCredentialSearch\}/); +assert.match(source, /\$: selectedCredentialGroup = visibleCredentialGroups\.find/); +assert.match(source, /class="modal-body history-layout"/); +assert.match(source, /class="history-filters"/); +assert.match(source, /class="history-error-detail"/); +assert.match(source, /\.history-table \.task-name span\s*\{[\s\S]*display: block/); +assert.match(source, /historySearch/); +assert.match(source, /historyFilter/); +assert.match(source, /\$: historyCounts = historyRows\.reduce/); +assert.match(source, /historyCounts\.completed/); +assert.doesNotMatch(source, /historyFinishedTime/); +assert.doesNotMatch(source, /class="modal-footer"/); diff --git a/src/lib/automation/AutomationDashboard.svelte b/src/lib/automation/AutomationDashboard.svelte index c4d77007..9e8e5e12 100644 --- a/src/lib/automation/AutomationDashboard.svelte +++ b/src/lib/automation/AutomationDashboard.svelte @@ -1,5 +1,7 @@