agent: lumotia-rebrand — localStorage keys + event channels migration
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

Phase 7 of the rebrand cascade. Persisted UI state + inter-window event
channels migrated from magnotia to lumotia naming, with one-shot
localStorage key migration so dogfooded UI state survives the rename.

src/lib/utils/localStorageMigration.ts (new):
- migrateLocalStorageKey(old, new): idempotent + crash-safe shim.
  - If new key exists, removes old (lumotia value is authoritative).
  - If only old exists, copies value to new key, removes old.
  - If neither, no-op.
- migrateLocalStorageKeys(pairs): batch wrapper.

src/lib/stores/page.svelte.ts:
- 4 key constants renamed to lumotia_settings / lumotia_profiles /
  lumotia_task_lists / lumotia_templates.
- BroadcastChannel name renamed to lumotia_task_lists.
- migrateLocalStorageKeys() called at module load before any read.

src/lib/stores/focusTimer.svelte.ts:
- STORAGE_KEY renamed to lumotia.focusTimer.v1.
- migrateLocalStorageKey() called at module load.

Event channels (magnotia: -> lumotia:) renamed across frontend + Rust:
- magnotia:toggle-recording (src/routes/+layout.svelte)
- magnotia:hotkey-pressed / -released (src-tauri/src/commands/hotkey.rs +
  consumers)
- magnotia:open-wind-down (src-tauri/src/tray.rs + consumer)
- magnotia:llm-download-progress (src-tauri/src/commands/llm.rs)
- magnotia:preferences-changed (src/lib/stores/preferences.svelte.ts +
  consumers)
- magnotia:start-timer (nudgeBus + dispatch sites)
- magnotia:focus-timer-{complete,cancelled} (focusTimer + nudgeBus)
- magnotia:microstep-generated (nudgeBus + dispatch sites)
- magnotia:step-completed (nudgeBus + dispatch sites)
- magnotia:task-{completed,uncompleted,deleted} (page.svelte.ts +
  nudgeBus + consumers)

Storage-event filters in src/routes/{float,viewer,preview}/+layout@.svelte
updated to filter on lumotia_settings.

User-facing toast strings still say "Magnotia" — deferred to Phase 8
(frontend strings).

npm run check: 0 errors / 0 warnings (3958 files).
cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:10:50 +01:00
parent 14313cfa84
commit 16081095e0
21 changed files with 122 additions and 58 deletions

View File

