omkit 0.4.3 → 0.4.5
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/client/channel.d.ts +3 -3
- package/dist/cli/client/channel.js +8 -3
- package/dist/cli/client/types.d.ts +2 -0
- package/dist/cli/ui/app.js +4 -0
- 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/dist/core/interaction.d.ts +3 -0
- package/dist/core/interaction.js +4 -0
- package/dist/core/types.d.ts +5 -4
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@ export interface Transport {
|
|
|
7
7
|
onClose(handler: (code: number | null) => void): void;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Turn a child transport into a {@link RunSession}: fan `log`/`prompt` child
|
|
11
|
-
* handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down
|
|
12
|
-
* close before `settled` resolves a failed verdict so `result` never hangs.
|
|
10
|
+
* Turn a child transport into a {@link RunSession}: fan `log`/`prompt`/`prompt-done` child
|
|
11
|
+
* messages out to handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down
|
|
12
|
+
* the wire. A close before `settled` resolves a failed verdict so `result` never hangs.
|
|
13
13
|
*/
|
|
14
14
|
export declare function createChannel(transport: Transport): RunSession;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Turn a child transport into a {@link RunSession}: fan `log`/`prompt` child
|
|
3
|
-
* handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down
|
|
4
|
-
* close before `settled` resolves a failed verdict so `result` never hangs.
|
|
2
|
+
* Turn a child transport into a {@link RunSession}: fan `log`/`prompt`/`prompt-done` child
|
|
3
|
+
* messages out to handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down
|
|
4
|
+
* the wire. A close before `settled` resolves a failed verdict so `result` never hangs.
|
|
5
5
|
*/
|
|
6
6
|
export function createChannel(transport) {
|
|
7
7
|
const handlers = {
|
|
8
8
|
log: [],
|
|
9
9
|
prompt: [],
|
|
10
|
+
promptDone: [],
|
|
10
11
|
settled: [],
|
|
11
12
|
};
|
|
12
13
|
let settled = false;
|
|
@@ -21,6 +22,10 @@ export function createChannel(transport) {
|
|
|
21
22
|
for (const h of handlers.prompt)
|
|
22
23
|
h({ id: message.id, spec: message.spec });
|
|
23
24
|
}
|
|
25
|
+
else if (message.kind === "prompt-done") {
|
|
26
|
+
for (const h of handlers.promptDone)
|
|
27
|
+
h(message.id);
|
|
28
|
+
}
|
|
24
29
|
else if (message.kind === "settled") {
|
|
25
30
|
settled = true;
|
|
26
31
|
const verdict = { ok: message.ok, folder: message.folder, summary: message.summary };
|
|
@@ -30,6 +30,8 @@ export interface PromptRequest {
|
|
|
30
30
|
export interface RunEvents {
|
|
31
31
|
log: (entry: LogEntry) => void;
|
|
32
32
|
prompt: (request: PromptRequest) => void;
|
|
33
|
+
/** A pending prompt was withdrawn by the child (timed out or torn down) — clear it from the UI. */
|
|
34
|
+
promptDone: (id: string) => void;
|
|
33
35
|
settled: (verdict: Verdict) => void;
|
|
34
36
|
}
|
|
35
37
|
/** A live handle to one supervised run. */
|
package/dist/cli/ui/app.js
CHANGED
|
@@ -39,6 +39,10 @@ export function App({ client, onExit, oms: initialOms, }) {
|
|
|
39
39
|
setLines((prev) => [...prev, rendered.text]);
|
|
40
40
|
});
|
|
41
41
|
s.on("prompt", (req) => setPrompt(req));
|
|
42
|
+
// The child gave up on the prompt (timed out or torn down). Withdraw its box so a stale,
|
|
43
|
+
// unanswerable prompt doesn't linger — but only if it's still the one on screen, so a late
|
|
44
|
+
// withdrawal can't clobber a fresh prompt.
|
|
45
|
+
s.on("promptDone", (id) => setPrompt((p) => (p?.id === id ? undefined : p)));
|
|
42
46
|
s.on("settled", (v) => {
|
|
43
47
|
setVerdict(v);
|
|
44
48
|
setPrompt(undefined);
|
|
@@ -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/dist/core/interaction.js
CHANGED
|
@@ -29,6 +29,10 @@ export class Supervisor {
|
|
|
29
29
|
return new Promise((resolve, reject) => {
|
|
30
30
|
const onAbort = () => {
|
|
31
31
|
this.pending.delete(id);
|
|
32
|
+
// The child is giving up on this prompt (timeout or teardown) and moving on. Tell the
|
|
33
|
+
// supervisor so it withdraws the prompt from its UI — otherwise a stale, unanswerable
|
|
34
|
+
// prompt box lingers on screen for the rest of the run.
|
|
35
|
+
this.send({ kind: "prompt-done", id });
|
|
32
36
|
reject(new Error("prompt aborted"));
|
|
33
37
|
};
|
|
34
38
|
if (signal.aborted)
|
package/dist/core/types.d.ts
CHANGED
|
@@ -58,10 +58,11 @@ export interface OmContext {
|
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* A launched action: the live node's awaitable/observable surface, returned by
|
|
61
|
-
* **invoking an {@link Action}**. `.result`
|
|
62
|
-
*
|
|
63
|
-
* the
|
|
64
|
-
*
|
|
61
|
+
* **invoking an {@link Action}**. `.result` (like `.ref` and `once`) resolves the value on
|
|
62
|
+
* success and **rejects** with the action's error on failure (or `CancelledError` on cancel);
|
|
63
|
+
* reading it observes the activity, so a handled failure won't tear the run down. The body is
|
|
64
|
+
* committed one microtask after the launching call, so pre-body config (`withCache`) chained
|
|
65
|
+
* before the first `await` still applies.
|
|
65
66
|
*/
|
|
66
67
|
export interface Activity<Result = unknown, Events extends object = NoEvents, Handle = void> {
|
|
67
68
|
readonly id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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",
|