fix: update test assertion for new key escaping format

The MappingKey::escape_project now replaces / with :: so
'issue:g/p:42' becomes 'issue:g::p:42'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-26 11:03:25 -05:00
parent 0efc09d4bd
commit 5078cb506a
11 changed files with 353 additions and 74 deletions

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@ import reactHooks from "eslint-plugin-react-hooks";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
export default tseslint.config( export default tseslint.config(
{ ignores: ["dist", "src-tauri"] }, { ignores: ["dist", "src-tauri", "src/lib/bindings.ts"] },
{ {
extends: [js.configs.recommended, ...tseslint.configs.recommended], extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"], files: ["**/*.{ts,tsx}"],

View File

@@ -873,7 +873,7 @@ mod tests {
let result = bridge.incremental_sync(&mut map).unwrap(); let result = bridge.incremental_sync(&mut map).unwrap();
assert_eq!(result.created, 1); assert_eq!(result.created, 1);
assert!(map.mappings.contains_key("issue:g/p:42")); assert!(map.mappings.contains_key("issue:g::p:42"));
} }
// -- bv triage command tests -- // -- bv triage command tests --

View File

@@ -137,6 +137,23 @@ export function CommandPalette({
setHighlightedIndex(-1); setHighlightedIndex(-1);
}, [selectableOptions]); }, [selectableOptions]);
const handleOptionSelect = useCallback(
(option: (typeof selectableOptions)[0]) => {
if (option.type === "command") {
// Parse the command and emit filter
if (option.id.startsWith("cmd:type:")) {
onFilter({ type: option.value as FocusItemType });
} else if (option.id.startsWith("cmd:stale:")) {
onFilter({ minAge: parseInt(option.value, 10) });
}
} else {
onSelect(option.value);
}
onClose();
},
[onFilter, onSelect, onClose]
);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
@@ -158,24 +175,7 @@ export function CommandPalette({
} }
} }
}, },
[selectableOptions, highlightedIndex] [selectableOptions, highlightedIndex, handleOptionSelect]
);
const handleOptionSelect = useCallback(
(option: (typeof selectableOptions)[0]) => {
if (option.type === "command") {
// Parse the command and emit filter
if (option.id.startsWith("cmd:type:")) {
onFilter({ type: option.value as FocusItemType });
} else if (option.id.startsWith("cmd:stale:")) {
onFilter({ minAge: parseInt(option.value, 10) });
}
} else {
onSelect(option.value);
}
onClose();
},
[onFilter, onSelect, onClose]
); );
const handleBackdropClick = useCallback( const handleBackdropClick = useCallback(

View File

@@ -9,7 +9,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useCaptureStore } from "@/stores/capture-store"; import { useCaptureStore } from "@/stores/capture-store";
import { quickCapture } from "@/lib/tauri"; import { quickCapture } from "@/lib/tauri";
import { isMcError } from "@/lib/types";
export function QuickCapture(): React.ReactElement | null { export function QuickCapture(): React.ReactElement | null {
const isOpen = useCaptureStore((s) => s.isOpen); const isOpen = useCaptureStore((s) => s.isOpen);
@@ -56,9 +55,15 @@ export function QuickCapture(): React.ReactElement | null {
setSubmitting(true); setSubmitting(true);
try { try {
const result = await quickCapture(trimmed); const result = await quickCapture(trimmed);
captureSuccess(result.bead_id); if (result.status === "error") {
captureError(result.error.message);
} else {
captureSuccess(result.data.bead_id);
}
} catch (err: unknown) { } catch (err: unknown) {
const message = isMcError(err) ? err.message : "Capture failed"; // With the Result pattern, McError comes through result.error (handled above).
// This catch only fires for Tauri-level failures (e.g., IPC unavailable).
const message = err instanceof Error ? err.message : "Capture failed";
captureError(message); captureError(message);
} }
}, [value, setSubmitting, captureSuccess, captureError]); }, [value, setSubmitting, captureSuccess, captureError]);

View File

@@ -12,7 +12,7 @@ import { useCallback } from "react";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { useFocusStore } from "@/stores/focus-store"; import { useFocusStore } from "@/stores/focus-store";
import type { DeferDuration, FocusAction } from "@/lib/types"; import type { DeferDuration } from "@/lib/types";
/** Minimal item shape needed for actions */ /** Minimal item shape needed for actions */
export interface ActionItem { export interface ActionItem {
@@ -128,7 +128,7 @@ export function useActions(): UseActionsReturn {
}); });
// Convert duration to FocusAction format and advance queue // Convert duration to FocusAction format and advance queue
const actionName: FocusAction = `defer_${duration}` as FocusAction; const actionName = `defer_${duration}` as const;
act(actionName, reason ?? undefined); act(actionName, reason ?? undefined);
}, },
[act] [act]

261
src/lib/bindings.ts Normal file
View File

@@ -0,0 +1,261 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
/** user-defined commands **/
export const commands = {
/**
* Simple greeting command for testing IPC
*/
async greet(name: string) : Promise<string> {
return await TAURI_INVOKE("greet", { name });
},
/**
* Get the current status of lore integration by calling the real CLI.
*/
async getLoreStatus() : Promise<Result<LoreStatus, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_lore_status") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Get the current status of the bridge (mapping counts, sync times).
*/
async getBridgeStatus() : Promise<Result<BridgeStatus, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_bridge_status") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Trigger an incremental sync (process since_last_check events).
*/
async syncNow() : Promise<Result<SyncResult, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("sync_now") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Trigger a full reconciliation pass.
*/
async reconcile() : Promise<Result<SyncResult, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("reconcile") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Quick-capture a thought as a new bead.
*/
async quickCapture(title: string) : Promise<Result<CaptureResult, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("quick_capture", { title }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Read persisted frontend state from ~/.local/share/mc/state.json.
*
* Returns null if no state exists (first run).
*/
async readState() : Promise<Result<JsonValue | null, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("read_state") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Write frontend state to ~/.local/share/mc/state.json.
*
* Uses atomic rename pattern to prevent corruption.
*/
async writeState(state: JsonValue) : Promise<Result<null, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("write_state", { state }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Clear persisted frontend state.
*/
async clearState() : Promise<Result<null, McError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("clear_state") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
}
}
/** user-defined events **/
/** user-defined constants **/
/** user-defined types **/
/**
* Bridge status for the frontend
*/
export type BridgeStatus = {
/**
* Total mapped items
*/
mapping_count: number;
/**
* Items with pending bead creation
*/
pending_count: number;
/**
* Items flagged as suspect orphan (first strike)
*/
suspect_count: number;
/**
* Last incremental sync timestamp
*/
last_sync: string | null;
/**
* Last full reconciliation timestamp
*/
last_reconciliation: string | null }
/**
* Response from quick_capture: the bead ID created
*/
export type CaptureResult = { bead_id: string }
export type JsonValue = null | boolean | number | string | JsonValue[] | Partial<{ [key in string]: JsonValue }>
/**
* Lore sync status
*/
export type LoreStatus = { last_sync: string | null; is_healthy: boolean; message: string; summary: LoreSummaryStatus | null }
/**
* Summary counts from lore for the status response
*/
export type LoreSummaryStatus = { open_issues: number; authored_mrs: number; reviewing_mrs: number }
/**
* Structured error type for Tauri IPC commands.
*
* This replaces string-based errors (`Result<T, String>`) with typed errors
* that the frontend can handle programmatically.
*/
export type McError = {
/**
* Machine-readable error code (e.g., "LORE_UNAVAILABLE", "BRIDGE_LOCKED")
*/
code: McErrorCode;
/**
* Human-readable error message
*/
message: string;
/**
* Whether this error is recoverable (user can retry)
*/
recoverable: boolean }
/**
* Error codes for frontend handling
*/
export type McErrorCode = "LORE_UNAVAILABLE" | "LORE_UNHEALTHY" | "LORE_FETCH_FAILED" | "BRIDGE_LOCKED" | "BRIDGE_MAP_CORRUPTED" | "BRIDGE_SYNC_FAILED" | "BEADS_UNAVAILABLE" | "BEADS_CREATE_FAILED" | "BEADS_CLOSE_FAILED" | "BV_UNAVAILABLE" | "BV_TRIAGE_FAILED" | "IO_ERROR" | "INTERNAL_ERROR"
/**
* Result of a sync operation
*/
export type SyncResult = {
/**
* Number of new beads created
*/
created: number;
/**
* Number of existing items skipped (dedup)
*/
skipped: number;
/**
* Number of beads closed (two-strike)
*/
closed: number;
/**
* Number of suspect_orphan flags cleared (item reappeared)
*/
healed: number;
/**
* Errors encountered (non-fatal, processing continued)
*/
errors: string[] }
/** tauri-specta globals **/
import {
invoke as TAURI_INVOKE,
Channel as TAURI_CHANNEL,
} from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
},
);
}