@@ -77,13 +77,13 @@
}
onMount(() => {
window.addEventListener("magnotia:start-timer", handleStartEvent);
window.addEventListener("lumotia:start-timer", handleStartEvent);
// Rehydrate any in-flight timer that survived a window close.
focusTimer.rehydrate();
});
onDestroy(() => {
window.removeEventListener("magnotia:start-timer", handleStartEvent);
window.removeEventListener("lumotia:start-timer", handleStartEvent);
});
function handleCancel() {

View File

@@ -58,7 +58,7 @@
// is completed in that window, a gentle "still with that one?"
// nudge fires.
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('magnotia:microstep-generated', {
window.dispatchEvent(new CustomEvent('lumotia:microstep-generated', {
detail: { parentTaskId },
}));
}
@@ -78,7 +78,7 @@
// micro-step-idle timer for this parent task — the breakdown
// is demonstrably getting worked, no nudge needed.
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('magnotia:step-completed', {
window.dispatchEvent(new CustomEvent('lumotia:step-completed', {
detail: { id: subtaskId, parentTaskId },
}));
}
@@ -86,7 +86,7 @@
}
function startTimer(subtaskId: string) {
window.dispatchEvent(new CustomEvent('magnotia:start-timer', {
window.dispatchEvent(new CustomEvent('lumotia:start-timer', {
detail: { taskId: subtaskId, seconds: 120 }
}));
}

View File

@@ -67,7 +67,7 @@
function dispatchTriageFinished(mode: 'empty' | 'skipped' | 'picked') {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent('magnotia:morning-triage-finished', {
window.dispatchEvent(new CustomEvent('lumotia:morning-triage-finished', {
detail: { date: todayKey(), mode },
}));
}

View File

@@ -5,7 +5,7 @@
import { ChevronDown, ChevronRight, Timer } from 'lucide-svelte';
function startFocusTimer(task: { id: string; text: string }) {
window.dispatchEvent(new CustomEvent('magnotia:start-timer', {
window.dispatchEvent(new CustomEvent('lumotia:start-timer', {
detail: { taskId: task.id, seconds: 300, label: task.text }
}));
}

View File

@@ -65,7 +65,7 @@
let hotkeyHandler = () => toggleRecording();
onMount(async () => {
window.addEventListener("magnotia:toggle-recording", hotkeyHandler);
window.addEventListener("lumotia:toggle-recording", hotkeyHandler);
if (!tauriRuntimeAvailable) {
error = browserPreviewMessage;
@@ -77,7 +77,7 @@
});
onDestroy(() => {
window.removeEventListener("magnotia:toggle-recording", hotkeyHandler);
window.removeEventListener("lumotia:toggle-recording", hotkeyHandler);
clearInterval(timerInterval);
if (page.recording) {
page.recording = false;

View File

@@ -870,7 +870,7 @@
downloadTotal = event.payload.total_bytes ?? event.payload.total ?? 0;
});
unlistenLlm = await listen("magnotia:llm-download-progress", (event) => {
unlistenLlm = await listen("lumotia:llm-download-progress", (event) => {
llmDownloadProgress = event.payload.percent || 0;
llmDownloadingModel = event.payload.modelId || llmDownloadingModel;
llmDownloadBytes = event.payload.done ?? 0;

View File

@@ -47,10 +47,10 @@ if (typeof window !== "undefined") {
const handler = () => {
refresh().catch(() => {});
};
window.addEventListener("magnotia:task-completed", handler);
window.addEventListener("magnotia:step-completed", handler);
window.addEventListener("magnotia:task-uncompleted", handler);
window.addEventListener("magnotia:task-deleted", handler);
window.addEventListener("lumotia:task-completed", handler);
window.addEventListener("lumotia:step-completed", handler);
window.addEventListener("lumotia:task-uncompleted", handler);
window.addEventListener("lumotia:task-deleted", handler);
window.addEventListener("focus", handler);
if (hasTauriRuntime()) {

View File

@@ -12,9 +12,14 @@
// so closing the window mid-timer still gets you the "done" signal
// on next launch.
const STORAGE_KEY = "magnotia.focusTimer.v1";
import { migrateLocalStorageKey } from "$lib/utils/localStorageMigration";
const STORAGE_KEY = "lumotia.focusTimer.v1";
const TICK_INTERVAL_MS = 250;
// One-shot key rename from the magnotia era. Idempotent.
migrateLocalStorageKey("magnotia.focusTimer.v1", STORAGE_KEY);
export type FocusTimerPersisted = {
startedAt: number;
durationMs: number;
@@ -114,7 +119,7 @@ function createFocusTimerStore() {
writePersisted(null);
stopTick();
if (wasActive && typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("magnotia:focus-timer-cancelled"));
window.dispatchEvent(new CustomEvent("lumotia:focus-timer-cancelled"));
}
}
@@ -172,7 +177,7 @@ function createFocusTimerStore() {
osc.onended = () => ctx.close().catch(() => {});
} catch { /* audio is a nicety; never fatal */ }
window.dispatchEvent(new CustomEvent("magnotia:focus-timer-complete", {
window.dispatchEvent(new CustomEvent("lumotia:focus-timer-complete", {
detail: { taskId, label },
}));
}

View File

@@ -13,7 +13,7 @@ import { page, settings, tasks } from "$lib/stores/page.svelte.js";
import { toasts } from "$lib/stores/toasts.svelte.js";
const TIME_RULE_POLL_MS = 30_000;
const RULES_CHANGED_EVENT = "magnotia:implementation-rules-changed";
const RULES_CHANGED_EVENT = "lumotia:implementation-rules-changed";
export const implementationRules = $state<ImplementationRule[]>([]);
@@ -157,7 +157,7 @@ async function executeAction(action: ImplementationRuleAction): Promise<void> {
return;
}
if (action.kind === "start_timer") {
window.dispatchEvent(new CustomEvent("magnotia:start-timer", {
window.dispatchEvent(new CustomEvent("lumotia:start-timer", {
detail: {
taskId: action.taskId ?? null,
seconds: 300,
@@ -239,8 +239,8 @@ export function startImplementationIntentions(): void {
void loadImplementationRules(true)
.then(() => checkTimeRules())
.catch(() => {});
window.addEventListener("magnotia:task-completed", onTaskCompleted);
window.addEventListener("magnotia:morning-triage-finished", onMorningTriageFinished);
window.addEventListener("lumotia:task-completed", onTaskCompleted);
window.addEventListener("lumotia:morning-triage-finished", onMorningTriageFinished);
window.addEventListener(RULES_CHANGED_EVENT, onRulesChanged);
timePollHandle = setInterval(() => {
void checkTimeRules().catch(() => {});
@@ -250,8 +250,8 @@ export function startImplementationIntentions(): void {
export function stopImplementationIntentions(): void {
if (!started) return;
started = false;
window.removeEventListener("magnotia:task-completed", onTaskCompleted);
window.removeEventListener("magnotia:morning-triage-finished", onMorningTriageFinished);
window.removeEventListener("lumotia:task-completed", onTaskCompleted);
window.removeEventListener("lumotia:morning-triage-finished", onMorningTriageFinished);
window.removeEventListener(RULES_CHANGED_EVENT, onRulesChanged);
if (timePollHandle !== null) {
clearInterval(timePollHandle);

View File

@@ -236,12 +236,12 @@ export function startNudgeBus(): void {
if (typeof window === "undefined") return;
started = true;
window.addEventListener("magnotia:start-timer", onTimerStart);
window.addEventListener("magnotia:focus-timer-complete", resetTimerState);
window.addEventListener("magnotia:focus-timer-cancelled", resetTimerState);
window.addEventListener("magnotia:microstep-generated", onMicroStepGenerated);
window.addEventListener("magnotia:step-completed", onStepOrTaskCompleted);
window.addEventListener("magnotia:task-completed", onStepOrTaskCompleted);
window.addEventListener("lumotia:start-timer", onTimerStart);
window.addEventListener("lumotia:focus-timer-complete", resetTimerState);
window.addEventListener("lumotia:focus-timer-cancelled", resetTimerState);
window.addEventListener("lumotia:microstep-generated", onMicroStepGenerated);
window.addEventListener("lumotia:step-completed", onStepOrTaskCompleted);
window.addEventListener("lumotia:task-completed", onStepOrTaskCompleted);
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
@@ -265,12 +265,12 @@ export function stopNudgeBus(): void {
if (!started) return;
started = false;
window.removeEventListener("magnotia:start-timer", onTimerStart);
window.removeEventListener("magnotia:focus-timer-complete", resetTimerState);
window.removeEventListener("magnotia:focus-timer-cancelled", resetTimerState);
window.removeEventListener("magnotia:microstep-generated", onMicroStepGenerated);
window.removeEventListener("magnotia:step-completed", onStepOrTaskCompleted);
window.removeEventListener("magnotia:task-completed", onStepOrTaskCompleted);
window.removeEventListener("lumotia:start-timer", onTimerStart);
window.removeEventListener("lumotia:focus-timer-complete", resetTimerState);
window.removeEventListener("lumotia:focus-timer-cancelled", resetTimerState);
window.removeEventListener("lumotia:microstep-generated", onMicroStepGenerated);
window.removeEventListener("lumotia:step-completed", onStepOrTaskCompleted);
window.removeEventListener("lumotia:task-completed", onStepOrTaskCompleted);
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);

View File

@@ -36,10 +36,21 @@ export const page = $state<PageState>({
taskSidebarOpen: false,
});
const SETTINGS_KEY = "magnotia_settings";
const PROFILES_KEY = "magnotia_profiles";
const TASK_LISTS_KEY = "magnotia_task_lists";
const TEMPLATES_KEY = "magnotia_templates";
import { migrateLocalStorageKeys } from "$lib/utils/localStorageMigration";
const SETTINGS_KEY = "lumotia_settings";
const PROFILES_KEY = "lumotia_profiles";
const TASK_LISTS_KEY = "lumotia_task_lists";
const TEMPLATES_KEY = "lumotia_templates";
// One-shot key rename from the magnotia era. Idempotent. Runs once at
// module load before any localStorage read below.
migrateLocalStorageKeys([
["lumotia_settings", SETTINGS_KEY],
["magnotia_profiles", PROFILES_KEY],
["magnotia_task_lists", TASK_LISTS_KEY],
["magnotia_templates", TEMPLATES_KEY],
]);
const defaults: SettingsState = {
engine: "whisper",
@@ -459,7 +470,7 @@ export async function deleteTask(id: string) {
tasks.splice(idx, 1);
broadcastTasks();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("magnotia:task-deleted", { detail: { id } }));
window.dispatchEvent(new CustomEvent("lumotia:task-deleted", { detail: { id } }));
}
}
} catch (err) {
@@ -477,7 +488,7 @@ export async function completeTask(id: string) {
// to this to clear micro-step-idle timers for the completed task
// and, later, to drive Phase 7 "after a task completes" rules.
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("magnotia:task-completed", { detail: { id } }));
window.dispatchEvent(new CustomEvent("lumotia:task-completed", { detail: { id } }));
}
} catch (err) {
toasts.error("Couldn't complete task", errorMessage(err));
@@ -491,7 +502,7 @@ export async function uncompleteTask(id: string) {
await invoke("uncomplete_task_cmd", { id });
applyLocalTaskUpdate(id, { done: false, doneAt: null });
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("magnotia:task-uncompleted", { detail: { id } }));
window.dispatchEvent(new CustomEvent("lumotia:task-uncompleted", { detail: { id } }));
}
} catch (err) {
toasts.error("Couldn't uncomplete task", errorMessage(err));
@@ -654,7 +665,7 @@ interface TaskListChannelMessage {
}
const taskListChannel = typeof BroadcastChannel !== "undefined"
? new BroadcastChannel("magnotia_task_lists")
? new BroadcastChannel("lumotia_task_lists")
: null;
if (taskListChannel) {

View File

@@ -6,7 +6,7 @@ import type { AccessibilityPreferences, Preferences } from "$lib/types/app";
import { errorMessage } from "$lib/utils/errors.js";
import { toasts } from "./toasts.svelte.ts";
export const PREFERENCES_CHANGED_EVENT = "magnotia:preferences-changed";
export const PREFERENCES_CHANGED_EVENT = "lumotia:preferences-changed";
type FontFamilies = Record<AccessibilityPreferences["fontFamily"], string>;

View File

@@ -0,0 +1,48 @@
/**
* Phase 7 of the magnotia -> lumotia rebrand cascade. Persisted UI state
* survives the rename via one-shot key migrations run at module-load
* inside the stores that own each key.
*
* `migrateLocalStorageKey` is idempotent and crash-safe:
* - If the new key already exists, the old key (if any) is removed
* silently — the lumotia value is authoritative.
* - If only the old key exists, its value is copied to the new key and
* the old key is removed.
* - If neither exists, this is a no-op.
*/
const STORAGE_NS = "lumotia-rebrand-migration";
export function migrateLocalStorageKey(oldKey: string, newKey: string): void {
if (typeof localStorage === "undefined") return;
try {
const newValue = localStorage.getItem(newKey);
if (newValue !== null) {
if (localStorage.getItem(oldKey) !== null) {
localStorage.removeItem(oldKey);
}
return;
}
const oldValue = localStorage.getItem(oldKey);
if (oldValue === null) return;
localStorage.setItem(newKey, oldValue);
localStorage.removeItem(oldKey);
} catch (err) {
// Quota / disabled-storage / DOMException — silently no-op. The store
// will fall back to the old key on next read (or fail there with a
// visible error). Logging is intentionally silent to avoid polluting
// dev-console output on every page in the multi-window flow.
void err;
}
}
/**
* Run a batch of migrations. Used by stores that own multiple keys.
*/
export function migrateLocalStorageKeys(pairs: Array<[string, string]>): void {
for (const [oldKey, newKey] of pairs) {
migrateLocalStorageKey(oldKey, newKey);
}
}
void STORAGE_NS;

View File

@@ -1,7 +1,7 @@
/**
* Forward-compatible, versioned settings migration.
*
* Historically, `localStorage["magnotia_settings"]` was a bare JSON object
* Historically, `localStorage["lumotia_settings"]` was a bare JSON object
* (whatever `SettingsState` looked like at write time). That shape
* tolerates new fields cleanly — spread over `defaults` — but does not
* survive:

View File

@@ -138,7 +138,7 @@
await mod.register(hotkey, () => {
if (page.current !== "dictation") page.current = "dictation";
requestAnimationFrame(() => {
window.dispatchEvent(new CustomEvent("magnotia:toggle-recording"));
window.dispatchEvent(new CustomEvent("lumotia:toggle-recording"));
});
});
registeredHotkey = hotkey;
@@ -174,13 +174,13 @@
async function setupEvdevListener() {
if (!tauriRuntimeAvailable) return;
const { listen } = await import("@tauri-apps/api/event");
unlistenEvdev = await listen("magnotia:hotkey-pressed", () => {
unlistenEvdev = await listen("lumotia:hotkey-pressed", () => {
const now = Date.now();
if (now - lastHotkeyAtMs < HOTKEY_DEBOUNCE_MS) return;
lastHotkeyAtMs = now;
if (page.current !== "dictation") page.current = "dictation";
requestAnimationFrame(() => {
window.dispatchEvent(new CustomEvent("magnotia:toggle-recording"));
window.dispatchEvent(new CustomEvent("lumotia:toggle-recording"));
});
});
}
@@ -248,7 +248,7 @@
let unlistenWindDown = null;
async function setupWindDownListener() {
if (!tauriRuntimeAvailable) return;
unlistenWindDown = await listen("magnotia:open-wind-down", () => {
unlistenWindDown = await listen("lumotia:open-wind-down", () => {
page.current = "shutdown";
});
}

View File

@@ -35,7 +35,7 @@
// Listen for settings changes from main window
if (typeof window !== "undefined") {
window.addEventListener("storage", (e) => {
if (e.key === "magnotia_settings" && e.newValue) {
if (e.key === "lumotia_settings" && e.newValue) {
try {
Object.assign(settings, JSON.parse(e.newValue));
} catch {}

View File

@@ -28,7 +28,7 @@
if (typeof window !== "undefined") {
window.addEventListener("storage", (event) => {
if (event.key === "magnotia_settings" && event.newValue) {
if (event.key === "lumotia_settings" && event.newValue) {
try { Object.assign(settings, JSON.parse(event.newValue)); } catch {}
}
});

View File

@@ -32,7 +32,7 @@
// Sync settings from main window
if (typeof window !== "undefined") {
window.addEventListener("storage", (e) => {
if (e.key === "magnotia_settings" && e.newValue) {
if (e.key === "lumotia_settings" && e.newValue) {
try { Object.assign(settings, JSON.parse(e.newValue)); } catch {}
}
});