@archastro/astroshot-review 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/bin/astroshot-review.mjs +17 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +240 -0
- package/dist/data/friction.d.ts +22 -0
- package/dist/data/friction.js +278 -0
- package/dist/data/hash-cache.d.ts +17 -0
- package/dist/data/hash-cache.js +51 -0
- package/dist/data/index-cache.d.ts +22 -0
- package/dist/data/index-cache.js +64 -0
- package/dist/data/manifest.d.ts +41 -0
- package/dist/data/manifest.js +105 -0
- package/dist/data/model.d.ts +96 -0
- package/dist/data/model.js +1 -0
- package/dist/data/paths.d.ts +27 -0
- package/dist/data/paths.js +98 -0
- package/dist/data/review-store.d.ts +67 -0
- package/dist/data/review-store.js +237 -0
- package/dist/data/scan.d.ts +32 -0
- package/dist/data/scan.js +227 -0
- package/dist/data/store.d.ts +92 -0
- package/dist/data/store.js +408 -0
- package/dist/data/watcher.d.ts +37 -0
- package/dist/data/watcher.js +126 -0
- package/dist/images/halfblocks.d.ts +10 -0
- package/dist/images/halfblocks.js +21 -0
- package/dist/images/png.d.ts +14 -0
- package/dist/images/png.js +45 -0
- package/dist/images/scale.d.ts +31 -0
- package/dist/images/scale.js +102 -0
- package/dist/images/service.d.ts +54 -0
- package/dist/images/service.js +163 -0
- package/dist/images/worker.d.ts +21 -0
- package/dist/images/worker.js +30 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/terminal/graphics-stdout.d.ts +9 -0
- package/dist/terminal/graphics-stdout.js +36 -0
- package/dist/terminal/herdr.d.ts +61 -0
- package/dist/terminal/herdr.js +327 -0
- package/dist/terminal/image-layer.d.ts +122 -0
- package/dist/terminal/image-layer.js +471 -0
- package/dist/terminal/kitty.d.ts +74 -0
- package/dist/terminal/kitty.js +112 -0
- package/dist/terminal/probe.d.ts +49 -0
- package/dist/terminal/probe.js +206 -0
- package/dist/ui/app.d.ts +6 -0
- package/dist/ui/app.js +695 -0
- package/dist/ui/chrome.d.ts +53 -0
- package/dist/ui/chrome.js +69 -0
- package/dist/ui/context.d.ts +16 -0
- package/dist/ui/context.js +8 -0
- package/dist/ui/detail.d.ts +33 -0
- package/dist/ui/detail.js +39 -0
- package/dist/ui/friction.d.ts +42 -0
- package/dist/ui/friction.js +84 -0
- package/dist/ui/help.d.ts +4 -0
- package/dist/ui/help.js +61 -0
- package/dist/ui/hooks.d.ts +9 -0
- package/dist/ui/hooks.js +32 -0
- package/dist/ui/movie-player.d.ts +29 -0
- package/dist/ui/movie-player.js +119 -0
- package/dist/ui/picture.d.ts +19 -0
- package/dist/ui/picture.js +100 -0
- package/dist/ui/selectors.d.ts +26 -0
- package/dist/ui/selectors.js +69 -0
- package/dist/ui/settings.d.ts +5 -0
- package/dist/ui/settings.js +17 -0
- package/dist/ui/stream.d.ts +38 -0
- package/dist/ui/stream.js +118 -0
- package/dist/ui/system.d.ts +4 -0
- package/dist/ui/system.js +34 -0
- package/dist/ui/takeover.d.ts +26 -0
- package/dist/ui/takeover.js +29 -0
- package/dist/ui/text-input.d.ts +8 -0
- package/dist/ui/text-input.js +74 -0
- package/dist/ui/theme.d.ts +21 -0
- package/dist/ui/theme.js +63 -0
- package/dist/video/ffmpeg.d.ts +54 -0
- package/dist/video/ffmpeg.js +206 -0
- package/package.json +71 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ReviewState } from "../data/model.js";
|
|
3
|
+
export declare function WorktreeChip({ label }: {
|
|
4
|
+
label: string;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
export declare function ReviewBadge({ state, stale }: {
|
|
7
|
+
state: ReviewState;
|
|
8
|
+
stale?: boolean;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
export declare function MovieBadge({ duration }: {
|
|
11
|
+
duration: string | null;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
export declare function StatusPill({ label }: {
|
|
14
|
+
label: string | null;
|
|
15
|
+
}): import("react").JSX.Element | null;
|
|
16
|
+
export declare function ExecutionPill({ status }: {
|
|
17
|
+
status: string | null;
|
|
18
|
+
}): import("react").JSX.Element | null;
|
|
19
|
+
export interface KeyHint {
|
|
20
|
+
key: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function HintBar({ hints, width }: {
|
|
24
|
+
hints: KeyHint[];
|
|
25
|
+
width: number;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
export declare function Toast({ message, width }: {
|
|
28
|
+
message: string | null;
|
|
29
|
+
width: number;
|
|
30
|
+
}): import("react").JSX.Element | null;
|
|
31
|
+
/** One terminal row that never collapses when its column overflows. */
|
|
32
|
+
export declare function Line({ children, width }: {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
width?: number;
|
|
35
|
+
}): import("react").JSX.Element;
|
|
36
|
+
export declare function SectionLabel({ children }: {
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
export declare function Rule({ width, color }: {
|
|
40
|
+
width: number;
|
|
41
|
+
color?: string;
|
|
42
|
+
}): import("react").JSX.Element;
|
|
43
|
+
export declare function MetaRow({ label, value, width }: {
|
|
44
|
+
label: string;
|
|
45
|
+
value: string;
|
|
46
|
+
width: number;
|
|
47
|
+
}): import("react").JSX.Element;
|
|
48
|
+
export declare function EmptyState({ title, body, action, width, }: {
|
|
49
|
+
title: string;
|
|
50
|
+
body: string;
|
|
51
|
+
action?: string;
|
|
52
|
+
width: number;
|
|
53
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { theme, truncate } from "./theme.js";
|
|
4
|
+
export function WorktreeChip({ label }) {
|
|
5
|
+
return (_jsx(Text, { backgroundColor: theme.selection, color: theme.purple, children: ` ${label} ` }));
|
|
6
|
+
}
|
|
7
|
+
export function ReviewBadge({ state, stale }) {
|
|
8
|
+
if (state === "seen")
|
|
9
|
+
return _jsx(Text, { color: theme.blue, children: "\u25CF Seen" });
|
|
10
|
+
return _jsx(Text, { color: theme.amber, children: stale ? "● Unseen · changed" : "● Unseen" });
|
|
11
|
+
}
|
|
12
|
+
export function MovieBadge({ duration }) {
|
|
13
|
+
return (_jsx(Text, { backgroundColor: "#3b2f6e", color: theme.purple, children: duration ? ` Movie · ${duration} ` : " Movie " }));
|
|
14
|
+
}
|
|
15
|
+
export function StatusPill({ label }) {
|
|
16
|
+
if (!label)
|
|
17
|
+
return null;
|
|
18
|
+
const color = label === "Complete"
|
|
19
|
+
? theme.green
|
|
20
|
+
: label === "Running"
|
|
21
|
+
? theme.amber
|
|
22
|
+
: label === "Failed"
|
|
23
|
+
? theme.red
|
|
24
|
+
: label === "Ready"
|
|
25
|
+
? theme.blue
|
|
26
|
+
: theme.muted;
|
|
27
|
+
return _jsx(Text, { color: color, children: label });
|
|
28
|
+
}
|
|
29
|
+
export function ExecutionPill({ status }) {
|
|
30
|
+
if (!status)
|
|
31
|
+
return null;
|
|
32
|
+
const color = status === "pass" ? theme.green : status === "fail" ? theme.red : status === "running" ? theme.amber : theme.muted;
|
|
33
|
+
return _jsxs(Text, { color: color, children: ["\u00B7 run ", status] });
|
|
34
|
+
}
|
|
35
|
+
export function HintBar({ hints, width }) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
let used = 0;
|
|
38
|
+
hints.forEach((hint, index) => {
|
|
39
|
+
const length = hint.key.length + 1 + hint.label.length + 3;
|
|
40
|
+
if (used + length > width)
|
|
41
|
+
return;
|
|
42
|
+
used += length;
|
|
43
|
+
parts.push(_jsxs(Text, { children: [_jsx(Text, { color: theme.brand, bold: true, children: hint.key }), _jsxs(Text, { color: theme.muted, children: [" ", hint.label, " "] })] }, `${hint.key}-${index}`));
|
|
44
|
+
});
|
|
45
|
+
return (_jsx(Box, { flexShrink: 0, width: width, height: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", children: parts }) }));
|
|
46
|
+
}
|
|
47
|
+
export function Toast({ message, width }) {
|
|
48
|
+
if (!message)
|
|
49
|
+
return null;
|
|
50
|
+
const text = ` ${truncate(message, Math.max(8, width - 6))} `;
|
|
51
|
+
return (_jsx(Box, { flexShrink: 0, width: width, justifyContent: "center", height: 1, children: _jsx(Text, { backgroundColor: "#2e3140", color: theme.text, bold: true, children: text }) }));
|
|
52
|
+
}
|
|
53
|
+
/** One terminal row that never collapses when its column overflows. */
|
|
54
|
+
export function Line({ children, width }) {
|
|
55
|
+
return (_jsx(Box, { flexShrink: 0, height: 1, width: width, overflow: "hidden", children: children }));
|
|
56
|
+
}
|
|
57
|
+
export function SectionLabel({ children }) {
|
|
58
|
+
return (_jsx(Text, { color: theme.muted, bold: true, children: children }));
|
|
59
|
+
}
|
|
60
|
+
export function Rule({ width, color }) {
|
|
61
|
+
return _jsx(Text, { color: color ?? theme.faint, children: "─".repeat(Math.max(0, width)) });
|
|
62
|
+
}
|
|
63
|
+
export function MetaRow({ label, value, width }) {
|
|
64
|
+
const labelWidth = 10;
|
|
65
|
+
return (_jsxs(Box, { flexShrink: 0, width: width, height: 1, children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: theme.muted, children: label }) }), _jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate-start", children: value }) })] }));
|
|
66
|
+
}
|
|
67
|
+
export function EmptyState({ title, body, action, width, }) {
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, paddingX: 2, width: width, children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { width: Math.min(width - 4, 60), justifyContent: "center", children: _jsx(Text, { color: theme.muted, wrap: "wrap", children: body }) }), action ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.brand, children: action }) })) : null] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReviewStore } from "../data/store.js";
|
|
2
|
+
import type { ImageService } from "../images/service.js";
|
|
3
|
+
import type { ImageLayer } from "../terminal/image-layer.js";
|
|
4
|
+
import type { TerminalCapabilities } from "../terminal/probe.js";
|
|
5
|
+
import type { FfmpegInfo } from "../video/ffmpeg.js";
|
|
6
|
+
export interface AppServices {
|
|
7
|
+
store: ReviewStore;
|
|
8
|
+
layer: ImageLayer;
|
|
9
|
+
service: ImageService;
|
|
10
|
+
capabilities: TerminalCapabilities;
|
|
11
|
+
ffmpeg: FfmpegInfo;
|
|
12
|
+
rootsSource: "app" | "cli" | "cwd";
|
|
13
|
+
version: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const ServicesContext: import("react").Context<AppServices | null>;
|
|
16
|
+
export declare function useServices(): AppServices;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
export const ServicesContext = createContext(null);
|
|
3
|
+
export function useServices() {
|
|
4
|
+
const services = useContext(ServicesContext);
|
|
5
|
+
if (!services)
|
|
6
|
+
throw new Error("ServicesContext is missing");
|
|
7
|
+
return services;
|
|
8
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Shot } from "../data/model.js";
|
|
2
|
+
import { type PlaybackState } from "./movie-player.js";
|
|
3
|
+
export interface DetailPaneProps {
|
|
4
|
+
shot: Shot;
|
|
5
|
+
position: {
|
|
6
|
+
index: number;
|
|
7
|
+
count: number;
|
|
8
|
+
};
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
/** `page` shows the back/paging header; `pane` is the split-view column. */
|
|
12
|
+
mode: "pane" | "page";
|
|
13
|
+
composer: boolean;
|
|
14
|
+
onComposerSubmit: (text: string) => void;
|
|
15
|
+
onComposerCancel: () => void;
|
|
16
|
+
inlinePlayer: boolean;
|
|
17
|
+
playback: PlaybackState;
|
|
18
|
+
onPlayback: (patch: Partial<PlaybackState>) => void;
|
|
19
|
+
busy: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
/** 1 shows the whole image; above magnifies. */
|
|
22
|
+
zoom: number;
|
|
23
|
+
/** Pan center as a fraction of the image, [0,1]. */
|
|
24
|
+
panX: number;
|
|
25
|
+
panY: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function previewHeight(height: number): number;
|
|
28
|
+
export declare function CommentList({ shot, width, max }: {
|
|
29
|
+
shot: Shot;
|
|
30
|
+
width: number;
|
|
31
|
+
max: number;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
export declare function DetailPane(props: DetailPaneProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Shot detail: preview, heading, movie actions, chapters, feedback, Seen,
|
|
4
|
+
* and the metadata card — the tray's Detail pane, in the app's order.
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { chapterTimeLabel, durationLabel } from "../data/manifest.js";
|
|
9
|
+
import { Line, MetaRow, MovieBadge, ReviewBadge, Rule, SectionLabel } from "./chrome.js";
|
|
10
|
+
import { useServices } from "./context.js";
|
|
11
|
+
import { MoviePlayer, ProgressBar } from "./movie-player.js";
|
|
12
|
+
import { Picture } from "./picture.js";
|
|
13
|
+
import { reviewStateOf } from "./selectors.js";
|
|
14
|
+
import { TextInput } from "./text-input.js";
|
|
15
|
+
import { abbreviatedDateTime, isoDateTime, theme, truncate } from "./theme.js";
|
|
16
|
+
export function previewHeight(height) {
|
|
17
|
+
return Math.max(6, Math.min(22, Math.round(height * 0.4)));
|
|
18
|
+
}
|
|
19
|
+
export function CommentList({ shot, width, max }) {
|
|
20
|
+
const comments = shot.review?.comments ?? [];
|
|
21
|
+
if (comments.length === 0) {
|
|
22
|
+
return (_jsx(Line, { width: width, children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: "No comments yet \u00B7 leave concise, actionable feedback for this frame." }) }));
|
|
23
|
+
}
|
|
24
|
+
const visible = comments.slice(-max);
|
|
25
|
+
const hidden = comments.length - visible.length;
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, flexShrink: 0, children: [hidden > 0 ? (_jsx(Line, { width: width, children: _jsxs(Text, { color: theme.faint, children: ["\u2026 ", hidden, " earlier"] }) })) : null, visible.map((comment) => (_jsxs(Box, { flexDirection: "column", width: width, flexShrink: 0, children: [_jsx(Line, { width: width, children: _jsxs(Text, { color: theme.muted, wrap: "truncate", children: [_jsx(Text, { color: theme.purple, children: "Reviewer" }), " \u00B7 ", abbreviatedDateTime(comment.createdAt)] }) }), _jsx(Box, { flexShrink: 0, width: width, height: Math.min(3, Math.max(1, Math.ceil(comment.body.length / Math.max(10, width)))), overflow: "hidden", children: _jsx(Text, { wrap: "wrap", children: truncate(comment.body, width * 3) }) })] }, comment.id)))] }));
|
|
27
|
+
}
|
|
28
|
+
export function DetailPane(props) {
|
|
29
|
+
const { shot, position, width, height, mode, composer, inlinePlayer, playback, onPlayback, busy, error } = props;
|
|
30
|
+
const { ffmpeg } = useServices();
|
|
31
|
+
const state = reviewStateOf(shot);
|
|
32
|
+
const inner = Math.max(20, width - 2);
|
|
33
|
+
const preview = previewHeight(height);
|
|
34
|
+
const heading = shot.sequence ? `${shot.sequence} · ${shot.slug}` : shot.slug;
|
|
35
|
+
const canPlay = shot.isMovie && shot.videoPath !== null;
|
|
36
|
+
const commentCount = shot.review?.comments.length ?? 0;
|
|
37
|
+
const chapters = shot.chapters.slice(0, 3);
|
|
38
|
+
return (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: width, height: height, paddingX: 1, overflow: "hidden", children: [mode === "page" ? (_jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(Text, { color: theme.blue, children: "\u2039 Stream" }), _jsxs(Text, { color: theme.muted, children: [position.index, " / ", position.count, " ", _jsx(Text, { color: theme.blue, children: "\u2039" }), " older \u00B7 newer ", _jsx(Text, { color: theme.blue, children: "\u203A" })] })] })) : (_jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(Text, { color: theme.muted, children: "Detail" }), _jsxs(Text, { color: theme.muted, children: [position.index, " / ", position.count] })] })), _jsx(Box, { width: inner, height: preview, justifyContent: "center", flexShrink: 0, children: inlinePlayer && canPlay ? (_jsx(MoviePlayer, { videoPath: shot.videoPath, posterPath: shot.path, posterVersion: shot.mtimeMs, width: inner, height: preview, playback: playback, onPlayback: onPlayback })) : (_jsx(Picture, { src: shot.path, version: shot.mtimeMs, width: inner, height: preview, maxUpscale: 8, zoom: props.zoom, panX: props.panX, panY: props.panY, label: shot.isMovie ? "▶ movie poster" : "still" })) }), inlinePlayer && canPlay ? (_jsx(ProgressBar, { positionMs: playback.positionMs, durationMs: playback.durationMs ?? shot.durationMs, chapters: shot.chapters, width: inner, playing: playback.playing })) : null, _jsxs(Box, { height: 1, width: inner, justifyContent: "space-between", flexShrink: 0, children: [_jsx(Text, { bold: true, wrap: "truncate", children: truncate(heading, Math.max(8, inner - 24)) }), _jsxs(Text, { children: [shot.isMovie ? (_jsxs(_Fragment, { children: [_jsx(MovieBadge, { duration: durationLabel(shot.durationMs) }), _jsx(Text, { children: " " })] })) : null, _jsx(ReviewBadge, { state: state, stale: shot.review?.isStale })] })] }), _jsx(Line, { width: inner, children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: truncate(shot.description || shot.fileName, inner) }) }), shot.isMovie ? (_jsxs(Box, { flexDirection: "column", width: inner, flexShrink: 0, children: [_jsx(Line, { width: inner, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: canPlay ? theme.green : theme.faint, bold: true, children: " p " }), _jsx(Text, { color: canPlay ? theme.text : theme.faint, children: inlinePlayer ? "Hide player" : "Play in tray" }), _jsx(Text, { color: canPlay ? theme.blue : theme.faint, bold: true, children: " O " }), _jsx(Text, { color: canPlay ? theme.text : theme.faint, children: "Open movie" }), !ffmpeg.ffmpeg && canPlay ? _jsx(Text, { color: theme.amber, children: " ffmpeg missing \u00B7 install to play here" }) : null] }) }), shot.videoFileName && !shot.videoPath ? (_jsx(Line, { width: inner, children: _jsx(Text, { color: theme.amber, children: "Video missing on disk" }) })) : !shot.videoFileName ? (_jsx(Line, { width: inner, children: _jsx(Text, { color: theme.muted, children: "Poster only \u2014 no video path in manifest" }) })) : null, chapters.length > 0 ? (_jsxs(Box, { flexDirection: "column", width: inner, flexShrink: 0, children: [_jsx(Line, { width: inner, children: _jsx(SectionLabel, { children: "CHAPTERS" }) }), chapters.map((chapter, index) => (_jsx(Line, { width: inner, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.purple, children: chapterTimeLabel(chapter.tMs).padStart(6) }), " ", truncate(chapter.title ?? chapter.slug ?? "chapter", inner - 8)] }) }, `${chapter.slug ?? index}`))), shot.chapters.length > chapters.length ? (_jsx(Line, { width: inner, children: _jsxs(Text, { color: theme.faint, children: [" \u2026 ", shot.chapters.length - chapters.length, " more"] }) })) : null] })) : null] })) : null, _jsx(Rule, { width: inner }), _jsxs(Box, { height: 1, width: inner, justifyContent: "space-between", flexShrink: 0, children: [_jsxs(SectionLabel, { children: ["FEEDBACK ", commentCount] }), busy ? _jsx(Text, { color: theme.muted, children: "Saving review\u2026" }) : error ? _jsx(Text, { color: theme.red, children: truncate(error, inner - 12) }) : null] }), _jsx(Box, { flexShrink: 0, width: inner, children: _jsx(CommentList, { shot: shot, width: inner, max: composer ? 2 : 3 }) }), composer ? (_jsx(Box, { flexShrink: 0, width: inner, children: _jsx(TextInput, { placeholder: "Share feedback\u2026", width: inner, onSubmit: props.onComposerSubmit, onCancel: props.onComposerCancel, submitLabel: "Send Feedback" }) })) : (_jsx(Line, { width: inner, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.blue, bold: true, children: " c " }), _jsx(Text, { color: theme.muted, children: "Send Feedback" }), _jsx(Text, { color: theme.green, bold: true, children: " s " }), _jsx(Text, { color: theme.muted, children: "Seen" }), _jsx(Text, { color: theme.purple, bold: true, children: " f " }), _jsx(Text, { color: theme.muted, children: "Full screen" }), _jsx(Text, { color: theme.blue, bold: true, children: " +/- " }), _jsx(Text, { color: theme.muted, children: "zoom" })] }) })), _jsx(Line, { width: inner, children: _jsx(Rule, { width: inner }) }), _jsxs(Box, { flexDirection: "column", width: inner, overflow: "hidden", flexGrow: 1, flexShrink: 100, minHeight: 0, children: [_jsx(MetaRow, { label: "Tree", value: shot.worktree, width: inner }), _jsx(MetaRow, { label: "Feature", value: shot.feature, width: inner }), _jsx(MetaRow, { label: "File", value: shot.fileName, width: inner }), shot.isMovie ? _jsx(MetaRow, { label: "Kind", value: "Movie", width: inner }) : null, shot.isMovie ? _jsx(MetaRow, { label: "Video", value: shot.videoFileName ?? "—", width: inner }) : null, shot.isMovie ? _jsx(MetaRow, { label: "Duration", value: durationLabel(shot.durationMs) ?? "—", width: inner }) : null, shot.isMovie && shot.source ? _jsx(MetaRow, { label: "Source", value: shot.source, width: inner }) : null, shot.isMovie && shot.chapters.length > 0 ? _jsx(MetaRow, { label: "Chapters", value: String(shot.chapters.length), width: inner }) : null, _jsx(MetaRow, { label: "Time", value: isoDateTime(shot.capturedAt), width: inner }), shot.url ? _jsx(MetaRow, { label: "URL", value: shot.url, width: inner }) : null, shot.runId ? _jsx(MetaRow, { label: "Run", value: shot.runId, width: inner }) : null, shot.status ? _jsx(MetaRow, { label: "Execution", value: shot.status.charAt(0).toUpperCase() + shot.status.slice(1), width: inner }) : null, _jsx(MetaRow, { label: "Review", value: state === "seen" ? "Seen" : "Unseen", width: inner }), _jsx(MetaRow, { label: "Path", value: path.dirname(shot.path), width: inner })] })] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FrictionLog, FrictionRun } from "../data/model.js";
|
|
2
|
+
import type { StreamFilter } from "./selectors.js";
|
|
3
|
+
export declare const FRICTION_ROW_HEIGHT = 4;
|
|
4
|
+
export interface FrictionListProps {
|
|
5
|
+
logs: FrictionLog[];
|
|
6
|
+
cursor: number;
|
|
7
|
+
scrollTop: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
filter: StreamFilter;
|
|
11
|
+
counts: {
|
|
12
|
+
pending: number;
|
|
13
|
+
seen: number;
|
|
14
|
+
};
|
|
15
|
+
focused: boolean;
|
|
16
|
+
total: number;
|
|
17
|
+
now: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function FrictionFilterBar({ filter, counts, width }: Pick<FrictionListProps, "filter" | "counts" | "width">): import("react").JSX.Element;
|
|
20
|
+
export declare function frictionScroll(count: number, cursor: number, scrollTop: number, height: number): number;
|
|
21
|
+
export declare function FrictionList(props: FrictionListProps): import("react").JSX.Element;
|
|
22
|
+
export interface FrictionLogDetailProps {
|
|
23
|
+
log: FrictionLog;
|
|
24
|
+
run: FrictionRun | null;
|
|
25
|
+
stepCursor: number;
|
|
26
|
+
promptOpen: boolean;
|
|
27
|
+
prompt: string | null;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function FrictionLogDetail({ log, run, stepCursor, promptOpen, prompt, width, height }: FrictionLogDetailProps): import("react").JSX.Element;
|
|
32
|
+
export interface FrictionStepDetailProps {
|
|
33
|
+
log: FrictionLog;
|
|
34
|
+
run: FrictionRun;
|
|
35
|
+
stepIndex: number;
|
|
36
|
+
imageIndex: number;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
mode: "page" | "takeover";
|
|
40
|
+
}
|
|
41
|
+
export declare function FrictionStepDetail({ log, run, stepIndex, imageIndex, width, height }: FrictionStepDetailProps): import("react").JSX.Element;
|
|
42
|
+
export declare function FrictionStepTakeover({ log, run, stepIndex, imageIndex, width, height }: FrictionStepDetailProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Friction Logs: scenario list, scenario detail with run picker and steps,
|
|
4
|
+
* step detail, and the step takeover.
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { frictionStatusLabel, runDisplayTitle, stepCountLabel } from "../data/friction.js";
|
|
9
|
+
import { EmptyState, MetaRow, Rule, SectionLabel, StatusPill, WorktreeChip } from "./chrome.js";
|
|
10
|
+
import { Picture } from "./picture.js";
|
|
11
|
+
import { frictionState, frictionSummary, latestRun } from "./selectors.js";
|
|
12
|
+
import { relativeTime, theme, truncate } from "./theme.js";
|
|
13
|
+
export const FRICTION_ROW_HEIGHT = 4;
|
|
14
|
+
export function FrictionFilterBar({ filter, counts, width }) {
|
|
15
|
+
const title = filter === "unseen" ? `Unseen (${counts.pending})` : `History (${counts.seen})`;
|
|
16
|
+
return (_jsxs(Box, { flexShrink: 0, width: width, height: 1, paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: title }), _jsxs(Text, { children: [filter === "unseen" && counts.pending > 0 ? _jsx(Text, { color: theme.green, children: " S Seen all " }) : null, _jsx(Text, { color: theme.blue, children: filter === "unseen" ? " u History " : " u Unseen " })] })] }));
|
|
17
|
+
}
|
|
18
|
+
function FrictionRow({ log, selected, width, now }) {
|
|
19
|
+
const run = latestRun(log);
|
|
20
|
+
const inner = width - 3;
|
|
21
|
+
const status = frictionStatusLabel(log.status);
|
|
22
|
+
const footer = run
|
|
23
|
+
? log.runs.length > 1
|
|
24
|
+
? `${log.runs.length} runs · ${runDisplayTitle(run.runId)}`
|
|
25
|
+
: runDisplayTitle(run.runId)
|
|
26
|
+
: "Prompt only · no runs yet";
|
|
27
|
+
return (_jsxs(Box, { flexShrink: 0, width: width, height: FRICTION_ROW_HEIGHT, flexDirection: "row", backgroundColor: selected ? theme.selection : undefined, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: theme.brand, children: selected ? "▎" : " " }) }), _jsxs(Box, { flexDirection: "column", width: inner, marginLeft: 1, overflow: "hidden", children: [_jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(Text, { bold: true, wrap: "truncate", children: truncate(log.title, Math.max(8, inner - 12)) }), _jsx(StatusPill, { label: status })] }), _jsx(Text, { color: theme.muted, wrap: "truncate", children: truncate(log.description, inner) }), _jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(WorktreeChip, { label: log.worktreeShort }), _jsxs(Text, { color: theme.muted, children: [" ", log.slug] })] }), _jsx(Text, { color: theme.muted, children: frictionSummary(log) })] }), _jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(Text, { color: theme.muted, wrap: "truncate", children: footer }), _jsxs(Text, { color: frictionState(log) === "seen" ? theme.blue : theme.amber, children: [frictionState(log) === "seen" ? "Seen" : "Unseen", run ? _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", relativeTime(run.capturedAt, now)] }) : null] })] })] })] }));
|
|
28
|
+
}
|
|
29
|
+
export function frictionScroll(count, cursor, scrollTop, height) {
|
|
30
|
+
const perPage = Math.max(1, Math.floor(height / FRICTION_ROW_HEIGHT));
|
|
31
|
+
let top = Math.min(scrollTop, Math.max(0, count - 1));
|
|
32
|
+
if (cursor < top)
|
|
33
|
+
top = cursor;
|
|
34
|
+
if (cursor >= top + perPage)
|
|
35
|
+
top = cursor - perPage + 1;
|
|
36
|
+
return Math.max(0, top);
|
|
37
|
+
}
|
|
38
|
+
export function FrictionList(props) {
|
|
39
|
+
const { logs, cursor, scrollTop, width, height, filter, focused, total, now } = props;
|
|
40
|
+
if (total === 0) {
|
|
41
|
+
return (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: width, height: height, children: [_jsx(EmptyState, { width: width, title: "No friction logs yet", body: "Author a scenario with the friction-log skill, then run it. Results land under .astroshot/friction-logs/." }), _jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsxs(Text, { color: theme.muted, children: [".astroshot/friction-logs/", "<slug>", "/prompt.md"] }), _jsxs(Text, { color: theme.muted, children: ["runs/", "<run-id>", "/log.jsonl + screenshots"] })] })] }));
|
|
42
|
+
}
|
|
43
|
+
if (logs.length === 0) {
|
|
44
|
+
return filter === "unseen" ? (_jsx(EmptyState, { width: width, title: "You\u2019re all caught up", body: "Every friction log has been seen.", action: "u View history" })) : (_jsx(EmptyState, { width: width, title: "No history yet", body: "Logs you mark Seen will appear here.", action: "u Back to unseen" }));
|
|
45
|
+
}
|
|
46
|
+
const perPage = Math.max(1, Math.floor(height / FRICTION_ROW_HEIGHT));
|
|
47
|
+
const visible = logs.slice(scrollTop, scrollTop + perPage);
|
|
48
|
+
return (_jsx(Box, { flexShrink: 0, flexDirection: "column", width: width, height: height, overflow: "hidden", children: visible.map((log, offset) => (_jsx(FrictionRow, { log: log, selected: focused && scrollTop + offset === cursor, width: width, now: now }, log.id))) }));
|
|
49
|
+
}
|
|
50
|
+
export function FrictionLogDetail({ log, run, stepCursor, promptOpen, prompt, width, height }) {
|
|
51
|
+
const inner = width - 2;
|
|
52
|
+
const runIndex = run ? log.runs.indexOf(run) : -1;
|
|
53
|
+
const improve = run ? run.steps.flatMap((step) => step.improve.map((note) => ({ step: step.step, note }))) : [];
|
|
54
|
+
const headerLines = 6 + (promptOpen ? 6 : 0) + (improve.length > 0 ? Math.min(improve.length, 3) + 1 : 0);
|
|
55
|
+
const stepsHeight = Math.max(3, height - headerLines - 2);
|
|
56
|
+
const perPage = Math.max(1, Math.floor(stepsHeight / 2));
|
|
57
|
+
const stepsTop = Math.max(0, Math.min(stepCursor - perPage + 1, run ? run.steps.length - perPage : 0));
|
|
58
|
+
const steps = run ? run.steps.slice(Math.max(0, Math.min(stepsTop, stepCursor)), Math.max(0, Math.min(stepsTop, stepCursor)) + perPage) : [];
|
|
59
|
+
const firstIndex = Math.max(0, Math.min(stepsTop, stepCursor));
|
|
60
|
+
return (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: width, height: height, paddingX: 1, overflow: "hidden", children: [_jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(Text, { color: theme.blue, children: "\u2039 Logs" }), _jsxs(Text, { color: theme.muted, children: [run ? stepCountLabel(run.steps.length) : "no runs", frictionState(log) !== "seen" && run ? _jsx(Text, { color: theme.green, children: " s Seen" }) : null] })] }), _jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(Text, { bold: true, wrap: "truncate", children: truncate(log.title, inner - 12) }), _jsx(StatusPill, { label: frictionStatusLabel(log.status) })] }), _jsx(Text, { color: theme.muted, wrap: "truncate", children: truncate(log.description, inner) }), _jsxs(Text, { wrap: "truncate", children: [_jsx(WorktreeChip, { label: log.worktreeShort }), _jsxs(Text, { color: theme.muted, children: [" ", log.slug] }), log.promptPath ? (_jsxs(Text, { color: theme.blue, children: [" p ", promptOpen ? "Hide prompt" : "Prompt"] })) : null] }), promptOpen && prompt !== null ? (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: inner, height: 6, overflow: "hidden", borderStyle: "round", borderColor: theme.faint, paddingX: 1, children: [_jsx(SectionLabel, { children: "SCENARIO PROMPT" }), _jsx(Text, { color: theme.muted, wrap: "wrap", children: truncate(prompt, (inner - 4) * 4) })] })) : null, _jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: log.runs.length > 1 ? `Runs ${log.runs.length}` : "Run" }), run ? (_jsxs(Text, { color: theme.muted, children: [" ", runDisplayTitle(run.runId), runIndex === 0 ? _jsx(Text, { color: theme.purple, children: " Latest" }) : null, " ", run.runId] })) : null] }), log.runs.length > 1 ? _jsx(Text, { color: theme.blue, children: "[ ] switch run" }) : null] }), improve.length > 0 ? (_jsxs(Box, { flexDirection: "column", width: inner, children: [_jsxs(Text, { color: theme.amber, bold: true, children: ["Improve rollup \u00B7 ", improve.length] }), improve.slice(0, 3).map((item, index) => (_jsxs(Text, { color: theme.amber, wrap: "truncate", children: [String(item.step).padStart(2, "0"), " ", truncate(item.note, inner - 3)] }, `${item.step}-${index}`)))] })) : null, _jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(SectionLabel, { children: "STEPS" }), _jsx(Text, { color: theme.muted, children: "\u23CE open step \u00B7 \u2191\u2193 move" })] }), !run ? (_jsxs(Box, { flexDirection: "column", width: inner, children: [_jsx(Text, { bold: true, children: "No runs yet" }), _jsx(Text, { color: theme.muted, wrap: "wrap", children: "This scenario has a prompt but no log.jsonl run. Use the friction-log skill to execute it; steps will appear here as the agent writes them." })] })) : run.steps.length === 0 ? (_jsx(Text, { color: theme.muted, children: "No JSONL steps in this run" })) : (_jsx(Box, { flexDirection: "column", width: inner, overflow: "hidden", flexGrow: 1, children: steps.map((step, offset) => (_jsx(StepRow, { step: step, selected: firstIndex + offset === stepCursor, width: inner }, step.id))) }))] }));
|
|
61
|
+
}
|
|
62
|
+
function StepRow({ step, selected, width }) {
|
|
63
|
+
return (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: width, height: 2, backgroundColor: selected ? theme.selection : undefined, children: [_jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: width, children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: selected ? theme.brand : theme.purple, children: String(step.step).padStart(2, "0") }), " ", _jsx(Text, { bold: true, children: truncate(step.title, width - 16) })] }), _jsxs(Text, { children: [step.transcript ? _jsx(Text, { color: theme.purple, children: "\u270E " }) : null, step.screenshots.length > 0 ? _jsx(Text, { color: theme.muted, children: "\u25EB " }) : null, step.good.length > 0 ? _jsxs(Text, { color: theme.green, children: [step.good.length, "+ "] }) : null, step.improve.length > 0 ? _jsxs(Text, { color: theme.amber, children: [step.improve.length, "! "] }) : null] })] }), _jsxs(Text, { color: theme.muted, wrap: "truncate", children: [" ", truncate(step.description, width - 4)] })] }));
|
|
64
|
+
}
|
|
65
|
+
function NoteCard({ title, color, items, empty, width }) {
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { color: color, bold: true, children: [title, items.length > 0 ? _jsxs(Text, { color: theme.muted, children: [" ", items.length] }) : null] }), items.length === 0 ? (_jsx(Text, { color: theme.muted, children: empty })) : (items.slice(0, 4).map((item, index) => (_jsxs(Text, { wrap: "truncate", children: ["• ", truncate(item, width - 2)] }, index))))] }));
|
|
67
|
+
}
|
|
68
|
+
export function FrictionStepDetail({ log, run, stepIndex, imageIndex, width, height }) {
|
|
69
|
+
const step = run.steps[stepIndex];
|
|
70
|
+
const inner = width - 2;
|
|
71
|
+
const preview = Math.max(6, Math.min(20, Math.round(height * 0.38)));
|
|
72
|
+
const screenshot = step.screenshots[Math.min(imageIndex, step.screenshots.length - 1)] ?? null;
|
|
73
|
+
return (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: width, height: height, paddingX: 1, overflow: "hidden", children: [_jsxs(Box, { flexShrink: 0, height: 1, justifyContent: "space-between", width: inner, children: [_jsx(Text, { color: theme.blue, children: "\u2039 Steps" }), _jsxs(Text, { color: theme.muted, children: [stepIndex + 1, " / ", run.steps.length, " ← → step"] })] }), _jsx(Box, { flexShrink: 0, width: inner, height: preview, children: _jsx(Picture, { src: screenshot, width: inner, height: preview, maxUpscale: 8, label: "No screenshot for this step" }) }), step.screenshots.length > 1 ? (_jsxs(Text, { color: theme.muted, children: [step.screenshots.map((_, index) => (index === imageIndex ? "● " : "○ ")), imageIndex + 1, " of ", step.screenshots.length, " \u00B7 [ ] image"] })) : null, _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.purple, bold: true, children: String(step.step).padStart(2, "0") }), " ", _jsx(Text, { bold: true, children: truncate(step.title, inner - 4) })] }), step.description ? (_jsx(Text, { color: theme.muted, wrap: "truncate", children: truncate(step.description, inner) })) : null, step.url ? _jsx(Text, { color: theme.blue, children: step.url }) : null, _jsxs(Box, { flexDirection: "column", width: inner, overflow: "hidden", flexGrow: 1, flexShrink: 1, minHeight: 0, children: [step.transcript ? (_jsxs(Box, { flexDirection: "column", width: inner, children: [_jsx(Text, { color: theme.purple, bold: true, children: "Transcript" }), _jsx(Text, { wrap: "wrap", children: truncate(step.transcript, inner * 3) })] })) : null, _jsx(NoteCard, { title: "Looks good", color: theme.green, items: step.good, empty: "No positives noted for this step", width: inner }), _jsx(NoteCard, { title: "Can improve", color: theme.amber, items: step.improve, empty: "No friction found for this step", width: inner }), _jsx(Rule, { width: inner }), _jsx(MetaRow, { label: "Log", value: log.slug, width: inner }), _jsx(MetaRow, { label: "Run", value: run.runId, width: inner }), _jsx(MetaRow, { label: "Tree", value: log.worktree, width: inner }), _jsx(MetaRow, { label: "File", value: screenshot ? path.basename(screenshot) : "—", width: inner })] })] }));
|
|
74
|
+
}
|
|
75
|
+
export function FrictionStepTakeover({ log, run, stepIndex, imageIndex, width, height }) {
|
|
76
|
+
const step = run.steps[stepIndex];
|
|
77
|
+
const railWidth = width >= 100 ? 42 : Math.max(28, Math.floor(width * 0.38));
|
|
78
|
+
const stageWidth = Math.max(10, width - railWidth - 1);
|
|
79
|
+
const stageHeight = Math.max(4, height - 3);
|
|
80
|
+
const screenshot = step.screenshots[Math.min(imageIndex, step.screenshots.length - 1)] ?? null;
|
|
81
|
+
const meta = [log.title, log.worktreeShort, run.runId, step.url ?? ""].filter(Boolean).join(" · ");
|
|
82
|
+
const railInner = railWidth - 2;
|
|
83
|
+
return (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: width, height: height, children: [_jsxs(Box, { flexShrink: 0, height: 1, paddingX: 1, justifyContent: "space-between", width: width, children: [_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { backgroundColor: theme.selection, color: theme.purple, children: [" ", String(step.step).padStart(2, "0"), " "] }), " ", _jsx(Text, { bold: true, children: truncate(step.title, width - 40) })] }), _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.blue, children: "\u2039" }), " ", stepIndex + 1, " / ", run.steps.length, " ", _jsx(Text, { color: theme.blue, children: "\u203A" }), " esc close"] })] }), _jsx(Box, { flexShrink: 0, height: 1, paddingX: 1, width: width, children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: truncate(meta, width - 2) }) }), _jsx(Box, { flexShrink: 0, height: 1, paddingX: 1, width: width, children: _jsx(Rule, { width: width - 2 }) }), _jsxs(Box, { flexShrink: 0, flexDirection: "row", width: width, height: stageHeight, children: [_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: stageWidth, height: stageHeight, alignItems: "center", justifyContent: "center", children: [_jsx(Picture, { src: screenshot, width: stageWidth, height: step.screenshots.length > 1 ? stageHeight - 1 : stageHeight, maxUpscale: 8, label: "No screenshot for this step" }), step.screenshots.length > 1 ? (_jsxs(Text, { color: theme.muted, children: ["Image ", imageIndex + 1, " / ", step.screenshots.length, " \u00B7 [ ] switch"] })) : null] }), _jsx(Box, { flexShrink: 0, width: 1, height: stageHeight, flexDirection: "column", children: Array.from({ length: stageHeight }, (_, index) => (_jsx(Text, { color: theme.faint, children: "\u2502" }, index))) }), _jsxs(Box, { flexShrink: 0, flexDirection: "column", width: railWidth, height: stageHeight, paddingX: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, children: "Step notes" }), _jsx(Text, { color: theme.muted, wrap: "wrap", children: truncate(step.description || "—", railInner * 3) }), step.url ? _jsx(Text, { color: theme.blue, children: step.url }) : null, _jsx(Rule, { width: railInner }), _jsx(Text, { color: theme.purple, bold: true, children: "Transcript" }), _jsx(Text, { wrap: "wrap", children: truncate(step.transcript || "—", railInner * 5) }), _jsx(Rule, { width: railInner }), _jsx(NoteCard, { title: "Looks good", color: theme.green, items: step.good, empty: "No positives noted", width: railInner }), _jsx(NoteCard, { title: "Can improve", color: theme.amber, items: step.improve, empty: "No friction found", width: railInner })] })] })] }));
|
|
84
|
+
}
|
package/dist/ui/help.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { theme } from "./theme.js";
|
|
4
|
+
const SECTIONS = [
|
|
5
|
+
{
|
|
6
|
+
title: "Everywhere",
|
|
7
|
+
keys: [
|
|
8
|
+
["1 / 2", "Shots · Friction Logs"],
|
|
9
|
+
["tab", "next tab"],
|
|
10
|
+
[",", "settings"],
|
|
11
|
+
["r", "rescan"],
|
|
12
|
+
["?", "this help"],
|
|
13
|
+
["q", "quit"],
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: "Stream",
|
|
18
|
+
keys: [
|
|
19
|
+
["↑↓ j k", "move"],
|
|
20
|
+
["⏎", "open detail"],
|
|
21
|
+
["f", "full-screen review"],
|
|
22
|
+
["u", "Unseen ⇄ History"],
|
|
23
|
+
["m", "Movies only"],
|
|
24
|
+
["s", "mark Seen"],
|
|
25
|
+
["S", "mark all visible Seen"],
|
|
26
|
+
["A", "mark this worktree Seen"],
|
|
27
|
+
["c", "send feedback"],
|
|
28
|
+
["z", "collapse worktree"],
|
|
29
|
+
["o", "reveal in Finder"],
|
|
30
|
+
["O", "open movie in player"],
|
|
31
|
+
["y", "copy image"],
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
title: "Detail / Review",
|
|
36
|
+
keys: [
|
|
37
|
+
["← →", "older · newer"],
|
|
38
|
+
["+ / -", "zoom in / out"],
|
|
39
|
+
["0", "reset zoom"],
|
|
40
|
+
["p", "play in tray"],
|
|
41
|
+
["space", "play / pause"],
|
|
42
|
+
[", .", "seek −5s / +5s"],
|
|
43
|
+
["[ ]", "previous / next chapter"],
|
|
44
|
+
["esc", "back / close"],
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Friction Logs",
|
|
49
|
+
keys: [
|
|
50
|
+
["⏎", "open log · open step"],
|
|
51
|
+
["[ ]", "switch run · switch image"],
|
|
52
|
+
["p", "toggle prompt"],
|
|
53
|
+
["← →", "previous / next step"],
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
export function HelpOverlay({ width, height }) {
|
|
58
|
+
const columns = width >= 100 ? 2 : 1;
|
|
59
|
+
const columnWidth = Math.floor((width - 4) / columns);
|
|
60
|
+
return (_jsxs(Box, { flexShrink: 0, flexDirection: "column", width: width, height: height, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Keyboard" }), _jsx(Text, { color: theme.muted, children: "Press ? or esc to close" }), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", marginTop: 1, children: SECTIONS.map((section) => (_jsxs(Box, { flexDirection: "column", width: columnWidth, marginBottom: 1, children: [_jsx(Text, { color: theme.brand, bold: true, children: section.title }), section.keys.map(([key, label]) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.blue, children: key.padEnd(10) }), _jsx(Text, { color: theme.text, children: label })] }, key)))] }, section.title))) })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StoreState } from "../data/store.js";
|
|
2
|
+
export declare function useStoreState(): StoreState;
|
|
3
|
+
export interface TerminalSize {
|
|
4
|
+
columns: number;
|
|
5
|
+
rows: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function useTerminalSize(): TerminalSize;
|
|
8
|
+
/** Re-render every `intervalMs` so relative times stay fresh. */
|
|
9
|
+
export declare function useClock(intervalMs: number): number;
|
package/dist/ui/hooks.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { useStdout } from "ink";
|
|
3
|
+
import { useServices } from "./context.js";
|
|
4
|
+
export function useStoreState() {
|
|
5
|
+
const { store } = useServices();
|
|
6
|
+
return useSyncExternalStore((listener) => store.subscribe(listener), () => store.getState(), () => store.getState());
|
|
7
|
+
}
|
|
8
|
+
export function useTerminalSize() {
|
|
9
|
+
const { stdout } = useStdout();
|
|
10
|
+
const [size, setSize] = useState({
|
|
11
|
+
columns: stdout.columns || 80,
|
|
12
|
+
rows: stdout.rows || 24,
|
|
13
|
+
});
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const onResize = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
16
|
+
stdout.on("resize", onResize);
|
|
17
|
+
return () => {
|
|
18
|
+
stdout.off("resize", onResize);
|
|
19
|
+
};
|
|
20
|
+
}, [stdout]);
|
|
21
|
+
return size;
|
|
22
|
+
}
|
|
23
|
+
/** Re-render every `intervalMs` so relative times stay fresh. */
|
|
24
|
+
export function useClock(intervalMs) {
|
|
25
|
+
const [now, setNow] = useState(() => Date.now());
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const timer = setInterval(() => setNow(Date.now()), intervalMs);
|
|
28
|
+
timer.unref();
|
|
29
|
+
return () => clearInterval(timer);
|
|
30
|
+
}, [intervalMs]);
|
|
31
|
+
return now;
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PlaybackState {
|
|
2
|
+
playing: boolean;
|
|
3
|
+
positionMs: number;
|
|
4
|
+
durationMs: number | null;
|
|
5
|
+
/** Bumped by the parent to request a seek to `positionMs`. */
|
|
6
|
+
seekToken: number;
|
|
7
|
+
error: string | null;
|
|
8
|
+
ended: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface MoviePlayerProps {
|
|
11
|
+
videoPath: string;
|
|
12
|
+
posterPath: string;
|
|
13
|
+
posterVersion?: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
playback: PlaybackState;
|
|
17
|
+
onPlayback: (patch: Partial<PlaybackState>) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function MoviePlayer({ videoPath, posterPath, posterVersion, width, height, playback, onPlayback }: MoviePlayerProps): import("react").JSX.Element;
|
|
20
|
+
export declare function formatClock(ms: number): string;
|
|
21
|
+
export declare function ProgressBar({ positionMs, durationMs, chapters, width, playing, }: {
|
|
22
|
+
positionMs: number;
|
|
23
|
+
durationMs: number | null;
|
|
24
|
+
chapters: Array<{
|
|
25
|
+
tMs?: number;
|
|
26
|
+
}>;
|
|
27
|
+
width: number;
|
|
28
|
+
playing: boolean;
|
|
29
|
+
}): import("react").JSX.Element;
|