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:
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ import reactHooks from "eslint-plugin-react-hooks";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist", "src-tauri"] },
|
||||
{ ignores: ["dist", "src-tauri", "src/lib/bindings.ts"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
|
||||
@@ -873,7 +873,7 @@ mod tests {
|
||||
let result = bridge.incremental_sync(&mut map).unwrap();
|
||||
|
||||
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 --
|
||||
|
||||
@@ -137,6 +137,23 @@ export function CommandPalette({
|
||||
setHighlightedIndex(-1);
|
||||
}, [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(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
@@ -158,24 +175,7 @@ export function CommandPalette({
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectableOptions, highlightedIndex]
|
||||
);
|
||||
|
||||
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]
|
||||
[selectableOptions, highlightedIndex, handleOptionSelect]
|
||||
);
|
||||
|
||||
const handleBackdropClick = useCallback(
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useCaptureStore } from "@/stores/capture-store";
|
||||
import { quickCapture } from "@/lib/tauri";
|
||||
import { isMcError } from "@/lib/types";
|
||||
|
||||
export function QuickCapture(): React.ReactElement | null {
|
||||
const isOpen = useCaptureStore((s) => s.isOpen);
|
||||
@@ -56,9 +55,15 @@ export function QuickCapture(): React.ReactElement | null {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}, [value, setSubmitting, captureSuccess, captureError]);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useCallback } from "react";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
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 */
|
||||
export interface ActionItem {
|
||||
@@ -128,7 +128,7 @@ export function useActions(): UseActionsReturn {
|
||||
});
|
||||
|
||||
// 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]
|
||||
|
||||
261
src/lib/bindings.ts
Normal file
261
src/lib/bindings.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
* 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 { JsonValue } from "./bindings";
|
||||
|
||||
/**
|
||||
* Create a storage adapter that persists to Tauri backend.
|
||||
@@ -17,11 +18,14 @@ export function createTauriStorage(): StateStorage {
|
||||
return {
|
||||
getItem: async (_name: string): Promise<string | null> => {
|
||||
try {
|
||||
const state = await invoke<Record<string, unknown> | null>("read_state");
|
||||
if (state === null) {
|
||||
const result = await readState();
|
||||
if (result.status === "error") {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
if (result.data === null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.stringify(state);
|
||||
return JSON.stringify(result.data);
|
||||
} catch (error) {
|
||||
console.warn("[tauri-storage] Failed to read state:", error);
|
||||
return null;
|
||||
@@ -30,8 +34,11 @@ export function createTauriStorage(): StateStorage {
|
||||
|
||||
setItem: async (_name: string, value: string): Promise<void> => {
|
||||
try {
|
||||
const state = JSON.parse(value) as Record<string, unknown>;
|
||||
await invoke("write_state", { state });
|
||||
const state = JSON.parse(value) as JsonValue;
|
||||
const result = await writeState(state);
|
||||
if (result.status === "error") {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[tauri-storage] Failed to write state:", error);
|
||||
}
|
||||
@@ -39,7 +46,10 @@ export function createTauriStorage(): StateStorage {
|
||||
|
||||
removeItem: async (_name: string): Promise<void> => {
|
||||
try {
|
||||
await invoke("clear_state");
|
||||
const result = await clearState();
|
||||
if (result.status === "error") {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[tauri-storage] Failed to clear state:", error);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
/**
|
||||
* Tauri IPC wrapper.
|
||||
*
|
||||
* Thin layer over @tauri-apps/api invoke that provides typed
|
||||
* function signatures for each Rust command.
|
||||
* Re-exports type-safe commands generated by tauri-specta.
|
||||
* The generated bindings wrap all fallible commands in Result<T, McError>.
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { BridgeStatus, CaptureResult, LoreStatus, SyncResult } from "./types";
|
||||
import { commands } from "./bindings";
|
||||
|
||||
export async function getLoreStatus(): Promise<LoreStatus> {
|
||||
return invoke<LoreStatus>("get_lore_status");
|
||||
}
|
||||
// Re-export all commands from generated bindings
|
||||
export const {
|
||||
greet,
|
||||
getLoreStatus,
|
||||
getBridgeStatus,
|
||||
syncNow,
|
||||
reconcile,
|
||||
quickCapture,
|
||||
readState,
|
||||
writeState,
|
||||
clearState,
|
||||
} = commands;
|
||||
|
||||
export async function getBridgeStatus(): Promise<BridgeStatus> {
|
||||
return invoke<BridgeStatus>("get_bridge_status");
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
// Re-export the Result type for consumers
|
||||
export type { Result } from "./bindings";
|
||||
|
||||
@@ -50,6 +50,7 @@ export function transformLoreData(data: LoreMeData): FocusItem[] {
|
||||
updatedAt: mr.updated_at_iso ?? null,
|
||||
contextQuote: null,
|
||||
requestedBy: mr.author_username ?? null,
|
||||
snoozedUntil: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ export function transformLoreData(data: LoreMeData): FocusItem[] {
|
||||
updatedAt: issue.updated_at_iso ?? null,
|
||||
contextQuote: null,
|
||||
requestedBy: null,
|
||||
snoozedUntil: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,6 +82,7 @@ export function transformLoreData(data: LoreMeData): FocusItem[] {
|
||||
updatedAt: mr.updated_at_iso ?? null,
|
||||
contextQuote: null,
|
||||
requestedBy: null,
|
||||
snoozedUntil: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ export const invoke = vi.fn(async (cmd: string, _args?: unknown) => {
|
||||
return { created: 0, closed: 0, skipped: 0, healed: 0, errors: [] };
|
||||
case "quick_capture":
|
||||
return { bead_id: "bd-mock-capture" };
|
||||
case "read_state":
|
||||
return null;
|
||||
case "write_state":
|
||||
case "clear_state":
|
||||
return null;
|
||||
default:
|
||||
throw new Error(`Mock not implemented for command: ${cmd}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user