View File

@@ -5,8 +5,9 @@
* instead of browser localStorage. Falls back to localStorage in browser context. * instead of browser localStorage. Falls back to localStorage in browser context.
*/ */
import { invoke } from "@tauri-apps/api/core"; import { readState, writeState, clearState } from "./tauri";
import type { StateStorage } from "zustand/middleware"; import type { StateStorage } from "zustand/middleware";
import type { JsonValue } from "./bindings";
/** /**
* Create a storage adapter that persists to Tauri backend. * Create a storage adapter that persists to Tauri backend.
@@ -17,11 +18,14 @@ export function createTauriStorage(): StateStorage {
return { return {
getItem: async (_name: string): Promise<string | null> => { getItem: async (_name: string): Promise<string | null> => {
try { try {
const state = await invoke<Record<string, unknown> | null>("read_state"); const result = await readState();
if (state === null) { if (result.status === "error") {
throw new Error(result.error.message);
}
if (result.data === null) {
return null; return null;
} }
return JSON.stringify(state); return JSON.stringify(result.data);
} catch (error) { } catch (error) {
console.warn("[tauri-storage] Failed to read state:", error); console.warn("[tauri-storage] Failed to read state:", error);
return null; return null;
@@ -30,8 +34,11 @@ export function createTauriStorage(): StateStorage {
setItem: async (_name: string, value: string): Promise<void> => { setItem: async (_name: string, value: string): Promise<void> => {
try { try {
const state = JSON.parse(value) as Record<string, unknown>; const state = JSON.parse(value) as JsonValue;
await invoke("write_state", { state }); const result = await writeState(state);
if (result.status === "error") {
throw new Error(result.error.message);
}
} catch (error) { } catch (error) {
console.warn("[tauri-storage] Failed to write state:", error); console.warn("[tauri-storage] Failed to write state:", error);
} }
@@ -39,7 +46,10 @@ export function createTauriStorage(): StateStorage {
removeItem: async (_name: string): Promise<void> => { removeItem: async (_name: string): Promise<void> => {
try { try {
await invoke("clear_state"); const result = await clearState();
if (result.status === "error") {
throw new Error(result.error.message);
}
} catch (error) { } catch (error) {
console.warn("[tauri-storage] Failed to clear state:", error); console.warn("[tauri-storage] Failed to clear state:", error);
} }

View File

@@ -1,29 +1,24 @@
/** /**
* Tauri IPC wrapper. * Tauri IPC wrapper.
* *
* Thin layer over @tauri-apps/api invoke that provides typed * Re-exports type-safe commands generated by tauri-specta.
* function signatures for each Rust command. * The generated bindings wrap all fallible commands in Result<T, McError>.
*/ */
import { invoke } from "@tauri-apps/api/core"; import { commands } from "./bindings";
import type { BridgeStatus, CaptureResult, LoreStatus, SyncResult } from "./types";
export async function getLoreStatus(): Promise<LoreStatus> { // Re-export all commands from generated bindings
return invoke<LoreStatus>("get_lore_status"); export const {
} greet,
getLoreStatus,
getBridgeStatus,
syncNow,
reconcile,
quickCapture,
readState,
writeState,
clearState,
} = commands;
export async function getBridgeStatus(): Promise<BridgeStatus> { // Re-export the Result type for consumers
return invoke<BridgeStatus>("get_bridge_status"); export type { Result } from "./bindings";
}
export async function syncNow(): Promise<SyncResult> {
return invoke<SyncResult>("sync_now");
}
export async function reconcile(): Promise<SyncResult> {
return invoke<SyncResult>("reconcile");
}
export async function quickCapture(title: string): Promise<CaptureResult> {
return invoke<CaptureResult>("quick_capture", { title });
}

