omkit 0.4.2 → 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.
@@ -68,11 +68,15 @@ export const chromePage = action("chromePage")
68
68
  // Oms run under tsx, which forces esbuild `keepNames`: every function serialized into
69
69
  // page.evaluate() carries a bare `__name(...)` wrapper that is undefined in the page and
70
70
  // throws `ReferenceError: __name is not defined`. Define a no-op __name so those bodies
71
- // run — seed the current document, and register it for any later navigation. Passed as a
72
- // string so tsx doesn't rewrite (and re-break) the shim itself.
71
+ // run. Scope it to the whole context, not just this page: downstream code often drives
72
+ // sibling windows via context.pages() (e.g. Electron windows), which never see a
73
+ // page-level shim. addInitScript covers documents created later; evaluate seeds the ones
74
+ // already open now (Electron windows exist before we attach). Passed as a string so tsx
75
+ // doesn't rewrite — and re-break — the shim itself.
73
76
  const nameShim = "globalThis.__name = globalThis.__name || function (f) { return f; };";
74
- await page.evaluate(nameShim);
75
- await page.addInitScript(nameShim);
77
+ const context = page.context();
78
+ await context.addInitScript(nameShim);
79
+ await Promise.all(context.pages().map((p) => p.evaluate(nameShim).catch(() => { })));
76
80
  attach(page); // resolves instance.ref for every downstream action
77
81
  console.log(`attached ${from}`);
78
82
  // Daemon: hold the page open until teardown, then drop the CDP session if ours.
@@ -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 { useState } from "react";
3
- import { Box, Text, useInput } from "ink";
4
- /** Case-insensitive substring filter on om names. */
5
- export function filterOms(oms, query) {
6
- const q = query.toLowerCase();
7
- return oms.filter((o) => o.name.toLowerCase().includes(q));
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
- const [query, setQuery] = useState("");
12
- const [index, setIndex] = useState(0);
13
- const filtered = filterOms(oms, query);
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 arrow-navigable choices. */
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
- /** Answer one pending prompt — free-text input, or arrow-navigable choices. */
5
- export function PromptView({ request, onAnswer, }) {
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
- // Refs hold the authoritative value: Ink can deliver several key events before React
9
- // re-renders, so the Enter handler must read a ref, not a possibly-stale render closure.
10
- // State mirrors the ref only to drive the display.
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
- const [index, setIndex] = useState(0);
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
- useInput((input, key) => (spec.kind === "choice" ? onChoiceKey(key) : onInputKey(input, key)));
42
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: spec.message }), spec.kind === "choice" ? (choices.map((c, i) => (_jsxs(Text, { inverse: i === index, children: [c.value === spec.default ? "* " : " ", c.label] }, c.value)))) : (_jsxs(Text, { children: ["> ", text] }))] }));
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.2",
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",