From 98a87ab08b2e16e60bcaa4857ae6631e0fc4bb93 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 00:41:30 -0400 Subject: [PATCH 01/30] 0.0.130-rc.1 --- CHANGELOG.md | 770 ++++++++++++++++++ Cargo.lock | 271 +++--- changes/releases/0.0.130-rc.1.md | 769 +++++++++++++++++ changes/unreleased/01-human-vs-ai-tracking.md | 43 - changes/unreleased/02-focus-code-review.md | 31 - changes/unreleased/03-flow-shield.md | 28 - changes/unreleased/04-adaptive-break-coach.md | 33 - changes/unreleased/05-struggle-ai-bridge.md | 31 - .../unreleased/06-flow-triggers-dashboard.md | 29 - changes/unreleased/07-focus-scored-commits.md | 38 - changes/unreleased/08-optimal-task-router.md | 29 - changes/unreleased/09-eeg-heatmap.md | 29 - changes/unreleased/10-daemon-event-storage.md | 16 - changes/unreleased/11-shared-daemon-client.md | 32 - changes/unreleased/feat-activity-dashboard.md | 19 - changes/unreleased/feat-brain-awareness.md | 12 - changes/unreleased/feat-brain-insights.md | 24 - changes/unreleased/feat-burn-mlx.md | 9 - .../unreleased/feat-conversations-search.md | 18 - changes/unreleased/feat-data-collection.md | 12 - changes/unreleased/feat-dependabot-fixes.md | 11 - changes/unreleased/feat-design-system.md | 19 - changes/unreleased/feat-dev-loops.md | 10 - .../unreleased/feat-exg-inference-backend.md | 9 - changes/unreleased/feat-fast-umap-1.6.md | 24 - changes/unreleased/feat-gpu-fft-1.2.md | 8 - changes/unreleased/feat-grayscale-dnd.md | 7 - .../feat-history-search-integration.md | 5 - changes/unreleased/feat-neuroskill-cli.md | 5 - changes/unreleased/feat-search-ui-redesign.md | 11 - changes/unreleased/feat-sidebar-cards.md | 30 - changes/unreleased/feat-terminal-tracking.md | 24 - changes/unreleased/feat-validation-daemon.md | 16 - .../unreleased/feat-validation-tauri-ui.md | 18 - changes/unreleased/feat-validation-tests.md | 11 - .../feat-validation-vscode-extension.md | 13 - .../unreleased/feat-vscode-extension-ci.md | 7 - changes/unreleased/feat-vscode-extension.md | 84 -- .../unreleased/feat-vscode-readme-rewrite.md | 5 - .../feat-vscode-readme-screenshots.md | 9 - .../feat-vscode-sidebar-light-mode.md | 7 - changes/unreleased/feat-widget-a11y-i18n.md | 3 - changes/unreleased/feat-widget-analysis.md | 3 - changes/unreleased/feat-widget-biometrics.md | 3 - .../unreleased/feat-widget-brain-dashboard.md | 3 - .../unreleased/feat-widget-calendar-mind.md | 3 - changes/unreleased/feat-widget-deep-links.md | 3 - changes/unreleased/feat-widget-dev-infra.md | 3 - .../feat-widget-focus-streak-session.md | 3 - changes/unreleased/feat-widget-interactive.md | 3 - .../unreleased/feat-widget-offline-cache.md | 3 - changes/unreleased/feat-widget-reload.md | 3 - changes/unreleased/feat-zuna-rs-mlx.md | 9 - .../fix-daemon-deadlocks-and-correctness.md | 9 - changes/unreleased/fix-daemon-performance.md | 8 - changes/unreleased/fix-daemon-reliability.md | 9 - changes/unreleased/fix-eeg-pipeline.md | 5 - changes/unreleased/fix-frontend-leaks.md | 9 - .../fix-macos-libusb-static-link.md | 3 - changes/unreleased/fix-security.md | 5 - changes/unreleased/fix-timezone.md | 6 - crates/skill-iroh/Cargo.toml | 3 + package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 65 files changed, 1693 insertions(+), 988 deletions(-) create mode 100644 changes/releases/0.0.130-rc.1.md delete mode 100644 changes/unreleased/01-human-vs-ai-tracking.md delete mode 100644 changes/unreleased/02-focus-code-review.md delete mode 100644 changes/unreleased/03-flow-shield.md delete mode 100644 changes/unreleased/04-adaptive-break-coach.md delete mode 100644 changes/unreleased/05-struggle-ai-bridge.md delete mode 100644 changes/unreleased/06-flow-triggers-dashboard.md delete mode 100644 changes/unreleased/07-focus-scored-commits.md delete mode 100644 changes/unreleased/08-optimal-task-router.md delete mode 100644 changes/unreleased/09-eeg-heatmap.md delete mode 100644 changes/unreleased/10-daemon-event-storage.md delete mode 100644 changes/unreleased/11-shared-daemon-client.md delete mode 100644 changes/unreleased/feat-activity-dashboard.md delete mode 100644 changes/unreleased/feat-brain-awareness.md delete mode 100644 changes/unreleased/feat-brain-insights.md delete mode 100644 changes/unreleased/feat-burn-mlx.md delete mode 100644 changes/unreleased/feat-conversations-search.md delete mode 100644 changes/unreleased/feat-data-collection.md delete mode 100644 changes/unreleased/feat-dependabot-fixes.md delete mode 100644 changes/unreleased/feat-design-system.md delete mode 100644 changes/unreleased/feat-dev-loops.md delete mode 100644 changes/unreleased/feat-exg-inference-backend.md delete mode 100644 changes/unreleased/feat-fast-umap-1.6.md delete mode 100644 changes/unreleased/feat-gpu-fft-1.2.md delete mode 100644 changes/unreleased/feat-grayscale-dnd.md delete mode 100644 changes/unreleased/feat-history-search-integration.md delete mode 100644 changes/unreleased/feat-neuroskill-cli.md delete mode 100644 changes/unreleased/feat-search-ui-redesign.md delete mode 100644 changes/unreleased/feat-sidebar-cards.md delete mode 100644 changes/unreleased/feat-terminal-tracking.md delete mode 100644 changes/unreleased/feat-validation-daemon.md delete mode 100644 changes/unreleased/feat-validation-tauri-ui.md delete mode 100644 changes/unreleased/feat-validation-tests.md delete mode 100644 changes/unreleased/feat-validation-vscode-extension.md delete mode 100644 changes/unreleased/feat-vscode-extension-ci.md delete mode 100644 changes/unreleased/feat-vscode-extension.md delete mode 100644 changes/unreleased/feat-vscode-readme-rewrite.md delete mode 100644 changes/unreleased/feat-vscode-readme-screenshots.md delete mode 100644 changes/unreleased/feat-vscode-sidebar-light-mode.md delete mode 100644 changes/unreleased/feat-widget-a11y-i18n.md delete mode 100644 changes/unreleased/feat-widget-analysis.md delete mode 100644 changes/unreleased/feat-widget-biometrics.md delete mode 100644 changes/unreleased/feat-widget-brain-dashboard.md delete mode 100644 changes/unreleased/feat-widget-calendar-mind.md delete mode 100644 changes/unreleased/feat-widget-deep-links.md delete mode 100644 changes/unreleased/feat-widget-dev-infra.md delete mode 100644 changes/unreleased/feat-widget-focus-streak-session.md delete mode 100644 changes/unreleased/feat-widget-interactive.md delete mode 100644 changes/unreleased/feat-widget-offline-cache.md delete mode 100644 changes/unreleased/feat-widget-reload.md delete mode 100644 changes/unreleased/feat-zuna-rs-mlx.md delete mode 100644 changes/unreleased/fix-daemon-deadlocks-and-correctness.md delete mode 100644 changes/unreleased/fix-daemon-performance.md delete mode 100644 changes/unreleased/fix-daemon-reliability.md delete mode 100644 changes/unreleased/fix-eeg-pipeline.md delete mode 100644 changes/unreleased/fix-frontend-leaks.md delete mode 100644 changes/unreleased/fix-macos-libusb-static-link.md delete mode 100644 changes/unreleased/fix-security.md delete mode 100644 changes/unreleased/fix-timezone.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6918cd4c..aa6a44c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4626,3 +4626,773 @@ Past releases are archived in [`changes/releases/`](changes/releases/). - Better updater configuration --- + +## [0.0.130-rc.1] — 2026-04-29 + +### Features + +- **Human vs AI activity tracking**: every edit and commit is now classified as `source: "human"` or `source: "ai"` in real-time. `AIActivityTracker` watches VSCode commands (Copilot/Codeium completions, inline chat, AI-generated commit messages) to tag subsequent edits/commits, exposes `getAIRatioForFile()` (rolling 5-minute ratio) for CodeLens + sidebar, and forwards the `source` field to the daemon for `build_events` / `ai_events` storage. + +## How it works + +The `AIActivityTracker` monitors VSCode command execution to detect AI tool usage: + +- **Inline completions** (Copilot, Codeium) — edits within 5 seconds after an AI command are tagged `source: "ai"` +- **Inline chat** (`inlineChat.start`, `copilot.interactiveEditor.*`) — subsequent edits in the same file are tagged AI +- **Commit messages** — `github.copilot.git.generateCommitMessage` marks the next commit as AI-assisted +- **Everything else** — classified as `source: "human"` + +## What's tracked + +| Signal | Classification | +|--------|---------------| +| Manual typing | `human` | +| Copilot inline suggestion accepted | `ai` | +| Copilot inline chat edits | `ai` | +| Paste from external source | `human` | +| AI-generated commit message | `ai` | +| Manually typed commit message | `human` | + +## Per-file AI ratio + +`AIActivityTracker.getAIRatioForFile(path)` returns a rolling 5-minute ratio (0.0 = all human, 1.0 = all AI) used by: +- CodeLens annotations (shows "AI-Assisted" vs focus score) +- Sidebar (Human/AI percentage display) +- Brain status command (Human/AI split) + +## Daemon integration + +The `source` field is sent on every `edit` and `git_commit` event to the daemon. The daemon stores: +- AI commits as `"git commit (ai-assisted)"` in `build_events` +- AI commits also as `ai_events` for analytics weighting +- Completion acceptances as `ai_events` with type `"suggestion_accepted"` + +## Files + +- `src/ai-tracker.ts` — Core tracker (new) +- `src/events.ts` — Wired to classify edits and commits +- `crates/skill-daemon/src/routes/settings_hooks_activity.rs` — Daemon-side storage + +- **Focus-aware code review CodeLens**: annotations at the top of each file show the developer's focus level when the file was last edited (`⚠ Low Focus (42)`, `ℹ Focus: 65/100`, `🤖 AI-Assisted (85%)`, or none for high focus). `NeuroSkill: Show Files Needing Review` command lists low-focus human-authored files. Toggle via `neuroskill.focusCodeLens`. + +## What you see + +- `⚠ Low Focus (42) — Review Recommended` — File was edited during low focus. Click to see all files needing review. +- `ℹ Focus: 65/100` — Moderate focus, informational only. +- `🤖 AI-Assisted (85%)` — Most edits were AI-generated, focus score not applicable. +- No annotation — High focus (>70) or no data yet. + +## Commands + +**NeuroSkill: Show Files Needing Review** (`Cmd+Shift+P`) +- Shows a QuickPick list of files edited during low focus (<50) that were mostly human-authored +- Sorted by focus score (lowest first) +- Select a file to open it + +## How it works + +- `FocusCodeLensProvider` queries `/brain/cognitive-load` (grouped by file) every 30 seconds +- Combines focus data with `AIActivityTracker.getAIRatioForFile()` to distinguish human vs AI code +- Files with high AI ratio (>70%) show AI label instead of focus score — AI code doesn't reflect human cognitive state + +## Settings + +`neuroskill.focusCodeLens` (default: `true`) — Toggle CodeLens annotations on/off. + +## Files + +- `src/codelens-provider.ts` — CodeLens provider (new) + +- **Smart Interruption Shield**: when `/brain/flow-state` reports `in_flow: true`, VS Code's Do Not Disturb mode auto-enables and the status bar shows `$(shield) In Flow 12m`. `NeuroSkill: Toggle Flow Shield` cycles Auto / Forced-on / Forced-off. Toggle via `neuroskill.flowShield`. + +## How it works + +- When `/brain/flow-state` reports `in_flow: true`, the Flow Shield activates +- Enables VSCode's Do Not Disturb mode (VSCode 1.90+) +- Shows `$(shield) In Flow 12m` in the status bar with elapsed time +- When flow state ends, DND is automatically disabled + +## Manual override + +**NeuroSkill: Toggle Flow Shield** (`Cmd+Shift+P`) + +Cycles through three modes: +1. **Auto** (default) — activates/deactivates based on EEG flow detection +2. **Forced on** — always active regardless of flow state +3. **Forced off** — never active + +## Settings + +`neuroskill.flowShield` (default: `true`) — Enable/disable the flow shield feature. + +## Files + +- `src/flow-shield.ts` — Flow shield implementation (new) +- `src/brain.ts` — Calls `flowShield.update()` every 30s + +- **Adaptive Break Coach**: personalized break timing based on the developer's actual EEG focus cycle (via `/brain/break-timing`), not generic Pomodoro. Status bar countdown `$(clock) Break in 8m`, max one notification per cycle, auto-resets when fatigue indicates idleness. `NeuroSkill: Take a Break` resets the timer manually. Toggle via `neuroskill.breakCoach`. + +## How it works + +- Queries `/brain/break-timing` to learn the developer's natural focus cycle length +- Shows a countdown in the status bar: `$(clock) Break in 8m` +- When the predicted focus drop is imminent (<5 min), the countdown turns visible +- When the cycle ends, shows `$(clock) Break time` and optionally notifies + +## Notifications + +- Max one notification per focus cycle +- Message: "You've been focused for 47m. Your natural cycle is 42m — take a break?" +- Buttons: "Take Break" (resets timer) or "Dismiss" + +## Timer sync + +The break coach automatically syncs with the daemon's fatigue data. If `continuous_work_mins` drops below 5 (indicating the user was idle), the session timer resets — no false break suggestions after returning from lunch. + +## Commands + +**NeuroSkill: Take a Break** (`Cmd+Shift+P`) — Manually acknowledge a break and reset the timer. + +## Settings + +`neuroskill.breakCoach` (default: `true`) — Enable/disable break coaching. + +## Files + +- `src/break-coach.ts` — Break coach implementation (new) +- `src/brain.ts` — Calls `breakCoach.refresh()` and `resetSessionIfIdle()` every 30s + +- **Struggle → AI assist bridge**: when `/brain/struggle-predict` flags a file (EEG focus + undo rate + velocity drop + time-on-file), shows an actionable notification with **Open Copilot Chat**, **Open Terminal**, and **Step Back** buttons. Debounced to one suggestion per file per 10 minutes. Toggle via `neuroskill.struggleBridge`. + +## How it works + +- Monitors `/brain/struggle-predict` (EEG focus + undo rate + velocity drop + time-on-file) +- When `struggling: true`, shows an actionable notification: + > "Stuck on auth.ts? (score: 78) Consider breaking the problem into smaller pieces." + +## Action buttons + +| Button | Action | +|--------|--------| +| **Open Copilot Chat** | Opens GitHub Copilot interactive chat (or generic chat panel) | +| **Open Terminal** | Toggles terminal for CLI debugging | +| **Step Back** | Dismiss and take a mental break | + +## Debouncing + +- Max one suggestion per file per 10 minutes +- Prevents notification fatigue while still catching genuine struggles + +## Settings + +`neuroskill.struggleBridge` (default: `true`) — Enable/disable struggle detection and AI suggestions. + +## Files + +- `src/struggle-bridge.ts` — Struggle bridge implementation (new) +- `src/brain.ts` — Calls `struggleBridge.check()` every 30s (replaces the old generic struggle notification) + +- **Personal Flow Triggers dashboard**: collapsible "Your Flow Recipe" sidebar section mining 7-day EEG + activity history — best language (`/brain/code-eeg`), peak hours (`/brain/optimal-hours`), natural cycle length (`/brain/break-timing`), top flow killer (`/brain/context-cost`). Toggle via `neuroskill.flowTriggers`. + +## What you see + +In the NeuroSkill sidebar panel, a collapsible "Your Flow Recipe" section shows: + +- **Best language** — "Focus best on Rust (82)" — from `/brain/code-eeg` +- **Peak hours** — "Peak hours: 9:00, 10:00, 14:00" — from `/brain/optimal-hours` +- **Natural cycle** — "Natural cycle: 42m" — from `/brain/break-timing` +- **Flow killer** — "Flow killer: Slack (focus 38 at switch)" — from `/brain/context-cost` + +## Data sources + +| Insight | API Endpoint | Time Range | +|---------|-------------|------------| +| Best languages | `/brain/code-eeg` | Last 7 days | +| Peak hours | `/brain/optimal-hours` | Last 7 days | +| Natural cycle | `/brain/break-timing` | Last 7 days | +| Flow killers | `/brain/context-cost` | Last 7 days | + +## Settings + +`neuroskill.flowTriggers` (default: `true`) — Show/hide the flow triggers section in the sidebar. + +## Files + +- `src/sidebar.ts` — `_fetchFlowTriggers()` and `_renderFlowTriggers()` methods + +- **Focus-scored git commits in sidebar**: collapsible "Recent Commits" section shows the last 8 commits with author marker (👤 human / 🤖 AI) and a color-coded focus badge captured at commit time via `/brain/flow-state`. AI-assisted commits show "AI" instead of a focus score. Toggle via `neuroskill.focusCommits`. + +## What you see + +In the NeuroSkill sidebar, a collapsible "Recent Commits" section shows the last 8 commits: + +``` +👤 82 fix: resolve auth race condition +👤 45 chore: update dependencies +🤖 AI refactor: extract helper functions +👤 71 feat: add user preferences +``` + +- **👤** = human-authored commit +- **🤖** = AI-assisted commit (message generated by Copilot) +- **Focus badge** = color-coded: green (>70), yellow (40-70), red (<40) +- AI commits show "AI" instead of a focus score — AI output doesn't measure human cognition + +## How it works + +- When the extension detects a git commit (SCM input box clears), it: + 1. Snapshots current EEG focus via `/brain/flow-state` + 2. Checks `AIActivityTracker.isCommitAIAssisted()` + 3. Records the commit with focus score + source label +- Commits stored in-memory (last 15), refreshed on sidebar render +- The daemon also stores commits with human/AI distinction in `build_events` + +## Settings + +`neuroskill.focusCommits` (default: `true`) — Show/hide the commits section in the sidebar. + +## Files + +- `src/sidebar.ts` — `recordCommit()`, `_renderCommits()` +- `src/extension.ts` — Wires commit detection to sidebar recording +- `src/events.ts` — `onCommit` callback with human/AI source + +- **Optimal Task Router**: monitors flow score every 30 s and, when it changes by >20 points, suggests an appropriate task type (refactoring/new features at high focus, code review at moderate, docs/routine at low). Debounced to one suggestion per 15 minutes. Toggle via `neuroskill.taskRouter`. + +## How it works + +- Monitors the flow state score every 30 seconds +- When focus changes by >20 points from the last reading, suggests an appropriate task type: + +| Focus Level | Suggestion | +|------------|------------| +| >75 | "Focus is high (85) — great time for complex work like refactoring or new features." | +| 45-75 | "Focus moderate (58) — good for code review, testing, or incremental tasks." | +| <45 | "Focus low (32) — consider documentation, routine tasks, or a break." | + +## Debouncing + +- Maximum one suggestion every 15 minutes +- No suggestion on the first reading (establishes baseline) +- No suggestion if focus stays within 20 points of the last reading + +## Settings + +`neuroskill.taskRouter` (default: `true`) — Enable/disable task routing suggestions. + +## Files + +- `src/task-router.ts` — Task router implementation (new) +- `src/brain.ts` — Calls `taskRouter.check()` every 30s + +- **Activity dashboard tab**: new Settings tab with daily summary, productivity score (0-100 composite), hourly heatmap, top files/projects, language breakdown, focus sessions, meetings, and stale file alerts. +- **Focus session replay**: click any session to expand a detailed view with file timeline, edit counts, EEG focus overlay bar, and meeting interruptions. +- **Weekly report with CSV export**: 7-day activity digest with daily breakdown chart and one-click CSV download. +- **Productivity scoring**: composite score from edit velocity + deep work time + context stability + EEG focus. +- **Stale file detection**: files edited but untouched for 7+ days. +- **Code-Brain correlation**: focus ranked by language, project, and individual files — shows which code drains or energizes you. +- **Activity timeline**: unified chronological feed of all events (files, builds, meetings, AI, clipboard) with EEG focus indicators. + +- **Brain awareness API**: 14 endpoints under `/v1/brain/*` — flow state, cognitive load, meeting recovery, optimal hours, fatigue check, undo struggle, daily brain report, break timing, deep work streak, task type detection, struggle prediction, interruption recovery, code-EEG correlation, and unified timeline. +- **Flow state detector**: real-time check if user is in deep focus (high focus + low switches + sustained editing). +- **Task type detection**: auto-classify coding/debugging/reviewing/refactoring/testing from activity patterns. +- **Struggle prediction**: fuse undo rate + velocity drop + focus decline to predict when user is stuck. Includes actionable suggestion. +- **Interruption recovery measurement**: measure actual focus recovery time per interruption source (Slack avg 12min, Zoom avg 23min, etc.). +- **Fatigue monitor**: 15-minute background check broadcasts `fatigue-alert` and sends OS notification when focus declines. +- **Daily brain report**: 6pm OS notification with morning/afternoon/evening brain summary. +- **Weekly digest notification**: Monday 9am OS notification with weekly summary. ISO week dedup prevents re-fire on daemon restart. +- **Break timing optimizer**: detect natural focus cycle length from 5-minute EEG buckets. +- **Deep work streak**: gamified consecutive days with 60+ minutes of deep work. + +- **Developer insights endpoint**: `POST /v1/brain/developer-insights` returns 7 actionable EEG-fused insights in one call. +- **Test failure by focus level**: correlates build/test exit codes with EEG focus (high/mid/low) — shows how focus level predicts test outcomes. +- **Hourly productivity**: churn volume + undo rate + avg EEG focus by hour of day — identifies peak and trough hours. +- **Context switch recovery**: EEG focus level at each editor/terminal/panel transition — measures the cognitive cost of switching. +- **AI tool impact on focus**: per-app (Claude, Pi) focus delta vs baseline — quantifies whether AI tools help or hurt flow state. +- **Focus by language**: avg EEG focus + undo rate per programming language — identifies which languages are cognitively easiest/hardest. +- **Dev loop efficiency by hour**: build/test cycle time + pass rate by time of day — shows when edit-test loops are fastest. +- **Tool focus impact**: avg EEG focus per command category (docker, git, deploy, etc.) — reveals which tools correlate with lowest focus. +- **EEG timeseries table**: periodic JSON snapshots of all brain metrics (every 5s during recording). Extensible — new metrics without schema changes. Any event correlatable by timestamp join. +- **EEG timeseries worker**: background thread writes full band powers to `eeg_timeseries` every 5s when an EEG session is active. +- **`POST /v1/brain/eeg-at`**: get EEG metrics at a specific timestamp (nearest sample). +- **`POST /v1/brain/eeg-range`**: get EEG time-series in a range for charts and correlation analysis. +- **Window focus tracking**: `window_focus` events sent when VS Code gains/loses focus. EEG not attributed to coding when VS Code is in background. +- **Live EEG attachment**: `latest_bands` focus/mood injected at event storage time for terminal commands, zone switches, and conversations. + +- **`mlx-e2e` test suite**: new suite in `scripts/test-all.sh` running UMAP and FFT e2e tests with MLX features. Auto-skips on non-macOS. Available via `npm run test:mlx-e2e`. + +- **Conversations table**: stores all AI coding assistant messages (Claude, Pi) with app, role (user/assistant/tool), text, cwd, timestamp, session ID, EEG focus/mood. +- **FTS5 full-text search**: SQLite virtual table for instant keyword search across all conversation text. `POST /v1/brain/search-conversations {"query":"JWT","mode":"fts"}`. +- **Fuzzy search**: LIKE-based substring matching for partial queries. `{"query":"rate limit","mode":"fuzzy"}`. +- **Structured search**: filter by app, role, time range. `{"mode":"structured","app":"claude","role":"user","since":...}`. +- **Semantic search**: user prompts embedded via fastembed (nomic-embed-text-v1.5, local, no API credits) and stored in HNSW label index. Searchable by meaning, not just keywords. +- **Generic embedding store**: `embeddings` table decoupled from specific data tables. Multi-model support — can re-embed with different models, store multiple vectors per item. Source tracking (source_type, source_id). +- **Code context HNSW index**: separate `code_context_index.hnsw` file for code-specific semantic search, keeping EEG label searches uncontaminated. +- **Conversation events**: VS Code extension sends `conversation_message` events to daemon for each Claude/Pi message. User prompts get embedded; assistant responses and tool calls get FTS-indexed only (saves compute). +- **Session tracking**: messages grouped by JSONL filename (session ID). Timestamps from app's own data (ISO 8601 UTC), not extension read time. +- **Embedding settings**: `neuroskill.embedding.maxInputLength` (default 1000 chars) and `neuroskill.embedding.enableConversations` (default true) in VS Code settings. + +- **Meeting detection**: automatically detect Zoom, Teams, Slack, Google Meet, FaceTime, Discord, and Webex meetings from window titles. Track start/end times in `meeting_events` table. +- **Browser tab extraction**: extract page titles from Chrome, Safari, Firefox, Edge, Brave, Arc, Opera, Vivaldi, Chromium. Stored in `browser_title` column on `active_windows`. +- **Clipboard monitoring**: opt-in macOS clipboard change tracking (metadata only — content never stored). Records source app, content type, and size. Includes Automation permission check and settings UI. +- **Multi-monitor awareness**: track windows on secondary monitors across macOS (AppleScript), Linux (wmctrl), Windows (EnumWindows). Dynamic primary screen resolution detection. +- **Undo frequency tracking**: detect undo/redo from file edit diffs via reversal heuristic. `undo_estimate` per 5-second chunk, `undo_count` per file interaction. + +- **Design tokens** (`webview-ui/src/lib/tokens.css`): 30+ CSS custom properties organized into core palette (5 colors), surfaces (4 levels), backgrounds (14 translucent tints), borders (3 colors), semantic aliases (7), and typography (3). Zero inline `rgba()` in App.svelte. +- **Reusable Svelte components** (`webview-ui/src/lib/`): + - `Card` — wrapper with title, info button, info panel, header-right slot, variant borders (warn/danger/info) + - `MetricRow` — label + value pair with variant colors (warn/accent/dim/good/bad) + - `Chevron` — collapsible section with chevron toggle, count badge, slot content + - `ProgressBar` — horizontal bar with value/max, 5 color variants, optional label + - `Gauge` — circular SVG ring with animated fill, value, label + - `Badge` — text badge with 7 variants (default/good/warn/bad/blue/live/score-circle/si) + - `Callout` — alert box with 3 variants (warn/danger/info) +- **Component index** (`webview-ui/src/lib/index.ts`): barrel export for all components. +- **Timestamp compliance**: 25 automated tests (`src/test-timestamps.ts`) verifying: + - No `toLocaleTimeString`/`toLocaleDateString` in data layer (activity-tracker, events, brain, config, vt-parser) + - `toLocaleTimeString` used in UI layer (App.svelte) for display + - `Date.now()` returns UTC milliseconds + - ISO 8601 strings parsed to UTC millis + - No hardcoded timezone offsets in data layer + - All stored timestamps are UTC; local conversion only at UI boundary + +- **Dev loop detection**: identifies edit-build-test cycles from terminal command history. Groups consecutive runs of the same build/test command into loops. +- **`dev_loops` table**: loop type, command, iteration count, pass/fail counts, avg cycle time, fastest/slowest cycle, EEG focus start/end, focus trend (rising/falling/stable). +- **`POST /v1/brain/dev-loops`**: returns detected loops for a time window with all metrics. +- **Enhanced `predict_struggle()`**: terminal failures (+8/fail, max +40) and re-running same failing command 3+ times (+15/rerun, max +30) boost struggle score. New suggestions: "You're re-running the same failing command", "Multiple failures — read the error messages." +- **Enhanced `detect_task_type()`**: terminal command categories override heuristics with higher confidence — docker/kubectl → infrastructure (0.8), deploy → deploying (0.85), git dominating → git_management (0.7), test → testing (0.85), debug → debugging (0.9). +- **Dev loops in sidebar**: command, iteration count, pass rate, avg cycle time, focus trend arrow. Failing loops get red left border. +- **Dev loops in Tauri Activity tab**: expanded view with pass rate percentage, cycle time, focus trend. +- **Loop efficiency by hour**: insight showing build/test cycle time + pass rate by time of day. + +- **Inference backend selector**: `exg_inference_device` setting expanded from `gpu | cpu` to `auto | mlx | gpu | cpu`. Default changed from `gpu` to `auto` (picks MLX on macOS, GPU elsewhere). +- **Four-button selector in EXG Settings**: Auto, MLX, GPU, CPU — each with localized label and description. Reconnect headset hint shown after change. +- **`embed-exg-mlx` feature**: new daemon Cargo feature that enables `skill-router/mlx`, `skill-eeg/mlx`, and `embed-zuna-mlx` together. + +- **fast-umap 1.6.0**: updated from 1.5.1 with MLX, PCA, and nearest-neighbor descent support. +- **MLX UMAP backend**: Apple Silicon native UMAP projection via `burn-mlx`. Runtime dispatch between MLX and GPU (wgpu) based on user preference. Auto defaults to MLX on macOS, GPU elsewhere. +- **Precision selector**: F32 / F16 precision for the GPU (wgpu) backend. MLX is F32-only (fast-umap trait constraint). Exposed in UMAP settings as chip group. +- **Backend & precision UI**: new "Compute Backend" section in UMAP settings with Auto / MLX / GPU chips and F32 / F16 precision chips. Pipeline summary badge shows active backend and precision. + +- **UMAP e2e benchmarks**: `umap_e2e_small` (200 pts), `umap_e2e_medium` (1K pts), `umap_e2e_large` (5K pts) with synthetic 32-dim EEG embeddings. Reports backend, timing, throughput (pts/sec), and separation score. + +- **gpu-fft 1.2.0**: updated from 1.1.1 with MLX FFT backend. EEG signal filtering (overlap-save convolution) uses MLX when `skill-eeg/mlx` is enabled. ~3.7x faster than wgpu at N=65536. +- **`skill-eeg/mlx` feature**: new feature gate that enables `gpu-fft/mlx` alongside `gpu-fft/wgpu`. Wired into `embed-exg-mlx` daemon feature. + +- **FFT MLX e2e**: `fft_e2e_roundtrip_256`, `fft_e2e_batch_4ch`, `fft_e2e_psd_peak_detection`, `fft_e2e_large_batch` (32 channels x 1024 samples). Validates round-trip accuracy, PSD peak detection, and batch throughput. + +- **Grayscale display mode**: optional system-wide grayscale that activates/deactivates in sync with Do Not Disturb. Reduces visual distraction during deep work. macOS only, default off. + +- **File activity in history sessions**: expanded EEG sessions show files worked on during the session with focus indicators, edit counts (+/-), language, and meeting interruption badges. +- **File activity in interactive search**: `file_activity` nodes in the 3D search graph linked to EEG epochs and text labels, showing which files were being edited during matching brain states. +- **Meeting nodes in search graph**: detected meetings appear alongside EEG results in the interactive search as amber nodes with temporal proximity edges. + +- **Git context card**: current branch (monospace), dirty/staged file count, ahead/behind indicators. Uses VS Code git extension API. +- **Session timeline card**: horizontal bar chart showing activity events by hour of day. Color-coded by volume. +- **Workspace activity card**: per-file edits, lines added/removed, focus time. Grouped by workspace folder (project), sorted by activity. Active file indicator (blue dot). Top 10 files per project. +- **Environment card**: editor/terminal/panel time split (colored stacked bar), tab count, editor groups, terminal count. Per-terminal cards with shell type, CWD, PID, shell integration status. +- **Terminal I/O sections**: collapsible with chevrons, minimized by default. Input shows timestamped commands with CWD. Output shows VT-parsed session content. +- **Terminal input card**: keystroke intensity per program (keys/min), duration, collapsible behind chevron. +- **Terminal impact card**: EEG focus delta by command category with pass rate percentage. +- **Context switch cost card**: focus level at each zone transition type with switch count. +- **Dev loops card**: edit-build-test cycles with iteration count, pass/fail rate, cycle time, focus trend (rising/falling/stable arrow). +- **Today's report card**: productivity score, morning/afternoon/evening period breakdown with focus bars and churn. +- **Energy card**: fatigue bar (inverse of focus decline), continuous work time, streak badge. +- **Struggle card**: EEG-only (hidden without recording), score 0-100, contributing factors. +- **Optimal hours card**: peak/avoid hours grid. +- **AI usage card**: acceptance rate bar, suggestion count, source breakdown. +- **Today vs yesterday card**: files and churn comparison with directional arrows. +- **Code review detection**: auto-detected when file switches > 5 and edit velocity < 1 line/min. +- **Process monitor card**: running dev servers (vite, next, webpack, cargo, node, python, docker, postgres) with ports and PIDs. +- **Info toggles**: every card has a `?` button explaining how metrics are calculated. +- **Dual-speed updates**: local data every 5s (visible), brain data every 30s. Pauses when sidebar hidden. + +- **OS-wide shell hooks**: preexec/precmd hooks for zsh, bash, fish, PowerShell. Background curl sends every command to daemon — zero delay to prompt. Self-contained scripts generated by daemon, stored in `~/.skill/shell-hooks/`. +- **Shell hook management**: `GET /v1/activity/shell-hook?shell=zsh` returns hook script, `POST /v1/activity/install-shell-hook` writes to rc file, `POST /v1/activity/uninstall-shell-hook` removes cleanly, `POST /v1/activity/shell-hook-status` health check per shell. +- **Terminal command table**: `terminal_commands` with command text, binary name, args, cwd, exit code, duration, auto-categorized (284 CLI tools across 18 categories), EEG focus at start + end for delta calculation. +- **Binary extraction**: raw binary name stored separately from args. Lazy categorization — `POST /v1/brain/recategorize-commands` reruns rules on all historical data. `POST /v1/brain/binary-stats` discovers uncategorized tools by frequency. +- **284 CLI tools recognized**: git, gh, glab, hf, docker, kubectl, helm, aws, gcloud, az, brew, pip, cargo, npm, psql, redis-cli, ollama, claude, pi, vim, tmux, htop, terraform, and 260+ more across 18 categories (build, test, run, git, docker, deploy, install, navigate, debug, network, database, ai, editor, multiplexer, monitor, env, system, other). +- **Script session recording**: `script` PTY proxy wraps shell sessions, captures all terminal I/O (including input inside interactive programs). Logs stored in `~/.skill/terminal-logs/` with auto-rotation. +- **VT100 terminal emulator**: `vt-parser.ts` replays script recordings through a virtual terminal with scrollback capture. Handles cursor movement, screen clears, alternate screen buffer. Separates input from output via prompt pattern detection. +- **App-specific JSONL parsing**: reads Claude Code (`~/.claude/projects/`) and Pi agent (`~/.pi/agent/sessions/`) conversation files directly. Extracts user prompts with real timestamps, assistant responses, and tool calls. Supports multiple parallel sessions. +- **Readline history polling**: monitors `~/.python_history`, `~/.node_repl_history`, `~/.irb_history`, `~/.psql_history` for REPL input. +- **`neuroskill terminal` CLI**: `status` (hook health per shell), `install [shell]`, `uninstall [shell]`, `commands` (recent tracked), `impact` (focus delta by category), `loops` (dev loop detection). +- **Tauri Terminal settings tab**: per-shell install/uninstall/repair buttons with health indicators (green/yellow/red dot), recent commands preview, "How it works" documentation. + +- **Validation / fatigue-research daemon backend**: foundations for calibrating the Break Coach and Focus Score against four external instruments — KSS (Karolinska Sleepiness Scale), NASA-TLX (workload), PVT (Psychomotor Vigilance Task), and an EEG-derived fatigue index per Jap et al. 2009. +- **`skill-data::validation_store`** — new SQLite store at `~/.skill/validation.sqlite` with five tables (`config`, `kss_responses`, `tlx_responses`, `pvt_runs`, `prompt_log`). Persistent config is a single-row JSON blob with serde defaults so adding a new channel later is a non-migration. New constant `VALIDATION_FILE` in `skill-constants`. +- **EEG fatigue index**: pure function `eeg_fatigue_index(bands) → Option` computing `(α + θ) / β` over the existing band-power snapshot. Guards against missing bands and zero-β. Passive; ships **on** because it costs nothing when no headset is attached. +- **Pure scheduler `decide_prompt(ctx, ...) → PromptDecision`**: respects the `respect_flow` master gate, configurable quiet hours, per-channel daily caps, runtime snoozes, and rate limits. Channel ordering: KSS first (lightest), TLX after a long task unit, PVT on a weekly cadence. Live `read_in_flow` and `read_break_coach_active` open the activity store read-only and ask `flow_state_now(300).in_flow` and `fatigue_check().fatigued` so the gates actually mean something. +- **Sane defaults — opt-in everywhere**: KSS, TLX, PVT all ship `enabled = false`. Only the passive EEG fatigue index ships on. The `respect_flow` master gate ships on. + +- **Validation & Research settings tab** (`src/lib/settings/ValidationTab.svelte`): new top-level settings tab gathering five `SettingsCard` blocks — global gates (`respect_flow`, quiet hours), KSS, NASA-TLX, PVT, and EEG fatigue index — plus a Calibration Week button and a recent-results card. + - Every toggle / numeric input PATCHes one nested field via `daemonPatch("/v1/validation/config", …)`; the daemon's recursive JSON merge keeps the rest of the config untouched. + - EEG fatigue card shows the live `(α + θ) / β` value polled every 5 s from `/v1/validation/fatigue-index`. + - Calibration Week: single button that batch-updates all four channels to higher-frequency presets (KSS 8/day, TLX after every flow block ≥ 20 min, PVT mid-week) in one PATCH. +- **PVT panel** (`src/lib/settings/PvtPanel.svelte`): full 3-minute Psychomotor Vigilance Task. Random ITIs (2–10 s), green-dot stimulus, `performance.now()` for sub-millisecond RT measurement, tracks responses + false starts. On finish computes mean RT, median RT, slowest-10% mean RT (anticipation-resistant), and lapse count (RT > 500 ms per Dinges & Powell 1985), POSTs to `/v1/validation/pvt`. State machine: intro → running → done. +- **NASA-TLX form** (`src/lib/settings/TlxForm.svelte`): six-slider modal for the raw (un-weighted) NASA-TLX. Performance scale uses inverted endpoints ("Failure" → "Perfect") per Hart 2006. POSTs to `/v1/validation/tlx` with `task_kind`, `task_duration_secs`, optional `prompt_id` echo-back. +- **Pure stats helper** (`src/lib/settings/pvt-stats.ts`): extracted `mean`, `median`, `slowest10Mean`, `lapseCount`, `computeStats` from the PVT panel so the math is unit-testable without a Svelte renderer. +- **`daemonPatch(path, body)`** in `src/lib/daemon/http.ts` — needed for the validation config endpoint. + +- **Test coverage for the validation feature across all three layers** — 72 tests total, all passing. + - **Rust unit tests** (`crates/skill-data/src/validation_store.rs`): 19 tests covering config persistence, store round-trips, the `(α + θ) / β` fatigue index (Jap et al. 2009) including zero-β and missing-band edge cases, every scheduler branch (flow respect, quiet hours, snooze blocking, KSS rate limit, TLX trigger after long task, TLX skip on short task, PVT weekly cadence, PVT silence after recent run, quiet-window-equals-end semantics), and prompt-log queries. + - **Rust HTTP integration tests** (`crates/skill-daemon/src/routes/validation.rs`): 10 tests using `tower::ServiceExt::oneshot()` against a tempdir-backed `AppState` — `GET /config` defaults, `PATCH /config` partial-update + round-trip, `POST /kss` happy path, KSS score-out-of-range → 400, TLX subscale-out-of-range → 400, `GET /should-prompt` returns `{kind: "none"}` with default config, `POST /snooze` envelope, plus three `merge_json` tests covering deep nesting. + - **Vitest — PVT statistics** (`src/tests/pvt-stats.test.ts`, 12 tests): edge cases for `mean`, `median`, `slowest10Mean` (including the n<10 fallback), `lapseCount` with the 500 ms Dinges & Powell threshold and explicit-threshold override, plus a known-fixture `computeStats` round-trip. + - **Vitest — i18n coverage** (`src/tests/validation-i18n.test.ts`, 23 tests): every VS Code l10n bundle exists and contains every user-facing key (KSS prompt + scores 1/5/9, TLX/PVT prompts, all four escape-hatch labels); every Tauri language `validation.ts` imports cleanly; English Tauri bundle covers every required key; every non-English Tauri bundle has at least the tab name and disclaimer translated. + +- **VS Code extension joins the validation prompt loop**: new `ValidationManager` (`extensions/vscode/src/validation.ts`) polls `/v1/validation/should-prompt` every 90 s and renders whichever channel the daemon decides to fire. + - **KSS** (1–9 sleepiness) — full QuickPick with Karolinska wording, plus Snooze 30m / Don't ask today / Stop these prompts escape hatches in-line. POSTs the answer to `/v1/validation/kss` echoing the daemon's `prompt_id` so the prompt log can mark it answered. + - **NASA-TLX** — fallback path: shows an information message offering to open the form in the Tauri app (deep link `neuroskill://validation/tlx`) plus the same escape hatches. + - **PVT** — weekly nudge: offers to deep-link into the Tauri PVT panel (`neuroskill://validation/pvt`) or skip a week (snoozes for 6 days so the next reminder fires on cadence). +- **Two new commands**: `NeuroSkill: Open Validation Settings…` (deep links into the Tauri preferences pane) and `NeuroSkill: Check for Validation Prompt Now` (forces a single scheduler poll — useful for opt-in onboarding). +- **`DaemonClient.patch()`**: extension's daemon client gained a PATCH method so the "Stop these prompts" escape hatch can flip `enabled = false` on the persistent config. + +- **VS Code extension**: separate repo (`NeuroSkill-com/vscode-neuroskill`) as git submodule at `extensions/vscode/`. 50+ tracked event types across editing, navigation, debugging, git, AI, terminal, clipboard, and more. +- **Sidebar webview (Svelte 5)**: full brain dashboard in the VS Code activity bar with circular flow gauge, metrics strip, daily report, energy, struggle, optimal hours, workspace activity, environment, terminal impact, context cost, and dev loops. +- **Activity bar icon**: neural network SVG icon in the VS Code sidebar; NeuroSkill logo (PNG) in the webview header. +- **Brain status bar in VS Code**: polls daemon every 30s showing flow state, fatigue, streak, task type, and struggle score. Shows "offline" when daemon unreachable. Notifications for fatigue and struggle. +- **Dual-speed sidebar updates**: local data (files, terminals, layout) refreshes every 5s when sidebar is visible; brain endpoints every 30s. Pauses when hidden. +- **Event caching**: offline-resilient `pendingEvents` array (10K cap) persisted to disk via `globalStorageUri`. Auto-flushes on reconnect. Survives VS Code restarts. Cache count shown in status bar tooltip. +- **Workspace activity tracking**: per-file edits, lines added/removed, focus time, active file indicator. Grouped by workspace folder (project), sorted by activity. Top 10 files per project. +- **Terminal command tracking**: full command text, exit code, cwd, output streaming (via `execution.read()`), shell type detection (zsh/bash/fish/powershell/cmd/node/python), focus time per terminal, PID. Expandable command output in sidebar. +- **Terminal shell integration**: captures commands via `onDidStartTerminalShellExecution` / `onDidEndTerminalShellExecution` (VS Code 1.93+). CWD tracked via `onDidChangeTerminalShellIntegration`. +- **Zone tracking**: editor/terminal/panel time split with stacked color bar (blue/green/yellow). Layout chips showing tab count, editor groups, terminal count. +- **Auto EEG labeling**: every significant VS Code event auto-inserts a searchable label into EEG recordings with smart categorization (editing, debugging, git commits, AI assistance, meetings, errors, navigation, terminal commands, zone switches). +- **Inline label embedding**: auto-labels embedded immediately via fastembed for instant searchability (no idle reembed wait). +- **Command execution tracking**: 40+ VS Code commands tracked (go-to-definition, rename, find, format, fold, git, AI, debug, layout) with semantic categorization. +- **IntelliSense acceptance detection**: multi-char single-line insertions heuristically identified as autocomplete acceptances. +- **Clipboard tracking**: clipboard content changes polled every 5s with debounce. +- **Layout snapshots**: periodic (60s) capture of editor groups, visible editors, open tabs, terminal count sent to daemon. +- **Zone switch events**: editor/terminal/panel transitions sent to daemon with EEG focus snapshot. +- **File system watcher**: selective watching of package.json, Cargo.toml, go.mod, .git/HEAD for external change detection. +- **Environment context**: one-time capture of appHost, remoteName, shell, uiKind, language. +- **Info toggles**: every sidebar card has a `?` button explaining how metrics are calculated (flow score formula, struggle signals, energy bar, optimal hours, dev loops, terminal impact, context cost). +- **Open NeuroSkill button**: launches native app from sidebar (cross-platform: `open -a` macOS, `start` Windows, `xdg-open` Linux). Also in command palette. +- **Command palette → sidebar**: `Show Brain Status`, `Today's Report`, `Am I Stuck?`, `Best Time to Code` now open sidebar and scroll to relevant section instead of showing toast notifications. + +- **Developer insights endpoint**: `POST /v1/brain/developer-insights` returns 7 actionable insights in one call. +- **Test failure by focus level**: correlates build/test exit codes with EEG focus (high/mid/low) — "your tests fail 45% more when focus is low." +- **Hourly productivity**: churn + undo rate + avg focus by hour of day — "you write 3x more bugs after 2pm." +- **Context switch recovery**: focus level at editor/terminal/panel transitions — "switching to terminal costs 4 focus points." +- **AI tool impact**: Claude/Pi focus delta vs baseline — "Claude conversations drop focus by 8 points." +- **Focus by language**: avg EEG focus + undo rate per programming language — "best focus in Rust, worst in CSS." +- **Dev loop efficiency by hour**: build/test cycle time + pass rate by time of day. +- **Tool focus impact**: avg focus when using each command category (docker, git, deploy, etc.). + +- **Widget accessibility and localization**. + +- **Analysis widgets (Optimal Hours, Daily Report, Weekly Trend)**. + +- **Biometric widgets (Heart Rate, Cognitive Load, EEG Band Power, Sleep)**. + +- **Brain Dashboard widget (medium)**. + +- **Calendar Mind State widget (large)**. + +- **Widget deep links (neuroskill:// URL scheme)**. + +- **Widget development infrastructure**. + +- **Core desktop widgets (Focus, Streak, Session, Break Timer)**. + +- **Interactive widget buttons (macOS 14+)**. + +- **Widget offline data caching**. + +- **Widget timeline reload on state changes**. + +- **zuna-rs 0.1.4**: updated from 0.1.3 with native MLX backend for EEG embedding inference. +- **`embed-zuna-mlx` feature**: new `ZunaMlxState` struct with `ZunaEncoder`. Adds `load_zuna_mlx()` and `encode_zuna_mlx()` functions matching the existing GPU/CPU variants. +- **Load priority**: when user selects Auto or MLX, encoder loading tries MLX first, then GPU f16, then GPU f32, then CPU. + +### Performance + +- **MLX vs GPU benchmarks** (Mac mini, Apple Silicon): + +| Dataset | Points | GPU (wgpu) | MLX | Speedup | +|---|---|---|---|---| +| Small | 200 | 120.9 s | 2.3 s | **51x** | +| Medium | 1,000 | 136.6 s | 7.1 s | **19x** | +| Large | 5,000 | 152.8 s | 23.8 s | **6.4x** | + +- **`open_readonly` for read-only handlers**: added `ActivityStore::open_readonly()` that skips 15+ ALTER TABLE migrations and opens in read-only mode. Switched 38+ HTTP handlers and 3 background tasks from `open` to `open_readonly`. +- **`PRAGMA busy_timeout=5000`**: added to `init_wal_pragmas` and `util::open_readonly` so all database connections (activity, labels, screenshots, EEG embeddings) wait up to 5 s on lock contention instead of failing immediately with SQLITE_BUSY. +- **Composite indexes**: added `(seen_at, file_path)` and `(seen_at, project)` indexes on `file_interactions` for queries that filter by time range and group by path or project. +- **Lock consolidation**: `productivity_score` reduced from 3 separate mutex acquisitions to 1 via internal `_q` query helpers. `weekly_digest` reduced from 12 to 4 locks. Introduced `daily_summary_q`, `context_switch_rate_q`, `get_focus_sessions_in_range_q` static methods that take `&Connection` directly. +- **`PRAGMA optimize` after pruning**: runs after hourly retention pruning to keep query planner statistics fresh. +- **Batch label lookup in interactive search**: replaced 15 per-epoch `get_labels_near` calls (each opening a new DB connection) with a single `get_labels_near_batch` call per text label, then in-memory filtering. Reduces search DB round-trips from ~28 to ~16. + +### Bugfixes + +- **Daemon-side event storage**: `git_commit`/`push`/`pull`/`checkout`/`stage`/`unstage`/`stash` events were received but never written to `build_events` (used only for EEG labels). Now persisted, with AI-assisted commits also recorded as `ai_events` (type `"ai_commit"`) and labelled `"git commit (AI)"`. +- **Completion-accepted events were dropped**: previously ignored by the daemon. Now stored as `ai_events` (type `"suggestion_accepted"`) and as edit chunks so they show up in code metrics and AI usage analytics. + +## Impact on analysis + +Brain analysis endpoints can now: +- Count human vs AI commits (`/brain/developer-insights`) +- Track AI suggestion acceptance rates (`/brain/ai-usage`) +- Include git activity in the activity timeline +- Weight human-authored code differently from AI output in focus/productivity scores + +## Files + +- `crates/skill-daemon/src/routes/settings_hooks_activity.rs` — Event handler updates + +- **Schema migration**: ALTER TABLE for existing databases missing columns added across releases. Runs before DDL to handle all legacy schemas. +- **Retention pruning**: meetings, clipboard, and secondary_windows pruned alongside file interactions during hourly maintenance. + +- **Dismiss stale Dependabot alerts**: crossbeam-deque, crossbeam-utils, crossbeam-queue, memoffset, and glib alerts dismissed (already at patched versions or fixed in fork). + +- **CORS allow-methods now includes `PATCH`**: `crates/skill-daemon/src/main.rs` was advertising only `GET, POST, PUT, DELETE, OPTIONS`, so the browser preflight blocked `PATCH /v1/validation/config` from the Tauri webview. Added `Method::PATCH` to the list. + +- **Equal padding on screenshot edges**: viewport width was 360px while the body was 320px wide, leaving an asymmetric 40px right gutter. Matched viewport to body width so left and right gutters are now identical. + +- **Deadlock in daily-report endpoint**: `daily_brain_report` held the database mutex while calling `productivity_score`, which re-acquires the same mutex. Scoped the lock to drop before the nested call. +- **Deadlock in struggle-predict endpoint**: same pattern in `predict_struggle` — held mutex while calling `get_recent_files`. +- **`overall_focus` returned `-0.0`**: always wrapped in `Some` even with no EEG data. Now returns `null` when no focus samples exist. +- **`best_period` arbitrary without EEG**: picked the first SQL result when all `avg_focus` were `None`. Now returns empty string when no EEG data to compare. +- **`weekly_avg_deep_mins` returned `-0.0`**: divided by hardcoded 7 regardless of actual days. Now divides by actual day count, returns `0.0` when empty. +- **Notification text ugly without EEG**: daily report notification showed "Best: . Score: 25. Focus: 0." — now conditionally includes only available fields. +- **Brain endpoints return HTTP 200 on errors**: all 18 brain handlers silently returned `null` with 200 OK when the activity store was offline or a query failed. Refactored to use `run_query` helper that returns 503 (db_unavailable) or 500 (task_error) with structured `ApiError` JSON. + +- **Worker thread panic recovery with auto-restart**: all 4 activity worker threads (poller, input monitor, file watcher, clipboard monitor) now wrapped in `catch_unwind` via `spawn_resilient`. On panic, the worker logs the error and restarts after 5 s with freshly cloned `AppState` and `Arc`. +- **osascript timeout**: all `osascript` calls (active window poll, secondary windows, clipboard monitor) now use a 3-second timeout via `run_osascript` helper. Previously, a hung app could block the poller thread indefinitely, silently killing all activity tracking. +- **Daily report catch-up on late startup**: if the daemon starts after 18:00 local, the daily brain report fires on the first tick instead of waiting up to 1 hour for the hourly check. +- **WAL checkpoint on shutdown**: daemon now runs `PRAGMA optimize` on the activity database during graceful shutdown, ensuring the WAL is checkpointed and the database is clean for next start. +- **Missing WAL pragmas on EEG embedding store**: `day_store.rs` opened connections without `init_wal_pragmas`, missing both WAL mode and `busy_timeout`. +- **Retention pruning for new tables**: added `prune_terminal_commands`, `prune_ai_events`, `prune_zone_switches`, `prune_layout_snapshots` — wired into the hourly maintenance cycle so these tables don't grow unbounded. +- **TOCTOU race in calibration settings**: concurrent profile CRUD could lose writes. Added `modify_settings_blocking()` helper that runs under a global mutex on tokio's blocking thread pool — serializes read-modify-write without blocking the async runtime. + +- **Duration-averaged EEG**: `FileSnapshot` now samples EEG focus/mood every 5 s (at each edit-chunk tick) and writes the average to `file_interactions` at finalize, replacing the single snapshot captured at file-switch time. Falls back to the initial snapshot if no samples were collected (`COALESCE`). +- **EEG mood/focus sample count mismatch**: both `FileSnapshot` and `build_focus_sessions` used a single counter for focus and mood samples, inflating mood averages when they arrived independently. Split into separate `focus_count` and `mood_count`. +- **EEG ghost data after disconnect**: `latest_bands` was not cleared when an EEG device disconnected, causing stale focus/mood values to persist in reports and the activity pipeline. Now cleared on all 3 disconnect paths (idle timeout, stream end, explicit disconnect). + +- **InteractiveGraph3D event listener leaks**: `pointerdown` and `dblclick` listeners on the WebGL canvas were never removed in `onDestroy`. Extracted into named refs with cleanup. +- **UmapViewer3D event listener leaks**: 4 canvas event listeners (pointermove, pointerleave, pointerdown, pointerup) added as anonymous functions were never removed on unmount. Extracted into named module-level refs with removal in `onDestroy`. +- **ActivityTab brain polling leak**: `startBrainPolling()` was called on mount but `stopBrainPolling()` was never called on destroy, leaving a 30-second interval running after leaving the tab. +- **Brain polling stopped by peer component**: `stopBrainPolling()` unconditionally killed the interval even when other components still needed it. Added reference counting so polling only stops when the last consumer unmounts. +- **Memory leak in terminal_command_end labeling**: `Box::leak` was used to format non-zero exit codes in EEG auto-labeling, permanently leaking memory for every failed command. Replaced with owned `String`. +- **Screenshot encode failures untracked**: `encode_webp` failures (disk full, I/O error) silently continued without incrementing the `capture_errors` counter. Now counted so metrics reflect actual failures. +- **401 auto-retry**: daemon HTTP client now retries once with a fresh token on 401 (stale token after daemon restart) instead of failing immediately. + +- **Path traversal in `delete_session`**: `csv_path` parameter was passed unsanitized to `fs::remove_file`. Now validates the path is within `skill_dir` via canonicalize + starts_with. +- **Timing-vulnerable token comparison**: default bearer token checks in auth middleware used `==` (variable-time). Switched to `constant_time_eq` for both in-memory and on-disk token paths. +- **Unbounded CSV memory in EXG loader**: loading a CSV with millions of rows would allocate unlimited RAM. Added a 4M row cap (~4.3 hours at 256 Hz). + +- **Timezone-wrong period classification**: `daily_brain_report`, `hourly_edit_heatmap`, and `optimal_hours` used `seen_at % 86400` (UTC hour) instead of local hour. Daily report now uses `seen_at - day_start`; heatmap and optimal-hours accept a timezone offset computed server-side. +- **UTC midnight in all clients**: search page, CLI (`cmdActivity`, `cmdBrain`), VS Code extension, and Swift widget all computed `day_start` as UTC midnight instead of local midnight. Fixed to use `Date.setHours(0,0,0,0)` (JS/TS) and `Calendar.startOfDay` (Swift). +- **Widget empty daily-report body**: `DaemonClient.fetchDailyReport()` sent an empty POST body; the endpoint requires `dayStart`. Now sends local midnight. +- **Widget UTC sleep/calendar endpoints**: `fetchSleep()` and `fetchCalendarEvents()` used `% 86400` UTC approximations. Now use `Calendar.current` for correct local time boundaries. + +### Refactor + +- **Shared `DaemonClient` for VS Code extension**: extracted the repeated `fetch` + auth-token + port-discovery + 3 s timeout pattern into one class. All features now use `client.post(path, body)`. Returns `null` on failure (never throws), with `setToken()` for refresh on reconnect. + +## Before + +Every component (brain.ts, sidebar.ts, extension.ts) independently constructed fetch calls: +```typescript +const port = await discoverDaemonPort(config); +const base = `http://${config.daemonHost}:${port}/v1`; +const headers = { "Content-Type": "application/json" }; +if (token) headers["Authorization"] = `Bearer ${token}`; +const resp = await fetch(`${base}${path}`, { method: "POST", headers, body, signal: AbortSignal.timeout(3000) }); +``` + +## After + +```typescript +const client = new DaemonClient(config, token); +const result = await client.post("/brain/flow-state", { windowSecs: 300 }); +``` + +## Benefits + +- Single place to update auth, timeout, port discovery +- All 8 new features use the shared client +- `setToken()` method for token refresh on reconnect +- Returns `null` on any failure (never throws) — all features handle gracefully + +## Files + +- `src/daemon-client.ts` — DaemonClient class (new) + +- **Sidebar disclaimer test now accepts either localiser**: existing `vscode-sidebar-disclaimer.test.ts` updated so the `vscode.l10n.t("sidebar.disclaimer")` ↔ `tr("sidebar.disclaimer")` migration doesn't break the assertion. Both look up the same key. + +- **Conversations table + FTS5**: full-text search on all AI conversation messages (user/assistant/tool). Three search modes: FTS, fuzzy, structured. +- **EEG timeseries table**: periodic JSON snapshots of all brain metrics. Extensible — new metrics without schema changes. Join-at-query-time correlation with any event. +- **Embeddings store**: generic, multi-model. User prompts embedded via fastembed (local, no API credits). Can re-embed with different models. +- **Code context HNSW index**: separate from label index for code-specific semantic search. +- **Binary extraction**: terminal commands store raw binary name + args separately. Lazy categorization — 284 CLI tools recognized, rerun anytime. +- **EEG attachment**: live focus/mood from `latest_bands` injected at event storage time. +- **EEG timeseries worker**: writes full band powers to `eeg_timeseries` every 5s during recording. + +### Build + +- **CI for the VS Code extension** (`extensions/vscode/.github/workflows/`): two workflows live in the submodule repo (`vscode-neuroskill`). + - **`ci.yml`** — runs on every PR and `main` push. `npm ci` → `tsc` build → `vsce package` → uploads the `.vsix` as a 14-day artifact. No secrets, no publish. + - **`release.yml`** — fires two ways. *Tag push* (`git push --tags` after `npm version patch`) verifies the tag matches `package.json` and publishes. *Manual dispatch* with a `patch | minor | major | x.y.z` input bumps `package.json`, commits, tags, pushes, then publishes. Both paths run `npx @vscode/vsce publish` (uses `VSCE_PAT`) and `npx ovsx publish` (uses `OVSX_PAT`), then create a GitHub release with the `.vsix` attached and the latest `## ` block from `CHANGELOG.md` as release notes. +- **Skip-on-missing-secret**: if either `VSCE_PAT` or `OVSX_PAT` is unset, the corresponding publish step emits a `::warning::` and is skipped — letting you wire up Open VSX later without breaking the workflow. +- **Releasing section in the extension README**: documents the secret setup (Azure DevOps PAT, Open VSX token), the two release flows, and the one-time namespace claims (Marketplace publisher registration + `ovsx create-namespace`). + +- **macOS: link libusb statically into `skill-daemon`**: vendored libusb via `rusb`'s `vendored` feature in `crates/skill-devices`. The `antneuro` USB driver pulls in `rusb` → `libusb1-sys`, which by default uses `pkg-config` to find a system libusb-1.0.dylib. On the macOS-26 release runner that resolved to Homebrew's `/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib`, so end users without Homebrew hit a launch-time `dyld: Library not loaded` error. Cargo feature unification now flips `libusb1-sys/vendored`, which compiles libusb from source and links `libusb-vendored.a` into the binary — verified by 105 `_libusb_*` text symbols present in the release binary and zero libusb references in `otool -L`. + +### CLI + +- **`neuroskill activity` expanded**: 19 subcommands — bands, window, input, windows, files, top-files, top-projects, languages, sessions, meetings, clipboard, builds, heatmap, switches, summary, score, digest, stale, timeline. +- **`neuroskill brain` added**: 14 subcommands — flow, load, load-files, recovery, optimal, fatigue, struggle, report, breaks, streak, task, stuck, interruptions, correlation. +- **`neuroskill vscode` added**: auto-build and install VS Code/VSCodium/Cursor extension from source on macOS, Linux, and Windows. + +### UI + +- **EEG focus timeline heatmap**: collapsible "Focus Timeline" sidebar section renders a ~280×36 px SVG sparkline of today's EEG focus (`/brain/eeg-range`, max 120 points), color-graded green/yellow/red, with hour labels and file-name annotations at peaks/valleys (merged from `/activity/timeline`). Toggle via `neuroskill.eegHeatmap`. + +## What you see + +In the NeuroSkill sidebar, a collapsible "Focus Timeline" section shows: + +- A ~280px wide, ~36px tall SVG sparkline +- Color gradient: green (>70 focus), yellow (40-70), red (<40) +- Hour labels along the bottom (0:00, 3:00, 6:00, ...) +- File names annotated at focus peaks and valleys + +## Data sources + +| Data | API Endpoint | +|------|-------------| +| EEG time-series | `/brain/eeg-range` (today, max 120 points) | +| File context | `/activity/timeline` (today, last 200 events) | + +The heatmap merges EEG data points with the closest timeline events to show which files correspond to focus peaks and dips. + +## Settings + +`neuroskill.eegHeatmap` (default: `true`) — Show/hide the heatmap in the sidebar. + +## Files + +- `src/sidebar.ts` — `_fetchHeatmap()` and `_renderHeatmap()` methods + +- **Brain status bar**: persistent bottom bar in Tauri app showing flow state, fatigue, streak, edit velocity, and focus score. +- **Brain dashboard card**: flow/fatigue/streak + focus progress bar on the EEG dashboard alongside existing BrainStateScores. +- **Brain indicators in ActivityTab**: flow state card (green), fatigue alert card (amber), and streak card (violet) at top. + +- **Tauri Brain Insights section** in Activity tab: test failure rates by focus level, focus by language bars, AI impact delta, tool focus grid, hourly productivity chart. +- **Tauri EEG focus inline**: terminal commands and conversation messages show EEG focus score (green/yellow/red) at the moment they occurred. +- **VS Code Brain Insights card**: test failure by focus chips, focus by language bars, tool impact chips. Only shown when EEG data is available. +- **VS Code Struggle card**: hidden when no EEG recording (focus is EEG-only, not activity-based). +- **VS Code Flow gauge label**: shows "focus" with EEG, "activity" without — no false claim of brain measurement. + +- **Tauri AI Conversations section** in Activity tab: timestamped message thread with role icons (user/assistant/tool), app name, EEG focus score, scrollable. +- **VS Code Conversations card**: timestamped messages with role icons, app badge, collapsible. +- **VS Code AI Usage card**: Copilot/Codeium acceptance rate bar, suggestion count, source breakdown chips. + +- **Search mode dropdown**: replaced 4-tab bar with a styled dropdown supporting 7 search modes — Interactive, EEG, Text, Images, Code, Meetings, Brain. +- **Diamond EEG nodes**: EEG epochs rendered as faceted octahedrons (diamonds) in the 3D search graph. Meeting nodes rendered as tetrahedrons (pyramids). Visual distinction by data type. +- **New search modes**: Code (file activity + brain state), Meetings (meeting events + recovery), Brain (flow/fatigue/struggle insights). +- **Terminal commands in Cmd-K**: added 4 terminal commands to the command palette — Recent Terminal Commands, Terminal Focus Impact, Context Switch Cost, Dev Loops. Translated labels in all 9 locales. + +- **Configurable alerts**: `neuroskill.alerts.focusLow` (warn when EEG focus drops below threshold), `neuroskill.alerts.continuousWorkMins` (warn after N minutes continuous work). +- **Daily digest notification**: at configurable time (default 17:30), shows productivity score + stats. "Full Report" button opens detail panel. +- **Full report panel**: `Cmd+Shift+R` opens full-width webview with same data as sidebar. +- **Keyboard shortcuts**: `Cmd+Shift+N` (toggle sidebar), `Cmd+Shift+R` (full report), `Cmd+Shift+Alt+N` (reconnect). +- **Open NeuroSkill button**: launches native app (cross-platform). +- **Event caching**: offline-resilient `pendingEvents` array (10K cap) persisted to disk. Auto-flushes on reconnect. + +- **Spacing pass on the validation tab and modals**: every card uses `flex flex-col gap-5 p-6` instead of the old `space-y-4` (which provided no padding override). Conditional sub-settings under a channel toggle are now indented under a left border (`ml-1 border-l-2 border-border pl-5`) so the parent/child relationship reads visually. Modals padded to `p-8` with `gap-3` button rows. TLX sliders gained `mt-1` lift off the description and `uppercase tracking-wide` low/high legend labels. + +- **Design tokens** (`tokens.css`): 30+ CSS custom properties — palette, surfaces, backgrounds, borders, semantic, typography. Zero inline rgba() in sidebar. +- **7 reusable Svelte components**: Card, MetricRow, Chevron, ProgressBar, Gauge, Badge, Callout. +- **Timestamp compliance**: 25 tests verifying UTC in data layer, local conversion only in UI. + +- **Theme-aware sidebar screenshots in the README**: every screenshot now ships in two variants (`*-dark.png`, `*-light.png`) and is embedded via a `` element with `prefers-color-scheme` sources. GitHub serves the variant matching the reader's OS theme; the marketplace gets the dark fallback. Six states captured: in-flow, stuck, fatigued, low-focus / off-peak, daemon-disconnected, status-bar strip. +- **Sidebar mock library + Playwright generator**: HTML stubs in `extensions/vscode/media/preview/` render the sidebar webview offline (no daemon required). A shared `_styles.css` drives both themes via a `:root.light` override; `npm run screenshots` (Playwright) toggles `` between captures and writes 12 PNGs to `media/screenshots/`. +- **Marketplace README pipeline**: `scripts/build-marketplace-readme.mjs` rewrites every relative `media/...` path (including ``, which `vsce` doesn't touch) to absolute GitHub raw URLs at package time. The source `README.md` keeps relative paths for GitHub + offline viewing; the generated `.marketplace.readme.md` is what `vsce package --readme-path` ships. + +- **Sidebar webview now renders correctly in light themes** (`extensions/vscode/src/sidebar.ts`). Three theme-fragile spots fixed: + - `.metric-card` background switched from `--vscode-sideBar-background` (which is the same colour as the surrounding sidebar — cards collapsed into a faint border in light mode) to `--vscode-editorWidget-background`, with `--vscode-input-background` and a translucent grey as cascading fallbacks. + - `.ai-bar` track switched from `--vscode-panel-border` to `--vscode-progressBar-background` so the empty bar is visible against a white background. + - Row-divider opacity bumped from 0.08 to 0.18; added `:last-child { border-bottom: none }` on commit and AI-metric rows so the last row doesn't double up against the disclaimer footer. +- **State colours stay hard-coded** — red ring for stuck, green for flow, amber for warning. They're semantic, not chrome, and they read fine on both themes. + +### Server + +- **`POST /v1/activity/shell-command`**: receives commands from shell hooks with command text, cwd, shell type, exit code. +- **`POST /v1/brain/terminal-commands`**: query recent commands with exit codes, durations, categories, EEG correlation. +- **`POST /v1/brain/terminal-impact`**: avg EEG focus delta by command category. +- **`POST /v1/brain/binary-stats`**: usage frequency per binary for discovering uncategorized tools. +- **`POST /v1/brain/recategorize-commands`**: rerun categorization rules on all historical commands. +- **Terminal EEG auto-labels**: `"running: cargo test"` on start, `"cargo test passed"` / `"cargo test failed (exit 1)"` on end. +- **Zone switch events**: editor/terminal/panel transitions stored with EEG focus snapshot. `POST /v1/brain/context-cost` returns focus at each transition type. +- **Layout snapshots**: periodic (60s) tab/group/terminal counts from VS Code. + +- **`/v1/validation/*` HTTP endpoints** (axum): `GET /config`, `PATCH /config` (recursive JSON merge for partial updates), `POST /snooze`, `POST /disable-today`, `GET /should-prompt` (returns the daemon's prompt decision; logs the fire so the rate-limiter sees it), `POST /kss`, `POST /tlx`, `POST /pvt`, `POST /close-prompt`, `GET /results`, `GET /fatigue-index` (live `(α+θ)/β` from the latest band snapshot). +- **`AppState.validation_runtime`** (`crates/skill-daemon-state/src/state.rs`): new `Arc>` field holding ephemeral snooze/disable-today state. Resets on daemon restart by design — a crash loop shouldn't keep prompts suppressed forever. + +- **`terminal_commands` table**: shell command text, cwd, exit code, duration, auto-categorized (50+ patterns: build/test/run/git/docker/deploy/install/navigate/debug/network/other), EEG focus at start and end for delta calculation. +- **`dev_loops` table**: edit→build/test cycle tracking with iteration count, pass/fail rate, avg cycle time, focus trend (rising/falling/stable). +- **`zone_switches` table**: editor/terminal/panel transitions with EEG focus snapshot at moment of switch. +- **`layout_snapshots` table**: periodic tab/group/terminal counts from VS Code. +- **Event handler**: new match arms for `terminal_command_start`, `terminal_command_end`, `zone_switch`, `layout_snapshot` in `activity_vscode_events_impl()`. +- **Command categorizer**: `categorize_command()` with 50+ patterns across build, test, run, git, docker, deploy, install, navigate, debug, network. +- **`POST /v1/brain/terminal-impact`**: avg EEG focus delta by command category — shows how builds, tests, git, docker affect brain state. +- **`POST /v1/brain/context-cost`**: focus level at each zone transition type with switch counts. +- **`POST /v1/brain/dev-loops`**: edit-build-test cycle detection with iterations, pass rate, cycle time, focus trend. +- **`POST /v1/brain/terminal-commands`**: recent commands with exit codes, durations, categories, EEG correlation. +- **Enhanced `predict_struggle()`**: terminal failures (+8/fail, max +40) and re-running same failing command 3+ times (+15/rerun, max +30) boost struggle score. New suggestions: "You're re-running the same failing command", "Multiple failures — read the error messages". +- **Enhanced `detect_task_type()`**: terminal command categories override heuristics with higher confidence — docker/kubectl→infrastructure (0.8), deploy→deploying (0.85), git dominating→git_management (0.7), test→testing (0.85), debug→debugging (0.9). +- **Terminal EEG auto-labels**: `"running: cargo test"` on start, `"cargo test passed"` / `"cargo test failed (exit 1)"` on end, `"switched to terminal"` on zone changes. +- **AI events table**: `ai_events` SQLite table tracking suggestion shown/accepted/rejected and chat sessions with source attribution. +- **OS-wide shell hooks**: `scripts/shell-hooks/` with preexec/precmd hooks for zsh, bash, fish, PowerShell. Sends every command to daemon via background curl. No delay to prompt. +- **Shell hook daemon endpoints**: `GET /v1/activity/shell-hook?shell=zsh` returns hook script, `POST /v1/activity/install-shell-hook` writes to `~/.skill/shell-hooks/` and appends to rc file, `POST /v1/activity/uninstall-shell-hook` removes cleanly, `POST /v1/activity/shell-hook-status` health check. +- **`neuroskill terminal` CLI**: `status` (hook health per shell), `install [shell]`, `uninstall [shell]`, `commands` (recent tracked), `impact` (focus delta by category), `loops` (dev loop detection). +- **`neuroskill brain` new subactions**: `terminal-impact`, `context-cost`, `dev-loops`. +- **`neuroskill activity` new subaction**: `terminal-commands`. +- **Tauri Terminal settings tab**: per-shell install/uninstall/repair buttons, health indicators (green/yellow/red), recent commands preview, "How it works" documentation. +- **`neuroskill vscode` CLI**: auto-install extension to VS Code, VSCodium, or Cursor on macOS, Linux, and Windows. +- **Meeting nodes in search graph**: meetings appear as amber nodes in the interactive 3D search graph linked by `meeting_prox` edges. + +### i18n + +- Activity dashboard, clipboard, file history, session replay, weekly report, brain status, timeline, and code-brain correlation translations for all 9 locales. + +- Added `settings.inferenceDeviceAuto`, `settings.inferenceDeviceAutoDesc`, `settings.inferenceDeviceMlx`, `settings.inferenceDeviceMlxDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). + +- Added `umapSettings.backend`, `umapSettings.backendDesc`, `umapSettings.precision`, `umapSettings.precisionDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). + +- Added `dnd.grayscale` and `dnd.grayscaleDesc` translations to all 9 locales. + +- Search mode labels (Code, Meetings, Brain) translated in all 9 locales. +- Terminal command palette entries translated in all 9 locales. + +- **New `validation` namespace** in all 9 languages (`src/lib/i18n/{de,en,es,fr,he,ja,ko,uk,zh}/validation.ts`): ~95 keys in English (full coverage), ~55 in each other language for the user-visible strings (long-tail descriptions fall back via the runtime `t()` chain). `index.ts` barrel exports updated for every language. + +- **22 new `validation.*` keys per bundle** in all 9 languages (`bundle.l10n.{de,en,es,fr,he,ja,ko,uk,zh-cn}.json`): KSS prompt + 9 score labels with translated Karolinska wording, TLX/PVT prompts, all four escape-hatch labels, the disabled-permanently acknowledgement. +- **Two new `cmd.*` keys** for the validation commands in `package.nls.json`. + +- Added `settings.inferenceDeviceAuto`, `settings.inferenceDeviceAutoDesc`, `settings.inferenceDeviceMlx`, `settings.inferenceDeviceMlxDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). + +### Docs + +- VS Code extension design plan at `docs/vscode-extension.md`. +- `neuroskill-activity` skill documentation with 18 activity + 24 brain subcommands, terminal integration, shell hook reference, command categorization table. +- Updated `neuroskill-dnd` skill with grayscale mode. +- Updated `neuroskill/README.md` with terminal, brain awareness, and VS Code extension features. +- Updated `skills/SKILL.md` index with terminal tracking skill reference. + +- **VS Code extension README rewritten for skeptical developers**: scientific framing throughout — every derived metric (focus score, flow detector, deep-work minute, struggle predictor, optimal hours, fatigue) now ships with its formula, hyperparameters, and a "what can go wrong" line. Added an explicit *What this is, and isn't* section, a *Validation status* table tracking each metric's evidence level (production / pilot / heuristic / descriptive-only), and a *Validation roadmap* describing the planned KSS / NASA-TLX / PVT / EEG-fatigue calibration channels. Replaced the marketing pain-points table with a per-API signal taxonomy and an architecture paragraph. +- **References section with verified citations**: nine entries with DOIs cross-checked via CrossRef and `doi.org` redirects — Csikszentmihalyi 1990 (flow), Klimesch 1999 (α/θ oscillations), Lubar 1991 (θ/β attention ratio), Barry et al. 2005 (caffeine confound), Newport 2016 (deep work), Roenneberg et al. 2003 (chronotypes), Hart & Staveland 1988 + Hart 2006 (NASA-TLX), Jap et al. 2009 (EEG fatigue index). Each reference linked back to the metric it grounds. +- **Repository metadata pointed at the right repo**: discovered `extensions/vscode/` is a git submodule of `vscode-neuroskill`, not a subdirectory of the monorepo. Reverted `repository.url`, `bugs.url`, `homepage`, `qna` to the submodule's actual repo so the marketplace links resolve. + +### Dependencies + +- **burn-mlx**: added `burn-mlx` from git (`eidola-ai/burn-mlx`, branch `burn-0-20`) as optional dependency in `skill-router` and `skill-daemon`. Workspace `[patch.crates-io]` redirects all `burn-mlx` references to the git version (crates.io 0.1.2 targets burn 0.16; project uses burn 0.20). +- **macOS deployment target**: bumped from 10.15 to 14.0 in `.cargo/config.toml` for Metal 3.0 `simdgroup_matrix` intrinsics required by MLX. Still satisfies llama.cpp's `std::filesystem` (available since 10.15). +- **half 2.4**: added to `skill-router` for GPU f16 precision backend type (`CubeBackend`). + +- **Fix rand 0.7.3 vulnerability**: vendored `phf_generator 0.8.0` with rand bumped from 0.7 to 0.8, eliminating the vulnerable transitive dependency. +- **Remove atty**: migrated `iroh_test_client` from `structopt` (clap v2) to `clap v4` derive, dropping the unmaintained `atty` crate. +- **Update llama-cpp-4 to 0.2.50**: upstream llama.cpp fixes including `common_*` symbol renames. +- **Update kittentts to 0.4.1**: TTS engine update. +- **Add grayscale 0.0.1**: macOS grayscale display control for DND mode. diff --git a/Cargo.lock b/Cargo.lock index 83aecfee..21fbd4b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -908,9 +908,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -1862,9 +1862,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -2428,9 +2428,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -3311,9 +3311,9 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "dbus" @@ -3827,14 +3827,14 @@ dependencies = [ [[package]] name = "embed-resource" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "vswhom", "winreg 0.55.0", ] @@ -4074,9 +4074,9 @@ checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" [[package]] name = "espeak-ng" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf53da2d1e3049cfff5ae289a05b456c753c15cbe0bff2b97251aef5a748da94" +checksum = "797dcf553dc7581c714b28dc0f4caabb7bbb98ce3f5b70e3c9c2d2f2f7cce5b2" dependencies = [ "bitflags 2.11.1", "espeak-ng-data-dict-ru", @@ -4217,9 +4217,9 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" dependencies = [ "bit-set 0.8.0", "regex-automata", @@ -4297,23 +4297,9 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -5884,9 +5870,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] @@ -6154,9 +6140,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -6254,9 +6240,9 @@ dependencies = [ [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" [[package]] name = "indexmap" @@ -6746,6 +6732,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -6801,9 +6817,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", @@ -6835,15 +6851,15 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.46.2" +version = "0.46.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50180452e7808015fe083eae3efcf1ec98b89b45dd8cc204f7b4a6b7b81ea675" +checksum = "cbe92a2f8b00686061eab5cdcfd6f382c27f2084456e7be90ae9f0fe4a30552a" dependencies = [ "ahash", "bytecount", "data-encoding", "email_address", - "fancy-regex 0.17.0", + "fancy-regex 0.18.0", "fraction", "getrandom 0.3.4", "idna", @@ -7274,10 +7290,11 @@ dependencies = [ [[package]] name = "llmfit-core" -version = "0.9.14" +version = "0.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e0d5d303daeb9a26f414d6e97cc123fa924314177bba06b4dd03b65a2313f0" +checksum = "f5eb09b5be6bade227582226bd0d74069abd88460756af4e93a8dfe97c38d57c" dependencies = [ + "dirs", "http 1.4.0", "serde", "serde_json", @@ -8482,7 +8499,7 @@ dependencies = [ "mac-notification-sys", "serde", "tauri-winrt-notification", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -9169,9 +9186,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onig" -version = "6.5.1" +version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ "bitflags 2.11.1", "libc", @@ -9181,9 +9198,9 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.9.1" +version = "69.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" dependencies = [ "cc", "pkg-config", @@ -9324,6 +9341,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -9909,13 +9935,13 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml 0.38.4", + "quick-xml 0.39.2", "serde", "time", ] @@ -10392,15 +10418,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.39.2" @@ -10842,9 +10859,9 @@ dependencies = [ [[package]] name = "referencing" -version = "0.46.2" +version = "0.46.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acb0c66c7b78c1da928bee668b5cc638c678642ff587faff6e6222f797be9d4c" +checksum = "e125f10bdcd507598c702daada18c47fe5bfba4d7a9545b015b5d432f7168ca3" dependencies = [ "ahash", "fluent-uri", @@ -10988,9 +11005,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -11402,9 +11419,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -11438,9 +11455,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -11448,13 +11465,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -12094,6 +12111,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -12132,7 +12159,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.129" +version = "0.0.130-rc.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -12521,6 +12548,7 @@ dependencies = [ "base64 0.22.1", "image", "iroh", + "pkcs8", "qrcodegen", "rand 0.9.4", "serde", @@ -13542,7 +13570,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "serde_repr", @@ -13696,7 +13724,7 @@ dependencies = [ "thiserror 2.0.18", "url", "windows 0.61.3", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -13721,7 +13749,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "windows-sys 0.60.2", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -13740,7 +13768,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest 0.13.2", + "reqwest 0.13.3", "rustls", "semver", "serde", @@ -13848,13 +13876,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", ] [[package]] @@ -15453,9 +15481,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -15466,9 +15494,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ "js-sys", "wasm-bindgen", @@ -15476,9 +15504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -15486,9 +15514,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -15499,9 +15527,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -15656,9 +15684,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -15989,7 +16017,7 @@ dependencies = [ "naga", "ndk-sys", "objc", - "ordered-float 4.6.0", + "ordered-float 5.0.0", "parking_lot", "portable-atomic", "portable-atomic-util", @@ -16675,9 +16703,6 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] [[package]] name = "winnow" @@ -17016,7 +17041,7 @@ dependencies = [ "widestring", "windows 0.62.2", "xcb", - "zbus 5.14.0", + "zbus 5.15.0", ] [[package]] @@ -17160,9 +17185,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" dependencies = [ "async-broadcast", "async-executor", @@ -17187,10 +17212,10 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.15", - "zbus_macros 5.14.0", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "winnow 1.0.2", + "zbus_macros 5.15.0", + "zbus_names 4.3.2", + "zvariant 5.10.1", ] [[package]] @@ -17208,17 +17233,17 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", - "zbus_names 4.3.1", - "zvariant 5.10.0", - "zvariant_utils 3.3.0", + "zbus_names 4.3.2", + "zvariant 5.10.1", + "zvariant_utils 3.3.1", ] [[package]] @@ -17234,13 +17259,13 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 0.7.15", - "zvariant 5.10.0", + "winnow 1.0.2", + "zvariant 5.10.1", ] [[package]] @@ -17505,16 +17530,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.10.0" +version = "5.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "c4db0ecb8987cf5e92653c57c098f7f0e39a03112edb796f4fe089fb7eaa14ff" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.15", - "zvariant_derive 5.10.0", - "zvariant_utils 3.3.0", + "winnow 1.0.2", + "zvariant_derive 5.10.1", + "zvariant_utils 3.3.1", ] [[package]] @@ -17532,15 +17557,15 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "5b949b639ab1b4bed763aa7481ba0e368af68d8b55532f8ed4bec86a59f2ca98" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils 3.3.0", + "zvariant_utils 3.3.1", ] [[package]] @@ -17556,13 +17581,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" dependencies = [ "proc-macro2", "quote", "serde", "syn 2.0.117", - "winnow 0.7.15", + "winnow 1.0.2", ] diff --git a/changes/releases/0.0.130-rc.1.md b/changes/releases/0.0.130-rc.1.md new file mode 100644 index 00000000..f1f4b4ff --- /dev/null +++ b/changes/releases/0.0.130-rc.1.md @@ -0,0 +1,769 @@ +## [0.0.130-rc.1] — 2026-04-29 + +### Features + +- **Human vs AI activity tracking**: every edit and commit is now classified as `source: "human"` or `source: "ai"` in real-time. `AIActivityTracker` watches VSCode commands (Copilot/Codeium completions, inline chat, AI-generated commit messages) to tag subsequent edits/commits, exposes `getAIRatioForFile()` (rolling 5-minute ratio) for CodeLens + sidebar, and forwards the `source` field to the daemon for `build_events` / `ai_events` storage. + +## How it works + +The `AIActivityTracker` monitors VSCode command execution to detect AI tool usage: + +- **Inline completions** (Copilot, Codeium) — edits within 5 seconds after an AI command are tagged `source: "ai"` +- **Inline chat** (`inlineChat.start`, `copilot.interactiveEditor.*`) — subsequent edits in the same file are tagged AI +- **Commit messages** — `github.copilot.git.generateCommitMessage` marks the next commit as AI-assisted +- **Everything else** — classified as `source: "human"` + +## What's tracked + +| Signal | Classification | +|--------|---------------| +| Manual typing | `human` | +| Copilot inline suggestion accepted | `ai` | +| Copilot inline chat edits | `ai` | +| Paste from external source | `human` | +| AI-generated commit message | `ai` | +| Manually typed commit message | `human` | + +## Per-file AI ratio + +`AIActivityTracker.getAIRatioForFile(path)` returns a rolling 5-minute ratio (0.0 = all human, 1.0 = all AI) used by: +- CodeLens annotations (shows "AI-Assisted" vs focus score) +- Sidebar (Human/AI percentage display) +- Brain status command (Human/AI split) + +## Daemon integration + +The `source` field is sent on every `edit` and `git_commit` event to the daemon. The daemon stores: +- AI commits as `"git commit (ai-assisted)"` in `build_events` +- AI commits also as `ai_events` for analytics weighting +- Completion acceptances as `ai_events` with type `"suggestion_accepted"` + +## Files + +- `src/ai-tracker.ts` — Core tracker (new) +- `src/events.ts` — Wired to classify edits and commits +- `crates/skill-daemon/src/routes/settings_hooks_activity.rs` — Daemon-side storage + +- **Focus-aware code review CodeLens**: annotations at the top of each file show the developer's focus level when the file was last edited (`⚠ Low Focus (42)`, `ℹ Focus: 65/100`, `🤖 AI-Assisted (85%)`, or none for high focus). `NeuroSkill: Show Files Needing Review` command lists low-focus human-authored files. Toggle via `neuroskill.focusCodeLens`. + +## What you see + +- `⚠ Low Focus (42) — Review Recommended` — File was edited during low focus. Click to see all files needing review. +- `ℹ Focus: 65/100` — Moderate focus, informational only. +- `🤖 AI-Assisted (85%)` — Most edits were AI-generated, focus score not applicable. +- No annotation — High focus (>70) or no data yet. + +## Commands + +**NeuroSkill: Show Files Needing Review** (`Cmd+Shift+P`) +- Shows a QuickPick list of files edited during low focus (<50) that were mostly human-authored +- Sorted by focus score (lowest first) +- Select a file to open it + +## How it works + +- `FocusCodeLensProvider` queries `/brain/cognitive-load` (grouped by file) every 30 seconds +- Combines focus data with `AIActivityTracker.getAIRatioForFile()` to distinguish human vs AI code +- Files with high AI ratio (>70%) show AI label instead of focus score — AI code doesn't reflect human cognitive state + +## Settings + +`neuroskill.focusCodeLens` (default: `true`) — Toggle CodeLens annotations on/off. + +## Files + +- `src/codelens-provider.ts` — CodeLens provider (new) + +- **Smart Interruption Shield**: when `/brain/flow-state` reports `in_flow: true`, VS Code's Do Not Disturb mode auto-enables and the status bar shows `$(shield) In Flow 12m`. `NeuroSkill: Toggle Flow Shield` cycles Auto / Forced-on / Forced-off. Toggle via `neuroskill.flowShield`. + +## How it works + +- When `/brain/flow-state` reports `in_flow: true`, the Flow Shield activates +- Enables VSCode's Do Not Disturb mode (VSCode 1.90+) +- Shows `$(shield) In Flow 12m` in the status bar with elapsed time +- When flow state ends, DND is automatically disabled + +## Manual override + +**NeuroSkill: Toggle Flow Shield** (`Cmd+Shift+P`) + +Cycles through three modes: +1. **Auto** (default) — activates/deactivates based on EEG flow detection +2. **Forced on** — always active regardless of flow state +3. **Forced off** — never active + +## Settings + +`neuroskill.flowShield` (default: `true`) — Enable/disable the flow shield feature. + +## Files + +- `src/flow-shield.ts` — Flow shield implementation (new) +- `src/brain.ts` — Calls `flowShield.update()` every 30s + +- **Adaptive Break Coach**: personalized break timing based on the developer's actual EEG focus cycle (via `/brain/break-timing`), not generic Pomodoro. Status bar countdown `$(clock) Break in 8m`, max one notification per cycle, auto-resets when fatigue indicates idleness. `NeuroSkill: Take a Break` resets the timer manually. Toggle via `neuroskill.breakCoach`. + +## How it works + +- Queries `/brain/break-timing` to learn the developer's natural focus cycle length +- Shows a countdown in the status bar: `$(clock) Break in 8m` +- When the predicted focus drop is imminent (<5 min), the countdown turns visible +- When the cycle ends, shows `$(clock) Break time` and optionally notifies + +## Notifications + +- Max one notification per focus cycle +- Message: "You've been focused for 47m. Your natural cycle is 42m — take a break?" +- Buttons: "Take Break" (resets timer) or "Dismiss" + +## Timer sync + +The break coach automatically syncs with the daemon's fatigue data. If `continuous_work_mins` drops below 5 (indicating the user was idle), the session timer resets — no false break suggestions after returning from lunch. + +## Commands + +**NeuroSkill: Take a Break** (`Cmd+Shift+P`) — Manually acknowledge a break and reset the timer. + +## Settings + +`neuroskill.breakCoach` (default: `true`) — Enable/disable break coaching. + +## Files + +- `src/break-coach.ts` — Break coach implementation (new) +- `src/brain.ts` — Calls `breakCoach.refresh()` and `resetSessionIfIdle()` every 30s + +- **Struggle → AI assist bridge**: when `/brain/struggle-predict` flags a file (EEG focus + undo rate + velocity drop + time-on-file), shows an actionable notification with **Open Copilot Chat**, **Open Terminal**, and **Step Back** buttons. Debounced to one suggestion per file per 10 minutes. Toggle via `neuroskill.struggleBridge`. + +## How it works + +- Monitors `/brain/struggle-predict` (EEG focus + undo rate + velocity drop + time-on-file) +- When `struggling: true`, shows an actionable notification: + > "Stuck on auth.ts? (score: 78) Consider breaking the problem into smaller pieces." + +## Action buttons + +| Button | Action | +|--------|--------| +| **Open Copilot Chat** | Opens GitHub Copilot interactive chat (or generic chat panel) | +| **Open Terminal** | Toggles terminal for CLI debugging | +| **Step Back** | Dismiss and take a mental break | + +## Debouncing + +- Max one suggestion per file per 10 minutes +- Prevents notification fatigue while still catching genuine struggles + +## Settings + +`neuroskill.struggleBridge` (default: `true`) — Enable/disable struggle detection and AI suggestions. + +## Files + +- `src/struggle-bridge.ts` — Struggle bridge implementation (new) +- `src/brain.ts` — Calls `struggleBridge.check()` every 30s (replaces the old generic struggle notification) + +- **Personal Flow Triggers dashboard**: collapsible "Your Flow Recipe" sidebar section mining 7-day EEG + activity history — best language (`/brain/code-eeg`), peak hours (`/brain/optimal-hours`), natural cycle length (`/brain/break-timing`), top flow killer (`/brain/context-cost`). Toggle via `neuroskill.flowTriggers`. + +## What you see + +In the NeuroSkill sidebar panel, a collapsible "Your Flow Recipe" section shows: + +- **Best language** — "Focus best on Rust (82)" — from `/brain/code-eeg` +- **Peak hours** — "Peak hours: 9:00, 10:00, 14:00" — from `/brain/optimal-hours` +- **Natural cycle** — "Natural cycle: 42m" — from `/brain/break-timing` +- **Flow killer** — "Flow killer: Slack (focus 38 at switch)" — from `/brain/context-cost` + +## Data sources + +| Insight | API Endpoint | Time Range | +|---------|-------------|------------| +| Best languages | `/brain/code-eeg` | Last 7 days | +| Peak hours | `/brain/optimal-hours` | Last 7 days | +| Natural cycle | `/brain/break-timing` | Last 7 days | +| Flow killers | `/brain/context-cost` | Last 7 days | + +## Settings + +`neuroskill.flowTriggers` (default: `true`) — Show/hide the flow triggers section in the sidebar. + +## Files + +- `src/sidebar.ts` — `_fetchFlowTriggers()` and `_renderFlowTriggers()` methods + +- **Focus-scored git commits in sidebar**: collapsible "Recent Commits" section shows the last 8 commits with author marker (👤 human / 🤖 AI) and a color-coded focus badge captured at commit time via `/brain/flow-state`. AI-assisted commits show "AI" instead of a focus score. Toggle via `neuroskill.focusCommits`. + +## What you see + +In the NeuroSkill sidebar, a collapsible "Recent Commits" section shows the last 8 commits: + +``` +👤 82 fix: resolve auth race condition +👤 45 chore: update dependencies +🤖 AI refactor: extract helper functions +👤 71 feat: add user preferences +``` + +- **👤** = human-authored commit +- **🤖** = AI-assisted commit (message generated by Copilot) +- **Focus badge** = color-coded: green (>70), yellow (40-70), red (<40) +- AI commits show "AI" instead of a focus score — AI output doesn't measure human cognition + +## How it works + +- When the extension detects a git commit (SCM input box clears), it: + 1. Snapshots current EEG focus via `/brain/flow-state` + 2. Checks `AIActivityTracker.isCommitAIAssisted()` + 3. Records the commit with focus score + source label +- Commits stored in-memory (last 15), refreshed on sidebar render +- The daemon also stores commits with human/AI distinction in `build_events` + +## Settings + +`neuroskill.focusCommits` (default: `true`) — Show/hide the commits section in the sidebar. + +## Files + +- `src/sidebar.ts` — `recordCommit()`, `_renderCommits()` +- `src/extension.ts` — Wires commit detection to sidebar recording +- `src/events.ts` — `onCommit` callback with human/AI source + +- **Optimal Task Router**: monitors flow score every 30 s and, when it changes by >20 points, suggests an appropriate task type (refactoring/new features at high focus, code review at moderate, docs/routine at low). Debounced to one suggestion per 15 minutes. Toggle via `neuroskill.taskRouter`. + +## How it works + +- Monitors the flow state score every 30 seconds +- When focus changes by >20 points from the last reading, suggests an appropriate task type: + +| Focus Level | Suggestion | +|------------|------------| +| >75 | "Focus is high (85) — great time for complex work like refactoring or new features." | +| 45-75 | "Focus moderate (58) — good for code review, testing, or incremental tasks." | +| <45 | "Focus low (32) — consider documentation, routine tasks, or a break." | + +## Debouncing + +- Maximum one suggestion every 15 minutes +- No suggestion on the first reading (establishes baseline) +- No suggestion if focus stays within 20 points of the last reading + +## Settings + +`neuroskill.taskRouter` (default: `true`) — Enable/disable task routing suggestions. + +## Files + +- `src/task-router.ts` — Task router implementation (new) +- `src/brain.ts` — Calls `taskRouter.check()` every 30s + +- **Activity dashboard tab**: new Settings tab with daily summary, productivity score (0-100 composite), hourly heatmap, top files/projects, language breakdown, focus sessions, meetings, and stale file alerts. +- **Focus session replay**: click any session to expand a detailed view with file timeline, edit counts, EEG focus overlay bar, and meeting interruptions. +- **Weekly report with CSV export**: 7-day activity digest with daily breakdown chart and one-click CSV download. +- **Productivity scoring**: composite score from edit velocity + deep work time + context stability + EEG focus. +- **Stale file detection**: files edited but untouched for 7+ days. +- **Code-Brain correlation**: focus ranked by language, project, and individual files — shows which code drains or energizes you. +- **Activity timeline**: unified chronological feed of all events (files, builds, meetings, AI, clipboard) with EEG focus indicators. + +- **Brain awareness API**: 14 endpoints under `/v1/brain/*` — flow state, cognitive load, meeting recovery, optimal hours, fatigue check, undo struggle, daily brain report, break timing, deep work streak, task type detection, struggle prediction, interruption recovery, code-EEG correlation, and unified timeline. +- **Flow state detector**: real-time check if user is in deep focus (high focus + low switches + sustained editing). +- **Task type detection**: auto-classify coding/debugging/reviewing/refactoring/testing from activity patterns. +- **Struggle prediction**: fuse undo rate + velocity drop + focus decline to predict when user is stuck. Includes actionable suggestion. +- **Interruption recovery measurement**: measure actual focus recovery time per interruption source (Slack avg 12min, Zoom avg 23min, etc.). +- **Fatigue monitor**: 15-minute background check broadcasts `fatigue-alert` and sends OS notification when focus declines. +- **Daily brain report**: 6pm OS notification with morning/afternoon/evening brain summary. +- **Weekly digest notification**: Monday 9am OS notification with weekly summary. ISO week dedup prevents re-fire on daemon restart. +- **Break timing optimizer**: detect natural focus cycle length from 5-minute EEG buckets. +- **Deep work streak**: gamified consecutive days with 60+ minutes of deep work. + +- **Developer insights endpoint**: `POST /v1/brain/developer-insights` returns 7 actionable EEG-fused insights in one call. +- **Test failure by focus level**: correlates build/test exit codes with EEG focus (high/mid/low) — shows how focus level predicts test outcomes. +- **Hourly productivity**: churn volume + undo rate + avg EEG focus by hour of day — identifies peak and trough hours. +- **Context switch recovery**: EEG focus level at each editor/terminal/panel transition — measures the cognitive cost of switching. +- **AI tool impact on focus**: per-app (Claude, Pi) focus delta vs baseline — quantifies whether AI tools help or hurt flow state. +- **Focus by language**: avg EEG focus + undo rate per programming language — identifies which languages are cognitively easiest/hardest. +- **Dev loop efficiency by hour**: build/test cycle time + pass rate by time of day — shows when edit-test loops are fastest. +- **Tool focus impact**: avg EEG focus per command category (docker, git, deploy, etc.) — reveals which tools correlate with lowest focus. +- **EEG timeseries table**: periodic JSON snapshots of all brain metrics (every 5s during recording). Extensible — new metrics without schema changes. Any event correlatable by timestamp join. +- **EEG timeseries worker**: background thread writes full band powers to `eeg_timeseries` every 5s when an EEG session is active. +- **`POST /v1/brain/eeg-at`**: get EEG metrics at a specific timestamp (nearest sample). +- **`POST /v1/brain/eeg-range`**: get EEG time-series in a range for charts and correlation analysis. +- **Window focus tracking**: `window_focus` events sent when VS Code gains/loses focus. EEG not attributed to coding when VS Code is in background. +- **Live EEG attachment**: `latest_bands` focus/mood injected at event storage time for terminal commands, zone switches, and conversations. + +- **`mlx-e2e` test suite**: new suite in `scripts/test-all.sh` running UMAP and FFT e2e tests with MLX features. Auto-skips on non-macOS. Available via `npm run test:mlx-e2e`. + +- **Conversations table**: stores all AI coding assistant messages (Claude, Pi) with app, role (user/assistant/tool), text, cwd, timestamp, session ID, EEG focus/mood. +- **FTS5 full-text search**: SQLite virtual table for instant keyword search across all conversation text. `POST /v1/brain/search-conversations {"query":"JWT","mode":"fts"}`. +- **Fuzzy search**: LIKE-based substring matching for partial queries. `{"query":"rate limit","mode":"fuzzy"}`. +- **Structured search**: filter by app, role, time range. `{"mode":"structured","app":"claude","role":"user","since":...}`. +- **Semantic search**: user prompts embedded via fastembed (nomic-embed-text-v1.5, local, no API credits) and stored in HNSW label index. Searchable by meaning, not just keywords. +- **Generic embedding store**: `embeddings` table decoupled from specific data tables. Multi-model support — can re-embed with different models, store multiple vectors per item. Source tracking (source_type, source_id). +- **Code context HNSW index**: separate `code_context_index.hnsw` file for code-specific semantic search, keeping EEG label searches uncontaminated. +- **Conversation events**: VS Code extension sends `conversation_message` events to daemon for each Claude/Pi message. User prompts get embedded; assistant responses and tool calls get FTS-indexed only (saves compute). +- **Session tracking**: messages grouped by JSONL filename (session ID). Timestamps from app's own data (ISO 8601 UTC), not extension read time. +- **Embedding settings**: `neuroskill.embedding.maxInputLength` (default 1000 chars) and `neuroskill.embedding.enableConversations` (default true) in VS Code settings. + +- **Meeting detection**: automatically detect Zoom, Teams, Slack, Google Meet, FaceTime, Discord, and Webex meetings from window titles. Track start/end times in `meeting_events` table. +- **Browser tab extraction**: extract page titles from Chrome, Safari, Firefox, Edge, Brave, Arc, Opera, Vivaldi, Chromium. Stored in `browser_title` column on `active_windows`. +- **Clipboard monitoring**: opt-in macOS clipboard change tracking (metadata only — content never stored). Records source app, content type, and size. Includes Automation permission check and settings UI. +- **Multi-monitor awareness**: track windows on secondary monitors across macOS (AppleScript), Linux (wmctrl), Windows (EnumWindows). Dynamic primary screen resolution detection. +- **Undo frequency tracking**: detect undo/redo from file edit diffs via reversal heuristic. `undo_estimate` per 5-second chunk, `undo_count` per file interaction. + +- **Design tokens** (`webview-ui/src/lib/tokens.css`): 30+ CSS custom properties organized into core palette (5 colors), surfaces (4 levels), backgrounds (14 translucent tints), borders (3 colors), semantic aliases (7), and typography (3). Zero inline `rgba()` in App.svelte. +- **Reusable Svelte components** (`webview-ui/src/lib/`): + - `Card` — wrapper with title, info button, info panel, header-right slot, variant borders (warn/danger/info) + - `MetricRow` — label + value pair with variant colors (warn/accent/dim/good/bad) + - `Chevron` — collapsible section with chevron toggle, count badge, slot content + - `ProgressBar` — horizontal bar with value/max, 5 color variants, optional label + - `Gauge` — circular SVG ring with animated fill, value, label + - `Badge` — text badge with 7 variants (default/good/warn/bad/blue/live/score-circle/si) + - `Callout` — alert box with 3 variants (warn/danger/info) +- **Component index** (`webview-ui/src/lib/index.ts`): barrel export for all components. +- **Timestamp compliance**: 25 automated tests (`src/test-timestamps.ts`) verifying: + - No `toLocaleTimeString`/`toLocaleDateString` in data layer (activity-tracker, events, brain, config, vt-parser) + - `toLocaleTimeString` used in UI layer (App.svelte) for display + - `Date.now()` returns UTC milliseconds + - ISO 8601 strings parsed to UTC millis + - No hardcoded timezone offsets in data layer + - All stored timestamps are UTC; local conversion only at UI boundary + +- **Dev loop detection**: identifies edit-build-test cycles from terminal command history. Groups consecutive runs of the same build/test command into loops. +- **`dev_loops` table**: loop type, command, iteration count, pass/fail counts, avg cycle time, fastest/slowest cycle, EEG focus start/end, focus trend (rising/falling/stable). +- **`POST /v1/brain/dev-loops`**: returns detected loops for a time window with all metrics. +- **Enhanced `predict_struggle()`**: terminal failures (+8/fail, max +40) and re-running same failing command 3+ times (+15/rerun, max +30) boost struggle score. New suggestions: "You're re-running the same failing command", "Multiple failures — read the error messages." +- **Enhanced `detect_task_type()`**: terminal command categories override heuristics with higher confidence — docker/kubectl → infrastructure (0.8), deploy → deploying (0.85), git dominating → git_management (0.7), test → testing (0.85), debug → debugging (0.9). +- **Dev loops in sidebar**: command, iteration count, pass rate, avg cycle time, focus trend arrow. Failing loops get red left border. +- **Dev loops in Tauri Activity tab**: expanded view with pass rate percentage, cycle time, focus trend. +- **Loop efficiency by hour**: insight showing build/test cycle time + pass rate by time of day. + +- **Inference backend selector**: `exg_inference_device` setting expanded from `gpu | cpu` to `auto | mlx | gpu | cpu`. Default changed from `gpu` to `auto` (picks MLX on macOS, GPU elsewhere). +- **Four-button selector in EXG Settings**: Auto, MLX, GPU, CPU — each with localized label and description. Reconnect headset hint shown after change. +- **`embed-exg-mlx` feature**: new daemon Cargo feature that enables `skill-router/mlx`, `skill-eeg/mlx`, and `embed-zuna-mlx` together. + +- **fast-umap 1.6.0**: updated from 1.5.1 with MLX, PCA, and nearest-neighbor descent support. +- **MLX UMAP backend**: Apple Silicon native UMAP projection via `burn-mlx`. Runtime dispatch between MLX and GPU (wgpu) based on user preference. Auto defaults to MLX on macOS, GPU elsewhere. +- **Precision selector**: F32 / F16 precision for the GPU (wgpu) backend. MLX is F32-only (fast-umap trait constraint). Exposed in UMAP settings as chip group. +- **Backend & precision UI**: new "Compute Backend" section in UMAP settings with Auto / MLX / GPU chips and F32 / F16 precision chips. Pipeline summary badge shows active backend and precision. + +- **UMAP e2e benchmarks**: `umap_e2e_small` (200 pts), `umap_e2e_medium` (1K pts), `umap_e2e_large` (5K pts) with synthetic 32-dim EEG embeddings. Reports backend, timing, throughput (pts/sec), and separation score. + +- **gpu-fft 1.2.0**: updated from 1.1.1 with MLX FFT backend. EEG signal filtering (overlap-save convolution) uses MLX when `skill-eeg/mlx` is enabled. ~3.7x faster than wgpu at N=65536. +- **`skill-eeg/mlx` feature**: new feature gate that enables `gpu-fft/mlx` alongside `gpu-fft/wgpu`. Wired into `embed-exg-mlx` daemon feature. + +- **FFT MLX e2e**: `fft_e2e_roundtrip_256`, `fft_e2e_batch_4ch`, `fft_e2e_psd_peak_detection`, `fft_e2e_large_batch` (32 channels x 1024 samples). Validates round-trip accuracy, PSD peak detection, and batch throughput. + +- **Grayscale display mode**: optional system-wide grayscale that activates/deactivates in sync with Do Not Disturb. Reduces visual distraction during deep work. macOS only, default off. + +- **File activity in history sessions**: expanded EEG sessions show files worked on during the session with focus indicators, edit counts (+/-), language, and meeting interruption badges. +- **File activity in interactive search**: `file_activity` nodes in the 3D search graph linked to EEG epochs and text labels, showing which files were being edited during matching brain states. +- **Meeting nodes in search graph**: detected meetings appear alongside EEG results in the interactive search as amber nodes with temporal proximity edges. + +- **Git context card**: current branch (monospace), dirty/staged file count, ahead/behind indicators. Uses VS Code git extension API. +- **Session timeline card**: horizontal bar chart showing activity events by hour of day. Color-coded by volume. +- **Workspace activity card**: per-file edits, lines added/removed, focus time. Grouped by workspace folder (project), sorted by activity. Active file indicator (blue dot). Top 10 files per project. +- **Environment card**: editor/terminal/panel time split (colored stacked bar), tab count, editor groups, terminal count. Per-terminal cards with shell type, CWD, PID, shell integration status. +- **Terminal I/O sections**: collapsible with chevrons, minimized by default. Input shows timestamped commands with CWD. Output shows VT-parsed session content. +- **Terminal input card**: keystroke intensity per program (keys/min), duration, collapsible behind chevron. +- **Terminal impact card**: EEG focus delta by command category with pass rate percentage. +- **Context switch cost card**: focus level at each zone transition type with switch count. +- **Dev loops card**: edit-build-test cycles with iteration count, pass/fail rate, cycle time, focus trend (rising/falling/stable arrow). +- **Today's report card**: productivity score, morning/afternoon/evening period breakdown with focus bars and churn. +- **Energy card**: fatigue bar (inverse of focus decline), continuous work time, streak badge. +- **Struggle card**: EEG-only (hidden without recording), score 0-100, contributing factors. +- **Optimal hours card**: peak/avoid hours grid. +- **AI usage card**: acceptance rate bar, suggestion count, source breakdown. +- **Today vs yesterday card**: files and churn comparison with directional arrows. +- **Code review detection**: auto-detected when file switches > 5 and edit velocity < 1 line/min. +- **Process monitor card**: running dev servers (vite, next, webpack, cargo, node, python, docker, postgres) with ports and PIDs. +- **Info toggles**: every card has a `?` button explaining how metrics are calculated. +- **Dual-speed updates**: local data every 5s (visible), brain data every 30s. Pauses when sidebar hidden. + +- **OS-wide shell hooks**: preexec/precmd hooks for zsh, bash, fish, PowerShell. Background curl sends every command to daemon — zero delay to prompt. Self-contained scripts generated by daemon, stored in `~/.skill/shell-hooks/`. +- **Shell hook management**: `GET /v1/activity/shell-hook?shell=zsh` returns hook script, `POST /v1/activity/install-shell-hook` writes to rc file, `POST /v1/activity/uninstall-shell-hook` removes cleanly, `POST /v1/activity/shell-hook-status` health check per shell. +- **Terminal command table**: `terminal_commands` with command text, binary name, args, cwd, exit code, duration, auto-categorized (284 CLI tools across 18 categories), EEG focus at start + end for delta calculation. +- **Binary extraction**: raw binary name stored separately from args. Lazy categorization — `POST /v1/brain/recategorize-commands` reruns rules on all historical data. `POST /v1/brain/binary-stats` discovers uncategorized tools by frequency. +- **284 CLI tools recognized**: git, gh, glab, hf, docker, kubectl, helm, aws, gcloud, az, brew, pip, cargo, npm, psql, redis-cli, ollama, claude, pi, vim, tmux, htop, terraform, and 260+ more across 18 categories (build, test, run, git, docker, deploy, install, navigate, debug, network, database, ai, editor, multiplexer, monitor, env, system, other). +- **Script session recording**: `script` PTY proxy wraps shell sessions, captures all terminal I/O (including input inside interactive programs). Logs stored in `~/.skill/terminal-logs/` with auto-rotation. +- **VT100 terminal emulator**: `vt-parser.ts` replays script recordings through a virtual terminal with scrollback capture. Handles cursor movement, screen clears, alternate screen buffer. Separates input from output via prompt pattern detection. +- **App-specific JSONL parsing**: reads Claude Code (`~/.claude/projects/`) and Pi agent (`~/.pi/agent/sessions/`) conversation files directly. Extracts user prompts with real timestamps, assistant responses, and tool calls. Supports multiple parallel sessions. +- **Readline history polling**: monitors `~/.python_history`, `~/.node_repl_history`, `~/.irb_history`, `~/.psql_history` for REPL input. +- **`neuroskill terminal` CLI**: `status` (hook health per shell), `install [shell]`, `uninstall [shell]`, `commands` (recent tracked), `impact` (focus delta by category), `loops` (dev loop detection). +- **Tauri Terminal settings tab**: per-shell install/uninstall/repair buttons with health indicators (green/yellow/red dot), recent commands preview, "How it works" documentation. + +- **Validation / fatigue-research daemon backend**: foundations for calibrating the Break Coach and Focus Score against four external instruments — KSS (Karolinska Sleepiness Scale), NASA-TLX (workload), PVT (Psychomotor Vigilance Task), and an EEG-derived fatigue index per Jap et al. 2009. +- **`skill-data::validation_store`** — new SQLite store at `~/.skill/validation.sqlite` with five tables (`config`, `kss_responses`, `tlx_responses`, `pvt_runs`, `prompt_log`). Persistent config is a single-row JSON blob with serde defaults so adding a new channel later is a non-migration. New constant `VALIDATION_FILE` in `skill-constants`. +- **EEG fatigue index**: pure function `eeg_fatigue_index(bands) → Option` computing `(α + θ) / β` over the existing band-power snapshot. Guards against missing bands and zero-β. Passive; ships **on** because it costs nothing when no headset is attached. +- **Pure scheduler `decide_prompt(ctx, ...) → PromptDecision`**: respects the `respect_flow` master gate, configurable quiet hours, per-channel daily caps, runtime snoozes, and rate limits. Channel ordering: KSS first (lightest), TLX after a long task unit, PVT on a weekly cadence. Live `read_in_flow` and `read_break_coach_active` open the activity store read-only and ask `flow_state_now(300).in_flow` and `fatigue_check().fatigued` so the gates actually mean something. +- **Sane defaults — opt-in everywhere**: KSS, TLX, PVT all ship `enabled = false`. Only the passive EEG fatigue index ships on. The `respect_flow` master gate ships on. + +- **Validation & Research settings tab** (`src/lib/settings/ValidationTab.svelte`): new top-level settings tab gathering five `SettingsCard` blocks — global gates (`respect_flow`, quiet hours), KSS, NASA-TLX, PVT, and EEG fatigue index — plus a Calibration Week button and a recent-results card. + - Every toggle / numeric input PATCHes one nested field via `daemonPatch("/v1/validation/config", …)`; the daemon's recursive JSON merge keeps the rest of the config untouched. + - EEG fatigue card shows the live `(α + θ) / β` value polled every 5 s from `/v1/validation/fatigue-index`. + - Calibration Week: single button that batch-updates all four channels to higher-frequency presets (KSS 8/day, TLX after every flow block ≥ 20 min, PVT mid-week) in one PATCH. +- **PVT panel** (`src/lib/settings/PvtPanel.svelte`): full 3-minute Psychomotor Vigilance Task. Random ITIs (2–10 s), green-dot stimulus, `performance.now()` for sub-millisecond RT measurement, tracks responses + false starts. On finish computes mean RT, median RT, slowest-10% mean RT (anticipation-resistant), and lapse count (RT > 500 ms per Dinges & Powell 1985), POSTs to `/v1/validation/pvt`. State machine: intro → running → done. +- **NASA-TLX form** (`src/lib/settings/TlxForm.svelte`): six-slider modal for the raw (un-weighted) NASA-TLX. Performance scale uses inverted endpoints ("Failure" → "Perfect") per Hart 2006. POSTs to `/v1/validation/tlx` with `task_kind`, `task_duration_secs`, optional `prompt_id` echo-back. +- **Pure stats helper** (`src/lib/settings/pvt-stats.ts`): extracted `mean`, `median`, `slowest10Mean`, `lapseCount`, `computeStats` from the PVT panel so the math is unit-testable without a Svelte renderer. +- **`daemonPatch(path, body)`** in `src/lib/daemon/http.ts` — needed for the validation config endpoint. + +- **Test coverage for the validation feature across all three layers** — 72 tests total, all passing. + - **Rust unit tests** (`crates/skill-data/src/validation_store.rs`): 19 tests covering config persistence, store round-trips, the `(α + θ) / β` fatigue index (Jap et al. 2009) including zero-β and missing-band edge cases, every scheduler branch (flow respect, quiet hours, snooze blocking, KSS rate limit, TLX trigger after long task, TLX skip on short task, PVT weekly cadence, PVT silence after recent run, quiet-window-equals-end semantics), and prompt-log queries. + - **Rust HTTP integration tests** (`crates/skill-daemon/src/routes/validation.rs`): 10 tests using `tower::ServiceExt::oneshot()` against a tempdir-backed `AppState` — `GET /config` defaults, `PATCH /config` partial-update + round-trip, `POST /kss` happy path, KSS score-out-of-range → 400, TLX subscale-out-of-range → 400, `GET /should-prompt` returns `{kind: "none"}` with default config, `POST /snooze` envelope, plus three `merge_json` tests covering deep nesting. + - **Vitest — PVT statistics** (`src/tests/pvt-stats.test.ts`, 12 tests): edge cases for `mean`, `median`, `slowest10Mean` (including the n<10 fallback), `lapseCount` with the 500 ms Dinges & Powell threshold and explicit-threshold override, plus a known-fixture `computeStats` round-trip. + - **Vitest — i18n coverage** (`src/tests/validation-i18n.test.ts`, 23 tests): every VS Code l10n bundle exists and contains every user-facing key (KSS prompt + scores 1/5/9, TLX/PVT prompts, all four escape-hatch labels); every Tauri language `validation.ts` imports cleanly; English Tauri bundle covers every required key; every non-English Tauri bundle has at least the tab name and disclaimer translated. + +- **VS Code extension joins the validation prompt loop**: new `ValidationManager` (`extensions/vscode/src/validation.ts`) polls `/v1/validation/should-prompt` every 90 s and renders whichever channel the daemon decides to fire. + - **KSS** (1–9 sleepiness) — full QuickPick with Karolinska wording, plus Snooze 30m / Don't ask today / Stop these prompts escape hatches in-line. POSTs the answer to `/v1/validation/kss` echoing the daemon's `prompt_id` so the prompt log can mark it answered. + - **NASA-TLX** — fallback path: shows an information message offering to open the form in the Tauri app (deep link `neuroskill://validation/tlx`) plus the same escape hatches. + - **PVT** — weekly nudge: offers to deep-link into the Tauri PVT panel (`neuroskill://validation/pvt`) or skip a week (snoozes for 6 days so the next reminder fires on cadence). +- **Two new commands**: `NeuroSkill: Open Validation Settings…` (deep links into the Tauri preferences pane) and `NeuroSkill: Check for Validation Prompt Now` (forces a single scheduler poll — useful for opt-in onboarding). +- **`DaemonClient.patch()`**: extension's daemon client gained a PATCH method so the "Stop these prompts" escape hatch can flip `enabled = false` on the persistent config. + +- **VS Code extension**: separate repo (`NeuroSkill-com/vscode-neuroskill`) as git submodule at `extensions/vscode/`. 50+ tracked event types across editing, navigation, debugging, git, AI, terminal, clipboard, and more. +- **Sidebar webview (Svelte 5)**: full brain dashboard in the VS Code activity bar with circular flow gauge, metrics strip, daily report, energy, struggle, optimal hours, workspace activity, environment, terminal impact, context cost, and dev loops. +- **Activity bar icon**: neural network SVG icon in the VS Code sidebar; NeuroSkill logo (PNG) in the webview header. +- **Brain status bar in VS Code**: polls daemon every 30s showing flow state, fatigue, streak, task type, and struggle score. Shows "offline" when daemon unreachable. Notifications for fatigue and struggle. +- **Dual-speed sidebar updates**: local data (files, terminals, layout) refreshes every 5s when sidebar is visible; brain endpoints every 30s. Pauses when hidden. +- **Event caching**: offline-resilient `pendingEvents` array (10K cap) persisted to disk via `globalStorageUri`. Auto-flushes on reconnect. Survives VS Code restarts. Cache count shown in status bar tooltip. +- **Workspace activity tracking**: per-file edits, lines added/removed, focus time, active file indicator. Grouped by workspace folder (project), sorted by activity. Top 10 files per project. +- **Terminal command tracking**: full command text, exit code, cwd, output streaming (via `execution.read()`), shell type detection (zsh/bash/fish/powershell/cmd/node/python), focus time per terminal, PID. Expandable command output in sidebar. +- **Terminal shell integration**: captures commands via `onDidStartTerminalShellExecution` / `onDidEndTerminalShellExecution` (VS Code 1.93+). CWD tracked via `onDidChangeTerminalShellIntegration`. +- **Zone tracking**: editor/terminal/panel time split with stacked color bar (blue/green/yellow). Layout chips showing tab count, editor groups, terminal count. +- **Auto EEG labeling**: every significant VS Code event auto-inserts a searchable label into EEG recordings with smart categorization (editing, debugging, git commits, AI assistance, meetings, errors, navigation, terminal commands, zone switches). +- **Inline label embedding**: auto-labels embedded immediately via fastembed for instant searchability (no idle reembed wait). +- **Command execution tracking**: 40+ VS Code commands tracked (go-to-definition, rename, find, format, fold, git, AI, debug, layout) with semantic categorization. +- **IntelliSense acceptance detection**: multi-char single-line insertions heuristically identified as autocomplete acceptances. +- **Clipboard tracking**: clipboard content changes polled every 5s with debounce. +- **Layout snapshots**: periodic (60s) capture of editor groups, visible editors, open tabs, terminal count sent to daemon. +- **Zone switch events**: editor/terminal/panel transitions sent to daemon with EEG focus snapshot. +- **File system watcher**: selective watching of package.json, Cargo.toml, go.mod, .git/HEAD for external change detection. +- **Environment context**: one-time capture of appHost, remoteName, shell, uiKind, language. +- **Info toggles**: every sidebar card has a `?` button explaining how metrics are calculated (flow score formula, struggle signals, energy bar, optimal hours, dev loops, terminal impact, context cost). +- **Open NeuroSkill button**: launches native app from sidebar (cross-platform: `open -a` macOS, `start` Windows, `xdg-open` Linux). Also in command palette. +- **Command palette → sidebar**: `Show Brain Status`, `Today's Report`, `Am I Stuck?`, `Best Time to Code` now open sidebar and scroll to relevant section instead of showing toast notifications. + +- **Developer insights endpoint**: `POST /v1/brain/developer-insights` returns 7 actionable insights in one call. +- **Test failure by focus level**: correlates build/test exit codes with EEG focus (high/mid/low) — "your tests fail 45% more when focus is low." +- **Hourly productivity**: churn + undo rate + avg focus by hour of day — "you write 3x more bugs after 2pm." +- **Context switch recovery**: focus level at editor/terminal/panel transitions — "switching to terminal costs 4 focus points." +- **AI tool impact**: Claude/Pi focus delta vs baseline — "Claude conversations drop focus by 8 points." +- **Focus by language**: avg EEG focus + undo rate per programming language — "best focus in Rust, worst in CSS." +- **Dev loop efficiency by hour**: build/test cycle time + pass rate by time of day. +- **Tool focus impact**: avg focus when using each command category (docker, git, deploy, etc.). + +- **Widget accessibility and localization**. + +- **Analysis widgets (Optimal Hours, Daily Report, Weekly Trend)**. + +- **Biometric widgets (Heart Rate, Cognitive Load, EEG Band Power, Sleep)**. + +- **Brain Dashboard widget (medium)**. + +- **Calendar Mind State widget (large)**. + +- **Widget deep links (neuroskill:// URL scheme)**. + +- **Widget development infrastructure**. + +- **Core desktop widgets (Focus, Streak, Session, Break Timer)**. + +- **Interactive widget buttons (macOS 14+)**. + +- **Widget offline data caching**. + +- **Widget timeline reload on state changes**. + +- **zuna-rs 0.1.4**: updated from 0.1.3 with native MLX backend for EEG embedding inference. +- **`embed-zuna-mlx` feature**: new `ZunaMlxState` struct with `ZunaEncoder`. Adds `load_zuna_mlx()` and `encode_zuna_mlx()` functions matching the existing GPU/CPU variants. +- **Load priority**: when user selects Auto or MLX, encoder loading tries MLX first, then GPU f16, then GPU f32, then CPU. + +### Performance + +- **MLX vs GPU benchmarks** (Mac mini, Apple Silicon): + +| Dataset | Points | GPU (wgpu) | MLX | Speedup | +|---|---|---|---|---| +| Small | 200 | 120.9 s | 2.3 s | **51x** | +| Medium | 1,000 | 136.6 s | 7.1 s | **19x** | +| Large | 5,000 | 152.8 s | 23.8 s | **6.4x** | + +- **`open_readonly` for read-only handlers**: added `ActivityStore::open_readonly()` that skips 15+ ALTER TABLE migrations and opens in read-only mode. Switched 38+ HTTP handlers and 3 background tasks from `open` to `open_readonly`. +- **`PRAGMA busy_timeout=5000`**: added to `init_wal_pragmas` and `util::open_readonly` so all database connections (activity, labels, screenshots, EEG embeddings) wait up to 5 s on lock contention instead of failing immediately with SQLITE_BUSY. +- **Composite indexes**: added `(seen_at, file_path)` and `(seen_at, project)` indexes on `file_interactions` for queries that filter by time range and group by path or project. +- **Lock consolidation**: `productivity_score` reduced from 3 separate mutex acquisitions to 1 via internal `_q` query helpers. `weekly_digest` reduced from 12 to 4 locks. Introduced `daily_summary_q`, `context_switch_rate_q`, `get_focus_sessions_in_range_q` static methods that take `&Connection` directly. +- **`PRAGMA optimize` after pruning**: runs after hourly retention pruning to keep query planner statistics fresh. +- **Batch label lookup in interactive search**: replaced 15 per-epoch `get_labels_near` calls (each opening a new DB connection) with a single `get_labels_near_batch` call per text label, then in-memory filtering. Reduces search DB round-trips from ~28 to ~16. + +### Bugfixes + +- **Daemon-side event storage**: `git_commit`/`push`/`pull`/`checkout`/`stage`/`unstage`/`stash` events were received but never written to `build_events` (used only for EEG labels). Now persisted, with AI-assisted commits also recorded as `ai_events` (type `"ai_commit"`) and labelled `"git commit (AI)"`. +- **Completion-accepted events were dropped**: previously ignored by the daemon. Now stored as `ai_events` (type `"suggestion_accepted"`) and as edit chunks so they show up in code metrics and AI usage analytics. + +## Impact on analysis + +Brain analysis endpoints can now: +- Count human vs AI commits (`/brain/developer-insights`) +- Track AI suggestion acceptance rates (`/brain/ai-usage`) +- Include git activity in the activity timeline +- Weight human-authored code differently from AI output in focus/productivity scores + +## Files + +- `crates/skill-daemon/src/routes/settings_hooks_activity.rs` — Event handler updates + +- **Schema migration**: ALTER TABLE for existing databases missing columns added across releases. Runs before DDL to handle all legacy schemas. +- **Retention pruning**: meetings, clipboard, and secondary_windows pruned alongside file interactions during hourly maintenance. + +- **Dismiss stale Dependabot alerts**: crossbeam-deque, crossbeam-utils, crossbeam-queue, memoffset, and glib alerts dismissed (already at patched versions or fixed in fork). + +- **CORS allow-methods now includes `PATCH`**: `crates/skill-daemon/src/main.rs` was advertising only `GET, POST, PUT, DELETE, OPTIONS`, so the browser preflight blocked `PATCH /v1/validation/config` from the Tauri webview. Added `Method::PATCH` to the list. + +- **Equal padding on screenshot edges**: viewport width was 360px while the body was 320px wide, leaving an asymmetric 40px right gutter. Matched viewport to body width so left and right gutters are now identical. + +- **Deadlock in daily-report endpoint**: `daily_brain_report` held the database mutex while calling `productivity_score`, which re-acquires the same mutex. Scoped the lock to drop before the nested call. +- **Deadlock in struggle-predict endpoint**: same pattern in `predict_struggle` — held mutex while calling `get_recent_files`. +- **`overall_focus` returned `-0.0`**: always wrapped in `Some` even with no EEG data. Now returns `null` when no focus samples exist. +- **`best_period` arbitrary without EEG**: picked the first SQL result when all `avg_focus` were `None`. Now returns empty string when no EEG data to compare. +- **`weekly_avg_deep_mins` returned `-0.0`**: divided by hardcoded 7 regardless of actual days. Now divides by actual day count, returns `0.0` when empty. +- **Notification text ugly without EEG**: daily report notification showed "Best: . Score: 25. Focus: 0." — now conditionally includes only available fields. +- **Brain endpoints return HTTP 200 on errors**: all 18 brain handlers silently returned `null` with 200 OK when the activity store was offline or a query failed. Refactored to use `run_query` helper that returns 503 (db_unavailable) or 500 (task_error) with structured `ApiError` JSON. + +- **Worker thread panic recovery with auto-restart**: all 4 activity worker threads (poller, input monitor, file watcher, clipboard monitor) now wrapped in `catch_unwind` via `spawn_resilient`. On panic, the worker logs the error and restarts after 5 s with freshly cloned `AppState` and `Arc`. +- **osascript timeout**: all `osascript` calls (active window poll, secondary windows, clipboard monitor) now use a 3-second timeout via `run_osascript` helper. Previously, a hung app could block the poller thread indefinitely, silently killing all activity tracking. +- **Daily report catch-up on late startup**: if the daemon starts after 18:00 local, the daily brain report fires on the first tick instead of waiting up to 1 hour for the hourly check. +- **WAL checkpoint on shutdown**: daemon now runs `PRAGMA optimize` on the activity database during graceful shutdown, ensuring the WAL is checkpointed and the database is clean for next start. +- **Missing WAL pragmas on EEG embedding store**: `day_store.rs` opened connections without `init_wal_pragmas`, missing both WAL mode and `busy_timeout`. +- **Retention pruning for new tables**: added `prune_terminal_commands`, `prune_ai_events`, `prune_zone_switches`, `prune_layout_snapshots` — wired into the hourly maintenance cycle so these tables don't grow unbounded. +- **TOCTOU race in calibration settings**: concurrent profile CRUD could lose writes. Added `modify_settings_blocking()` helper that runs under a global mutex on tokio's blocking thread pool — serializes read-modify-write without blocking the async runtime. + +- **Duration-averaged EEG**: `FileSnapshot` now samples EEG focus/mood every 5 s (at each edit-chunk tick) and writes the average to `file_interactions` at finalize, replacing the single snapshot captured at file-switch time. Falls back to the initial snapshot if no samples were collected (`COALESCE`). +- **EEG mood/focus sample count mismatch**: both `FileSnapshot` and `build_focus_sessions` used a single counter for focus and mood samples, inflating mood averages when they arrived independently. Split into separate `focus_count` and `mood_count`. +- **EEG ghost data after disconnect**: `latest_bands` was not cleared when an EEG device disconnected, causing stale focus/mood values to persist in reports and the activity pipeline. Now cleared on all 3 disconnect paths (idle timeout, stream end, explicit disconnect). + +- **InteractiveGraph3D event listener leaks**: `pointerdown` and `dblclick` listeners on the WebGL canvas were never removed in `onDestroy`. Extracted into named refs with cleanup. +- **UmapViewer3D event listener leaks**: 4 canvas event listeners (pointermove, pointerleave, pointerdown, pointerup) added as anonymous functions were never removed on unmount. Extracted into named module-level refs with removal in `onDestroy`. +- **ActivityTab brain polling leak**: `startBrainPolling()` was called on mount but `stopBrainPolling()` was never called on destroy, leaving a 30-second interval running after leaving the tab. +- **Brain polling stopped by peer component**: `stopBrainPolling()` unconditionally killed the interval even when other components still needed it. Added reference counting so polling only stops when the last consumer unmounts. +- **Memory leak in terminal_command_end labeling**: `Box::leak` was used to format non-zero exit codes in EEG auto-labeling, permanently leaking memory for every failed command. Replaced with owned `String`. +- **Screenshot encode failures untracked**: `encode_webp` failures (disk full, I/O error) silently continued without incrementing the `capture_errors` counter. Now counted so metrics reflect actual failures. +- **401 auto-retry**: daemon HTTP client now retries once with a fresh token on 401 (stale token after daemon restart) instead of failing immediately. + +- **Path traversal in `delete_session`**: `csv_path` parameter was passed unsanitized to `fs::remove_file`. Now validates the path is within `skill_dir` via canonicalize + starts_with. +- **Timing-vulnerable token comparison**: default bearer token checks in auth middleware used `==` (variable-time). Switched to `constant_time_eq` for both in-memory and on-disk token paths. +- **Unbounded CSV memory in EXG loader**: loading a CSV with millions of rows would allocate unlimited RAM. Added a 4M row cap (~4.3 hours at 256 Hz). + +- **Timezone-wrong period classification**: `daily_brain_report`, `hourly_edit_heatmap`, and `optimal_hours` used `seen_at % 86400` (UTC hour) instead of local hour. Daily report now uses `seen_at - day_start`; heatmap and optimal-hours accept a timezone offset computed server-side. +- **UTC midnight in all clients**: search page, CLI (`cmdActivity`, `cmdBrain`), VS Code extension, and Swift widget all computed `day_start` as UTC midnight instead of local midnight. Fixed to use `Date.setHours(0,0,0,0)` (JS/TS) and `Calendar.startOfDay` (Swift). +- **Widget empty daily-report body**: `DaemonClient.fetchDailyReport()` sent an empty POST body; the endpoint requires `dayStart`. Now sends local midnight. +- **Widget UTC sleep/calendar endpoints**: `fetchSleep()` and `fetchCalendarEvents()` used `% 86400` UTC approximations. Now use `Calendar.current` for correct local time boundaries. + +### Refactor + +- **Shared `DaemonClient` for VS Code extension**: extracted the repeated `fetch` + auth-token + port-discovery + 3 s timeout pattern into one class. All features now use `client.post(path, body)`. Returns `null` on failure (never throws), with `setToken()` for refresh on reconnect. + +## Before + +Every component (brain.ts, sidebar.ts, extension.ts) independently constructed fetch calls: +```typescript +const port = await discoverDaemonPort(config); +const base = `http://${config.daemonHost}:${port}/v1`; +const headers = { "Content-Type": "application/json" }; +if (token) headers["Authorization"] = `Bearer ${token}`; +const resp = await fetch(`${base}${path}`, { method: "POST", headers, body, signal: AbortSignal.timeout(3000) }); +``` + +## After + +```typescript +const client = new DaemonClient(config, token); +const result = await client.post("/brain/flow-state", { windowSecs: 300 }); +``` + +## Benefits + +- Single place to update auth, timeout, port discovery +- All 8 new features use the shared client +- `setToken()` method for token refresh on reconnect +- Returns `null` on any failure (never throws) — all features handle gracefully + +## Files + +- `src/daemon-client.ts` — DaemonClient class (new) + +- **Sidebar disclaimer test now accepts either localiser**: existing `vscode-sidebar-disclaimer.test.ts` updated so the `vscode.l10n.t("sidebar.disclaimer")` ↔ `tr("sidebar.disclaimer")` migration doesn't break the assertion. Both look up the same key. + +- **Conversations table + FTS5**: full-text search on all AI conversation messages (user/assistant/tool). Three search modes: FTS, fuzzy, structured. +- **EEG timeseries table**: periodic JSON snapshots of all brain metrics. Extensible — new metrics without schema changes. Join-at-query-time correlation with any event. +- **Embeddings store**: generic, multi-model. User prompts embedded via fastembed (local, no API credits). Can re-embed with different models. +- **Code context HNSW index**: separate from label index for code-specific semantic search. +- **Binary extraction**: terminal commands store raw binary name + args separately. Lazy categorization — 284 CLI tools recognized, rerun anytime. +- **EEG attachment**: live focus/mood from `latest_bands` injected at event storage time. +- **EEG timeseries worker**: writes full band powers to `eeg_timeseries` every 5s during recording. + +### Build + +- **CI for the VS Code extension** (`extensions/vscode/.github/workflows/`): two workflows live in the submodule repo (`vscode-neuroskill`). + - **`ci.yml`** — runs on every PR and `main` push. `npm ci` → `tsc` build → `vsce package` → uploads the `.vsix` as a 14-day artifact. No secrets, no publish. + - **`release.yml`** — fires two ways. *Tag push* (`git push --tags` after `npm version patch`) verifies the tag matches `package.json` and publishes. *Manual dispatch* with a `patch | minor | major | x.y.z` input bumps `package.json`, commits, tags, pushes, then publishes. Both paths run `npx @vscode/vsce publish` (uses `VSCE_PAT`) and `npx ovsx publish` (uses `OVSX_PAT`), then create a GitHub release with the `.vsix` attached and the latest `## ` block from `CHANGELOG.md` as release notes. +- **Skip-on-missing-secret**: if either `VSCE_PAT` or `OVSX_PAT` is unset, the corresponding publish step emits a `::warning::` and is skipped — letting you wire up Open VSX later without breaking the workflow. +- **Releasing section in the extension README**: documents the secret setup (Azure DevOps PAT, Open VSX token), the two release flows, and the one-time namespace claims (Marketplace publisher registration + `ovsx create-namespace`). + +- **macOS: link libusb statically into `skill-daemon`**: vendored libusb via `rusb`'s `vendored` feature in `crates/skill-devices`. The `antneuro` USB driver pulls in `rusb` → `libusb1-sys`, which by default uses `pkg-config` to find a system libusb-1.0.dylib. On the macOS-26 release runner that resolved to Homebrew's `/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib`, so end users without Homebrew hit a launch-time `dyld: Library not loaded` error. Cargo feature unification now flips `libusb1-sys/vendored`, which compiles libusb from source and links `libusb-vendored.a` into the binary — verified by 105 `_libusb_*` text symbols present in the release binary and zero libusb references in `otool -L`. + +### CLI + +- **`neuroskill activity` expanded**: 19 subcommands — bands, window, input, windows, files, top-files, top-projects, languages, sessions, meetings, clipboard, builds, heatmap, switches, summary, score, digest, stale, timeline. +- **`neuroskill brain` added**: 14 subcommands — flow, load, load-files, recovery, optimal, fatigue, struggle, report, breaks, streak, task, stuck, interruptions, correlation. +- **`neuroskill vscode` added**: auto-build and install VS Code/VSCodium/Cursor extension from source on macOS, Linux, and Windows. + +### UI + +- **EEG focus timeline heatmap**: collapsible "Focus Timeline" sidebar section renders a ~280×36 px SVG sparkline of today's EEG focus (`/brain/eeg-range`, max 120 points), color-graded green/yellow/red, with hour labels and file-name annotations at peaks/valleys (merged from `/activity/timeline`). Toggle via `neuroskill.eegHeatmap`. + +## What you see + +In the NeuroSkill sidebar, a collapsible "Focus Timeline" section shows: + +- A ~280px wide, ~36px tall SVG sparkline +- Color gradient: green (>70 focus), yellow (40-70), red (<40) +- Hour labels along the bottom (0:00, 3:00, 6:00, ...) +- File names annotated at focus peaks and valleys + +## Data sources + +| Data | API Endpoint | +|------|-------------| +| EEG time-series | `/brain/eeg-range` (today, max 120 points) | +| File context | `/activity/timeline` (today, last 200 events) | + +The heatmap merges EEG data points with the closest timeline events to show which files correspond to focus peaks and dips. + +## Settings + +`neuroskill.eegHeatmap` (default: `true`) — Show/hide the heatmap in the sidebar. + +## Files + +- `src/sidebar.ts` — `_fetchHeatmap()` and `_renderHeatmap()` methods + +- **Brain status bar**: persistent bottom bar in Tauri app showing flow state, fatigue, streak, edit velocity, and focus score. +- **Brain dashboard card**: flow/fatigue/streak + focus progress bar on the EEG dashboard alongside existing BrainStateScores. +- **Brain indicators in ActivityTab**: flow state card (green), fatigue alert card (amber), and streak card (violet) at top. + +- **Tauri Brain Insights section** in Activity tab: test failure rates by focus level, focus by language bars, AI impact delta, tool focus grid, hourly productivity chart. +- **Tauri EEG focus inline**: terminal commands and conversation messages show EEG focus score (green/yellow/red) at the moment they occurred. +- **VS Code Brain Insights card**: test failure by focus chips, focus by language bars, tool impact chips. Only shown when EEG data is available. +- **VS Code Struggle card**: hidden when no EEG recording (focus is EEG-only, not activity-based). +- **VS Code Flow gauge label**: shows "focus" with EEG, "activity" without — no false claim of brain measurement. + +- **Tauri AI Conversations section** in Activity tab: timestamped message thread with role icons (user/assistant/tool), app name, EEG focus score, scrollable. +- **VS Code Conversations card**: timestamped messages with role icons, app badge, collapsible. +- **VS Code AI Usage card**: Copilot/Codeium acceptance rate bar, suggestion count, source breakdown chips. + +- **Search mode dropdown**: replaced 4-tab bar with a styled dropdown supporting 7 search modes — Interactive, EEG, Text, Images, Code, Meetings, Brain. +- **Diamond EEG nodes**: EEG epochs rendered as faceted octahedrons (diamonds) in the 3D search graph. Meeting nodes rendered as tetrahedrons (pyramids). Visual distinction by data type. +- **New search modes**: Code (file activity + brain state), Meetings (meeting events + recovery), Brain (flow/fatigue/struggle insights). +- **Terminal commands in Cmd-K**: added 4 terminal commands to the command palette — Recent Terminal Commands, Terminal Focus Impact, Context Switch Cost, Dev Loops. Translated labels in all 9 locales. + +- **Configurable alerts**: `neuroskill.alerts.focusLow` (warn when EEG focus drops below threshold), `neuroskill.alerts.continuousWorkMins` (warn after N minutes continuous work). +- **Daily digest notification**: at configurable time (default 17:30), shows productivity score + stats. "Full Report" button opens detail panel. +- **Full report panel**: `Cmd+Shift+R` opens full-width webview with same data as sidebar. +- **Keyboard shortcuts**: `Cmd+Shift+N` (toggle sidebar), `Cmd+Shift+R` (full report), `Cmd+Shift+Alt+N` (reconnect). +- **Open NeuroSkill button**: launches native app (cross-platform). +- **Event caching**: offline-resilient `pendingEvents` array (10K cap) persisted to disk. Auto-flushes on reconnect. + +- **Spacing pass on the validation tab and modals**: every card uses `flex flex-col gap-5 p-6` instead of the old `space-y-4` (which provided no padding override). Conditional sub-settings under a channel toggle are now indented under a left border (`ml-1 border-l-2 border-border pl-5`) so the parent/child relationship reads visually. Modals padded to `p-8` with `gap-3` button rows. TLX sliders gained `mt-1` lift off the description and `uppercase tracking-wide` low/high legend labels. + +- **Design tokens** (`tokens.css`): 30+ CSS custom properties — palette, surfaces, backgrounds, borders, semantic, typography. Zero inline rgba() in sidebar. +- **7 reusable Svelte components**: Card, MetricRow, Chevron, ProgressBar, Gauge, Badge, Callout. +- **Timestamp compliance**: 25 tests verifying UTC in data layer, local conversion only in UI. + +- **Theme-aware sidebar screenshots in the README**: every screenshot now ships in two variants (`*-dark.png`, `*-light.png`) and is embedded via a `` element with `prefers-color-scheme` sources. GitHub serves the variant matching the reader's OS theme; the marketplace gets the dark fallback. Six states captured: in-flow, stuck, fatigued, low-focus / off-peak, daemon-disconnected, status-bar strip. +- **Sidebar mock library + Playwright generator**: HTML stubs in `extensions/vscode/media/preview/` render the sidebar webview offline (no daemon required). A shared `_styles.css` drives both themes via a `:root.light` override; `npm run screenshots` (Playwright) toggles `` between captures and writes 12 PNGs to `media/screenshots/`. +- **Marketplace README pipeline**: `scripts/build-marketplace-readme.mjs` rewrites every relative `media/...` path (including ``, which `vsce` doesn't touch) to absolute GitHub raw URLs at package time. The source `README.md` keeps relative paths for GitHub + offline viewing; the generated `.marketplace.readme.md` is what `vsce package --readme-path` ships. + +- **Sidebar webview now renders correctly in light themes** (`extensions/vscode/src/sidebar.ts`). Three theme-fragile spots fixed: + - `.metric-card` background switched from `--vscode-sideBar-background` (which is the same colour as the surrounding sidebar — cards collapsed into a faint border in light mode) to `--vscode-editorWidget-background`, with `--vscode-input-background` and a translucent grey as cascading fallbacks. + - `.ai-bar` track switched from `--vscode-panel-border` to `--vscode-progressBar-background` so the empty bar is visible against a white background. + - Row-divider opacity bumped from 0.08 to 0.18; added `:last-child { border-bottom: none }` on commit and AI-metric rows so the last row doesn't double up against the disclaimer footer. +- **State colours stay hard-coded** — red ring for stuck, green for flow, amber for warning. They're semantic, not chrome, and they read fine on both themes. + +### Server + +- **`POST /v1/activity/shell-command`**: receives commands from shell hooks with command text, cwd, shell type, exit code. +- **`POST /v1/brain/terminal-commands`**: query recent commands with exit codes, durations, categories, EEG correlation. +- **`POST /v1/brain/terminal-impact`**: avg EEG focus delta by command category. +- **`POST /v1/brain/binary-stats`**: usage frequency per binary for discovering uncategorized tools. +- **`POST /v1/brain/recategorize-commands`**: rerun categorization rules on all historical commands. +- **Terminal EEG auto-labels**: `"running: cargo test"` on start, `"cargo test passed"` / `"cargo test failed (exit 1)"` on end. +- **Zone switch events**: editor/terminal/panel transitions stored with EEG focus snapshot. `POST /v1/brain/context-cost` returns focus at each transition type. +- **Layout snapshots**: periodic (60s) tab/group/terminal counts from VS Code. + +- **`/v1/validation/*` HTTP endpoints** (axum): `GET /config`, `PATCH /config` (recursive JSON merge for partial updates), `POST /snooze`, `POST /disable-today`, `GET /should-prompt` (returns the daemon's prompt decision; logs the fire so the rate-limiter sees it), `POST /kss`, `POST /tlx`, `POST /pvt`, `POST /close-prompt`, `GET /results`, `GET /fatigue-index` (live `(α+θ)/β` from the latest band snapshot). +- **`AppState.validation_runtime`** (`crates/skill-daemon-state/src/state.rs`): new `Arc>` field holding ephemeral snooze/disable-today state. Resets on daemon restart by design — a crash loop shouldn't keep prompts suppressed forever. + +- **`terminal_commands` table**: shell command text, cwd, exit code, duration, auto-categorized (50+ patterns: build/test/run/git/docker/deploy/install/navigate/debug/network/other), EEG focus at start and end for delta calculation. +- **`dev_loops` table**: edit→build/test cycle tracking with iteration count, pass/fail rate, avg cycle time, focus trend (rising/falling/stable). +- **`zone_switches` table**: editor/terminal/panel transitions with EEG focus snapshot at moment of switch. +- **`layout_snapshots` table**: periodic tab/group/terminal counts from VS Code. +- **Event handler**: new match arms for `terminal_command_start`, `terminal_command_end`, `zone_switch`, `layout_snapshot` in `activity_vscode_events_impl()`. +- **Command categorizer**: `categorize_command()` with 50+ patterns across build, test, run, git, docker, deploy, install, navigate, debug, network. +- **`POST /v1/brain/terminal-impact`**: avg EEG focus delta by command category — shows how builds, tests, git, docker affect brain state. +- **`POST /v1/brain/context-cost`**: focus level at each zone transition type with switch counts. +- **`POST /v1/brain/dev-loops`**: edit-build-test cycle detection with iterations, pass rate, cycle time, focus trend. +- **`POST /v1/brain/terminal-commands`**: recent commands with exit codes, durations, categories, EEG correlation. +- **Enhanced `predict_struggle()`**: terminal failures (+8/fail, max +40) and re-running same failing command 3+ times (+15/rerun, max +30) boost struggle score. New suggestions: "You're re-running the same failing command", "Multiple failures — read the error messages". +- **Enhanced `detect_task_type()`**: terminal command categories override heuristics with higher confidence — docker/kubectl→infrastructure (0.8), deploy→deploying (0.85), git dominating→git_management (0.7), test→testing (0.85), debug→debugging (0.9). +- **Terminal EEG auto-labels**: `"running: cargo test"` on start, `"cargo test passed"` / `"cargo test failed (exit 1)"` on end, `"switched to terminal"` on zone changes. +- **AI events table**: `ai_events` SQLite table tracking suggestion shown/accepted/rejected and chat sessions with source attribution. +- **OS-wide shell hooks**: `scripts/shell-hooks/` with preexec/precmd hooks for zsh, bash, fish, PowerShell. Sends every command to daemon via background curl. No delay to prompt. +- **Shell hook daemon endpoints**: `GET /v1/activity/shell-hook?shell=zsh` returns hook script, `POST /v1/activity/install-shell-hook` writes to `~/.skill/shell-hooks/` and appends to rc file, `POST /v1/activity/uninstall-shell-hook` removes cleanly, `POST /v1/activity/shell-hook-status` health check. +- **`neuroskill terminal` CLI**: `status` (hook health per shell), `install [shell]`, `uninstall [shell]`, `commands` (recent tracked), `impact` (focus delta by category), `loops` (dev loop detection). +- **`neuroskill brain` new subactions**: `terminal-impact`, `context-cost`, `dev-loops`. +- **`neuroskill activity` new subaction**: `terminal-commands`. +- **Tauri Terminal settings tab**: per-shell install/uninstall/repair buttons, health indicators (green/yellow/red), recent commands preview, "How it works" documentation. +- **`neuroskill vscode` CLI**: auto-install extension to VS Code, VSCodium, or Cursor on macOS, Linux, and Windows. +- **Meeting nodes in search graph**: meetings appear as amber nodes in the interactive 3D search graph linked by `meeting_prox` edges. + +### i18n + +- Activity dashboard, clipboard, file history, session replay, weekly report, brain status, timeline, and code-brain correlation translations for all 9 locales. + +- Added `settings.inferenceDeviceAuto`, `settings.inferenceDeviceAutoDesc`, `settings.inferenceDeviceMlx`, `settings.inferenceDeviceMlxDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). + +- Added `umapSettings.backend`, `umapSettings.backendDesc`, `umapSettings.precision`, `umapSettings.precisionDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). + +- Added `dnd.grayscale` and `dnd.grayscaleDesc` translations to all 9 locales. + +- Search mode labels (Code, Meetings, Brain) translated in all 9 locales. +- Terminal command palette entries translated in all 9 locales. + +- **New `validation` namespace** in all 9 languages (`src/lib/i18n/{de,en,es,fr,he,ja,ko,uk,zh}/validation.ts`): ~95 keys in English (full coverage), ~55 in each other language for the user-visible strings (long-tail descriptions fall back via the runtime `t()` chain). `index.ts` barrel exports updated for every language. + +- **22 new `validation.*` keys per bundle** in all 9 languages (`bundle.l10n.{de,en,es,fr,he,ja,ko,uk,zh-cn}.json`): KSS prompt + 9 score labels with translated Karolinska wording, TLX/PVT prompts, all four escape-hatch labels, the disabled-permanently acknowledgement. +- **Two new `cmd.*` keys** for the validation commands in `package.nls.json`. + +- Added `settings.inferenceDeviceAuto`, `settings.inferenceDeviceAutoDesc`, `settings.inferenceDeviceMlx`, `settings.inferenceDeviceMlxDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). + +### Docs + +- VS Code extension design plan at `docs/vscode-extension.md`. +- `neuroskill-activity` skill documentation with 18 activity + 24 brain subcommands, terminal integration, shell hook reference, command categorization table. +- Updated `neuroskill-dnd` skill with grayscale mode. +- Updated `neuroskill/README.md` with terminal, brain awareness, and VS Code extension features. +- Updated `skills/SKILL.md` index with terminal tracking skill reference. + +- **VS Code extension README rewritten for skeptical developers**: scientific framing throughout — every derived metric (focus score, flow detector, deep-work minute, struggle predictor, optimal hours, fatigue) now ships with its formula, hyperparameters, and a "what can go wrong" line. Added an explicit *What this is, and isn't* section, a *Validation status* table tracking each metric's evidence level (production / pilot / heuristic / descriptive-only), and a *Validation roadmap* describing the planned KSS / NASA-TLX / PVT / EEG-fatigue calibration channels. Replaced the marketing pain-points table with a per-API signal taxonomy and an architecture paragraph. +- **References section with verified citations**: nine entries with DOIs cross-checked via CrossRef and `doi.org` redirects — Csikszentmihalyi 1990 (flow), Klimesch 1999 (α/θ oscillations), Lubar 1991 (θ/β attention ratio), Barry et al. 2005 (caffeine confound), Newport 2016 (deep work), Roenneberg et al. 2003 (chronotypes), Hart & Staveland 1988 + Hart 2006 (NASA-TLX), Jap et al. 2009 (EEG fatigue index). Each reference linked back to the metric it grounds. +- **Repository metadata pointed at the right repo**: discovered `extensions/vscode/` is a git submodule of `vscode-neuroskill`, not a subdirectory of the monorepo. Reverted `repository.url`, `bugs.url`, `homepage`, `qna` to the submodule's actual repo so the marketplace links resolve. + +### Dependencies + +- **burn-mlx**: added `burn-mlx` from git (`eidola-ai/burn-mlx`, branch `burn-0-20`) as optional dependency in `skill-router` and `skill-daemon`. Workspace `[patch.crates-io]` redirects all `burn-mlx` references to the git version (crates.io 0.1.2 targets burn 0.16; project uses burn 0.20). +- **macOS deployment target**: bumped from 10.15 to 14.0 in `.cargo/config.toml` for Metal 3.0 `simdgroup_matrix` intrinsics required by MLX. Still satisfies llama.cpp's `std::filesystem` (available since 10.15). +- **half 2.4**: added to `skill-router` for GPU f16 precision backend type (`CubeBackend`). + +- **Fix rand 0.7.3 vulnerability**: vendored `phf_generator 0.8.0` with rand bumped from 0.7 to 0.8, eliminating the vulnerable transitive dependency. +- **Remove atty**: migrated `iroh_test_client` from `structopt` (clap v2) to `clap v4` derive, dropping the unmaintained `atty` crate. +- **Update llama-cpp-4 to 0.2.50**: upstream llama.cpp fixes including `common_*` symbol renames. +- **Update kittentts to 0.4.1**: TTS engine update. +- **Add grayscale 0.0.1**: macOS grayscale display control for DND mode. diff --git a/changes/unreleased/01-human-vs-ai-tracking.md b/changes/unreleased/01-human-vs-ai-tracking.md deleted file mode 100644 index 6c856913..00000000 --- a/changes/unreleased/01-human-vs-ai-tracking.md +++ /dev/null @@ -1,43 +0,0 @@ -### Features - -- **Human vs AI activity tracking**: every edit and commit is now classified as `source: "human"` or `source: "ai"` in real-time. `AIActivityTracker` watches VSCode commands (Copilot/Codeium completions, inline chat, AI-generated commit messages) to tag subsequent edits/commits, exposes `getAIRatioForFile()` (rolling 5-minute ratio) for CodeLens + sidebar, and forwards the `source` field to the daemon for `build_events` / `ai_events` storage. - -## How it works - -The `AIActivityTracker` monitors VSCode command execution to detect AI tool usage: - -- **Inline completions** (Copilot, Codeium) — edits within 5 seconds after an AI command are tagged `source: "ai"` -- **Inline chat** (`inlineChat.start`, `copilot.interactiveEditor.*`) — subsequent edits in the same file are tagged AI -- **Commit messages** — `github.copilot.git.generateCommitMessage` marks the next commit as AI-assisted -- **Everything else** — classified as `source: "human"` - -## What's tracked - -| Signal | Classification | -|--------|---------------| -| Manual typing | `human` | -| Copilot inline suggestion accepted | `ai` | -| Copilot inline chat edits | `ai` | -| Paste from external source | `human` | -| AI-generated commit message | `ai` | -| Manually typed commit message | `human` | - -## Per-file AI ratio - -`AIActivityTracker.getAIRatioForFile(path)` returns a rolling 5-minute ratio (0.0 = all human, 1.0 = all AI) used by: -- CodeLens annotations (shows "AI-Assisted" vs focus score) -- Sidebar (Human/AI percentage display) -- Brain status command (Human/AI split) - -## Daemon integration - -The `source` field is sent on every `edit` and `git_commit` event to the daemon. The daemon stores: -- AI commits as `"git commit (ai-assisted)"` in `build_events` -- AI commits also as `ai_events` for analytics weighting -- Completion acceptances as `ai_events` with type `"suggestion_accepted"` - -## Files - -- `src/ai-tracker.ts` — Core tracker (new) -- `src/events.ts` — Wired to classify edits and commits -- `crates/skill-daemon/src/routes/settings_hooks_activity.rs` — Daemon-side storage diff --git a/changes/unreleased/02-focus-code-review.md b/changes/unreleased/02-focus-code-review.md deleted file mode 100644 index eafe1068..00000000 --- a/changes/unreleased/02-focus-code-review.md +++ /dev/null @@ -1,31 +0,0 @@ -### Features - -- **Focus-aware code review CodeLens**: annotations at the top of each file show the developer's focus level when the file was last edited (`⚠ Low Focus (42)`, `ℹ Focus: 65/100`, `🤖 AI-Assisted (85%)`, or none for high focus). `NeuroSkill: Show Files Needing Review` command lists low-focus human-authored files. Toggle via `neuroskill.focusCodeLens`. - -## What you see - -- `⚠ Low Focus (42) — Review Recommended` — File was edited during low focus. Click to see all files needing review. -- `ℹ Focus: 65/100` — Moderate focus, informational only. -- `🤖 AI-Assisted (85%)` — Most edits were AI-generated, focus score not applicable. -- No annotation — High focus (>70) or no data yet. - -## Commands - -**NeuroSkill: Show Files Needing Review** (`Cmd+Shift+P`) -- Shows a QuickPick list of files edited during low focus (<50) that were mostly human-authored -- Sorted by focus score (lowest first) -- Select a file to open it - -## How it works - -- `FocusCodeLensProvider` queries `/brain/cognitive-load` (grouped by file) every 30 seconds -- Combines focus data with `AIActivityTracker.getAIRatioForFile()` to distinguish human vs AI code -- Files with high AI ratio (>70%) show AI label instead of focus score — AI code doesn't reflect human cognitive state - -## Settings - -`neuroskill.focusCodeLens` (default: `true`) — Toggle CodeLens annotations on/off. - -## Files - -- `src/codelens-provider.ts` — CodeLens provider (new) diff --git a/changes/unreleased/03-flow-shield.md b/changes/unreleased/03-flow-shield.md deleted file mode 100644 index 54b5c842..00000000 --- a/changes/unreleased/03-flow-shield.md +++ /dev/null @@ -1,28 +0,0 @@ -### Features - -- **Smart Interruption Shield**: when `/brain/flow-state` reports `in_flow: true`, VS Code's Do Not Disturb mode auto-enables and the status bar shows `$(shield) In Flow 12m`. `NeuroSkill: Toggle Flow Shield` cycles Auto / Forced-on / Forced-off. Toggle via `neuroskill.flowShield`. - -## How it works - -- When `/brain/flow-state` reports `in_flow: true`, the Flow Shield activates -- Enables VSCode's Do Not Disturb mode (VSCode 1.90+) -- Shows `$(shield) In Flow 12m` in the status bar with elapsed time -- When flow state ends, DND is automatically disabled - -## Manual override - -**NeuroSkill: Toggle Flow Shield** (`Cmd+Shift+P`) - -Cycles through three modes: -1. **Auto** (default) — activates/deactivates based on EEG flow detection -2. **Forced on** — always active regardless of flow state -3. **Forced off** — never active - -## Settings - -`neuroskill.flowShield` (default: `true`) — Enable/disable the flow shield feature. - -## Files - -- `src/flow-shield.ts` — Flow shield implementation (new) -- `src/brain.ts` — Calls `flowShield.update()` every 30s diff --git a/changes/unreleased/04-adaptive-break-coach.md b/changes/unreleased/04-adaptive-break-coach.md deleted file mode 100644 index bda490e5..00000000 --- a/changes/unreleased/04-adaptive-break-coach.md +++ /dev/null @@ -1,33 +0,0 @@ -### Features - -- **Adaptive Break Coach**: personalized break timing based on the developer's actual EEG focus cycle (via `/brain/break-timing`), not generic Pomodoro. Status bar countdown `$(clock) Break in 8m`, max one notification per cycle, auto-resets when fatigue indicates idleness. `NeuroSkill: Take a Break` resets the timer manually. Toggle via `neuroskill.breakCoach`. - -## How it works - -- Queries `/brain/break-timing` to learn the developer's natural focus cycle length -- Shows a countdown in the status bar: `$(clock) Break in 8m` -- When the predicted focus drop is imminent (<5 min), the countdown turns visible -- When the cycle ends, shows `$(clock) Break time` and optionally notifies - -## Notifications - -- Max one notification per focus cycle -- Message: "You've been focused for 47m. Your natural cycle is 42m — take a break?" -- Buttons: "Take Break" (resets timer) or "Dismiss" - -## Timer sync - -The break coach automatically syncs with the daemon's fatigue data. If `continuous_work_mins` drops below 5 (indicating the user was idle), the session timer resets — no false break suggestions after returning from lunch. - -## Commands - -**NeuroSkill: Take a Break** (`Cmd+Shift+P`) — Manually acknowledge a break and reset the timer. - -## Settings - -`neuroskill.breakCoach` (default: `true`) — Enable/disable break coaching. - -## Files - -- `src/break-coach.ts` — Break coach implementation (new) -- `src/brain.ts` — Calls `breakCoach.refresh()` and `resetSessionIfIdle()` every 30s diff --git a/changes/unreleased/05-struggle-ai-bridge.md b/changes/unreleased/05-struggle-ai-bridge.md deleted file mode 100644 index 17979881..00000000 --- a/changes/unreleased/05-struggle-ai-bridge.md +++ /dev/null @@ -1,31 +0,0 @@ -### Features - -- **Struggle → AI assist bridge**: when `/brain/struggle-predict` flags a file (EEG focus + undo rate + velocity drop + time-on-file), shows an actionable notification with **Open Copilot Chat**, **Open Terminal**, and **Step Back** buttons. Debounced to one suggestion per file per 10 minutes. Toggle via `neuroskill.struggleBridge`. - -## How it works - -- Monitors `/brain/struggle-predict` (EEG focus + undo rate + velocity drop + time-on-file) -- When `struggling: true`, shows an actionable notification: - > "Stuck on auth.ts? (score: 78) Consider breaking the problem into smaller pieces." - -## Action buttons - -| Button | Action | -|--------|--------| -| **Open Copilot Chat** | Opens GitHub Copilot interactive chat (or generic chat panel) | -| **Open Terminal** | Toggles terminal for CLI debugging | -| **Step Back** | Dismiss and take a mental break | - -## Debouncing - -- Max one suggestion per file per 10 minutes -- Prevents notification fatigue while still catching genuine struggles - -## Settings - -`neuroskill.struggleBridge` (default: `true`) — Enable/disable struggle detection and AI suggestions. - -## Files - -- `src/struggle-bridge.ts` — Struggle bridge implementation (new) -- `src/brain.ts` — Calls `struggleBridge.check()` every 30s (replaces the old generic struggle notification) diff --git a/changes/unreleased/06-flow-triggers-dashboard.md b/changes/unreleased/06-flow-triggers-dashboard.md deleted file mode 100644 index d31f425d..00000000 --- a/changes/unreleased/06-flow-triggers-dashboard.md +++ /dev/null @@ -1,29 +0,0 @@ -### Features - -- **Personal Flow Triggers dashboard**: collapsible "Your Flow Recipe" sidebar section mining 7-day EEG + activity history — best language (`/brain/code-eeg`), peak hours (`/brain/optimal-hours`), natural cycle length (`/brain/break-timing`), top flow killer (`/brain/context-cost`). Toggle via `neuroskill.flowTriggers`. - -## What you see - -In the NeuroSkill sidebar panel, a collapsible "Your Flow Recipe" section shows: - -- **Best language** — "Focus best on Rust (82)" — from `/brain/code-eeg` -- **Peak hours** — "Peak hours: 9:00, 10:00, 14:00" — from `/brain/optimal-hours` -- **Natural cycle** — "Natural cycle: 42m" — from `/brain/break-timing` -- **Flow killer** — "Flow killer: Slack (focus 38 at switch)" — from `/brain/context-cost` - -## Data sources - -| Insight | API Endpoint | Time Range | -|---------|-------------|------------| -| Best languages | `/brain/code-eeg` | Last 7 days | -| Peak hours | `/brain/optimal-hours` | Last 7 days | -| Natural cycle | `/brain/break-timing` | Last 7 days | -| Flow killers | `/brain/context-cost` | Last 7 days | - -## Settings - -`neuroskill.flowTriggers` (default: `true`) — Show/hide the flow triggers section in the sidebar. - -## Files - -- `src/sidebar.ts` — `_fetchFlowTriggers()` and `_renderFlowTriggers()` methods diff --git a/changes/unreleased/07-focus-scored-commits.md b/changes/unreleased/07-focus-scored-commits.md deleted file mode 100644 index bb49b4e1..00000000 --- a/changes/unreleased/07-focus-scored-commits.md +++ /dev/null @@ -1,38 +0,0 @@ -### Features - -- **Focus-scored git commits in sidebar**: collapsible "Recent Commits" section shows the last 8 commits with author marker (👤 human / 🤖 AI) and a color-coded focus badge captured at commit time via `/brain/flow-state`. AI-assisted commits show "AI" instead of a focus score. Toggle via `neuroskill.focusCommits`. - -## What you see - -In the NeuroSkill sidebar, a collapsible "Recent Commits" section shows the last 8 commits: - -``` -👤 82 fix: resolve auth race condition -👤 45 chore: update dependencies -🤖 AI refactor: extract helper functions -👤 71 feat: add user preferences -``` - -- **👤** = human-authored commit -- **🤖** = AI-assisted commit (message generated by Copilot) -- **Focus badge** = color-coded: green (>70), yellow (40-70), red (<40) -- AI commits show "AI" instead of a focus score — AI output doesn't measure human cognition - -## How it works - -- When the extension detects a git commit (SCM input box clears), it: - 1. Snapshots current EEG focus via `/brain/flow-state` - 2. Checks `AIActivityTracker.isCommitAIAssisted()` - 3. Records the commit with focus score + source label -- Commits stored in-memory (last 15), refreshed on sidebar render -- The daemon also stores commits with human/AI distinction in `build_events` - -## Settings - -`neuroskill.focusCommits` (default: `true`) — Show/hide the commits section in the sidebar. - -## Files - -- `src/sidebar.ts` — `recordCommit()`, `_renderCommits()` -- `src/extension.ts` — Wires commit detection to sidebar recording -- `src/events.ts` — `onCommit` callback with human/AI source diff --git a/changes/unreleased/08-optimal-task-router.md b/changes/unreleased/08-optimal-task-router.md deleted file mode 100644 index 982c1ff9..00000000 --- a/changes/unreleased/08-optimal-task-router.md +++ /dev/null @@ -1,29 +0,0 @@ -### Features - -- **Optimal Task Router**: monitors flow score every 30 s and, when it changes by >20 points, suggests an appropriate task type (refactoring/new features at high focus, code review at moderate, docs/routine at low). Debounced to one suggestion per 15 minutes. Toggle via `neuroskill.taskRouter`. - -## How it works - -- Monitors the flow state score every 30 seconds -- When focus changes by >20 points from the last reading, suggests an appropriate task type: - -| Focus Level | Suggestion | -|------------|------------| -| >75 | "Focus is high (85) — great time for complex work like refactoring or new features." | -| 45-75 | "Focus moderate (58) — good for code review, testing, or incremental tasks." | -| <45 | "Focus low (32) — consider documentation, routine tasks, or a break." | - -## Debouncing - -- Maximum one suggestion every 15 minutes -- No suggestion on the first reading (establishes baseline) -- No suggestion if focus stays within 20 points of the last reading - -## Settings - -`neuroskill.taskRouter` (default: `true`) — Enable/disable task routing suggestions. - -## Files - -- `src/task-router.ts` — Task router implementation (new) -- `src/brain.ts` — Calls `taskRouter.check()` every 30s diff --git a/changes/unreleased/09-eeg-heatmap.md b/changes/unreleased/09-eeg-heatmap.md deleted file mode 100644 index 297546f9..00000000 --- a/changes/unreleased/09-eeg-heatmap.md +++ /dev/null @@ -1,29 +0,0 @@ -### UI - -- **EEG focus timeline heatmap**: collapsible "Focus Timeline" sidebar section renders a ~280×36 px SVG sparkline of today's EEG focus (`/brain/eeg-range`, max 120 points), color-graded green/yellow/red, with hour labels and file-name annotations at peaks/valleys (merged from `/activity/timeline`). Toggle via `neuroskill.eegHeatmap`. - -## What you see - -In the NeuroSkill sidebar, a collapsible "Focus Timeline" section shows: - -- A ~280px wide, ~36px tall SVG sparkline -- Color gradient: green (>70 focus), yellow (40-70), red (<40) -- Hour labels along the bottom (0:00, 3:00, 6:00, ...) -- File names annotated at focus peaks and valleys - -## Data sources - -| Data | API Endpoint | -|------|-------------| -| EEG time-series | `/brain/eeg-range` (today, max 120 points) | -| File context | `/activity/timeline` (today, last 200 events) | - -The heatmap merges EEG data points with the closest timeline events to show which files correspond to focus peaks and dips. - -## Settings - -`neuroskill.eegHeatmap` (default: `true`) — Show/hide the heatmap in the sidebar. - -## Files - -- `src/sidebar.ts` — `_fetchHeatmap()` and `_renderHeatmap()` methods diff --git a/changes/unreleased/10-daemon-event-storage.md b/changes/unreleased/10-daemon-event-storage.md deleted file mode 100644 index 21df0863..00000000 --- a/changes/unreleased/10-daemon-event-storage.md +++ /dev/null @@ -1,16 +0,0 @@ -### Bugfixes - -- **Daemon-side event storage**: `git_commit`/`push`/`pull`/`checkout`/`stage`/`unstage`/`stash` events were received but never written to `build_events` (used only for EEG labels). Now persisted, with AI-assisted commits also recorded as `ai_events` (type `"ai_commit"`) and labelled `"git commit (AI)"`. -- **Completion-accepted events were dropped**: previously ignored by the daemon. Now stored as `ai_events` (type `"suggestion_accepted"`) and as edit chunks so they show up in code metrics and AI usage analytics. - -## Impact on analysis - -Brain analysis endpoints can now: -- Count human vs AI commits (`/brain/developer-insights`) -- Track AI suggestion acceptance rates (`/brain/ai-usage`) -- Include git activity in the activity timeline -- Weight human-authored code differently from AI output in focus/productivity scores - -## Files - -- `crates/skill-daemon/src/routes/settings_hooks_activity.rs` — Event handler updates diff --git a/changes/unreleased/11-shared-daemon-client.md b/changes/unreleased/11-shared-daemon-client.md deleted file mode 100644 index 230205fb..00000000 --- a/changes/unreleased/11-shared-daemon-client.md +++ /dev/null @@ -1,32 +0,0 @@ -### Refactor - -- **Shared `DaemonClient` for VS Code extension**: extracted the repeated `fetch` + auth-token + port-discovery + 3 s timeout pattern into one class. All features now use `client.post(path, body)`. Returns `null` on failure (never throws), with `setToken()` for refresh on reconnect. - -## Before - -Every component (brain.ts, sidebar.ts, extension.ts) independently constructed fetch calls: -```typescript -const port = await discoverDaemonPort(config); -const base = `http://${config.daemonHost}:${port}/v1`; -const headers = { "Content-Type": "application/json" }; -if (token) headers["Authorization"] = `Bearer ${token}`; -const resp = await fetch(`${base}${path}`, { method: "POST", headers, body, signal: AbortSignal.timeout(3000) }); -``` - -## After - -```typescript -const client = new DaemonClient(config, token); -const result = await client.post("/brain/flow-state", { windowSecs: 300 }); -``` - -## Benefits - -- Single place to update auth, timeout, port discovery -- All 8 new features use the shared client -- `setToken()` method for token refresh on reconnect -- Returns `null` on any failure (never throws) — all features handle gracefully - -## Files - -- `src/daemon-client.ts` — DaemonClient class (new) diff --git a/changes/unreleased/feat-activity-dashboard.md b/changes/unreleased/feat-activity-dashboard.md deleted file mode 100644 index 40f22654..00000000 --- a/changes/unreleased/feat-activity-dashboard.md +++ /dev/null @@ -1,19 +0,0 @@ -### Features - -- **Activity dashboard tab**: new Settings tab with daily summary, productivity score (0-100 composite), hourly heatmap, top files/projects, language breakdown, focus sessions, meetings, and stale file alerts. -- **Focus session replay**: click any session to expand a detailed view with file timeline, edit counts, EEG focus overlay bar, and meeting interruptions. -- **Weekly report with CSV export**: 7-day activity digest with daily breakdown chart and one-click CSV download. -- **Productivity scoring**: composite score from edit velocity + deep work time + context stability + EEG focus. -- **Stale file detection**: files edited but untouched for 7+ days. -- **Code-Brain correlation**: focus ranked by language, project, and individual files — shows which code drains or energizes you. -- **Activity timeline**: unified chronological feed of all events (files, builds, meetings, AI, clipboard) with EEG focus indicators. - -### UI - -- **Brain status bar**: persistent bottom bar in Tauri app showing flow state, fatigue, streak, edit velocity, and focus score. -- **Brain dashboard card**: flow/fatigue/streak + focus progress bar on the EEG dashboard alongside existing BrainStateScores. -- **Brain indicators in ActivityTab**: flow state card (green), fatigue alert card (amber), and streak card (violet) at top. - -### i18n - -- Activity dashboard, clipboard, file history, session replay, weekly report, brain status, timeline, and code-brain correlation translations for all 9 locales. diff --git a/changes/unreleased/feat-brain-awareness.md b/changes/unreleased/feat-brain-awareness.md deleted file mode 100644 index 522f25c4..00000000 --- a/changes/unreleased/feat-brain-awareness.md +++ /dev/null @@ -1,12 +0,0 @@ -### Features - -- **Brain awareness API**: 14 endpoints under `/v1/brain/*` — flow state, cognitive load, meeting recovery, optimal hours, fatigue check, undo struggle, daily brain report, break timing, deep work streak, task type detection, struggle prediction, interruption recovery, code-EEG correlation, and unified timeline. -- **Flow state detector**: real-time check if user is in deep focus (high focus + low switches + sustained editing). -- **Task type detection**: auto-classify coding/debugging/reviewing/refactoring/testing from activity patterns. -- **Struggle prediction**: fuse undo rate + velocity drop + focus decline to predict when user is stuck. Includes actionable suggestion. -- **Interruption recovery measurement**: measure actual focus recovery time per interruption source (Slack avg 12min, Zoom avg 23min, etc.). -- **Fatigue monitor**: 15-minute background check broadcasts `fatigue-alert` and sends OS notification when focus declines. -- **Daily brain report**: 6pm OS notification with morning/afternoon/evening brain summary. -- **Weekly digest notification**: Monday 9am OS notification with weekly summary. ISO week dedup prevents re-fire on daemon restart. -- **Break timing optimizer**: detect natural focus cycle length from 5-minute EEG buckets. -- **Deep work streak**: gamified consecutive days with 60+ minutes of deep work. diff --git a/changes/unreleased/feat-brain-insights.md b/changes/unreleased/feat-brain-insights.md deleted file mode 100644 index 991c09f3..00000000 --- a/changes/unreleased/feat-brain-insights.md +++ /dev/null @@ -1,24 +0,0 @@ -### Features - -- **Developer insights endpoint**: `POST /v1/brain/developer-insights` returns 7 actionable EEG-fused insights in one call. -- **Test failure by focus level**: correlates build/test exit codes with EEG focus (high/mid/low) — shows how focus level predicts test outcomes. -- **Hourly productivity**: churn volume + undo rate + avg EEG focus by hour of day — identifies peak and trough hours. -- **Context switch recovery**: EEG focus level at each editor/terminal/panel transition — measures the cognitive cost of switching. -- **AI tool impact on focus**: per-app (Claude, Pi) focus delta vs baseline — quantifies whether AI tools help or hurt flow state. -- **Focus by language**: avg EEG focus + undo rate per programming language — identifies which languages are cognitively easiest/hardest. -- **Dev loop efficiency by hour**: build/test cycle time + pass rate by time of day — shows when edit-test loops are fastest. -- **Tool focus impact**: avg EEG focus per command category (docker, git, deploy, etc.) — reveals which tools correlate with lowest focus. -- **EEG timeseries table**: periodic JSON snapshots of all brain metrics (every 5s during recording). Extensible — new metrics without schema changes. Any event correlatable by timestamp join. -- **EEG timeseries worker**: background thread writes full band powers to `eeg_timeseries` every 5s when an EEG session is active. -- **`POST /v1/brain/eeg-at`**: get EEG metrics at a specific timestamp (nearest sample). -- **`POST /v1/brain/eeg-range`**: get EEG time-series in a range for charts and correlation analysis. -- **Window focus tracking**: `window_focus` events sent when VS Code gains/loses focus. EEG not attributed to coding when VS Code is in background. -- **Live EEG attachment**: `latest_bands` focus/mood injected at event storage time for terminal commands, zone switches, and conversations. - -### UI - -- **Tauri Brain Insights section** in Activity tab: test failure rates by focus level, focus by language bars, AI impact delta, tool focus grid, hourly productivity chart. -- **Tauri EEG focus inline**: terminal commands and conversation messages show EEG focus score (green/yellow/red) at the moment they occurred. -- **VS Code Brain Insights card**: test failure by focus chips, focus by language bars, tool impact chips. Only shown when EEG data is available. -- **VS Code Struggle card**: hidden when no EEG recording (focus is EEG-only, not activity-based). -- **VS Code Flow gauge label**: shows "focus" with EEG, "activity" without — no false claim of brain measurement. diff --git a/changes/unreleased/feat-burn-mlx.md b/changes/unreleased/feat-burn-mlx.md deleted file mode 100644 index f4d152ac..00000000 --- a/changes/unreleased/feat-burn-mlx.md +++ /dev/null @@ -1,9 +0,0 @@ -### Dependencies - -- **burn-mlx**: added `burn-mlx` from git (`eidola-ai/burn-mlx`, branch `burn-0-20`) as optional dependency in `skill-router` and `skill-daemon`. Workspace `[patch.crates-io]` redirects all `burn-mlx` references to the git version (crates.io 0.1.2 targets burn 0.16; project uses burn 0.20). -- **macOS deployment target**: bumped from 10.15 to 14.0 in `.cargo/config.toml` for Metal 3.0 `simdgroup_matrix` intrinsics required by MLX. Still satisfies llama.cpp's `std::filesystem` (available since 10.15). -- **half 2.4**: added to `skill-router` for GPU f16 precision backend type (`CubeBackend`). - -### Features - -- **`mlx-e2e` test suite**: new suite in `scripts/test-all.sh` running UMAP and FFT e2e tests with MLX features. Auto-skips on non-macOS. Available via `npm run test:mlx-e2e`. diff --git a/changes/unreleased/feat-conversations-search.md b/changes/unreleased/feat-conversations-search.md deleted file mode 100644 index de4ad4ea..00000000 --- a/changes/unreleased/feat-conversations-search.md +++ /dev/null @@ -1,18 +0,0 @@ -### Features - -- **Conversations table**: stores all AI coding assistant messages (Claude, Pi) with app, role (user/assistant/tool), text, cwd, timestamp, session ID, EEG focus/mood. -- **FTS5 full-text search**: SQLite virtual table for instant keyword search across all conversation text. `POST /v1/brain/search-conversations {"query":"JWT","mode":"fts"}`. -- **Fuzzy search**: LIKE-based substring matching for partial queries. `{"query":"rate limit","mode":"fuzzy"}`. -- **Structured search**: filter by app, role, time range. `{"mode":"structured","app":"claude","role":"user","since":...}`. -- **Semantic search**: user prompts embedded via fastembed (nomic-embed-text-v1.5, local, no API credits) and stored in HNSW label index. Searchable by meaning, not just keywords. -- **Generic embedding store**: `embeddings` table decoupled from specific data tables. Multi-model support — can re-embed with different models, store multiple vectors per item. Source tracking (source_type, source_id). -- **Code context HNSW index**: separate `code_context_index.hnsw` file for code-specific semantic search, keeping EEG label searches uncontaminated. -- **Conversation events**: VS Code extension sends `conversation_message` events to daemon for each Claude/Pi message. User prompts get embedded; assistant responses and tool calls get FTS-indexed only (saves compute). -- **Session tracking**: messages grouped by JSONL filename (session ID). Timestamps from app's own data (ISO 8601 UTC), not extension read time. -- **Embedding settings**: `neuroskill.embedding.maxInputLength` (default 1000 chars) and `neuroskill.embedding.enableConversations` (default true) in VS Code settings. - -### UI - -- **Tauri AI Conversations section** in Activity tab: timestamped message thread with role icons (user/assistant/tool), app name, EEG focus score, scrollable. -- **VS Code Conversations card**: timestamped messages with role icons, app badge, collapsible. -- **VS Code AI Usage card**: Copilot/Codeium acceptance rate bar, suggestion count, source breakdown chips. diff --git a/changes/unreleased/feat-data-collection.md b/changes/unreleased/feat-data-collection.md deleted file mode 100644 index 607e5934..00000000 --- a/changes/unreleased/feat-data-collection.md +++ /dev/null @@ -1,12 +0,0 @@ -### Features - -- **Meeting detection**: automatically detect Zoom, Teams, Slack, Google Meet, FaceTime, Discord, and Webex meetings from window titles. Track start/end times in `meeting_events` table. -- **Browser tab extraction**: extract page titles from Chrome, Safari, Firefox, Edge, Brave, Arc, Opera, Vivaldi, Chromium. Stored in `browser_title` column on `active_windows`. -- **Clipboard monitoring**: opt-in macOS clipboard change tracking (metadata only — content never stored). Records source app, content type, and size. Includes Automation permission check and settings UI. -- **Multi-monitor awareness**: track windows on secondary monitors across macOS (AppleScript), Linux (wmctrl), Windows (EnumWindows). Dynamic primary screen resolution detection. -- **Undo frequency tracking**: detect undo/redo from file edit diffs via reversal heuristic. `undo_estimate` per 5-second chunk, `undo_count` per file interaction. - -### Bugfixes - -- **Schema migration**: ALTER TABLE for existing databases missing columns added across releases. Runs before DDL to handle all legacy schemas. -- **Retention pruning**: meetings, clipboard, and secondary_windows pruned alongside file interactions during hourly maintenance. diff --git a/changes/unreleased/feat-dependabot-fixes.md b/changes/unreleased/feat-dependabot-fixes.md deleted file mode 100644 index 6c79a615..00000000 --- a/changes/unreleased/feat-dependabot-fixes.md +++ /dev/null @@ -1,11 +0,0 @@ -### Dependencies - -- **Fix rand 0.7.3 vulnerability**: vendored `phf_generator 0.8.0` with rand bumped from 0.7 to 0.8, eliminating the vulnerable transitive dependency. -- **Remove atty**: migrated `iroh_test_client` from `structopt` (clap v2) to `clap v4` derive, dropping the unmaintained `atty` crate. -- **Update llama-cpp-4 to 0.2.50**: upstream llama.cpp fixes including `common_*` symbol renames. -- **Update kittentts to 0.4.1**: TTS engine update. -- **Add grayscale 0.0.1**: macOS grayscale display control for DND mode. - -### Bugfixes - -- **Dismiss stale Dependabot alerts**: crossbeam-deque, crossbeam-utils, crossbeam-queue, memoffset, and glib alerts dismissed (already at patched versions or fixed in fork). diff --git a/changes/unreleased/feat-design-system.md b/changes/unreleased/feat-design-system.md deleted file mode 100644 index ae6b1838..00000000 --- a/changes/unreleased/feat-design-system.md +++ /dev/null @@ -1,19 +0,0 @@ -### Features - -- **Design tokens** (`webview-ui/src/lib/tokens.css`): 30+ CSS custom properties organized into core palette (5 colors), surfaces (4 levels), backgrounds (14 translucent tints), borders (3 colors), semantic aliases (7), and typography (3). Zero inline `rgba()` in App.svelte. -- **Reusable Svelte components** (`webview-ui/src/lib/`): - - `Card` — wrapper with title, info button, info panel, header-right slot, variant borders (warn/danger/info) - - `MetricRow` — label + value pair with variant colors (warn/accent/dim/good/bad) - - `Chevron` — collapsible section with chevron toggle, count badge, slot content - - `ProgressBar` — horizontal bar with value/max, 5 color variants, optional label - - `Gauge` — circular SVG ring with animated fill, value, label - - `Badge` — text badge with 7 variants (default/good/warn/bad/blue/live/score-circle/si) - - `Callout` — alert box with 3 variants (warn/danger/info) -- **Component index** (`webview-ui/src/lib/index.ts`): barrel export for all components. -- **Timestamp compliance**: 25 automated tests (`src/test-timestamps.ts`) verifying: - - No `toLocaleTimeString`/`toLocaleDateString` in data layer (activity-tracker, events, brain, config, vt-parser) - - `toLocaleTimeString` used in UI layer (App.svelte) for display - - `Date.now()` returns UTC milliseconds - - ISO 8601 strings parsed to UTC millis - - No hardcoded timezone offsets in data layer - - All stored timestamps are UTC; local conversion only at UI boundary diff --git a/changes/unreleased/feat-dev-loops.md b/changes/unreleased/feat-dev-loops.md deleted file mode 100644 index 7bcb5493..00000000 --- a/changes/unreleased/feat-dev-loops.md +++ /dev/null @@ -1,10 +0,0 @@ -### Features - -- **Dev loop detection**: identifies edit-build-test cycles from terminal command history. Groups consecutive runs of the same build/test command into loops. -- **`dev_loops` table**: loop type, command, iteration count, pass/fail counts, avg cycle time, fastest/slowest cycle, EEG focus start/end, focus trend (rising/falling/stable). -- **`POST /v1/brain/dev-loops`**: returns detected loops for a time window with all metrics. -- **Enhanced `predict_struggle()`**: terminal failures (+8/fail, max +40) and re-running same failing command 3+ times (+15/rerun, max +30) boost struggle score. New suggestions: "You're re-running the same failing command", "Multiple failures — read the error messages." -- **Enhanced `detect_task_type()`**: terminal command categories override heuristics with higher confidence — docker/kubectl → infrastructure (0.8), deploy → deploying (0.85), git dominating → git_management (0.7), test → testing (0.85), debug → debugging (0.9). -- **Dev loops in sidebar**: command, iteration count, pass rate, avg cycle time, focus trend arrow. Failing loops get red left border. -- **Dev loops in Tauri Activity tab**: expanded view with pass rate percentage, cycle time, focus trend. -- **Loop efficiency by hour**: insight showing build/test cycle time + pass rate by time of day. diff --git a/changes/unreleased/feat-exg-inference-backend.md b/changes/unreleased/feat-exg-inference-backend.md deleted file mode 100644 index 2ac7759d..00000000 --- a/changes/unreleased/feat-exg-inference-backend.md +++ /dev/null @@ -1,9 +0,0 @@ -### Features - -- **Inference backend selector**: `exg_inference_device` setting expanded from `gpu | cpu` to `auto | mlx | gpu | cpu`. Default changed from `gpu` to `auto` (picks MLX on macOS, GPU elsewhere). -- **Four-button selector in EXG Settings**: Auto, MLX, GPU, CPU — each with localized label and description. Reconnect headset hint shown after change. -- **`embed-exg-mlx` feature**: new daemon Cargo feature that enables `skill-router/mlx`, `skill-eeg/mlx`, and `embed-zuna-mlx` together. - -### i18n - -- Added `settings.inferenceDeviceAuto`, `settings.inferenceDeviceAutoDesc`, `settings.inferenceDeviceMlx`, `settings.inferenceDeviceMlxDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). diff --git a/changes/unreleased/feat-fast-umap-1.6.md b/changes/unreleased/feat-fast-umap-1.6.md deleted file mode 100644 index 7c5ef60b..00000000 --- a/changes/unreleased/feat-fast-umap-1.6.md +++ /dev/null @@ -1,24 +0,0 @@ -### Features - -- **fast-umap 1.6.0**: updated from 1.5.1 with MLX, PCA, and nearest-neighbor descent support. -- **MLX UMAP backend**: Apple Silicon native UMAP projection via `burn-mlx`. Runtime dispatch between MLX and GPU (wgpu) based on user preference. Auto defaults to MLX on macOS, GPU elsewhere. -- **Precision selector**: F32 / F16 precision for the GPU (wgpu) backend. MLX is F32-only (fast-umap trait constraint). Exposed in UMAP settings as chip group. -- **Backend & precision UI**: new "Compute Backend" section in UMAP settings with Auto / MLX / GPU chips and F32 / F16 precision chips. Pipeline summary badge shows active backend and precision. - -### Performance - -- **MLX vs GPU benchmarks** (Mac mini, Apple Silicon): - -| Dataset | Points | GPU (wgpu) | MLX | Speedup | -|---|---|---|---|---| -| Small | 200 | 120.9 s | 2.3 s | **51x** | -| Medium | 1,000 | 136.6 s | 7.1 s | **19x** | -| Large | 5,000 | 152.8 s | 23.8 s | **6.4x** | - -### Features - -- **UMAP e2e benchmarks**: `umap_e2e_small` (200 pts), `umap_e2e_medium` (1K pts), `umap_e2e_large` (5K pts) with synthetic 32-dim EEG embeddings. Reports backend, timing, throughput (pts/sec), and separation score. - -### i18n - -- Added `umapSettings.backend`, `umapSettings.backendDesc`, `umapSettings.precision`, `umapSettings.precisionDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). diff --git a/changes/unreleased/feat-gpu-fft-1.2.md b/changes/unreleased/feat-gpu-fft-1.2.md deleted file mode 100644 index c6f6a6d8..00000000 --- a/changes/unreleased/feat-gpu-fft-1.2.md +++ /dev/null @@ -1,8 +0,0 @@ -### Features - -- **gpu-fft 1.2.0**: updated from 1.1.1 with MLX FFT backend. EEG signal filtering (overlap-save convolution) uses MLX when `skill-eeg/mlx` is enabled. ~3.7x faster than wgpu at N=65536. -- **`skill-eeg/mlx` feature**: new feature gate that enables `gpu-fft/mlx` alongside `gpu-fft/wgpu`. Wired into `embed-exg-mlx` daemon feature. - -### Features - -- **FFT MLX e2e**: `fft_e2e_roundtrip_256`, `fft_e2e_batch_4ch`, `fft_e2e_psd_peak_detection`, `fft_e2e_large_batch` (32 channels x 1024 samples). Validates round-trip accuracy, PSD peak detection, and batch throughput. diff --git a/changes/unreleased/feat-grayscale-dnd.md b/changes/unreleased/feat-grayscale-dnd.md deleted file mode 100644 index 0c26d0cf..00000000 --- a/changes/unreleased/feat-grayscale-dnd.md +++ /dev/null @@ -1,7 +0,0 @@ -### Features - -- **Grayscale display mode**: optional system-wide grayscale that activates/deactivates in sync with Do Not Disturb. Reduces visual distraction during deep work. macOS only, default off. - -### i18n - -- Added `dnd.grayscale` and `dnd.grayscaleDesc` translations to all 9 locales. diff --git a/changes/unreleased/feat-history-search-integration.md b/changes/unreleased/feat-history-search-integration.md deleted file mode 100644 index d41f7a37..00000000 --- a/changes/unreleased/feat-history-search-integration.md +++ /dev/null @@ -1,5 +0,0 @@ -### Features - -- **File activity in history sessions**: expanded EEG sessions show files worked on during the session with focus indicators, edit counts (+/-), language, and meeting interruption badges. -- **File activity in interactive search**: `file_activity` nodes in the 3D search graph linked to EEG epochs and text labels, showing which files were being edited during matching brain states. -- **Meeting nodes in search graph**: detected meetings appear alongside EEG results in the interactive search as amber nodes with temporal proximity edges. diff --git a/changes/unreleased/feat-neuroskill-cli.md b/changes/unreleased/feat-neuroskill-cli.md deleted file mode 100644 index 23eec985..00000000 --- a/changes/unreleased/feat-neuroskill-cli.md +++ /dev/null @@ -1,5 +0,0 @@ -### CLI - -- **`neuroskill activity` expanded**: 19 subcommands — bands, window, input, windows, files, top-files, top-projects, languages, sessions, meetings, clipboard, builds, heatmap, switches, summary, score, digest, stale, timeline. -- **`neuroskill brain` added**: 14 subcommands — flow, load, load-files, recovery, optimal, fatigue, struggle, report, breaks, streak, task, stuck, interruptions, correlation. -- **`neuroskill vscode` added**: auto-build and install VS Code/VSCodium/Cursor extension from source on macOS, Linux, and Windows. diff --git a/changes/unreleased/feat-search-ui-redesign.md b/changes/unreleased/feat-search-ui-redesign.md deleted file mode 100644 index 6a3104d6..00000000 --- a/changes/unreleased/feat-search-ui-redesign.md +++ /dev/null @@ -1,11 +0,0 @@ -### UI - -- **Search mode dropdown**: replaced 4-tab bar with a styled dropdown supporting 7 search modes — Interactive, EEG, Text, Images, Code, Meetings, Brain. -- **Diamond EEG nodes**: EEG epochs rendered as faceted octahedrons (diamonds) in the 3D search graph. Meeting nodes rendered as tetrahedrons (pyramids). Visual distinction by data type. -- **New search modes**: Code (file activity + brain state), Meetings (meeting events + recovery), Brain (flow/fatigue/struggle insights). -- **Terminal commands in Cmd-K**: added 4 terminal commands to the command palette — Recent Terminal Commands, Terminal Focus Impact, Context Switch Cost, Dev Loops. Translated labels in all 9 locales. - -### i18n - -- Search mode labels (Code, Meetings, Brain) translated in all 9 locales. -- Terminal command palette entries translated in all 9 locales. diff --git a/changes/unreleased/feat-sidebar-cards.md b/changes/unreleased/feat-sidebar-cards.md deleted file mode 100644 index ee0c48e0..00000000 --- a/changes/unreleased/feat-sidebar-cards.md +++ /dev/null @@ -1,30 +0,0 @@ -### Features - -- **Git context card**: current branch (monospace), dirty/staged file count, ahead/behind indicators. Uses VS Code git extension API. -- **Session timeline card**: horizontal bar chart showing activity events by hour of day. Color-coded by volume. -- **Workspace activity card**: per-file edits, lines added/removed, focus time. Grouped by workspace folder (project), sorted by activity. Active file indicator (blue dot). Top 10 files per project. -- **Environment card**: editor/terminal/panel time split (colored stacked bar), tab count, editor groups, terminal count. Per-terminal cards with shell type, CWD, PID, shell integration status. -- **Terminal I/O sections**: collapsible with chevrons, minimized by default. Input shows timestamped commands with CWD. Output shows VT-parsed session content. -- **Terminal input card**: keystroke intensity per program (keys/min), duration, collapsible behind chevron. -- **Terminal impact card**: EEG focus delta by command category with pass rate percentage. -- **Context switch cost card**: focus level at each zone transition type with switch count. -- **Dev loops card**: edit-build-test cycles with iteration count, pass/fail rate, cycle time, focus trend (rising/falling/stable arrow). -- **Today's report card**: productivity score, morning/afternoon/evening period breakdown with focus bars and churn. -- **Energy card**: fatigue bar (inverse of focus decline), continuous work time, streak badge. -- **Struggle card**: EEG-only (hidden without recording), score 0-100, contributing factors. -- **Optimal hours card**: peak/avoid hours grid. -- **AI usage card**: acceptance rate bar, suggestion count, source breakdown. -- **Today vs yesterday card**: files and churn comparison with directional arrows. -- **Code review detection**: auto-detected when file switches > 5 and edit velocity < 1 line/min. -- **Process monitor card**: running dev servers (vite, next, webpack, cargo, node, python, docker, postgres) with ports and PIDs. -- **Info toggles**: every card has a `?` button explaining how metrics are calculated. -- **Dual-speed updates**: local data every 5s (visible), brain data every 30s. Pauses when sidebar hidden. - -### UI - -- **Configurable alerts**: `neuroskill.alerts.focusLow` (warn when EEG focus drops below threshold), `neuroskill.alerts.continuousWorkMins` (warn after N minutes continuous work). -- **Daily digest notification**: at configurable time (default 17:30), shows productivity score + stats. "Full Report" button opens detail panel. -- **Full report panel**: `Cmd+Shift+R` opens full-width webview with same data as sidebar. -- **Keyboard shortcuts**: `Cmd+Shift+N` (toggle sidebar), `Cmd+Shift+R` (full report), `Cmd+Shift+Alt+N` (reconnect). -- **Open NeuroSkill button**: launches native app (cross-platform). -- **Event caching**: offline-resilient `pendingEvents` array (10K cap) persisted to disk. Auto-flushes on reconnect. diff --git a/changes/unreleased/feat-terminal-tracking.md b/changes/unreleased/feat-terminal-tracking.md deleted file mode 100644 index 9e7e3b51..00000000 --- a/changes/unreleased/feat-terminal-tracking.md +++ /dev/null @@ -1,24 +0,0 @@ -### Features - -- **OS-wide shell hooks**: preexec/precmd hooks for zsh, bash, fish, PowerShell. Background curl sends every command to daemon — zero delay to prompt. Self-contained scripts generated by daemon, stored in `~/.skill/shell-hooks/`. -- **Shell hook management**: `GET /v1/activity/shell-hook?shell=zsh` returns hook script, `POST /v1/activity/install-shell-hook` writes to rc file, `POST /v1/activity/uninstall-shell-hook` removes cleanly, `POST /v1/activity/shell-hook-status` health check per shell. -- **Terminal command table**: `terminal_commands` with command text, binary name, args, cwd, exit code, duration, auto-categorized (284 CLI tools across 18 categories), EEG focus at start + end for delta calculation. -- **Binary extraction**: raw binary name stored separately from args. Lazy categorization — `POST /v1/brain/recategorize-commands` reruns rules on all historical data. `POST /v1/brain/binary-stats` discovers uncategorized tools by frequency. -- **284 CLI tools recognized**: git, gh, glab, hf, docker, kubectl, helm, aws, gcloud, az, brew, pip, cargo, npm, psql, redis-cli, ollama, claude, pi, vim, tmux, htop, terraform, and 260+ more across 18 categories (build, test, run, git, docker, deploy, install, navigate, debug, network, database, ai, editor, multiplexer, monitor, env, system, other). -- **Script session recording**: `script` PTY proxy wraps shell sessions, captures all terminal I/O (including input inside interactive programs). Logs stored in `~/.skill/terminal-logs/` with auto-rotation. -- **VT100 terminal emulator**: `vt-parser.ts` replays script recordings through a virtual terminal with scrollback capture. Handles cursor movement, screen clears, alternate screen buffer. Separates input from output via prompt pattern detection. -- **App-specific JSONL parsing**: reads Claude Code (`~/.claude/projects/`) and Pi agent (`~/.pi/agent/sessions/`) conversation files directly. Extracts user prompts with real timestamps, assistant responses, and tool calls. Supports multiple parallel sessions. -- **Readline history polling**: monitors `~/.python_history`, `~/.node_repl_history`, `~/.irb_history`, `~/.psql_history` for REPL input. -- **`neuroskill terminal` CLI**: `status` (hook health per shell), `install [shell]`, `uninstall [shell]`, `commands` (recent tracked), `impact` (focus delta by category), `loops` (dev loop detection). -- **Tauri Terminal settings tab**: per-shell install/uninstall/repair buttons with health indicators (green/yellow/red dot), recent commands preview, "How it works" documentation. - -### Server - -- **`POST /v1/activity/shell-command`**: receives commands from shell hooks with command text, cwd, shell type, exit code. -- **`POST /v1/brain/terminal-commands`**: query recent commands with exit codes, durations, categories, EEG correlation. -- **`POST /v1/brain/terminal-impact`**: avg EEG focus delta by command category. -- **`POST /v1/brain/binary-stats`**: usage frequency per binary for discovering uncategorized tools. -- **`POST /v1/brain/recategorize-commands`**: rerun categorization rules on all historical commands. -- **Terminal EEG auto-labels**: `"running: cargo test"` on start, `"cargo test passed"` / `"cargo test failed (exit 1)"` on end. -- **Zone switch events**: editor/terminal/panel transitions stored with EEG focus snapshot. `POST /v1/brain/context-cost` returns focus at each transition type. -- **Layout snapshots**: periodic (60s) tab/group/terminal counts from VS Code. diff --git a/changes/unreleased/feat-validation-daemon.md b/changes/unreleased/feat-validation-daemon.md deleted file mode 100644 index c32daedf..00000000 --- a/changes/unreleased/feat-validation-daemon.md +++ /dev/null @@ -1,16 +0,0 @@ -### Features - -- **Validation / fatigue-research daemon backend**: foundations for calibrating the Break Coach and Focus Score against four external instruments — KSS (Karolinska Sleepiness Scale), NASA-TLX (workload), PVT (Psychomotor Vigilance Task), and an EEG-derived fatigue index per Jap et al. 2009. -- **`skill-data::validation_store`** — new SQLite store at `~/.skill/validation.sqlite` with five tables (`config`, `kss_responses`, `tlx_responses`, `pvt_runs`, `prompt_log`). Persistent config is a single-row JSON blob with serde defaults so adding a new channel later is a non-migration. New constant `VALIDATION_FILE` in `skill-constants`. -- **EEG fatigue index**: pure function `eeg_fatigue_index(bands) → Option` computing `(α + θ) / β` over the existing band-power snapshot. Guards against missing bands and zero-β. Passive; ships **on** because it costs nothing when no headset is attached. -- **Pure scheduler `decide_prompt(ctx, ...) → PromptDecision`**: respects the `respect_flow` master gate, configurable quiet hours, per-channel daily caps, runtime snoozes, and rate limits. Channel ordering: KSS first (lightest), TLX after a long task unit, PVT on a weekly cadence. Live `read_in_flow` and `read_break_coach_active` open the activity store read-only and ask `flow_state_now(300).in_flow` and `fatigue_check().fatigued` so the gates actually mean something. -- **Sane defaults — opt-in everywhere**: KSS, TLX, PVT all ship `enabled = false`. Only the passive EEG fatigue index ships on. The `respect_flow` master gate ships on. - -### Server - -- **`/v1/validation/*` HTTP endpoints** (axum): `GET /config`, `PATCH /config` (recursive JSON merge for partial updates), `POST /snooze`, `POST /disable-today`, `GET /should-prompt` (returns the daemon's prompt decision; logs the fire so the rate-limiter sees it), `POST /kss`, `POST /tlx`, `POST /pvt`, `POST /close-prompt`, `GET /results`, `GET /fatigue-index` (live `(α+θ)/β` from the latest band snapshot). -- **`AppState.validation_runtime`** (`crates/skill-daemon-state/src/state.rs`): new `Arc>` field holding ephemeral snooze/disable-today state. Resets on daemon restart by design — a crash loop shouldn't keep prompts suppressed forever. - -### Bugfixes - -- **CORS allow-methods now includes `PATCH`**: `crates/skill-daemon/src/main.rs` was advertising only `GET, POST, PUT, DELETE, OPTIONS`, so the browser preflight blocked `PATCH /v1/validation/config` from the Tauri webview. Added `Method::PATCH` to the list. diff --git a/changes/unreleased/feat-validation-tauri-ui.md b/changes/unreleased/feat-validation-tauri-ui.md deleted file mode 100644 index b5be6ae7..00000000 --- a/changes/unreleased/feat-validation-tauri-ui.md +++ /dev/null @@ -1,18 +0,0 @@ -### Features - -- **Validation & Research settings tab** (`src/lib/settings/ValidationTab.svelte`): new top-level settings tab gathering five `SettingsCard` blocks — global gates (`respect_flow`, quiet hours), KSS, NASA-TLX, PVT, and EEG fatigue index — plus a Calibration Week button and a recent-results card. - - Every toggle / numeric input PATCHes one nested field via `daemonPatch("/v1/validation/config", …)`; the daemon's recursive JSON merge keeps the rest of the config untouched. - - EEG fatigue card shows the live `(α + θ) / β` value polled every 5 s from `/v1/validation/fatigue-index`. - - Calibration Week: single button that batch-updates all four channels to higher-frequency presets (KSS 8/day, TLX after every flow block ≥ 20 min, PVT mid-week) in one PATCH. -- **PVT panel** (`src/lib/settings/PvtPanel.svelte`): full 3-minute Psychomotor Vigilance Task. Random ITIs (2–10 s), green-dot stimulus, `performance.now()` for sub-millisecond RT measurement, tracks responses + false starts. On finish computes mean RT, median RT, slowest-10% mean RT (anticipation-resistant), and lapse count (RT > 500 ms per Dinges & Powell 1985), POSTs to `/v1/validation/pvt`. State machine: intro → running → done. -- **NASA-TLX form** (`src/lib/settings/TlxForm.svelte`): six-slider modal for the raw (un-weighted) NASA-TLX. Performance scale uses inverted endpoints ("Failure" → "Perfect") per Hart 2006. POSTs to `/v1/validation/tlx` with `task_kind`, `task_duration_secs`, optional `prompt_id` echo-back. -- **Pure stats helper** (`src/lib/settings/pvt-stats.ts`): extracted `mean`, `median`, `slowest10Mean`, `lapseCount`, `computeStats` from the PVT panel so the math is unit-testable without a Svelte renderer. -- **`daemonPatch(path, body)`** in `src/lib/daemon/http.ts` — needed for the validation config endpoint. - -### UI - -- **Spacing pass on the validation tab and modals**: every card uses `flex flex-col gap-5 p-6` instead of the old `space-y-4` (which provided no padding override). Conditional sub-settings under a channel toggle are now indented under a left border (`ml-1 border-l-2 border-border pl-5`) so the parent/child relationship reads visually. Modals padded to `p-8` with `gap-3` button rows. TLX sliders gained `mt-1` lift off the description and `uppercase tracking-wide` low/high legend labels. - -### i18n - -- **New `validation` namespace** in all 9 languages (`src/lib/i18n/{de,en,es,fr,he,ja,ko,uk,zh}/validation.ts`): ~95 keys in English (full coverage), ~55 in each other language for the user-visible strings (long-tail descriptions fall back via the runtime `t()` chain). `index.ts` barrel exports updated for every language. diff --git a/changes/unreleased/feat-validation-tests.md b/changes/unreleased/feat-validation-tests.md deleted file mode 100644 index faeaccb2..00000000 --- a/changes/unreleased/feat-validation-tests.md +++ /dev/null @@ -1,11 +0,0 @@ -### Features - -- **Test coverage for the validation feature across all three layers** — 72 tests total, all passing. - - **Rust unit tests** (`crates/skill-data/src/validation_store.rs`): 19 tests covering config persistence, store round-trips, the `(α + θ) / β` fatigue index (Jap et al. 2009) including zero-β and missing-band edge cases, every scheduler branch (flow respect, quiet hours, snooze blocking, KSS rate limit, TLX trigger after long task, TLX skip on short task, PVT weekly cadence, PVT silence after recent run, quiet-window-equals-end semantics), and prompt-log queries. - - **Rust HTTP integration tests** (`crates/skill-daemon/src/routes/validation.rs`): 10 tests using `tower::ServiceExt::oneshot()` against a tempdir-backed `AppState` — `GET /config` defaults, `PATCH /config` partial-update + round-trip, `POST /kss` happy path, KSS score-out-of-range → 400, TLX subscale-out-of-range → 400, `GET /should-prompt` returns `{kind: "none"}` with default config, `POST /snooze` envelope, plus three `merge_json` tests covering deep nesting. - - **Vitest — PVT statistics** (`src/tests/pvt-stats.test.ts`, 12 tests): edge cases for `mean`, `median`, `slowest10Mean` (including the n<10 fallback), `lapseCount` with the 500 ms Dinges & Powell threshold and explicit-threshold override, plus a known-fixture `computeStats` round-trip. - - **Vitest — i18n coverage** (`src/tests/validation-i18n.test.ts`, 23 tests): every VS Code l10n bundle exists and contains every user-facing key (KSS prompt + scores 1/5/9, TLX/PVT prompts, all four escape-hatch labels); every Tauri language `validation.ts` imports cleanly; English Tauri bundle covers every required key; every non-English Tauri bundle has at least the tab name and disclaimer translated. - -### Refactor - -- **Sidebar disclaimer test now accepts either localiser**: existing `vscode-sidebar-disclaimer.test.ts` updated so the `vscode.l10n.t("sidebar.disclaimer")` ↔ `tr("sidebar.disclaimer")` migration doesn't break the assertion. Both look up the same key. diff --git a/changes/unreleased/feat-validation-vscode-extension.md b/changes/unreleased/feat-validation-vscode-extension.md deleted file mode 100644 index ae058eb6..00000000 --- a/changes/unreleased/feat-validation-vscode-extension.md +++ /dev/null @@ -1,13 +0,0 @@ -### Features - -- **VS Code extension joins the validation prompt loop**: new `ValidationManager` (`extensions/vscode/src/validation.ts`) polls `/v1/validation/should-prompt` every 90 s and renders whichever channel the daemon decides to fire. - - **KSS** (1–9 sleepiness) — full QuickPick with Karolinska wording, plus Snooze 30m / Don't ask today / Stop these prompts escape hatches in-line. POSTs the answer to `/v1/validation/kss` echoing the daemon's `prompt_id` so the prompt log can mark it answered. - - **NASA-TLX** — fallback path: shows an information message offering to open the form in the Tauri app (deep link `neuroskill://validation/tlx`) plus the same escape hatches. - - **PVT** — weekly nudge: offers to deep-link into the Tauri PVT panel (`neuroskill://validation/pvt`) or skip a week (snoozes for 6 days so the next reminder fires on cadence). -- **Two new commands**: `NeuroSkill: Open Validation Settings…` (deep links into the Tauri preferences pane) and `NeuroSkill: Check for Validation Prompt Now` (forces a single scheduler poll — useful for opt-in onboarding). -- **`DaemonClient.patch()`**: extension's daemon client gained a PATCH method so the "Stop these prompts" escape hatch can flip `enabled = false` on the persistent config. - -### i18n - -- **22 new `validation.*` keys per bundle** in all 9 languages (`bundle.l10n.{de,en,es,fr,he,ja,ko,uk,zh-cn}.json`): KSS prompt + 9 score labels with translated Karolinska wording, TLX/PVT prompts, all four escape-hatch labels, the disabled-permanently acknowledgement. -- **Two new `cmd.*` keys** for the validation commands in `package.nls.json`. diff --git a/changes/unreleased/feat-vscode-extension-ci.md b/changes/unreleased/feat-vscode-extension-ci.md deleted file mode 100644 index 0efee66b..00000000 --- a/changes/unreleased/feat-vscode-extension-ci.md +++ /dev/null @@ -1,7 +0,0 @@ -### Build - -- **CI for the VS Code extension** (`extensions/vscode/.github/workflows/`): two workflows live in the submodule repo (`vscode-neuroskill`). - - **`ci.yml`** — runs on every PR and `main` push. `npm ci` → `tsc` build → `vsce package` → uploads the `.vsix` as a 14-day artifact. No secrets, no publish. - - **`release.yml`** — fires two ways. *Tag push* (`git push --tags` after `npm version patch`) verifies the tag matches `package.json` and publishes. *Manual dispatch* with a `patch | minor | major | x.y.z` input bumps `package.json`, commits, tags, pushes, then publishes. Both paths run `npx @vscode/vsce publish` (uses `VSCE_PAT`) and `npx ovsx publish` (uses `OVSX_PAT`), then create a GitHub release with the `.vsix` attached and the latest `## ` block from `CHANGELOG.md` as release notes. -- **Skip-on-missing-secret**: if either `VSCE_PAT` or `OVSX_PAT` is unset, the corresponding publish step emits a `::warning::` and is skipped — letting you wire up Open VSX later without breaking the workflow. -- **Releasing section in the extension README**: documents the secret setup (Azure DevOps PAT, Open VSX token), the two release flows, and the one-time namespace claims (Marketplace publisher registration + `ovsx create-namespace`). diff --git a/changes/unreleased/feat-vscode-extension.md b/changes/unreleased/feat-vscode-extension.md deleted file mode 100644 index dedc2f14..00000000 --- a/changes/unreleased/feat-vscode-extension.md +++ /dev/null @@ -1,84 +0,0 @@ -### Features - -- **VS Code extension**: separate repo (`NeuroSkill-com/vscode-neuroskill`) as git submodule at `extensions/vscode/`. 50+ tracked event types across editing, navigation, debugging, git, AI, terminal, clipboard, and more. -- **Sidebar webview (Svelte 5)**: full brain dashboard in the VS Code activity bar with circular flow gauge, metrics strip, daily report, energy, struggle, optimal hours, workspace activity, environment, terminal impact, context cost, and dev loops. -- **Activity bar icon**: neural network SVG icon in the VS Code sidebar; NeuroSkill logo (PNG) in the webview header. -- **Brain status bar in VS Code**: polls daemon every 30s showing flow state, fatigue, streak, task type, and struggle score. Shows "offline" when daemon unreachable. Notifications for fatigue and struggle. -- **Dual-speed sidebar updates**: local data (files, terminals, layout) refreshes every 5s when sidebar is visible; brain endpoints every 30s. Pauses when hidden. -- **Event caching**: offline-resilient `pendingEvents` array (10K cap) persisted to disk via `globalStorageUri`. Auto-flushes on reconnect. Survives VS Code restarts. Cache count shown in status bar tooltip. -- **Workspace activity tracking**: per-file edits, lines added/removed, focus time, active file indicator. Grouped by workspace folder (project), sorted by activity. Top 10 files per project. -- **Terminal command tracking**: full command text, exit code, cwd, output streaming (via `execution.read()`), shell type detection (zsh/bash/fish/powershell/cmd/node/python), focus time per terminal, PID. Expandable command output in sidebar. -- **Terminal shell integration**: captures commands via `onDidStartTerminalShellExecution` / `onDidEndTerminalShellExecution` (VS Code 1.93+). CWD tracked via `onDidChangeTerminalShellIntegration`. -- **Zone tracking**: editor/terminal/panel time split with stacked color bar (blue/green/yellow). Layout chips showing tab count, editor groups, terminal count. -- **Auto EEG labeling**: every significant VS Code event auto-inserts a searchable label into EEG recordings with smart categorization (editing, debugging, git commits, AI assistance, meetings, errors, navigation, terminal commands, zone switches). -- **Inline label embedding**: auto-labels embedded immediately via fastembed for instant searchability (no idle reembed wait). -- **Command execution tracking**: 40+ VS Code commands tracked (go-to-definition, rename, find, format, fold, git, AI, debug, layout) with semantic categorization. -- **IntelliSense acceptance detection**: multi-char single-line insertions heuristically identified as autocomplete acceptances. -- **Clipboard tracking**: clipboard content changes polled every 5s with debounce. -- **Layout snapshots**: periodic (60s) capture of editor groups, visible editors, open tabs, terminal count sent to daemon. -- **Zone switch events**: editor/terminal/panel transitions sent to daemon with EEG focus snapshot. -- **File system watcher**: selective watching of package.json, Cargo.toml, go.mod, .git/HEAD for external change detection. -- **Environment context**: one-time capture of appHost, remoteName, shell, uiKind, language. -- **Info toggles**: every sidebar card has a `?` button explaining how metrics are calculated (flow score formula, struggle signals, energy bar, optimal hours, dev loops, terminal impact, context cost). -- **Open NeuroSkill button**: launches native app from sidebar (cross-platform: `open -a` macOS, `start` Windows, `xdg-open` Linux). Also in command palette. -- **Command palette → sidebar**: `Show Brain Status`, `Today's Report`, `Am I Stuck?`, `Best Time to Code` now open sidebar and scroll to relevant section instead of showing toast notifications. - -### Server - -- **`terminal_commands` table**: shell command text, cwd, exit code, duration, auto-categorized (50+ patterns: build/test/run/git/docker/deploy/install/navigate/debug/network/other), EEG focus at start and end for delta calculation. -- **`dev_loops` table**: edit→build/test cycle tracking with iteration count, pass/fail rate, avg cycle time, focus trend (rising/falling/stable). -- **`zone_switches` table**: editor/terminal/panel transitions with EEG focus snapshot at moment of switch. -- **`layout_snapshots` table**: periodic tab/group/terminal counts from VS Code. -- **Event handler**: new match arms for `terminal_command_start`, `terminal_command_end`, `zone_switch`, `layout_snapshot` in `activity_vscode_events_impl()`. -- **Command categorizer**: `categorize_command()` with 50+ patterns across build, test, run, git, docker, deploy, install, navigate, debug, network. -- **`POST /v1/brain/terminal-impact`**: avg EEG focus delta by command category — shows how builds, tests, git, docker affect brain state. -- **`POST /v1/brain/context-cost`**: focus level at each zone transition type with switch counts. -- **`POST /v1/brain/dev-loops`**: edit-build-test cycle detection with iterations, pass rate, cycle time, focus trend. -- **`POST /v1/brain/terminal-commands`**: recent commands with exit codes, durations, categories, EEG correlation. -- **Enhanced `predict_struggle()`**: terminal failures (+8/fail, max +40) and re-running same failing command 3+ times (+15/rerun, max +30) boost struggle score. New suggestions: "You're re-running the same failing command", "Multiple failures — read the error messages". -- **Enhanced `detect_task_type()`**: terminal command categories override heuristics with higher confidence — docker/kubectl→infrastructure (0.8), deploy→deploying (0.85), git dominating→git_management (0.7), test→testing (0.85), debug→debugging (0.9). -- **Terminal EEG auto-labels**: `"running: cargo test"` on start, `"cargo test passed"` / `"cargo test failed (exit 1)"` on end, `"switched to terminal"` on zone changes. -- **AI events table**: `ai_events` SQLite table tracking suggestion shown/accepted/rejected and chat sessions with source attribution. -- **OS-wide shell hooks**: `scripts/shell-hooks/` with preexec/precmd hooks for zsh, bash, fish, PowerShell. Sends every command to daemon via background curl. No delay to prompt. -- **Shell hook daemon endpoints**: `GET /v1/activity/shell-hook?shell=zsh` returns hook script, `POST /v1/activity/install-shell-hook` writes to `~/.skill/shell-hooks/` and appends to rc file, `POST /v1/activity/uninstall-shell-hook` removes cleanly, `POST /v1/activity/shell-hook-status` health check. -- **`neuroskill terminal` CLI**: `status` (hook health per shell), `install [shell]`, `uninstall [shell]`, `commands` (recent tracked), `impact` (focus delta by category), `loops` (dev loop detection). -- **`neuroskill brain` new subactions**: `terminal-impact`, `context-cost`, `dev-loops`. -- **`neuroskill activity` new subaction**: `terminal-commands`. -- **Tauri Terminal settings tab**: per-shell install/uninstall/repair buttons, health indicators (green/yellow/red), recent commands preview, "How it works" documentation. -- **`neuroskill vscode` CLI**: auto-install extension to VS Code, VSCodium, or Cursor on macOS, Linux, and Windows. -- **Meeting nodes in search graph**: meetings appear as amber nodes in the interactive 3D search graph linked by `meeting_prox` edges. - -### Features - -- **Developer insights endpoint**: `POST /v1/brain/developer-insights` returns 7 actionable insights in one call. -- **Test failure by focus level**: correlates build/test exit codes with EEG focus (high/mid/low) — "your tests fail 45% more when focus is low." -- **Hourly productivity**: churn + undo rate + avg focus by hour of day — "you write 3x more bugs after 2pm." -- **Context switch recovery**: focus level at editor/terminal/panel transitions — "switching to terminal costs 4 focus points." -- **AI tool impact**: Claude/Pi focus delta vs baseline — "Claude conversations drop focus by 8 points." -- **Focus by language**: avg EEG focus + undo rate per programming language — "best focus in Rust, worst in CSS." -- **Dev loop efficiency by hour**: build/test cycle time + pass rate by time of day. -- **Tool focus impact**: avg focus when using each command category (docker, git, deploy, etc.). - -### Refactor - -- **Conversations table + FTS5**: full-text search on all AI conversation messages (user/assistant/tool). Three search modes: FTS, fuzzy, structured. -- **EEG timeseries table**: periodic JSON snapshots of all brain metrics. Extensible — new metrics without schema changes. Join-at-query-time correlation with any event. -- **Embeddings store**: generic, multi-model. User prompts embedded via fastembed (local, no API credits). Can re-embed with different models. -- **Code context HNSW index**: separate from label index for code-specific semantic search. -- **Binary extraction**: terminal commands store raw binary name + args separately. Lazy categorization — 284 CLI tools recognized, rerun anytime. -- **EEG attachment**: live focus/mood from `latest_bands` injected at event storage time. -- **EEG timeseries worker**: writes full band powers to `eeg_timeseries` every 5s during recording. - -### UI - -- **Design tokens** (`tokens.css`): 30+ CSS custom properties — palette, surfaces, backgrounds, borders, semantic, typography. Zero inline rgba() in sidebar. -- **7 reusable Svelte components**: Card, MetricRow, Chevron, ProgressBar, Gauge, Badge, Callout. -- **Timestamp compliance**: 25 tests verifying UTC in data layer, local conversion only in UI. - -### Docs - -- VS Code extension design plan at `docs/vscode-extension.md`. -- `neuroskill-activity` skill documentation with 18 activity + 24 brain subcommands, terminal integration, shell hook reference, command categorization table. -- Updated `neuroskill-dnd` skill with grayscale mode. -- Updated `neuroskill/README.md` with terminal, brain awareness, and VS Code extension features. -- Updated `skills/SKILL.md` index with terminal tracking skill reference. diff --git a/changes/unreleased/feat-vscode-readme-rewrite.md b/changes/unreleased/feat-vscode-readme-rewrite.md deleted file mode 100644 index 0babf46f..00000000 --- a/changes/unreleased/feat-vscode-readme-rewrite.md +++ /dev/null @@ -1,5 +0,0 @@ -### Docs - -- **VS Code extension README rewritten for skeptical developers**: scientific framing throughout — every derived metric (focus score, flow detector, deep-work minute, struggle predictor, optimal hours, fatigue) now ships with its formula, hyperparameters, and a "what can go wrong" line. Added an explicit *What this is, and isn't* section, a *Validation status* table tracking each metric's evidence level (production / pilot / heuristic / descriptive-only), and a *Validation roadmap* describing the planned KSS / NASA-TLX / PVT / EEG-fatigue calibration channels. Replaced the marketing pain-points table with a per-API signal taxonomy and an architecture paragraph. -- **References section with verified citations**: nine entries with DOIs cross-checked via CrossRef and `doi.org` redirects — Csikszentmihalyi 1990 (flow), Klimesch 1999 (α/θ oscillations), Lubar 1991 (θ/β attention ratio), Barry et al. 2005 (caffeine confound), Newport 2016 (deep work), Roenneberg et al. 2003 (chronotypes), Hart & Staveland 1988 + Hart 2006 (NASA-TLX), Jap et al. 2009 (EEG fatigue index). Each reference linked back to the metric it grounds. -- **Repository metadata pointed at the right repo**: discovered `extensions/vscode/` is a git submodule of `vscode-neuroskill`, not a subdirectory of the monorepo. Reverted `repository.url`, `bugs.url`, `homepage`, `qna` to the submodule's actual repo so the marketplace links resolve. diff --git a/changes/unreleased/feat-vscode-readme-screenshots.md b/changes/unreleased/feat-vscode-readme-screenshots.md deleted file mode 100644 index 17eccd9c..00000000 --- a/changes/unreleased/feat-vscode-readme-screenshots.md +++ /dev/null @@ -1,9 +0,0 @@ -### UI - -- **Theme-aware sidebar screenshots in the README**: every screenshot now ships in two variants (`*-dark.png`, `*-light.png`) and is embedded via a `` element with `prefers-color-scheme` sources. GitHub serves the variant matching the reader's OS theme; the marketplace gets the dark fallback. Six states captured: in-flow, stuck, fatigued, low-focus / off-peak, daemon-disconnected, status-bar strip. -- **Sidebar mock library + Playwright generator**: HTML stubs in `extensions/vscode/media/preview/` render the sidebar webview offline (no daemon required). A shared `_styles.css` drives both themes via a `:root.light` override; `npm run screenshots` (Playwright) toggles `` between captures and writes 12 PNGs to `media/screenshots/`. -- **Marketplace README pipeline**: `scripts/build-marketplace-readme.mjs` rewrites every relative `media/...` path (including ``, which `vsce` doesn't touch) to absolute GitHub raw URLs at package time. The source `README.md` keeps relative paths for GitHub + offline viewing; the generated `.marketplace.readme.md` is what `vsce package --readme-path` ships. - -### Bugfixes - -- **Equal padding on screenshot edges**: viewport width was 360px while the body was 320px wide, leaving an asymmetric 40px right gutter. Matched viewport to body width so left and right gutters are now identical. diff --git a/changes/unreleased/feat-vscode-sidebar-light-mode.md b/changes/unreleased/feat-vscode-sidebar-light-mode.md deleted file mode 100644 index 936152bd..00000000 --- a/changes/unreleased/feat-vscode-sidebar-light-mode.md +++ /dev/null @@ -1,7 +0,0 @@ -### UI - -- **Sidebar webview now renders correctly in light themes** (`extensions/vscode/src/sidebar.ts`). Three theme-fragile spots fixed: - - `.metric-card` background switched from `--vscode-sideBar-background` (which is the same colour as the surrounding sidebar — cards collapsed into a faint border in light mode) to `--vscode-editorWidget-background`, with `--vscode-input-background` and a translucent grey as cascading fallbacks. - - `.ai-bar` track switched from `--vscode-panel-border` to `--vscode-progressBar-background` so the empty bar is visible against a white background. - - Row-divider opacity bumped from 0.08 to 0.18; added `:last-child { border-bottom: none }` on commit and AI-metric rows so the last row doesn't double up against the disclaimer footer. -- **State colours stay hard-coded** — red ring for stuck, green for flow, amber for warning. They're semantic, not chrome, and they read fine on both themes. diff --git a/changes/unreleased/feat-widget-a11y-i18n.md b/changes/unreleased/feat-widget-a11y-i18n.md deleted file mode 100644 index c9333273..00000000 --- a/changes/unreleased/feat-widget-a11y-i18n.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Widget accessibility and localization**. diff --git a/changes/unreleased/feat-widget-analysis.md b/changes/unreleased/feat-widget-analysis.md deleted file mode 100644 index 8a39433c..00000000 --- a/changes/unreleased/feat-widget-analysis.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Analysis widgets (Optimal Hours, Daily Report, Weekly Trend)**. diff --git a/changes/unreleased/feat-widget-biometrics.md b/changes/unreleased/feat-widget-biometrics.md deleted file mode 100644 index 55410d4f..00000000 --- a/changes/unreleased/feat-widget-biometrics.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Biometric widgets (Heart Rate, Cognitive Load, EEG Band Power, Sleep)**. diff --git a/changes/unreleased/feat-widget-brain-dashboard.md b/changes/unreleased/feat-widget-brain-dashboard.md deleted file mode 100644 index 0c167080..00000000 --- a/changes/unreleased/feat-widget-brain-dashboard.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Brain Dashboard widget (medium)**. diff --git a/changes/unreleased/feat-widget-calendar-mind.md b/changes/unreleased/feat-widget-calendar-mind.md deleted file mode 100644 index 3d9f6b99..00000000 --- a/changes/unreleased/feat-widget-calendar-mind.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Calendar Mind State widget (large)**. diff --git a/changes/unreleased/feat-widget-deep-links.md b/changes/unreleased/feat-widget-deep-links.md deleted file mode 100644 index 19dbca2c..00000000 --- a/changes/unreleased/feat-widget-deep-links.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Widget deep links (neuroskill:// URL scheme)**. diff --git a/changes/unreleased/feat-widget-dev-infra.md b/changes/unreleased/feat-widget-dev-infra.md deleted file mode 100644 index 92ed4ea0..00000000 --- a/changes/unreleased/feat-widget-dev-infra.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Widget development infrastructure**. diff --git a/changes/unreleased/feat-widget-focus-streak-session.md b/changes/unreleased/feat-widget-focus-streak-session.md deleted file mode 100644 index 54ab6bb9..00000000 --- a/changes/unreleased/feat-widget-focus-streak-session.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Core desktop widgets (Focus, Streak, Session, Break Timer)**. diff --git a/changes/unreleased/feat-widget-interactive.md b/changes/unreleased/feat-widget-interactive.md deleted file mode 100644 index 53de9a66..00000000 --- a/changes/unreleased/feat-widget-interactive.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Interactive widget buttons (macOS 14+)**. diff --git a/changes/unreleased/feat-widget-offline-cache.md b/changes/unreleased/feat-widget-offline-cache.md deleted file mode 100644 index c705a33d..00000000 --- a/changes/unreleased/feat-widget-offline-cache.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Widget offline data caching**. diff --git a/changes/unreleased/feat-widget-reload.md b/changes/unreleased/feat-widget-reload.md deleted file mode 100644 index 625a9ac9..00000000 --- a/changes/unreleased/feat-widget-reload.md +++ /dev/null @@ -1,3 +0,0 @@ -### Features - -- **Widget timeline reload on state changes**. diff --git a/changes/unreleased/feat-zuna-rs-mlx.md b/changes/unreleased/feat-zuna-rs-mlx.md deleted file mode 100644 index d71ce4ee..00000000 --- a/changes/unreleased/feat-zuna-rs-mlx.md +++ /dev/null @@ -1,9 +0,0 @@ -### Features - -- **zuna-rs 0.1.4**: updated from 0.1.3 with native MLX backend for EEG embedding inference. -- **`embed-zuna-mlx` feature**: new `ZunaMlxState` struct with `ZunaEncoder`. Adds `load_zuna_mlx()` and `encode_zuna_mlx()` functions matching the existing GPU/CPU variants. -- **Load priority**: when user selects Auto or MLX, encoder loading tries MLX first, then GPU f16, then GPU f32, then CPU. - -### i18n - -- Added `settings.inferenceDeviceAuto`, `settings.inferenceDeviceAutoDesc`, `settings.inferenceDeviceMlx`, `settings.inferenceDeviceMlxDesc` to all 9 locales (en, de, es, fr, he, ja, ko, uk, zh). diff --git a/changes/unreleased/fix-daemon-deadlocks-and-correctness.md b/changes/unreleased/fix-daemon-deadlocks-and-correctness.md deleted file mode 100644 index 10da19d0..00000000 --- a/changes/unreleased/fix-daemon-deadlocks-and-correctness.md +++ /dev/null @@ -1,9 +0,0 @@ -### Bugfixes - -- **Deadlock in daily-report endpoint**: `daily_brain_report` held the database mutex while calling `productivity_score`, which re-acquires the same mutex. Scoped the lock to drop before the nested call. -- **Deadlock in struggle-predict endpoint**: same pattern in `predict_struggle` — held mutex while calling `get_recent_files`. -- **`overall_focus` returned `-0.0`**: always wrapped in `Some` even with no EEG data. Now returns `null` when no focus samples exist. -- **`best_period` arbitrary without EEG**: picked the first SQL result when all `avg_focus` were `None`. Now returns empty string when no EEG data to compare. -- **`weekly_avg_deep_mins` returned `-0.0`**: divided by hardcoded 7 regardless of actual days. Now divides by actual day count, returns `0.0` when empty. -- **Notification text ugly without EEG**: daily report notification showed "Best: . Score: 25. Focus: 0." — now conditionally includes only available fields. -- **Brain endpoints return HTTP 200 on errors**: all 18 brain handlers silently returned `null` with 200 OK when the activity store was offline or a query failed. Refactored to use `run_query` helper that returns 503 (db_unavailable) or 500 (task_error) with structured `ApiError` JSON. diff --git a/changes/unreleased/fix-daemon-performance.md b/changes/unreleased/fix-daemon-performance.md deleted file mode 100644 index 8663c739..00000000 --- a/changes/unreleased/fix-daemon-performance.md +++ /dev/null @@ -1,8 +0,0 @@ -### Performance - -- **`open_readonly` for read-only handlers**: added `ActivityStore::open_readonly()` that skips 15+ ALTER TABLE migrations and opens in read-only mode. Switched 38+ HTTP handlers and 3 background tasks from `open` to `open_readonly`. -- **`PRAGMA busy_timeout=5000`**: added to `init_wal_pragmas` and `util::open_readonly` so all database connections (activity, labels, screenshots, EEG embeddings) wait up to 5 s on lock contention instead of failing immediately with SQLITE_BUSY. -- **Composite indexes**: added `(seen_at, file_path)` and `(seen_at, project)` indexes on `file_interactions` for queries that filter by time range and group by path or project. -- **Lock consolidation**: `productivity_score` reduced from 3 separate mutex acquisitions to 1 via internal `_q` query helpers. `weekly_digest` reduced from 12 to 4 locks. Introduced `daily_summary_q`, `context_switch_rate_q`, `get_focus_sessions_in_range_q` static methods that take `&Connection` directly. -- **`PRAGMA optimize` after pruning**: runs after hourly retention pruning to keep query planner statistics fresh. -- **Batch label lookup in interactive search**: replaced 15 per-epoch `get_labels_near` calls (each opening a new DB connection) with a single `get_labels_near_batch` call per text label, then in-memory filtering. Reduces search DB round-trips from ~28 to ~16. diff --git a/changes/unreleased/fix-daemon-reliability.md b/changes/unreleased/fix-daemon-reliability.md deleted file mode 100644 index 075d6b80..00000000 --- a/changes/unreleased/fix-daemon-reliability.md +++ /dev/null @@ -1,9 +0,0 @@ -### Bugfixes - -- **Worker thread panic recovery with auto-restart**: all 4 activity worker threads (poller, input monitor, file watcher, clipboard monitor) now wrapped in `catch_unwind` via `spawn_resilient`. On panic, the worker logs the error and restarts after 5 s with freshly cloned `AppState` and `Arc`. -- **osascript timeout**: all `osascript` calls (active window poll, secondary windows, clipboard monitor) now use a 3-second timeout via `run_osascript` helper. Previously, a hung app could block the poller thread indefinitely, silently killing all activity tracking. -- **Daily report catch-up on late startup**: if the daemon starts after 18:00 local, the daily brain report fires on the first tick instead of waiting up to 1 hour for the hourly check. -- **WAL checkpoint on shutdown**: daemon now runs `PRAGMA optimize` on the activity database during graceful shutdown, ensuring the WAL is checkpointed and the database is clean for next start. -- **Missing WAL pragmas on EEG embedding store**: `day_store.rs` opened connections without `init_wal_pragmas`, missing both WAL mode and `busy_timeout`. -- **Retention pruning for new tables**: added `prune_terminal_commands`, `prune_ai_events`, `prune_zone_switches`, `prune_layout_snapshots` — wired into the hourly maintenance cycle so these tables don't grow unbounded. -- **TOCTOU race in calibration settings**: concurrent profile CRUD could lose writes. Added `modify_settings_blocking()` helper that runs under a global mutex on tokio's blocking thread pool — serializes read-modify-write without blocking the async runtime. diff --git a/changes/unreleased/fix-eeg-pipeline.md b/changes/unreleased/fix-eeg-pipeline.md deleted file mode 100644 index 6f8f7ca2..00000000 --- a/changes/unreleased/fix-eeg-pipeline.md +++ /dev/null @@ -1,5 +0,0 @@ -### Bugfixes - -- **Duration-averaged EEG**: `FileSnapshot` now samples EEG focus/mood every 5 s (at each edit-chunk tick) and writes the average to `file_interactions` at finalize, replacing the single snapshot captured at file-switch time. Falls back to the initial snapshot if no samples were collected (`COALESCE`). -- **EEG mood/focus sample count mismatch**: both `FileSnapshot` and `build_focus_sessions` used a single counter for focus and mood samples, inflating mood averages when they arrived independently. Split into separate `focus_count` and `mood_count`. -- **EEG ghost data after disconnect**: `latest_bands` was not cleared when an EEG device disconnected, causing stale focus/mood values to persist in reports and the activity pipeline. Now cleared on all 3 disconnect paths (idle timeout, stream end, explicit disconnect). diff --git a/changes/unreleased/fix-frontend-leaks.md b/changes/unreleased/fix-frontend-leaks.md deleted file mode 100644 index 93a8abf6..00000000 --- a/changes/unreleased/fix-frontend-leaks.md +++ /dev/null @@ -1,9 +0,0 @@ -### Bugfixes - -- **InteractiveGraph3D event listener leaks**: `pointerdown` and `dblclick` listeners on the WebGL canvas were never removed in `onDestroy`. Extracted into named refs with cleanup. -- **UmapViewer3D event listener leaks**: 4 canvas event listeners (pointermove, pointerleave, pointerdown, pointerup) added as anonymous functions were never removed on unmount. Extracted into named module-level refs with removal in `onDestroy`. -- **ActivityTab brain polling leak**: `startBrainPolling()` was called on mount but `stopBrainPolling()` was never called on destroy, leaving a 30-second interval running after leaving the tab. -- **Brain polling stopped by peer component**: `stopBrainPolling()` unconditionally killed the interval even when other components still needed it. Added reference counting so polling only stops when the last consumer unmounts. -- **Memory leak in terminal_command_end labeling**: `Box::leak` was used to format non-zero exit codes in EEG auto-labeling, permanently leaking memory for every failed command. Replaced with owned `String`. -- **Screenshot encode failures untracked**: `encode_webp` failures (disk full, I/O error) silently continued without incrementing the `capture_errors` counter. Now counted so metrics reflect actual failures. -- **401 auto-retry**: daemon HTTP client now retries once with a fresh token on 401 (stale token after daemon restart) instead of failing immediately. diff --git a/changes/unreleased/fix-macos-libusb-static-link.md b/changes/unreleased/fix-macos-libusb-static-link.md deleted file mode 100644 index 8a250c88..00000000 --- a/changes/unreleased/fix-macos-libusb-static-link.md +++ /dev/null @@ -1,3 +0,0 @@ -### Build - -- **macOS: link libusb statically into `skill-daemon`**: vendored libusb via `rusb`'s `vendored` feature in `crates/skill-devices`. The `antneuro` USB driver pulls in `rusb` → `libusb1-sys`, which by default uses `pkg-config` to find a system libusb-1.0.dylib. On the macOS-26 release runner that resolved to Homebrew's `/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib`, so end users without Homebrew hit a launch-time `dyld: Library not loaded` error. Cargo feature unification now flips `libusb1-sys/vendored`, which compiles libusb from source and links `libusb-vendored.a` into the binary — verified by 105 `_libusb_*` text symbols present in the release binary and zero libusb references in `otool -L`. diff --git a/changes/unreleased/fix-security.md b/changes/unreleased/fix-security.md deleted file mode 100644 index 3628e310..00000000 --- a/changes/unreleased/fix-security.md +++ /dev/null @@ -1,5 +0,0 @@ -### Bugfixes - -- **Path traversal in `delete_session`**: `csv_path` parameter was passed unsanitized to `fs::remove_file`. Now validates the path is within `skill_dir` via canonicalize + starts_with. -- **Timing-vulnerable token comparison**: default bearer token checks in auth middleware used `==` (variable-time). Switched to `constant_time_eq` for both in-memory and on-disk token paths. -- **Unbounded CSV memory in EXG loader**: loading a CSV with millions of rows would allocate unlimited RAM. Added a 4M row cap (~4.3 hours at 256 Hz). diff --git a/changes/unreleased/fix-timezone.md b/changes/unreleased/fix-timezone.md deleted file mode 100644 index ef571dfd..00000000 --- a/changes/unreleased/fix-timezone.md +++ /dev/null @@ -1,6 +0,0 @@ -### Bugfixes - -- **Timezone-wrong period classification**: `daily_brain_report`, `hourly_edit_heatmap`, and `optimal_hours` used `seen_at % 86400` (UTC hour) instead of local hour. Daily report now uses `seen_at - day_start`; heatmap and optimal-hours accept a timezone offset computed server-side. -- **UTC midnight in all clients**: search page, CLI (`cmdActivity`, `cmdBrain`), VS Code extension, and Swift widget all computed `day_start` as UTC midnight instead of local midnight. Fixed to use `Date.setHours(0,0,0,0)` (JS/TS) and `Calendar.startOfDay` (Swift). -- **Widget empty daily-report body**: `DaemonClient.fetchDailyReport()` sent an empty POST body; the endpoint requires `dayStart`. Now sends local midnight. -- **Widget UTC sleep/calendar endpoints**: `fetchSleep()` and `fetchCalendarEvents()` used `% 86400` UTC approximations. Now use `Calendar.current` for correct local time boundaries. diff --git a/crates/skill-iroh/Cargo.toml b/crates/skill-iroh/Cargo.toml index 8aca6bfe..5b76f8a3 100644 --- a/crates/skill-iroh/Cargo.toml +++ b/crates/skill-iroh/Cargo.toml @@ -11,6 +11,9 @@ thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } iroh = "0.97" +# Pin transitive pkcs8 to the rc that matches iroh's pre-release ed25519/ed25519-dalek; +# pkcs8 0.11.0 stable made KeyMalformed a tuple variant and broke them. +pkcs8 = "=0.11.0-rc.11" rand = "0.9" tokio = { version = "1", features = ["full"] } totp-rs = { version = "5.7", features = ["gen_secret", "otpauth", "qr"] } diff --git a/package.json b/package.json index c68afc55..6d48563f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.129", + "version": "0.0.130-rc.1", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 23c2c2ff..28156783 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.129" +version = "0.0.130-rc.1" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index be59e875..c2c61d3d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.129", + "version": "0.0.130-rc.1", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From 3770e81377e2043e1da6081e79d06f418acbdd00 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 02:01:31 -0400 Subject: [PATCH 02/30] fix win/linux --- crates/skill-daemon/src/activity.rs | 1 + crates/skill-daemon/src/main.rs | 2 -- extensions/browser | 2 +- scripts/package-linux-system-bundles.sh | 5 +++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/skill-daemon/src/activity.rs b/crates/skill-daemon/src/activity.rs index 25718c17..bd51a87c 100644 --- a/crates/skill-daemon/src/activity.rs +++ b/crates/skill-daemon/src/activity.rs @@ -599,6 +599,7 @@ fn poll_active_window() -> Option { document_path: None, activated_at: unix_secs(), browser_title: None, + monitor_id: None, }) } } diff --git a/crates/skill-daemon/src/main.rs b/crates/skill-daemon/src/main.rs index 35389ad2..ab20b261 100644 --- a/crates/skill-daemon/src/main.rs +++ b/crates/skill-daemon/src/main.rs @@ -21,7 +21,6 @@ pub(crate) mod session_runner; mod tty; #[cfg(unix)] mod tty_backfill; -#[cfg(unix)] mod tty_embedder; #[cfg(unix)] mod tty_finalizer; @@ -150,7 +149,6 @@ async fn daemon_main() -> anyhow::Result<()> { tty_finalizer::spawn(state.clone()); // Fill in `terminal_outputs.embedding` for finalized rows. Runs every // 30 s, batches of 32, int8-quantised vectors. - #[cfg(unix)] tty_embedder::spawn(state.clone()); // Auto-refresh installed shell hooks so upgrades propagate fixes (e.g. the diff --git a/extensions/browser b/extensions/browser index ab5d4226..3fada5eb 160000 --- a/extensions/browser +++ b/extensions/browser @@ -1 +1 @@ -Subproject commit ab5d42266f852c21e68acab8d78b20643b6198b0 +Subproject commit 3fada5eb2559d457ba64aecebb8c8e4e3cb6620c diff --git a/scripts/package-linux-system-bundles.sh b/scripts/package-linux-system-bundles.sh index f9f8f319..5aec8192 100755 --- a/scripts/package-linux-system-bundles.sh +++ b/scripts/package-linux-system-bundles.sh @@ -67,6 +67,7 @@ if ! command -v rpmbuild >/dev/null 2>&1; then fi version="$(node -p "JSON.parse(require('fs').readFileSync('$ROOT_DIR/package.json','utf8')).version")" +rpm_version="${version//-/\~}" binary_path="$ROOT_DIR/src-tauri/target/$target/release/skill" resources_dir="$ROOT_DIR/src-tauri/resources" @@ -199,7 +200,7 @@ tar -czf "$rpm_top/SOURCES/neuroskill-root.tar.gz" -C "$work_root" "$(basename " cat > "$rpm_top/SPECS/neuroskill.spec" < - $version-1 +* $(date '+%a %b %d %Y') NeuroSkill CI - $rpm_version-1 - CI system-tool Linux package build EOF From 808758c3807c7a73083c6f3400b283484f069ea6 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 02:05:17 -0400 Subject: [PATCH 03/30] 0.0.130-rc.2 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 6 +++--- changes/releases/0.0.130-rc.2.md | 5 +++++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 changes/releases/0.0.130-rc.2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6a44c7..87a7a4e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5396,3 +5396,9 @@ The heatmap merges EEG data points with the closest timeline events to show whic - **Update llama-cpp-4 to 0.2.50**: upstream llama.cpp fixes including `common_*` symbol renames. - **Update kittentts to 0.4.1**: TTS engine update. - **Add grayscale 0.0.1**: macOS grayscale display control for DND mode. + +## [0.0.130-rc.2] — 2026-04-29 + +### Features + +- fix win/linux diff --git a/Cargo.lock b/Cargo.lock index 21fbd4b7..59c33f36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2357,9 +2357,9 @@ dependencies = [ [[package]] name = "coreaudio-rs" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ "bitflags 2.11.1", "libc", @@ -12159,7 +12159,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.1" +version = "0.0.130-rc.2" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/releases/0.0.130-rc.2.md b/changes/releases/0.0.130-rc.2.md new file mode 100644 index 00000000..79eba5fa --- /dev/null +++ b/changes/releases/0.0.130-rc.2.md @@ -0,0 +1,5 @@ +## [0.0.130-rc.2] — 2026-04-29 + +### Features + +- fix win/linux diff --git a/package.json b/package.json index 6d48563f..6bd0ac39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.1", + "version": "0.0.130-rc.2", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 28156783..0fa29920 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.1" +version = "0.0.130-rc.2" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c2c61d3d..e6141199 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.1", + "version": "0.0.130-rc.2", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From e479229abc78f338f8de2b929a4eac4014042d83 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 02:26:38 -0400 Subject: [PATCH 04/30] umap e2e --- .githooks/pre-commit | 18 ++++++++++++++++-- crates/skill-router/tests/umap_e2e_bench.rs | 2 ++ scripts/test-all.sh | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 11a89f19..21f6e494 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -30,8 +30,22 @@ fi # Run cargo fmt on staged Rust files if echo "$STAGED_FILES" | grep -q '\.rs$'; then echo "🦀 Running cargo fmt…" - cargo fmt - git add $( echo "$STAGED_FILES" | grep '\.rs$' ) + # GUI git clients (editors, Tower, etc.) launch hooks in a non-interactive + # shell that doesn't source ~/.zshrc, so rustup's ~/.cargo/bin is missing + # from PATH. Source the env file rustup ships, or fall back to the default + # install path. + if [ -f "$HOME/.cargo/env" ]; then + . "$HOME/.cargo/env" + elif [ -d "$HOME/.cargo/bin" ]; then + export PATH="$HOME/.cargo/bin:$PATH" + fi + if ! command -v cargo >/dev/null 2>&1; then + echo "⚠️ cargo not found on PATH; skipping cargo fmt." >&2 + echo " Install rustup (https://rustup.rs) or add ~/.cargo/bin to your shell's PATH." >&2 + else + cargo fmt + git add $( echo "$STAGED_FILES" | grep '\.rs$' ) + fi fi echo "✅ Pre-commit checks passed (basic validation only)." diff --git a/crates/skill-router/tests/umap_e2e_bench.rs b/crates/skill-router/tests/umap_e2e_bench.rs index 10c569b1..2c289184 100644 --- a/crates/skill-router/tests/umap_e2e_bench.rs +++ b/crates/skill-router/tests/umap_e2e_bench.rs @@ -161,6 +161,7 @@ fn umap_e2e_small() { /// Medium dataset (1000 points) — representative of a typical EEG session pair. #[test] +#[ignore = "slow benchmark; run with --include-ignored or via npm run test:mlx-e2e"] #[cfg(any(feature = "gpu", feature = "mlx"))] fn umap_e2e_medium() { let (skill_dir, a_start, a_end, b_start, b_end) = seed_synthetic_embeddings("medium", 500, 500); @@ -195,6 +196,7 @@ fn umap_e2e_medium() { /// Large dataset (5000 points) — stress test matching real-world cache sizes. #[test] +#[ignore = "slow benchmark; run with --include-ignored or via npm run test:mlx-e2e"] #[cfg(any(feature = "gpu", feature = "mlx"))] fn umap_e2e_large() { let (skill_dir, a_start, a_end, b_start, b_end) = seed_synthetic_embeddings("large", 2500, 2500); diff --git a/scripts/test-all.sh b/scripts/test-all.sh index 92cca301..cfff6ecd 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -185,7 +185,7 @@ for suite in "${SUITES[@]}"; do ;; mlx-e2e) if [[ "$(uname -s)" == "Darwin" ]]; then - run_suite "UMAP MLX E2E" cargo test -p skill-router --features mlx -- umap_e2e --nocapture --test-threads=1 || { $STOP_ON_FAIL && break; } + run_suite "UMAP MLX E2E" cargo test -p skill-router --features mlx -- umap_e2e --nocapture --test-threads=1 --include-ignored || { $STOP_ON_FAIL && break; } run_suite "FFT MLX E2E" cargo test -p skill-eeg --features mlx -- fft_e2e --nocapture || { $STOP_ON_FAIL && break; } else skip_suite "MLX E2E" "requires macOS with Apple Silicon" From b62d33e52da8140f901e3ba078c1a1e9c3df5f19 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 16:07:10 -0400 Subject: [PATCH 05/30] fixed issues with the metrics, keychain access (3 -> 1 + on demand), windows CI --- Cargo.lock | 119 +------------ changes/unreleased.md | 8 + .../src/cmd_dispatch/system_cmds.rs | 9 +- .../src/routes/settings_device.rs | 29 +++- crates/skill-daemon/src/routes/settings_ui.rs | 11 +- crates/skill-daemon/src/scanner.rs | 8 +- .../skill-daemon/src/session/connect_ble.rs | 10 +- .../skill-daemon/src/session/connect_wired.rs | 26 +-- crates/skill-daemon/src/session/shared.rs | 32 +--- crates/skill-devices/src/lib.rs | 163 ++++++++++++++++++ crates/skill-eeg/src/eeg_bands.rs | 15 ++ crates/skill-exg/Cargo.toml | 1 + crates/skill-exg/src/lib.rs | 150 ++++++++++++++-- crates/skill-headless/Cargo.toml | 4 +- crates/skill-settings/src/keychain.rs | 135 +++++++++++++-- crates/skill-settings/src/lib.rs | 24 ++- scripts/create-windows-nsis.ps1 | 17 +- scripts/release.js | 38 +++- scripts/smoke-test.sh | 149 +++++++++++++--- scripts/test-all.sh | 9 +- src-tauri/src/helpers.rs | 10 +- src-tauri/src/setup.rs | 3 +- src-tauri/src/state.rs | 4 - src-tauri/src/window_cmds.rs | 31 +++- test.ts | 17 +- 25 files changed, 729 insertions(+), 293 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59c33f36..942526bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8887,16 +8887,6 @@ dependencies = [ "objc2-foundation 0.3.2", ] -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-core-media" version = "0.3.2" @@ -9106,27 +9096,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.11.1", - "block2 0.6.2", "objc2 0.6.4", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation 0.3.2", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -12482,6 +12453,7 @@ dependencies = [ "serde_json", "skill-constants", "skill-data", + "skill-devices", "skill-eeg", "ureq 3.3.0", ] @@ -12508,9 +12480,9 @@ dependencies = [ "http 1.4.0", "serde", "serde_json", - "tao 0.35.0", + "tao", "thiserror 2.0.18", - "wry 0.55.0", + "wry", ] [[package]] @@ -13468,43 +13440,6 @@ dependencies = [ "x11-dl", ] -[[package]] -name = "tao" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159" -dependencies = [ - "bitflags 2.11.1", - "block2 0.6.2", - "core-foundation 0.10.1", - "core-graphics", - "crossbeam-channel", - "dispatch2", - "dlopen2 0.8.2", - "dpi", - "gdkwayland-sys", - "gtk", - "jni 0.21.1", - "libc", - "log", - "ndk", - "ndk-sys", - "objc2 0.6.4", - "objc2-app-kit", - "objc2-foundation 0.3.2", - "objc2-ui-kit", - "once_cell", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-version", -] - [[package]] name = "tao-macros" version = "0.1.3" @@ -13826,14 +13761,14 @@ dependencies = [ "percent-encoding", "raw-window-handle", "softbuffer", - "tao 0.34.8", + "tao", "tauri-runtime", "tauri-utils", "url", "webkit2gtk", "webview2-com", "windows 0.61.3", - "wry 0.54.4", + "wry", ] [[package]] @@ -16902,50 +16837,6 @@ dependencies = [ "x11-dl", ] -[[package]] -name = "wry" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" -dependencies = [ - "base64 0.22.1", - "block2 0.6.2", - "cookie", - "crossbeam-channel", - "dirs", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http 1.4.0", - "javascriptcore-rs", - "jni 0.21.1", - "libc", - "ndk", - "objc2 0.6.4", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2 0.10.9", - "soup3", - "tao-macros", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - [[package]] name = "ws_stream_wasm" version = "0.7.5" diff --git a/changes/unreleased.md b/changes/unreleased.md index 2f16bedc..d6cfc1b4 100644 --- a/changes/unreleased.md +++ b/changes/unreleased.md @@ -3,3 +3,11 @@ ### UI - Add "Force Restart" button to the engine status hover panel on the dashboard + +### Build + +- Align `skill-headless` to `wry 0.54` / `tao 0.34` so it matches the versions `tauri-runtime-wry 2.10.1` already pulls in. Previously the workspace built two copies of wry/tao (0.54.4 + 0.55.0, 0.34.8 + 0.35.0) because `skill-headless` pinned the newer pair. Single resolved version now, smaller binary, no functional change. + +### Security + +- **Lazy keychain access**: the macOS keychain is no longer read at app/daemon startup. Previously, `load_settings()` eagerly fetched all eight stored secrets (api_token, Emotiv, IDUN, Oura, Neurosity), and three separate processes (Tauri shell, daemon `state::new`, daemon `main`) each ran it during boot. On a fresh build the code signature changes, so the OS prompted up to three times before the user could see the app. Secrets are now fetched on demand from the keychain only when the user actually opens device settings, connects a device, or runs a sync — so at most one prompt appears, gated on user intent. Tauri's `AppState` no longer caches `api_token` / `device_api_config`; the daemon's route handlers (`set_device_api_config`, `set_api_token`) write secrets directly to the keychain and skip empty values to avoid clobbering existing entries on partial saves. diff --git a/crates/skill-daemon/src/cmd_dispatch/system_cmds.rs b/crates/skill-daemon/src/cmd_dispatch/system_cmds.rs index 8d44b09b..84130d4b 100644 --- a/crates/skill-daemon/src/cmd_dispatch/system_cmds.rs +++ b/crates/skill-daemon/src/cmd_dispatch/system_cmds.rs @@ -169,10 +169,8 @@ pub(super) async fn cmd_health_metric_types(state: &AppState) -> Result Result { - let skill_dir = skill_dir(state); - let settings = skill_settings::load_settings(&skill_dir); - let has_token = !settings.device_api.oura_access_token.is_empty(); +pub(super) async fn cmd_oura_status(_state: &AppState) -> Result { + let has_token = !skill_settings::keychain::get_oura_access_token().is_empty(); Ok(json!({ "connected": has_token, "has_token": has_token, @@ -183,8 +181,7 @@ pub(super) async fn cmd_oura_sync(state: &AppState, msg: &Value) -> Result) -> Json { let c = load_user_settings(&state).device_api; + let (emotiv_client_id, emotiv_client_secret) = skill_settings::keychain::get_emotiv_credentials(); + let idun_api_token = skill_settings::keychain::get_idun_api_token(); + let oura_access_token = skill_settings::keychain::get_oura_access_token(); + let (neurosity_email, neurosity_password, neurosity_device_id) = + skill_settings::keychain::get_neurosity_credentials(); Json(serde_json::json!({ - "emotiv_client_id": c.emotiv_client_id, - "emotiv_client_secret": c.emotiv_client_secret, - "idun_api_token": c.idun_api_token, - "oura_access_token": c.oura_access_token, - "neurosity_email": c.neurosity_email, - "neurosity_password": c.neurosity_password, - "neurosity_device_id": c.neurosity_device_id, + "emotiv_client_id": emotiv_client_id, + "emotiv_client_secret": emotiv_client_secret, + "idun_api_token": idun_api_token, + "oura_access_token": oura_access_token, + "neurosity_email": neurosity_email, + "neurosity_password": neurosity_password, + "neurosity_device_id": neurosity_device_id, "brainmaster_model": c.brainmaster_model, })) } @@ -138,6 +143,16 @@ pub(crate) async fn set_device_api_config( let mut settings = load_user_settings(&state); settings.device_api = config.clone(); save_user_settings(&state, &settings); + skill_settings::keychain::save_device_api_secrets(&skill_settings::keychain::Secrets { + api_token: String::new(), + emotiv_client_id: config.emotiv_client_id.clone(), + emotiv_client_secret: config.emotiv_client_secret.clone(), + idun_api_token: config.idun_api_token.clone(), + oura_access_token: config.oura_access_token.clone(), + neurosity_email: config.neurosity_email.clone(), + neurosity_password: config.neurosity_password.clone(), + neurosity_device_id: config.neurosity_device_id.clone(), + }); if let Ok(mut cortex) = state.scanner_cortex_config.lock() { cortex.emotiv_client_id = config.emotiv_client_id; cortex.emotiv_client_secret = config.emotiv_client_secret; diff --git a/crates/skill-daemon/src/routes/settings_ui.rs b/crates/skill-daemon/src/routes/settings_ui.rs index 58943aba..70720858 100644 --- a/crates/skill-daemon/src/routes/settings_ui.rs +++ b/crates/skill-daemon/src/routes/settings_ui.rs @@ -235,18 +235,15 @@ pub(crate) async fn test_location() -> Json { Json(v) } -pub(crate) async fn get_api_token(State(state): State) -> Json { - let settings = load_user_settings(&state); - Json(serde_json::json!({"value": settings.api_token})) +pub(crate) async fn get_api_token(State(_state): State) -> Json { + Json(serde_json::json!({"value": skill_settings::keychain::get_api_token()})) } pub(crate) async fn set_api_token( - State(state): State, + State(_state): State, Json(req): Json, ) -> Json { - let mut settings = load_user_settings(&state); - settings.api_token = req.value; - save_user_settings(&state, &settings); + skill_settings::keychain::set_api_token(&req.value); Json(serde_json::json!({"ok": true})) } diff --git a/crates/skill-daemon/src/scanner.rs b/crates/skill-daemon/src/scanner.rs index a0bd4332..f2cd077d 100644 --- a/crates/skill-daemon/src/scanner.rs +++ b/crates/skill-daemon/src/scanner.rs @@ -601,12 +601,8 @@ pub(crate) fn detect_manual_device_hints(state: &AppState) -> Vec) -> anyhow::Resul // ── IDUN Guardian (BLE) ────────────────────────────────────────────────────── pub(super) async fn connect_idun( - state: &AppState, + _state: &AppState, paired_name: Option, ) -> anyhow::Result> { use skill_devices::idun::prelude::*; use skill_devices::session::idun::IdunAdapter; - let api_token = { - let skill_dir = state.skill_dir.lock().map(|g| g.clone()).unwrap_or_default(); - skill_settings::load_settings(&skill_dir) - .device_api - .idun_api_token - .clone() - }; + let api_token = skill_settings::keychain::get_idun_api_token(); info!("connecting to IDUN Guardian…"); let config = GuardianClientConfig { diff --git a/crates/skill-daemon/src/session/connect_wired.rs b/crates/skill-daemon/src/session/connect_wired.rs index 7f7a9b3c..e7182be7 100644 --- a/crates/skill-daemon/src/session/connect_wired.rs +++ b/crates/skill-daemon/src/session/connect_wired.rs @@ -538,33 +538,32 @@ impl skill_devices::session::DeviceAdapter for NeuroSkyAdapter { // ── Neurosity Crown/Notion (Cloud API) ───────────────────────────────────── -pub(super) async fn connect_neurosity(state: &AppState, target: &str) -> anyhow::Result> { +pub(super) async fn connect_neurosity(_state: &AppState, target: &str) -> anyhow::Result> { use neurosity::prelude::*; let requested_device_id = target.strip_prefix("neurosity:").unwrap_or("").trim().to_string(); let (device_id, email, password) = { - let skill_dir = state.skill_dir.lock().map(|g| g.clone()).unwrap_or_default(); - let settings = skill_settings::load_settings(&skill_dir); + let (kc_email, kc_password, kc_device_id) = skill_settings::keychain::get_neurosity_credentials(); let device_id = if requested_device_id.is_empty() { - settings.device_api.neurosity_device_id.clone() + kc_device_id } else { requested_device_id }; - let email = if settings.device_api.neurosity_email.trim().is_empty() { + let email = if kc_email.trim().is_empty() { std::env::var("SKILL_NEUROSITY_EMAIL") .or_else(|_| std::env::var("NEUROSITY_EMAIL")) .unwrap_or_default() } else { - settings.device_api.neurosity_email.clone() + kc_email }; - let password = if settings.device_api.neurosity_password.trim().is_empty() { + let password = if kc_password.trim().is_empty() { std::env::var("SKILL_NEUROSITY_PASSWORD") .or_else(|_| std::env::var("NEUROSITY_PASSWORD")) .unwrap_or_default() } else { - settings.device_api.neurosity_password.clone() + kc_password }; (device_id, email, password) @@ -913,18 +912,11 @@ pub(super) async fn connect_antneuro(state: &AppState, target: &str) -> anyhow:: // ── Emotiv (Cortex WebSocket API) ──────────────────────────────────────────── -pub(super) async fn connect_emotiv(state: &AppState) -> anyhow::Result> { +pub(super) async fn connect_emotiv(_state: &AppState) -> anyhow::Result> { use skill_devices::emotiv::prelude::*; use skill_devices::session::emotiv::EmotivAdapter; - let (client_id, client_secret) = { - let skill_dir = state.skill_dir.lock().map(|g| g.clone()).unwrap_or_default(); - let settings = skill_settings::load_settings(&skill_dir); - ( - settings.device_api.emotiv_client_id.clone(), - settings.device_api.emotiv_client_secret.clone(), - ) - }; + let (client_id, client_secret) = skill_settings::keychain::get_emotiv_credentials(); if client_id.trim().is_empty() || client_secret.trim().is_empty() { anyhow::bail!("Emotiv client_id/client_secret not configured in Settings → Device API"); diff --git a/crates/skill-daemon/src/session/shared.rs b/crates/skill-daemon/src/session/shared.rs index e33cf9d2..b63ef9b6 100644 --- a/crates/skill-daemon/src/session/shared.rs +++ b/crates/skill-daemon/src/session/shared.rs @@ -66,14 +66,17 @@ pub fn broadcast_event(tx: &broadcast::Sender, event_type: &str, // ── Band snapshot enrichment ────────────────────────────────────────────────── -/// Enrich a `BandSnapshot` with composite scores (focus, relaxation, engagement, -/// artifacts) and return the result as JSON. +/// Enrich a `BandSnapshot` with composite scores and return the result as JSON. +/// +/// All composite-score math (engagement / relaxation / focus / meditation / +/// cognitive_load / drowsiness) lives in `skill_devices` and is written +/// directly onto the snapshot fields by `skill_devices::enrich_band_snapshot`. +/// This wrapper only adds the daemon-side context (artifacts, GPU stats) and +/// serializes — every consumer reads identical values from the snapshot. pub fn enrich_band_snapshot( snap: &mut skill_eeg::eeg_bands::BandSnapshot, artifacts: Option<&skill_eeg::artifact_detection::ArtifactMetrics>, ) -> serde_json::Value { - // Use skill_devices::enrich_band_snapshot for the full enrichment - // (blink_count, blink_rate, head_pose, composite scores). let ctx = skill_devices::SnapshotContext { ppg: None, artifacts: artifacts.copied(), @@ -82,26 +85,7 @@ pub fn enrich_band_snapshot( gpu: skill_data::gpu_stats::read(), }; skill_devices::enrich_band_snapshot(snap, &ctx); - - // Add composite scores derived from band power. - let mut val = serde_json::to_value(&*snap).unwrap_or_default(); - if let Some(obj) = val.as_object_mut() { - let engage_raw = skill_devices::compute_engagement_raw(snap); - let focus = skill_devices::focus_score(engage_raw); - let nch = snap.channels.len().max(1) as f64; - let avg_alpha = snap.channels.iter().map(|c| c.rel_alpha as f64).sum::() / nch; - let avg_beta = snap.channels.iter().map(|c| c.rel_beta as f64).sum::() / nch; - let relaxation = if (avg_alpha + avg_beta) > 0.0 { - (avg_alpha / (avg_alpha + avg_beta)) * 100.0 - } else { - 0.0 - }; - let engagement = 100.0 / (1.0 + (-2.0 * (engage_raw as f64 - 0.8)).exp()); - obj.insert("focus".into(), serde_json::json!(focus)); - obj.insert("relaxation".into(), serde_json::json!(relaxation)); - obj.insert("engagement".into(), serde_json::json!(engagement)); - } - val + serde_json::to_value(&*snap).unwrap_or_default() } // ── Session metadata ────────────────────────────────────────────────────────── diff --git a/crates/skill-devices/src/lib.rs b/crates/skill-devices/src/lib.rs index d37e4998..186315f9 100644 --- a/crates/skill-devices/src/lib.rs +++ b/crates/skill-devices/src/lib.rs @@ -90,6 +90,14 @@ pub fn enrich_band_snapshot(snap: &mut BandSnapshot, ctx: &SnapshotContext) { let drowsiness = compute_drowsiness(snap); snap.drowsiness = Some((drowsiness * 10.0).round() / 10.0); + // Canonical engagement / relaxation / focus — single source of truth. + let engagement = compute_engagement(snap); + snap.engagement = Some((engagement * 10.0).round() / 10.0); + let relaxation = compute_relaxation(snap); + snap.relaxation = Some((relaxation * 10.0).round() / 10.0); + let focus = compute_focus(snap); + snap.focus = Some((focus * 10.0).round() / 10.0); + // GPU stats if let Some(ref gpu) = ctx.gpu { snap.gpu_overall = Some(gpu.overall as f64); @@ -219,6 +227,67 @@ pub fn focus_score(engagement_raw: f32) -> f64 { (100.0_f32 / (1.0 + (-2.0 * (engagement_raw - 0.8)).exp())) as f64 } +// ── Canonical engagement / relaxation / focus ──────────────────────────────── +// +// Single source of truth for these three composite scores. Every consumer +// (live `latest_bands`, persisted `metrics_json`, time-series cache, websocket +// broadcasts, frontend, VS Code extension, widgets) reads identical values via +// `enrich_band_snapshot` populating `snap.engagement` / `snap.relaxation` / +// `snap.focus`. Do not re-derive these in caller code — read the snapshot. + +/// Sigmoid (0,∞) → (0,100): `100 / (1 + exp(−k·(x − mid)))`. +/// +/// Shared by both `compute_engagement` and `compute_relaxation`. Identical +/// shape to `EpochMetrics::sigmoid100` — duplicated only to keep this crate +/// dependency-free of `skill-exg`. +fn sigmoid_0_100(x: f32, k: f32, mid: f32) -> f64 { + (100.0_f32 / (1.0 + (-k * (x - mid)).exp())) as f64 +} + +/// Engagement score (0–100) — final, sigmoided. +/// +/// Per-channel β / (α + θ), with a `0.5` neutral fallback for channels whose +/// (α + θ) collapses to zero (poor electrode contact, missing band power, …). +/// Without the fallback, low-signal channels would drag the average toward +/// zero and pin the score at a constant ~16.8 — the historical "engagement +/// doesn't move" bug. +pub fn compute_engagement(snap: &BandSnapshot) -> f64 { + sigmoid_0_100(compute_engagement_raw(snap), 2.0, 0.8) +} + +/// Relaxation score (0–100) — final, sigmoided. +/// +/// Per-channel α / (β + θ), with the same `0.5` neutral fallback for +/// degenerate channels. Theta is in the denominator (matches Putman 2010 / +/// Angelidis 2016) — earlier sites that used `α / (α + β)` are deprecated. +pub fn compute_relaxation(snap: &BandSnapshot) -> f64 { + if snap.channels.is_empty() { + return sigmoid_0_100(0.5, 2.5, 1.0); + } + let n = snap.channels.len() as f32; + let raw: f32 = snap + .channels + .iter() + .map(|ch| { + let d = ch.rel_beta + ch.rel_theta; + if d > 1e-6 { + ch.rel_alpha / d + } else { + 0.5 + } + }) + .sum::() + / n; + sigmoid_0_100(raw, 2.5, 1.0) +} + +/// Focus score (0–100). Currently the same as engagement; kept distinct so +/// the UI can surface a "focus" label and so the formula can diverge later +/// without another rename across consumers. +pub fn compute_focus(snap: &BandSnapshot) -> f64 { + focus_score(compute_engagement_raw(snap)) +} + // ── Battery EMA ─────────────────────────────────────────────────────────────── /// Exponential moving average for battery level with low-battery alerts. @@ -534,6 +603,9 @@ mod tests { meditation: None, cognitive_load: None, drowsiness: None, + engagement: None, + relaxation: None, + focus: None, temperature_raw: None, gpu_overall: None, gpu_render: None, @@ -572,6 +644,97 @@ mod tests { assert!(focus_score(1.0) <= 100.0); } + /// End-to-end sanity: enriching a snapshot must populate engagement / + /// relaxation / focus, and the values must equal the canonical compute_* + /// functions. Locks down the single-source-of-truth contract so any future + /// regression where a caller computes its own metric will fail loudly. + #[test] + fn enrich_populates_canonical_engagement_relaxation_focus() { + let mut snap = test_snap(); + let ctx = SnapshotContext { + ppg: None, + artifacts: None, + head_pose: None, + temperature_raw: 0, + gpu: None, + }; + // Canonical values, computed *before* enrichment so the snapshot is + // unmutated — proves the enrich path doesn't have hidden state. + let want_engagement = compute_engagement(&snap); + let want_relaxation = compute_relaxation(&snap); + let want_focus = compute_focus(&snap); + + enrich_band_snapshot(&mut snap, &ctx); + + let got_e = snap.engagement.expect("engagement populated"); + let got_r = snap.relaxation.expect("relaxation populated"); + let got_f = snap.focus.expect("focus populated"); + + // Snapshot values are rounded to 1 decimal; canonical values are not. + assert!( + (got_e - want_engagement).abs() < 0.05, + "engagement mismatch: {got_e} vs {want_engagement}" + ); + assert!( + (got_r - want_relaxation).abs() < 0.05, + "relaxation mismatch: {got_r} vs {want_relaxation}" + ); + assert!( + (got_f - want_focus).abs() < 0.05, + "focus mismatch: {got_f} vs {want_focus}" + ); + + // Sanity: scores are 0–100. + for (name, v) in [("engagement", got_e), ("relaxation", got_r), ("focus", got_f)] { + assert!((0.0..=100.0).contains(&v), "{name}={v} out of range"); + } + } + + /// Reproduces the stuck-engagement failure mode: channels with + /// `rel_alpha + rel_theta ≈ 0`. Pre-refactor this drove engagement to a + /// constant ~16.8. Post-refactor the per-channel 0.5 fallback keeps the + /// score at the neutral midpoint and — critically — makes it *move* when + /// other channels recover signal. + #[test] + fn engagement_does_not_collapse_on_zero_alpha_theta() { + // All channels: alpha=0, theta=0, beta>0. Pre-refactor: stuck-low ~16.8. + let mut snap = test_snap(); + for ch in &mut snap.channels { + ch.alpha = 0.0; + ch.theta = 0.0; + ch.beta = 1.0; + ch.rel_alpha = 0.0; + ch.rel_theta = 0.0; + ch.rel_beta = 1.0; + } + + let stuck_low = compute_engagement(&snap); + // Should be at the neutral-fallback midpoint, not pinned at ~16.8. + assert!( + stuck_low > 30.0, + "engagement collapsed to {stuck_low} on zero-α+θ channels" + ); + + // Now flip one channel to a high-engagement profile and verify the + // score *moves* — the original bug was that it didn't. + snap.channels[0].rel_alpha = 0.10; + snap.channels[0].rel_theta = 0.10; + // Same rel_beta=1.0 → β/(α+θ) = 5 → strong engagement signal. + let moved = compute_engagement(&snap); + assert!( + moved > stuck_low + 1.0, + "engagement did not move: {stuck_low} -> {moved}" + ); + } + + /// Storage-side parity: `EpochMetrics::from_snapshot` (in `skill-exg`) + /// must produce the same engagement/relaxation as the canonical compute + /// functions. We can't import skill-exg here without a dep cycle, so this + /// test lives in `skill-exg`'s own test suite — see + /// `crates/skill-exg/src/lib.rs::tests::epoch_metrics_match_canonical`. + #[test] + fn _see_epoch_metrics_match_canonical_in_skill_exg() {} + #[test] fn battery_ema_first_reading() { let mut b = BatteryEma::new(0.1); diff --git a/crates/skill-eeg/src/eeg_bands.rs b/crates/skill-eeg/src/eeg_bands.rs index 5c9805af..283aa922 100644 --- a/crates/skill-eeg/src/eeg_bands.rs +++ b/crates/skill-eeg/src/eeg_bands.rs @@ -289,6 +289,18 @@ pub struct BandSnapshot { /// Drowsiness score (0–100). High TAR + alpha spindles. #[serde(skip_serializing_if = "Option::is_none")] pub drowsiness: Option, + /// Engagement score (0–100). Per-channel β / (α + θ), per-channel `0.5` + /// fallback for low-signal channels, then sigmoid. + #[serde(skip_serializing_if = "Option::is_none")] + pub engagement: Option, + /// Relaxation score (0–100). Per-channel α / (β + θ), per-channel `0.5` + /// fallback for low-signal channels, then sigmoid. + #[serde(skip_serializing_if = "Option::is_none")] + pub relaxation: Option, + /// Focus score (0–100). Currently identical to `engagement`; kept as a + /// distinct field for UI semantics and future divergence. + #[serde(skip_serializing_if = "Option::is_none")] + pub focus: Option, // ── Device telemetry ───────────────────────────────────────────────────── /// Raw temperature ADC value from headset (Classic firmware only). @@ -1001,6 +1013,9 @@ impl BandAnalyzer { meditation: None, cognitive_load: None, drowsiness: None, + engagement: None, + relaxation: None, + focus: None, temperature_raw: None, gpu_overall: None, gpu_render: None, diff --git a/crates/skill-exg/Cargo.toml b/crates/skill-exg/Cargo.toml index 96dc4495..d0f8f538 100644 --- a/crates/skill-exg/Cargo.toml +++ b/crates/skill-exg/Cargo.toml @@ -17,6 +17,7 @@ anyhow = { workspace = true } skill-constants = { path = "../skill-constants" } skill-eeg = { path = "../skill-eeg" } skill-data = { path = "../skill-data" } +skill-devices = { path = "../skill-devices" } serde = { version = "1", features = ["derive"] } serde_json = "1" hf-hub = "0.5" diff --git a/crates/skill-exg/src/lib.rs b/crates/skill-exg/src/lib.rs index bf504de1..840df519 100644 --- a/crates/skill-exg/src/lib.rs +++ b/crates/skill-exg/src/lib.rs @@ -776,6 +776,10 @@ pub struct EpochMetrics { impl EpochMetrics { /// Derive metrics from a `BandSnapshot` by averaging across all channels. + /// + /// Engagement and relaxation delegate to `skill_devices::compute_engagement` + /// / `compute_relaxation` — the single source of truth shared with the live + /// `latest_bands` path. Storing here is fine; *computing* here is not. pub fn from_snapshot(snap: &BandSnapshot) -> Self { let n = snap.channels.len() as f32; if n < 1.0 { @@ -788,8 +792,6 @@ impl EpochMetrics { let mut rb = 0.0f32; let mut rg = 0.0f32; let mut rhg = 0.0f32; - let mut sum_relax = 0.0f32; - let mut sum_engage = 0.0f32; for ch in &snap.channels { rd += ch.rel_delta; @@ -798,17 +800,6 @@ impl EpochMetrics { rb += ch.rel_beta; rg += ch.rel_gamma; rhg += ch.rel_high_gamma; - let a = ch.rel_alpha; - let b = ch.rel_beta; - let t = ch.rel_theta; - let d1 = a + t; - let d2 = b + t; - if d2 > 1e-6 { - sum_relax += a / d2; - } - if d1 > 1e-6 { - sum_engage += b / d1; - } } rd /= n; rt /= n; @@ -832,8 +823,8 @@ impl EpochMetrics { rel_beta: rb, rel_gamma: rg, rel_high_gamma: rhg, - relaxation: Self::sigmoid100(sum_relax / n, 2.5, 1.0), - engagement: Self::sigmoid100(sum_engage / n, 2.0, 0.8), + relaxation: skill_devices::compute_relaxation(snap) as f32, + engagement: skill_devices::compute_engagement(snap) as f32, faa, tar: snap.tar, bar: snap.bar, @@ -1136,6 +1127,135 @@ mod tests { assert_eq!(back.rel_delta, m.rel_delta); } + /// Closes the single-source-of-truth loop: storage path + /// (`EpochMetrics::from_snapshot`) and live path + /// (`skill_devices::compute_engagement` / `compute_relaxation`) must + /// agree on the same `BandSnapshot`. Pre-refactor they diverged — this + /// test would have caught the stuck-engagement bug. + #[test] + fn epoch_metrics_match_canonical_compute() { + use skill_eeg::eeg_bands::{BandPowers, BandSnapshot}; + + let ch = BandPowers { + channel: "AF7".into(), + delta: 5.0, + theta: 3.0, + alpha: 4.0, + beta: 6.0, + gamma: 1.0, + high_gamma: 0.5, + rel_delta: 0.25, + rel_theta: 0.15, + rel_alpha: 0.20, + rel_beta: 0.30, + rel_gamma: 0.05, + rel_high_gamma: 0.05, + dominant: "beta".into(), + dominant_symbol: "β".into(), + dominant_color: "#22c55e".into(), + }; + let mut snap = BandSnapshot { + timestamp: 0.0, + channels: vec![ch.clone(), ch.clone(), ch.clone(), ch], + faa: 0.0, + tar: 0.5, + bar: 0.4, + dtr: 1.2, + pse: 0.7, + apf: 10.0, + bps: -1.5, + snr: 12.0, + coherence: 0.5, + mu_suppression: 0.1, + mood: 60.0, + tbr: 0.8, + sef95: 22.0, + spectral_centroid: 15.0, + hjorth_activity: 0.1, + hjorth_mobility: 0.2, + hjorth_complexity: 0.3, + permutation_entropy: 0.6, + higuchi_fd: 1.5, + dfa_exponent: 0.7, + sample_entropy: 0.4, + pac_theta_gamma: 0.1, + laterality_index: 0.05, + headache_index: 10.0, + migraine_index: 5.0, + consciousness_lzc: 50.0, + consciousness_wakefulness: 70.0, + consciousness_integration: 60.0, + hr: None, + rmssd: None, + sdnn: None, + pnn50: None, + lf_hf_ratio: None, + respiratory_rate: None, + spo2_estimate: None, + perfusion_index: None, + stress_index: None, + blink_count: None, + blink_rate: None, + head_pitch: None, + head_roll: None, + stillness: None, + nod_count: None, + shake_count: None, + meditation: None, + cognitive_load: None, + drowsiness: None, + engagement: None, + relaxation: None, + focus: None, + temperature_raw: None, + gpu_overall: None, + gpu_render: None, + gpu_tiler: None, + rel_delta: 0.25, + rel_theta: 0.15, + rel_alpha: 0.20, + rel_beta: 0.30, + rel_gamma: 0.05, + }; + + let metrics = EpochMetrics::from_snapshot(&snap); + let canonical_e = skill_devices::compute_engagement(&snap) as f32; + let canonical_r = skill_devices::compute_relaxation(&snap) as f32; + + assert!( + (metrics.engagement - canonical_e).abs() < 0.001, + "EpochMetrics.engagement={} diverges from canonical={canonical_e}", + metrics.engagement, + ); + assert!( + (metrics.relaxation - canonical_r).abs() < 0.001, + "EpochMetrics.relaxation={} diverges from canonical={canonical_r}", + metrics.relaxation, + ); + + // And confirm enrich_band_snapshot puts the same value on the wire format. + skill_devices::enrich_band_snapshot( + &mut snap, + &skill_devices::SnapshotContext { + ppg: None, + artifacts: None, + head_pose: None, + temperature_raw: 0, + gpu: None, + }, + ); + let on_snapshot_e = snap.engagement.unwrap(); + let on_snapshot_r = snap.relaxation.unwrap(); + assert!( + (on_snapshot_e as f32 - canonical_e).abs() < 0.05, + "snapshot.engagement={on_snapshot_e} diverges from canonical={canonical_e}", + ); + assert!( + (on_snapshot_r as f32 - canonical_r).abs() < 0.05, + "snapshot.relaxation={on_snapshot_r} diverges from canonical={canonical_r}", + ); + } + // ── validate_safetensors ───────────────────────────────────────────── #[test] diff --git a/crates/skill-headless/Cargo.toml b/crates/skill-headless/Cargo.toml index d7d82cb1..48f37281 100644 --- a/crates/skill-headless/Cargo.toml +++ b/crates/skill-headless/Cargo.toml @@ -8,10 +8,10 @@ description = "Headless browser engine — CDP-like API over wry/tao for navigat [dependencies] anyhow = { workspace = true } # Windowing — hidden window hosts the webview; provides event loop + proxy. -tao = { version = "0.35", default-features = false, features = ["rwh_06"] } +tao = { version = "0.34", default-features = false, features = ["rwh_06"] } # WebView — system webview with full JS, DOM, network stack. -wry = { version = "0.55" } +wry = { version = "0.54" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/skill-settings/src/keychain.rs b/crates/skill-settings/src/keychain.rs index 82f6865a..c6926edb 100644 --- a/crates/skill-settings/src/keychain.rs +++ b/crates/skill-settings/src/keychain.rs @@ -75,16 +75,108 @@ pub struct Secrets { pub neurosity_device_id: String, } -/// Load all secrets from the system keychain. +// ── Lazy per-secret accessors ───────────────────────────────────────────────── +// +// macOS prompts for keychain access whenever the calling binary's code +// signature doesn't match the ACL on a stored item. A fresh app build has +// a fresh signature, so eagerly reading every secret at startup produces +// one prompt per item per process, before the user has done anything. +// +// These accessors read individual entries on demand, so the prompt only +// appears when the user initiates an action that actually needs the secret +// (e.g. clicking "Connect Emotiv" or opening the device settings tab). +// +// Each accessor is a no-op in debug builds — see [`load_secrets`]. + +pub fn get_api_token() -> String { + if cfg!(debug_assertions) { + return String::new(); + } + get_secret(KEY_API_TOKEN) +} + +pub fn set_api_token(value: &str) { + if cfg!(debug_assertions) { + return; + } + set_secret(KEY_API_TOKEN, value); +} + +pub fn get_emotiv_credentials() -> (String, String) { + if cfg!(debug_assertions) { + return (String::new(), String::new()); + } + (get_secret(KEY_EMOTIV_CLIENT_ID), get_secret(KEY_EMOTIV_CLIENT_SECRET)) +} + +pub fn get_idun_api_token() -> String { + if cfg!(debug_assertions) { + return String::new(); + } + get_secret(KEY_IDUN_API_TOKEN) +} + +pub fn get_oura_access_token() -> String { + if cfg!(debug_assertions) { + return String::new(); + } + get_secret(KEY_OURA_ACCESS_TOKEN) +} + +pub fn get_neurosity_credentials() -> (String, String, String) { + if cfg!(debug_assertions) { + return (String::new(), String::new(), String::new()); + } + ( + get_secret(KEY_NEUROSITY_EMAIL), + get_secret(KEY_NEUROSITY_PASSWORD), + get_secret(KEY_NEUROSITY_DEVICE_ID), + ) +} + +pub fn get_neurosity_device_id() -> String { + if cfg!(debug_assertions) { + return String::new(); + } + get_secret(KEY_NEUROSITY_DEVICE_ID) +} + +/// Write device-API secrets supplied in `secrets` to the keychain. +/// +/// Empty fields are **ignored** rather than treated as deletion: if the user +/// denies a keychain prompt during the GET round-trip, the in-memory copy of +/// untouched secrets will be empty, and we don't want to clobber valid stored +/// values on the next save. Use [`set_api_token`] (or extend with explicit +/// delete helpers) when an empty value is genuinely meant to clear. /// -/// In debug builds the keychain is **skipped** entirely to avoid macOS -/// Keychain authorization dialogs on every `cargo run` / `tauri dev` -/// (the dev binary has a different code signature each build, so macOS -/// asks for permission every time). Secrets fall back to the JSON -/// settings file which still contains them in dev mode. +/// Used by the daemon's `set_device_api_config` route. +pub fn save_device_api_secrets(secrets: &Secrets) { + if cfg!(debug_assertions) { + return; + } + let pairs: &[(&str, &str)] = &[ + (KEY_EMOTIV_CLIENT_ID, &secrets.emotiv_client_id), + (KEY_EMOTIV_CLIENT_SECRET, &secrets.emotiv_client_secret), + (KEY_IDUN_API_TOKEN, &secrets.idun_api_token), + (KEY_OURA_ACCESS_TOKEN, &secrets.oura_access_token), + (KEY_NEUROSITY_EMAIL, &secrets.neurosity_email), + (KEY_NEUROSITY_PASSWORD, &secrets.neurosity_password), + (KEY_NEUROSITY_DEVICE_ID, &secrets.neurosity_device_id), + ]; + for &(key, value) in pairs { + if !value.is_empty() { + set_secret(key, value); + } + } +} + +/// Load all secrets eagerly from the keychain. +/// +/// Retained only for the legacy round-trip through [`save_secrets`] used by +/// the Tauri shell's `save_settings_now`. New code should use the per-secret +/// accessors above so prompts only fire on user-initiated actions. pub fn load_secrets() -> Secrets { if cfg!(debug_assertions) { - eprintln!("[keychain] skipping keychain in debug build"); return Secrets::default(); } Secrets { @@ -101,19 +193,32 @@ pub fn load_secrets() -> Secrets { /// Save all secrets to the system keychain. /// +/// Empty values are **ignored** rather than treated as a deletion request. +/// This avoids clobbering previously-stored secrets when the caller's +/// in-memory copy was never populated (e.g. lazy-load callers that don't +/// hydrate every field). Use the dedicated `set_*` helpers above to +/// explicitly delete a secret. +/// /// No-op in debug builds (see [`load_secrets`] for rationale). pub fn save_secrets(secrets: &Secrets) { if cfg!(debug_assertions) { return; } - set_secret(KEY_API_TOKEN, &secrets.api_token); - set_secret(KEY_EMOTIV_CLIENT_ID, &secrets.emotiv_client_id); - set_secret(KEY_EMOTIV_CLIENT_SECRET, &secrets.emotiv_client_secret); - set_secret(KEY_IDUN_API_TOKEN, &secrets.idun_api_token); - set_secret(KEY_OURA_ACCESS_TOKEN, &secrets.oura_access_token); - set_secret(KEY_NEUROSITY_EMAIL, &secrets.neurosity_email); - set_secret(KEY_NEUROSITY_PASSWORD, &secrets.neurosity_password); - set_secret(KEY_NEUROSITY_DEVICE_ID, &secrets.neurosity_device_id); + let pairs: &[(&str, &str)] = &[ + (KEY_API_TOKEN, &secrets.api_token), + (KEY_EMOTIV_CLIENT_ID, &secrets.emotiv_client_id), + (KEY_EMOTIV_CLIENT_SECRET, &secrets.emotiv_client_secret), + (KEY_IDUN_API_TOKEN, &secrets.idun_api_token), + (KEY_OURA_ACCESS_TOKEN, &secrets.oura_access_token), + (KEY_NEUROSITY_EMAIL, &secrets.neurosity_email), + (KEY_NEUROSITY_PASSWORD, &secrets.neurosity_password), + (KEY_NEUROSITY_DEVICE_ID, &secrets.neurosity_device_id), + ]; + for &(key, value) in pairs { + if !value.is_empty() { + set_secret(key, value); + } + } } /// Migrate plaintext secrets from settings JSON into the keychain. diff --git a/crates/skill-settings/src/lib.rs b/crates/skill-settings/src/lib.rs index c5533bb8..7fde7a36 100644 --- a/crates/skill-settings/src/lib.rs +++ b/crates/skill-settings/src/lib.rs @@ -1313,20 +1313,16 @@ pub fn load_settings(skill_dir: &Path) -> UserSettings { } } - // ── Load secrets from keychain (release) or keep JSON values (debug) ── - if !cfg!(debug_assertions) { - let secrets = keychain::load_secrets(); - s.api_token = secrets.api_token; - s.device_api.emotiv_client_id = secrets.emotiv_client_id; - s.device_api.emotiv_client_secret = secrets.emotiv_client_secret; - s.device_api.idun_api_token = secrets.idun_api_token; - s.device_api.oura_access_token = secrets.oura_access_token; - s.device_api.neurosity_email = secrets.neurosity_email; - s.device_api.neurosity_password = secrets.neurosity_password; - s.device_api.neurosity_device_id = secrets.neurosity_device_id; - } - // In debug mode, secrets stay as loaded from the JSON file — no keychain - // interaction, no macOS authorization prompts on every dev build. + // Secrets are deliberately **not** hydrated here. Loading every secret at + // startup triggers one macOS keychain prompt per item per process whenever + // the binary's code signature changes (i.e. on every release upgrade), and + // `load_settings` is called by both the Tauri shell and the daemon during + // boot. Callers that actually need a secret read it on demand from + // `keychain::get_*`, so a prompt only appears when the user initiates an + // action that requires that specific secret. + // + // In debug builds, secrets stay as loaded from the JSON file (the JSON + // round-trip is preserved by `skip_secret_in_release` returning false). s } diff --git a/scripts/create-windows-nsis.ps1 b/scripts/create-windows-nsis.ps1 index 84de391c..46b0eaa6 100644 --- a/scripts/create-windows-nsis.ps1 +++ b/scripts/create-windows-nsis.ps1 @@ -56,6 +56,21 @@ $Conf = Get-Content (Join-Path $TauriDir "tauri.conf.json") -Raw | ConvertFrom-J $ProductName = $Conf.productName $ProductDisplayName = if ($ProductName.EndsWith("™")) { $ProductName } else { "$ProductName™" } $Version = $Conf.version + +# NSIS's VIProductVersion requires strict 4-segment numeric format X.X.X.X +# (Win32 VS_FIXEDFILEINFO). User-facing ProductVersion/FileVersion strings +# accept any text, but VIProductVersion does not — it rejects "-rc.N" suffixes. +# Map the SemVer string to a numeric 4-tuple: +# "0.0.130" -> "0.0.130.0" +# "0.0.130-rc.2" -> "0.0.130.2" (use RC number as fourth segment) +# "0.0.130-beta.7" -> "0.0.130.7" +if ($Version -match '^(\d+\.\d+\.\d+)(?:-[A-Za-z]+\.(\d+))?') { + $vibase = $Matches[1] + $vibuild = if ($Matches[2]) { $Matches[2] } else { "0" } + $VIVersion = "$vibase.$vibuild" +} else { + $VIVersion = "0.0.0.0" +} $Identifier = $Conf.identifier $BinaryName = "skill.exe" $TargetReleaseDir = Join-Path $TauriDir "target/$Target/release" @@ -531,7 +546,7 @@ $imageDirectives !insertmacro MUI_LANGUAGE "English" ; ── Version info ──────────────────────────────────────────────────────── -VIProductVersion "$Version.0" +VIProductVersion "$VIVersion" VIAddVersionKey "ProductName" "$ProductDisplayName" VIAddVersionKey "ProductVersion" "$Version" VIAddVersionKey "FileVersion" "$Version" diff --git a/scripts/release.js b/scripts/release.js index 00136269..c9a92b34 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -19,9 +19,27 @@ // works if no rebuild happens at promotion time. import { execSync, spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { baseVersion, bumpVersion } from "./version-utils.mjs"; +// GitHub caps PR/issue bodies at 65_536 chars. Leave headroom for the +// surrounding template; truncate the embedded notes if they exceed this. +const NOTES_MAX_CHARS = 50_000; + +function readReleaseNotes(version) { + const path = `changes/releases/${version}.md`; + if (!existsSync(path)) return null; + let body = readFileSync(path, "utf8").trim(); + // The file leads with `## [] — ` which is redundant with the + // PR's surrounding heading; strip it so the embedded section starts at the + // first content heading (Features / Bugfixes / etc.). + body = body.replace(/^##\s+\[[^\]]+\][^\n]*\n+/, ""); + if (body.length > NOTES_MAX_CHARS) { + body = `${body.slice(0, NOTES_MAX_CHARS)}\n\n_…notes truncated — see \`changes/releases/${version}.md\` for the full text._`; + } + return body; +} + // ── Shell + git helpers ───────────────────────────────────────────────────── function sh(cmd, args, opts = {}) { @@ -231,9 +249,11 @@ async function main() { prs = JSON.parse(prList.stdout || "[]"); } catch {} + const notes = readReleaseNotes(newVersion); + if (prs.length === 0) { log("gh pr create"); - const body = [ + const sections = [ `## Release v${base}`, "", `Tracking release candidates for **v${base}**.`, @@ -251,7 +271,11 @@ async function main() { `- ${tag}`, "", "_(more added as RCs are cut)_", - ].join("\n"); + ]; + if (notes) { + sections.push("", "---", "", `## What's in this release (\`${tag}\`)`, "", notes); + } + const body = sections.join("\n"); sh( "gh", [ @@ -273,11 +297,15 @@ async function main() { } else { const pr = prs[0]; log(`gh pr comment ${pr.number}`); - const body = [ + const sections = [ `🚀 New RC: \`${tag}\``, "", "CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.", - ].join("\n"); + ]; + if (notes) { + sections.push("", "
Release notes for this RC", "", notes, "", "
"); + } + const body = sections.join("\n"); sh("gh", ["pr", "comment", String(pr.number), "--body", body], { check: true }); } diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 9474c361..c46fff93 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -1,40 +1,135 @@ #!/usr/bin/env bash # smoke-test.sh — Launch the Skill app and run test.ts once it's ready. # +# Two modes, auto-selected: +# • headless (default in CI / non-TTY): app runs in the background, logs to +# a file; test.ts runs in the foreground with a bounded discovery timeout; +# the app is terminated on exit and the test's exit status propagates. +# • tmux (default in interactive shells): app + test.ts run in a split-pane +# tmux session you can attach to. Same behaviour as before. +# +# Override the mode with SMOKE_MODE=headless|tmux. Override the headless +# discovery + run timeout with SMOKE_TIMEOUT_SECS (default 180). +# # Usage: -# ./smoke-test.sh # auto-discover port via mDNS (retries until Ctrl-C) +# ./smoke-test.sh # auto-discover port # ./smoke-test.sh 62853 # pass explicit port to test.ts # ./smoke-test.sh --http # forward flags to test.ts # ./smoke-test.sh 62853 --ws # combine port + flags # -# Requires: tmux, Node ≥ 18 +# Requires: Node ≥ 18 (tmux only used in interactive mode). set -euo pipefail -SESSION="smoke" DIR="$(cd "$(dirname "$0")/.." && pwd)" -TEST_ARGS="${*:-}" # forward all args to test.ts - -# Kill previous session if it exists -tmux kill-session -t "$SESSION" 2>/dev/null || true - -tmux new-session -d -s "$SESSION" -c "$DIR" \ - "echo '═══ Starting Skill app ═══'; npm run tauri dev; echo '═══ App exited ═══'; read" \; \ - split-window -h -c "$DIR" "\ - echo '═══ Waiting for Skill to start… ═══' - sleep 5 - npx tsx test.ts $TEST_ARGS - STATUS=\$? - echo '' - if [ \$STATUS -eq 0 ]; then - echo '══════════════════════════' - echo ' ✓ SMOKE TEST PASSED' - echo '══════════════════════════' - else - echo '══════════════════════════' - echo ' ✗ SMOKE TEST FAILED' - echo '══════════════════════════' +TIMEOUT_SECS="${SMOKE_TIMEOUT_SECS:-180}" + +# ── Mode selection ──────────────────────────────────────────────────────────── +# +# Pick headless when stdout isn't a TTY (CI, log capture), or when CI=true, +# or when tmux is unavailable. Otherwise use the tmux split-pane. +choose_mode() { + if [ -n "${SMOKE_MODE:-}" ]; then + echo "$SMOKE_MODE" + return + fi + if [ "${CI:-}" = "true" ] || [ ! -t 1 ] || ! command -v tmux >/dev/null 2>&1; then + echo "headless" + else + echo "tmux" + fi +} +MODE="$(choose_mode)" + +# ── Headless mode ───────────────────────────────────────────────────────────── +run_headless() { + cd "$DIR" + local app_log + app_log="$(mktemp -t skill-smoke-app.XXXXXX.log)" + + echo "→ smoke (headless) — log: $app_log timeout: ${TIMEOUT_SECS}s" + + # Enable job control so the background `npm run tauri dev` becomes its own + # process group leader (PGID = PID). Without this, the npm → tauri → cargo → + # app chain inherits the script's PGID and a single SIGTERM only hits npm, + # leaving cargo + the app holding the listening port. + set -m + npm run tauri dev >"$app_log" 2>&1 & + local app_pid=$! + set +m + echo "→ app pid: $app_pid (process group leader)" + + cleanup() { + if kill -0 "$app_pid" 2>/dev/null; then + echo "→ stopping app (PID $app_pid)" + # Kill the whole process group: `npm run tauri dev` spawns a chain + # (npm → tauri → cargo → app), and SIGTERM on the parent alone leaves + # the cargo+app children orphaned to occupy the port. + kill -TERM -- "-$app_pid" 2>/dev/null || kill -TERM "$app_pid" 2>/dev/null || true + for _ in 1 2 3 4 5 6 7 8 9 10; do + kill -0 "$app_pid" 2>/dev/null || break + sleep 1 + done + kill -KILL -- "-$app_pid" 2>/dev/null || kill -KILL "$app_pid" 2>/dev/null || true fi - echo 'Press Enter to close.'; read - exit \$STATUS" \; \ - attach + } + trap cleanup EXIT INT TERM + + # Hand the discovery timeout to test.ts so its retry loop exits cleanly + # if the app fails to register on mDNS. Reserve ~10s for the test run + # itself to start, but never less than 30s. + local discover_secs=$(( TIMEOUT_SECS - 10 )) + if [ "$discover_secs" -lt 30 ]; then discover_secs=30; fi + + local status=0 + SKILL_DISCOVER_TIMEOUT_SECS="$discover_secs" \ + npx tsx test.ts "$@" || status=$? + + echo + if [ "$status" -eq 0 ]; then + echo "══════════════════════════" + echo " ✓ SMOKE TEST PASSED" + echo "══════════════════════════" + else + echo "══════════════════════════" + echo " ✗ SMOKE TEST FAILED (exit $status)" + echo "──── App log (last 100 lines) ────" + tail -n 100 "$app_log" || true + echo "══════════════════════════" + fi + exit "$status" +} + +# ── Interactive tmux mode ───────────────────────────────────────────────────── +run_tmux() { + local session="smoke" + local test_args + test_args="$*" + tmux kill-session -t "$session" 2>/dev/null || true + tmux new-session -d -s "$session" -c "$DIR" \ + "echo '═══ Starting Skill app ═══'; npm run tauri dev; echo '═══ App exited ═══'; read" \; \ + split-window -h -c "$DIR" "\ + echo '═══ Waiting for Skill to start… ═══' + sleep 5 + npx tsx test.ts $test_args + STATUS=\$? + echo '' + if [ \$STATUS -eq 0 ]; then + echo '══════════════════════════' + echo ' ✓ SMOKE TEST PASSED' + echo '══════════════════════════' + else + echo '══════════════════════════' + echo ' ✗ SMOKE TEST FAILED' + echo '══════════════════════════' + fi + echo 'Press Enter to close.'; read + exit \$STATUS" \; \ + attach +} + +case "$MODE" in + headless) run_headless "$@" ;; + tmux) run_tmux "$@" ;; + *) echo "unknown SMOKE_MODE: $MODE (expected: headless | tmux)" >&2; exit 2 ;; +esac diff --git a/scripts/test-all.sh b/scripts/test-all.sh index cfff6ecd..a8aa6d10 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -154,11 +154,10 @@ for suite in "${SUITES[@]}"; do run_suite "Windows manifest" node scripts/check-windows-manifest.mjs || { $STOP_ON_FAIL && break; } ;; smoke) - if command -v tmux >/dev/null 2>&1 && [ -t 0 ]; then - run_suite "smoke test" bash scripts/smoke-test.sh || { $STOP_ON_FAIL && break; } - else - skip_suite "smoke test" "requires tmux + interactive terminal" - fi + # smoke-test.sh auto-selects headless mode when stdout isn't a TTY or + # CI=true, so it runs unattended in CI / piped shells. Interactive + # terminals get the tmux split-pane unless overridden by SMOKE_MODE. + run_suite "smoke test" bash scripts/smoke-test.sh || { $STOP_ON_FAIL && break; } ;; daemon) if ls src-tauri/target/*/release/bundle/dmg/*.dmg >/dev/null 2>&1 || \ diff --git a/src-tauri/src/helpers.rs b/src-tauri/src/helpers.rs index 564f9015..9fdf8d1c 100644 --- a/src-tauri/src/helpers.rs +++ b/src-tauri/src/helpers.rs @@ -11,7 +11,7 @@ use serde::Serialize; use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_notification::NotificationExt; -use crate::settings::{save_secrets_from_settings, settings_path}; +use crate::settings::settings_path; use crate::state::*; use crate::ws_server::WsBroadcaster; use crate::MutexExt; @@ -269,13 +269,14 @@ pub(crate) fn save_settings_now(app: &AppHandle) { // Infrastructure / server config data.ws_host = s.ws_host.clone(); data.ws_port = s.ws_port; - data.api_token = s.api_token.clone(); + // Secrets (api_token, device_api credentials) are owned by the daemon's + // route handlers and stored exclusively in the system keychain — Tauri + // no longer round-trips them through AppState. See keychain::get_*. data.hf_endpoint = s.hf_endpoint.clone(); data.update_check_interval_secs = s.update_check_interval_secs; // Hardware / device config data.openbci = s.openbci_config.clone(); - data.device_api = s.device_api_config.clone(); data.neutts = s.neutts_config.clone(); data.tts_preload = s.tts_preload; data.screenshot = s.screenshot_config.clone(); @@ -304,9 +305,6 @@ pub(crate) fn save_settings_now(app: &AppHandle) { drop(s); - // Persist secrets to the system keychain (encrypted, survives updates). - save_secrets_from_settings(&data); - if let Ok(json) = serde_json::to_string_pretty(&data) { if let Err(e) = std::fs::write(&path, &json) { eprintln!("[settings] save error: {e}"); diff --git a/src-tauri/src/setup.rs b/src-tauri/src/setup.rs index b41550a7..09d88baa 100644 --- a/src-tauri/src/setup.rs +++ b/src-tauri/src/setup.rs @@ -391,11 +391,10 @@ fn load_and_apply_settings(app: &mut tauri::App, skill_dir: &std::path::Path) { s.hooks = data.hooks; s.ws_host = data.ws_host.clone(); s.ws_port = data.ws_port; - s.api_token = data.api_token.clone(); + // Secrets stay in the keychain; Tauri no longer caches them in AppState. s.hf_endpoint = data.hf_endpoint.clone(); s.update_check_interval_secs = data.update_check_interval_secs; s.openbci_config = data.openbci; - s.device_api_config = data.device_api; s.scanner_config = data.scanner; s.location_enabled = data.location_enabled; s.inference_device = data.inference_device.clone(); diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 24d001a3..9d61eaed 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -376,7 +376,6 @@ pub struct AppState { // ── Network / services ──────────────────────────────────────────────── pub ws_host: String, pub ws_port: u16, - pub api_token: String, pub hf_endpoint: String, pub update_check_interval_secs: u64, /// Set by the frontend when an update has been downloaded and is ready @@ -385,7 +384,6 @@ pub struct AppState { // ── Device configs ──────────────────────────────────────────────────── pub openbci_config: crate::settings::OpenBciConfig, - pub device_api_config: crate::settings::DeviceApiConfig, pub scanner_config: crate::settings::ScannerConfig, /// Location services enabled by the user (default false). @@ -497,7 +495,6 @@ impl Default for AppState { )), ws_host: default_ws_host(), ws_port: default_ws_port(), - api_token: String::new(), hf_endpoint: skill_settings::default_hf_endpoint(), update_check_interval_secs: default_update_check_interval(), update_ready_to_install: false, @@ -506,7 +503,6 @@ impl Default for AppState { inference_device: skill_settings::default_inference_device(), llm_gpu_layers_saved: skill_settings::default_llm_gpu_layers_saved(), exg_inference_device: skill_settings::default_exg_inference_device(), - device_api_config: crate::settings::DeviceApiConfig::default(), scanner_config: crate::settings::ScannerConfig::default(), neutts_config: NeuttsConfig::default(), tts_preload: true, diff --git a/src-tauri/src/window_cmds.rs b/src-tauri/src/window_cmds.rs index f77ed190..bad14420 100644 --- a/src-tauri/src/window_cmds.rs +++ b/src-tauri/src/window_cmds.rs @@ -46,6 +46,24 @@ impl<'a> Default for WindowSpec<'a> { } } +/// Clamp a requested logical inner size so it fits on the primary monitor. +/// +/// Without this, windows configured larger than the user's screen (e.g. the +/// 880-tall onboarding window on a 13" laptop) open partially off-screen with +/// their footer controls unreachable. +fn clamp_to_monitor(app: &AppHandle, requested: (f64, f64)) -> (f64, f64) { + // Reserve room for menubar/taskbar/dock so the title bar stays visible. + const CHROME_MARGIN: f64 = 80.0; + const FLOOR: f64 = 320.0; + let Ok(Some(monitor)) = app.primary_monitor() else { + return requested; + }; + let scale = monitor.scale_factor(); + let max_w = (monitor.size().width as f64 / scale - CHROME_MARGIN).max(FLOOR); + let max_h = (monitor.size().height as f64 / scale - CHROME_MARGIN).max(FLOOR); + (requested.0.min(max_w), requested.1.min(max_h)) +} + /// Focus an existing window or create a new one from `spec`. /// /// Deduplicates the repeated "check-existing → unminimize/show/focus → or build new" @@ -57,20 +75,23 @@ pub(crate) fn focus_or_create(app: &AppHandle, spec: WindowSpec) -> Result<(), S let _ = win.set_focus(); return Ok(()); } + let (inner_w, inner_h) = clamp_to_monitor(app, spec.inner_size); let mut builder = tauri::WebviewWindowBuilder::new( app, spec.label, tauri::WebviewUrl::App(spec.route.into()), ) .title(spec.title) - .inner_size(spec.inner_size.0, spec.inner_size.1) + .inner_size(inner_w, inner_h) .resizable(spec.resizable) .center() .decorations(false) .transparent(true); if let Some((w, h)) = spec.min_inner_size { - builder = builder.min_inner_size(w, h); + // Min must not exceed the (possibly clamped) inner size, or the + // builder will silently grow the window past the screen. + builder = builder.min_inner_size(w.min(inner_w), h.min(inner_h)); } if spec.always_on_top { builder = builder.always_on_top(true); @@ -102,21 +123,21 @@ pub(crate) fn focus_or_create_with_emit( let _ = win.emit(event, payload.to_string()); return Ok(()); } - // Fall through to normal builder + let (inner_w, inner_h) = clamp_to_monitor(app, spec.inner_size); let mut builder = tauri::WebviewWindowBuilder::new( app, spec.label, tauri::WebviewUrl::App(spec.route.into()), ) .title(spec.title) - .inner_size(spec.inner_size.0, spec.inner_size.1) + .inner_size(inner_w, inner_h) .resizable(spec.resizable) .center() .decorations(false) .transparent(true); if let Some((w, h)) = spec.min_inner_size { - builder = builder.min_inner_size(w, h); + builder = builder.min_inner_size(w.min(inner_w), h.min(inner_h)); } builder diff --git a/test.ts b/test.ts index 00370299..9a5a3479 100644 --- a/test.ts +++ b/test.ts @@ -312,13 +312,24 @@ function fmt(v: unknown): string { async function discover(): Promise { if (PORT) return PORT; - // Retry discovery indefinitely until Ctrl-C. - // Each attempt tries mDNS (5s timeout) then lsof fallback. + // Bounded discovery for CI/headless callers. Set SKILL_DISCOVER_TIMEOUT_SECS + // to cap total wall time spent retrying mDNS + lsof. Unset = retry forever + // (the historical interactive behaviour — Ctrl-C to abort). + const timeoutEnv = process.env.SKILL_DISCOVER_TIMEOUT_SECS; + const deadlineMs = timeoutEnv ? Date.now() + Number(timeoutEnv) * 1000 : Number.POSITIVE_INFINITY; + let attempt = 0; while (true) { + if (Date.now() > deadlineMs) { + throw new Error( + `discovery timed out after ${timeoutEnv}s — app did not register on mDNS or open a listening port`, + ); + } attempt++; if (attempt === 1) { - info("discovering Skill port (retries until Ctrl-C)…"); + info(timeoutEnv + ? `discovering Skill port (timeout ${timeoutEnv}s)…` + : "discovering Skill port (retries until Ctrl-C)…"); } else { info(`discovery attempt #${attempt} — retrying in 3s…`); await new Promise(r => setTimeout(r, 3000)); From 4357697031a6c2317f477215afa0414834b17fa2 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 16:08:55 -0400 Subject: [PATCH 06/30] 0.0.130-rc.3 --- CHANGELOG.md | 7 ++ Cargo.lock | 6 +- changes/releases/0.0.130-rc.3.md | 6 ++ crates/skill-settings/src/keychain.rs | 116 ++++++++++++++++---------- deny.toml | 2 - package.json | 2 +- scripts/release.js | 79 +++++++++++++++++- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 9 files changed, 170 insertions(+), 52 deletions(-) create mode 100644 changes/releases/0.0.130-rc.3.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a7a4e0..87de3c36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5402,3 +5402,10 @@ The heatmap merges EEG data points with the closest timeline events to show whic ### Features - fix win/linux + +## [0.0.130-rc.3] — 2026-04-29 + +### Features + +- fixed issues with the metrics, keychain access (3 -> 1 + on demand), windows CI +- umap e2e diff --git a/Cargo.lock b/Cargo.lock index 942526bf..f8b5cfe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7290,9 +7290,9 @@ dependencies = [ [[package]] name = "llmfit-core" -version = "0.9.16" +version = "0.9.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5eb09b5be6bade227582226bd0d74069abd88460756af4e93a8dfe97c38d57c" +checksum = "ab3d28d163be4423375ed1f7fb0bdc23e40fd1fe56e7a5beb025a9240bb6b978" dependencies = [ "dirs", "http 1.4.0", @@ -12130,7 +12130,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.2" +version = "0.0.130-rc.3" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/releases/0.0.130-rc.3.md b/changes/releases/0.0.130-rc.3.md new file mode 100644 index 00000000..c2f9fb6b --- /dev/null +++ b/changes/releases/0.0.130-rc.3.md @@ -0,0 +1,6 @@ +## [0.0.130-rc.3] — 2026-04-29 + +### Features + +- fixed issues with the metrics, keychain access (3 -> 1 + on demand), windows CI +- umap e2e diff --git a/crates/skill-settings/src/keychain.rs b/crates/skill-settings/src/keychain.rs index c6926edb..dcfef5b8 100644 --- a/crates/skill-settings/src/keychain.rs +++ b/crates/skill-settings/src/keychain.rs @@ -10,11 +10,59 @@ //! Secrets survive app re-installs and build updates because they live in //! the system credential store, not in the app data directory. +#[cfg(not(debug_assertions))] use keyring::Entry; /// Service name used as the keychain namespace for all NeuroSkill secrets. +#[cfg(not(debug_assertions))] const SERVICE: &str = "com.neuroskill.skill"; +// ── Debug-build in-memory store ────────────────────────────────────────────── +// +// Debug builds (`cargo run`, `tauri dev`, `cargo test`) deliberately avoid the +// OS keychain — every rebuild produces a binary with a different code +// signature, which on macOS triggers a fresh authorization prompt. The dev +// loop becomes unbearable. +// +// Pre-this commit the workaround was to short-circuit getters to `""` and +// setters to no-op, but that broke any code (including unit tests) that +// expected `set` then `get` to roundtrip. We now keep a process-local +// `Mutex` instead — no OS prompt, but values survive within the same +// process so the route handlers behave like real keychain code. +// +// Release builds bypass this entirely and use `keyring::Entry`. + +#[cfg(debug_assertions)] +mod dev_store { + use std::collections::HashMap; + use std::sync::Mutex; + use std::sync::OnceLock; + + static STORE: OnceLock>> = OnceLock::new(); + + fn store() -> &'static Mutex> { + STORE.get_or_init(|| Mutex::new(HashMap::new())) + } + + pub fn get(key: &str) -> String { + store() + .lock() + .ok() + .and_then(|g| g.get(key).cloned()) + .unwrap_or_default() + } + + pub fn set(key: &str, value: &str) { + if let Ok(mut g) = store().lock() { + if value.is_empty() { + g.remove(key); + } else { + g.insert(key.to_string(), value.to_string()); + } + } + } +} + // ── Key names ───────────────────────────────────────────────────────────────── const KEY_API_TOKEN: &str = "api_token"; @@ -27,7 +75,13 @@ const KEY_NEUROSITY_PASSWORD: &str = "neurosity_password"; const KEY_NEUROSITY_DEVICE_ID: &str = "neurosity_device_id"; // ── Low-level helpers ───────────────────────────────────────────────────────── +// +// In debug builds these route through `dev_store` (process-local, no OS +// keychain access). In release they hit the real OS keychain. Per-secret +// helpers above don't need their own `cfg!(debug_assertions)` checks — the +// switch happens here so the callers behave identically in both modes. +#[cfg(not(debug_assertions))] fn get_secret(key: &str) -> String { match Entry::new(SERVICE, key).and_then(|e| e.get_password()) { Ok(v) => v, @@ -39,6 +93,12 @@ fn get_secret(key: &str) -> String { } } +#[cfg(debug_assertions)] +fn get_secret(key: &str) -> String { + dev_store::get(key) +} + +#[cfg(not(debug_assertions))] fn set_secret(key: &str, value: &str) { let entry = match Entry::new(SERVICE, key) { Ok(e) => e, @@ -53,13 +113,16 @@ fn set_secret(key: &str, value: &str) { Ok(()) | Err(keyring::Error::NoEntry) => {} Err(e) => eprintln!("[keychain] failed to delete {key}: {e}"), } - } else { - if let Err(e) = entry.set_password(value) { - eprintln!("[keychain] failed to store {key}: {e}"); - } + } else if let Err(e) = entry.set_password(value) { + eprintln!("[keychain] failed to store {key}: {e}"); } } +#[cfg(debug_assertions)] +fn set_secret(key: &str, value: &str) { + dev_store::set(key, value); +} + // ── Public API ──────────────────────────────────────────────────────────────── /// All secret fields managed by the keychain. @@ -82,51 +145,34 @@ pub struct Secrets { // a fresh signature, so eagerly reading every secret at startup produces // one prompt per item per process, before the user has done anything. // -// These accessors read individual entries on demand, so the prompt only -// appears when the user initiates an action that actually needs the secret -// (e.g. clicking "Connect Emotiv" or opening the device settings tab). -// -// Each accessor is a no-op in debug builds — see [`load_secrets`]. +// These accessors read individual entries on demand, so the OS keychain +// prompt only appears when the user initiates an action that actually needs +// the secret (e.g. clicking "Connect Emotiv" or opening the device settings +// tab). In debug builds the low-level helpers route through `dev_store` +// instead of the OS keychain, so dev/test workflows roundtrip values without +// any auth dialogs. pub fn get_api_token() -> String { - if cfg!(debug_assertions) { - return String::new(); - } get_secret(KEY_API_TOKEN) } pub fn set_api_token(value: &str) { - if cfg!(debug_assertions) { - return; - } set_secret(KEY_API_TOKEN, value); } pub fn get_emotiv_credentials() -> (String, String) { - if cfg!(debug_assertions) { - return (String::new(), String::new()); - } (get_secret(KEY_EMOTIV_CLIENT_ID), get_secret(KEY_EMOTIV_CLIENT_SECRET)) } pub fn get_idun_api_token() -> String { - if cfg!(debug_assertions) { - return String::new(); - } get_secret(KEY_IDUN_API_TOKEN) } pub fn get_oura_access_token() -> String { - if cfg!(debug_assertions) { - return String::new(); - } get_secret(KEY_OURA_ACCESS_TOKEN) } pub fn get_neurosity_credentials() -> (String, String, String) { - if cfg!(debug_assertions) { - return (String::new(), String::new(), String::new()); - } ( get_secret(KEY_NEUROSITY_EMAIL), get_secret(KEY_NEUROSITY_PASSWORD), @@ -135,9 +181,6 @@ pub fn get_neurosity_credentials() -> (String, String, String) { } pub fn get_neurosity_device_id() -> String { - if cfg!(debug_assertions) { - return String::new(); - } get_secret(KEY_NEUROSITY_DEVICE_ID) } @@ -151,9 +194,6 @@ pub fn get_neurosity_device_id() -> String { /// /// Used by the daemon's `set_device_api_config` route. pub fn save_device_api_secrets(secrets: &Secrets) { - if cfg!(debug_assertions) { - return; - } let pairs: &[(&str, &str)] = &[ (KEY_EMOTIV_CLIENT_ID, &secrets.emotiv_client_id), (KEY_EMOTIV_CLIENT_SECRET, &secrets.emotiv_client_secret), @@ -176,9 +216,6 @@ pub fn save_device_api_secrets(secrets: &Secrets) { /// the Tauri shell's `save_settings_now`. New code should use the per-secret /// accessors above so prompts only fire on user-initiated actions. pub fn load_secrets() -> Secrets { - if cfg!(debug_assertions) { - return Secrets::default(); - } Secrets { api_token: get_secret(KEY_API_TOKEN), emotiv_client_id: get_secret(KEY_EMOTIV_CLIENT_ID), @@ -199,11 +236,7 @@ pub fn load_secrets() -> Secrets { /// hydrate every field). Use the dedicated `set_*` helpers above to /// explicitly delete a secret. /// -/// No-op in debug builds (see [`load_secrets`] for rationale). pub fn save_secrets(secrets: &Secrets) { - if cfg!(debug_assertions) { - return; - } let pairs: &[(&str, &str)] = &[ (KEY_API_TOKEN, &secrets.api_token), (KEY_EMOTIV_CLIENT_ID, &secrets.emotiv_client_id), @@ -228,9 +261,6 @@ pub fn save_secrets(secrets: &Secrets) { /// into the keychain. Returns `true` if any migration happened (caller /// should re-save settings to strip the plaintext values). pub fn migrate_plaintext_secrets(secrets: &Secrets) -> bool { - if cfg!(debug_assertions) { - return false; - } let mut migrated = false; let pairs: &[(&str, &str)] = &[ diff --git a/deny.toml b/deny.toml index a7c85393..ffaa52af 100644 --- a/deny.toml +++ b/deny.toml @@ -203,8 +203,6 @@ skip = [ { crate = "bitflags@1.3.2" }, { crate = "winreg@0.55.0" }, { crate = "winreg@0.56.0" }, - { crate = "wry@0.54.4" }, - { crate = "wry@0.55.0" }, { crate = "zip@2.4.2" }, { crate = "zip@4.6.1" }, { crate = "zip@7.2.0" }, diff --git a/package.json b/package.json index 6bd0ac39..d126722b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.2", + "version": "0.0.130-rc.3", "description": "", "type": "module", "scripts": { diff --git a/scripts/release.js b/scripts/release.js index c9a92b34..bc647b4d 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -77,6 +77,75 @@ function gitTracksRemote(b) { return captureOut(`git for-each-ref --format=%(upstream:short) refs/heads/${b}`).length > 0; } +function gitTagExistsLocal(tag) { + return sh("git", ["rev-parse", "--verify", `refs/tags/${tag}`], { capture: true }).status === 0; +} + +function gitTagExistsOnAnyRemote(tag) { + const remotes = captureOut("git remote") + .split("\n") + .map((s) => s.trim()) + .filter(Boolean); + for (const remote of remotes) { + const r = sh("git", ["ls-remote", "--tags", "--exit-code", remote, `refs/tags/${tag}`], { capture: true }); + if (r.status === 0) return true; + } + return false; +} + +function gitHeadPackageVersion() { + // Read package.json at HEAD to confirm the current commit is the bump for `currentVersion`. + const out = sh("git", ["show", "HEAD:package.json"], { capture: true }); + if (out.status !== 0) return null; + try { + return JSON.parse(out.stdout).version || null; + } catch { + return null; + } +} + +/// Self-heal a half-finished previous iteration: if `currentVersion`'s tag +/// is missing locally or on the remote, push the branch (if needed) and +/// create + push the tag before we try to bump. Without this, every aborted +/// push (failed pre-push hook, killed CI, network blip) wedges the release +/// branch until someone runs `npm run tag` by hand. +function ensureCurrentVersionTagged({ currentVersion, branchName, onReleaseBranch }) { + if (!onReleaseBranch) return; // Cutting from main — there's no prior version on this branch to tag. + + const tag = `v${currentVersion}`; + const haveLocal = gitTagExistsLocal(tag); + const haveRemote = haveLocal && gitTagExistsOnAnyRemote(tag); + + if (haveLocal && haveRemote) return; // Nothing to recover. + + // Sanity: HEAD's package.json must match `currentVersion`. If it doesn't, + // we're not on the bump commit and tagging here would produce a wrong tag. + const headVersion = gitHeadPackageVersion(); + if (headVersion !== currentVersion) { + fail( + `Cannot self-heal: HEAD's package.json version (${headVersion ?? "unknown"}) doesn't match the ` + + `current version (${currentVersion}). Resolve manually: tag the right commit, push, then re-run.`, + ); + } + + log(`recovering: tag ${tag} is missing — completing the previous iteration first`); + + // The remote-tag check requires HEAD's commit to be reachable on the remote, + // so the branch must be pushed before we push the tag. + if (!gitTracksRemote(branchName)) { + log(`git push -u origin ${branchName} (recovery)`); + sh("git", ["push", "-u", "origin", branchName], { check: true }); + } else { + log("git push (recovery)"); + sh("git", ["push"], { check: true }); + } + + log("npm run tag (recovery)"); + sh("npm", ["run", "tag"], { check: true }); + + ok(`recovered: ${tag} tagged and pushed; resuming next-RC iteration`); +} + function ensureGhReady() { if (sh("gh", ["--version"], { capture: true }).status !== 0) { fail("`gh` (GitHub CLI) not installed. Install with `brew install gh` then `gh auth login`."); @@ -210,7 +279,15 @@ async function main() { sh("git", ["checkout", "-b", branchName], { check: true }); } - // ── 2. Run bump (mutates files, runs preflight, creates commit) ──────── + // ── 2. Self-heal: tag any prior iteration that didn't get pushed ─────── + // The previous run can die mid-flight (failed pre-push hook, killed CI, + // network blip) after the bump commit but before `npm run tag`. That + // leaves the release branch in a state where bump's preflight refuses to + // run because the current version isn't tagged. Detect + recover here so + // the user doesn't need to remember the manual `npm run tag` dance. + ensureCurrentVersionTagged({ currentVersion, branchName, onReleaseBranch }); + + // ── 3. Run bump (mutates files, runs preflight, creates commit) ──────── const bumpArgs = ["run", "bump", "--", "--rc"]; if (force) bumpArgs.push("--force"); log(`npm ${bumpArgs.join(" ")}`); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0fa29920..5292c529 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.2" +version = "0.0.130-rc.3" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e6141199..6ec92b92 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.2", + "version": "0.0.130-rc.3", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From 2420f1a6d288bfc7b88677deee5b2d45553f5528 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 16:46:48 -0400 Subject: [PATCH 07/30] 0.0.130-rc.4 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- changes/releases/0.0.130-rc.4.md | 5 +++++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 changes/releases/0.0.130-rc.4.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 87de3c36..dacfc1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5409,3 +5409,9 @@ The heatmap merges EEG data points with the closest timeline events to show whic - fixed issues with the metrics, keychain access (3 -> 1 + on demand), windows CI - umap e2e + +## [0.0.130-rc.4] — 2026-04-29 + +### Features + +- Minor updates and improvements diff --git a/Cargo.lock b/Cargo.lock index f8b5cfe9..fda93c57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12130,7 +12130,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.3" +version = "0.0.130-rc.4" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/releases/0.0.130-rc.4.md b/changes/releases/0.0.130-rc.4.md new file mode 100644 index 00000000..5e46fef9 --- /dev/null +++ b/changes/releases/0.0.130-rc.4.md @@ -0,0 +1,5 @@ +## [0.0.130-rc.4] — 2026-04-29 + +### Features + +- Minor updates and improvements diff --git a/package.json b/package.json index d126722b..dcdfaa1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.3", + "version": "0.0.130-rc.4", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5292c529..428be0c1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.3" +version = "0.0.130-rc.4" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6ec92b92..6d0ece40 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.3", + "version": "0.0.130-rc.4", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From e5ce3bc8bbfdb35b7f364d6e11e66d2057264bcc Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 17:01:08 -0400 Subject: [PATCH 08/30] fix vulkan cache on windows ci --- .github/workflows/ci.yml | 4 ++++ .github/workflows/release-windows.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d02c7d62..7303180b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -610,6 +610,10 @@ jobs: # only happens once per change to the install script. On cache hit the # install script detects the SDK via filesystem and skips the download. - name: Cache Vulkan SDK + # Don't fail the job on transient cache-service errors (actions/cache@v5 + # occasionally returns "failure" instead of "miss"). The install script + # below handles a missing SDK by downloading it on the spot. + continue-on-error: true uses: actions/cache@v5 with: path: C:\VulkanSDK diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index ace22bb4..5a00b336 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -225,6 +225,10 @@ jobs: # toolchain. Installing them in a single step via background jobs cuts # ~1-2 min of sequential wait time on cache-miss runs. - name: Cache Vulkan SDK + # Don't fail the job on transient cache-service errors (actions/cache@v5 + # occasionally returns "failure" instead of "miss"). The install step + # below handles a missing SDK by downloading it on the spot. + continue-on-error: true uses: actions/cache@v5 with: path: C:\VulkanSDK From 922e7ea62e7365368a5c195383d3e3aa4c71a0ef Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 21:14:57 -0400 Subject: [PATCH 09/30] 0.0.130-rc.5 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 6 +++--- changes/releases/0.0.130-rc.5.md | 5 +++++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 changes/releases/0.0.130-rc.5.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dacfc1f8..314cc363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5415,3 +5415,9 @@ The heatmap merges EEG data points with the closest timeline events to show whic ### Features - Minor updates and improvements + +## [0.0.130-rc.5] — 2026-04-30 + +### Features + +- fix vulkan cache on windows ci diff --git a/Cargo.lock b/Cargo.lock index fda93c57..56cc5621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8490,9 +8490,9 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.16.0" +version = "4.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e551a9f0db223eaf3eb156906f99f46897fd951ee66dd1cb0be14db4d36d2fa" +checksum = "3bdaf6120b9df005d37e58f6b75329be6255450453fbeba9ce4192324f921fb9" dependencies = [ "futures-lite", "log", @@ -12130,7 +12130,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.4" +version = "0.0.130-rc.5" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/releases/0.0.130-rc.5.md b/changes/releases/0.0.130-rc.5.md new file mode 100644 index 00000000..b092198b --- /dev/null +++ b/changes/releases/0.0.130-rc.5.md @@ -0,0 +1,5 @@ +## [0.0.130-rc.5] — 2026-04-30 + +### Features + +- fix vulkan cache on windows ci diff --git a/package.json b/package.json index dcdfaa1d..27d31762 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.4", + "version": "0.0.130-rc.5", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 428be0c1..d0eba84f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.4" +version = "0.0.130-rc.5" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6d0ece40..1210125b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.4", + "version": "0.0.130-rc.5", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From 0b1917743aa1e7f6dff6bff35a4a538562cff792 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Wed, 29 Apr 2026 21:36:20 -0400 Subject: [PATCH 10/30] 0.0.130-rc.6 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- changes/releases/0.0.130-rc.6.md | 5 +++++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 changes/releases/0.0.130-rc.6.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 314cc363..2cbacbb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5421,3 +5421,9 @@ The heatmap merges EEG data points with the closest timeline events to show whic ### Features - fix vulkan cache on windows ci + +## [0.0.130-rc.6] — 2026-04-30 + +### Features + +- Minor updates and improvements diff --git a/Cargo.lock b/Cargo.lock index 56cc5621..475f18ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12130,7 +12130,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.5" +version = "0.0.130-rc.6" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/releases/0.0.130-rc.6.md b/changes/releases/0.0.130-rc.6.md new file mode 100644 index 00000000..9769dc36 --- /dev/null +++ b/changes/releases/0.0.130-rc.6.md @@ -0,0 +1,5 @@ +## [0.0.130-rc.6] — 2026-04-30 + +### Features + +- Minor updates and improvements diff --git a/package.json b/package.json index 27d31762..1d0fde53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.5", + "version": "0.0.130-rc.6", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d0eba84f..488ee310 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.5" +version = "0.0.130-rc.6" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1210125b..9f26e026 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.5", + "version": "0.0.130-rc.6", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From b5ce79d6d13fdfc087e9249b125d3754865ab48f Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:21:50 -0400 Subject: [PATCH 11/30] Replace per-app AppleScript window tracking (which triggered a macOS TCC Automation dialog for every new foreground app) with native Accessibility API (AXUIElement) + CoreGraphics calls that require only a single one-time "Accessibility" permission for NeuroSkill. --- Cargo.lock | 2 + crates/skill-daemon/Cargo.toml | 2 + crates/skill-daemon/src/activity.rs | 482 ++++++++++++++++++++++++---- 3 files changed, 431 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 475f18ef..8199a356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12271,6 +12271,8 @@ dependencies = [ "neurosky", "notify", "notify-rust", + "objc2 0.6.4", + "objc2-app-kit", "osf-rs", "rand 0.10.1", "regex", diff --git a/crates/skill-daemon/Cargo.toml b/crates/skill-daemon/Cargo.toml index 0ff3e4b9..68862f59 100644 --- a/crates/skill-daemon/Cargo.toml +++ b/crates/skill-daemon/Cargo.toml @@ -133,6 +133,8 @@ tokio-tungstenite = "0.28" # macOS: use Apple Accelerate for BLAS (AMX coprocessor on M-series) [target.'cfg(target_os = "macos")'.dependencies] burn-ndarray = { version = "0.20.1", default-features = false, features = ["blas-accelerate"], optional = true } +objc2 = "0.6" +objc2-app-kit = { version = "0.3", features = ["NSWorkspace", "NSRunningApplication"] } # Linux: use system OpenBLAS when available [target.'cfg(target_os = "linux")'.dependencies] diff --git a/crates/skill-daemon/src/activity.rs b/crates/skill-daemon/src/activity.rs index bd51a87c..5baf5df1 100644 --- a/crates/skill-daemon/src/activity.rs +++ b/crates/skill-daemon/src/activity.rs @@ -118,8 +118,229 @@ fn run_osascript(script: &str) -> Option { Some(String::from_utf8_lossy(&out.stdout).to_string()) } +/// macOS: try the native Accessibility-API path first; fall back to +/// AppleScript only when Accessibility permission has not been granted yet. +/// +/// The native path (`ax_poll_active_window`) requires ONE one-time +/// "Accessibility" permission for NeuroSkill that covers every application +/// forever — no per-app Automation dialogs appear. The AppleScript fallback +/// (`applescript_poll_active_window`) may trigger macOS TCC dialogs for each +/// new app that comes to the foreground. #[cfg(target_os = "macos")] fn poll_active_window() -> Option { + ax_poll_active_window().or_else(applescript_poll_active_window) +} + +/// Native macOS window polling via NSWorkspace + Accessibility API. +/// +/// * App name / path — obtained from `NSWorkspace.frontmostApplication` +/// (no permissions required at all). +/// * Window title — obtained via `AXFocusedWindow` + `AXTitle` +/// (single one-time "Accessibility" permission for NeuroSkill). +/// * Document path — obtained via `AXDocument` on the focused window +/// (same Accessibility permission; replaces the per-app AppleScript lookup). +/// +/// Returns `None` (causing a fall-through to AppleScript) if Accessibility +/// permission is not yet granted. +#[cfg(target_os = "macos")] +fn ax_poll_active_window() -> Option { + use std::ffi::{c_void, CStr}; + use std::os::raw::c_char; + + type CFTypeRef = *const c_void; + type CFStringRef = *const c_void; + type CFAllocatorRef = *const c_void; + type AXUIElementRef = *const c_void; + type AXError = i32; + + const AX_SUCCESS: AXError = 0; + const KCF_STRING_ENCODING_UTF8: u32 = 0x0800_0100; + + #[link(name = "ApplicationServices", kind = "framework")] + extern "C" { + fn AXIsProcessTrusted() -> bool; + fn AXUIElementCreateApplication(pid: i32) -> AXUIElementRef; + fn AXUIElementCopyAttributeValue( + element: AXUIElementRef, + attribute: CFStringRef, + value: *mut CFTypeRef, + ) -> AXError; + } + + #[link(name = "CoreFoundation", kind = "framework")] + extern "C" { + fn CFStringCreateWithCString(alloc: CFAllocatorRef, c_str: *const c_char, encoding: u32) -> CFStringRef; + fn CFStringGetLength(s: CFStringRef) -> isize; + fn CFStringGetMaximumSizeForEncoding(len: isize, encoding: u32) -> isize; + fn CFStringGetCString(s: CFStringRef, buf: *mut c_char, size: isize, encoding: u32) -> bool; + fn CFRelease(cf: CFTypeRef); + } + + // SAFETY: AXIsProcessTrusted is thread-safe and returns immediately. + if !unsafe { AXIsProcessTrusted() } { + // Accessibility not yet granted — fall through to AppleScript path. + return None; + } + + // ── Step 1: frontmost app info from NSWorkspace (zero permissions) ──────── + let (pid, app_name, app_path) = { + use objc2::msg_send; + use objc2::runtime::AnyObject; + use objc2_app_kit::NSWorkspace; + + // SAFETY: NSWorkspace and NSRunningApplication are stable AppKit APIs. + // Returned Objective-C objects have autorelease lifetime tied to the + // current thread's autorelease pool which Tauri/the OS maintains. + unsafe { + let workspace = NSWorkspace::sharedWorkspace(); + let front_app: Option<&AnyObject> = msg_send![&workspace, frontmostApplication]; + let front_app = front_app?; + + let pid: i32 = msg_send![front_app, processIdentifier]; + if pid <= 0 { + return None; + } + + let name_obj: Option<&AnyObject> = msg_send![front_app, localizedName]; + let app_name = name_obj + .map(|n| { + let bytes: *const c_char = msg_send![n, UTF8String]; + if bytes.is_null() { + String::new() + } else { + CStr::from_ptr(bytes).to_string_lossy().into_owned() + } + }) + .unwrap_or_default(); + + let url_obj: Option<&AnyObject> = msg_send![front_app, executableURL]; + let app_path = url_obj + .and_then(|u| { + let path_obj: Option<&AnyObject> = msg_send![u, path]; + path_obj.map(|p| { + let bytes: *const c_char = msg_send![p, UTF8String]; + if bytes.is_null() { + String::new() + } else { + CStr::from_ptr(bytes).to_string_lossy().into_owned() + } + }) + }) + .unwrap_or_default(); + + (pid, app_name, app_path) + } + }; + + if app_name.is_empty() { + return None; + } + + // ── Step 2: window title + document path via AXUIElement ───────────────── + // One "Accessibility" permission covers all apps — no per-app dialogs. + let (window_title, document_path) = unsafe { + /// Convert a non-null CFStringRef to a Rust `String`. + /// + /// SAFETY: `s` must be a valid, non-null CFStringRef. + unsafe fn cfstr_to_string(s: CFStringRef, enc: u32) -> String { + unsafe { + let len = CFStringGetLength(s); + let max = CFStringGetMaximumSizeForEncoding(len, enc) + 1; + let mut buf: Vec = vec![0; max as usize]; + if CFStringGetCString(s, buf.as_mut_ptr(), max, enc) { + CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() + } else { + String::new() + } + } + } + + let key_focused_win = CFStringCreateWithCString( + std::ptr::null(), + b"AXFocusedWindow\0".as_ptr() as *const c_char, + KCF_STRING_ENCODING_UTF8, + ); + let key_title = CFStringCreateWithCString( + std::ptr::null(), + b"AXTitle\0".as_ptr() as *const c_char, + KCF_STRING_ENCODING_UTF8, + ); + let key_document = CFStringCreateWithCString( + std::ptr::null(), + b"AXDocument\0".as_ptr() as *const c_char, + KCF_STRING_ENCODING_UTF8, + ); + + let app_ax = AXUIElementCreateApplication(pid); + + let mut win_ref: CFTypeRef = std::ptr::null(); + let err = AXUIElementCopyAttributeValue(app_ax, key_focused_win, &mut win_ref); + + let (title, doc_path) = if err == AX_SUCCESS && !win_ref.is_null() { + let mut title_ref: CFTypeRef = std::ptr::null(); + let title = if AXUIElementCopyAttributeValue(win_ref, key_title, &mut title_ref) == AX_SUCCESS + && !title_ref.is_null() + { + let t = cfstr_to_string(title_ref, KCF_STRING_ENCODING_UTF8); + CFRelease(title_ref); + t + } else { + String::new() + }; + + let mut doc_ref: CFTypeRef = std::ptr::null(); + let doc_path = if AXUIElementCopyAttributeValue(win_ref, key_document, &mut doc_ref) == AX_SUCCESS + && !doc_ref.is_null() + { + // AXDocument returns a URL string: "file:///path/to/doc.txt" + let raw = cfstr_to_string(doc_ref, KCF_STRING_ENCODING_UTF8); + CFRelease(doc_ref); + let path = raw.strip_prefix("file://").unwrap_or(&raw); + let decoded = urlencoding::decode(path) + .map(|s| s.into_owned()) + .unwrap_or_else(|_| path.to_string()); + if decoded.is_empty() { + None + } else { + Some(decoded) + } + } else { + None + }; + + CFRelease(win_ref); + (title, doc_path) + } else { + (String::new(), None) + }; + + // SAFETY: app_ax is a retained AXUIElementRef (CFType); must be released. + CFRelease(app_ax as CFTypeRef); + CFRelease(key_focused_win); + CFRelease(key_title); + CFRelease(key_document); + + (title, doc_path) + }; + + Some(ActiveWindowInfo { + app_name, + app_path, + window_title, + document_path, + activated_at: unix_secs(), + browser_title: None, // Enriched later in run_poller. + monitor_id: None, // Enriched later if multi-monitor detection succeeds. + }) +} + +/// AppleScript fallback for active-window polling (macOS). +/// +/// Used only when Accessibility permission has not been granted yet. +/// May trigger macOS TCC Automation permission dialogs for each new +/// foreground application. +#[cfg(target_os = "macos")] +fn applescript_poll_active_window() -> Option { let script = r#" tell application "System Events" set frontApp to first application process whose frontmost is true @@ -189,70 +410,221 @@ return appName & "|||" & appPath & "|||" & winTitle & "|||" & docPath"#; } /// Poll all visible windows on non-primary monitors (macOS only). -/// Returns a list of windows that are on secondary screens. +/// +/// Uses `CGWindowListCopyWindowInfo` (CoreGraphics) and `CGMainDisplayID` / +/// `CGDisplayPixelsWide` to detect secondary-monitor windows without any +/// AppleScript or TCC permission prompts. +/// +/// Window titles (`kCGWindowName`) may be empty without Screen Recording +/// permission; owner names (`kCGWindowOwnerName`) are always available. #[cfg(target_os = "macos")] fn poll_secondary_windows() -> Vec { - // Use AppleScript to get all visible windows with their positions, - // then compare against screen bounds to determine which monitor. - let script = r#" -set result to "" -tell application "System Events" - set frontName to name of first application process whose frontmost is true - repeat with proc in (application processes whose visible is true) - set procName to name of proc - if procName is not frontName then - try - repeat with w in windows of proc - try - set winTitle to name of w - set winPos to position of w - set xPos to item 1 of winPos - -- Use x position to infer monitor (primary is typically x >= 0 and < primary width) - set result to result & procName & "|||" & winTitle & "|||" & xPos & linefeed - end try - end repeat - end try - end if - end repeat -end tell -return result"#; - - let out = match run_osascript(script) { - Some(s) => s, - None => return vec![], - }; + use std::ffi::{c_void, CStr}; + use std::os::raw::c_char; + + type CFTypeRef = *const c_void; + type CFDictionaryRef = *const c_void; + type CFStringRef = *const c_void; + type CFAllocatorRef = *const c_void; + type CFArrayRef = *const c_void; + type CFIndex = isize; + type CFNumberType = i32; + type CGWindowID = u32; + + const ON_SCREEN_ONLY: u32 = 1 << 0; + const EXCLUDE_DESKTOP: u32 = 1 << 4; + const K_CG_NULL_WINDOW_ID: CGWindowID = 0; + const K_CF_NUMBER_SINT32_TYPE: CFNumberType = 3; + const K_CF_NUMBER_FLOAT64_TYPE: CFNumberType = 13; + const K_CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100; + + #[link(name = "CoreGraphics", kind = "framework")] + extern "C" { + fn CGWindowListCopyWindowInfo(option: u32, relativeToWindow: CGWindowID) -> CFArrayRef; + fn CGMainDisplayID() -> u32; + fn CGDisplayPixelsWide(display: u32) -> usize; + } - // Parse: each line is "appName|||windowTitle|||xPosition" - // Query actual primary screen width to avoid hardcoded values. - let primary_width: i64 = run_osascript("tell application \"Finder\" to get bounds of window of desktop") - .and_then(|s| s.split(',').nth(2)?.trim().parse::().ok()) - .unwrap_or(2000); + #[link(name = "CoreFoundation", kind = "framework")] + extern "C" { + fn CFArrayGetCount(theArray: CFArrayRef) -> CFIndex; + fn CFArrayGetValueAtIndex(theArray: CFArrayRef, idx: CFIndex) -> CFTypeRef; + fn CFDictionaryGetValue(dict: CFDictionaryRef, key: CFStringRef) -> CFTypeRef; + fn CFNumberGetValue(number: CFTypeRef, the_type: CFNumberType, value_ptr: *mut i64) -> bool; + fn CFRelease(cf: CFTypeRef); + fn CFStringCreateWithCString(alloc: CFAllocatorRef, c_str: *const c_char, encoding: u32) -> CFStringRef; + fn CFStringGetLength(s: CFStringRef) -> isize; + fn CFStringGetMaximumSizeForEncoding(len: isize, encoding: u32) -> isize; + fn CFStringGetCString(s: CFStringRef, buf: *mut c_char, size: isize, encoding: u32) -> bool; + } + + // SAFETY: CoreGraphics C APIs — all pointers are valid and non-null-checked. + unsafe { + /// Create a UTF-8 CFString from a NUL-terminated byte literal. + /// + /// SAFETY: `s` must be a NUL-terminated byte slice. Caller must CFRelease. + unsafe fn cfstr(s: &[u8]) -> CFStringRef { + unsafe { + CFStringCreateWithCString(std::ptr::null(), s.as_ptr() as *const c_char, K_CF_STRING_ENCODING_UTF8) + } + } - out.lines() - .filter_map(|line| { - let line = line.trim(); - if line.is_empty() { + /// Read a CFNumber as i32. + /// + /// SAFETY: `n` must be a valid CFNumber (or null). + unsafe fn cfnum_i32(n: CFTypeRef) -> Option { + if n.is_null() { return None; } - let mut parts = line.splitn(3, "|||"); - let app_name = parts.next()?.trim().to_string(); - let window_title = parts.next()?.trim().to_string(); - let x_pos: i64 = parts.next()?.trim().parse().ok()?; - if app_name.is_empty() || window_title.is_empty() { + let mut v: i64 = 0; + unsafe { + if CFNumberGetValue(n, K_CF_NUMBER_SINT32_TYPE, &mut v) { + Some(v as i32) + } else { + None + } + } + } + + /// Read a CFNumber as f64. + /// + /// SAFETY: `n` must be a valid CFNumber (or null). + unsafe fn cfnum_f64(n: CFTypeRef) -> Option { + if n.is_null() { return None; } - // If window is outside primary monitor bounds, it's on a secondary monitor. - if x_pos < 0 || x_pos >= primary_width { - Some(SecondaryWindowInfo { - app_name, - window_title, - monitor_id: if x_pos < 0 { 2 } else { 1 }, - }) - } else { - None + let mut v: i64 = 0; + unsafe { + // CFNumberGetValue writes the numeric bits; reinterpret as f64. + if CFNumberGetValue(n, K_CF_NUMBER_FLOAT64_TYPE, &mut v) { + Some(f64::from_bits(v as u64)) + } else { + None + } } - }) - .collect() + } + + /// Convert a non-null CFStringRef to a Rust String. + /// + /// SAFETY: `s` must be a valid, non-null CFStringRef. + unsafe fn cfstr_to_string(s: CFStringRef) -> String { + unsafe { + let len = CFStringGetLength(s); + let max = CFStringGetMaximumSizeForEncoding(len, K_CF_STRING_ENCODING_UTF8) + 1; + let mut buf: Vec = vec![0; max as usize]; + if CFStringGetCString(s, buf.as_mut_ptr(), max, K_CF_STRING_ENCODING_UTF8) { + CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() + } else { + String::new() + } + } + } + + // Primary display width — used to determine which monitor a window is on. + let primary_width = CGDisplayPixelsWide(CGMainDisplayID()) as i64; + + let key_pid = cfstr(b"kCGWindowOwnerPID\0"); + let key_layer = cfstr(b"kCGWindowLayer\0"); + let key_owner_name = cfstr(b"kCGWindowOwnerName\0"); + let key_name = cfstr(b"kCGWindowName\0"); + let key_bounds = cfstr(b"kCGWindowBounds\0"); + let key_x = cfstr(b"X\0"); + + let list = CGWindowListCopyWindowInfo(ON_SCREEN_ONLY | EXCLUDE_DESKTOP, K_CG_NULL_WINDOW_ID); + if list.is_null() { + CFRelease(key_pid); + CFRelease(key_layer); + CFRelease(key_owner_name); + CFRelease(key_name); + CFRelease(key_bounds); + CFRelease(key_x); + return vec![]; + } + + // Identify the frontmost app's PID so we can skip its windows. + let frontmost_pid: i32 = { + use objc2::msg_send; + use objc2::runtime::AnyObject; + use objc2_app_kit::NSWorkspace; + let workspace = NSWorkspace::sharedWorkspace(); + let front_app: Option<&AnyObject> = msg_send![&workspace, frontmostApplication]; + front_app.map(|a| msg_send![a, processIdentifier]).unwrap_or(-1) + }; + + let count = CFArrayGetCount(list); + let mut results: Vec = Vec::new(); + + for i in 0..count { + let dict = CFArrayGetValueAtIndex(list, i); + if dict.is_null() { + continue; + } + + // Layer 0 = normal windows only. + let layer = cfnum_i32(CFDictionaryGetValue(dict, key_layer)).unwrap_or(-1); + if layer != 0 { + continue; + } + + // Skip the frontmost app's windows (those belong to primary tracking). + let pid = cfnum_i32(CFDictionaryGetValue(dict, key_pid)).unwrap_or(-1); + if pid == frontmost_pid { + continue; + } + + // Window x-position from bounds dictionary. + let bounds_dict = CFDictionaryGetValue(dict, key_bounds); + if bounds_dict.is_null() { + continue; + } + let x_val = CFDictionaryGetValue(bounds_dict, key_x); + let x_pos = cfnum_f64(x_val).unwrap_or(0.0) as i64; + + // Only include windows that are outside the primary monitor. + if x_pos >= 0 && x_pos < primary_width { + continue; + } + + let owner_name_ref = CFDictionaryGetValue(dict, key_owner_name); + if owner_name_ref.is_null() { + continue; + } + let app_name = cfstr_to_string(owner_name_ref); + if app_name.is_empty() { + continue; + } + + // kCGWindowName may be null without Screen Recording permission; + // fall back to the app name to keep the record useful. + let win_name_ref = CFDictionaryGetValue(dict, key_name); + let window_title = if win_name_ref.is_null() { + app_name.clone() + } else { + let t = cfstr_to_string(win_name_ref); + if t.is_empty() { + app_name.clone() + } else { + t + } + }; + + results.push(SecondaryWindowInfo { + app_name, + window_title, + monitor_id: if x_pos < 0 { 2 } else { 1 }, + }); + } + + CFRelease(list); + CFRelease(key_pid); + CFRelease(key_layer); + CFRelease(key_owner_name); + CFRelease(key_name); + CFRelease(key_bounds); + CFRelease(key_x); + + results + } } /// Poll visible windows on non-primary monitors (Linux). From 3d9e108d0d080e07a32df425275c7615614d8358 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:23:28 -0400 Subject: [PATCH 12/30] 0.0.130-rc.7 --- CHANGELOG.md | 6 + Cargo.lock | 569 ++++++++++++------------------- changes/releases/0.0.130-rc.7.md | 5 + package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 235 insertions(+), 351 deletions(-) create mode 100644 changes/releases/0.0.130-rc.7.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cbacbb2..6ba1ea42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5427,3 +5427,9 @@ The heatmap merges EEG data points with the closest timeline events to show whic ### Features - Minor updates and improvements + +## [0.0.130-rc.7] — 2026-05-02 + +### Features + +- Replace per-app AppleScript window tracking (which triggered a macOS TCC Automation dialog for every new foreground app) with native Accessibility API (AXUIElement) + CoreGraphics calls that require only a single one-time "Accessibility" permission for NeuroSkill. diff --git a/Cargo.lock b/Cargo.lock index 8199a356..819a5be8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2225,12 +2225,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d3e02915a2cea4d74caa8681e2d44b1c3254bdbf17d11d41d587ff858832c" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.8.0" @@ -2524,7 +2518,7 @@ checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags 2.11.1", "crossterm_winapi", - "derive_more 2.1.1", + "derive_more", "document-features", "mio", "parking_lot", @@ -2578,23 +2572,6 @@ dependencies = [ "phf 0.11.3", ] -[[package]] -name = "cssparser" -version = "0.29.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - [[package]] name = "cssparser" version = "0.36.0" @@ -2641,14 +2618,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "ctr" version = "0.9.2" @@ -2698,7 +2681,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "derive-new", - "derive_more 2.1.1", + "derive_more", "dirs", "embassy-futures", "embassy-time", @@ -2736,7 +2719,7 @@ dependencies = [ "cubecl-macros", "cubecl-runtime", "derive-new", - "derive_more 2.1.1", + "derive_more", "enumset", "float-ord", "half", @@ -2849,7 +2832,7 @@ dependencies = [ "cubecl-common", "cubecl-macros-internal", "derive-new", - "derive_more 2.1.1", + "derive_more", "enumset", "float-ord", "fnv", @@ -2920,7 +2903,7 @@ dependencies = [ "cubecl-common", "cubecl-ir", "derive-new", - "derive_more 2.1.1", + "derive_more", "dirs", "enumset", "foldhash 0.1.5", @@ -2990,7 +2973,7 @@ dependencies = [ "cubecl-runtime", "cubecl-spirv", "derive-new", - "derive_more 2.1.1", + "derive_more", "half", "hashbrown 0.15.5", "log", @@ -3452,19 +3435,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -3646,12 +3616,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set 0.8.0", - "cssparser 0.36.0", + "cssparser", "foldhash 0.2.0", - "html5ever 0.38.0", + "html5ever", "precomputed-hash", - "selectors 0.36.1", - "tendril 0.5.0", + "selectors", + "tendril", ] [[package]] @@ -3724,6 +3694,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -4267,7 +4252,7 @@ dependencies = [ "getrandom 0.3.4", "libm", "rand 0.9.4", - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -4541,16 +4526,6 @@ dependencies = [ "libc", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.32" @@ -5773,18 +5748,6 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever 0.14.1", - "match_token", -] - [[package]] name = "html5ever" version = "0.38.0" @@ -5792,7 +5755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "markup5ever 0.38.0", + "markup5ever", ] [[package]] @@ -6420,7 +6383,7 @@ dependencies = [ "bytes", "cfg_aliases", "data-encoding", - "derive_more 2.1.1", + "derive_more", "ed25519-dalek", "futures-util", "getrandom 0.3.4", @@ -6471,7 +6434,7 @@ checksum = "55a354e3396b62c14717ee807dfee9a7f43f6dad47e4ac0fd1d49f1ffad14ef0" dependencies = [ "curve25519-dalek", "data-encoding", - "derive_more 2.1.1", + "derive_more", "digest 0.11.0-rc.10", "ed25519-dalek", "n0-error", @@ -6538,7 +6501,7 @@ dependencies = [ "bytes", "cfg_aliases", "data-encoding", - "derive_more 2.1.1", + "derive_more", "getrandom 0.3.4", "hickory-resolver", "http 1.4.0", @@ -6851,9 +6814,9 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.46.3" +version = "0.46.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbe92a2f8b00686061eab5cdcfd6f382c27f2084456e7be90ae9f0fe4a30552a" +checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f" dependencies = [ "ahash", "bytecount", @@ -6972,18 +6935,6 @@ dependencies = [ "libc", ] -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser 0.29.6", - "html5ever 0.29.1", - "indexmap 2.14.0", - "selectors 0.24.0", -] - [[package]] name = "lab" version = "0.11.0" @@ -7265,9 +7216,9 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "llama-cpp-4" -version = "0.2.50" +version = "0.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcf0cd079ad2f022bf031670df8ba456c21912563e820aa88e7102e33afb194" +checksum = "848f0db0643df8e38aabe14e6a74d99cb6202b142a83cbffa1fb5bc2e427ecb4" dependencies = [ "enumflags2", "llama-cpp-sys-4", @@ -7277,9 +7228,9 @@ dependencies = [ [[package]] name = "llama-cpp-sys-4" -version = "0.2.50" +version = "0.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca95ff4c86ec27cba44c939e7161bc66a7edef083f7e59186e3bf4e5778a76a" +checksum = "00267ef600935213dbbb794409c16a450a61eb1f66ce723a5179bfaa949d0f6e" dependencies = [ "bindgen 0.72.1", "cc", @@ -7290,9 +7241,9 @@ dependencies = [ [[package]] name = "llmfit-core" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3d28d163be4423375ed1f7fb0bdc23e40fd1fe56e7a5beb025a9240bb6b978" +checksum = "57796197a65dea17e886c10cd474d8be9dbf755c84d4909064961a62e5fd5f42" dependencies = [ "dirs", "http 1.4.0", @@ -7395,12 +7346,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "mac-addr" version = "0.3.0" @@ -7528,20 +7473,6 @@ dependencies = [ "libc", ] -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", - "tendril 0.4.3", -] - [[package]] name = "markup5ever" version = "0.38.0" @@ -7549,7 +7480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "tendril 0.5.0", + "tendril", "web_atoms", ] @@ -7559,17 +7490,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "matchers" version = "0.2.0" @@ -7579,12 +7499,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "matchit" version = "0.8.4" @@ -7871,7 +7785,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.2" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" dependencies = [ "crossbeam-channel", "dpi", @@ -7882,10 +7798,10 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation 0.3.2", "once_cell", - "png 0.17.16", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7976,7 +7892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" dependencies = [ "cfg_aliases", - "derive_more 2.1.1", + "derive_more", "futures-buffered", "futures-lite", "futures-util", @@ -7996,7 +7912,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38795f7932e6e9d1c6e989270ef5b3ff24ebb910e2c9d4bed2d28d8bae3007dc" dependencies = [ - "derive_more 2.1.1", + "derive_more", "n0-error", "n0-future", ] @@ -8201,7 +8117,7 @@ dependencies = [ "atomic-waker", "bytes", "cfg_aliases", - "derive_more 2.1.1", + "derive_more", "js-sys", "libc", "n0-error", @@ -8378,12 +8294,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "nom" version = "7.1.3" @@ -8438,7 +8348,7 @@ checksum = "5c61b72abd670eebc05b5cf720e077b04a3ef3354bc7bc19f1c3524cb424db7b" dependencies = [ "aes-gcm", "bytes", - "derive_more 2.1.1", + "derive_more", "enum-assoc", "fastbloom", "getrandom 0.3.4", @@ -8887,6 +8797,16 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-media" version = "0.3.2" @@ -9096,8 +9016,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.11.1", + "block2 0.6.2", "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation 0.3.2", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -9613,26 +9552,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - [[package]] name = "phf" version = "0.11.3" @@ -9654,16 +9573,6 @@ dependencies = [ "serde", ] -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - [[package]] name = "phf_codegen" version = "0.11.3" @@ -9684,24 +9593,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_generator" -version = "0.8.0" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.8.6", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.6", -] - [[package]] name = "phf_generator" version = "0.11.3" @@ -9722,20 +9613,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "phf_macros" version = "0.11.3" @@ -9762,31 +9639,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -9795,7 +9654,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -9865,17 +9724,19 @@ dependencies = [ [[package]] name = "pkarr" -version = "5.0.4" +version = "5.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bfb9143bbba379f246211eb68074d78db9cc048e4c5701f3b0e6cb1ec67ca2" +checksum = "0db5bc018bd8e26cb7e7913623292e5eddd71caf29801ea2b2bd627167044e05" dependencies = [ "base32", "bytes", "cfg_aliases", "document-features", + "ed25519", "ed25519-dalek", "getrandom 0.4.2", "ntimestamp", + "pkcs8", "self_cell", "serde", "simple-dns", @@ -10029,7 +9890,7 @@ checksum = "74748bc706fa6b6aebac6bbe0bbe0de806b384cb5c557ea974f771360a4e3858" dependencies = [ "base64 0.22.1", "bytes", - "derive_more 2.1.1", + "derive_more", "futures-lite", "futures-util", "hyper-util", @@ -10204,12 +10065,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.106" @@ -10830,9 +10685,9 @@ dependencies = [ [[package]] name = "referencing" -version = "0.46.3" +version = "0.46.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e125f10bdcd507598c702daada18c47fe5bfba4d7a9545b015b5d432f7168ca3" +checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730" dependencies = [ "ahash", "fluent-uri", @@ -11682,24 +11537,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser 0.29.6", - "derive_more 0.99.20", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc 0.2.0", - "smallvec", -] - [[package]] name = "selectors" version = "0.36.1" @@ -11707,15 +11544,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ "bitflags 2.11.1", - "cssparser 0.36.0", - "derive_more 2.1.1", + "cssparser", + "derive_more", "log", "new_debug_unreachable", "phf 0.13.1", "phf_codegen 0.13.1", "precomputed-hash", "rustc-hash 2.1.2", - "servo_arc 0.4.3", + "servo_arc", "smallvec", ] @@ -11972,16 +11809,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "servo_arc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - [[package]] name = "servo_arc" version = "0.4.3" @@ -12116,12 +11943,6 @@ dependencies = [ "bitflags 2.11.1", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.2" @@ -12130,7 +11951,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.6" +version = "0.0.130-rc.7" dependencies = [ "anyhow", "base64 0.22.1", @@ -12482,9 +12303,9 @@ dependencies = [ "http 1.4.0", "serde", "serde_json", - "tao", + "tao 0.34.8", "thiserror 2.0.18", - "wry", + "wry 0.54.4", ] [[package]] @@ -13049,19 +12870,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -13074,18 +12882,6 @@ dependencies = [ "precomputed-hash", ] -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - [[package]] name = "string_cache_codegen" version = "0.6.1" @@ -13419,7 +13215,6 @@ dependencies = [ "dlopen2 0.8.2", "dpi", "gdkwayland-sys", - "gdkx11-sys", "gtk", "jni 0.21.1", "libc", @@ -13439,6 +13234,45 @@ dependencies = [ "windows 0.61.3", "windows-core 0.61.2", "windows-version", +] + +[[package]] +name = "tao" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2 0.8.2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", "x11-dl", ] @@ -13478,9 +13312,9 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" dependencies = [ "anyhow", "bytes", @@ -13530,9 +13364,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" dependencies = [ "anyhow", "cargo_toml", @@ -13546,15 +13380,14 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" dependencies = [ "base64 0.22.1", "brotli", @@ -13579,9 +13412,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -13593,9 +13426,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" dependencies = [ "anyhow", "glob", @@ -13604,7 +13437,6 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.12+spec-1.1.0", "walkdir", ] @@ -13724,9 +13556,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" dependencies = [ "cookie", "dpi", @@ -13749,9 +13581,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" dependencies = [ "gtk", "http 1.4.0", @@ -13763,36 +13595,36 @@ dependencies = [ "percent-encoding", "raw-window-handle", "softbuffer", - "tao", + "tao 0.35.0", "tauri-runtime", "tauri-utils", "url", "webkit2gtk", "webview2-com", "windows 0.61.3", - "wry", + "wry 0.55.0", ] [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever 0.29.1", "http 1.4.0", "infer", "json-patch", - "kuchikiki", "log", "memchr", "phf 0.11.3", + "plist", "proc-macro2", "quote", "regex", @@ -13804,7 +13636,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -13847,17 +13679,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "tendril" version = "0.5.0" @@ -13936,7 +13757,7 @@ dependencies = [ "phf 0.11.3", "sha2 0.10.9", "signal-hook", - "siphasher 1.0.2", + "siphasher", "terminfo", "termios", "thiserror 1.0.69", @@ -14691,9 +14512,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.3" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" dependencies = [ "crossbeam-channel", "dirs", @@ -14705,10 +14526,10 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation 0.3.2", "once_cell", - "png 0.17.16", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -15647,8 +15468,8 @@ checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ "phf 0.13.1", "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -16839,6 +16660,50 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wry" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http 1.4.0", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + [[package]] name = "ws_stream_wasm" version = "0.7.5" @@ -17484,3 +17349,11 @@ dependencies = [ "syn 2.0.117", "winnow 1.0.2", ] + +[[patch.unused]] +name = "muda" +version = "0.17.2" + +[[patch.unused]] +name = "phf_generator" +version = "0.8.0" diff --git a/changes/releases/0.0.130-rc.7.md b/changes/releases/0.0.130-rc.7.md new file mode 100644 index 00000000..9b8e30de --- /dev/null +++ b/changes/releases/0.0.130-rc.7.md @@ -0,0 +1,5 @@ +## [0.0.130-rc.7] — 2026-05-02 + +### Features + +- Replace per-app AppleScript window tracking (which triggered a macOS TCC Automation dialog for every new foreground app) with native Accessibility API (AXUIElement) + CoreGraphics calls that require only a single one-time "Accessibility" permission for NeuroSkill. diff --git a/package.json b/package.json index 1d0fde53..a2fe704d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.6", + "version": "0.0.130-rc.7", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 488ee310..ff475c1c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.6" +version = "0.0.130-rc.7" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9f26e026..529d9b6b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.6", + "version": "0.0.130-rc.7", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From 7026bc9b9b4e0525de338723c4ec320f78521e6f Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:26:20 -0400 Subject: [PATCH 13/30] cargo deny --- deny.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/deny.toml b/deny.toml index ffaa52af..74320389 100644 --- a/deny.toml +++ b/deny.toml @@ -25,6 +25,11 @@ ignore = [ { id = "RUSTSEC-2024-0415", reason = "transitive via tauri/libappindicator, not directly upgradeable" }, # lru 0.12.5 — IterMut unsoundness, patched in >=0.16.3. # Pulled transitively; not directly used by workspace code paths that call IterMut. + + # hickory-proto 0.25.2 — pulled transitively via iroh → hickory-resolver. + # Cannot upgrade until iroh ships a version that depends on hickory-proto >=0.25.3. + { id = "RUSTSEC-2026-0118", reason = "transitive via iroh/hickory-resolver, not directly upgradeable" }, + { id = "RUSTSEC-2026-0119", reason = "transitive via iroh/hickory-resolver, not directly upgradeable" }, ] # ── Licenses ────────────────────────────────────────────────────────────────── From 6506c3756d24627f2e29d36edf1a9c58c1cd7c0a Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:31:34 -0400 Subject: [PATCH 14/30] updated llm catalog --- src-tauri/llm_catalog.json | 271 +++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) diff --git a/src-tauri/llm_catalog.json b/src-tauri/llm_catalog.json index 4e605321..6d803cb6 100644 --- a/src-tauri/llm_catalog.json +++ b/src-tauri/llm_catalog.json @@ -445,6 +445,33 @@ "is_mmproj": false, "params_b": 4.0, "max_context_length": 131072 + }, + "mistral-medium-3.5-128b": { + "name": "Mistral Medium 3.5 128B", + "description": "Mistral AI's 128B parameter medium-class model with strong reasoning and chat quality. Requires 64 GB+ unified memory or VRAM for Q4 quants.", + "repo": "bartowski/mistralai_Mistral-Medium-3.5-128B-GGUF", + "tags": [ + "chat", + "reasoning", + "large" + ], + "is_mmproj": false, + "params_b": 128.0, + "max_context_length": 131072 + }, + "nemotron-3-nano-omni-30b": { + "name": "NVIDIA Nemotron-3 Nano Omni 30B", + "description": "NVIDIA's multimodal MoE model (30B, 3B active) with video, audio, image, and text understanding. Supports reasoning, tool calling, OCR, and GUI automation.", + "repo": "unsloth/NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-GGUF", + "tags": [ + "chat", + "reasoning", + "multimodal", + "moe" + ], + "is_mmproj": true, + "params_b": 30.0, + "max_context_length": 131072 } }, "models": [ @@ -4073,6 +4100,250 @@ "size_gb": 35.81, "description": "8-bit with important layers at higher precision", "advanced": true + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q2_K.gguf", + "quant": "Q2_K", + "size_gb": 49.86, + "description": "Very low quality; smallest single-file download", + "advanced": true + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-IQ2_M.gguf", + "quant": "IQ2_M", + "size_gb": 48.61, + "description": "Low quality imatrix; surprisingly usable", + "advanced": true + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q2_K_L/mistralai_Mistral-Medium-3.5-128B-Q2_K_L-00001-of-00002.gguf", + "quant": "Q2_K_L", + "size_gb": 51.43, + "description": "Q8_0 embed/output weights; very low quality", + "advanced": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-Q2_K_L/mistralai_Mistral-Medium-3.5-128B-Q2_K_L-00001-of-00002.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q2_K_L/mistralai_Mistral-Medium-3.5-128B-Q2_K_L-00002-of-00002.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-IQ3_M/mistralai_Mistral-Medium-3.5-128B-IQ3_M-00001-of-00002.gguf", + "quant": "IQ3_M", + "size_gb": 59.53, + "description": "Medium-low quality imatrix; decent performance", + "advanced": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-IQ3_M/mistralai_Mistral-Medium-3.5-128B-IQ3_M-00001-of-00002.gguf", + "mistralai_Mistral-Medium-3.5-128B-IQ3_M/mistralai_Mistral-Medium-3.5-128B-IQ3_M-00002-of-00002.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q3_K_M/mistralai_Mistral-Medium-3.5-128B-Q3_K_M-00001-of-00002.gguf", + "quant": "Q3_K_M", + "size_gb": 63.28, + "description": "Low quality; good for limited RAM/VRAM", + "advanced": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-Q3_K_M/mistralai_Mistral-Medium-3.5-128B-Q3_K_M-00001-of-00002.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q3_K_M/mistralai_Mistral-Medium-3.5-128B-Q3_K_M-00002-of-00002.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-IQ4_XS/mistralai_Mistral-Medium-3.5-128B-IQ4_XS-00001-of-00002.gguf", + "quant": "IQ4_XS", + "size_gb": 69.14, + "description": "Decent quality imatrix; smaller than Q4_K_S", + "advanced": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-IQ4_XS/mistralai_Mistral-Medium-3.5-128B-IQ4_XS-00001-of-00002.gguf", + "mistralai_Mistral-Medium-3.5-128B-IQ4_XS/mistralai_Mistral-Medium-3.5-128B-IQ4_XS-00002-of-00002.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q4_K_S/mistralai_Mistral-Medium-3.5-128B-Q4_K_S-00001-of-00002.gguf", + "quant": "Q4_K_S", + "size_gb": 73.02, + "description": "Good quality with space savings", + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-Q4_K_S/mistralai_Mistral-Medium-3.5-128B-Q4_K_S-00001-of-00002.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q4_K_S/mistralai_Mistral-Medium-3.5-128B-Q4_K_S-00002-of-00002.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q4_K_M/mistralai_Mistral-Medium-3.5-128B-Q4_K_M-00001-of-00002.gguf", + "quant": "Q4_K_M", + "size_gb": 78.41, + "description": "Recommended -- best quality/size tradeoff", + "recommended": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-Q4_K_M/mistralai_Mistral-Medium-3.5-128B-Q4_K_M-00001-of-00002.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q4_K_M/mistralai_Mistral-Medium-3.5-128B-Q4_K_M-00002-of-00002.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q5_K_M/mistralai_Mistral-Medium-3.5-128B-Q5_K_M-00001-of-00003.gguf", + "quant": "Q5_K_M", + "size_gb": 91.11, + "description": "High quality; >= 96 GB unified memory", + "advanced": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-Q5_K_M/mistralai_Mistral-Medium-3.5-128B-Q5_K_M-00001-of-00003.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q5_K_M/mistralai_Mistral-Medium-3.5-128B-Q5_K_M-00002-of-00003.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q5_K_M/mistralai_Mistral-Medium-3.5-128B-Q5_K_M-00003-of-00003.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q6_K/mistralai_Mistral-Medium-3.5-128B-Q6_K-00001-of-00003.gguf", + "quant": "Q6_K", + "size_gb": 107.8, + "description": "Very high quality, near perfect; >= 112 GB unified memory", + "advanced": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-Q6_K/mistralai_Mistral-Medium-3.5-128B-Q6_K-00001-of-00003.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q6_K/mistralai_Mistral-Medium-3.5-128B-Q6_K-00002-of-00003.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q6_K/mistralai_Mistral-Medium-3.5-128B-Q6_K-00003-of-00003.gguf" + ] + }, + { + "family": "mistral-medium-3.5-128b", + "filename": "mistralai_Mistral-Medium-3.5-128B-Q8_0/mistralai_Mistral-Medium-3.5-128B-Q8_0-00001-of-00004.gguf", + "quant": "Q8_0", + "size_gb": 132.85, + "description": "Effectively lossless; very large", + "advanced": true, + "shard_files": [ + "mistralai_Mistral-Medium-3.5-128B-Q8_0/mistralai_Mistral-Medium-3.5-128B-Q8_0-00001-of-00004.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q8_0/mistralai_Mistral-Medium-3.5-128B-Q8_0-00002-of-00004.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q8_0/mistralai_Mistral-Medium-3.5-128B-Q8_0-00003-of-00004.gguf", + "mistralai_Mistral-Medium-3.5-128B-Q8_0/mistralai_Mistral-Medium-3.5-128B-Q8_0-00004-of-00004.gguf" + ] + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-IQ2_M.gguf", + "quant": "IQ2_M", + "size_gb": 18.5, + "description": "Ultra-low quality imatrix; smallest useful option", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q2_K_XL.gguf", + "quant": "Q2_K_XL", + "size_gb": 18.5, + "description": "Ultra-low quality; Q8_0 embed/output weights", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-IQ3_S.gguf", + "quant": "IQ3_S", + "size_gb": 18.82, + "description": "Low quality imatrix", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-IQ3_XXS.gguf", + "quant": "IQ3_XXS", + "size_gb": 19.46, + "description": "Low quality imatrix", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-IQ4_XS.gguf", + "quant": "IQ4_XS", + "size_gb": 19.54, + "description": "Decent quality; smaller than Q4_K_S", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-IQ4_NL.gguf", + "quant": "IQ4_NL", + "size_gb": 19.54, + "description": "Decent quality; good for ARM CPU inference", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-MXFP4_MOE.gguf", + "quant": "MXFP4_MOE", + "size_gb": 21.73, + "description": "MX FP4 MoE quant -- fast on supported hardware" + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_S.gguf", + "quant": "Q4_K_S", + "size_gb": 23.05, + "description": "Good quality with space savings" + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q4_K_M.gguf", + "quant": "Q4_K_M", + "size_gb": 23.89, + "description": "Recommended -- best quality/size tradeoff", + "recommended": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q5_K_S.gguf", + "quant": "Q5_K_S", + "size_gb": 24.8, + "description": "High quality", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q5_K_M.gguf", + "quant": "Q5_K_M", + "size_gb": 29.0, + "description": "High quality; >= 32 GB VRAM", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-UD-Q6_K.gguf", + "quant": "Q6_K", + "size_gb": 33.59, + "description": "Very high quality, near perfect", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-Q8_0.gguf", + "quant": "Q8_0", + "size_gb": 33.59, + "description": "Effectively lossless; very large", + "advanced": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "mmproj-BF16.gguf", + "quant": "BF16", + "size_gb": 1.59, + "description": "Nemotron-3 Nano Omni vision/audio projector -- BF16 (recommended)", + "recommended": true + }, + { + "family": "nemotron-3-nano-omni-30b", + "filename": "mmproj-F16.gguf", + "quant": "F16", + "size_gb": 1.59, + "description": "Nemotron-3 Nano Omni vision/audio projector -- FP16" } ] } From d3ea991c4fe2d84e807b76309beaf9c2e4d195a6 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:39:19 -0400 Subject: [PATCH 15/30] fix tty --- crates/skill-daemon/src/main.rs | 11 ++++++++++- crates/skill-daemon/src/routes/settings.rs | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/skill-daemon/src/main.rs b/crates/skill-daemon/src/main.rs index ab20b261..51811ef8 100644 --- a/crates/skill-daemon/src/main.rs +++ b/crates/skill-daemon/src/main.rs @@ -52,7 +52,16 @@ fn main() -> anyhow::Result<()> { let args: Vec = std::env::args().collect(); #[cfg(unix)] if args.get(1).map(String::as_str) == Some("tty") { - return tty::run(&args[2..]); + // Exit 126 signals the shell hook that the PTY shim failed to start + // (not a tty, openpty failed, etc.) so the hook can fall through to a + // plain shell instead of closing the terminal. Any other exit code is + // the inner shell's own exit code and is forwarded as-is (tty::run + // calls std::process::exit internally on success). + if let Err(e) = tty::run(&args[2..]) { + eprintln!("skill-daemon tty: {e:#}"); + std::process::exit(126); + } + return Ok(()); } daemon_main() } diff --git a/crates/skill-daemon/src/routes/settings.rs b/crates/skill-daemon/src/routes/settings.rs index fa35c7cb..b8ff445b 100644 --- a/crates/skill-daemon/src/routes/settings.rs +++ b/crates/skill-daemon/src/routes/settings.rs @@ -1300,9 +1300,16 @@ fi # fresh PTY and proxies stdin/stdout while forwarding SIGWINCH correctly. # Log path is chosen internally (under ~/.skill/terminal-logs/) so it never # appears in argv or the terminal title. Set NEUROSKILL_RECORDING=1 to opt out. +# We intentionally avoid `exec` here: if the shim exits with code 126 it +# means startup failed (not a tty, can't open PTY, etc.) and we fall through +# to a plain interactive shell. For any other exit code (normal user exit, +# Ctrl-D, …) we forward it and close this shell too. if [[ -z "$NEUROSKILL_RECORDING" && -x "{daemon_path}" ]]; then export NEUROSKILL_RECORDING=1 - exec "{daemon_path}" tty + "{daemon_path}" tty + _ns_rc=$? + unset NEUROSKILL_RECORDING + [[ $_ns_rc -ne 126 ]] && exit $_ns_rc fi "# ), @@ -1356,9 +1363,13 @@ fi # Session recording via the daemon's `tty` PTY shim (forwards SIGWINCH). # Log path chosen internally — nothing leaks into argv or the tab title. +# See zsh block above for the fallback-on-failure rationale. if [[ -z "$NEUROSKILL_RECORDING" && -x "{daemon_path}" ]]; then export NEUROSKILL_RECORDING=1 - exec "{daemon_path}" tty + "{daemon_path}" tty + _ns_rc=$? + unset NEUROSKILL_RECORDING + [[ $_ns_rc -ne 126 ]] && exit $_ns_rc fi "# ), From 8c76d720685b6dc7d96aea34649cb79f5a6ddcb5 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:40:45 -0400 Subject: [PATCH 16/30] safety checks --- crates/skill-daemon/src/activity.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/crates/skill-daemon/src/activity.rs b/crates/skill-daemon/src/activity.rs index 5baf5df1..31bb49a2 100644 --- a/crates/skill-daemon/src/activity.rs +++ b/crates/skill-daemon/src/activity.rs @@ -238,11 +238,14 @@ fn ax_poll_active_window() -> Option { // ── Step 2: window title + document path via AXUIElement ───────────────── // One "Accessibility" permission covers all apps — no per-app dialogs. + // SAFETY: All CF/AX objects are null-checked before use; owned refs are + // released via CFRelease before the block exits. let (window_title, document_path) = unsafe { /// Convert a non-null CFStringRef to a Rust `String`. /// /// SAFETY: `s` must be a valid, non-null CFStringRef. unsafe fn cfstr_to_string(s: CFStringRef, enc: u32) -> String { + // SAFETY: upheld by the caller (see fn-level doc). unsafe { let len = CFStringGetLength(s); let max = CFStringGetMaximumSizeForEncoding(len, enc) + 1; @@ -255,21 +258,11 @@ fn ax_poll_active_window() -> Option { } } - let key_focused_win = CFStringCreateWithCString( - std::ptr::null(), - b"AXFocusedWindow\0".as_ptr() as *const c_char, - KCF_STRING_ENCODING_UTF8, - ); - let key_title = CFStringCreateWithCString( - std::ptr::null(), - b"AXTitle\0".as_ptr() as *const c_char, - KCF_STRING_ENCODING_UTF8, - ); - let key_document = CFStringCreateWithCString( - std::ptr::null(), - b"AXDocument\0".as_ptr() as *const c_char, - KCF_STRING_ENCODING_UTF8, - ); + let key_focused_win = + CFStringCreateWithCString(std::ptr::null(), c"AXFocusedWindow".as_ptr(), KCF_STRING_ENCODING_UTF8); + let key_title = CFStringCreateWithCString(std::ptr::null(), c"AXTitle".as_ptr(), KCF_STRING_ENCODING_UTF8); + let key_document = + CFStringCreateWithCString(std::ptr::null(), c"AXDocument".as_ptr(), KCF_STRING_ENCODING_UTF8); let app_ax = AXUIElementCreateApplication(pid); @@ -464,6 +457,7 @@ fn poll_secondary_windows() -> Vec { /// /// SAFETY: `s` must be a NUL-terminated byte slice. Caller must CFRelease. unsafe fn cfstr(s: &[u8]) -> CFStringRef { + // SAFETY: upheld by caller (NUL-terminated slice, result CFRelease'd). unsafe { CFStringCreateWithCString(std::ptr::null(), s.as_ptr() as *const c_char, K_CF_STRING_ENCODING_UTF8) } @@ -477,6 +471,7 @@ fn poll_secondary_windows() -> Vec { return None; } let mut v: i64 = 0; + // SAFETY: `n` is non-null and a valid CFNumber; `v` is a local i64. unsafe { if CFNumberGetValue(n, K_CF_NUMBER_SINT32_TYPE, &mut v) { Some(v as i32) @@ -494,6 +489,7 @@ fn poll_secondary_windows() -> Vec { return None; } let mut v: i64 = 0; + // SAFETY: `n` is non-null and a valid CFNumber; reinterpret bits as f64. unsafe { // CFNumberGetValue writes the numeric bits; reinterpret as f64. if CFNumberGetValue(n, K_CF_NUMBER_FLOAT64_TYPE, &mut v) { @@ -508,6 +504,7 @@ fn poll_secondary_windows() -> Vec { /// /// SAFETY: `s` must be a valid, non-null CFStringRef. unsafe fn cfstr_to_string(s: CFStringRef) -> String { + // SAFETY: upheld by the caller (see fn-level doc). unsafe { let len = CFStringGetLength(s); let max = CFStringGetMaximumSizeForEncoding(len, K_CF_STRING_ENCODING_UTF8) + 1; From 236abeb0f295bfedca30095aba0be3db9492feba Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:49:49 -0400 Subject: [PATCH 17/30] fix windows CI --- .github/workflows/release-windows.yml | 3 +-- changes/unreleased/auto-git-log.md | 3 +++ scripts/ci.mjs | 11 ++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 changes/unreleased/auto-git-log.md diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index 5a00b336..c8111389 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -910,9 +910,8 @@ jobs: # ── Discord notification ────────────────────────────────────────────────── - name: Notify Discord of release - if: always() && steps.version_meta.outputs.dry_run != 'true' + if: always() && steps.version_meta.outputs.is_release == 'true' shell: bash - env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: >- node scripts/ci.mjs discord-notify diff --git a/changes/unreleased/auto-git-log.md b/changes/unreleased/auto-git-log.md new file mode 100644 index 00000000..6da3566d --- /dev/null +++ b/changes/unreleased/auto-git-log.md @@ -0,0 +1,3 @@ +### Features + +- Minor updates and improvements diff --git a/scripts/ci.mjs b/scripts/ci.mjs index 028a7833..dae2ceea 100644 --- a/scripts/ci.mjs +++ b/scripts/ci.mjs @@ -323,7 +323,7 @@ function ensureRcLatestRelease() { ], { check: true }); } -function cmdDiscordNotify(args) { +async function cmdDiscordNotify(args) { const webhook = process.env.DISCORD_WEBHOOK_URL; if (!webhook) { console.log("⚠ DISCORD_WEBHOOK_URL not set, skipping."); @@ -359,8 +359,13 @@ function cmdDiscordNotify(args) { }); try { - const r = spawnSync("curl", ["-sf", "-X", "POST", webhook, "-H", "Content-Type: application/json", "-d", payload], { stdio: "pipe", encoding: "utf8" }); - if (r.status !== 0) throw new Error(`curl exited ${r.status}`); + // Use built-in fetch (Node 18+) to avoid platform-specific curl quoting issues. + const r = await fetch(webhook, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: payload, + }); + if (!r.ok) throw new Error(`HTTP ${r.status}`); } catch { console.log("⚠ Discord notification failed (non-fatal)."); } From c8d730716aad5dca1c9587855a3bc19ce37a1ef4 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:53:12 -0400 Subject: [PATCH 18/30] fix(cargo): remove obsolete muda and phf_generator patches Both patches are no longer used in the crate graph: - muda 0.17.2 patch: dependency has been upgraded to muda 0.19.1 - phf_generator 0.8.0 patch: dependency no longer resolves to 0.8.0 Cargo emits 'warning: patch ... was not used in the crate graph' for each of these, which the bump.js preflight treats as a hard failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2a2d7dd1..7cd26a18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ [workspace] resolver = "2" -exclude = ["patches/muda-0.17.2"] +exclude = [] members = [ "src-tauri", "crates/skill-autostart", @@ -68,8 +68,6 @@ members = [ [patch.crates-io] cubek-matmul = { git = "https://github.com/eugenehp/cubek.git", branch = "cubek-matmul", package = "cubek-matmul" } btleplug = { git = "https://github.com/eugenehp/btleplug.git", branch = "imrpoved_mac_version" } -# Fix muda ZeroWidth panic in to_png() on macOS (upstream bug in 0.17.2) -muda = { path = "patches/muda-0.17.2" } # Fix glib VariantStrIter unsoundness (GHSA-wrw7-89jp-8q8g) — &p → &mut p # All gtk-rs-core crates must come from the same source to keep -sys types aligned. glib = { git = "https://github.com/eugenehp/gtk-rs-core.git", branch = "0.18-patched" } @@ -78,8 +76,6 @@ gobject-sys = { git = "https://github.com/eugenehp/gtk-rs-core.git", branch gio = { git = "https://github.com/eugenehp/gtk-rs-core.git", branch = "0.18-patched" } gio-sys = { git = "https://github.com/eugenehp/gtk-rs-core.git", branch = "0.18-patched" } glib-macros = { git = "https://github.com/eugenehp/gtk-rs-core.git", branch = "0.18-patched" } -# Fix rand 0.7.3 vulnerability (GHSA-2qph-qpvm-2qf7) pulled in by selectors → phf_codegen → phf_generator -phf_generator = { path = "patches/phf_generator-0.8.0" } # burn-mlx on crates.io (0.1.2) targets burn 0.16; the git main branch supports # burn 0.20 which we use. This patch also covers fast-umap's transitive dep. burn-mlx = { git = "https://github.com/eidola-ai/burn-mlx", branch = "burn-0-20" } From aea9dd2b05573a43b033d0f3eefc009d00d3cc05 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:54:20 -0400 Subject: [PATCH 19/30] fixed cargo clippy --- Cargo.lock | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 819a5be8..cecf1f0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17349,11 +17349,3 @@ dependencies = [ "syn 2.0.117", "winnow 1.0.2", ] - -[[patch.unused]] -name = "muda" -version = "0.17.2" - -[[patch.unused]] -name = "phf_generator" -version = "0.8.0" From 2dda39e8b9146d2133cbf67e306df0e1a2219a56 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 20:56:23 -0400 Subject: [PATCH 20/30] 0.0.130-rc.8 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- .../auto-git-log.md => releases/0.0.130-rc.8.md} | 2 ++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 12 insertions(+), 4 deletions(-) rename changes/{unreleased/auto-git-log.md => releases/0.0.130-rc.8.md} (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba1ea42..c63b786b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5433,3 +5433,9 @@ The heatmap merges EEG data points with the closest timeline events to show whic ### Features - Replace per-app AppleScript window tracking (which triggered a macOS TCC Automation dialog for every new foreground app) with native Accessibility API (AXUIElement) + CoreGraphics calls that require only a single one-time "Accessibility" permission for NeuroSkill. + +## [0.0.130-rc.8] — 2026-05-02 + +### Features + +- Minor updates and improvements diff --git a/Cargo.lock b/Cargo.lock index cecf1f0d..e020dfac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11951,7 +11951,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.7" +version = "0.0.130-rc.8" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/unreleased/auto-git-log.md b/changes/releases/0.0.130-rc.8.md similarity index 58% rename from changes/unreleased/auto-git-log.md rename to changes/releases/0.0.130-rc.8.md index 6da3566d..710ed735 100644 --- a/changes/unreleased/auto-git-log.md +++ b/changes/releases/0.0.130-rc.8.md @@ -1,3 +1,5 @@ +## [0.0.130-rc.8] — 2026-05-02 + ### Features - Minor updates and improvements diff --git a/package.json b/package.json index a2fe704d..d82cb600 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.7", + "version": "0.0.130-rc.8", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ff475c1c..38942f17 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.7" +version = "0.0.130-rc.8" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 529d9b6b..3aa5e16c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.7", + "version": "0.0.130-rc.8", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From d63c625e01108de0a2d777d2afe33a5c9952c52d Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 21:06:53 -0400 Subject: [PATCH 21/30] 0.0.130-rc.9 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- changes/releases/0.0.130-rc.9.md | 5 +++++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 changes/releases/0.0.130-rc.9.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c63b786b..6be20c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5439,3 +5439,9 @@ The heatmap merges EEG data points with the closest timeline events to show whic ### Features - Minor updates and improvements + +## [0.0.130-rc.9] — 2026-05-02 + +### Features + +- Minor updates and improvements diff --git a/Cargo.lock b/Cargo.lock index e020dfac..59c6227d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11951,7 +11951,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.8" +version = "0.0.130-rc.9" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/releases/0.0.130-rc.9.md b/changes/releases/0.0.130-rc.9.md new file mode 100644 index 00000000..2e6109e1 --- /dev/null +++ b/changes/releases/0.0.130-rc.9.md @@ -0,0 +1,5 @@ +## [0.0.130-rc.9] — 2026-05-02 + +### Features + +- Minor updates and improvements diff --git a/package.json b/package.json index d82cb600..47a978e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.8", + "version": "0.0.130-rc.9", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 38942f17..d1f282d7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.8" +version = "0.0.130-rc.9" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3aa5e16c..7fe31e97 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.8", + "version": "0.0.130-rc.9", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From d258390995ff5c231db165072e0efab7345eec66 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 21:18:19 -0400 Subject: [PATCH 22/30] fix windows ci --- .github/workflows/release-windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index c8111389..deac7f58 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -912,6 +912,7 @@ jobs: - name: Notify Discord of release if: always() && steps.version_meta.outputs.is_release == 'true' shell: bash + env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: >- node scripts/ci.mjs discord-notify From 5d8caed1c5628bfa34a7b47f3dcdde04e74c9ff7 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 21:19:35 -0400 Subject: [PATCH 23/30] 0.0.130-rc.10 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- changes/releases/0.0.130-rc.10.md | 5 +++++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 changes/releases/0.0.130-rc.10.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be20c96..88111bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5397,6 +5397,12 @@ The heatmap merges EEG data points with the closest timeline events to show whic - **Update kittentts to 0.4.1**: TTS engine update. - **Add grayscale 0.0.1**: macOS grayscale display control for DND mode. +## [0.0.130-rc.10] — 2026-05-02 + +### Features + +- fix windows ci + ## [0.0.130-rc.2] — 2026-04-29 ### Features diff --git a/Cargo.lock b/Cargo.lock index 59c6227d..bf6ed607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11951,7 +11951,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.9" +version = "0.0.130-rc.10" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/releases/0.0.130-rc.10.md b/changes/releases/0.0.130-rc.10.md new file mode 100644 index 00000000..f0623da0 --- /dev/null +++ b/changes/releases/0.0.130-rc.10.md @@ -0,0 +1,5 @@ +## [0.0.130-rc.10] — 2026-05-02 + +### Features + +- fix windows ci diff --git a/package.json b/package.json index 47a978e0..c44af60a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.9", + "version": "0.0.130-rc.10", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d1f282d7..e1ad47e3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.9" +version = "0.0.130-rc.10" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7fe31e97..b5a33954 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.9", + "version": "0.0.130-rc.10", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From f529353c015117515e07f778cd031b026c1df25e Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 23:27:36 -0400 Subject: [PATCH 24/30] =?UTF-8?q?1.=20GPU=20f16=20SHADER=5FF16=20panic=20?= =?UTF-8?q?=E2=86=92=20`catch=5Funwind`=20in=20worker.rs=202.=20Search=20B?= =?UTF-8?q?ETWEEN=20mismatch=20=E2=86=92=20`DualTimestampRange`=20in=20ski?= =?UTF-8?q?ll-commands=203.=20`date=5Ffrom=5Fts`=20wrong=20date=20folder?= =?UTF-8?q?=20for=2017-digit=20=E2=86=92=20digit-count=20dispatch=20in=20s?= =?UTF-8?q?kill-commands=204.=20UMAP=20`load=5Fembeddings=5Frange`=20same?= =?UTF-8?q?=20BETWEEN=20+=20`ts/1000`=20bugs=20=E2=86=92=20skill-router=20?= =?UTF-8?q?5.=20Reembed=20`extract=5Fepoch=5Fsamples`=20given=20garbage=20?= =?UTF-8?q?seconds=20=E2=86=92=20`epoch=5Fts=5Fto=5Funix`=20in=20settings?= =?UTF-8?q?=5Fexg.rs=206.=20Session=20epoch-count=20always=20`None`=20in?= =?UTF-8?q?=20history=20UI=20=E2=86=92=20`epoch=5Fts=5Fto=5Funix`=20in=20s?= =?UTF-8?q?kill-history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/skill-commands/src/lib.rs | 111 +++++++++++++----- crates/skill-daemon/src/embed/worker.rs | 21 +++- .../skill-daemon/src/routes/settings_exg.rs | 2 +- crates/skill-data/src/lib.rs | 3 + crates/skill-history/src/lib.rs | 2 +- crates/skill-router/src/lib.rs | 36 ++++-- 6 files changed, 127 insertions(+), 48 deletions(-) diff --git a/crates/skill-commands/src/lib.rs b/crates/skill-commands/src/lib.rs index 1d272de5..4bc4aaf6 100644 --- a/crates/skill-commands/src/lib.rs +++ b/crates/skill-commands/src/lib.rs @@ -38,7 +38,7 @@ pub mod graph; pub use graph::{dot_edge_label, dot_esc, dot_node_label, generate_dot, generate_svg, generate_svg_3d, SvgLabels}; // Re-export shared utilities so downstream crates keep compiling. -pub use skill_data::util::{fmt_unix_utc, ts_to_unix, unix_to_ts, MutexExt}; +pub use skill_data::util::{fmt_unix_utc, ts_to_unix, unix_to_ts, DualTimestampRange, MutexExt}; /// Shared, optionally-ready global HNSW index. /// @@ -244,15 +244,21 @@ struct RawEmb { embedding: Vec, } -/// Read every embedding in [start_ts, end_ts] from a single day's SQLite. -fn read_embeddings_in_range(db_path: &Path, start_ts: i64, end_ts: i64) -> Vec { - read_embeddings_in_range_filtered(db_path, start_ts, end_ts, None) +/// Read every embedding in [start_utc, end_utc] from a single day's SQLite. +/// +/// Uses [`DualTimestampRange`] so it matches all three timestamp formats that +/// may appear in the `embeddings` table: +/// - Unix milliseconds (13 digits) +/// - `YYYYMMDDHHmmss` (14 digits, pre-Apr 2026) +/// - `YYYYMMDDHHmmss × 1000` (17 digits, Apr 2026+) +fn read_embeddings_in_range(db_path: &Path, start_utc: u64, end_utc: u64) -> Vec { + read_embeddings_in_range_filtered(db_path, start_utc, end_utc, None) } fn read_embeddings_in_range_filtered( db_path: &Path, - start_ts: i64, - end_ts: i64, + start_utc: u64, + end_utc: u64, device_filter: Option<&str>, ) -> Vec { let conn = match skill_data::util::open_readonly(db_path) { @@ -263,30 +269,46 @@ fn read_embeddings_in_range_filtered( } }; + let r = skill_data::util::DualTimestampRange::from_unix_secs(start_utc, end_utc); + let ts_where = skill_data::util::DualTimestampRange::WHERE_CLAUSE; + let (sql, params): (String, Vec>) = if let Some(dev) = device_filter { ( - "SELECT hnsw_id, timestamp, eeg_embedding \ - FROM embeddings \ - WHERE timestamp BETWEEN ?1 AND ?2 \ - AND length(eeg_embedding) >= 4 \ - AND device_name = ?3 \ - ORDER BY timestamp" - .into(), + format!( + "SELECT hnsw_id, timestamp, eeg_embedding \ + FROM embeddings \ + WHERE ({ts_where}) \ + AND length(eeg_embedding) >= 4 \ + AND device_name = ?7 \ + ORDER BY timestamp" + ), vec![ - Box::new(start_ts) as Box, - Box::new(end_ts), + Box::new(r.unix_ms_start) as Box, + Box::new(r.unix_ms_end), + Box::new(r.dt14_start), + Box::new(r.dt14_end), + Box::new(r.dt17_start), + Box::new(r.dt17_end), Box::new(dev.to_string()), ], ) } else { ( - "SELECT hnsw_id, timestamp, eeg_embedding \ - FROM embeddings \ - WHERE timestamp BETWEEN ?1 AND ?2 \ - AND length(eeg_embedding) >= 4 \ - ORDER BY timestamp" - .into(), - vec![Box::new(start_ts) as Box, Box::new(end_ts)], + format!( + "SELECT hnsw_id, timestamp, eeg_embedding \ + FROM embeddings \ + WHERE ({ts_where}) \ + AND length(eeg_embedding) >= 4 \ + ORDER BY timestamp" + ), + vec![ + Box::new(r.unix_ms_start) as Box, + Box::new(r.unix_ms_end), + Box::new(r.dt14_start), + Box::new(r.dt14_end), + Box::new(r.dt17_start), + Box::new(r.dt17_end), + ], ) }; @@ -315,8 +337,24 @@ fn read_embeddings_in_range_filtered( } /// Derive the `YYYYMMDD` date string from a `YYYYMMDDHHmmss` timestamp integer. +/// Extract a `YYYYMMDD` directory name from any embeddings-table timestamp. +/// +/// Handles all three historical formats: +/// - 17-digit `YYYYMMDDHHmmss × 1000` (e.g. `20260427034308000`) → divide by 10^9 +/// - 14-digit `YYYYMMDDHHmmss` (e.g. `20260427034308`) → divide by 10^6 +/// - 13-digit Unix milliseconds (e.g. `1777362376000`) → convert via calendar fn date_from_ts(ts: i64) -> String { - format!("{}", ts / 1_000_000) + let digits = if ts > 0 { (ts as f64).log10() as u32 + 1 } else { 0 }; + match digits { + 17 => format!("{}", ts / 1_000_000_000), // YYYYMMDDHHmmss×1000 → YYYYMMDD + 14 => format!("{}", ts / 1_000_000), // YYYYMMDDHHmmss → YYYYMMDD + _ => { + // Unix milliseconds: convert to Unix secs, then to YYYYMMDDHHmmss, take date part. + let secs = (ts.max(0) / 1000) as u64; + let dt14 = skill_data::util::unix_to_ts(secs); + format!("{}", dt14 / 1_000_000) + } + } } /// Convert a database timestamp (ms) to Unix seconds. @@ -464,12 +502,10 @@ pub fn search_embeddings_in_range_for( global_index: GlobalIndexHandle, model_backend: &str, ) -> SearchResult { - let start_ts = (start_utc as i64) * 1000; - let end_ts = (end_utc as i64) * 1000; let labels_db = skill_dir.join(LABELS_FILE); let date_dirs = list_date_dirs(skill_dir); - // ── Collect query embeddings from days that overlap [start_ts, end_ts] ──── + // ── Collect query embeddings from days that overlap [start_utc, end_utc] ──── // Store index into `date_dirs` to avoid cloning String/PathBuf per embedding. let mut query_embs: Vec<(usize, RawEmb)> = Vec::new(); for (dd_idx, (date, dir)) in date_dirs.iter().enumerate() { @@ -477,7 +513,7 @@ pub fn search_embeddings_in_range_for( if !db_path.exists() { continue; } - let embs = read_embeddings_in_range(&db_path, start_ts, end_ts); + let embs = read_embeddings_in_range(&db_path, start_utc, end_utc); if !embs.is_empty() { eprintln!("[search] {} query embs from {}", embs.len(), date); } @@ -648,8 +684,6 @@ pub fn stream_search_inner_for( emit: &dyn Fn(SearchProgress), model_backend: &str, ) { - let start_ts = (start_utc as i64) * 1000; - let end_ts = (end_utc as i64) * 1000; let labels_db = skill_dir.join(LABELS_FILE); let date_dirs = list_date_dirs(skill_dir); @@ -681,7 +715,7 @@ pub fn stream_search_inner_for( if !db_path.exists() { continue; } - let embs = read_embeddings_in_range_filtered(&db_path, start_ts, end_ts, device_filter); + let embs = read_embeddings_in_range_filtered(&db_path, start_utc, end_utc, device_filter); let _ = date; // used only for db_path for emb in embs { query_embs.push((dd_idx, emb)); @@ -1460,7 +1494,7 @@ mod tests { #[test] fn date_from_ts_extracts_date_prefix() { - // ts format is YYYYMMDDHHmmss — dividing by 1_000_000 gives YYYYMMDD + // 14-digit YYYYMMDDHHmmss → divide by 10^6 assert_eq!(date_from_ts(20260414143000), "20260414"); } @@ -1469,6 +1503,21 @@ mod tests { assert_eq!(date_from_ts(19700101000000), "19700101"); } + #[test] + fn date_from_ts_17digit() { + // 17-digit YYYYMMDDHHmmss×1000 (current stored format) → divide by 10^9 + assert_eq!(date_from_ts(20260427034308000), "20260427"); + } + + #[test] + fn date_from_ts_unix_ms() { + // Unix milliseconds (13 digits) → calendar conversion + // 1777362376000 ms = 2026-04-26 ... UTC + let result = date_from_ts(1777362376000); + assert!(result.starts_with("2026"), "expected 2026 date, got {result}"); + assert_eq!(result.len(), 8, "YYYYMMDD must be 8 chars, got {result}"); + } + // ── ts_ms_to_unix ──────────────────────────────────────────────────── #[test] diff --git a/crates/skill-daemon/src/embed/worker.rs b/crates/skill-daemon/src/embed/worker.rs index 3e6cc7d9..95af9200 100644 --- a/crates/skill-daemon/src/embed/worker.rs +++ b/crates/skill-daemon/src/embed/worker.rs @@ -628,11 +628,24 @@ fn load_encoder(config: &ExgModelConfig, _skill_dir: &Path) -> Option { } #[cfg(feature = "embed-zuna-gpu-f16")] if try_gpu { - if let Some(s) = load_zuna_gpu_f16(config) { - info!("ZUNA GPU f16 encoder loaded"); - return Some(Encoder::ZunaGpuF16(Box::new(s))); + // Wrap in catch_unwind: on adapters where wgpu does not expose + // SHADER_F16 (e.g. Vulkan/DX12 without storageInputOutput16), + // burn's naga validation panics with "Using f16 values requires + // the naga::valid::Capabilities::FLOAT16 flag". Without this + // guard the entire embed worker thread would die and epochs + // would be silently dropped for the rest of the session. + let f16_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| load_zuna_gpu_f16(config))); + match f16_result { + Ok(Some(s)) => { + info!("ZUNA GPU f16 encoder loaded"); + return Some(Encoder::ZunaGpuF16(Box::new(s))); + } + Ok(None) => warn!("GPU f16 unavailable, trying GPU f32"), + Err(_) => warn!( + "ZUNA GPU f16 panicked — adapter likely lacks SHADER_F16 \ + (naga FLOAT16 capability); falling back to GPU f32" + ), } - warn!("GPU f16 unavailable, trying GPU f32"); } #[cfg(feature = "embed-zuna-gpu")] if try_gpu { diff --git a/crates/skill-daemon/src/routes/settings_exg.rs b/crates/skill-daemon/src/routes/settings_exg.rs index 9022fe15..8e0ea6b2 100644 --- a/crates/skill-daemon/src/routes/settings_exg.rs +++ b/crates/skill-daemon/src/routes/settings_exg.rs @@ -305,7 +305,7 @@ pub(crate) fn run_batch_reembed_with_cancel( let _ = conn.execute_batch("BEGIN"); for (row_id, ts_ms) in chunk { - let ts_secs = (*ts_ms as f64) / 1000.0; + let ts_secs = skill_data::util::epoch_ts_to_unix(*ts_ms) as f64; let (samples, seg_ch_names) = extract_epoch_samples(&raw_data, ts_secs, epoch_samples); if samples.is_empty() { diff --git a/crates/skill-data/src/lib.rs b/crates/skill-data/src/lib.rs index bddf02af..8fb37226 100644 --- a/crates/skill-data/src/lib.rs +++ b/crates/skill-data/src/lib.rs @@ -39,3 +39,6 @@ pub mod util; pub mod validation_store; pub use error::{SessionError, StoreError}; +// Timestamp utilities re-exported for convenience — prefer these over +// hand-rolling `ts * 1000` arithmetic at call sites. +pub use util::{epoch_ts_to_unix, unix_to_ts, yyyymmddhhmmss_utc, DualTimestampRange}; diff --git a/crates/skill-history/src/lib.rs b/crates/skill-history/src/lib.rs index c04f3a44..f131a34e 100644 --- a/crates/skill-history/src/lib.rs +++ b/crates/skill-history/src/lib.rs @@ -911,7 +911,7 @@ pub fn list_embedding_sessions(skill_dir: &Path) -> Vec { day_names.push(day_name); if let Ok(rows) = rows { for row in rows.filter_map(std::result::Result::ok) { - all_ts.push(((row / 1000) as u64, day_idx)); + all_ts.push((skill_data::util::epoch_ts_to_unix(row), day_idx)); } } } diff --git a/crates/skill-router/src/lib.rs b/crates/skill-router/src/lib.rs index 70ff8eae..f802f753 100644 --- a/crates/skill-router/src/lib.rs +++ b/crates/skill-router/src/lib.rs @@ -109,9 +109,13 @@ pub struct RoundedScores { // ── Embedding / label loaders ───────────────────────────────────────────────── /// Load all embedding vectors from daily SQLite DBs in [start, end] UTC range. +/// +/// Uses [`skill_data::util::DualTimestampRange`] to match all three timestamp +/// formats that may be stored in the `embeddings` table (Unix ms, 14-digit +/// `YYYYMMDDHHmmss`, or 17-digit `YYYYMMDDHHmmss × 1000`). pub fn load_embeddings_range(skill_dir: &Path, start_utc: u64, end_utc: u64) -> Vec<(u64, Vec)> { - let ts_start = (start_utc as i64) * 1000; - let ts_end = (end_utc as i64) * 1000; + let r = skill_data::util::DualTimestampRange::from_unix_secs(start_utc, end_utc); + let ts_where = skill_data::util::DualTimestampRange::WHERE_CLAUSE; let mut out: Vec<(u64, Vec)> = Vec::new(); let Ok(entries) = std::fs::read_dir(skill_dir) else { @@ -130,19 +134,29 @@ pub fn load_embeddings_range(skill_dir: &Path, start_utc: u64, end_utc: u64) -> continue; }; let _ = conn.execute_batch("PRAGMA busy_timeout=2000;"); - let Ok(mut stmt) = conn.prepare( + let Ok(mut stmt) = conn.prepare(&format!( "SELECT timestamp, eeg_embedding FROM embeddings - WHERE timestamp >= ?1 AND timestamp <= ?2 ORDER BY timestamp", - ) else { + WHERE ({ts_where}) ORDER BY timestamp" + )) else { continue; }; - let rows = stmt.query_map(rusqlite::params![ts_start, ts_end], |row| { - let ts: i64 = row.get(0)?; - let blob: Vec = row.get(1)?; - let emb: Vec = skill_data::util::blob_to_f32(&blob); - Ok(((ts / 1000) as u64, emb)) - }); + let rows = stmt.query_map( + rusqlite::params![ + r.unix_ms_start, + r.unix_ms_end, + r.dt14_start, + r.dt14_end, + r.dt17_start, + r.dt17_end + ], + |row| { + let ts: i64 = row.get(0)?; + let blob: Vec = row.get(1)?; + let emb: Vec = skill_data::util::blob_to_f32(&blob); + Ok((skill_data::util::epoch_ts_to_unix(ts), emb)) + }, + ); if let Ok(rows) = rows { for r in rows.flatten() { out.push(r); From bdd3d7af398a032b1e5a81d37bb59bcbcdea40da Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 23:33:46 -0400 Subject: [PATCH 25/30] log_enabled_by_default --- changes/unreleased/auto-git-log.md | 3 +++ crates/skill-tts/src/log.rs | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 changes/unreleased/auto-git-log.md diff --git a/changes/unreleased/auto-git-log.md b/changes/unreleased/auto-git-log.md new file mode 100644 index 00000000..8fa5c493 --- /dev/null +++ b/changes/unreleased/auto-git-log.md @@ -0,0 +1,3 @@ +### Features + +- 1. GPU f16 SHADER_F16 panic → `catch_unwind` in worker.rs diff --git a/crates/skill-tts/src/log.rs b/crates/skill-tts/src/log.rs index f8f541d9..4796d12e 100644 --- a/crates/skill-tts/src/log.rs +++ b/crates/skill-tts/src/log.rs @@ -80,15 +80,20 @@ pub fn write_log(tag: &str, msg: &str) { #[cfg(test)] mod tests { use super::*; + use std::sync::Mutex; + + // Serialize all tests that read or write the global ENABLED flag. + static ENABLED_LOCK: Mutex<()> = Mutex::new(()); #[test] fn log_enabled_by_default() { - // Note: other tests may have toggled this, so we just check the function works - let _ = log_enabled(); + let _g = ENABLED_LOCK.lock().unwrap(); + assert!(log_enabled()); } #[test] fn set_enabled_toggles() { + let _g = ENABLED_LOCK.lock().unwrap(); set_log_enabled(false); assert!(!log_enabled()); set_log_enabled(true); @@ -97,12 +102,14 @@ mod tests { #[test] fn write_log_does_not_panic_without_callback() { + let _g = ENABLED_LOCK.lock().unwrap(); set_log_enabled(true); write_log("test", "hello from test"); } #[test] fn write_log_noop_when_disabled() { + let _g = ENABLED_LOCK.lock().unwrap(); set_log_enabled(false); write_log("test", "should not appear"); set_log_enabled(true); From b57b03dcc3ae2a5aa88ccb23b7c69fc4a8f171d3 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Fri, 1 May 2026 23:34:55 -0400 Subject: [PATCH 26/30] 0.0.130-rc.11 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- .../auto-git-log.md => releases/0.0.130-rc.11.md} | 2 ++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 6 files changed, 12 insertions(+), 4 deletions(-) rename changes/{unreleased/auto-git-log.md => releases/0.0.130-rc.11.md} (68%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88111bf4..e7b079d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5403,6 +5403,12 @@ The heatmap merges EEG data points with the closest timeline events to show whic - fix windows ci +## [0.0.130-rc.11] — 2026-05-02 + +### Features + +- 1. GPU f16 SHADER_F16 panic → `catch_unwind` in worker.rs + ## [0.0.130-rc.2] — 2026-04-29 ### Features diff --git a/Cargo.lock b/Cargo.lock index bf6ed607..25d82e37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11951,7 +11951,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.10" +version = "0.0.130-rc.11" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/changes/unreleased/auto-git-log.md b/changes/releases/0.0.130-rc.11.md similarity index 68% rename from changes/unreleased/auto-git-log.md rename to changes/releases/0.0.130-rc.11.md index 8fa5c493..48183a52 100644 --- a/changes/unreleased/auto-git-log.md +++ b/changes/releases/0.0.130-rc.11.md @@ -1,3 +1,5 @@ +## [0.0.130-rc.11] — 2026-05-02 + ### Features - 1. GPU f16 SHADER_F16 panic → `catch_unwind` in worker.rs diff --git a/package.json b/package.json index c44af60a..3c463152 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.10", + "version": "0.0.130-rc.11", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e1ad47e3..a8ad5b15 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.10" +version = "0.0.130-rc.11" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b5a33954..7fbb7664 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.10", + "version": "0.0.130-rc.11", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", From e08ea9b7fc2c971ae013dcd6c0a4e3e034a64a09 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Sun, 3 May 2026 00:11:42 -0400 Subject: [PATCH 27/30] auto-update + update RC settings --- src-tauri/src/auto_update.rs | 55 +++++++++++++++++ src-tauri/src/lib.rs | 4 ++ src/lib/i18n/de/ui.ts | 6 ++ src/lib/i18n/en/ui.ts | 5 ++ src/lib/i18n/es/ui.ts | 6 ++ src/lib/i18n/fr/ui.ts | 6 ++ src/lib/i18n/he/ui.ts | 6 ++ src/lib/i18n/ja/ui.ts | 6 ++ src/lib/i18n/keys.ts | 6 ++ src/lib/i18n/ko/ui.ts | 6 ++ src/lib/i18n/uk/ui.ts | 6 ++ src/lib/i18n/zh/ui.ts | 5 ++ src/lib/settings/UpdatesTab.svelte | 97 +++++++++++++++++++++++++++++- 13 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src-tauri/src/auto_update.rs diff --git a/src-tauri/src/auto_update.rs b/src-tauri/src/auto_update.rs new file mode 100644 index 00000000..b11b71a1 --- /dev/null +++ b/src-tauri/src/auto_update.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 NeuroSkill.com +// +//! Auto-update opt-out preference. +//! +//! When enabled (the default), the frontend automatically downloads and +//! installs an update as soon as the background poller emits +//! `update-available`. When disabled, the same event surfaces a notice in +//! the Updates tab and the user must click "Install" to proceed. +//! +//! Storage mirrors `update_channel.rs`: a single ASCII line in +//! `/auto-update.txt` containing `true` or `false`. A +//! missing or unreadable file is treated as `true` so first-run users get +//! today's behavior. + +use std::path::PathBuf; +use tauri::{AppHandle, Manager}; + +const PREF_FILE: &str = "auto-update.txt"; + +fn pref_path(app: &AppHandle) -> Option { + app.path() + .app_local_data_dir() + .ok() + .map(|d| d.join(PREF_FILE)) +} + +pub fn read_auto_update_enabled(app: &AppHandle) -> bool { + let Some(path) = pref_path(app) else { + return true; + }; + match std::fs::read_to_string(&path) { + Ok(s) => match s.trim().to_ascii_lowercase().as_str() { + "false" => false, + "true" => true, + _ => true, + }, + Err(_) => true, + } +} + +#[tauri::command] +pub fn get_auto_update_enabled(app: AppHandle) -> bool { + read_auto_update_enabled(&app) +} + +#[tauri::command] +pub fn set_auto_update_enabled(app: AppHandle, enabled: bool) -> Result<(), String> { + let path = pref_path(&app).ok_or_else(|| "app_local_data_dir unavailable".to_string())?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + std::fs::write(&path, if enabled { "true" } else { "false" }).map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9616716b..695373e2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -79,6 +79,7 @@ mod tray; mod about; mod active_window; +mod auto_update; mod shortcut_cmds; mod update_channel; @@ -108,6 +109,7 @@ use std::sync::{Arc, Mutex}; use tauri::Manager; use about::{get_about_info, open_about_window}; +use auto_update::{get_auto_update_enabled, set_auto_update_enabled}; use daemon_cmds::{ cancel_session, cancel_weights_download, daemon_install_service, daemon_uninstall_service, estimate_reembed, force_restart_daemon, get_daemon_bootstrap, get_daemon_service_status, @@ -311,6 +313,8 @@ pub fn run() { set_update_channel, channel_check_for_update, channel_download_and_install, + get_auto_update_enabled, + set_auto_update_enabled, pick_ref_wav_file, get_recent_active_windows, get_recent_input_activity, diff --git a/src/lib/i18n/de/ui.ts b/src/lib/i18n/de/ui.ts index 3850e66c..6d8d5d4b 100644 --- a/src/lib/i18n/de/ui.ts +++ b/src/lib/i18n/de/ui.ts @@ -215,6 +215,12 @@ const ui: Record = { "Automatische Updateprüfung ist deaktiviert. Nutze den Button oben zur manuellen Prüfung.", "updates.autostart": "Bei Anmeldung starten", "updates.autostartDesc": "Startet automatisch, wenn du dich an deinem Computer anmeldest.", + "updates.autoUpdate": "Updates automatisch installieren", + "updates.autoUpdateDesc": + "Neue Versionen im Hintergrund herunterladen und beim nächsten Neustart installieren. Deaktivieren, um den Zeitpunkt selbst zu wählen.", + "updates.autoUpdateOffNotice": + "Automatische Installation ist aus — auf „Installieren“ klicken, um herunterzuladen und zu aktualisieren.", + "updates.installNow": "Installieren", "updates.autoCheckDesc": "Nach Updates prüfen, sobald die App startet, einmal pro Tag.", "updates.footer": "Updates werden automatisch heruntergeladen. Starten Sie neu, wenn Sie bereit sind.", diff --git a/src/lib/i18n/en/ui.ts b/src/lib/i18n/en/ui.ts index b440bc22..8e51b9fc 100644 --- a/src/lib/i18n/en/ui.ts +++ b/src/lib/i18n/en/ui.ts @@ -119,6 +119,11 @@ const ui: Record = { "updates.intervalOffWarning": "Automatic update checks are disabled. Use the button above to check manually.", "updates.autostart": "Launch at Login", "updates.autostartDesc": "Start automatically when you log in to your computer.", + "updates.autoUpdate": "Install updates automatically", + "updates.autoUpdateDesc": + "Download new versions in the background and install them on the next restart. Turn off to choose when to install.", + "updates.autoUpdateOffNotice": "Automatic install is off — click Install to download and update.", + "updates.installNow": "Install", "updates.receivePrereleases": "Receive pre-releases", "updates.receivePrereleasesDesc": "Opt into release candidates ahead of stable releases. Both manual and background update checks honor this setting live.", diff --git a/src/lib/i18n/es/ui.ts b/src/lib/i18n/es/ui.ts index 276347af..5230ca05 100644 --- a/src/lib/i18n/es/ui.ts +++ b/src/lib/i18n/es/ui.ts @@ -135,6 +135,12 @@ const ui: Record = { "Las comprobaciones de actualizaciones automáticas están deshabilitadas. Utilice el botón de arriba para comprobarlo manualmente.", "updates.autostart": "Iniciar sesión", "updates.autostartDesc": "Se inicia automáticamente cuando inicia sesión en su computadora.", + "updates.autoUpdate": "Instalar actualizaciones automáticamente", + "updates.autoUpdateDesc": + "Descarga las nuevas versiones en segundo plano e instálalas al reiniciar. Desactiva esta opción para elegir cuándo instalar.", + "updates.autoUpdateOffNotice": + "La instalación automática está desactivada — haz clic en Instalar para descargar y actualizar.", + "updates.installNow": "Instalar", "updates.footer": "Las actualizaciones se descargan automáticamente. Reinicie cuando esté listo para presentar la solicitud.", diff --git a/src/lib/i18n/fr/ui.ts b/src/lib/i18n/fr/ui.ts index cf836148..9fb2e825 100644 --- a/src/lib/i18n/fr/ui.ts +++ b/src/lib/i18n/fr/ui.ts @@ -216,6 +216,12 @@ const ui: Record = { "Les vérifications automatiques sont désactivées. Utilisez le bouton ci-dessus pour vérifier manuellement.", "updates.autostart": "Lancer à la connexion", "updates.autostartDesc": "Démarre automatiquement quand vous ouvrez une session.", + "updates.autoUpdate": "Installer les mises à jour automatiquement", + "updates.autoUpdateDesc": + "Télécharger les nouvelles versions en arrière-plan et les installer au prochain redémarrage. Désactivez pour choisir quand installer.", + "updates.autoUpdateOffNotice": + "L'installation automatique est désactivée — cliquez sur Installer pour télécharger et mettre à jour.", + "updates.installNow": "Installer", "updates.autoCheckDesc": "Vérifier les mises à jour une fois par jour au démarrage de l'application.", "updates.footer": "Les mises à jour sont téléchargées automatiquement. Redémarrez quand vous êtes prêt.", diff --git a/src/lib/i18n/he/ui.ts b/src/lib/i18n/he/ui.ts index d7cf4789..8e9353f5 100644 --- a/src/lib/i18n/he/ui.ts +++ b/src/lib/i18n/he/ui.ts @@ -220,6 +220,12 @@ const ui: Record = { "updates.intervalOffWarning": "בדיקות אוטומטיות מושבתות. השתמש בכפתור למעלה לבדיקה ידנית.", "updates.autostart": "הפעלה בכניסה למערכת", "updates.autostartDesc": "מתחיל אוטומטית כשנכנסים למחשב.", + "updates.autoUpdate": "התקן עדכונים אוטומטית", + "updates.autoUpdateDesc": + "הורד גרסאות חדשות ברקע והתקן בהפעלה הבאה. כבה כדי לבחור מתי להתקין.", + "updates.autoUpdateOffNotice": + "התקנה אוטומטית כבויה — לחץ על התקן כדי להוריד ולעדכן.", + "updates.installNow": "התקן", "updates.autoCheckDesc": "בדוק עדכונים פעם ביום כאשר האפליקציה מתחילה.", "updates.footer": "עדכונים מורדים אוטומטית. הפעל מחדש כשנוח לך.", diff --git a/src/lib/i18n/ja/ui.ts b/src/lib/i18n/ja/ui.ts index 0c887005..5b403d5c 100644 --- a/src/lib/i18n/ja/ui.ts +++ b/src/lib/i18n/ja/ui.ts @@ -116,6 +116,12 @@ const ui: Record = { "updates.intervalOffWarning": "自動アップデート確認が無効です。上のボタンを使用して手動で確認してください。", "updates.autostart": "ログイン時に起動", "updates.autostartDesc": "コンピューターにログインしたときに自動的に起動します。", + "updates.autoUpdate": "アップデートを自動的にインストール", + "updates.autoUpdateDesc": + "新しいバージョンをバックグラウンドでダウンロードし、次回の再起動時にインストールします。タイミングを自分で選ぶには無効にしてください。", + "updates.autoUpdateOffNotice": + "自動インストールはオフです — 「インストール」をクリックしてダウンロードと更新を行ってください。", + "updates.installNow": "インストール", "updates.footer": "アップデートは自動的にダウンロードされます。準備ができたら再起動して適用してください。", "whatsNew.title": "新着情報", diff --git a/src/lib/i18n/keys.ts b/src/lib/i18n/keys.ts index 89e36b1f..4157f03b 100644 --- a/src/lib/i18n/keys.ts +++ b/src/lib/i18n/keys.ts @@ -2727,7 +2727,10 @@ export type TranslationKey = | "umapSettings.timeoutDesc" | "updates.autoCheck" | "updates.autoCheckDesc" + | "updates.autoUpdate" + | "updates.autoUpdateDesc" | "updates.autoUpdateFailedOnline" + | "updates.autoUpdateOffNotice" | "updates.autostart" | "updates.autostartDesc" | "updates.available" @@ -2740,6 +2743,7 @@ export type TranslationKey = | "updates.downloadNow" | "updates.downloading" | "updates.footer" + | "updates.installNow" | "updates.installed" | "updates.interval15m" | "updates.interval1h" @@ -2751,6 +2755,8 @@ export type TranslationKey = | "updates.lastChecked" | "updates.openDownloadPageFailed" | "updates.readyToRestart" + | "updates.receivePrereleases" + | "updates.receivePrereleasesDesc" | "updates.restartNow" | "updates.restartToApply" | "updates.restartWhenReady" diff --git a/src/lib/i18n/ko/ui.ts b/src/lib/i18n/ko/ui.ts index efa9c981..bd8ebb88 100644 --- a/src/lib/i18n/ko/ui.ts +++ b/src/lib/i18n/ko/ui.ts @@ -112,6 +112,12 @@ const ui: Record = { "updates.intervalOffWarning": "자동 업데이트 확인이 비활성화되었습니다. 위의 버튼으로 수동 확인하세요.", "updates.autostart": "로그인 시 시작", "updates.autostartDesc": "컴퓨터에 로그인할 때 자동으로 시작합니다.", + "updates.autoUpdate": "업데이트 자동 설치", + "updates.autoUpdateDesc": + "백그라운드에서 새 버전을 다운로드하고 다음 재시작 시 설치합니다. 설치 시점을 직접 선택하려면 끄세요.", + "updates.autoUpdateOffNotice": + "자동 설치가 꺼져 있습니다 — 다운로드 및 업데이트하려면 설치를 클릭하세요.", + "updates.installNow": "설치", "updates.footer": "업데이트는 자동으로 다운로드됩니다. 적용 준비가 되면 재시작하세요.", "whatsNew.title": "새로운 기능", diff --git a/src/lib/i18n/uk/ui.ts b/src/lib/i18n/uk/ui.ts index 0aff8697..e512a08f 100644 --- a/src/lib/i18n/uk/ui.ts +++ b/src/lib/i18n/uk/ui.ts @@ -209,6 +209,12 @@ const ui: Record = { "updates.intervalOffWarning": "Автоматичну перевірку вимкнено. Скористайтесь кнопкою вище для перевірки вручну.", "updates.autostart": "Запуск під час входу", "updates.autostartDesc": "Запускається автоматично при вході в систему.", + "updates.autoUpdate": "Встановлювати оновлення автоматично", + "updates.autoUpdateDesc": + "Завантажувати нові версії у фоні та встановлювати під час наступного перезапуску. Вимкніть, щоб обирати момент встановлення вручну.", + "updates.autoUpdateOffNotice": + "Автоматичне встановлення вимкнено — натисніть «Встановити», щоб завантажити та оновити.", + "updates.installNow": "Встановити", "updates.autoCheckDesc": "Перевіряти оновлення раз на день під час запуску застосунку.", "updates.footer": "Оновлення завантажуються автоматично. Перезапустіть, коли будете готові.", diff --git a/src/lib/i18n/zh/ui.ts b/src/lib/i18n/zh/ui.ts index 466f2020..c936d354 100644 --- a/src/lib/i18n/zh/ui.ts +++ b/src/lib/i18n/zh/ui.ts @@ -114,6 +114,11 @@ const ui: Record = { "updates.intervalOffWarning": "已禁用自动更新检查。请使用上方按钮手动检查。", "updates.autostart": "登录时启动", "updates.autostartDesc": "登录计算机时自动启动。", + "updates.autoUpdate": "自动安装更新", + "updates.autoUpdateDesc": + "在后台下载新版本,并在下次重启时安装。关闭后可自行选择安装时机。", + "updates.autoUpdateOffNotice": "自动安装已关闭 — 点击“安装”以下载并更新。", + "updates.installNow": "安装", "updates.footer": "更新会自动下载。准备好后重启即可应用。", "whatsNew.title": "新功能", diff --git a/src/lib/settings/UpdatesTab.svelte b/src/lib/settings/UpdatesTab.svelte index ab5e07e8..7047860e 100644 --- a/src/lib/settings/UpdatesTab.svelte +++ b/src/lib/settings/UpdatesTab.svelte @@ -53,6 +53,13 @@ let updateChannelRc = $state(false); let channelSaving = $state(false); let channelError = $state(""); +// Auto-update (backend-persisted; default true). When false, the +// `update-available` event surfaces a notice + manual Install button +// instead of immediately downloading and installing. +let autoUpdateEnabled = $state(true); +let autoUpdateSaving = $state(false); +let autoUpdateError = $state(""); + // ── Interval options ────────────────────────────────────────────────────── const INTERVAL_OPTIONS: [number, string][] = [ [900, "updates.interval15m"], @@ -261,6 +268,21 @@ async function toggleChannel() { } } +// ── Auto-update opt-out ─────────────────────────────────────────────────── +async function toggleAutoUpdate() { + autoUpdateError = ""; + autoUpdateSaving = true; + const next = !autoUpdateEnabled; + try { + await invoke("set_auto_update_enabled", { enabled: next }); + autoUpdateEnabled = next; + } catch (e) { + autoUpdateError = String(e); + } finally { + autoUpdateSaving = false; + } +} + // ── Update-check interval ───────────────────────────────────────────────── async function setCheckInterval(secs: number) { intervalSaving = true; @@ -279,20 +301,28 @@ onMount(async () => { loadLastChecked(); appVersion = await invoke("get_app_version"); - const [autoEnabled, intervalSecs, savedChannel] = await Promise.all([ + const [autoEnabled, intervalSecs, savedChannel, autoUpdate] = await Promise.all([ invoke("get_autostart_enabled").catch(() => false), invoke("get_update_check_interval").catch(() => 3600), invoke("get_update_channel").catch(() => "stable"), + invoke("get_auto_update_enabled").catch(() => true), ]); autostartEnabled = autoEnabled; checkIntervalSecs = intervalSecs; updateChannelRc = savedChannel === "rc"; + autoUpdateEnabled = autoUpdate; unlisteners.push( // Background Rust task found an update — kick off download automatically. await listen<{ version: string; date?: string; body?: string }>("update-available", (ev) => { if (phase === "checking" || phase === "downloading" || phase === "ready") return; saveLastChecked(); + if (!autoUpdateEnabled) { + // Surface the notice; user installs manually via the button. + available = ev.payload; + phase = "idle"; + return; + } // Pass the event payload as a hint so checkAndDownload() keeps the // version visible in the UI while it fetches a fresh Update object, // and surfaces an error if check() returns null (CDN race) instead @@ -423,6 +453,23 @@ onDestroy(() => { {t("updates.downloadFailed")} + {:else if phase === "idle" && available && !autoUpdateEnabled} + +
+ ⬆ +
+
+ + v{available.version} {t("updates.available")} + + + {t("updates.autoUpdateOffNotice")} + +
+ {:else}
@@ -435,6 +482,7 @@ onDestroy(() => { {/if} + {#if !(phase === "idle" && available && !autoUpdateEnabled)} + {/if}
@@ -519,6 +568,52 @@ onDestroy(() => { + + + + + + {#if autoUpdateError} +
+ {autoUpdateError} +
+ {/if} +
+
+ From 57415e66ebc55eebd6583c5035a4b3f613b6fd3c Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Sun, 3 May 2026 00:28:17 -0400 Subject: [PATCH 28/30] =?UTF-8?q?1.=20Settings=20tab=20font-size=20lint=20?= =?UTF-8?q?rule=20(scripts/check-settings-font-sizes.js)=20-=20Detects=20b?= =?UTF-8?q?are=20Tailwind=20text=20sizes=20(text-xs,=20text-2xl,=20text-[1?= =?UTF-8?q?0px],=20=E2=80=A6)=20in=20src/lib/settings/*.svelte;=20only=20t?= =?UTF-8?q?ext-ui-{xs,sm,base,md,lg,xl}=20is=20allowed.=20-=20Snapshot-bas?= =?UTF-8?q?eline=20approach=20(scripts/check-settings-font-sizes.baseline.?= =?UTF-8?q?json)=20=E2=80=94=20records=20the=20existing=20287=20violations?= =?UTF-8?q?=20across=2029=20tabs=20and=20fails=20CI=20if=20any=20file's=20?= =?UTF-8?q?count=20grows=20or=20a=20clean=20file=20gains=20a=20violation.?= =?UTF-8?q?=20New=20files=20must=20start=20at=20zero.=20-=20Reports=20drop?= =?UTF-8?q?s=20too,=20and=20prints=20the=20--update=20command=20to=20refre?= =?UTF-8?q?sh=20the=20baseline=20after=20a=20real=20cleanup.=20-=20Verifie?= =?UTF-8?q?d:=20simulated=20regression=20in=20UpdatesTab.svelte=20(5=20?= =?UTF-8?q?=E2=86=92=207)=20was=20caught=20with=20exit=20code=201;=20rever?= =?UTF-8?q?ting=20brought=20it=20back=20to=20=E2=9C=85.=20-=20Wired=20into?= =?UTF-8?q?=20npm=20run=20check.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The baseline doubles as a punch list — top offenders are ActivityTab (95), ValidationTab (36), and TerminalSessionsCard (27). 2. Tray hint when auto-update is OFF and an update is detected - New AppState.update_available_pending: Option (state.rs). - background.rs poller writes the pending version + refreshes the tray when auto_update_enabled is false. - tray.rs::build_menu shows ⬆ Update available: v{version} near the top; the entry is dropped automatically when staged or when auto-update is re-enabled. - Click → opens the Updates settings tab (tray_setup.rs). - Cleared in set_update_ready(true) (after install) and in set_auto_update_enabled(true) (toggle back on); both refresh the tray. --- package.json | 3 +- .../check-settings-font-sizes.baseline.json | 31 +++++ scripts/check-settings-font-sizes.js | 110 ++++++++++++++++++ src-tauri/src/auto_update.rs | 14 +++ src-tauri/src/background.rs | 11 ++ src-tauri/src/state.rs | 7 ++ src-tauri/src/tray.rs | 30 ++++- src-tauri/src/tray_setup.rs | 2 +- src-tauri/src/window_cmds.rs | 6 + 9 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 scripts/check-settings-font-sizes.baseline.json create mode 100644 scripts/check-settings-font-sizes.js diff --git a/package.json b/package.json index 3c463152..78286a30 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,14 @@ "preview": "vite preview", "check:markdown-renderer": "node scripts/check-markdown-renderer.js", "check:daemon-invokes": "node scripts/check-daemon-invokes.js", + "check:settings-fonts": "node scripts/check-settings-font-sizes.js", "audit:daemon-routes": "node scripts/audit-daemon-routes.js", "verify:tauri:frontend": "node scripts/verify-tauri-frontend-structure.js", "check:i18n": "npx tsx scripts/audit-i18n.ts --check", "health": "node scripts/health.mjs", "check:i18n:locales": "node scripts/check-critical-i18n-locales.js", "check:i18n:critical": "npm run -s check:i18n:locales", - "check": "npm run -s check:markdown-renderer && npm run -s check:daemon-invokes && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check": "npm run -s check:markdown-renderer && npm run -s check:daemon-invokes && npm run -s check:settings-fonts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "npm run -s dev:guard && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --ignore src-tauri --watch", "tauri": "node scripts/tauri-build.js", "tauri:flamegraph": "node scripts/tauri-flamegraph.js", diff --git a/scripts/check-settings-font-sizes.baseline.json b/scripts/check-settings-font-sizes.baseline.json new file mode 100644 index 00000000..6cc0ccb5 --- /dev/null +++ b/scripts/check-settings-font-sizes.baseline.json @@ -0,0 +1,31 @@ +{ + "ActivityTab.svelte": 95, + "AppearanceTab.svelte": 3, + "CalibrationTab.svelte": 1, + "ClientsTab.svelte": 16, + "DevicesTab.svelte": 15, + "EegModelTab.svelte": 1, + "EmbeddingsTab.svelte": 0, + "ExgTab.svelte": 2, + "ExtensionsTab.svelte": 12, + "GoalsTab.svelte": 11, + "HooksTab.svelte": 18, + "LlmTab.svelte": 0, + "LslTab.svelte": 3, + "PermissionsTab.svelte": 7, + "PvtPanel.svelte": 6, + "ScreenshotsTab.svelte": 0, + "SettingsTab.svelte": 1, + "ShortcutsTab.svelte": 0, + "SleepTab.svelte": 3, + "TerminalSessionsCard.svelte": 27, + "TerminalTab.svelte": 9, + "TlxForm.svelte": 7, + "TokensTab.svelte": 4, + "ToolsTab.svelte": 0, + "TtsTab.svelte": 2, + "UmapTab.svelte": 0, + "UpdatesTab.svelte": 5, + "ValidationTab.svelte": 36, + "VirtualEegTab.svelte": 0 +} diff --git a/scripts/check-settings-font-sizes.js b/scripts/check-settings-font-sizes.js new file mode 100644 index 00000000..dad567bd --- /dev/null +++ b/scripts/check-settings-font-sizes.js @@ -0,0 +1,110 @@ +#!/usr/bin/env node +// +// check-settings-font-sizes.js — guard against font-size drift in +// src/lib/settings/*.svelte. +// +// Settings tabs should size text via the `text-ui-{xs,sm,base,md,lg,xl}` scale +// only. Bare Tailwind sizes (`text-xs`, `text-base`, `text-lg`, `text-2xl`, +// `text-[10px]`, …) cause visual inconsistency between tabs and were the +// motivation for introducing the `text-ui-*` system. +// +// We don't fix the existing 200+ pre-existing violations — that's a separate +// cleanup pass. Instead this script snapshots the current per-file violation +// counts in `check-settings-font-sizes.baseline.json` and fails if any file's +// count grows or a previously-clean file gains a violation. New files must +// start at zero. +// +// Refresh the baseline after an intentional cleanup with: +// node scripts/check-settings-font-sizes.js --update +// +// Allowed: text-ui-xs | text-ui-sm | text-ui-base | text-ui-md | text-ui-lg | text-ui-xl +// Violation: text-(xs|sm|base|lg|xl|xl|[]) + +import { readdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const SETTINGS_DIR = path.resolve("src/lib/settings"); +const BASELINE_PATH = path.resolve("scripts/check-settings-font-sizes.baseline.json"); + +// `(?!ui-)` skips `text-ui-*`. Trailing `\b` works for word-char endings; +// the alternation includes `\[…\]` for arbitrary values. +const VIOLATION_RE = /text-(?!ui-)((?:\d?xl|xs|sm|base|lg|xl)\b|\[[^\]]+\])/g; + +function listSettingsTabs() { + return readdirSync(SETTINGS_DIR) + .filter((f) => f.endsWith(".svelte")) + .sort(); +} + +function countViolations(filePath) { + const src = readFileSync(filePath, "utf8"); + return (src.match(VIOLATION_RE) ?? []).length; +} + +function currentCounts() { + const out = {}; + for (const file of listSettingsTabs()) { + out[file] = countViolations(path.join(SETTINGS_DIR, file)); + } + return out; +} + +function loadBaseline() { + try { + return JSON.parse(readFileSync(BASELINE_PATH, "utf8")); + } catch { + return null; + } +} + +const update = process.argv.includes("--update"); +const counts = currentCounts(); + +if (update) { + writeFileSync(BASELINE_PATH, `${JSON.stringify(counts, null, 2)}\n`); + console.log(`✅ baseline written: ${BASELINE_PATH}`); + process.exit(0); +} + +const baseline = loadBaseline(); +if (!baseline) { + writeFileSync(BASELINE_PATH, `${JSON.stringify(counts, null, 2)}\n`); + console.log(`📌 baseline initialised: ${BASELINE_PATH}`); + process.exit(0); +} + +const regressions = []; +for (const [file, count] of Object.entries(counts)) { + const prev = baseline[file]; + if (prev === undefined && count > 0) { + regressions.push(` ✗ ${file}: new file with ${count} non-ui- text size(s)`); + } else if (prev !== undefined && count > prev) { + regressions.push(` ✗ ${file}: ${prev} → ${count} non-ui- text size(s)`); + } +} + +if (regressions.length > 0) { + console.error("❌ settings tab font-size regressions:"); + console.error(regressions.join("\n")); + console.error( + "\nUse the `text-ui-{xs,sm,base,md,lg,xl}` scale instead of bare Tailwind sizes.\n" + + "If the change is intentional (e.g. removed an outlier), refresh the baseline:\n" + + " node scripts/check-settings-font-sizes.js --update", + ); + process.exit(1); +} + +// Surface drops too — they're not failures, but worth knowing. +const drops = []; +for (const [file, prev] of Object.entries(baseline)) { + const cur = counts[file]; + if (cur === undefined) continue; + if (cur < prev) drops.push(` ✓ ${file}: ${prev} → ${cur}`); +} +if (drops.length > 0) { + console.log("ℹ︎ settings tab font-size violations decreased — refresh baseline to lock in:"); + console.log(drops.join("\n")); + console.log(" node scripts/check-settings-font-sizes.js --update"); +} + +console.log("✅ no settings tab font-size regressions"); diff --git a/src-tauri/src/auto_update.rs b/src-tauri/src/auto_update.rs index b11b71a1..16ffe1b8 100644 --- a/src-tauri/src/auto_update.rs +++ b/src-tauri/src/auto_update.rs @@ -14,8 +14,12 @@ //! today's behavior. use std::path::PathBuf; +use std::sync::Mutex; use tauri::{AppHandle, Manager}; +use crate::state::AppState; +use crate::MutexExt; + const PREF_FILE: &str = "auto-update.txt"; fn pref_path(app: &AppHandle) -> Option { @@ -51,5 +55,15 @@ pub fn set_auto_update_enabled(app: AppHandle, enabled: bool) -> Result<(), Stri std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; } std::fs::write(&path, if enabled { "true" } else { "false" }).map_err(|e| e.to_string())?; + if enabled { + // Auto-update is back on — drop any "⬆ Update available" tray hint; + // the next background poll will trigger the usual auto-download. + let r = app.state::>>(); + let mut g = r.lock_or_recover(); + if g.update_available_pending.take().is_some() { + drop(g); + crate::tray::refresh_tray(&app); + } + } Ok(()) } diff --git a/src-tauri/src/background.rs b/src-tauri/src/background.rs index 924aba13..ed7d61ee 100644 --- a/src-tauri/src/background.rs +++ b/src-tauri/src/background.rs @@ -226,6 +226,17 @@ fn spawn_updater_poll(handle: &AppHandle) { Err(_) => eprintln!("[updater] check timed out after 30 s"), Ok(Ok(Some(update))) => { eprintln!("[updater] update available: {}", update.version); + // When auto-update is off, mirror the version into AppState so + // the tray menu can surface "⬆ Update available …". The + // frontend gets the same event either way and decides whether + // to auto-download. + if !crate::auto_update::read_auto_update_enabled(&app) { + let r = app.state::>>(); + let mut g = r.lock_or_recover(); + g.update_available_pending = Some(update.version.clone()); + drop(g); + crate::tray::refresh_tray(&app); + } let payload = serde_json::json!({ "version": update.version, "date": update.date, diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 9d61eaed..c11df45e 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -381,6 +381,12 @@ pub struct AppState { /// Set by the frontend when an update has been downloaded and is ready /// to install on next restart / relaunch. pub update_ready_to_install: bool, + /// Version string of an update detected by the background poller while + /// the auto-update toggle is OFF — surfaces in the tray menu so the + /// user notices a pending update without opening Settings. Cleared + /// when the user installs (`set_update_ready(true)`) or re-enables + /// auto-update. + pub update_available_pending: Option, // ── Device configs ──────────────────────────────────────────────────── pub openbci_config: crate::settings::OpenBciConfig, @@ -498,6 +504,7 @@ impl Default for AppState { hf_endpoint: skill_settings::default_hf_endpoint(), update_check_interval_secs: default_update_check_interval(), update_ready_to_install: false, + update_available_pending: None, openbci_config: crate::settings::OpenBciConfig::default(), location_enabled: false, inference_device: skill_settings::default_inference_device(), diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 6c067384..8164d115 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -143,6 +143,15 @@ fn structure_key(st: &DeviceStatus, app: &AppHandle) -> String { let llm_downloads = tray_download_fingerprint(app); + // Pending update version (only set when auto-update is OFF). Including it + // in the structure key forces a rebuild that adds/removes the tray hint + // when the background poller flips the state. + let pending_update = { + let r = app.app_state(); + let g = r.lock_or_recover(); + g.update_available_pending.clone().unwrap_or_default() + }; + let mut pair_parts = st .paired_devices .iter() @@ -156,7 +165,7 @@ fn structure_key(st: &DeviceStatus, app: &AppHandle) -> String { let state = st.state.as_str(); format!( - "{state}|{pairs}|{ls}|{ss}|{sets}|{cs}|{hs}|{hist}|{api}|{ts}|{ft}|{chat}|{compare}|{llm_downloads}" + "{state}|{pairs}|{ls}|{ss}|{sets}|{cs}|{hs}|{hist}|{api}|{ts}|{ft}|{chat}|{compare}|{llm_downloads}|{pending_update}" ) } @@ -274,6 +283,25 @@ pub(crate) fn build_menu(app: &AppHandle, st: &DeviceStatus) -> tauri::Result, + )?)?; + } + menu.append(&PredefinedMenuItem::separator(app)?)?; // ── Status info (always present — updated in-place by update_status_items) ── diff --git a/src-tauri/src/tray_setup.rs b/src-tauri/src/tray_setup.rs index 320ee935..060bde83 100644 --- a/src-tauri/src/tray_setup.rs +++ b/src-tauri/src/tray_setup.rs @@ -122,7 +122,7 @@ pub(crate) fn build_tray( }); } else if id == "show_logs" { crate::window_cmds::open_latest_log(); - } else if id == "check_update" { + } else if id == "check_update" || id == "update_available" { let a = app.clone(); tauri::async_runtime::spawn(async move { let _ = crate::window_cmds::open_updates_window(a).await; diff --git a/src-tauri/src/window_cmds.rs b/src-tauri/src/window_cmds.rs index bad14420..19e5732f 100644 --- a/src-tauri/src/window_cmds.rs +++ b/src-tauri/src/window_cmds.rs @@ -1255,6 +1255,12 @@ pub fn set_update_ready(app: AppHandle, ready: bool) { let r = app.state::>>(); let mut g = r.lock_or_recover(); g.update_ready_to_install = ready; + if ready { + // Once staged, the "⬆ Update available" tray hint is redundant. + g.update_available_pending = None; + drop(g); + crate::tray::refresh_tray(&app); + } } #[tauri::command] From 8c6f9848decc589f0dcda59c3bdf33b94b9da9f6 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Sun, 3 May 2026 00:30:34 -0400 Subject: [PATCH 29/30] translations --- src/lib/i18n/he/ui.ts | 6 ++---- src/lib/i18n/ko/ui.ts | 3 +-- src/lib/i18n/zh/ui.ts | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/lib/i18n/he/ui.ts b/src/lib/i18n/he/ui.ts index 8e9353f5..99ee85b3 100644 --- a/src/lib/i18n/he/ui.ts +++ b/src/lib/i18n/he/ui.ts @@ -221,10 +221,8 @@ const ui: Record = { "updates.autostart": "הפעלה בכניסה למערכת", "updates.autostartDesc": "מתחיל אוטומטית כשנכנסים למחשב.", "updates.autoUpdate": "התקן עדכונים אוטומטית", - "updates.autoUpdateDesc": - "הורד גרסאות חדשות ברקע והתקן בהפעלה הבאה. כבה כדי לבחור מתי להתקין.", - "updates.autoUpdateOffNotice": - "התקנה אוטומטית כבויה — לחץ על התקן כדי להוריד ולעדכן.", + "updates.autoUpdateDesc": "הורד גרסאות חדשות ברקע והתקן בהפעלה הבאה. כבה כדי לבחור מתי להתקין.", + "updates.autoUpdateOffNotice": "התקנה אוטומטית כבויה — לחץ על התקן כדי להוריד ולעדכן.", "updates.installNow": "התקן", "updates.autoCheckDesc": "בדוק עדכונים פעם ביום כאשר האפליקציה מתחילה.", "updates.footer": "עדכונים מורדים אוטומטית. הפעל מחדש כשנוח לך.", diff --git a/src/lib/i18n/ko/ui.ts b/src/lib/i18n/ko/ui.ts index bd8ebb88..f047be45 100644 --- a/src/lib/i18n/ko/ui.ts +++ b/src/lib/i18n/ko/ui.ts @@ -115,8 +115,7 @@ const ui: Record = { "updates.autoUpdate": "업데이트 자동 설치", "updates.autoUpdateDesc": "백그라운드에서 새 버전을 다운로드하고 다음 재시작 시 설치합니다. 설치 시점을 직접 선택하려면 끄세요.", - "updates.autoUpdateOffNotice": - "자동 설치가 꺼져 있습니다 — 다운로드 및 업데이트하려면 설치를 클릭하세요.", + "updates.autoUpdateOffNotice": "자동 설치가 꺼져 있습니다 — 다운로드 및 업데이트하려면 설치를 클릭하세요.", "updates.installNow": "설치", "updates.footer": "업데이트는 자동으로 다운로드됩니다. 적용 준비가 되면 재시작하세요.", diff --git a/src/lib/i18n/zh/ui.ts b/src/lib/i18n/zh/ui.ts index c936d354..6dc5bc93 100644 --- a/src/lib/i18n/zh/ui.ts +++ b/src/lib/i18n/zh/ui.ts @@ -115,8 +115,7 @@ const ui: Record = { "updates.autostart": "登录时启动", "updates.autostartDesc": "登录计算机时自动启动。", "updates.autoUpdate": "自动安装更新", - "updates.autoUpdateDesc": - "在后台下载新版本,并在下次重启时安装。关闭后可自行选择安装时机。", + "updates.autoUpdateDesc": "在后台下载新版本,并在下次重启时安装。关闭后可自行选择安装时机。", "updates.autoUpdateOffNotice": "自动安装已关闭 — 点击“安装”以下载并更新。", "updates.installNow": "安装", "updates.footer": "更新会自动下载。准备好后重启即可应用。", From 02078553ae51f879e5f40bbbd5e1bb468e165d38 Mon Sep 17 00:00:00 2001 From: Eugene Hauptmann Date: Sun, 3 May 2026 00:31:48 -0400 Subject: [PATCH 30/30] 0.0.130-rc.12 --- CHANGELOG.md | 10 ++++ Cargo.lock | 54 +++++++++---------- changes/releases/0.0.130-rc.12.md | 9 ++++ package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- .../generated/settings-search-index.de.json | 6 +++ .../generated/settings-search-index.en.json | 6 +++ .../generated/settings-search-index.es.json | 6 +++ .../generated/settings-search-index.fr.json | 6 +++ .../generated/settings-search-index.he.json | 6 +++ .../generated/settings-search-index.ja.json | 6 +++ .../generated/settings-search-index.ko.json | 6 +++ .../generated/settings-search-index.uk.json | 6 +++ .../generated/settings-search-index.zh.json | 6 +++ .../generated/settings-search-manifest.json | 2 +- 16 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 changes/releases/0.0.130-rc.12.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b079d9..bd6075e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5409,6 +5409,16 @@ The heatmap merges EEG data points with the closest timeline events to show whic - 1. GPU f16 SHADER_F16 panic → `catch_unwind` in worker.rs +## [0.0.130-rc.12] — 2026-05-03 + +### Features + +- translations +- 1. Settings tab font-size lint rule (scripts/check-settings-font-sizes.js) +- The baseline doubles as a punch list — top offenders are ActivityTab (95), ValidationTab (36), and TerminalSessionsCard (27). +- 2. Tray hint when auto-update is OFF and an update is detected +- auto-update + update RC settings + ## [0.0.130-rc.2] — 2026-04-29 ### Features diff --git a/Cargo.lock b/Cargo.lock index 25d82e37..79ba55b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,9 +292,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow-array" -version = "58.1.0" +version = "58.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772bd34cacdda8baec9418d80d23d0fb4d50ef0735685bd45158b83dfeb6e62d" +checksum = "841321891f247aa86c6112c80d83d89cb36e0addd020fa2425085b8eb6c3f579" dependencies = [ "ahash", "arrow-buffer", @@ -302,7 +302,7 @@ dependencies = [ "arrow-schema", "chrono", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "num-complex", "num-integer", "num-traits", @@ -310,9 +310,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "58.1.0" +version = "58.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898f4cf1e9598fdb77f356fdf2134feedfd0ee8d5a4e0a5f573e7d0aec16baa4" +checksum = "f955dfb73fae000425f49c8226d2044dab60fb7ad4af1e24f961756354d996c9" dependencies = [ "bytes", "half", @@ -322,9 +322,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "58.1.0" +version = "58.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d10beeab2b1c3bb0b53a00f7c944a178b622173a5c7bcabc3cb45d90238df4" +checksum = "db3b5846209775b6dc8056d77ff9a032b27043383dd5488abd0b663e265b9373" dependencies = [ "arrow-buffer", "arrow-schema", @@ -335,9 +335,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "58.1.0" +version = "58.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609a441080e338147a84e8e6904b6da482cefb957c5cdc0f3398872f69a315d0" +checksum = "fd8907ddd8f9fbabf91ec2c85c1d81fe2874e336d2443eb36373595e28b98dd5" dependencies = [ "arrow-array", "arrow-buffer", @@ -349,15 +349,15 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "58.1.0" +version = "58.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30a1365d7a7dc50cc847e54154e6af49e4c4b0fddc9f607b687f29212082743" +checksum = "18aa020f6bc8e5201dcd2d4b7f98c68f8a410ef37128263243e6ff2a47a67d4f" [[package]] name = "arrow-select" -version = "58.1.0" +version = "58.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78694888660a9e8ac949853db393af2a8b8fc82c19ce333132dfa2e72cc1a7fe" +checksum = "a657ab5132e9c8ca3b24eb15a823d0ced38017fe3930ff50167466b02e2d592c" dependencies = [ "ahash", "arrow-array", @@ -9419,9 +9419,9 @@ dependencies = [ [[package]] name = "parquet" -version = "58.1.0" +version = "58.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3f9f2205199603564127932b89695f52b62322f541d0fc7179d57c2e1c9877" +checksum = "43d7efd3052f7d6ef601085559a246bc991e9a8cc77e02753737df6322ce35f1" dependencies = [ "ahash", "arrow-array", @@ -9434,7 +9434,7 @@ dependencies = [ "bytes", "chrono", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "num-bigint", "num-integer", "num-traits", @@ -11726,9 +11726,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" dependencies = [ "base64 0.22.1", "chrono", @@ -11745,9 +11745,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -11899,9 +11899,9 @@ dependencies = [ [[package]] name = "signature" -version = "3.0.0-rc.10" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" [[package]] name = "simd-adler32" @@ -11951,7 +11951,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skill" -version = "0.0.130-rc.11" +version = "0.0.130-rc.12" dependencies = [ "anyhow", "base64 0.22.1", @@ -13476,9 +13476,9 @@ dependencies = [ [[package]] name = "tauri-plugin-opener" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" dependencies = [ "dunce", "glob", @@ -13508,9 +13508,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33a5b7d78f0dec4406b003ea87c40bf928d801b6fd9323a556172c91d8712c1" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" dependencies = [ "serde", "serde_json", diff --git a/changes/releases/0.0.130-rc.12.md b/changes/releases/0.0.130-rc.12.md new file mode 100644 index 00000000..a251d880 --- /dev/null +++ b/changes/releases/0.0.130-rc.12.md @@ -0,0 +1,9 @@ +## [0.0.130-rc.12] — 2026-05-03 + +### Features + +- translations +- 1. Settings tab font-size lint rule (scripts/check-settings-font-sizes.js) +- The baseline doubles as a punch list — top offenders are ActivityTab (95), ValidationTab (36), and TerminalSessionsCard (27). +- 2. Tray hint when auto-update is OFF and an update is detected +- auto-update + update RC settings diff --git a/package.json b/package.json index 78286a30..3857723b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neuroskill", - "version": "0.0.130-rc.11", + "version": "0.0.130-rc.12", "description": "", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a8ad5b15..a021a1d3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skill" -version = "0.0.130-rc.11" +version = "0.0.130-rc.12" description = "A Tauri App" authors = ["NeuroSkill authors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7fbb7664..898d430b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NeuroSkill", - "version": "0.0.130-rc.11", + "version": "0.0.130-rc.12", "identifier": "com.neuroskill.skill", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/lib/generated/settings-search-index.de.json b/src/lib/generated/settings-search-index.de.json index d8d37c27..506a0060 100644 --- a/src/lib/generated/settings-search-index.de.json +++ b/src/lib/generated/settings-search-index.de.json @@ -833,6 +833,12 @@ "label": "Bei Anmeldung starten", "desc": "Startet automatisch, wenn du dich an deinem Computer anmeldest." }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "Updates automatisch installieren", + "desc": "Neue Versionen im Hintergrund herunterladen und beim nächsten Neustart installieren. Deaktivieren, um den Zeitpunkt selbst zu wählen." + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.en.json b/src/lib/generated/settings-search-index.en.json index 631bd1da..a7e33d1a 100644 --- a/src/lib/generated/settings-search-index.en.json +++ b/src/lib/generated/settings-search-index.en.json @@ -833,6 +833,12 @@ "label": "Launch at Login", "desc": "Start automatically when you log in to your computer." }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "Install updates automatically", + "desc": "Download new versions in the background and install them on the next restart. Turn off to choose when to install." + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.es.json b/src/lib/generated/settings-search-index.es.json index 74948814..b65de8aa 100644 --- a/src/lib/generated/settings-search-index.es.json +++ b/src/lib/generated/settings-search-index.es.json @@ -833,6 +833,12 @@ "label": "Iniciar sesión", "desc": "Se inicia automáticamente cuando inicia sesión en su computadora." }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "Instalar actualizaciones automáticamente", + "desc": "Descarga las nuevas versiones en segundo plano e instálalas al reiniciar. Desactiva esta opción para elegir cuándo instalar." + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.fr.json b/src/lib/generated/settings-search-index.fr.json index 8c90297f..2adeae81 100644 --- a/src/lib/generated/settings-search-index.fr.json +++ b/src/lib/generated/settings-search-index.fr.json @@ -833,6 +833,12 @@ "label": "Lancer à la connexion", "desc": "Démarre automatiquement quand vous ouvrez une session." }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "Installer les mises à jour automatiquement", + "desc": "Télécharger les nouvelles versions en arrière-plan et les installer au prochain redémarrage. Désactivez pour choisir quand installer." + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.he.json b/src/lib/generated/settings-search-index.he.json index d0eb849b..9f2d290b 100644 --- a/src/lib/generated/settings-search-index.he.json +++ b/src/lib/generated/settings-search-index.he.json @@ -833,6 +833,12 @@ "label": "הפעלה בכניסה למערכת", "desc": "מתחיל אוטומטית כשנכנסים למחשב." }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "התקן עדכונים אוטומטית", + "desc": "הורד גרסאות חדשות ברקע והתקן בהפעלה הבאה. כבה כדי לבחור מתי להתקין." + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.ja.json b/src/lib/generated/settings-search-index.ja.json index f9d39625..53d08d74 100644 --- a/src/lib/generated/settings-search-index.ja.json +++ b/src/lib/generated/settings-search-index.ja.json @@ -833,6 +833,12 @@ "label": "ログイン時に起動", "desc": "コンピューターにログインしたときに自動的に起動します。" }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "アップデートを自動的にインストール", + "desc": "新しいバージョンをバックグラウンドでダウンロードし、次回の再起動時にインストールします。タイミングを自分で選ぶには無効にしてください。" + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.ko.json b/src/lib/generated/settings-search-index.ko.json index 84794b7d..670108dc 100644 --- a/src/lib/generated/settings-search-index.ko.json +++ b/src/lib/generated/settings-search-index.ko.json @@ -833,6 +833,12 @@ "label": "로그인 시 시작", "desc": "컴퓨터에 로그인할 때 자동으로 시작합니다." }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "업데이트 자동 설치", + "desc": "백그라운드에서 새 버전을 다운로드하고 다음 재시작 시 설치합니다. 설치 시점을 직접 선택하려면 끄세요." + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.uk.json b/src/lib/generated/settings-search-index.uk.json index 2877b856..b870f649 100644 --- a/src/lib/generated/settings-search-index.uk.json +++ b/src/lib/generated/settings-search-index.uk.json @@ -833,6 +833,12 @@ "label": "Запуск під час входу", "desc": "Запускається автоматично при вході в систему." }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "Встановлювати оновлення автоматично", + "desc": "Завантажувати нові версії у фоні та встановлювати під час наступного перезапуску. Вимкніть, щоб обирати момент встановлення вручну." + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-index.zh.json b/src/lib/generated/settings-search-index.zh.json index e1a41fb8..abd4b6e9 100644 --- a/src/lib/generated/settings-search-index.zh.json +++ b/src/lib/generated/settings-search-index.zh.json @@ -833,6 +833,12 @@ "label": "登录时启动", "desc": "登录计算机时自动启动。" }, + { + "tab": "updates", + "key": "updates.autoUpdate", + "label": "自动安装更新", + "desc": "在后台下载新版本,并在下次重启时安装。关闭后可自行选择安装时机。" + }, { "tab": "updates", "key": "updates.checkInterval", diff --git a/src/lib/generated/settings-search-manifest.json b/src/lib/generated/settings-search-manifest.json index 9404325c..2551ac7e 100644 --- a/src/lib/generated/settings-search-manifest.json +++ b/src/lib/generated/settings-search-manifest.json @@ -10,5 +10,5 @@ "uk", "zh" ], - "entriesPerLocale": 142 + "entriesPerLocale": 143 } \ No newline at end of file