View File

@@ -50,6 +50,7 @@ export function transformLoreData(data: LoreMeData): FocusItem[] {
updatedAt: mr.updated_at_iso ?? null, updatedAt: mr.updated_at_iso ?? null,
contextQuote: null, contextQuote: null,
requestedBy: mr.author_username ?? null, requestedBy: mr.author_username ?? null,
snoozedUntil: null,
}); });
} }
@@ -65,6 +66,7 @@ export function transformLoreData(data: LoreMeData): FocusItem[] {
updatedAt: issue.updated_at_iso ?? null, updatedAt: issue.updated_at_iso ?? null,
contextQuote: null, contextQuote: null,
requestedBy: null, requestedBy: null,
snoozedUntil: null,
}); });
} }
@@ -80,6 +82,7 @@ export function transformLoreData(data: LoreMeData): FocusItem[] {
updatedAt: mr.updated_at_iso ?? null, updatedAt: mr.updated_at_iso ?? null,
contextQuote: null, contextQuote: null,
requestedBy: null, requestedBy: null,
snoozedUntil: null,
}); });
} }

View File

@@ -39,6 +39,11 @@ export const invoke = vi.fn(async (cmd: string, _args?: unknown) => {
return { created: 0, closed: 0, skipped: 0, healed: 0, errors: [] }; return { created: 0, closed: 0, skipped: 0, healed: 0, errors: [] };
case "quick_capture": case "quick_capture":
return { bead_id: "bd-mock-capture" }; return { bead_id: "bd-mock-capture" };
case "read_state":
return null;
case "write_state":
case "clear_state":
return null;
default: default:
throw new Error(`Mock not implemented for command: ${cmd}`); throw new Error(`Mock not implemented for command: ${cmd}`);
} }