@stigmer/ink 3.0.8-dev.20260613085218 → 3.0.8
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/components/AgentPicker.d.ts +33 -0
- package/components/AgentPicker.d.ts.map +1 -0
- package/components/AgentPicker.js +50 -0
- package/components/AgentPicker.js.map +1 -0
- package/components/ResourcePicker.d.ts +72 -0
- package/components/ResourcePicker.d.ts.map +1 -0
- package/components/ResourcePicker.js +90 -0
- package/components/ResourcePicker.js.map +1 -0
- package/components/SessionPicker.d.ts +31 -0
- package/components/SessionPicker.d.ts.map +1 -0
- package/components/SessionPicker.js +86 -0
- package/components/SessionPicker.js.map +1 -0
- package/components/ToolCallItem.js +14 -0
- package/components/ToolCallItem.js.map +1 -1
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -1
- package/index.js +4 -0
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/data-pickers.test.tsx +108 -0
- package/src/__tests__/resource-picker.test.tsx +176 -0
- package/src/components/AgentPicker.tsx +84 -0
- package/src/components/ResourcePicker.tsx +187 -0
- package/src/components/SessionPicker.tsx +117 -0
- package/src/components/ToolCallItem.tsx +12 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SearchResult } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
|
|
2
|
+
/** Props for {@link AgentPicker}. */
|
|
3
|
+
export interface AgentPickerProps {
|
|
4
|
+
/** Organization to search within. */
|
|
5
|
+
readonly org: string;
|
|
6
|
+
/** Seeds the search box (e.g. an unresolved slug the user already typed). */
|
|
7
|
+
readonly initialQuery?: string;
|
|
8
|
+
/** `"org"` (default) searches the org; `"all"` includes public agents. */
|
|
9
|
+
readonly scope?: "org" | "all";
|
|
10
|
+
/** Called with the chosen agent's search result on selection. */
|
|
11
|
+
readonly onSelect: (agent: SearchResult) => void;
|
|
12
|
+
/** Called when the user cancels (Esc / Ctrl+C). */
|
|
13
|
+
readonly onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Interactive picker for choosing an agent by name.
|
|
17
|
+
*
|
|
18
|
+
* Composes the {@link useAgentSearch} data hook with {@link ResourcePicker}:
|
|
19
|
+
* the user types, results stream in (debounced by the hook), and the selected
|
|
20
|
+
* agent's {@link SearchResult} is returned via `onSelect`. Use the `id` field of
|
|
21
|
+
* that result to resolve and run the agent.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* render(
|
|
26
|
+
* <InkStigmerProvider client={client}>
|
|
27
|
+
* <AgentPicker org="acme" onSelect={(a) => run(a.id)} onCancel={() => process.exit(0)} />
|
|
28
|
+
* </InkStigmerProvider>,
|
|
29
|
+
* );
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function AgentPicker({ org, initialQuery, scope, onSelect, onCancel }: AgentPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
//# sourceMappingURL=AgentPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AgentPicker.d.ts","sourceRoot":"","sources":["../../src/components/AgentPicker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAC;AAI/E,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACjD,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;CAC/B;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CAkC7F"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
3
|
+
import { useAgentSearch } from "@stigmer/react";
|
|
4
|
+
import { ResourcePicker } from "./ResourcePicker.js";
|
|
5
|
+
/** Max description length shown as the row subtitle before truncation. */
|
|
6
|
+
const SUBTITLE_MAX = 60;
|
|
7
|
+
/**
|
|
8
|
+
* Interactive picker for choosing an agent by name.
|
|
9
|
+
*
|
|
10
|
+
* Composes the {@link useAgentSearch} data hook with {@link ResourcePicker}:
|
|
11
|
+
* the user types, results stream in (debounced by the hook), and the selected
|
|
12
|
+
* agent's {@link SearchResult} is returned via `onSelect`. Use the `id` field of
|
|
13
|
+
* that result to resolve and run the agent.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* render(
|
|
18
|
+
* <InkStigmerProvider client={client}>
|
|
19
|
+
* <AgentPicker org="acme" onSelect={(a) => run(a.id)} onCancel={() => process.exit(0)} />
|
|
20
|
+
* </InkStigmerProvider>,
|
|
21
|
+
* );
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function AgentPicker({ org, initialQuery, scope, onSelect, onCancel }) {
|
|
25
|
+
const { results, isLoading, error, query, setQuery } = useAgentSearch(org, { scope });
|
|
26
|
+
// Seed the typed-but-unresolved query once on mount so the user picks up
|
|
27
|
+
// where the smart-resolution attempt left off.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (initialQuery !== undefined && initialQuery !== "")
|
|
30
|
+
setQuery(initialQuery);
|
|
31
|
+
// Mount-only: re-seeding on every render would fight the user's typing.
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, []);
|
|
34
|
+
const items = useMemo(() => results.map(toAgentItem), [results]);
|
|
35
|
+
const handleSelect = useCallback((item) => {
|
|
36
|
+
const found = results.find((r) => r.id === item.id);
|
|
37
|
+
if (found !== undefined)
|
|
38
|
+
onSelect(found);
|
|
39
|
+
}, [results, onSelect]);
|
|
40
|
+
return (_jsx(ResourcePicker, { prompt: "Select an agent", items: items, query: query, onQueryChange: setQuery, isLoading: isLoading, error: error, onSelect: handleSelect, onCancel: onCancel, emptyLabel: "no agents found" }));
|
|
41
|
+
}
|
|
42
|
+
/** Map a search result to a picker row: org/slug title, description subtitle. */
|
|
43
|
+
function toAgentItem(result) {
|
|
44
|
+
const title = result.qualifiedSlug !== "" ? result.qualifiedSlug : `${result.org}/${result.slug}`;
|
|
45
|
+
return { id: result.id, title, subtitle: truncate(result.description, SUBTITLE_MAX) };
|
|
46
|
+
}
|
|
47
|
+
function truncate(text, max) {
|
|
48
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=AgentPicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AgentPicker.js","sourceRoot":"","sources":["../../src/components/AgentPicker.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAE/D,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAmB,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAgBtE,0EAA0E;AAC1E,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAoB;IAC5F,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAEtF,yEAAyE;IACzE,+CAA+C;IAC/C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,EAAE;YAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;QAC9E,wEAAwE;QACxE,uDAAuD;IACzD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,OAAO,CAAe,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAE/E,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,IAAgB,EAAE,EAAE;QACnB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC;QACpD,IAAI,KAAK,KAAK,SAAS;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC,EACD,CAAC,OAAO,EAAE,QAAQ,CAAC,CACpB,CAAC;IAEF,OAAO,CACL,KAAC,cAAc,IACb,MAAM,EAAC,iBAAiB,EACxB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,EACZ,aAAa,EAAE,QAAQ,EACvB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAC,iBAAiB,GAC5B,CACH,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,SAAS,WAAW,CAAC,MAAoB;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;IAClG,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE,CAAC;AACxF,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW;IACzC,OAAO,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single selectable entry in a {@link ResourcePicker}.
|
|
3
|
+
*
|
|
4
|
+
* Callers map their domain objects (agents, sessions, ...) into this shape and
|
|
5
|
+
* receive the chosen item back via `onSelect`. The picker is domain-agnostic:
|
|
6
|
+
* it knows only how to render and navigate these rows.
|
|
7
|
+
*/
|
|
8
|
+
export interface PickerItem {
|
|
9
|
+
/** Opaque identity returned to the caller on selection. */
|
|
10
|
+
readonly id: string;
|
|
11
|
+
/** Primary display text, rendered bold when active (e.g. "acme/deploy"). */
|
|
12
|
+
readonly title: string;
|
|
13
|
+
/** Secondary text, rendered dimmed after the title (e.g. a description). */
|
|
14
|
+
readonly subtitle?: string;
|
|
15
|
+
/** Right-aligned metadata, rendered dimmed (e.g. "2 hours ago"). */
|
|
16
|
+
readonly meta?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Props for {@link ResourcePicker}. */
|
|
19
|
+
export interface ResourcePickerProps {
|
|
20
|
+
/** The current result rows to choose from (owned by the caller). */
|
|
21
|
+
readonly items: readonly PickerItem[];
|
|
22
|
+
/** Current query text (controlled — the caller owns search state). */
|
|
23
|
+
readonly query: string;
|
|
24
|
+
/** Called when the query text changes; the caller re-runs its search. */
|
|
25
|
+
readonly onQueryChange: (query: string) => void;
|
|
26
|
+
/** Called with the chosen item when the user presses Enter. */
|
|
27
|
+
readonly onSelect: (item: PickerItem) => void;
|
|
28
|
+
/** Called when the user cancels (Esc or Ctrl+C). */
|
|
29
|
+
readonly onCancel: () => void;
|
|
30
|
+
/** `true` while a search request is in flight. */
|
|
31
|
+
readonly isLoading?: boolean;
|
|
32
|
+
/** Error from the last failed search, shown in place of the list. */
|
|
33
|
+
readonly error?: Error | null;
|
|
34
|
+
/** Label shown before the input (e.g. "Select an agent"). */
|
|
35
|
+
readonly prompt?: string;
|
|
36
|
+
/** Placeholder shown in the empty input. */
|
|
37
|
+
readonly placeholder?: string;
|
|
38
|
+
/** Label shown when a completed search returns no rows. */
|
|
39
|
+
readonly emptyLabel?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generic, interactive search-and-select list for the terminal.
|
|
43
|
+
*
|
|
44
|
+
* Renders a text input above a scrollable result list. The user types to
|
|
45
|
+
* filter (the caller performs the actual search via `onQueryChange`), navigates
|
|
46
|
+
* with the arrow keys, selects with Enter, and cancels with Esc. It holds no
|
|
47
|
+
* data of its own — pass `items`/`query`/`isLoading` from a data hook such as
|
|
48
|
+
* `useAgentSearch` or `useSessionList`.
|
|
49
|
+
*
|
|
50
|
+
* This is the reusable organism behind {@link AgentPicker} and
|
|
51
|
+
* {@link SessionPicker}; platform builders can compose it with any search
|
|
52
|
+
* function to pick any resource.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { results, isLoading, query, setQuery } = useAgentSearch("acme");
|
|
57
|
+
* const items = results.map((r) => ({ id: r.id, title: r.qualifiedSlug }));
|
|
58
|
+
* render(
|
|
59
|
+
* <ResourcePicker
|
|
60
|
+
* prompt="Select an agent"
|
|
61
|
+
* items={items}
|
|
62
|
+
* query={query}
|
|
63
|
+
* onQueryChange={setQuery}
|
|
64
|
+
* isLoading={isLoading}
|
|
65
|
+
* onSelect={(item) => console.log(item.id)}
|
|
66
|
+
* onCancel={() => process.exit(0)}
|
|
67
|
+
* />,
|
|
68
|
+
* );
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare function ResourcePicker({ items, query, onQueryChange, onSelect, onCancel, isLoading, error, prompt, placeholder, emptyLabel, }: ResourcePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
72
|
+
//# sourceMappingURL=ResourcePicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourcePicker.d.ts","sourceRoot":"","sources":["../../src/components/ResourcePicker.tsx"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,KAAK,EACL,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,SAAiB,EACjB,KAAY,EACZ,MAAiB,EACjB,WAAiC,EACjC,UAAyB,GAC1B,EAAE,mBAAmB,2CA6DrB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
/** Maximum rows shown at once; the list scrolls a window around the cursor. */
|
|
6
|
+
const MAX_VISIBLE = 10;
|
|
7
|
+
/**
|
|
8
|
+
* Generic, interactive search-and-select list for the terminal.
|
|
9
|
+
*
|
|
10
|
+
* Renders a text input above a scrollable result list. The user types to
|
|
11
|
+
* filter (the caller performs the actual search via `onQueryChange`), navigates
|
|
12
|
+
* with the arrow keys, selects with Enter, and cancels with Esc. It holds no
|
|
13
|
+
* data of its own — pass `items`/`query`/`isLoading` from a data hook such as
|
|
14
|
+
* `useAgentSearch` or `useSessionList`.
|
|
15
|
+
*
|
|
16
|
+
* This is the reusable organism behind {@link AgentPicker} and
|
|
17
|
+
* {@link SessionPicker}; platform builders can compose it with any search
|
|
18
|
+
* function to pick any resource.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const { results, isLoading, query, setQuery } = useAgentSearch("acme");
|
|
23
|
+
* const items = results.map((r) => ({ id: r.id, title: r.qualifiedSlug }));
|
|
24
|
+
* render(
|
|
25
|
+
* <ResourcePicker
|
|
26
|
+
* prompt="Select an agent"
|
|
27
|
+
* items={items}
|
|
28
|
+
* query={query}
|
|
29
|
+
* onQueryChange={setQuery}
|
|
30
|
+
* isLoading={isLoading}
|
|
31
|
+
* onSelect={(item) => console.log(item.id)}
|
|
32
|
+
* onCancel={() => process.exit(0)}
|
|
33
|
+
* />,
|
|
34
|
+
* );
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function ResourcePicker({ items, query, onQueryChange, onSelect, onCancel, isLoading = false, error = null, prompt = "Search", placeholder = "type to search...", emptyLabel = "no results", }) {
|
|
38
|
+
const [cursor, setCursor] = useState(0);
|
|
39
|
+
// Reset the cursor to the top whenever the query changes — the result set is
|
|
40
|
+
// about to be replaced, so a stale cursor would point at the wrong row.
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setCursor(0);
|
|
43
|
+
}, [query]);
|
|
44
|
+
const active = items.length === 0 ? 0 : Math.min(cursor, items.length - 1);
|
|
45
|
+
useInput((input, key) => {
|
|
46
|
+
if (key.escape || (key.ctrl && input === "c")) {
|
|
47
|
+
onCancel();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.return) {
|
|
51
|
+
if (items.length > 0)
|
|
52
|
+
onSelect(items[active]);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (key.upArrow) {
|
|
56
|
+
setCursor((c) => Math.max(0, Math.min(c, items.length - 1) - 1));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (key.downArrow) {
|
|
60
|
+
setCursor((c) => Math.min(items.length - 1, c + 1));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const [start, end] = visibleWindow(active, items.length);
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "?" }), _jsxs(Text, { bold: true, children: [prompt, ":"] }), _jsx(TextInput, { value: query, onChange: onQueryChange, placeholder: placeholder })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: error !== null ? (_jsx(Text, { color: "red", children: ` Error: ${error.message}` })) : isLoading ? (_jsx(Text, { dimColor: true, children: " searching..." })) : items.length === 0 ? (_jsx(Text, { dimColor: true, children: ` ${emptyLabel}` })) : (_jsxs(_Fragment, { children: [items.slice(start, end).map((item, i) => (_jsx(PickerRow, { item: item, isActive: start + i === active }, item.id))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ` ${countLabel(items.length)} | up/down: navigate | enter: select | esc: cancel` }) })] })) })] }));
|
|
65
|
+
}
|
|
66
|
+
/** One result row: a cursor glyph, the title, then optional subtitle/meta. */
|
|
67
|
+
function PickerRow({ item, isActive }) {
|
|
68
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? "cyan" : undefined, children: isActive ? "❯ " : " " }), _jsx(Text, { color: isActive ? "cyan" : undefined, bold: isActive, children: item.title }), item.subtitle !== undefined && item.subtitle !== "" ? _jsx(Text, { dimColor: true, children: ` ${item.subtitle}` }) : null, item.meta !== undefined && item.meta !== "" ? _jsx(Text, { dimColor: true, children: ` ${item.meta}` }) : null] }));
|
|
69
|
+
}
|
|
70
|
+
/** "1 result" / "N results" — matches the Go picker's footer wording. */
|
|
71
|
+
function countLabel(count) {
|
|
72
|
+
return `${count} result${count === 1 ? "" : "s"}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Compute the visible slice `[start, end)` so the active row stays in view,
|
|
76
|
+
* centering the cursor once the list grows past {@link MAX_VISIBLE}.
|
|
77
|
+
*/
|
|
78
|
+
function visibleWindow(active, total) {
|
|
79
|
+
if (total <= MAX_VISIBLE)
|
|
80
|
+
return [0, total];
|
|
81
|
+
const half = Math.floor(MAX_VISIBLE / 2);
|
|
82
|
+
let start = Math.max(0, active - half);
|
|
83
|
+
let end = start + MAX_VISIBLE;
|
|
84
|
+
if (end > total) {
|
|
85
|
+
end = total;
|
|
86
|
+
start = end - MAX_VISIBLE;
|
|
87
|
+
}
|
|
88
|
+
return [start, end];
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=ResourcePicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourcePicker.js","sourceRoot":"","sources":["../../src/components/ResourcePicker.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
2
|
+
/** Props for {@link SessionPicker}. */
|
|
3
|
+
export interface SessionPickerProps {
|
|
4
|
+
/** Seeds the filter box (e.g. text the user already typed). */
|
|
5
|
+
readonly initialQuery?: string;
|
|
6
|
+
/** Maximum sessions to fetch. Defaults to 50. */
|
|
7
|
+
readonly pageSize?: number;
|
|
8
|
+
/** Called with the chosen session on selection. */
|
|
9
|
+
readonly onSelect: (session: Session) => void;
|
|
10
|
+
/** Called when the user cancels (Esc / Ctrl+C). */
|
|
11
|
+
readonly onCancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Interactive picker for re-opening an existing session.
|
|
15
|
+
*
|
|
16
|
+
* Unlike agents, sessions are not search-indexed, so this composes
|
|
17
|
+
* {@link useSessionList} with client-side text filtering over the recent
|
|
18
|
+
* session list. The selected {@link Session} is returned via `onSelect`; use
|
|
19
|
+
* `session.metadata.id` to resume it.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* render(
|
|
24
|
+
* <InkStigmerProvider client={client}>
|
|
25
|
+
* <SessionPicker onSelect={(s) => resume(s.metadata?.id)} onCancel={() => process.exit(0)} />
|
|
26
|
+
* </InkStigmerProvider>,
|
|
27
|
+
* );
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function SessionPicker({ initialQuery, pageSize, onSelect, onCancel }: SessionPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
//# sourceMappingURL=SessionPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionPicker.d.ts","sourceRoot":"","sources":["../../src/components/SessionPicker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sDAAsD,CAAC;AAIpF,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;CAC/B;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,EAAE,YAAY,EAAE,QAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CA4BpG"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useSessionList } from "@stigmer/react";
|
|
4
|
+
import { ResourcePicker } from "./ResourcePicker.js";
|
|
5
|
+
const PENDING_SUBJECT = "Auto-created session";
|
|
6
|
+
/**
|
|
7
|
+
* Interactive picker for re-opening an existing session.
|
|
8
|
+
*
|
|
9
|
+
* Unlike agents, sessions are not search-indexed, so this composes
|
|
10
|
+
* {@link useSessionList} with client-side text filtering over the recent
|
|
11
|
+
* session list. The selected {@link Session} is returned via `onSelect`; use
|
|
12
|
+
* `session.metadata.id` to resume it.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* render(
|
|
17
|
+
* <InkStigmerProvider client={client}>
|
|
18
|
+
* <SessionPicker onSelect={(s) => resume(s.metadata?.id)} onCancel={() => process.exit(0)} />
|
|
19
|
+
* </InkStigmerProvider>,
|
|
20
|
+
* );
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function SessionPicker({ initialQuery, pageSize = 50, onSelect, onCancel }) {
|
|
24
|
+
const { sessions, isLoading, error } = useSessionList({ pageSize });
|
|
25
|
+
const [query, setQuery] = useState(initialQuery ?? "");
|
|
26
|
+
const filtered = useMemo(() => filterSessions(sessions, query), [sessions, query]);
|
|
27
|
+
const items = useMemo(() => filtered.map(toSessionItem), [filtered]);
|
|
28
|
+
const handleSelect = useCallback((item) => {
|
|
29
|
+
const found = sessions.find((s) => (s.metadata?.id ?? "") === item.id);
|
|
30
|
+
if (found !== undefined)
|
|
31
|
+
onSelect(found);
|
|
32
|
+
}, [sessions, onSelect]);
|
|
33
|
+
return (_jsx(ResourcePicker, { prompt: "Select a session", items: items, query: query, onQueryChange: setQuery, isLoading: isLoading, error: error, onSelect: handleSelect, onCancel: onCancel, emptyLabel: "no sessions found" }));
|
|
34
|
+
}
|
|
35
|
+
/** Case-insensitive filter over a session's subject, agent, and id. */
|
|
36
|
+
function filterSessions(sessions, query) {
|
|
37
|
+
const needle = query.trim().toLowerCase();
|
|
38
|
+
if (needle === "")
|
|
39
|
+
return sessions;
|
|
40
|
+
return sessions.filter((s) => sessionHaystack(s).includes(needle));
|
|
41
|
+
}
|
|
42
|
+
function sessionHaystack(session) {
|
|
43
|
+
return [sessionSubject(session), session.spec?.agentInstanceId ?? "", session.metadata?.id ?? ""]
|
|
44
|
+
.join(" ")
|
|
45
|
+
.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
/** Map a session to a picker row: subject title, agent subtitle, age meta. */
|
|
48
|
+
function toSessionItem(session) {
|
|
49
|
+
return {
|
|
50
|
+
id: session.metadata?.id ?? "",
|
|
51
|
+
title: sessionSubject(session),
|
|
52
|
+
subtitle: session.spec?.agentInstanceId ?? "",
|
|
53
|
+
meta: relativeTime(session.status?.audit?.specAudit?.createdAt),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// The session's display subject, suppressing the backend's auto-create
|
|
57
|
+
// sentinel (the async title activity replaces it shortly after creation).
|
|
58
|
+
function sessionSubject(session) {
|
|
59
|
+
const subject = session.spec?.subject ?? "";
|
|
60
|
+
if (subject !== "" && subject !== PENDING_SUBJECT)
|
|
61
|
+
return subject;
|
|
62
|
+
return session.metadata?.name !== undefined && session.metadata.name !== ""
|
|
63
|
+
? session.metadata.name
|
|
64
|
+
: (session.metadata?.id ?? "(untitled session)");
|
|
65
|
+
}
|
|
66
|
+
// Coarse "N units ago" label from a protobuf Timestamp. Empty when unknown.
|
|
67
|
+
function relativeTime(ts) {
|
|
68
|
+
if (ts === undefined)
|
|
69
|
+
return "";
|
|
70
|
+
const then = Number(ts.seconds) * 1000;
|
|
71
|
+
if (!Number.isFinite(then) || then <= 0)
|
|
72
|
+
return "";
|
|
73
|
+
const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
74
|
+
const units = [
|
|
75
|
+
[86400, "day"],
|
|
76
|
+
[3600, "hour"],
|
|
77
|
+
[60, "minute"],
|
|
78
|
+
];
|
|
79
|
+
for (const [size, label] of units) {
|
|
80
|
+
const value = Math.floor(seconds / size);
|
|
81
|
+
if (value >= 1)
|
|
82
|
+
return `${value} ${label}${value === 1 ? "" : "s"} ago`;
|
|
83
|
+
}
|
|
84
|
+
return "just now";
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=SessionPicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionPicker.js","sourceRoot":"","sources":["../../src/components/SessionPicker.tsx"],"names":[],"mappings":"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"}
|
|
@@ -69,12 +69,26 @@ function describeResultView(view) {
|
|
|
69
69
|
return truncate(view.text);
|
|
70
70
|
case "json":
|
|
71
71
|
return truncate(JSON.stringify(view.value));
|
|
72
|
+
case "outputRef": {
|
|
73
|
+
const size = view.sizeBytes ? ` (${formatBytes(view.sizeBytes)})` : "";
|
|
74
|
+
if (view.isImage)
|
|
75
|
+
return `image${size}: ${view.downloadUrl}`;
|
|
76
|
+
const preview = view.preview ? `${truncate(view.preview)}\n` : "";
|
|
77
|
+
return `${preview}full output${size}: ${view.downloadUrl}`;
|
|
78
|
+
}
|
|
72
79
|
case "error":
|
|
73
80
|
return view.message;
|
|
74
81
|
case "empty":
|
|
75
82
|
return null;
|
|
76
83
|
}
|
|
77
84
|
}
|
|
85
|
+
function formatBytes(n) {
|
|
86
|
+
if (n >= 1024 * 1024)
|
|
87
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
88
|
+
if (n >= 1024)
|
|
89
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
90
|
+
return `${n} B`;
|
|
91
|
+
}
|
|
78
92
|
function truncate(s, maxLines = 5) {
|
|
79
93
|
const lines = s.split("\n");
|
|
80
94
|
if (lines.length <= maxLines)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolCallItem.js","sourceRoot":"","sources":["../../src/components/ToolCallItem.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAEhC,OAAO,EAAE,cAAc,EAAE,MAAM,8DAA8D,CAAC;AAC9F,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAW9E,MAAM,gBAAgB,GAAuD;IAC3E,CAAC,cAAc,CAAC,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE;IACpE,CAAC,cAAc,CAAC,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE;IACrE,CAAC,cAAc,CAAC,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE;CACjE,CAAC;AAEF,8EAA8E;AAC9E,wEAAwE;AACxE,qEAAqE;AACrE,MAAM,UAAU,GAAsC;IACpD,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM;IAC5B,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO;IAC9B,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM;IAC5B,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ;IAChC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO;IACzB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ;IAC3B,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;IACvB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO;IACzB,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY;IACnC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU;IAC5B,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO;IACxB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW;CACjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,KAAK,EAAqB;IAC5E,MAAM,SAAS,GAAG,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAEvE,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAE9D,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,MAAC,GAAG,IAAC,GAAG,EAAE,CAAC,aACT,KAAC,IAAI,IAAC,KAAK,EAAE,SAAS,CAAC,KAAK,YAAG,SAAS,CAAC,MAAM,GAAQ,EACvD,KAAC,IAAI,cAAE,KAAK,GAAQ,EACnB,QAAQ,CAAC,MAAM,KAAK,cAAc,CAAC,iBAAiB,IAAI,CACvD,KAAC,IAAI,IAAC,QAAQ,8BAAe,CAC9B,IACG,EACL,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CACvB,QAAQ,IAAI,CACV,KAAC,GAAG,IAAC,WAAW,EAAE,CAAC,YACjB,KAAC,IAAI,IAAC,KAAK,EAAC,KAAK,EAAC,IAAI,EAAC,cAAc,YAClC,IAAI,CAAC,OAAO,GACR,GACH,CACP,CACF,CAAC,CAAC,CAAC,CACF,UAAU,IAAI,CACZ,KAAC,GAAG,IAAC,WAAW,EAAE,CAAC,YACjB,KAAC,IAAI,IAAC,QAAQ,QAAC,IAAI,EAAC,cAAc,YAC/B,UAAU,GACN,GACH,CACP,CACF,IACG,CACP,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,QAAkB,EAAE,IAAc;IACnD,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;QAC3B,OAAO,GAAG,QAAQ,CAAC,aAAa,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC;AAC3C,CAAC;AAED,gFAAgF;AAChF,SAAS,kBAAkB,CAAC,IAAoB;IAC9C,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,KAAK,GACT,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;gBAC9D,CAAC,CAAC,MAAM,IAAI,CAAC,UAAU,IAAI,CAAC,KAAK,IAAI,CAAC,YAAY,IAAI,CAAC,GAAG;gBAC1D,CAAC,CAAC,EAAE,CAAC;YACT,OAAO,GAAG,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;QAChC,CAAC;QACD,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,MAAM,IAAI,GACR,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACvF,OAAO,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;QACrD,CAAC;QACD,KAAK,QAAQ;YACX,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;QACnE,KAAK,MAAM;YACT,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QAChE,KAAK,MAAM;YACT,OAAO,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC;QAC3B,KAAK,eAAe;YAClB,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7E,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9C,KAAK,OAAO;YACV,OAAO,IAAI,CAAC,OAAO,CAAC;QACtB,KAAK,OAAO;YACV,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,QAAQ,GAAG,CAAC;IACvC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,UAAU,KAAK,CAAC,MAAM,GAAG,QAAQ,cAAc,CAAC;AAC/F,CAAC"}
|
|
1
|
+
{"version":3,"file":"ToolCallItem.js","sourceRoot":"","sources":["../../src/components/ToolCallItem.tsx"],"names":[],"mappings":"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"}
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export { ExecutionProgress, type ExecutionProgressProps } from "./components/Exe
|
|
|
12
12
|
export { FollowUpInput, type FollowUpInputProps } from "./components/FollowUpInput.js";
|
|
13
13
|
export { UsageWidget, type UsageWidgetProps } from "./components/UsageWidget.js";
|
|
14
14
|
export { ContextGauge, type ContextGaugeProps } from "./components/ContextGauge.js";
|
|
15
|
+
export { ResourcePicker, type ResourcePickerProps, type PickerItem } from "./components/ResourcePicker.js";
|
|
16
|
+
export { AgentPicker, type AgentPickerProps } from "./components/AgentPicker.js";
|
|
17
|
+
export { SessionPicker, type SessionPickerProps } from "./components/SessionPicker.js";
|
|
15
18
|
export { SessionView, type SessionViewProps, type InteractionMode } from "./app/SessionView.js";
|
|
16
19
|
export { SessionApp, type SessionAppProps } from "./app/SessionApp.js";
|
|
17
20
|
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAGjF,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AACnG,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGpF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAChG,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAGjF,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AACnG,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGpF,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC3G,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAGvF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAChG,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC"}
|
package/index.js
CHANGED
|
@@ -16,6 +16,10 @@ export { ExecutionProgress } from "./components/ExecutionProgress.js";
|
|
|
16
16
|
export { FollowUpInput } from "./components/FollowUpInput.js";
|
|
17
17
|
export { UsageWidget } from "./components/UsageWidget.js";
|
|
18
18
|
export { ContextGauge } from "./components/ContextGauge.js";
|
|
19
|
+
// Pickers (interactive search-and-select)
|
|
20
|
+
export { ResourcePicker } from "./components/ResourcePicker.js";
|
|
21
|
+
export { AgentPicker } from "./components/AgentPicker.js";
|
|
22
|
+
export { SessionPicker } from "./components/SessionPicker.js";
|
|
19
23
|
// Composed views
|
|
20
24
|
export { SessionView } from "./app/SessionView.js";
|
|
21
25
|
export { SessionApp } from "./app/SessionApp.js";
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,WAAW;AACX,OAAO,EAAE,kBAAkB,EAAgC,MAAM,eAAe,CAAC;AAEjF,iDAAiD;AACjD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAE3B,WAAW;AACX,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,aAAa;AACb,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAsB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAA4B,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAA+B,MAAM,mCAAmC,CAAC;AACnG,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,WAAW,EAAyB,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AAEpF,iBAAiB;AACjB,OAAO,EAAE,WAAW,EAA+C,MAAM,sBAAsB,CAAC;AAChG,OAAO,EAAE,UAAU,EAAwB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,WAAW;AACX,OAAO,EAAE,kBAAkB,EAAgC,MAAM,eAAe,CAAC;AAEjF,iDAAiD;AACjD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAE3B,WAAW;AACX,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,aAAa;AACb,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAsB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAA4B,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAA+B,MAAM,mCAAmC,CAAC;AACnG,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,WAAW,EAAyB,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AAEpF,0CAA0C;AAC1C,OAAO,EAAE,cAAc,EAA6C,MAAM,gCAAgC,CAAC;AAC3G,OAAO,EAAE,WAAW,EAAyB,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AAEvF,iBAAiB;AACjB,OAAO,EAAE,WAAW,EAA+C,MAAM,sBAAsB,CAAC;AAChG,OAAO,EAAE,UAAU,EAAwB,MAAM,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/ink",
|
|
3
|
-
"version": "3.0.8
|
|
3
|
+
"version": "3.0.8",
|
|
4
4
|
"description": "Ink (React for terminals) components for rendering Stigmer agent sessions in the terminal",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@connectrpc/connect-node": "^2.1.1",
|
|
38
|
-
"@stigmer/react": "3.0.8
|
|
38
|
+
"@stigmer/react": "3.0.8",
|
|
39
39
|
"ink-spinner": "^5.0.0",
|
|
40
40
|
"ink-text-input": "^6.0.0",
|
|
41
41
|
"marked": "^15.0.7",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@bufbuild/protobuf": "^2.0.0",
|
|
46
|
-
"@stigmer/protos": "3.0.8
|
|
47
|
-
"@stigmer/sdk": "3.0.8
|
|
46
|
+
"@stigmer/protos": "3.0.8",
|
|
47
|
+
"@stigmer/sdk": "3.0.8",
|
|
48
48
|
"ink": "^7.0.0",
|
|
49
49
|
"react": ">=19.0.0"
|
|
50
50
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { render } from "ink-testing-library";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
6
|
+
import { SearchResultSchema } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
|
|
7
|
+
import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
8
|
+
import { InkStigmerProvider } from "../provider.js";
|
|
9
|
+
import { AgentPicker } from "../components/AgentPicker.js";
|
|
10
|
+
import { SessionPicker } from "../components/SessionPicker.js";
|
|
11
|
+
|
|
12
|
+
const KEY = { enter: "\r" } as const;
|
|
13
|
+
|
|
14
|
+
// Allow the hooks' async fetch + a re-render to settle.
|
|
15
|
+
function settle(): Promise<void> {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, 60));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function agentResult(id: string, slug: string, description: string) {
|
|
20
|
+
return create(SearchResultSchema, { id, qualifiedSlug: slug, description });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function session(id: string, subject: string, agentInstanceId: string) {
|
|
24
|
+
return create(SessionSchema, {
|
|
25
|
+
metadata: { id },
|
|
26
|
+
spec: { subject, agentInstanceId },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Minimal Stigmer stub exposing only the methods the picker hooks call.
|
|
31
|
+
function fakeClient(overrides: Record<string, unknown>): Stigmer {
|
|
32
|
+
return overrides as unknown as Stigmer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("AgentPicker", () => {
|
|
36
|
+
it("renders search results and returns the chosen agent on Enter", async () => {
|
|
37
|
+
const results = [
|
|
38
|
+
agentResult("agt_1", "acme/alpha", "first"),
|
|
39
|
+
agentResult("agt_2", "acme/beta", "second"),
|
|
40
|
+
];
|
|
41
|
+
const client = fakeClient({
|
|
42
|
+
agent: { list: vi.fn(async () => ({ entries: results, totalPages: 1, totalCount: 2 })) },
|
|
43
|
+
});
|
|
44
|
+
const onSelect = vi.fn();
|
|
45
|
+
|
|
46
|
+
const { lastFrame, stdin } = render(
|
|
47
|
+
<InkStigmerProvider client={client}>
|
|
48
|
+
<AgentPicker org="acme" onSelect={onSelect} onCancel={vi.fn()} />
|
|
49
|
+
</InkStigmerProvider>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await settle();
|
|
53
|
+
expect(lastFrame() ?? "").toContain("acme/alpha");
|
|
54
|
+
expect(lastFrame() ?? "").toContain("first");
|
|
55
|
+
|
|
56
|
+
stdin.write(KEY.enter);
|
|
57
|
+
await settle();
|
|
58
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(onSelect.mock.calls[0][0].id).toBe("agt_1");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("seeds the search box from initialQuery", async () => {
|
|
63
|
+
const list = vi.fn(async () => ({ entries: [], totalPages: 0, totalCount: 0 }));
|
|
64
|
+
const client = fakeClient({ agent: { list } });
|
|
65
|
+
|
|
66
|
+
const { lastFrame } = render(
|
|
67
|
+
<InkStigmerProvider client={client}>
|
|
68
|
+
<AgentPicker org="acme" initialQuery="deploy" onSelect={vi.fn()} onCancel={vi.fn()} />
|
|
69
|
+
</InkStigmerProvider>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await settle();
|
|
73
|
+
// The picker pre-fills the input with the unresolved query the user
|
|
74
|
+
// already typed; useAgentSearch then re-searches it (debounced).
|
|
75
|
+
expect(lastFrame() ?? "").toContain("deploy");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("SessionPicker", () => {
|
|
80
|
+
it("client-side filters the session list and returns the chosen session", async () => {
|
|
81
|
+
const sessions = [
|
|
82
|
+
session("ses_1", "deploy staging", "ai_1"),
|
|
83
|
+
session("ses_2", "review PR", "ai_2"),
|
|
84
|
+
session("ses_3", "deploy prod", "ai_3"),
|
|
85
|
+
];
|
|
86
|
+
const client = fakeClient({
|
|
87
|
+
session: { list: vi.fn(async () => ({ entries: sessions })) },
|
|
88
|
+
});
|
|
89
|
+
const onSelect = vi.fn();
|
|
90
|
+
|
|
91
|
+
const { lastFrame, stdin } = render(
|
|
92
|
+
<InkStigmerProvider client={client}>
|
|
93
|
+
<SessionPicker initialQuery="deploy" onSelect={onSelect} onCancel={vi.fn()} />
|
|
94
|
+
</InkStigmerProvider>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await settle();
|
|
98
|
+
const frame = lastFrame() ?? "";
|
|
99
|
+
expect(frame).toContain("deploy staging");
|
|
100
|
+
expect(frame).toContain("deploy prod");
|
|
101
|
+
expect(frame).not.toContain("review PR");
|
|
102
|
+
|
|
103
|
+
stdin.write(KEY.enter);
|
|
104
|
+
await settle();
|
|
105
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(onSelect.mock.calls[0][0].metadata.id).toBe("ses_1");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { render } from "ink-testing-library";
|
|
4
|
+
import { type PickerItem, ResourcePicker } from "../components/ResourcePicker.js";
|
|
5
|
+
|
|
6
|
+
// Key escape sequences understood by ink's input parser.
|
|
7
|
+
const KEY = {
|
|
8
|
+
up: "\u001B[A",
|
|
9
|
+
down: "\u001B[B",
|
|
10
|
+
enter: "\r",
|
|
11
|
+
escape: "\u001B",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
const ITEMS: PickerItem[] = [
|
|
15
|
+
{ id: "agt_1", title: "acme/alpha", subtitle: "first agent" },
|
|
16
|
+
{ id: "agt_2", title: "acme/beta", subtitle: "second agent" },
|
|
17
|
+
{ id: "agt_3", title: "acme/gamma", subtitle: "third agent", meta: "2 hours ago" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Ink processes stdin asynchronously; yield a macrotask so the frame settles.
|
|
21
|
+
function tick(): Promise<void> {
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, 20));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function noop(): void {}
|
|
26
|
+
|
|
27
|
+
describe("ResourcePicker", () => {
|
|
28
|
+
it("renders the prompt, all items, and the result count", () => {
|
|
29
|
+
const { lastFrame } = render(
|
|
30
|
+
<ResourcePicker
|
|
31
|
+
prompt="Select an agent"
|
|
32
|
+
items={ITEMS}
|
|
33
|
+
query=""
|
|
34
|
+
onQueryChange={noop}
|
|
35
|
+
onSelect={noop}
|
|
36
|
+
onCancel={noop}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
const out = lastFrame() ?? "";
|
|
40
|
+
expect(out).toContain("Select an agent");
|
|
41
|
+
expect(out).toContain("acme/alpha");
|
|
42
|
+
expect(out).toContain("acme/gamma");
|
|
43
|
+
expect(out).toContain("first agent");
|
|
44
|
+
expect(out).toContain("2 hours ago");
|
|
45
|
+
expect(out).toContain("3 results");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("marks the first item active by default", () => {
|
|
49
|
+
const { lastFrame } = render(
|
|
50
|
+
<ResourcePicker items={ITEMS} query="" onQueryChange={noop} onSelect={noop} onCancel={noop} />,
|
|
51
|
+
);
|
|
52
|
+
expect(lastFrame() ?? "").toContain("❯ acme/alpha");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("selects the active item on Enter", async () => {
|
|
56
|
+
const onSelect = vi.fn();
|
|
57
|
+
const { stdin } = render(
|
|
58
|
+
<ResourcePicker items={ITEMS} query="" onQueryChange={noop} onSelect={onSelect} onCancel={noop} />,
|
|
59
|
+
);
|
|
60
|
+
stdin.write(KEY.enter);
|
|
61
|
+
await tick();
|
|
62
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(onSelect).toHaveBeenCalledWith(ITEMS[0]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("moves the cursor down and selects the new active item", async () => {
|
|
67
|
+
const onSelect = vi.fn();
|
|
68
|
+
const { stdin } = render(
|
|
69
|
+
<ResourcePicker items={ITEMS} query="" onQueryChange={noop} onSelect={onSelect} onCancel={noop} />,
|
|
70
|
+
);
|
|
71
|
+
stdin.write(KEY.down);
|
|
72
|
+
await tick();
|
|
73
|
+
stdin.write(KEY.down);
|
|
74
|
+
await tick();
|
|
75
|
+
stdin.write(KEY.enter);
|
|
76
|
+
await tick();
|
|
77
|
+
expect(onSelect).toHaveBeenCalledWith(ITEMS[2]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("clamps cursor movement at the list boundaries", async () => {
|
|
81
|
+
const onSelect = vi.fn();
|
|
82
|
+
const { stdin } = render(
|
|
83
|
+
<ResourcePicker items={ITEMS} query="" onQueryChange={noop} onSelect={onSelect} onCancel={noop} />,
|
|
84
|
+
);
|
|
85
|
+
// Up at the top stays on the first row.
|
|
86
|
+
stdin.write(KEY.up);
|
|
87
|
+
await tick();
|
|
88
|
+
stdin.write(KEY.enter);
|
|
89
|
+
await tick();
|
|
90
|
+
expect(onSelect).toHaveBeenLastCalledWith(ITEMS[0]);
|
|
91
|
+
// Past the bottom stays on the last row.
|
|
92
|
+
stdin.write(KEY.down);
|
|
93
|
+
await tick();
|
|
94
|
+
stdin.write(KEY.down);
|
|
95
|
+
await tick();
|
|
96
|
+
stdin.write(KEY.down);
|
|
97
|
+
await tick();
|
|
98
|
+
stdin.write(KEY.enter);
|
|
99
|
+
await tick();
|
|
100
|
+
expect(onSelect).toHaveBeenLastCalledWith(ITEMS[2]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("cancels on Escape without selecting", async () => {
|
|
104
|
+
const onSelect = vi.fn();
|
|
105
|
+
const onCancel = vi.fn();
|
|
106
|
+
const { stdin } = render(
|
|
107
|
+
<ResourcePicker items={ITEMS} query="" onQueryChange={noop} onSelect={onSelect} onCancel={onCancel} />,
|
|
108
|
+
);
|
|
109
|
+
stdin.write(KEY.escape);
|
|
110
|
+
await tick();
|
|
111
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("forwards typed characters to onQueryChange", async () => {
|
|
116
|
+
const onQueryChange = vi.fn();
|
|
117
|
+
const { stdin } = render(
|
|
118
|
+
<ResourcePicker items={ITEMS} query="" onQueryChange={onQueryChange} onSelect={noop} onCancel={noop} />,
|
|
119
|
+
);
|
|
120
|
+
stdin.write("a");
|
|
121
|
+
await tick();
|
|
122
|
+
expect(onQueryChange).toHaveBeenCalledWith("a");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("does not select when Enter is pressed on an empty list", async () => {
|
|
126
|
+
const onSelect = vi.fn();
|
|
127
|
+
const { stdin, lastFrame } = render(
|
|
128
|
+
<ResourcePicker items={[]} query="zzz" onQueryChange={noop} onSelect={onSelect} onCancel={noop} />,
|
|
129
|
+
);
|
|
130
|
+
expect(lastFrame() ?? "").toContain("no results");
|
|
131
|
+
stdin.write(KEY.enter);
|
|
132
|
+
await tick();
|
|
133
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("shows a loading state while searching", () => {
|
|
137
|
+
const { lastFrame } = render(
|
|
138
|
+
<ResourcePicker items={[]} query="x" onQueryChange={noop} onSelect={noop} onCancel={noop} isLoading />,
|
|
139
|
+
);
|
|
140
|
+
expect(lastFrame() ?? "").toContain("searching...");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("shows an error state in place of the list", () => {
|
|
144
|
+
const { lastFrame } = render(
|
|
145
|
+
<ResourcePicker
|
|
146
|
+
items={[]}
|
|
147
|
+
query="x"
|
|
148
|
+
onQueryChange={noop}
|
|
149
|
+
onSelect={noop}
|
|
150
|
+
onCancel={noop}
|
|
151
|
+
error={new Error("backend unreachable")}
|
|
152
|
+
/>,
|
|
153
|
+
);
|
|
154
|
+
expect(lastFrame() ?? "").toContain("Error: backend unreachable");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("scrolls the visible window to keep the active row in view", async () => {
|
|
158
|
+
const many: PickerItem[] = Array.from({ length: 14 }, (_, i) => ({
|
|
159
|
+
id: `id_${i}`,
|
|
160
|
+
title: `item-${i}`,
|
|
161
|
+
}));
|
|
162
|
+
const { stdin, lastFrame } = render(
|
|
163
|
+
<ResourcePicker items={many} query="" onQueryChange={noop} onSelect={noop} onCancel={noop} />,
|
|
164
|
+
);
|
|
165
|
+
// Initially the top of the list is visible, the tail is not.
|
|
166
|
+
expect(lastFrame() ?? "").toContain("item-0");
|
|
167
|
+
expect(lastFrame() ?? "").not.toContain("item-13");
|
|
168
|
+
// Drive the cursor to the bottom; the window should scroll to reveal it.
|
|
169
|
+
for (let i = 0; i < 13; i++) {
|
|
170
|
+
stdin.write(KEY.down);
|
|
171
|
+
await tick();
|
|
172
|
+
}
|
|
173
|
+
expect(lastFrame() ?? "").toContain("item-13");
|
|
174
|
+
expect(lastFrame() ?? "").not.toContain("item-0");
|
|
175
|
+
}, 20000);
|
|
176
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import type { SearchResult } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
|
|
3
|
+
import { useAgentSearch } from "@stigmer/react";
|
|
4
|
+
import { type PickerItem, ResourcePicker } from "./ResourcePicker.js";
|
|
5
|
+
|
|
6
|
+
/** Props for {@link AgentPicker}. */
|
|
7
|
+
export interface AgentPickerProps {
|
|
8
|
+
/** Organization to search within. */
|
|
9
|
+
readonly org: string;
|
|
10
|
+
/** Seeds the search box (e.g. an unresolved slug the user already typed). */
|
|
11
|
+
readonly initialQuery?: string;
|
|
12
|
+
/** `"org"` (default) searches the org; `"all"` includes public agents. */
|
|
13
|
+
readonly scope?: "org" | "all";
|
|
14
|
+
/** Called with the chosen agent's search result on selection. */
|
|
15
|
+
readonly onSelect: (agent: SearchResult) => void;
|
|
16
|
+
/** Called when the user cancels (Esc / Ctrl+C). */
|
|
17
|
+
readonly onCancel: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Max description length shown as the row subtitle before truncation. */
|
|
21
|
+
const SUBTITLE_MAX = 60;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Interactive picker for choosing an agent by name.
|
|
25
|
+
*
|
|
26
|
+
* Composes the {@link useAgentSearch} data hook with {@link ResourcePicker}:
|
|
27
|
+
* the user types, results stream in (debounced by the hook), and the selected
|
|
28
|
+
* agent's {@link SearchResult} is returned via `onSelect`. Use the `id` field of
|
|
29
|
+
* that result to resolve and run the agent.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* render(
|
|
34
|
+
* <InkStigmerProvider client={client}>
|
|
35
|
+
* <AgentPicker org="acme" onSelect={(a) => run(a.id)} onCancel={() => process.exit(0)} />
|
|
36
|
+
* </InkStigmerProvider>,
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function AgentPicker({ org, initialQuery, scope, onSelect, onCancel }: AgentPickerProps) {
|
|
41
|
+
const { results, isLoading, error, query, setQuery } = useAgentSearch(org, { scope });
|
|
42
|
+
|
|
43
|
+
// Seed the typed-but-unresolved query once on mount so the user picks up
|
|
44
|
+
// where the smart-resolution attempt left off.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (initialQuery !== undefined && initialQuery !== "") setQuery(initialQuery);
|
|
47
|
+
// Mount-only: re-seeding on every render would fight the user's typing.
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const items = useMemo<PickerItem[]>(() => results.map(toAgentItem), [results]);
|
|
52
|
+
|
|
53
|
+
const handleSelect = useCallback(
|
|
54
|
+
(item: PickerItem) => {
|
|
55
|
+
const found = results.find((r) => r.id === item.id);
|
|
56
|
+
if (found !== undefined) onSelect(found);
|
|
57
|
+
},
|
|
58
|
+
[results, onSelect],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ResourcePicker
|
|
63
|
+
prompt="Select an agent"
|
|
64
|
+
items={items}
|
|
65
|
+
query={query}
|
|
66
|
+
onQueryChange={setQuery}
|
|
67
|
+
isLoading={isLoading}
|
|
68
|
+
error={error}
|
|
69
|
+
onSelect={handleSelect}
|
|
70
|
+
onCancel={onCancel}
|
|
71
|
+
emptyLabel="no agents found"
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Map a search result to a picker row: org/slug title, description subtitle. */
|
|
77
|
+
function toAgentItem(result: SearchResult): PickerItem {
|
|
78
|
+
const title = result.qualifiedSlug !== "" ? result.qualifiedSlug : `${result.org}/${result.slug}`;
|
|
79
|
+
return { id: result.id, title, subtitle: truncate(result.description, SUBTITLE_MAX) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function truncate(text: string, max: number): string {
|
|
83
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single selectable entry in a {@link ResourcePicker}.
|
|
7
|
+
*
|
|
8
|
+
* Callers map their domain objects (agents, sessions, ...) into this shape and
|
|
9
|
+
* receive the chosen item back via `onSelect`. The picker is domain-agnostic:
|
|
10
|
+
* it knows only how to render and navigate these rows.
|
|
11
|
+
*/
|
|
12
|
+
export interface PickerItem {
|
|
13
|
+
/** Opaque identity returned to the caller on selection. */
|
|
14
|
+
readonly id: string;
|
|
15
|
+
/** Primary display text, rendered bold when active (e.g. "acme/deploy"). */
|
|
16
|
+
readonly title: string;
|
|
17
|
+
/** Secondary text, rendered dimmed after the title (e.g. a description). */
|
|
18
|
+
readonly subtitle?: string;
|
|
19
|
+
/** Right-aligned metadata, rendered dimmed (e.g. "2 hours ago"). */
|
|
20
|
+
readonly meta?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Props for {@link ResourcePicker}. */
|
|
24
|
+
export interface ResourcePickerProps {
|
|
25
|
+
/** The current result rows to choose from (owned by the caller). */
|
|
26
|
+
readonly items: readonly PickerItem[];
|
|
27
|
+
/** Current query text (controlled — the caller owns search state). */
|
|
28
|
+
readonly query: string;
|
|
29
|
+
/** Called when the query text changes; the caller re-runs its search. */
|
|
30
|
+
readonly onQueryChange: (query: string) => void;
|
|
31
|
+
/** Called with the chosen item when the user presses Enter. */
|
|
32
|
+
readonly onSelect: (item: PickerItem) => void;
|
|
33
|
+
/** Called when the user cancels (Esc or Ctrl+C). */
|
|
34
|
+
readonly onCancel: () => void;
|
|
35
|
+
/** `true` while a search request is in flight. */
|
|
36
|
+
readonly isLoading?: boolean;
|
|
37
|
+
/** Error from the last failed search, shown in place of the list. */
|
|
38
|
+
readonly error?: Error | null;
|
|
39
|
+
/** Label shown before the input (e.g. "Select an agent"). */
|
|
40
|
+
readonly prompt?: string;
|
|
41
|
+
/** Placeholder shown in the empty input. */
|
|
42
|
+
readonly placeholder?: string;
|
|
43
|
+
/** Label shown when a completed search returns no rows. */
|
|
44
|
+
readonly emptyLabel?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Maximum rows shown at once; the list scrolls a window around the cursor. */
|
|
48
|
+
const MAX_VISIBLE = 10;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generic, interactive search-and-select list for the terminal.
|
|
52
|
+
*
|
|
53
|
+
* Renders a text input above a scrollable result list. The user types to
|
|
54
|
+
* filter (the caller performs the actual search via `onQueryChange`), navigates
|
|
55
|
+
* with the arrow keys, selects with Enter, and cancels with Esc. It holds no
|
|
56
|
+
* data of its own — pass `items`/`query`/`isLoading` from a data hook such as
|
|
57
|
+
* `useAgentSearch` or `useSessionList`.
|
|
58
|
+
*
|
|
59
|
+
* This is the reusable organism behind {@link AgentPicker} and
|
|
60
|
+
* {@link SessionPicker}; platform builders can compose it with any search
|
|
61
|
+
* function to pick any resource.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* const { results, isLoading, query, setQuery } = useAgentSearch("acme");
|
|
66
|
+
* const items = results.map((r) => ({ id: r.id, title: r.qualifiedSlug }));
|
|
67
|
+
* render(
|
|
68
|
+
* <ResourcePicker
|
|
69
|
+
* prompt="Select an agent"
|
|
70
|
+
* items={items}
|
|
71
|
+
* query={query}
|
|
72
|
+
* onQueryChange={setQuery}
|
|
73
|
+
* isLoading={isLoading}
|
|
74
|
+
* onSelect={(item) => console.log(item.id)}
|
|
75
|
+
* onCancel={() => process.exit(0)}
|
|
76
|
+
* />,
|
|
77
|
+
* );
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function ResourcePicker({
|
|
81
|
+
items,
|
|
82
|
+
query,
|
|
83
|
+
onQueryChange,
|
|
84
|
+
onSelect,
|
|
85
|
+
onCancel,
|
|
86
|
+
isLoading = false,
|
|
87
|
+
error = null,
|
|
88
|
+
prompt = "Search",
|
|
89
|
+
placeholder = "type to search...",
|
|
90
|
+
emptyLabel = "no results",
|
|
91
|
+
}: ResourcePickerProps) {
|
|
92
|
+
const [cursor, setCursor] = useState(0);
|
|
93
|
+
|
|
94
|
+
// Reset the cursor to the top whenever the query changes — the result set is
|
|
95
|
+
// about to be replaced, so a stale cursor would point at the wrong row.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
setCursor(0);
|
|
98
|
+
}, [query]);
|
|
99
|
+
|
|
100
|
+
const active = items.length === 0 ? 0 : Math.min(cursor, items.length - 1);
|
|
101
|
+
|
|
102
|
+
useInput((input, key) => {
|
|
103
|
+
if (key.escape || (key.ctrl && input === "c")) {
|
|
104
|
+
onCancel();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.return) {
|
|
108
|
+
if (items.length > 0) onSelect(items[active]);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (key.upArrow) {
|
|
112
|
+
setCursor((c) => Math.max(0, Math.min(c, items.length - 1) - 1));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (key.downArrow) {
|
|
116
|
+
setCursor((c) => Math.min(items.length - 1, c + 1));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const [start, end] = visibleWindow(active, items.length);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Box flexDirection="column">
|
|
124
|
+
<Box gap={1}>
|
|
125
|
+
<Text color="cyan" bold>
|
|
126
|
+
?
|
|
127
|
+
</Text>
|
|
128
|
+
<Text bold>{prompt}:</Text>
|
|
129
|
+
<TextInput value={query} onChange={onQueryChange} placeholder={placeholder} />
|
|
130
|
+
</Box>
|
|
131
|
+
|
|
132
|
+
<Box marginTop={1} flexDirection="column">
|
|
133
|
+
{error !== null ? (
|
|
134
|
+
<Text color="red">{` Error: ${error.message}`}</Text>
|
|
135
|
+
) : isLoading ? (
|
|
136
|
+
<Text dimColor> searching...</Text>
|
|
137
|
+
) : items.length === 0 ? (
|
|
138
|
+
<Text dimColor>{` ${emptyLabel}`}</Text>
|
|
139
|
+
) : (
|
|
140
|
+
<>
|
|
141
|
+
{items.slice(start, end).map((item, i) => (
|
|
142
|
+
<PickerRow key={item.id} item={item} isActive={start + i === active} />
|
|
143
|
+
))}
|
|
144
|
+
<Box marginTop={1}>
|
|
145
|
+
<Text dimColor>{` ${countLabel(items.length)} | up/down: navigate | enter: select | esc: cancel`}</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</Box>
|
|
150
|
+
</Box>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** One result row: a cursor glyph, the title, then optional subtitle/meta. */
|
|
155
|
+
function PickerRow({ item, isActive }: { readonly item: PickerItem; readonly isActive: boolean }) {
|
|
156
|
+
return (
|
|
157
|
+
<Box>
|
|
158
|
+
<Text color={isActive ? "cyan" : undefined}>{isActive ? "❯ " : " "}</Text>
|
|
159
|
+
<Text color={isActive ? "cyan" : undefined} bold={isActive}>
|
|
160
|
+
{item.title}
|
|
161
|
+
</Text>
|
|
162
|
+
{item.subtitle !== undefined && item.subtitle !== "" ? <Text dimColor>{` ${item.subtitle}`}</Text> : null}
|
|
163
|
+
{item.meta !== undefined && item.meta !== "" ? <Text dimColor>{` ${item.meta}`}</Text> : null}
|
|
164
|
+
</Box>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** "1 result" / "N results" — matches the Go picker's footer wording. */
|
|
169
|
+
function countLabel(count: number): string {
|
|
170
|
+
return `${count} result${count === 1 ? "" : "s"}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Compute the visible slice `[start, end)` so the active row stays in view,
|
|
175
|
+
* centering the cursor once the list grows past {@link MAX_VISIBLE}.
|
|
176
|
+
*/
|
|
177
|
+
function visibleWindow(active: number, total: number): readonly [number, number] {
|
|
178
|
+
if (total <= MAX_VISIBLE) return [0, total];
|
|
179
|
+
const half = Math.floor(MAX_VISIBLE / 2);
|
|
180
|
+
let start = Math.max(0, active - half);
|
|
181
|
+
let end = start + MAX_VISIBLE;
|
|
182
|
+
if (end > total) {
|
|
183
|
+
end = total;
|
|
184
|
+
start = end - MAX_VISIBLE;
|
|
185
|
+
}
|
|
186
|
+
return [start, end];
|
|
187
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
|
3
|
+
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
4
|
+
import { useSessionList } from "@stigmer/react";
|
|
5
|
+
import { type PickerItem, ResourcePicker } from "./ResourcePicker.js";
|
|
6
|
+
|
|
7
|
+
/** Props for {@link SessionPicker}. */
|
|
8
|
+
export interface SessionPickerProps {
|
|
9
|
+
/** Seeds the filter box (e.g. text the user already typed). */
|
|
10
|
+
readonly initialQuery?: string;
|
|
11
|
+
/** Maximum sessions to fetch. Defaults to 50. */
|
|
12
|
+
readonly pageSize?: number;
|
|
13
|
+
/** Called with the chosen session on selection. */
|
|
14
|
+
readonly onSelect: (session: Session) => void;
|
|
15
|
+
/** Called when the user cancels (Esc / Ctrl+C). */
|
|
16
|
+
readonly onCancel: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PENDING_SUBJECT = "Auto-created session";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Interactive picker for re-opening an existing session.
|
|
23
|
+
*
|
|
24
|
+
* Unlike agents, sessions are not search-indexed, so this composes
|
|
25
|
+
* {@link useSessionList} with client-side text filtering over the recent
|
|
26
|
+
* session list. The selected {@link Session} is returned via `onSelect`; use
|
|
27
|
+
* `session.metadata.id` to resume it.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* render(
|
|
32
|
+
* <InkStigmerProvider client={client}>
|
|
33
|
+
* <SessionPicker onSelect={(s) => resume(s.metadata?.id)} onCancel={() => process.exit(0)} />
|
|
34
|
+
* </InkStigmerProvider>,
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function SessionPicker({ initialQuery, pageSize = 50, onSelect, onCancel }: SessionPickerProps) {
|
|
39
|
+
const { sessions, isLoading, error } = useSessionList({ pageSize });
|
|
40
|
+
const [query, setQuery] = useState(initialQuery ?? "");
|
|
41
|
+
|
|
42
|
+
const filtered = useMemo(() => filterSessions(sessions, query), [sessions, query]);
|
|
43
|
+
const items = useMemo<PickerItem[]>(() => filtered.map(toSessionItem), [filtered]);
|
|
44
|
+
|
|
45
|
+
const handleSelect = useCallback(
|
|
46
|
+
(item: PickerItem) => {
|
|
47
|
+
const found = sessions.find((s) => (s.metadata?.id ?? "") === item.id);
|
|
48
|
+
if (found !== undefined) onSelect(found);
|
|
49
|
+
},
|
|
50
|
+
[sessions, onSelect],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ResourcePicker
|
|
55
|
+
prompt="Select a session"
|
|
56
|
+
items={items}
|
|
57
|
+
query={query}
|
|
58
|
+
onQueryChange={setQuery}
|
|
59
|
+
isLoading={isLoading}
|
|
60
|
+
error={error}
|
|
61
|
+
onSelect={handleSelect}
|
|
62
|
+
onCancel={onCancel}
|
|
63
|
+
emptyLabel="no sessions found"
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Case-insensitive filter over a session's subject, agent, and id. */
|
|
69
|
+
function filterSessions(sessions: readonly Session[], query: string): readonly Session[] {
|
|
70
|
+
const needle = query.trim().toLowerCase();
|
|
71
|
+
if (needle === "") return sessions;
|
|
72
|
+
return sessions.filter((s) => sessionHaystack(s).includes(needle));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sessionHaystack(session: Session): string {
|
|
76
|
+
return [sessionSubject(session), session.spec?.agentInstanceId ?? "", session.metadata?.id ?? ""]
|
|
77
|
+
.join(" ")
|
|
78
|
+
.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Map a session to a picker row: subject title, agent subtitle, age meta. */
|
|
82
|
+
function toSessionItem(session: Session): PickerItem {
|
|
83
|
+
return {
|
|
84
|
+
id: session.metadata?.id ?? "",
|
|
85
|
+
title: sessionSubject(session),
|
|
86
|
+
subtitle: session.spec?.agentInstanceId ?? "",
|
|
87
|
+
meta: relativeTime(session.status?.audit?.specAudit?.createdAt),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The session's display subject, suppressing the backend's auto-create
|
|
92
|
+
// sentinel (the async title activity replaces it shortly after creation).
|
|
93
|
+
function sessionSubject(session: Session): string {
|
|
94
|
+
const subject = session.spec?.subject ?? "";
|
|
95
|
+
if (subject !== "" && subject !== PENDING_SUBJECT) return subject;
|
|
96
|
+
return session.metadata?.name !== undefined && session.metadata.name !== ""
|
|
97
|
+
? session.metadata.name
|
|
98
|
+
: (session.metadata?.id ?? "(untitled session)");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Coarse "N units ago" label from a protobuf Timestamp. Empty when unknown.
|
|
102
|
+
function relativeTime(ts: Timestamp | undefined): string {
|
|
103
|
+
if (ts === undefined) return "";
|
|
104
|
+
const then = Number(ts.seconds) * 1000;
|
|
105
|
+
if (!Number.isFinite(then) || then <= 0) return "";
|
|
106
|
+
const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
107
|
+
const units: ReadonlyArray<readonly [number, string]> = [
|
|
108
|
+
[86400, "day"],
|
|
109
|
+
[3600, "hour"],
|
|
110
|
+
[60, "minute"],
|
|
111
|
+
];
|
|
112
|
+
for (const [size, label] of units) {
|
|
113
|
+
const value = Math.floor(seconds / size);
|
|
114
|
+
if (value >= 1) return `${value} ${label}${value === 1 ? "" : "s"} ago`;
|
|
115
|
+
}
|
|
116
|
+
return "just now";
|
|
117
|
+
}
|
|
@@ -115,6 +115,12 @@ function describeResultView(view: ToolResultView): string | null {
|
|
|
115
115
|
return truncate(view.text);
|
|
116
116
|
case "json":
|
|
117
117
|
return truncate(JSON.stringify(view.value));
|
|
118
|
+
case "outputRef": {
|
|
119
|
+
const size = view.sizeBytes ? ` (${formatBytes(view.sizeBytes)})` : "";
|
|
120
|
+
if (view.isImage) return `image${size}: ${view.downloadUrl}`;
|
|
121
|
+
const preview = view.preview ? `${truncate(view.preview)}\n` : "";
|
|
122
|
+
return `${preview}full output${size}: ${view.downloadUrl}`;
|
|
123
|
+
}
|
|
118
124
|
case "error":
|
|
119
125
|
return view.message;
|
|
120
126
|
case "empty":
|
|
@@ -122,6 +128,12 @@ function describeResultView(view: ToolResultView): string | null {
|
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
function formatBytes(n: number): string {
|
|
132
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
133
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
134
|
+
return `${n} B`;
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
function truncate(s: string, maxLines = 5): string {
|
|
126
138
|
const lines = s.split("\n");
|
|
127
139
|
if (lines.length <= maxLines) return s;
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,11 @@ export { FollowUpInput, type FollowUpInputProps } from "./components/FollowUpInp
|
|
|
24
24
|
export { UsageWidget, type UsageWidgetProps } from "./components/UsageWidget.js";
|
|
25
25
|
export { ContextGauge, type ContextGaugeProps } from "./components/ContextGauge.js";
|
|
26
26
|
|
|
27
|
+
// Pickers (interactive search-and-select)
|
|
28
|
+
export { ResourcePicker, type ResourcePickerProps, type PickerItem } from "./components/ResourcePicker.js";
|
|
29
|
+
export { AgentPicker, type AgentPickerProps } from "./components/AgentPicker.js";
|
|
30
|
+
export { SessionPicker, type SessionPickerProps } from "./components/SessionPicker.js";
|
|
31
|
+
|
|
27
32
|
// Composed views
|
|
28
33
|
export { SessionView, type SessionViewProps, type InteractionMode } from "./app/SessionView.js";
|
|
29
34
|
export { SessionApp, type SessionAppProps } from "./app/SessionApp.js";
|