Files
Triple-C/app/src/viewer/ViewerApp.tsx
T
jknappandClaude Opus 5.5 8305c96e20
Build App / compute-version (push) Successful in 7s
Secret Scan / scan (push) Successful in 8s
Build App / build-macos (push) Successful in 2m53s
Build App / build-linux (push) Successful in 5m12s
Build App / build-windows (push) Successful in 5m15s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 1m5s
Terminal file viewer/editor + per-window app-command lockdown (#60)
Clicking a file path in Claude's terminal output now opens the file in its own window with a CodeMirror 6 editor. The editor highlights the target line, live-reloads while the file changes, and saves explicitly with hash-based conflict detection. The viewer commands are gated by window label.

Every app command is now ACL-gated per window through a Tauri AppManifest. build.rs checks the handler list against the capability files and fails the build on any mismatch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 17:05:50 +00:00

55 lines
2.0 KiB
TypeScript

import { useEffect, useState } from "react";
import Button from "../components/ui/Button";
import { viewerChooseFile, viewerGetState } from "../lib/tauri-commands";
import type { ViewerState } from "../lib/types";
import EditorPane from "./EditorPane";
const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e));
export default function ViewerApp() {
const [state, setState] = useState<ViewerState | { error: string } | null>(null);
const [chooseError, setChooseError] = useState<string | null>(null);
useEffect(() => {
viewerGetState().then(setState, (e) => setState({ error: errorText(e) }));
}, []);
if (state === null) return <p className="p-4 text-sm text-[var(--text-secondary)]">Loading</p>;
if ("error" in state) return <p className="p-4 text-sm">{state.error}</p>;
const choose = (index: number) => {
setChooseError(null);
viewerChooseFile(index).then(setState, (e) => setChooseError(errorText(e)));
};
switch (state.state.kind) {
case "resolved":
return <EditorPane state={state} />;
case "not_found":
return (
<div className="p-4 text-sm">
<p>Could not find <span className="font-mono">{state.raw_path}</span> in the container. Looked in:</p>
<ul className="mt-2 list-disc pl-6 font-mono text-xs text-[var(--text-secondary)]">
{state.state.tried.map((p) => <li key={p}>{p}</li>)}
</ul>
</div>
);
case "choose":
return (
<div className="p-4 text-sm">
<p>Several files match <span className="font-mono">{state.raw_path}</span>. Open which?</p>
<ul className="mt-2 flex flex-col items-start gap-1">
{state.state.candidates.map((p, i) => (
<li key={p}>
<Button size="sm" onClick={() => choose(i)}>
<span className="font-mono">{p}</span>
</Button>
</li>
))}
</ul>
{chooseError && <p role="alert" className="mt-2">{chooseError}</p>}
</div>
);
}
}