Feat/global launcher convert - #10
Merged
Merged
Conversation
…dupe Five pure text→text tools added to the TEXT_TOOLS registry (utils/devTools.js), so each is a validate/format pair that plugs into the existing tool dialog with no new component: - JWT Decode — header + payload as one JSON object (signature never verified) - Epoch / Date — Unix s/ms ↔ ISO 8601 UTC - URL Encode / Decode — toggles on percent-encoding, survives malformed input - HTML Entities Encode / Decode — named, decimal and hex entities - Sort & Dedupe Lines Wired into MENU_ACTIONS + both menus (app + MenuBar); they also appear in the sidebar Tools list automatically (derived from TEXT_TOOLS keys). Hash/checksum and a regex tester are intentionally out of scope here — they need async / a second input, which the single-input sync dialog can't host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds applyUnifiedDiff(base, patch) — a pure, exact-match unified-diff applier that rejects (never silently misapplies) hunks whose context doesn't match, and returns the rejected indices. Property-tested by round-tripping every patch toUnifiedDiff produces back to the changed side (add/delete/replace, EOF-newline and multi-hunk cases). UX is option A: "Apply Patch…" picks a base file and a .patch, then opens base ↔ patched in the normal diff view so the change is shown, not just written. Wired as diffStore.applyPatch() + the 'apply-patch' action, in the Edit menu (next to Copy Diff as Patch), the MenuBar, and a pinned sidebar Tools button. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fuzzy palette over every menu command, built by flattening the SAME
buildMenus() the menu bar uses — no duplicated action list. Submenu labels fold
into their leaves ("JSON · Format / Validate") so each command reads on its own;
matching reuses rank() and the keyboard driver is useQuickLookKeys with its
list-only defaults, so all event logic stays in tested composables. Backdrop
dismiss goes through useBackdropClose.
flattenCommands is pure and unit-tested; the overlay styles through design
tokens only. Opens from View → Command Palette… and the MenuBar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
File → Export Diff as HTML… writes the current comparison (files or paste mode) as a single self-contained document — inline CSS, no external assets, so it opens anywhere and can't phone home — for pasting into a ticket or PR. diffToHtml is pure and unit-tested: it reuses the new diffLines() (factored out of toUnifiedDiff so both share one aligned line list) and escapes every piece of user text and both names, so content can never inject markup. Main only writes the document the renderer built (diff:exportHtml → save dialog). PDF/image can follow later by printing this same HTML. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
File → Import Snippets… reads a chosen file and adds snippets to the encrypted library. A VS Code .code-snippets JSON becomes one snippet per entry; any other file is imported verbatim as a single snippet named after it. parseSnippetImport is a pure, size-capped, shape-validated untrusted-input surface (CLAUDE.md #6) — it only produces drafts; the snippet store re-encrypts each through the existing add()/vault path before anything is stored, so an import can never write undecryptable or unvalidated data. The diff store orchestrates and reports the count via the shared notice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold a keyboard-navigable convert section into Quick Look: summon the launcher (its existing rebindable global shortcut — no new binding, so no VS Code collision), type "base64", and convert a string inline — encode/decode, JWT, URL, HTML entities, epoch, JSON, UUID — without ever raising the app. The transforms are the same pure helpers the in-app tools use (utils/quickLookCommands: base64 + TEXT_TOOLS), so the two never diverge and the work stays offline. Commands rank next to snippets/diffs (kind:'command'). Navigation mirrors the snippet flow so it reads the same on every row: - → (or Enter) on a tool opens its convert panel; the input auto-focuses. - ← at the start of the input, or Escape, backs out to the list — and focus returns to the search box so arrow-key navigation resumes. - Tab cycles input ↔ output (read-only, selectable); the focused field shows the same accent edge the snippet preview uses. The preview panel is now kind-aware so a command selection never reads tags/labels meant for snippets. Pure runConvert + the new onExpand key path are unit-tested; the full summon→→→convert→copy→back flow (auto-focus, ←/Tab/Escape, nav resume) is covered by e2e/quick-look-convert.spec.mjs (DOM-driven — the transparent hide-on-blur window can't be screenshotted reliably). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the Epoch tool from a single text box into a purpose-built panel, hosted in BOTH the Tools dialog and the Quick Look launcher: - utils/epoch.js — pure nowEpoch / epochRows / pickToEpoch / relativeTime (Vue-free, unit-tested) - SegmentedControl.vue — reusable one-of-N kit primitive (v-model) - ToolEpoch.vue — live "Now" readout, seconds/millis + UTC/Local toggles, date-picker → epoch and epoch → ISO/Relative/Local rows with per-row copy - TEXT_TOOLS gains a `panel` kind; TextToolDialog renders the panel instead of Monaco, the launcher's convert panel renders it compact - clock + calendar icons Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drop stale references: DEVELOPMENT_PLAN.md (absent) and the README "mermaid diagram" (the README carries no mermaid, only Mermaid as an app feature). - Add a rule to sweep in-progress reasoning comments once a feature settles, before committing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The convert tools now sit under a single "Tools" row below the snippets seam — Enter or → expands a nicely-formatted, indented tool list; Enter/←/Esc collapses it — so browsing snippets stays compact. A tool-matching search auto-expands and lands on the first match, and keyboard nav stays one flat index-based list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tform macOS defaulted to Shift+Space, which misfires constantly — Shift is held while typing capitals, so a stray Space popped the launcher. Unify to the two-modifier Cmd/Ctrl+Shift+Space (already the Windows/Linux default), which can't fire by accident and is Spotlight-adjacent. Still rebindable in Settings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking outside the native date picker to dismiss it also closed the whole Epoch dialog. A panel tool now keeps close-on-backdrop off, so an outside click dismisses only the picker; Escape and the Close button still close the dialog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the bottom
The line-copy feature stepped the active line with scrollIntoView('nearest'),
which under-scrolls near the end — smooth-scroll lag makes 'nearest' read a
mid-animation position as already visible, so ↓ stalls once the max height is
reached (mouse wheel still worked). Scroll the container by the exact overflow
via scrollTop instead (instant, scroll-behavior removed), so it never stalls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ert, verify - utils/uuid.js: pure generators — v1/v6 (time), v4 (random), v7 (time-ordered), v5 (deterministic SHA-1); formatUuid (canonical / 32-hex / urn / braced + case); uuidInfo (version/variant); toCanonicalUuid (any form → canonical, both-way convert). v5 hashes with Web Crypto — a public digest, no key material — so it runs inline in the launcher too. Tests include the documented DNS/python.org v5 vector. - ToolUuid.vue: version selector, v5 namespace presets (DNS/URL/OID/X.500) + custom + name, a v5 verify/match field (a v5 hash can't be reversed to a name, so a candidate is checked instead), case/form controls, and a live version badge; hosted in the Tools dialog and the Quick Look launcher. - Flatten the Tools → UUID menu to a direct item (its "Convert" leaf was stale). - hash icon; e2e for generate, reverse-convert, and verify. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- utils/url.js: pure encodeUrl/decodeUrl (component or full-URI), parseQuery (decoded key/value pairs, fragment kept out), buildQuery (re-encode, drop empty rows). - ToolUrl.vue: explicit Encode/Decode + Component/Full-URI toggles, and an editable query-param table that rebuilds the URL live (add/remove rows, per- output copy); hosted in the Tools dialog and the Quick Look launcher. - headIcon map (adds the link icon) for the launcher panel head. - Tests: url util (7) + e2e (encode/decode · component/full, live param edit, clipboard). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- utils/jwt.js: pure parseJwt (base64url header/payload) + jwtClaims (alg, humanized iat/nbf/exp with relative + absolute time, expired/not-yet-valid flags), reusing relativeTime from utils/epoch. Decode only — the signature is never verified. - ToolJwt.vue: alg badge + expired/not-expired pill, a time-claim summary, pretty header/payload JSON with copy, and a "signature isn't verified" note; hosted in the Tools dialog and the Quick Look launcher (shield-check icon). - Tests: jwt util (4, incl. the jwt.io example) + e2e (decode + copy, malformed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…yntax tree Replaces the plain JSON format/validate dialog with a panel tool: - Pretty / Minify / Sort-keys via SegmentedControl (deep key sort). - JSONPath-subset filter (child, index, wildcard, recursive descent, bracket notation) with a live match count; hand-rolled, no deps. - Syntax-colored, collapsible JsonTree (recursive, self-imported) using the semantic status tokens so highlighting stays legible in all 14 themes; deep nodes auto-collapse. - Wired into textTools/TextToolDialog and the Quick Look-up launcher (braces headIcon), plus the flattened Tools → JSON menu (both surfaces), Cmd/Ctrl+Shift+J. Pure shaping + path logic in utils/json.js with thorough tests over messy real-world payloads (JSON-in-string, XML/HTML strings, unicode, big ints) that never re-parse string values. E2E covers format/filter/minify/copy and invalid-input reporting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the plain "Sort & Dedupe Lines" dialog (now just "Lines") with a panel tool: - Clean up: trim, drop blanks, dedupe (keep first). - Sort: Original / A→Z / Z→A / Natural (numeric-aware). - List builder: literal (non-regex) per-line find/replace, wrap each line with a prefix/suffix, and join with a separator + optional one-per-line — e.g. a paste of UUIDs → a SQL IN-clause list. - Live in→out·dupes count and one-click copy (via the main process). Wired into textTools/TextToolDialog and the Quick Look-up launcher (list headIcon), plus the renamed Tools → Lines menu (both surfaces). Pure pipeline in utils/lines.js with thorough tests (ordering, natural sort, literal find/replace, the SQL-list headline case) that supersede the old sortDedupeLines helper. E2E covers the SQL-list build + copy and natural sort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Split input by" field to the Lines panel: besides newlines, the input is also split on a literal delimiter, so a single delimited line (e.g. `aaad, adad, ddd, 444`) explodes into separate lines before the pipeline — pair it with wrap + join to turn a CSV row into a quoted list. Blank leaves it splitting on lines as before. The existing per-line find/replace is unchanged. Util tests + e2e cover the explode-to-list case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unts Replaces the standalone Base64 dialog with a panel tool, consistent with the other rich tools: - Explicit Encode/Decode segmented toggle with live output. - URL-safe alphabet (-_ , padding stripped) and optional 76-column MIME wrap on encode; decode is tolerant (accepts standard or URL-safe input, strips whitespace, re-pads). - Live byte + character counts and one-click copy (via the main process). Now a TEXT_TOOLS `panel` entry rendered in both the Tools dialog and the Quick Look launcher, replacing the launcher's two Base64 Encode/Decode text-converts with one panel. Retires Base64Dialog.vue + showBase64Dialog; the sidebar chip now derives from TEXT_TOOLS. Flattened Tools → Base64 menu (both surfaces), Cmd/Ctrl+Shift+B. utils/base64 gains encode options, a tolerant decode, and byteLength, with tests (URL-safe, wrap, whitespace-tolerant decode, byte counts). The launcher's generic convert-flow e2e now rides HTML Entities (the remaining text-convert); a new base64 e2e covers encode/decode/URL-safe/copy/error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
npm 11 gates install scripts behind an `allowScripts` allow-list; without one, every install warns about six unreviewed packages. All six are build toolchain that must run their script to work — electron and esbuild fetch their platform binaries, fsevents is the macOS watcher, electron-winstaller picks the 7z arch, vue-demi shims the Vue major — and none ship in the app, so they are approved by `npm install-scripts approve`. package.json only: the lockfile is untouched (a lock rewrite goes through `make install` with the container's npm, per CLAUDE.md). Documents the gate in CLAUDE.md so a new dependency's install script gets read before it is approved, rather than the gate being disabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sidebar shelf and the launcher's Tools list both showed twelve identical entries — a wall of uppercase pills, and rows that all wore the same wrench under a blanket "CONVERT" that was wrong for most of them (JWT decodes, UUID generates, JSON formats). Both now read from one registry. - utils/tools.js: the single registry — each tool declares its icon, a proper-case name, an accurate one-word kind, and the menu action that opens it, so the surfaces cannot drift apart. Includes Encrypt, which the old shelf inconsistently omitted. - Sidebar shelf (ToolsShelf.vue): up to five recent tools as icon chips plus one "All tools" control — two rows instead of four, with the tools you actually use one click away. - The full list is the command palette in a tools scope (no fourth list to maintain): Recent above All tools, each row with its own icon and kind. Section labels ride as a pseudo-element on the opening row so keyboard indices stay 1:1 with results. - Launcher rows take their icon and action word from the same registry. - Recents persist in settings (max 5, most-recent-first) and are recorded in handleMenuAction — one choke point, so the menu, a shortcut, the shelf and the palette all count. - icons.js gains the three icons the registry needed: binary, tag, replace. Tests: utils/tools.js unit-tested including a cross-check that every declared icon exists in the icon map; settingsStore covers dedupe/cap/junk input; e2e covers the empty shelf, opening and searching the palette, recents appearing in both surfaces, and a chip opening its tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four of the six install-script approvals were version-pinned; `electron` and `vue-demi` fell back to "approved by name (all versions)", which would let a future version inherit the approval without review. npm can only pin what it can locate, and both packages were missing a `resolved` URL — and an `integrity` hash — in the lockfile. Dropping the two entries and re-resolving them through the container's npm (`make install`'s path, so the lock stays Linux-complete) adds both fields: four lines, no version drift, and two packages that now carry integrity hashes they previously lacked. The approval itself reads npm's HIDDEN lockfile (node_modules/.package-lock.json), not the committed one, so that had to be refreshed too before `npm install-scripts approve` would pin. All six entries are now exact; noted in CLAUDE.md so the next person doesn't repeat the detour. Verified with `npm ci` inside the container. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`npm install` on Node 20 warns EBADENGINE three times: for the app itself and for @electron/rebuild / node-abi, all of which require >=22.12. Docker (node:22-bookworm-slim) and CI (node-version: 22) already run 22 — only a local machine could drift, and nothing made the requirement discoverable outside the `engines` field. Adds .nvmrc so `nvm use` picks the right major, states it in the README build steps, and notes in CLAUDE.md that all four places (.nvmrc, engines, Dockerfile, CI) move together. Widening `engines` is explicitly ruled out: the toolchain requirement is real, not cosmetic. Also records that the three deprecation warnings (inflight, rimraf@2, glob@7) are transitive dev-only dependencies of electron-builder — already at its latest, zero audit findings, nothing we import — so they are upstream's to fix and must not be papered over with `overrides` inside a build tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes both tools end to end — TEXT_TOOLS entries, registry rows, menu items (including SQL's Cmd/Ctrl+Shift+Q), store actions, the shortcut listing, and their tests. The HTML-entity helpers go with them; SQL keeps utils/sqlFormat.js, which the snippet editor still uses to format SQL snippets. SQL and HTML remain fully supported as LANGUAGES for diffing, snippet highlighting and detection — only the tools are gone. HTML Entities was the last text→text tool, so the launcher's convert machinery had no reachable user: runConvert, the convertInput/convertResult plumbing, and QuickLookConvert's input/output textareas are all deleted rather than left as unreachable code. Every launcher tool now renders a rich panel, and the component is just the frame around one. That exposed a real gap the old textarea had hidden: entering a launcher tool left focus on the body, so Escape never reached the panel's handler and there was no keyboard way back to the list. The panel now lands the caret in its first field — you can type immediately, and Escape works with no click first. Also refreshes the README, which still described the pre-panel tool lineup and "nine themes" (there are fourteen), and removes the dead DEVELOPMENT_PLAN.md links from the README and docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last plain format/validate dialog becomes a panel like the rest: - Pretty / Minify toggle, live as you type. - An XPath-subset filter (/a/b, //descendant, [n] 1-based as XPath means it, @attr and *) with a live match count. Hand-rolled, no new dependency. Attribute matches are values rather than elements, so they list on their own instead of being forced into the tree. - A collapsible element tree that reads attributes on the tag, the way the source does; deep nodes auto-collapse. - Parse failures show inline with the line and column the validator already knew, instead of a bare status line. Parsing goes through DOMParser in 'text/xml': it builds a tree without executing anything and does not resolve external entities, and the document is never inserted into the page — only read into plain objects the template renders as text (CLAUDE.md #7). Fixes a colour bug this work exposed in BOTH trees: --warning-text is the ink for text ON a warning fill, so it is near-black on all six dark themes. JSON numbers and booleans have been invisible there since that tree shipped, and XML attribute names inherited it. Both now use --favorite, the palette's legible amber on every ground. check:themes does not cover this pairing, so it was caught by eye. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lines already did a literal per-line find/replace, so the standalone Replace tool was a second, weaker copy of the same idea. Lines now runs the real thing through the shared replaceText(): Text / Word / Regex modes, an Ignore-case toggle, `$1` capture groups in regex mode, a count of what was replaced, and a compile error surfaced inline instead of input passing through untouched. Replace runs per line, which is what makes `^`/`$` anchors useful here and keeps it composable with split/sort/dedupe/wrap. The one thing it cannot express is a pattern meant to span newlines — noted in the util. The standalone dialog is gone (utils/findReplace.js stays; Lines is its caller now). Cmd/Ctrl+Shift+R was freed by that removal and now opens Lines, so the old muscle memory still lands where replace lives. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s list Two keyboard bugs in the launcher: ← no longer left a tool panel. That is a regression from the panel-only rework: the ArrowLeft handler lived on the input/output textareas, so deleting them took the only way back to the list with it (Escape and the back button still worked, but ← is the mirror of the → that entered). The guard is now useCaretBackOut — a composable with its own unit tests, because this is exactly the kind of event logic that regresses invisibly when a component is refactored. It backs out only when the caret has nowhere left to go: inside a field with a caret past 0, a live selection, or a modifier held, the key still belongs to the field. Arrowing down never scrolled the results list. useQuickLook had no scrollIntoView at all — the command palette has one, the launcher was simply missing it, so the selection walked off the bottom and out of sight. It got noticeable once the Tools section made the list long. The snippet preview was never affected; usePreviewLines scrolls that by exact overflow. Both covered by e2e: ← held by a mid-field caret and honoured at its start, and the list scrolling once the selection passes the fold. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every TEXT_TOOLS entry became a panel, so TextToolDialog's Monaco branch had
no reachable user — and neither did the fields feeding it. Removing it takes
out a whole layer:
- TextToolDialog is now the dialog frame plus a panel: no Monaco buffer, no
status line, no Format/Copy/Add-to-Snippets actions, no file drop.
- utils/textTools.js is gone. Once validate/format/language/validLabel/
requiresValid/actionLabel went, each entry was {title, panel} with panel
always equal to its key — the registry already carries the name, so the
dialog reads that and switches on the id. One list, not two. The Epoch
dialog is titled "Epoch" now, matching the shelf and palette.
- utils/devTools.js is gone entirely: the panels use utils/jwt, epoch and url,
so nothing referenced it. uuid.js loses convertUuid/validateUuid and the
three helpers only they used; the TextTool typedef goes too.
Comment sweep across the tools work, per the standing rule: the multi-line
headers narrating what each panel does are gone (the template and imports
already say it), leaving only the terse "why" lines — the DOMParser safety
note, the per-line replace limitation, the self-import needed for recursive
components, the --favorite token gotcha, and the pseudo-element section
labels that keep keyboard indices 1:1 with rows.
CLAUDE.md's "new tool" instruction pointed at the deleted file; it now
describes the registry + panel + case, and records that no validate path
remains to fall back on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audited all eight tools across all fourteen themes with an instrumented pass rather than by eye — measuring text contrast against each element's own resolved background, horizontal overflow, and vertical centring of controls sharing a row. Nothing was off: zero findings. Worth recording how that number was reached, because the first run reported 51 problems and every one was a lie. It sampled colours 30ms after switching theme, mid-`transition: color 0.12s`, so it read values partway between the old and new palette — `.seg-opt.on` "failing" at 1.08:1 when its --accent/--text-on-accent pairing is ~7.6:1. Disabling transitions before measuring cleared all 51. The instrument was then validated against the bug that did ship: re-applying --warning-text to .xt-key reads 1.07:1 versus 6.85:1 healthy, well under the 2.2 threshold, so a real regression of that class would be caught. Driving the routine on the keyboard alone did surface one wart: a tool dialog opened with nothing focused, so the first Tab landed on Close and you could not paste without reaching for the mouse. The panel's first field is now focused on open, matching what the launcher panel already did. The new spec covers what click-driven tests walk past: every tool opened by typing in the palette and closed with Escape, the caret landing in the first field, Tab never escaping the dialog, and the launcher's full arrow chain (list → Tools → expand → panel → ← → navigation resumes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… copy Four things the screenshots caught: The palette listed recent tools twice — once under Recent, then again in "All tools" — so URL, Encrypt and Patch each appeared in the same view. The second section now holds only what Recent does not, and is labelled "Other tools" to stay honest; with no recents it is still one "All tools" list. The section label had a single pixel of clearance and read as if it were touching the row above it. Its row now reserves a label's height plus a gap. A tall panel could not be scrolled from the keyboard in the launcher. The window is short, the Lines panel overflows it, and since the caret starts in the first textarea every ArrowDown was swallowed by the field — the result at the bottom was mouse-only. usePanelScroll takes the key once the field has no caret left to give (a textarea keeps it while it can still move; an input, button or checkbox never can), and handles PageUp/PageDown. A composable with unit tests, like the ← guard beside it. "Default" was the placeholder for a snippet or diff carrying no tags, in a span already classed `untagged`. It reads as a tag named Default; it now says Untagged. Also drops the .qc-field/.qc-in/.qc-out/.qc-err rules the panel-only refactor orphaned, and covers the launcher scroll in the keyboard spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Tools section sat after every snippet and saved diff, so on a large library reaching a tool meant scrolling to the bottom of the list. It now leads the list. Collapsed — which is its default — that costs exactly one row, and → expands it in place without moving anything else. The seam that separated the two groups moves with it: it now sits under the tools block, above the first snippet. A tool-matching query still opens the section and lands the selection on the first tool, now at index 1. Also retires the last "convert" wording in the footer hint: → on a tool row reads "open tool", since these tools format and generate as well as convert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dismissing the launcher on macOS makes the main window non-focusable for 300ms so the OS hands focus back to whatever you were in, rather than raising DiffBro. That is a timed state nothing else knew about, so any code that focused the main window inside that window silently did nothing — the window came up, but not as the key window, and you had to click it. Three paths could land in it: choosing a launcher result (openInMain), relaunching the app (second-instance), and re-activating from the dock. All three now clear the state first through allowMainFocus(), which also cancels the pending timer instead of racing it, so repeated summon/dismiss cycles cannot leave overlapping timers fighting over focusability. The timer is guarded against a destroyed window on quit. Verified on Linux only: this whole branch is darwin-gated, so the Docker e2e cannot reach it — the launcher and smoke suites confirm nothing else regressed, and the focus behaviour itself needs a manual check on macOS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The empty-state format tiles are buttons now: each opens the file picker filtered to that format, into the first free side. The renderer names a format and main resolves it to dialog filters (src/main/fileFilters.js), so it can never supply a filter of its own. Every dialog keeps "All files", since everything non-.xlsx still diffs as text. The filter lookup uses hasOwn — a test for 'constructor' caught it resolving up the prototype chain and handing the dialog a function. - Mini icon buttons show a hover tooltip. The tool-panel copy buttons had only aria-label, which names them for a screen reader but shows nothing on hover; the sidebar glyphs had tooltips that were whole sentences. - The snippet preview opens from the title only. Hovering anywhere on the row popped the card while you were reaching for its buttons. - The palette's first section label had less room than its own height, so "Recent" clipped against the search bar. Fixes a regression from the tooltip pass in the same change: an icon-only button takes its accessible name from `title`, so shortening those labels renamed the buttons — six e2e specs that locate them by name failed, and the screen-reader name degraded to "New". Short text stays in `title`; the full name moves to `aria-label`. Each fix is covered in e2e/ui-affordances.spec.mjs so none of them can come back quietly: the tiles are buttons, icon buttons keep both labels, the copy button has a title, the preview ignores the row, and the section label has clearance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Icon buttons had `title`, which the OS draws — the app cannot see, style or time it, and in this window it often never appeared. AppTooltip renders one bubble in the page instead, opted into with data-tip and teleported to body. That teleport matters: left inside the app tree the bubble had a box and passed a visibility check while never being painted, because a parent stacking context capped it. The test now asserts it escaped the tree, since toBeVisible() alone would have gone on passing. Converted the icon buttons in the tool panels, the snippet rows, the sidebar section headers and the saved-diff rows. Where `title` had been the only label, aria-label now carries the full name so the accessible name is unchanged — and two e2e locators that reached for `getByTitle` move to the role/name they should have used. Also makes the tools shelf padding symmetric: 6px above and 10px below read as a misaligned band once the chips wrapped to a second row. Every fix here was written test-first, red before green, per the workflow now at the top of CLAUDE.md. The four regression tests from the previous commit were retro-validated the same way — each was re-run against a reverted fix and confirmed to fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backing out of a tool destroyed the panel, so an accidental Escape threw away
whatever you had typed. The panel is now mounted once and hidden with v-show
rather than removed, and each tool keeps its own state.
KeepAlive was the obvious route and does not work here: Vue's compiler cannot
generate a KeepAlive whose only child is a v-if with no v-else, and it crashes
the build. Mounting once and toggling visibility avoids that entirely.
A hidden panel is not remounted, so it no longer gets focus from onMounted — a
`visible` prop tells it when it is shown again and it re-takes the caret.
Extracting the result rows into QuickLookResults keeps QuickLook.vue inside the
120-line template cap that this change pushed it past; the scroll-into-view
watcher moves with the rows, so the child owns that behaviour outright.
Both behaviours were written test-first and seen failing: the input came back
empty ("" instead of "keep me") and the mode reset to Encode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copying files in a file manager puts a file list on the clipboard AND a plain-text flavour holding just their names. Paste-to-compare read only the text, so two copied CSVs pasted "mock_products.csv\nstatement.csv" into the editor instead of opening them. Paste now asks for files first: two become the two sides, one fills the free side, and a clipboard holding only text falls through to the existing paste flow untouched. The flavours differ per platform — text/uri-list on X11, an XML plist under NSFilenamesPboardType on macOS, with public.file-url and FileNameW as single-file fallbacks. Parsing is a pure module so all four are unit-tested, including the cases that must NOT yield a file: a copied https:// link, a file:// URL pointing at another host, and a relative plist entry. Paths off the clipboard are untrusted, so they take the same route as a dialog pick — resolved, refused under userData, stat'd as a real file, and run through allow() before anything reads them (rule 6). Written test-first: the e2e failed with the slots still reading "left file… ⇄ right file…", and the plain-text case passed throughout, which is what proves the existing behaviour survived. Only the X11 path is exercised end to end — the Docker e2e is Linux, so the macOS and Windows flavours rest on their unit tests and need a manual check on those platforms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The file check ran after the confirm, so Cmd+V on copied files still opened "Paste detected" and only loaded them once you answered it — a prompt about entering paste mode, which is not what you asked for. It now runs before: copied files load straight away, and the prompt is left to pasted TEXT, where switching into paste mode is a real decision. My own e2e hid this. It clicked the confirm dialog if it happened to be there, so the flow passed while still showing a prompt it should never have shown. It now asserts the dialog stays hidden, which is what failed before this fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… files Copied files still did nothing on macOS. Three separate reasons, all found by dumping a real pasteboard rather than guessing again: - availableFormats() never mentions NSFilenamesPboardType, so gating reads on that list meant never reading the files at all. - It DOES list text/uri-list — and hands back zero bytes for it. Returning at the first format merely present threw the files away. - NSFilenamesPboardType is a BINARY plist, not the XML one the parser expected, and it is the only flavour carrying every copied file. The Apple URL flavour holds just the first. Sources are now tried by content, in order, and the first that actually yields paths wins. pathsFromBinaryPlist walks the plist's string objects — ASCII and UTF-16BE, inline or trailing-int lengths — which is far less than decoding the format but enough for a list of paths. Tested against bytes captured from a real macOS pasteboard, including the two traps above, and verified end to end on macOS: two files copied, Cmd+V, no paste prompt, both slots loaded. The Linux uri-list path is unchanged and still covered by its own e2e. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explorer copies files as CF_HDROP — a 20-byte DROPFILES header whose first field is the offset to a double-null-terminated path list, and whose last says whether those paths are UTF-16. Only the single-path FileNameW fallback was handled, so a multi-file copy had nothing to read. Adds pathsFromHdrop, and tries text/uri-list through read() as well as readBuffer(), since newer Chromium exposes CF_HDROP under that name and Electron serves some flavours through only one of the two. Writing the tests turned up a bug that would have broken Windows on its own: file:///C:/x parses to "/C:/x", and that leading slash makes the path unopenable. The drive letter now leads. NOT verified on a real Windows machine — I have no Windows here. The parsers are unit-tested against accurate CF_HDROP buffers (wide, ANSI, UNC, unicode, truncated), but whether Electron hands us those bytes under the name "CF_HDROP" is untested: it is a predefined Windows format rather than a registered one, and readBuffer is marked experimental. FileNameW is a registered name, so single-file paste is the likelier of the two to work. Sources are tried in order and fall through, so an unreadable flavour costs nothing but a miss. macOS re-verified end to end after the change; the Linux e2e still passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A snippet whose language is "url" is a saved link: the row offers an Open button that hands it to the OS browser. Opening is fenced the way the claude.ai links already were — validated in the MAIN process and confirmed by the user before anything reaches the OS. The fence is the SCHEME: only http(s) passes, because openExternal will otherwise open a local file, run a script handler, or launch another application from a crafted string. That check is a pure policy function with its own tests (javascript:, file:, data:, vbscript:, smb:, ms-msdt: and friends all refused, along with junk and absurd lengths). URL snippets never travel. _bundle leaves them out of an export and restoreBundle drops them from an import, so a link can only ever be one typed on this machine — a shared bundle cannot carry one for someone else to click. Both halves are tested, and both failed before the filters existed. Also on the saved-diff rows, which the same pass touched: delete uses the trash icon the rest of the app uses, the "kept" label is gone (it said nothing a blank does not), and the actions only appear on hover — the resting row now gives its width to the diff name, matching the snippet rows. The sharing e2e hovers first, as a user must. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rule added with the URL snippet said openExternal was reachable "only through links.js". The audit it was meant to support found a third call site it had missed: menu.js opens the issue tracker. That one is safe for a different reason — the URL is hardcoded in main and the renderer cannot influence it — but a rule that names the wrong set is worse than none, since the next reader trusts it instead of grepping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mindaugaskasp
enabled auto-merge (squash)
July 31, 2026 05:40
Main brought PRs #8 (phase0 quick wins) and #9 (brand/monoline icon). The merge base was old enough that git saw both sides adding the same features independently, so every conflict was one architecture against its successor. Resolved to this branch's side throughout, because each conflict was the tools rework against what it replaced: - CommandPalette: ours is a strict superset — main's version plus the tools scope (Recent/Other sections, action dispatch, tools-aware placeholder). - textTools.js / devTools.js: main extended the TEXT_TOOLS table with JWT, Epoch, URL, HTML entities and sort-dedupe. All of those exist here as rich panels, and HTML entities and the standalone sort-dedupe were deliberately dropped, so both files stay deleted along with their tests. Nothing imports them (src/main/textTools.js is the unrelated encryption IPC). - diffStore / menus / menu: the same story — showBase64Dialog, showFindReplaceDialog and tools-html are gone here on purpose. - SavedDiffs: main's inline TOOLS array against the extracted ToolsShelf. Main's non-conflicting work is kept: the HTML export, snippet import, unified-diff apply, and commandPalette flattening are all present and their tests run. Conflicts were resolved hunk by hunk rather than with --ours, so main's cleanly-merged changes to those same files survived. 837 unit and 104 e2e pass on the merged tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.