feat: add frontend TypeScript types and IPC layer
Establishes type-safe communication between React frontend and Rust backend. Types mirror the Rust structs to ensure consistency across the Tauri boundary. Type definitions (src/lib/types.ts): - LoreStatus, LoreSummaryStatus: Lore CLI response shapes - BridgeStatus, SyncResult: Bridge operation results - McError, McErrorCode: Structured error handling with type guard - FocusItem, FocusItemType: THE ONE THING work items - FocusAction, DecisionEntry: User action tracking - Staleness computation: fresh/normal/amber/urgent based on age IPC wrapper (src/lib/tauri.ts): - Typed invoke() calls for each Rust command - getLoreStatus, getBridgeStatus, syncNow, reconcile, quickCapture Data transformation (src/lib/transform.ts): - transformLoreData: Converts lore response to FocusItem[] - Priority ordering: reviews first (blocking others), issues, authored MRs - Generates stable IDs matching bridge mapping keys Formatting utilities (src/lib/format.ts): - formatIid: Prefix with ! for MRs, # for issues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
17
src/lib/format.ts
Normal file
17
src/lib/format.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Shared formatting utilities for Mission Control.
|
||||||
|
*
|
||||||
|
* Centralizes formatting logic to avoid duplication across components.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FocusItemType } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an IID with the appropriate prefix for its type.
|
||||||
|
*
|
||||||
|
* MRs use ! prefix, issues use # prefix.
|
||||||
|
*/
|
||||||
|
export function formatIid(type: FocusItemType, iid: number): string {
|
||||||
|
if (type === "mr_review" || type === "mr_authored") return `!${iid}`;
|
||||||
|
return `#${iid}`;
|
||||||
|
}
|
||||||
29
src/lib/tauri.ts
Normal file
29
src/lib/tauri.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Tauri IPC wrapper.
|
||||||
|
*
|
||||||
|
* Thin layer over @tauri-apps/api invoke that provides typed
|
||||||
|
* function signatures for each Rust command.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { BridgeStatus, CaptureResult, LoreStatus, SyncResult } from "./types";
|
||||||
|
|
||||||
|
export async function getLoreStatus(): Promise<LoreStatus> {
|
||||||
|
return invoke<LoreStatus>("get_lore_status");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
87
src/lib/transform.ts
Normal file
87
src/lib/transform.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Transform lore backend data into FocusItem[] for the UI.
|
||||||
|
*
|
||||||
|
* Converts LoreStatus (issues, MRs) into a flat list of FocusItems
|
||||||
|
* sorted by priority: reviews first (blocking others), then issues,
|
||||||
|
* then authored MRs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FocusItem } from "./types";
|
||||||
|
|
||||||
|
/** Shape of lore issue from the backend */
|
||||||
|
interface LoreIssue {
|
||||||
|
iid: number;
|
||||||
|
title: string;
|
||||||
|
project: string;
|
||||||
|
web_url: string;
|
||||||
|
updated_at_iso?: string | null;
|
||||||
|
attention_state?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shape of lore MR from the backend */
|
||||||
|
interface LoreMr {
|
||||||
|
iid: number;
|
||||||
|
title: string;
|
||||||
|
project: string;
|
||||||
|
web_url: string;
|
||||||
|
updated_at_iso?: string | null;
|
||||||
|
attention_state?: string | null;
|
||||||
|
author_username?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoreMeData {
|
||||||
|
open_issues: LoreIssue[];
|
||||||
|
open_mrs_authored: LoreMr[];
|
||||||
|
reviewing_mrs: LoreMr[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformLoreData(data: LoreMeData): FocusItem[] {
|
||||||
|
const items: FocusItem[] = [];
|
||||||
|
|
||||||
|
// Reviews first (you're blocking someone)
|
||||||
|
for (const mr of data.reviewing_mrs) {
|
||||||
|
items.push({
|
||||||
|
id: `mr_review:${mr.project}:${mr.iid}`,
|
||||||
|
title: mr.title,
|
||||||
|
type: "mr_review",
|
||||||
|
project: mr.project,
|
||||||
|
url: mr.web_url,
|
||||||
|
iid: mr.iid,
|
||||||
|
updatedAt: mr.updated_at_iso ?? null,
|
||||||
|
contextQuote: null,
|
||||||
|
requestedBy: mr.author_username ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assigned issues
|
||||||
|
for (const issue of data.open_issues) {
|
||||||
|
items.push({
|
||||||
|
id: `issue:${issue.project}:${issue.iid}`,
|
||||||
|
title: issue.title,
|
||||||
|
type: "issue",
|
||||||
|
project: issue.project,
|
||||||
|
url: issue.web_url,
|
||||||
|
iid: issue.iid,
|
||||||
|
updatedAt: issue.updated_at_iso ?? null,
|
||||||
|
contextQuote: null,
|
||||||
|
requestedBy: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authored MRs last (your own work, less urgent)
|
||||||
|
for (const mr of data.open_mrs_authored) {
|
||||||
|
items.push({
|
||||||
|
id: `mr_authored:${mr.project}:${mr.iid}`,
|
||||||
|
title: mr.title,
|
||||||
|
type: "mr_authored",
|
||||||
|
project: mr.project,
|
||||||
|
url: mr.web_url,
|
||||||
|
iid: mr.iid,
|
||||||
|
updatedAt: mr.updated_at_iso ?? null,
|
||||||
|
contextQuote: null,
|
||||||
|
requestedBy: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
132
src/lib/types.ts
Normal file
132
src/lib/types.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* TypeScript types mirroring the Rust backend data structures.
|
||||||
|
*
|
||||||
|
* These are used by the IPC layer and components to maintain
|
||||||
|
* type safety across the Tauri boundary.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// -- Backend response types (match Rust structs in commands/mod.rs) --
|
||||||
|
|
||||||
|
export interface LoreStatus {
|
||||||
|
last_sync: string | null;
|
||||||
|
is_healthy: boolean;
|
||||||
|
message: string;
|
||||||
|
summary: LoreSummaryStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoreSummaryStatus {
|
||||||
|
open_issues: number;
|
||||||
|
authored_mrs: number;
|
||||||
|
reviewing_mrs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BridgeStatus {
|
||||||
|
mapping_count: number;
|
||||||
|
pending_count: number;
|
||||||
|
suspect_count: number;
|
||||||
|
last_sync: string | null;
|
||||||
|
last_reconciliation: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncResult {
|
||||||
|
created: number;
|
||||||
|
closed: number;
|
||||||
|
skipped: number;
|
||||||
|
/** Number of suspect_orphan flags cleared (item reappeared) */
|
||||||
|
healed: number;
|
||||||
|
/** Error messages from non-fatal errors during sync */
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Structured error types (match Rust error.rs) --
|
||||||
|
|
||||||
|
/** Error codes for programmatic 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"
|
||||||
|
| "IO_ERROR"
|
||||||
|
| "INTERNAL_ERROR";
|
||||||
|
|
||||||
|
/** Structured error from Tauri IPC commands */
|
||||||
|
export interface McError {
|
||||||
|
code: McErrorCode;
|
||||||
|
message: string;
|
||||||
|
recoverable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type guard to check if an error is a structured McError */
|
||||||
|
export function isMcError(err: unknown): err is McError {
|
||||||
|
return (
|
||||||
|
typeof err === "object" &&
|
||||||
|
err !== null &&
|
||||||
|
"code" in err &&
|
||||||
|
"message" in err &&
|
||||||
|
"recoverable" in err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result from the quick_capture command */
|
||||||
|
export interface CaptureResult {
|
||||||
|
bead_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Frontend-only types --
|
||||||
|
|
||||||
|
/** The type of work item surfaced in the Focus View */
|
||||||
|
export type FocusItemType = "mr_review" | "issue" | "mr_authored" | "manual";
|
||||||
|
|
||||||
|
/** A single work item that can be THE ONE THING */
|
||||||
|
export interface FocusItem {
|
||||||
|
/** Unique key matching bridge mapping (e.g., "mr_review:g/p:847") */
|
||||||
|
id: string;
|
||||||
|
/** Human-readable title */
|
||||||
|
title: string;
|
||||||
|
/** Type badge to display */
|
||||||
|
type: FocusItemType;
|
||||||
|
/** Project path (e.g., "platform/core") */
|
||||||
|
project: string;
|
||||||
|
/** URL to open in browser (GitLab link) */
|
||||||
|
url: string;
|
||||||
|
/** Entity IID (e.g., MR !847, Issue #42) */
|
||||||
|
iid: number;
|
||||||
|
/** ISO timestamp of last update */
|
||||||
|
updatedAt: string | null;
|
||||||
|
/** Optional context quote (e.g., reviewer comment) */
|
||||||
|
contextQuote: string | null;
|
||||||
|
/** Who is requesting attention */
|
||||||
|
requestedBy: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Action the user takes on a focused item */
|
||||||
|
export type FocusAction = "start" | "defer_1h" | "defer_tomorrow" | "skip";
|
||||||
|
|
||||||
|
/** An entry in the decision log */
|
||||||
|
export interface DecisionEntry {
|
||||||
|
timestamp: string;
|
||||||
|
action: FocusAction;
|
||||||
|
itemId: string;
|
||||||
|
reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Staleness level derived from item age */
|
||||||
|
export type Staleness = "fresh" | "normal" | "amber" | "urgent";
|
||||||
|
|
||||||
|
/** Compute staleness from an ISO timestamp */
|
||||||
|
export function computeStaleness(updatedAt: string | null): Staleness {
|
||||||
|
if (!updatedAt) return "normal";
|
||||||
|
|
||||||
|
const ageMs = Date.now() - new Date(updatedAt).getTime();
|
||||||
|
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
if (ageDays < 1) return "fresh";
|
||||||
|
if (ageDays < 3) return "normal";
|
||||||
|
if (ageDays < 7) return "amber";
|
||||||
|
return "urgent";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user