Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ src-tauri/target/
*.ilk
*.exp
*.lib
.gstack/

# Test artifacts (the test scripts themselves are tracked)
scripts/ui-screenshots/
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [v0.39.1](https://github.com/mandarwagh9/embedist/releases/tag/v0.39.1) — 2026-05-28

### Fixed
- **Focus ring no longer renders as 1px UA default.** Headless UI testing surfaced that the v0.39.0 `:focus-visible` rule used `outline: 2px solid var(--accent)` but Chromium has a long-standing quirk where the UA `outline-style: auto` 1px focus ring can't be fully replaced by an author `outline-style: solid` — the rendered width stays at 1px regardless. Switched the focus ring to `box-shadow` (the modern recommendation), which has no such issue. Verified: every focused interactive element now shows a clean `0 0 0 2px var(--accent)` shadow.
- **SetupWizard now follows the theme.** The wizard modal had a hardcoded Catppuccin palette (`#1e1e2e`, `#cdd6f4`, `#89b4fa`, etc), so flipping to light theme left the first-launch modal stuck on dark surfaces. Rewrote to use `var(--bg-*)`, `var(--text-*)`, `var(--accent)` everywhere. Verified in both themes.
- **Status toasts now follow the theme.** Toast success/error/warning/info backgrounds used hardcoded RGBA literals; replaced with `color-mix(in srgb, var(--success) 15%, transparent)` (and equivalents) so they tint correctly when `--success`/`--error`/etc change with theme.

### Added
- **Headless UI smoke test.** `scripts/test-ui-headless.py` boots the Vite dev server in a Playwright Chromium with a Tauri shim, walks through the default view, focus ring, light theme, Serial sidebar, and Plot view toggle. Captures screenshots under `scripts/ui-screenshots/` (gitignored). Useful for catching CSS regressions before release.
- **Pure-logic smoke test.** `scripts/test-pure-logic.mjs` compiles `serial-parsing.ts` and `ai-pricing.ts` via esbuild and runs 30 assertions covering CSV/JSON/key-value parsing, pricing math, and formatter edge cases.

### Documentation
- `docs/superpowers/specs/2026-05-28-theme-token-sweep.md` — the rest of the audit: 17 component files still have hardcoded hex values. Split into Category A (replace with tokens — 9 files), Category B (intentional palettes — leave), Category C (TSX inline styles).

---

## [v0.39.0](https://github.com/mandarwagh9/embedist/releases/tag/v0.39.0) — 2026-05-28

### Added
Expand Down
58 changes: 58 additions & 0 deletions docs/superpowers/specs/2026-05-28-theme-token-sweep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Spec: Theme Token Sweep

**Status:** proposed
**Owners:** TBD
**Tier:** 3
**Sibling:** the v0.39.0 design-system commit landed the token *system* (focus ring, light theme, shadows). This spec is about the *consumers* still bypassing it.

## Why

A headless UI test against `localhost:1420` after the v0.39.0 token rebuild revealed that flipping `data-theme="light"` only restyled the outer chrome (titlebar, sidebar, statusbar, body). The SetupWizard modal stayed dark because it was hardcoded to a Catppuccin palette. After fixing SetupWizard.css and Toast.css inline as part of the post-v0.39.0 hotfix, the audit shows **17 more files** with hex literals that should be tokens but aren't.

## Scope

`grep -rn -E "#[0-9a-fA-F]{3,6}\b" src/components/` finds 147 hits across 19 files. Fixed in this session: SetupWizard.css, Toast.css. The remaining 17 break into three categories.

### Category A — bug, fix to tokens

These use a hardcoded color where a status token exists. Pure mechanical replace.

| File | Hits | Replace with |
|---|---|---|
| `AgentActivityPanel.css:70` | error red `#e74c3c` | `var(--error)` |
| `AgentToolbar.css:69` | error red `#e74c3c` | `var(--error)` |
| `FeedbackPanel.css:35` | success green `#22c55e` | `var(--success)` |
| `FeedbackPanel.css:41,81` | error red `#ef4444` | `var(--error)` |
| `ContextMenu.css` | check on file | `var(--bg-surface)` etc |
| `FileExplorer.css` | check on file | mix of bg + status |
| `CodeEditor.css` (wrapper) | check on file | `var(--bg-primary)` |
| `TerminalPanel.css` | check on file | `var(--bg-primary)` |
| `ModeToggle.css:51` | agent purple `#A78BFA` | introduce `var(--mode-agent)` token |

### Category B — intentional, leave alone

These are domain-specific palettes that should NOT follow the app theme.

- **`MarkdownRenderer.css`** — One Dark Pro syntax highlighting (`#C678DD`, `#98C379`, etc). Code blocks should remain dark-themed even in a light app theme; that's the convention every code IDE follows (VS Code, Cursor, Zed). Leave as-is.
- **`AgentActivityPanel.css:40-43`** — per-activity-type icon colors (shell `#9b59b6`, search `#3498db`, error `#e74c3c`). These are semantic icons, not theme surfaces. Could promote to `--icon-shell`, `--icon-search` tokens but they don't need to flip with theme.

### Category C — TSX inline style colors

Inline `style={{ color: '#...' }}` in TSX files (e.g. `AgentActivityPanel.tsx`, `PlanPhaseIndicator.tsx`, `ToolCallBlock.tsx`). Need case-by-case judgment. Roughly half are category A, half are category B per pattern above.

## Verification

After the sweep:

1. Run the existing headless UI test: every panel screenshot should render with appropriate contrast in both `data-theme="dark"` and `data-theme="light"`.
2. The Monaco editor stays dark regardless of theme (its theme is independent — `settingsStore.editor.theme`).
3. `MarkdownRenderer` code-block syntax highlighting stays the same dark palette in both themes.

## Risks

- Color-mix isn't supported in older Chromium. Embedist's bundled Tauri WebView is recent enough (Chromium 120+), but if cross-platform support ever lands the macOS WebKit baseline (Safari 16.4) covers it; older WebKit fallback would need explicit rgba values.
- The agent-purple-as-mode-color (`#A78BFA`) appears in several places. Adding `--mode-agent` and similar tokens makes mode color theming a coherent system — worth doing once across all mode-tagged surfaces, not piecemeal.

## Estimate

~2 hours: one pass through 9 component CSS files in Category A + a smaller TSX sweep + a screenshot diff against this branch's baseline.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "embedist",
"version": "0.39.0",
"version": "0.39.1",
"description": "AI-native embedded development environment",
"type": "module",
"scripts": {
Expand Down
80 changes: 80 additions & 0 deletions scripts/test-pure-logic.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Quick sanity tests for the new pure-logic modules from v0.39.0.
// Not part of the production build — run with `node scripts/test-pure-logic.mjs`.
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve, dirname } from 'node:path';
import { pathToFileURL, fileURLToPath } from 'node:url';
import { transformSync } from 'esbuild';

const here = dirname(fileURLToPath(import.meta.url));
const root = resolve(here, '..');

function loadTs(rel) {
const src = readFileSync(resolve(root, rel), 'utf8');
return transformSync(src, { loader: 'ts', format: 'esm', target: 'esnext' }).code;
}

const tmp = mkdtempSync(join(tmpdir(), 'embedist-test-'));
const parserPath = join(tmp, 'parser.mjs');
const pricingPath = join(tmp, 'pricing.mjs');
writeFileSync(parserPath, loadTs('src/lib/serial-parsing.ts'));
writeFileSync(pricingPath, loadTs('src/lib/ai-pricing.ts'));

const { parseTelemetryLine } = await import(pathToFileURL(parserPath).href);
const { estimateCostUSD, formatUSD, formatTokens } = await import(pathToFileURL(pricingPath).href);

let pass = 0, fail = 0;
function eq(label, actual, expected) {
const ok = JSON.stringify(actual) === JSON.stringify(expected);
if (ok) { pass++; console.log(` PASS ${label}`); }
else { fail++; console.log(` FAIL ${label}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); }
}

console.log('parseTelemetryLine — CSV shape:');
eq('basic CSV', parseTelemetryLine('1.23,4.56,7.89'), { values: { ch0: 1.23, ch1: 4.56, ch2: 7.89 } });
eq('integer CSV', parseTelemetryLine('10,20,30'), { values: { ch0: 10, ch1: 20, ch2: 30 } });
eq('negative CSV', parseTelemetryLine('-1,-2.5,3'), { values: { ch0: -1, ch1: -2.5, ch2: 3 } });
eq('TSV (tab separator)', parseTelemetryLine('1\t2\t3'), { values: { ch0: 1, ch1: 2, ch2: 3 } });
eq('space separator', parseTelemetryLine('1 2 3'), { values: { ch0: 1, ch1: 2, ch2: 3 } });
eq('single number', parseTelemetryLine('42'), { values: { ch0: 42 } });

console.log('\nparseTelemetryLine — key:value shape:');
eq('colon style', parseTelemetryLine('temp:23.5 humidity:60'), { values: { temp: 23.5, humidity: 60 } });
eq('equals style', parseTelemetryLine('temp=23.5 humidity=60'), { values: { temp: 23.5, humidity: 60 } });
eq('mixed separators', parseTelemetryLine('x:1, y=2.5'), { values: { x: 1, y: 2.5 } });
eq('embedded in text', parseTelemetryLine('INFO temp:42 humidity:55 packets received'), { values: { temp: 42, humidity: 55 } });

console.log('\nparseTelemetryLine — JSON shape:');
eq('basic JSON', parseTelemetryLine('{"x":1,"y":2}'), { values: { x: 1, y: 2 } });
eq('float JSON', parseTelemetryLine('{"temp":23.5}'), { values: { temp: 23.5 } });
eq('mixed JSON drops non-numeric', parseTelemetryLine('{"x":1,"name":"sensor","y":2}'), { values: { x: 1, y: 2 } });

console.log('\nparseTelemetryLine — rejection cases:');
eq('plain log line', parseTelemetryLine('hello world'), null);
eq('empty string', parseTelemetryLine(''), null);
eq('whitespace only', parseTelemetryLine(' '), null);
eq('non-numeric label', parseTelemetryLine('INFO: starting up'), null);

console.log('\nestimateCostUSD — pricing math:');
// gpt-4o: $2.50 in / $10.00 out per Mtok. 1000 in + 500 out = 0.0025 + 0.005 = 0.0075
eq('gpt-4o', estimateCostUSD('gpt-4o', 1000, 500), 0.0075);
// claude-3-5-sonnet: $3 / $15. 10k in + 5k out = 0.03 + 0.075 = 0.105
eq('claude-3-5-sonnet', estimateCostUSD('claude-3-5-sonnet-20241014', 10000, 5000), 0.105);
eq('ollama is free', estimateCostUSD('llama3.2', 1_000_000, 1_000_000), 0);
eq('unknown returns null',estimateCostUSD('mystery-model-99', 1000, 500), null);
eq('null model', estimateCostUSD(null, 1000, 500), null);

console.log('\nformatUSD — display rounding:');
eq('zero', formatUSD(0), '$0');
eq('tiny', formatUSD(0.00005), '<$0.0001');
eq('sub-cent', formatUSD(0.0035), '$0.0035');
eq('sub-dollar', formatUSD(0.123), '$0.123');
eq('dollars', formatUSD(12.5), '$12.50');

console.log('\nformatTokens — compact display:');
eq('small', formatTokens(42), '42');
eq('thousands', formatTokens(1234), '1.2K');
eq('millions', formatTokens(2_500_000), '2.50M');

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail > 0 ? 1 : 0);
Loading
Loading