Skip to content

chore: rename Myme to Marfa wholesale#68

Open
aicayzer wants to merge 27 commits into
mainfrom
chore/rename-myme-to-marfa
Open

chore: rename Myme to Marfa wholesale#68
aicayzer wants to merge 27 commits into
mainfrom
chore/rename-myme-to-marfa

Conversation

@aicayzer
Copy link
Copy Markdown
Owner

Summary

Wholesale Myme to Marfa rename across the SuperWhisper Analytics codebase to match the broader Marfa cutover (T-263). Substitution pattern matches the main Marfa repo group:

  • mymehq -> withmarfa
  • MYME_ -> MARFA_
  • Myme -> Marfa
  • myme -> marfa
  • @mymehq/* -> @withmarfa/*@^1.0.0

Dep pin flipped from @mymehq/sdk@^5.9.0 to @withmarfa/sdk@^1.0.0. Lockfile regenerated; @withmarfa/sdk@1.0.0 resolves cleanly from npm. Source paths renamed: src/main/myme/ -> src/main/marfa/, state/mymeStore.ts -> state/marfaStore.ts, shared/myme-labels.ts -> shared/marfa-labels.ts, scripts/myme-* -> scripts/marfa-*.

Test plan

  • pnpm typecheck — clean
  • pnpm test — 148 passed (15 files)
  • Verification grep git grep -nE '(mymehq|MYMEHQ|MYME_|myme_|Myme[A-Z]|\bmyme\b)' returns 0 matches
  • Lockfile shows 0 @mymehq/* entries

aicayzer added 27 commits May 17, 2026 00:19
Pulls in the Myme TypeScript SDK at 5.6.0. First step of the
optional Myme integration — no runtime code uses it yet, but
locking the version in for the rest of the work.
Adds the two custom-type schemas that the Myme integration will
write against (recording inherits from core.note; session is
standalone) and a one-off registration script that posts them to
the configured tenant.

The script reads ~/.myme/admin.json for direct-admin access; the
runtime integration uses the OAuth device flow instead. Types are
registered against staging.myme.so as part of bringing this milestone
up — re-running the script is a no-op upsert.
Adds the Settings → Integrations → Myme card, the IPC surface for
the integration, and a stub main-side module that satisfies the
contract end-to-end. The card renders four states off the
MymeStatus discriminated union (disconnected / connecting /
connected / syncing) plus a renderer-composed 'disabled' state
when demo mode is on or no recordings path is configured.

Endpoint defaults to https://staging.myme.so and persists in
config.json via an additive migration. Real OAuth + sync land in
milestones 3+; the stubs let the renderer exercise every state
today.

Smoke-tested in dev: the card renders correctly in disconnected,
connecting, and disabled states against the real 11.8k-recording
dataset.
Connects the Settings → Integrations → Myme card to the Myme SDK
end-to-end. The 'Connect' affordance now opens a paste-API-key
pane; the supplied key is verified via items.stats(), encrypted
with Electron's safeStorage (Keychain-backed on macOS) and
persisted to <userData>/myme-credential.enc. Boot-time
credential probe restores the connected state across launches.

Bypasses the OAuth device flow the integration spec mandates —
staging's OAuth path can't be bootstrapped via dynamic client
registration today, and the well-known doesn't advertise the
device_code grant. See the running log for the full chain of
findings; API key is the worktree-experiment substitute that
unblocks milestones 4+.

Also fixes the @mymehq/sdk ESM-only packaging issue by adding the
SDK to electron-vite's externalizeDeps exclude list — without
this, the bundled main process tries to require() the ESM SDK at
runtime and crashes with ERR_PACKAGE_PATH_NOT_EXPORTED.

Smoke-tested against staging.myme.so: paste-and-connect with a
valid key flips to connected; bad key surfaces 'Invalid API key'
inline; disconnect clears the credential file and reverts to
disconnected.
Builds the per-recording projection layer (Recording → Myme item
payload with stable content hashes), the on-disk sync-state file
(per-source_id hash + server-assigned itemId + last-pushed time;
atomic write-temp + rename), and the engine that drives the diff:
new → upsert, hash-mismatch → upsert, vanished → transition trashed,
match → no-op.

Substitutes parallel singular items.upsert for the spec's
items.bulk path — bulk is admin-only on the server side, which
makes it unreachable from any out-of-tree member-role
integration. Caps concurrency at 10 to stay under staging's
2000 req/min budget while keeping the channel saturated.

Smoke-tested against staging: Sync now flips the card to
'Syncing recordings 0 / 11,765 → 20 / 11,765 …', items land on
the server, status returns to 'connected' after the run. The
initial 11.7k-recording push is bottlenecked by per-request
latency — subsequent reindexes only diff the delta, so the
steady-state cost is small.

Tests: 14 new (projection field selection + hash stability,
state load/save/clear + atomic write + defensive parse).
Adds an onReindexed listener registry to cache.ts and threads it
through the rescan exits. The Myme integration registers a single
listener at app.ready that fires syncNow() fire-and-forget
whenever the cache rebuilds — fs.watch debounce hits, manual
Reindex from Settings, demo-mode toggles. The engine's
syncInFlight guard coalesces overlapping runs, so back-to-back
watcher events don't spawn parallel syncs.

The diff semantics for mutation handling and disk-delete
propagation already landed with the engine in the previous
commit (hash-mismatch → upsert, vanished source_id → transition
trashed). This commit lights up the trigger so they actually run
without a manual Sync now click.
Adds gap-grouping (pure, in sessions.ts) and the session-projection
+ engine pass that mints superwhisper.session items with inline
core.parent-of edges to the constituent recordings.

The natural key is (first_recording_id, gap_threshold_minutes),
so a threshold change yields fresh source_ids — the engine's diff
naturally trash-and-re-mints the prior session set without
in-place updates. Trash-and-re-mint over in-place is the resolution
to the open question in the integration spec.

Edge writes use the inline edges field on items.upsert, sourced
from the recording itemIds resolved in the same sync pass. If a
recording's upsert failed earlier in the run, the session keeps
the partial edge set rather than failing whole — the next sync
fills in the missing edge.

Tests: 9 new gap-grouping cases (threshold boundaries, dominant
mode + tie-break, out-of-order input, source_id stability).
Adds scripts/myme-smoke.mts — a deterministic 5-recording smoke
against staging.myme.so that exercises every diff path from the
integration spec:

  1. first-run push (5 recordings + 1 session + 5 parent-of edges)
  2. mutation handling (edit one recording, verify version bump)
  3. disk-delete propagation (drop one, verify trashed state)
  4. threshold-change re-mint (bump gap, verify fresh source_ids)

Decoupled from the on-disk state file so it can be re-run cleanly:
teardown step trashes every prior superwhisper.* item in the
tenant before each run.

Reads admin creds from ~/.myme/admin.json. Run with
'pnpm dlx tsx scripts/myme-smoke.mts'. Existence + size kept
small so it doesn't paper over the real-corpus push performance
problem documented in the running log.
Two affordances added to the Settings → Myme card:

  - Cancel button in the syncing state. The store calls a new
    myme:cancelSync IPC that aborts the active sync's
    AbortController. The engine checks signal.aborted between
    items.upsert / items.transition calls and persists everything
    that landed before the abort, returning with error =
    'Cancelled'. In-flight requests can't be cut short via the
    public SDK, so an aborted upsert's catch handler now
    suppresses the resulting transport error if the signal fired
    while it was in flight — keeps 'Cancelled' as the canonical
    outcome rather than letting a timeout / network error
    overwrite it.

  - 'Push N most recent (testing)' knob in the connected state.
    Persisted as Config.myme.syncLimit (additive migration,
    default 0 = full sync). When the limit is in effect the
    engine slices recordings.slice(0, n) (newest-first per
    scanner) and skips the soft-delete + session passes entirely
    — both are all-or-nothing concepts that would be incoherent
    against a partial recording view.

Smoke-tested locally: setting the knob to 5 and clicking Sync now
flips to 'Preparing… 0 / 5' with a visible Cancel button. Engine
respects the limit (5 attempted upserts, not 11.7k).
… setting (#57)

The 'push N most recent (testing)' knob has been the de-facto sync-cap
control since the engine landed, but it was framed as a debug toggle —
default 0 (uncapped), dashed-border styling, "testing" copy. This
promotes it to a real setting in the Integrations tab.

Changes:

- `src/main/config.ts` — default `syncLimit` flips from 0 → 100. Protects
  against first-sync floods on large corpuses (the wider motivation for
  the wave); explicit `0` still means no cap for users who want to
  test the full sync surface.
- `src/renderer/.../MymeCard.tsx` — reframe the row: title "Sync most
  recent N recordings"; copy "0 = no cap. When capped, session
  derivation and disk-delete propagation are skipped — turn off the
  cap to test the full sync surface."; drop `border-dashed`; update
  the card's module-doc + the `SyncLimitRow` JSDoc.
- `src/main/myme/engine.ts` — cap-gate comments in the soft-delete pass
  + session-derivation pass drop the "testing-knob" framing and point
  at the UI surface for the trade-off rationale. Engine behaviour
  unchanged (all-or-nothing under a cap; partial-view safety).

Trade-off remains intentional and now explicit: capping disables
session derivation + disk-delete propagation. Users who want both can
flip the cap off; users who don't want to flood Myme on first sync get
a sensible default.

Refs T-165.
…#58)

Default Connect now enters the OAuth device flow (SDK 5.7.1's
`startDeviceFlow` from `@mymehq/sdk/auth`); the renderer shows the
user code + a "Verify in browser" button (deep-link variant when
provided), with a copy-to-clipboard and a "Use API key instead" link
to the existing paste path.

- Tokens persist via `safeStorage`-encrypted JSON blob — discriminated
  union of `{ kind: 'api-key', key }` and `{ kind: 'oauth', clientId,
  tokens }`. Legacy bare-string credentials up-convert transparently so
  existing API-key users don't get logged out.
- `MymeClient` builder accepts the new OAuth credential by wrapping it
  in a minimal `TokenProvider` that proactively refreshes when < 60s
  remain, single-flight, persists rotated bundles back to disk.
- DCR (`POST /auth/oauth2/register`) runs once per `connect()` so each
  device ends up with its own client id alongside the tokens.
- IPC surface adds `myme:useApiKey` (switch into the api-key pane) and
  `myme:cancelConnect` (abort an in-flight device-flow poll). `connect`
  is now async.
- State-machine tests cover device-flow happy path, user-denied,
  code-expired, DCR network failure, and cancel-mid-flow. Renderer
  ceremony stays out of unit-test scope (covered by T-166 e2e walk
  later). Tokens round-trip tests pin the on-disk format + legacy
  up-conversion.
…59)

The DCR scopes in `DEFAULT_SCOPES` were placeholders (`['*:read',
'*:write']`) — bare wildcards that aren't in the Myme server's scope
allowlist. Live test in the test app produced
`Client registration failed: invalid_scope (400)` on first connect.

The server's allowlist is built from `TYPE_REGISTRY.keys() +
EDGE_TYPE_REGISTRY.keys()` plus OIDC standards. Replace the placeholder
with the genuine surface this app uses:

- `superwhisper.recording:{read,write}` — data plane
- `superwhisper.session:{read,write}` — derived sessions
- `metadata.types:write` — registers the custom types on first sync
- `openid`, `profile`, `email`, `offline_access` — OIDC standards +
  refresh-token issuance

Narrower than the API-key path's effective blast radius (admin keys
have everything) but accurate to what the engine actually touches —
matches the T-164 ticket's own note ("Tighten when the engine settles
on what it actually needs"; we now know what it needs).

Caught during T-166 e2e validation. Unit tests didn't surface this
because they mock the SDK transport — only the live server's scope
validator rejects bare wildcards.

Refs T-164.
Audit pass on the engine: session derivation mints a `parent-of` edge
from session → recording (`engine.ts:464`). The OAuth grant must carry
matching edge scopes for the server's `requireEdgePermission` gate;
otherwise uncapped syncs would 403 the moment sessions try to write.

Add the two scopes:
  - `edge.parent-of:read`
  - `edge.parent-of:write`

Capped-mode runs (cap > 0) skip the sessions pass entirely, so this
gap didn't surface in the initial T-166 device-flow validation
(which was capped). Surfaces immediately under uncapped sync.

Refs T-164.
… to 'parent-of' (#61)

The engine wrote session→recording edges with the wrong type literal:

    edges: { 'core.parent-of': recordingItemIds }

The server's registered edge type id is `parent-of` — no `core.`
prefix — per `packages/types/core/edges/parent-of.json`. The wrong
literal would have failed the server's edge-type validator at write
time with an `edge type not registered` error.

Latent because no prior testing exercised it:
- API-key flow: all prior staging testing was capped, and capped runs
  skip the sessions pass entirely (engine all-or-nothing semantics).
- OAuth flow: just added in T-164; uncapped T-166 validation would
  have hit this immediately.

Caught during the T-164 scope audit (grep for edge writes in the
engine, looking for the matching `edge.<type>:write` scopes).

Fix: one-line edit at engine.ts:464 + three comment cleanups across
engine.ts and sessions.ts to keep the documentation honest.

Refs T-172.
Replace the hardcoded `superwhisper.recording` / `superwhisper.session`
type IDs in the projection layer with a `MymeMapping` config block that
binds each source kind (recording, session) to a target type and a
field map. Default = bundled, identity field map — existing source_ids
stay stable for users mid-migration.

Non-bundled bindings get an 8-char fingerprint folded into source_ids
so swapping the binding produces fresh ids and old items diff out on
the next sync (trash-and-re-mint, mirroring the existing gap-threshold
pattern). Bundled uses a sentinel suffix-free shape.

Adds:
- `mapping.ts` — types, defaults, fingerprint helper, default field-map
  generator with a name-alias table for auto-pairing target fields.
- `registration.ts` — ensures bundled / authored types exist server-side
  on each sync; idempotent.
- IPC: `getMapping`, `setMapping`, `getModeFilter`, `setModeFilter`,
  `probeConnection`, `listTypes`, `registerType`.
- Mode filter wiring through the engine.
- `mapping.test.ts` + extended `projection.test.ts`.

Default mapping persists transparently for older configs; the engine
clears sync state on mapping change so the next pass starts from a
clean diff.
Adds a MappingPanel inside the connected MymeCard with two rows
(recordings + sessions) showing the current binding and an Edit
action. Edit expands an inline editor offering three modes:

- Bundled — app-defined default, fixed field map.
- Existing — pick a type already in the user's Myme tenant via
  `types.list()`; auto-pair recording fields to target fields by
  name alias.
- Authored — define a new type from scratch via an inline form;
  registers the schema on Apply.

Apply gates behind a trash-and-re-mint confirmation explaining that
old items soft-delete on next sync. Cancel discards the draft. The
panel reads the type list lazily on first edit, falls back gracefully
when the list fails to load, and validates authored type ids before
registration.

Store gains mapping CRUD, type-list cache (with loading flag),
mode-filter mirroring, probe and register-type passthroughs. Read on
demand; main is source of truth.
Splits the connected MymeCard into three composable panels:

- ConnectionPanel — endpoint, account identity from `profile.get`,
  reachability indicator with reason on failure, Test connection +
  Disconnect actions.
- MappingPanel — unchanged from PR 2; slots in unmodified.
- SyncControlsPanel — sync-cap (reframed copy), mode-filter chips
  derived from observed Superwhisper modes, last-sync drift, recent-
  error row, manual sync trigger.

Mode chips compute counts client-side from the indexed dataset; clicking
a chip toggles the persisted mode filter (`null` = sync everything,
list = sync only those modes). The engine reads the filter from config
on each sync.

Test connection runs `profile.get()` on mount + on click; result lands
in local state and renders a green pill on success or the error inline
on failure. No bespoke ping endpoint — the cheapest authed call covers
both the reachability check and the identity display.
* feat(sw-app): redesign the connected card layout

The original layout buried the type mapping behind an Edit / Close
gate, opened with Connection (the least frequently touched part),
and gave every subsection the same border treatment — flat and
patchwork.

This rework treats the card as three clearly-separated sections with
a consistent header pattern (uppercase tracked label + subtitle):

- Type mapping  (top — the headline decision)
- Sync          (middle — what lands, when)
- Connection    (bottom — compact footer)

Type mapping is now always-visible: each binding card shows the
segmented [Bundled | Existing | Authored] control inline, the type
id in monospace, and the field map open. Bundled fields render
read-only with a 'Fixed by the bundled type.' note; existing and
authored modes expose the field-picker UI.

Apply only appears when the draft differs from the persisted
binding, and still gates the trash-and-re-mint warning before
committing. Resetting the binding is one click rather than 'Cancel +
re-open Edit.'

Connection shrinks to a compact footer: endpoint + identity on the
left, a status pill on the right, Disconnect / Test connection in a
small button row below. Sync gains an inline Sync-now button next to
the Last-synced row so the common action sits next to its status.

Renderer-only — no engine, IPC, or main-process changes.

* refactor(sw-app): split the Myme tab into three Settings cards

Replaces the single MymeCard with three sibling SettingsCards that
match the rest of the Settings tabs (Indexing / Filler words shape):
Connection · Sync · Type mapping. The integration tab in the
segmented strip renames from 'Integrations' to 'Myme'.

Changes:

- Connection (top, Cloud icon) — endpoint, account identity, status
  pill, Disconnect + Test connection. Pill moves into the header
  via the SettingsCard.headerExtra slot.
- Sync (middle, RefreshCw icon) — mode filter rendered as toggle
  rows per observed Superwhisper mode (matches Watch folder pattern),
  sync cap, Last-synced display with inline Sync now / Cancel
  buttons. During syncing, the header pill shows progress and the
  body switches to phase + processed/total.
- Type mapping (bottom, Layers icon) — binding panels for recording
  and session. Field map laid out with CSS subgrid so target / arrow
  / source columns line up across rows regardless of name length.
  Authored / Existing modes expose the field-picker inline; bundled
  renders the canonical layout read-only with a fixed-source label.

Buttons are unified on the standard chrome treatment (no
'primary' variant). The disconnect/connect flow lives in its own
Connect-to-Myme card during disconnected / connecting states, so the
empty-state copy matches the connected hierarchy.

Drops the three superseded panel files (MappingPanel,
ConnectionPanel, SyncControlsPanel) — fully replaced by the new
SettingsCard-wrapped components.

* refactor(sw-app): drop sync-cap UI, add test-sync button, polish copy

The sync-cap had nasty semantics — skipped soft-delete, skipped
session derivation, orphaned items beyond the cap — and it was a
testing knob that leaked into the user surface. Drop it.

- Remove `syncLimit` from Config.myme, MymeStatus, the
  setSyncLimit IPC + store action. Engine's `limit` option stays
  for tests but no production code path sets it.
- New `testSync` IPC + store action: runs `syncRun` with limit=5
  for a quick sanity-check pass. Surfaced in the Sync card as a
  secondary 'Test sync' button next to 'Sync now'.

Field-map grid: target sits flush left, source cluster (arrow +
value + remove) sits flush right. A spacer column between the two
halves keeps that split stable regardless of target-name length.
Subgrid carries the column widths across rows.

Copy pass: tighten the card subtitles, retire 'Local-only by
default' for 'Off by default — local-only until you connect',
rewrite the trash-and-re-mint warning, replace generic 'No fields
mapped' with action-oriented prompts. All buttons unified on the
chrome treatment.

* fix(sw-app): inline arrow next to source on field rows

Drop the cross-row arrow alignment. Each row uses flex justify-between so the target sits on the left and the arrow+source cluster sits on the right, trailing the row. Same spacing for both bundled (read-only) and editable variants.

* fix(sw-app): align arrows in a fixed centre column on the field map

Target column is max-content (locks to the longest target name); arrow column auto; source column 1fr right-aligned. Result: arrows sit at the same x across every row, source labels hang flush right.

* fix(sw-app): drop the arrow from field rows

The arrow column added noise without clarifying the relationship. Target flush left, source flush right reads as the mapping just fine on its own.
Local-only patches so this build can be installed in /Applications
alongside the v0.2.x release without sharing config, Myme tokens, or
sync state, and without the upstream auto-updater clobbering it.

- appId: me.cyzr.superwhisper-analytics-myme
- productName: SuperWhisper Analytics (Myme)
- userData dir redirected to superwhisper-analytics-myme
- publish stanza dropped; initAutoUpdater() call removed

Lives on local/myme-install branch, never pushed.
Importing ./updater pulls electron-updater into the bundle, and the
pnpm-packaged asar is missing transitive deps (e.g. `ms`), which
crashes the main process at module load.

The myme install build doesn't auto-update anyway, so the renderer's
"Check for updates" button now silently no-ops. The shipped main
branch has the same packaging issue and will need a proper fix (likely
node-linker=hoisted in .npmrc) before any release exercising the
auto-updater code path can be cut.
…vice

Pairs with the monorepo property-shadow ban in T-203; companion to
vault ticket T-204. Bumps superwhisper.recording schema to v2.

- schemas.ts: rename field, update description, version 1 → 2
- mapping.ts: rename target key + source-field-ref + alias value
- projection.ts: rename case label in readRecordingField
- engine.ts: populate first-class item.device from os.hostname()
- TypeMappingCard.tsx: mirror rename in renderer-side defaults
- projection.test.ts: update expected property keys
…lter

Follow-on to commit 114d11e (T-204 property rename). Fixes the
end-to-end story so the rename actually lands on existing installs:

- config.ts + mapping.ts: forward-migrate persisted mappings that
  reference the stale `recording.device` source-ref so installs from
  before the rename project the device value into the new shape
  instead of silently dropping it. Bundled bindings are rebuilt from
  the canonical map; authored / existing bindings only get the
  source-ref rewritten so user-defined target keys are preserved.

- registration.ts: re-register when the local schema version is
  higher than the server's, not only when the type is absent. Without
  this, a bundle bump like v1 → v2 never propagates to a server that
  already has the older row, so projection writes input_device but
  the server validates against the v1 schema and drops it.

- mapping.ts: add the `input_device` key to the recording-field alias
  table so authored types declaring an `input_device` field auto-pair
  correctly, and rename the field in `authoredRecordingStarter` to
  match (the previous `device` field would now trip T-203's
  property_shadows_field ban).

- engine.ts: drop recordings with empty `result` before projection.
  Superwhisper writes meta.json files with empty transcripts for
  failed captures; projecting them produces a payload missing the
  required `body` field (inherited from core.note), which 400s on
  /items and halts the sync via the validation path.

Tests added for migrateRecordingMapping (4 cases) and
ensureTypesRegistered (7 cases).
- Tabs: General / Analysis / Sync / Developer / About (was four; Sync
  separated from a single Myme card).
- Tokens: accent-green and border-strong added in both light + dark.
- Buttons: default border-radius bumped 4→6 (rounded-md → rounded-lg
  across all sized variants of the primitive).
- General: Recordings folder card de-monoed and de-chunked; identity
  header, plain-text path bar, single muted stats line, icon-only
  Reindex button in the top-right. Appearance picker rebuilt with
  larger window mocks that read as the top of a real window, single
  sidebar in the System tile, monochrome active state.
- Analysis: filler dictionary regrouped into six plain-English
  categories (Hesitations / Conversational fillers / Softeners /
  Emphasis words / Uncertainty and corrections / Vague references).
  Categories drive the UI; the flat phrase list is derived. Session
  gap moved here as a 15/30/60/120-min dropdown — canonical home,
  unification with the engine still to come.
- Sync (shell only — sign-in flow is a follow-up): ConnectionCard
  rebuilt with six visible states including device-flow code render;
  Pipeline cards (Recordings / Sessions); MappingRoot inline editor
  with DestinationPickerSheet + FieldSourcePickerSheet; SyncActionBar.
- Developer: single Developer card carrying Demo data + DevTools
  toggles. Separate App data card with the Reset action (moved out
  of About).
- About: License first, then Source, Version, Updates. No `v` prefix
  on the version. Auto-checks for updates on mount.
- Switches: row alignment switched to items-center across Developer,
  Indexing, Transcripts, AppData (was items-start).
- Engine: pipeline-enabled gates honoured (no upserts and no soft-
  deletes when off). Session gap read from config at sync time.
- Config: additive migration for `recordingPipelineEnabled`,
  `sessionPipelineEnabled`, `sessionGapThresholdMinutes`. IPC handlers
  added for each.
- Shared: filler-categories.ts as the canonical filler source;
  text-metrics.ts re-exports a flat DEFAULT_FILLER_PHRASES derived
  from it. myme-labels.ts holds plain-English source-field labels
  for the picker sheets.
- AppearancePicker: System tile first; sidebar halved (~17% of the
  window width); content tone softened so it sits between the sidebar
  grey and the tile background; monochrome active state (foreground/40
  border + ring) replaces the accent-blue; subtle wash on all three
  tiles for consistency with the path bar.
- FillerDictionaryCard: title row given a fixed min-height so the
  switch stays vertically centred against the title whether the
  category is collapsed or expanded — no more upward drift on
  expand. Preview line + chip cloud indented by 22px (chevron width +
  gap) so they line up with the title text rather than the chevron.
  Pills rounded-full. "new phrase" placeholder (no ellipsis). Add
  input switched to onKeyDown (Enter saves, Escape cancels) — the
  form wrapper was eating submits. Custom rendered as a regular
  toggleable row when populated.
- RecordingsFolderCard: stats line split — "12k recordings · 279 hours
  of audio" left, "Indexed Xm" right (capital I). Reindex icon button
  stays top-right.
- SessionGapCard: dropdown replaced with a SegmentedTabs picker
  (15m / 30m / 60m / 120m). self-center passed in so it aligns to the
  label block, not the row top.
UI

- ConnectionCard final shape — outlined SyncSplitButton (matches
  CHROME_BUTTON, no shadow, no black face) used across every connected
  sub-state with different labels: Start sync / Sync now / Resume /
  Retry. Internal pipe + chevron-dropdown for secondary actions. No
  more separate primary + overflow controls.
- StatusPill in the header reflects the connection state only —
  "Connected" stays even when the last sync failed; the failure surfaces
  as a muted body caption instead of an orange-bordered panel.
- Identity collapses to two rows: Account ("name (email)") + Endpoint.
- Caption uses the new X-of-Y synced counts: "X of Y recordings synced
  · Z ago" / "Sync cancelled · X of Y synced" / "Sync failed: <reason>
  · X of Y synced".
- Field source picker rebuilt as a two-column grid of compact tiles.
  Each tile carries plain label + canonical ref subtitle. No type tag,
  preview value, or radio icon — selection is conveyed by border + tint.
  Footer flipped: caption left, "Remove this field" right.
- Destination picker rebuilt with the same tile pattern. Default tile
  on its own; custom types in a grid below a search input.
- MappingRoot de-monoed end to end. Plain sans-serif type IDs and
  field names.
- PipelineCard session-gap stepper replaced with a read-only mirror of
  the canonical value on the Analysis tab. Modes section dropped its
  accent-blue affordances for a monochrome treatment.
- Wipe-all-data moved off the Sync tab back to the Developer tab where
  the other testing affordances sit; gets a native confirm prompt.

Toasts

- Sonner-backed toast system. toastError / toastInfo / toastSuccess
  helpers. Failure renders destructive red on a faint red tint;
  success renders accent-green on a faint green tint. Padding tighter
  than the default. Action buttons (Copy logs) outlined rather than
  the heavy black face. App.tsx mounts the Toaster + a transition
  watcher that fires toastError on lastError changes only.
- ESLint no-restricted-imports blocks bare `sonner` imports
  everywhere except the wrapper modules.

Backend

- `MymeStatus.connected` extended with `syncedRecordings`,
  `syncedSessions`, `lastSyncCancelled`. Distinguishes a user-cancelled
  sync (Resume) from a genuine failure (Retry).
- `connectedStatus()` helper centralises status construction across
  the four transitions (initial boot, device-flow success, API-key
  success, post-sync). `buildInitialStatus()` now reads
  `lastFullSyncAt` from engine state on boot instead of hardcoding null.
- SDK + OAuth wiring fixes from the parallel sign-in pass — fresh
  `SafeStorageTokenStorage`, `token-storage.ts`, and DCR scope
  corrections that unblock the device flow.
- SyncActionBar component removed — ConnectionCard owns the sync
  controls.
Picks up the T-218 async bulk_action substrate. The SDK's bulkAction()
signature is unchanged — it polls internally and resolves with the
same BulkActionResult — so the Developer-tab Purge button stops
timing out at 30 seconds without any code change here.

src/main/myme/index.ts:512 already calls client.items.bulkAction({
action: 'purge', confirm: 'PURGE', ... }) for both recordings and
sessions. Both calls now poll under the hood; the previous 30s
ceiling that surfaced this whole work item is gone.

T-218.
Wholesale rename across the SuperWhisper Analytics codebase to match the
Marfa cutover. Substitution pattern: mymehq -> withmarfa, MYME_ -> MARFA_,
Myme -> Marfa, myme -> marfa, @mymehq/* -> @withmarfa/*. Dep pin flipped
from @mymehq/sdk@^5.9.0 to @withmarfa/sdk@^1.0.0; lockfile regenerated
resolves cleanly from npm. Source dir src/main/myme -> src/main/marfa,
state/mymeStore -> state/marfaStore, shared/myme-labels -> shared/marfa-labels,
scripts/myme-* -> scripts/marfa-*. Typecheck passes; all 148 tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant