omkit 0.4.3 → 0.4.4
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/dist/cli/ui/views/OmList.d.ts +0 -2
- package/dist/cli/ui/views/OmList.js +12 -32
- package/dist/cli/ui/views/RunView.d.ts +1 -1
- package/dist/cli/ui/views/RunView.js +18 -24
- package/dist/cli/ui/views/SearchableList.d.ts +24 -0
- package/dist/cli/ui/views/SearchableList.js +66 -0
- package/package.json +1 -1
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import type { DiscoveredOm } from "../../client/registry.ts";
|
|
3
|
-
/** Case-insensitive substring filter on om names. */
|
|
4
|
-
export declare function filterOms(oms: DiscoveredOm[], query: string): DiscoveredOm[];
|
|
5
3
|
/** A searchable, arrow-navigable list of oms; Enter selects the highlighted one. */
|
|
6
4
|
export declare function OmList({ oms, onSelect, }: {
|
|
7
5
|
oms: DiscoveredOm[];
|
|
@@ -1,36 +1,16 @@
|
|
|
1
|
-
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {} from "react";
|
|
4
|
+
import { Text } from "ink";
|
|
5
|
+
import { SearchableList } from "./SearchableList.js";
|
|
6
|
+
/** Show the om's path relative to cwd when it's inside the project; else the absolute path. */
|
|
7
|
+
function displayPath(file) {
|
|
8
|
+
const rel = path.relative(process.cwd(), file);
|
|
9
|
+
return rel && !rel.startsWith("..") && !path.isAbsolute(rel) ? rel : file;
|
|
8
10
|
}
|
|
9
11
|
/** A searchable, arrow-navigable list of oms; Enter selects the highlighted one. */
|
|
10
12
|
export function OmList({ oms, onSelect, }) {
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
useInput((input, key) => {
|
|
15
|
-
if (key.upArrow) {
|
|
16
|
-
setIndex((i) => Math.max(0, i - 1));
|
|
17
|
-
}
|
|
18
|
-
else if (key.downArrow) {
|
|
19
|
-
setIndex((i) => Math.min(filtered.length - 1, i + 1));
|
|
20
|
-
}
|
|
21
|
-
else if (key.return) {
|
|
22
|
-
const picked = filtered[index];
|
|
23
|
-
if (picked)
|
|
24
|
-
onSelect(picked);
|
|
25
|
-
}
|
|
26
|
-
else if (key.backspace || key.delete) {
|
|
27
|
-
setQuery((q) => q.slice(0, -1));
|
|
28
|
-
setIndex(0);
|
|
29
|
-
}
|
|
30
|
-
else if (input && !key.ctrl && !key.meta) {
|
|
31
|
-
setQuery((q) => q + input);
|
|
32
|
-
setIndex(0);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["search: ", query] }), filtered.map((o, i) => (_jsxs(Text, { inverse: i === index, children: [o.name, " ", o.file] }, o.name)))] }));
|
|
13
|
+
// Column width so paths line up; capped so one long name can't push paths off-screen.
|
|
14
|
+
const nameWidth = Math.min(30, oms.reduce((w, o) => Math.max(w, o.name.length), 0));
|
|
15
|
+
return (_jsx(SearchableList, { items: oms, getKey: (o) => o.name, getSearchText: (o) => o.name, onSelect: onSelect, renderRow: (o, selected) => (_jsxs(Text, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "❯ " : " " }), _jsx(Text, { bold: selected, children: o.name.padEnd(nameWidth) }), " ", _jsx(Text, { dimColor: true, children: displayPath(o.file) })] })) }));
|
|
36
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import type { PromptRequest, Verdict } from "../../client/types.ts";
|
|
3
|
-
/** Answer one pending prompt — free-text input, or
|
|
3
|
+
/** Answer one pending prompt — free-text input, or searchable choices. */
|
|
4
4
|
export declare function PromptView({ request, onAnswer, }: {
|
|
5
5
|
request: PromptRequest;
|
|
6
6
|
onAnswer: (value: string) => void;
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { SearchableList } from "./SearchableList.js";
|
|
5
|
+
/** A choice prompt: searchable, arrow-navigable options, Enter picks the highlighted one. */
|
|
6
|
+
function ChoicePrompt({ request, onAnswer, }) {
|
|
6
7
|
const { spec } = request;
|
|
7
8
|
const choices = spec.choices ?? [];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const defaultIndex = Math.max(0, choices.findIndex((c) => c.value === spec.default));
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: spec.message }), _jsx(SearchableList, { items: choices, getKey: (c) => c.value, getSearchText: (c) => c.label, initialIndex: defaultIndex, onSelect: (c) => onAnswer(c.value), renderRow: (c, selected) => (_jsxs(Text, { inverse: selected, children: [c.value === spec.default ? "* " : " ", c.label] })) })] }));
|
|
11
|
+
}
|
|
12
|
+
/** A free-text prompt: a bordered field with a block cursor and a dim default/enter hint. */
|
|
13
|
+
function InputPrompt({ request, onAnswer, }) {
|
|
14
|
+
const { spec } = request;
|
|
15
|
+
// Ref holds the authoritative text: Ink can deliver several key events before React re-renders,
|
|
16
|
+
// so the Enter handler must read a ref, not a possibly-stale render closure.
|
|
11
17
|
const textRef = useRef("");
|
|
12
|
-
const indexRef = useRef(0);
|
|
13
18
|
const [text, setText] = useState("");
|
|
14
|
-
|
|
15
|
-
const onChoiceKey = (key) => {
|
|
16
|
-
if (key.upArrow) {
|
|
17
|
-
indexRef.current = Math.max(0, indexRef.current - 1);
|
|
18
|
-
setIndex(indexRef.current);
|
|
19
|
-
}
|
|
20
|
-
else if (key.downArrow) {
|
|
21
|
-
indexRef.current = Math.min(choices.length - 1, indexRef.current + 1);
|
|
22
|
-
setIndex(indexRef.current);
|
|
23
|
-
}
|
|
24
|
-
else if (key.return) {
|
|
25
|
-
onAnswer(choices[indexRef.current]?.value ?? spec.default);
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
const onInputKey = (input, key) => {
|
|
19
|
+
useInput((input, key) => {
|
|
29
20
|
if (key.return) {
|
|
30
21
|
onAnswer(textRef.current === "" ? spec.default : textRef.current);
|
|
31
22
|
}
|
|
@@ -37,9 +28,12 @@ export function PromptView({ request, onAnswer, }) {
|
|
|
37
28
|
textRef.current += input;
|
|
38
29
|
setText(textRef.current);
|
|
39
30
|
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
});
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: spec.message }), _jsxs(Text, { children: ["› ", text, _jsx(Text, { inverse: true, children: " " })] }), _jsxs(Text, { dimColor: true, children: ["enter \u23CE", spec.default ? ` · default: ${spec.default}` : ""] })] }));
|
|
33
|
+
}
|
|
34
|
+
/** Answer one pending prompt — free-text input, or searchable choices. */
|
|
35
|
+
export function PromptView({ request, onAnswer, }) {
|
|
36
|
+
return request.spec.kind === "choice" ? (_jsx(ChoicePrompt, { request: request, onAnswer: onAnswer })) : (_jsx(InputPrompt, { request: request, onAnswer: onAnswer }));
|
|
43
37
|
}
|
|
44
38
|
/** The live run surface: milestone lines, a pending prompt, and the final verdict. */
|
|
45
39
|
export function RunView({ lines, prompt, verdict, onAnswer, }) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
export interface SearchableListProps<T> {
|
|
3
|
+
items: T[];
|
|
4
|
+
/** Stable React key per item. */
|
|
5
|
+
getKey: (item: T) => string;
|
|
6
|
+
/** Text the type-to-filter search matches against (case-insensitive substring). */
|
|
7
|
+
getSearchText: (item: T) => string;
|
|
8
|
+
/** Draw one row; `selected` is the highlighted item. */
|
|
9
|
+
renderRow: (item: T, selected: boolean) => ReactNode;
|
|
10
|
+
onSelect: (item: T) => void;
|
|
11
|
+
/** Where the highlight starts (e.g. a prompt's default). Default 0. */
|
|
12
|
+
initialIndex?: number;
|
|
13
|
+
/** Viewport size; clamped to {@link MAX_VISIBLE}. Default {@link MAX_VISIBLE}. */
|
|
14
|
+
max?: number;
|
|
15
|
+
/** Prefix for the search line. Default "search: ". */
|
|
16
|
+
searchLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A searchable, arrow-navigable list with a scrolling viewport. Owns search, selection, and
|
|
20
|
+
* windowing; the caller owns row rendering via {@link SearchableListProps.renderRow}. The
|
|
21
|
+
* `search:` line and the `+N more` footer only appear once the list overflows the viewport
|
|
22
|
+
* (or the user starts typing), so short lists stay a plain arrow list.
|
|
23
|
+
*/
|
|
24
|
+
export declare function SearchableList<T>({ items, getKey, getSearchText, renderRow, onSelect, initialIndex, max, searchLabel, }: SearchableListProps<T>): ReactElement;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
/** Hard cap on visible rows, regardless of `max`. */
|
|
5
|
+
const MAX_VISIBLE = 15;
|
|
6
|
+
/**
|
|
7
|
+
* A searchable, arrow-navigable list with a scrolling viewport. Owns search, selection, and
|
|
8
|
+
* windowing; the caller owns row rendering via {@link SearchableListProps.renderRow}. The
|
|
9
|
+
* `search:` line and the `+N more` footer only appear once the list overflows the viewport
|
|
10
|
+
* (or the user starts typing), so short lists stay a plain arrow list.
|
|
11
|
+
*/
|
|
12
|
+
export function SearchableList({ items, getKey, getSearchText, renderRow, onSelect, initialIndex = 0, max = MAX_VISIBLE, searchLabel = "search: ", }) {
|
|
13
|
+
// Refs hold the authoritative query/index: Ink can deliver several key events before React
|
|
14
|
+
// re-renders, so the Enter handler must read refs, not a possibly-stale render closure. State
|
|
15
|
+
// mirrors the refs only to drive the display.
|
|
16
|
+
const queryRef = useRef("");
|
|
17
|
+
const indexRef = useRef(initialIndex);
|
|
18
|
+
const [query, setQuery] = useState("");
|
|
19
|
+
const [index, setIndex] = useState(initialIndex);
|
|
20
|
+
const match = (q) => {
|
|
21
|
+
const needle = q.toLowerCase();
|
|
22
|
+
return needle ? items.filter((it) => getSearchText(it).toLowerCase().includes(needle)) : items;
|
|
23
|
+
};
|
|
24
|
+
const clampIndex = (i, len) => Math.min(i, Math.max(0, len - 1));
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
if (key.upArrow) {
|
|
27
|
+
indexRef.current = Math.max(0, indexRef.current - 1);
|
|
28
|
+
setIndex(indexRef.current);
|
|
29
|
+
}
|
|
30
|
+
else if (key.downArrow) {
|
|
31
|
+
const len = match(queryRef.current).length;
|
|
32
|
+
indexRef.current = Math.min(len - 1, clampIndex(indexRef.current, len) + 1);
|
|
33
|
+
setIndex(indexRef.current);
|
|
34
|
+
}
|
|
35
|
+
else if (key.return) {
|
|
36
|
+
const list = match(queryRef.current);
|
|
37
|
+
const picked = list[clampIndex(indexRef.current, list.length)];
|
|
38
|
+
if (picked)
|
|
39
|
+
onSelect(picked);
|
|
40
|
+
}
|
|
41
|
+
else if (key.backspace || key.delete) {
|
|
42
|
+
queryRef.current = queryRef.current.slice(0, -1);
|
|
43
|
+
indexRef.current = 0;
|
|
44
|
+
setQuery(queryRef.current);
|
|
45
|
+
setIndex(0);
|
|
46
|
+
}
|
|
47
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
48
|
+
queryRef.current += input;
|
|
49
|
+
indexRef.current = 0;
|
|
50
|
+
setQuery(queryRef.current);
|
|
51
|
+
setIndex(0);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const filtered = match(query);
|
|
55
|
+
const selected = clampIndex(index, filtered.length);
|
|
56
|
+
const window = Math.min(max, MAX_VISIBLE, filtered.length);
|
|
57
|
+
// Slide the viewport so the selected row stays visible, without overscrolling the end.
|
|
58
|
+
let start = selected >= window ? selected - window + 1 : 0;
|
|
59
|
+
start = Math.min(start, Math.max(0, filtered.length - window));
|
|
60
|
+
const visible = filtered.slice(start, start + window);
|
|
61
|
+
const above = start;
|
|
62
|
+
const below = filtered.length - (start + window);
|
|
63
|
+
const overflows = items.length > Math.min(max, MAX_VISIBLE);
|
|
64
|
+
const showSearch = overflows || query.length > 0;
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showSearch ? (_jsxs(Text, { dimColor: true, children: [searchLabel, query] })) : null, above > 0 ? _jsx(Text, { dimColor: true, children: `↑ ${above} more` }) : null, filtered.length === 0 ? _jsx(Text, { dimColor: true, children: "no matches" }) : null, visible.map((it, vi) => (_jsx(Box, { children: renderRow(it, start + vi === selected) }, getKey(it)))), below > 0 ? _jsx(Text, { dimColor: true, children: `+${below} more` }) : null] }));
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|