Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 169 additions & 158 deletions apps/desktop/src/renderer/App.tsx

Large diffs are not rendered by default.

42 changes: 38 additions & 4 deletions apps/desktop/src/renderer/Panels.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { Finding, Operation } from "@open-wiki/access";
import type { PageSource, SourceLocation } from "../main/sources.js";
import { bridge } from "./bridge.js";
Expand Down Expand Up @@ -88,14 +88,48 @@ export function PageSources({
* where the person reading the problem is, and never to invent advice of its
* own.
*/
export function Findings({ reloadKey }: { reloadKey: number }): React.JSX.Element {
export function Findings({
reloadKey,
onCount,
}: {
reloadKey: number;
/**
* How many there were, for the status bar (spec `desktop-shell`, R5.2).
*
* Handed up from this one load rather than counted again: `ow check` walks
* the whole project, and running it a second time to fill in a number in the
* frame is how a status bar becomes the slowest thing in the window.
*/
onCount?: (count: number) => void;
}): React.JSX.Element {
const [findings, setFindings] = useState<Finding[] | null>(null);
// Read through a ref so the effect below depends on `reloadKey` alone. A
// caller passing a fresh closure each render would otherwise re-run the
// whole check on every render.
const report = useRef(onCount);
report.current = onCount;

useEffect(() => {
// Guarded, like `PageSources` above and for a sharper reason: two
// `reloadKey` bumps can overlap, and a slow answer for the older one
// arriving last would put a stale count in the status bar as well as stale
// findings on screen.
let live = true;
void bridge()
.findings()
.then(setFindings)
.catch(() => setFindings([]));
.then((found) => {
if (!live) return;
setFindings(found);
report.current?.(found.length);
})
.catch(() => {
if (!live) return;
setFindings([]);
report.current?.(0);
});
return () => {
live = false;
};
}, [reloadKey]);

if (!findings) return <p className="empty">Checking…</p>;
Expand Down
68 changes: 68 additions & 0 deletions apps/desktop/src/renderer/Rail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { BookText, CircleCheck, Globe, Layers } from "lucide-react";
import type { Pane } from "./navigation.js";
import { ICON_MD, type Icon } from "./ui/icons.js";

/**
* The icon rail (spec `desktop-shell`, R4).
*
* **Every pane the window has, all of them visible at once.** A rail is not a
* menu: the point is that moving between the wiki and its sources costs one
* click and no memory of where the thing was, which is what makes it bearable
* to keep checking one against the other.
*
* MCP is absent, and that is the honest state rather than an omission — its
* pane is waiting on a server nobody has built (`specs/mcp-pane/`), and a rail
* entry leading to a pane that can say nothing is worse than no entry.
*/
export interface RailPane {
pane: Pane;
label: string;
icon: Icon;
}

/**
* In the order the draft draws them: what you read, what it rests on, what is
* wrong with it. That is also the order a page is written in.
*/
export const PANES: readonly RailPane[] = [
{ pane: "wiki", label: "Wiki", icon: BookText },
{ pane: "sources", label: "Sources", icon: Layers },
{ pane: "checks", label: "Checks", icon: CircleCheck },
];

export interface RailProps {
current: Pane;
onGoTo: (pane: Pane) => void;
/** The project's content language, as its code — `en`, `pt`, `es` (8.12). */
language: string;
}

export function Rail({ current, onGoTo, language }: RailProps): React.JSX.Element {
return (
<nav className="rail" role="tablist" aria-label="Panes">
{PANES.map(({ pane, label, icon: IconGlyph }) => (
<button
key={pane}
type="button"
role="tab"
className="rail-btn"
// The selected pane is marked twice over: the accent, and a bar down
// its left edge. Colour alone is not a state anyone can rely on.
aria-selected={current === pane}
onClick={() => onGoTo(pane)}
>
<IconGlyph size={ICON_MD} aria-hidden />
{label}
</button>
))}
<span className="rail-spacer" />
{/* Shown, not offered. The language is a project setting (`ow.json`), and
the sheet is where it is changed — this is the reminder that the agent
was told to write in it. */}
<span className="rail-btn rail-btn--static" title={`Content language: ${language}`}>
<Globe size={ICON_MD} aria-hidden />
{language.toUpperCase()}
</span>
</nav>
);
}
58 changes: 58 additions & 0 deletions apps/desktop/src/renderer/StatusBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* The status bar (spec `desktop-shell`, R5).
*
* Three facts, and each is a fact somebody needs without asking for it: which
* directory this window is actually looking at, whether the checks found
* anything, and a way back from the last write.
*
* **The findings count is handed in, never computed here.** `ow check` walks
* the whole project, and running it to fill a number in the frame is how a
* status bar becomes the slowest thing in the window. Until the checks pane has
* loaded once there is no count, and the bar says that rather than showing a
* confident zero (R5.2) — "no findings" and "not looked yet" are not the same
* sentence, and the second one dressed as the first is the more dangerous of
* the two.
*/
export interface StatusBarProps {
/** The project directory, to be read off the screen and typed into a shell. */
root: string;
/** How many findings the checks last reported; null until they have run. */
findings: number | null;
onGoToChecks: () => void;
/** Undoing the last recorded write, or null when there is none to undo. */
onUndo: (() => void) | null;
}

export function StatusBar({
root,
findings,
onGoToChecks,
onUndo,
}: StatusBarProps): React.JSX.Element {
return (
<footer className="statusbar">
<span className="statusbar__path">{root}</span>
<span className="chrome__spacer" />

{findings === null ? (
<span>not checked yet</span>
) : (
<button type="button" className="statusbar__button" onClick={onGoToChecks}>
{findings === 0
? "no findings"
: `${findings} ${findings === 1 ? "finding" : "findings"}`}
</button>
)}

{onUndo ? (
<button type="button" className="statusbar__button" onClick={onUndo}>
Undo last write
</button>
) : (
// Said rather than offered (R5.5). A disabled button invites a click
// and then explains nothing; this explains and invites nothing.
<span>nothing to undo</span>
)}
</footer>
);
}
77 changes: 77 additions & 0 deletions apps/desktop/src/renderer/Titlebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ArrowLeft, ArrowRight, Mic, Pause, Play, Settings2, Square } from "lucide-react";
import { RecordingIndicator } from "./RecordingIndicator.js";
import type { Recording } from "./recording.js";
import { Button } from "./ui/Button.js";
import { IconButton } from "./ui/IconButton.js";

/**
* The titlebar (spec `desktop-shell`, R3).
*
* **What is here is what is true regardless of which pane is open**, and that
* is the whole rule for what belongs: which project you are in, and whether you
* are being recorded. Both are questions whose answer must never require
* looking somewhere — and the recording one especially, because this
* application captures other people's conversation and somebody who forgets it
* is running has a recording of a meeting the room thinks ended.
*
* The pause and stop controls sit beside the indicator rather than on the
* sources pane for the same reason: the moment you need them is the moment you
* are looking at something else.
*/
export interface TitlebarProps {
project: string;
recording: Recording;
onRecord: (action: "start" | "pause" | "resume" | "stop") => void;
onSettings: () => void;
onBack: () => void;
onForward: () => void;
canGoBack: boolean;
canGoForward: boolean;
}

export function Titlebar({
project,
recording,
onRecord,
onSettings,
onBack,
onForward,
canGoBack,
canGoForward,
}: TitlebarProps): React.JSX.Element {
const running = recording.state !== "idle";
return (
<header className="titlebar">
{/* **Not in the draft, and here anyway.** The draft's titlebar has no
Back, because it draws a wiki you move around with the tree. But a
wiki is also read by following a link and returning, and the spec's
own purpose says so — a shell with no way to return has removed half
of how the content is used, quietly, while every screenshot still
looks right. */}
<IconButton icon={ArrowLeft} label="Back" disabled={!canGoBack} onClick={onBack} />
<IconButton icon={ArrowRight} label="Forward" disabled={!canGoForward} onClick={onForward} />

<span className="chrome__project">{project}</span>
<span className="chrome__spacer" />

<RecordingIndicator recording={recording} />

{running ? (
<>
{recording.state === "paused" ? (
<IconButton icon={Play} label="Resume recording" onClick={() => onRecord("resume")} />
) : (
<IconButton icon={Pause} label="Pause recording" onClick={() => onRecord("pause")} />
)}
<IconButton icon={Square} label="Stop recording" onClick={() => onRecord("stop")} />
</>
) : (
<Button size="sm" icon={Mic} onClick={() => onRecord("start")}>
Record
</Button>
)}

<IconButton icon={Settings2} label="Settings" onClick={onSettings} />
</header>
);
}
Loading
Loading