@north-light/crouter 0.3.184 → 0.3.185

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.
Files changed (61) hide show
  1. package/dist/build-root.js +10 -3
  2. package/dist/builtin-memory/internal/plugins.md +1 -1
  3. package/dist/clients/attach/input/capabilities.js +3 -0
  4. package/dist/clients/attach/slash/dispatch.js +12 -0
  5. package/dist/clients/attach/viewer.js +191 -191
  6. package/dist/commands/pkg/browse/actions.d.ts +7 -0
  7. package/dist/commands/pkg/browse/actions.js +199 -0
  8. package/dist/commands/pkg/browse/catalog.d.ts +9 -0
  9. package/dist/commands/pkg/browse/catalog.js +466 -0
  10. package/dist/commands/pkg/browse/command-view.d.ts +3 -0
  11. package/dist/commands/pkg/browse/command-view.js +191 -0
  12. package/dist/commands/pkg/browse/detail-panel.d.ts +66 -0
  13. package/dist/commands/pkg/browse/detail-panel.js +402 -0
  14. package/dist/commands/pkg/browse/doc-view.d.ts +3 -0
  15. package/dist/commands/pkg/browse/doc-view.js +81 -0
  16. package/dist/commands/pkg/browse/list-panel.d.ts +28 -0
  17. package/dist/commands/pkg/browse/list-panel.js +168 -0
  18. package/dist/commands/pkg/browse/model.d.ts +230 -0
  19. package/dist/commands/pkg/browse/model.js +36 -0
  20. package/dist/commands/pkg/browse/shell.d.ts +93 -0
  21. package/dist/commands/pkg/browse/shell.js +640 -0
  22. package/dist/commands/pkg/browse.d.ts +2 -0
  23. package/dist/commands/pkg/browse.js +27 -0
  24. package/dist/commands/pkg/market-manage.js +13 -4
  25. package/dist/commands/pkg/plugin-manage.js +8 -3
  26. package/dist/commands/pkg.js +2 -1
  27. package/dist/commands/sys/doctor.js +6 -4
  28. package/dist/commands/sys/panels/broker-limits-panel.d.ts +1 -1
  29. package/dist/commands/sys/panels/broker-limits-panel.js +1 -1
  30. package/dist/commands/sys/panels/dependencies-panel.d.ts +1 -1
  31. package/dist/commands/sys/panels/dependencies-panel.js +1 -1
  32. package/dist/commands/sys/panels/keybindings-panel.js +1 -1
  33. package/dist/commands/sys/panels/models-panel.d.ts +1 -1
  34. package/dist/commands/sys/panels/models-panel.js +1 -1
  35. package/dist/commands/sys/panels/plugins-panel.d.ts +1 -1
  36. package/dist/commands/sys/panels/plugins-panel.js +1 -1
  37. package/dist/commands/sys/panels/prefs-panel.d.ts +1 -1
  38. package/dist/commands/sys/panels/prefs-panel.js +1 -1
  39. package/dist/commands/sys/panels/provider-panel.d.ts +1 -1
  40. package/dist/commands/sys/panels/provider-panel.js +1 -1
  41. package/dist/commands/sys/settings-shell.js +1 -1
  42. package/dist/commands/sys/setup-settings-runner.d.ts +7 -18
  43. package/dist/commands/sys/setup-settings-runner.js +3 -52
  44. package/dist/commands/sys/setup-wizard.js +1 -1
  45. package/dist/core/__tests__/serial/command-plugins.test.js +21 -40
  46. package/dist/core/command-manifests/registry.d.ts +10 -3
  47. package/dist/core/command-manifests/registry.js +36 -22
  48. package/dist/core/command-plugins/compose.js +1 -1
  49. package/dist/core/command-plugins/discovery.js +15 -7
  50. package/dist/core/keybindings/catalog.d.ts +1 -1
  51. package/dist/core/keybindings/catalog.js +1 -0
  52. package/dist/core/resolver.d.ts +2 -2
  53. package/dist/core/runtime/tmux-bindings.js +2 -0
  54. package/dist/core/tui/page-host.d.ts +25 -0
  55. package/dist/core/tui/page-host.js +65 -0
  56. package/dist/pi-extensions/canvas-bash-valve.js +45 -1
  57. package/dist/types.d.ts +4 -1
  58. package/package.json +1 -1
  59. package/runtime.lock.json +2 -2
  60. /package/dist/{commands/sys/panels → core/tui}/panel.d.ts +0 -0
  61. /package/dist/{commands/sys/panels → core/tui}/panel.js +0 -0
@@ -0,0 +1,168 @@
1
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import { padAnsi, theme, visibleRange } from "../../../core/tui/panel.js";
3
+ import { displayVersion, plural } from "./model.js";
4
+ /** The middle column of the plugin browser. It owns filtering and row position;
5
+ * the shell owns horizontal navigation and actions. */
6
+ export class PkgListPanel {
7
+ onChange;
8
+ source;
9
+ index = 0;
10
+ filter = "";
11
+ filtering = false;
12
+ notice;
13
+ hasMarketplaces = false;
14
+ constructor(onChange) {
15
+ this.onChange = onChange;
16
+ }
17
+ get selected() {
18
+ const entries = this.entries();
19
+ this.index = Math.max(0, Math.min(this.index, entries.length - 1));
20
+ return entries[this.index];
21
+ }
22
+ get ownsKeyboard() {
23
+ return this.filtering;
24
+ }
25
+ setSource(source, selectedKey) {
26
+ const changed = this.source?.id !== source.id;
27
+ this.source = source;
28
+ const entries = this.entries();
29
+ const found = selectedKey === undefined ? -1 : entries.findIndex((entry) => entry.key === selectedKey);
30
+ this.index = found >= 0 ? found : changed ? 0 : Math.min(this.index, Math.max(0, entries.length - 1));
31
+ if (changed) {
32
+ this.filter = "";
33
+ this.filtering = false;
34
+ }
35
+ }
36
+ setEmptyState(hasMarketplaces) {
37
+ this.hasMarketplaces = hasMarketplaces;
38
+ }
39
+ move(delta) {
40
+ const entries = this.entries();
41
+ if (entries.length === 0)
42
+ return;
43
+ this.index = Math.max(0, Math.min(entries.length - 1, this.index + delta));
44
+ this.onChange();
45
+ }
46
+ /** Returns true only when a live filter consumed the input. */
47
+ handleInput(data) {
48
+ if (this.filtering) {
49
+ if (matchesKey(data, "escape")) {
50
+ this.filter = "";
51
+ this.filtering = false;
52
+ }
53
+ else if (matchesKey(data, "enter")) {
54
+ // Enter leaves entry mode but deliberately keeps the narrowed list.
55
+ this.filtering = false;
56
+ }
57
+ else if (matchesKey(data, "backspace")) {
58
+ this.filter = this.filter.slice(0, -1);
59
+ this.index = 0;
60
+ }
61
+ else if (data.length === 1 && data.charCodeAt(0) >= 32) {
62
+ this.filter += data;
63
+ this.index = 0;
64
+ }
65
+ this.onChange();
66
+ return true;
67
+ }
68
+ if (data === "/" || matchesKey(data, "/")) {
69
+ this.filtering = true;
70
+ this.onChange();
71
+ return true;
72
+ }
73
+ // Escape has a useful local meaning only while a committed filter remains.
74
+ if (matchesKey(data, "escape") && this.filter !== "") {
75
+ this.filter = "";
76
+ this.index = 0;
77
+ this.onChange();
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+ setNotice(notice) {
83
+ this.notice = notice;
84
+ }
85
+ entries() {
86
+ const entries = this.source?.entries ?? [];
87
+ const needle = this.filter.trim().toLowerCase();
88
+ if (needle === "")
89
+ return entries;
90
+ return entries.filter((entry) => [entry.name, entry.description ?? "", ...entry.keywords]
91
+ .join("\n")
92
+ .toLowerCase()
93
+ .includes(needle));
94
+ }
95
+ render(width, height, focused) {
96
+ const out = [];
97
+ const source = this.source;
98
+ const entries = this.entries();
99
+ const sourceLabel = source?.kind === "installed" ? "Installed" : source?.label ?? "Library";
100
+ const filterLabel = this.filtering
101
+ ? ` /${this.filter} · ${plural(entries.length, "match", "matches")}`
102
+ : this.filter === ""
103
+ ? ` ${plural(entries.length, "plugin", "plugins")}`
104
+ : ` ${plural(entries.length, "match", "matches")} · esc clears`;
105
+ // A marketplace name can be wider than this whole column, so the source
106
+ // label yields first: how many rows matched and how to get back out are
107
+ // what a filtering user needs, and truncating the pair together drops
108
+ // exactly that half off the end.
109
+ const labelRoom = Math.max(3, width - visibleWidth(filterLabel));
110
+ out.push(truncateToWidth(`${theme.fg("accent", theme.bold(truncateToWidth(sourceLabel, labelRoom)))}${theme.fg("dim", filterLabel)}`, width));
111
+ if (this.notice !== undefined) {
112
+ out.push(truncateToWidth(theme.fg("warning", `! ${this.notice}`), width));
113
+ }
114
+ if (entries.length === 0) {
115
+ out.push("");
116
+ const firstRun = source?.kind === "installed" && !this.hasMarketplaces;
117
+ if (firstRun) {
118
+ out.push(truncateToWidth(theme.fg("accent", "No marketplaces yet."), width));
119
+ out.push(truncateToWidth(theme.fg("dim", "Press a to add a marketplace."), width));
120
+ }
121
+ else {
122
+ out.push(truncateToWidth(theme.fg("dim", "No plugins installed yet."), width));
123
+ out.push(truncateToWidth(theme.fg("dim", "Press tab to browse marketplaces."), width));
124
+ }
125
+ return this.fit(out, height);
126
+ }
127
+ const available = Math.max(1, height - out.length);
128
+ const range = visibleRange(entries.length, this.index, available);
129
+ for (let position = range.start; position < range.end; position += 1) {
130
+ const entry = entries[position];
131
+ const selected = position === this.index;
132
+ const glyph = this.stateGlyph(entry);
133
+ const version = displayVersion(entry);
134
+ const prefix = `${glyph} ${entry.name}`;
135
+ const suffix = version === undefined ? "" : ` ${version}`;
136
+ const fixed = visibleWidth(prefix) + visibleWidth(suffix);
137
+ const descriptionWidth = width - fixed - 2;
138
+ const description = descriptionWidth >= 12 && entry.description
139
+ ? theme.fg("dim", ` ${truncateToWidth(entry.description, descriptionWidth)}`)
140
+ : "";
141
+ const row = truncateToWidth(`${prefix}${suffix}${description}`, width);
142
+ out.push(selected && focused
143
+ ? theme.bg("selectedBg", padAnsi(row, width))
144
+ : selected
145
+ ? `${theme.fg("accent", "▌")}${truncateToWidth(theme.fg("accent", row), Math.max(0, width - 1))}`
146
+ : row);
147
+ }
148
+ return this.fit(out, height);
149
+ }
150
+ /** Pad rather than trim: this column sets the length of the divider drawn
151
+ * beside it, and a divider that stops where the content runs out makes the
152
+ * page look half-drawn. */
153
+ fit(lines, height) {
154
+ while (lines.length < height)
155
+ lines.push("");
156
+ return lines.slice(0, height);
157
+ }
158
+ stateGlyph(entry) {
159
+ if (entry.install.kind === "absent")
160
+ return " ";
161
+ if (entry.install.scope === "builtin")
162
+ return theme.fg("muted", "⌂");
163
+ if (entry.install.enabled) {
164
+ return entry.install.updateAvailable ? theme.fg("warning", "↑") : theme.fg("success", "✓");
165
+ }
166
+ return theme.fg("dim", "○");
167
+ }
168
+ }
@@ -0,0 +1,230 @@
1
+ import type { Field, InputParam } from "../../../core/help.js";
2
+ import type { Rung } from "../../../core/substrate/schema.js";
3
+ import type { OwnerRef, Scope } from "../../../types.js";
4
+ /** The scopes that can hold a marketplace, and that a plugin can be installed
5
+ * into. Narrower than `Scope`: `builtin` names plugins that ship with crtr, and
6
+ * `listAllMarketplaces()` never yields it, so a builtin marketplace or a
7
+ * builtin install target is not a state this surface can reach. */
8
+ export type WritableScope = Exclude<Scope, "builtin">;
9
+ /** Rail row identity: `installed`, or `market:<scope>:<name>`.
10
+ *
11
+ * A marketplace id carries its scope because the same name can be registered
12
+ * in both the user and the project scope. Collapsing those to one id makes two
13
+ * distinct marketplaces share a row, and an action on it reach whichever one
14
+ * the leaf's unscoped lookup resolves first — including a destructive one. */
15
+ export type SourceId = string;
16
+ export declare const INSTALLED_SOURCE_ID: SourceId;
17
+ export declare function marketplaceSourceId(name: string, scope: WritableScope): SourceId;
18
+ export type PluginInstallState = {
19
+ kind: "absent";
20
+ } | {
21
+ kind: "installed";
22
+ scope: Scope;
23
+ enabled: boolean;
24
+ /** Version recorded for the installed copy, when it declares one. */
25
+ version?: string;
26
+ path: string;
27
+ /** The marketplace offers a version newer than the installed one. */
28
+ updateAvailable: boolean;
29
+ };
30
+ /** One command node a plugin contributes to the `crtr` tree, with the whole
31
+ * contract an agent would read from `-h`. The browser renders the tree, so
32
+ * children hang off their parent rather than arriving pre-flattened. */
33
+ export interface CommandNode {
34
+ /** Path words from the crtr root, e.g. `['search', 'web']`. */
35
+ path: string[];
36
+ kind: "branch" | "leaf";
37
+ description: string;
38
+ /** The selection rubric — when to reach for this command. */
39
+ whenToUse: string;
40
+ summary: string;
41
+ tier?: "normal" | "common" | "important";
42
+ /** Empty for a leaf and for a passthrough branch. */
43
+ children: CommandNode[];
44
+ /** Branch only: forwards every argv token to an external binary instead of
45
+ * parsing children. */
46
+ passthrough?: {
47
+ bin: string;
48
+ installHint: string;
49
+ };
50
+ /** Top-level branch only: its entry in the crtr root listing. */
51
+ rootEntry?: {
52
+ concept: string;
53
+ description: string;
54
+ whenToUse: string;
55
+ };
56
+ /** Leaf only: the exact invocation schema, straight from the manifest. */
57
+ params: InputParam[];
58
+ output: Field[];
59
+ effects: string[];
60
+ /** Leaf on an http-transport plugin only: the REST call it maps to. */
61
+ rest?: {
62
+ method: string;
63
+ path: string;
64
+ streaming: boolean;
65
+ };
66
+ }
67
+ /** `crtr search web` — how a user types the command a node names. The one place
68
+ * a command path becomes a label, so no corner of the page invents its own. */
69
+ export declare function commandLabel(node: CommandNode): string;
70
+ /** Every node in a command forest, parents before their children. */
71
+ export declare function flattenCommands(nodes: CommandNode[]): CommandNode[];
72
+ /** One memory doc a plugin provides — whole, because the resolver already read
73
+ * the file to report the doc exists, so the body costs nothing extra.
74
+ *
75
+ * Two fields sound alike and are not: `shortForm` is the human-facing hook —
76
+ * what this doc SAYS — and is the description the page shows beside a doc's
77
+ * name. `routingLine` is agent read-routing — WHEN to open it — and belongs
78
+ * under the doc view's own heading, never in the description slot. */
79
+ export interface DocDetail {
80
+ /** `<plugin>/<path>` — the identifier `crtr memory read` takes. */
81
+ name: string;
82
+ kind: "knowledge" | "preference";
83
+ /** Frontmatter `short-form`: the abbreviated content. */
84
+ shortForm?: string;
85
+ /** Frontmatter `when-and-why-to-read`: one sentence routing a reading agent. */
86
+ routingLine?: string;
87
+ /** Absolute path to the `.md` on disk. */
88
+ path: string;
89
+ sizeBytes?: number;
90
+ /** How much of the doc surfaces at boot, and on a related file read. */
91
+ systemPromptVisibility: Rung;
92
+ fileReadVisibility: Rung;
93
+ /** Invocable as a pi slash command (`slash: true`). */
94
+ slash: boolean;
95
+ /** Frontmatter `applies-to`: explicit file-context routes. */
96
+ appliesTo?: string[];
97
+ /** Frontmatter `gate`: eligibility predicate over the node's config. */
98
+ gate?: Record<string, unknown>;
99
+ /** Frontmatter `read-when`: predicate over a READ FILE's own frontmatter.
100
+ * Unrelated to `routingLine` despite the neighbouring names. */
101
+ readWhenPredicate?: Record<string, unknown>;
102
+ /** Frontmatter `rationale`: the observed failure the doc exists to close.
103
+ * Maintainer-facing — never delivered to a reading agent. */
104
+ rationale?: string;
105
+ /** Markdown body, frontmatter stripped. */
106
+ body: string;
107
+ }
108
+ /** A plugin as one list row: everything cheap enough to load for every row. */
109
+ export interface PluginEntry {
110
+ /** Stable row key, unique across the whole catalog: `<sourceId>:<scope>:<name>`.
111
+ * Scope participates because one name can name two different installations. */
112
+ key: string;
113
+ name: string;
114
+ /** What `pkg plugin install` takes: `<marketplace>/<plugin>`, else the name. */
115
+ ref: string;
116
+ sourceId: SourceId;
117
+ /** The marketplace offering it; absent for a plugin installed from a URL or path. */
118
+ marketplace?: string;
119
+ /** Version the marketplace offers (installed version lives in `install`). */
120
+ version?: string;
121
+ description?: string;
122
+ keywords: string[];
123
+ owner?: OwnerRef;
124
+ /** Upstream repo the marketplace entry points at. */
125
+ source?: string;
126
+ install: PluginInstallState;
127
+ /** Directory holding the plugin's files — the installed copy when there is
128
+ * one, else the marketplace clone's vendored copy. Absent when neither
129
+ * exists on disk, which is the only case where capability is unknowable. */
130
+ root?: string;
131
+ }
132
+ /** The version to show beside a plugin's name, anywhere in the page.
133
+ *
134
+ * One rule, one place: the version beside a name is always the one in play on
135
+ * THIS machine — what is installed, or what installing would get you. The
136
+ * marketplace's offer for an already-installed plugin is a different fact, and
137
+ * it appears only as the right-hand side of the update arrow in the status
138
+ * line. Headline and list row both call this so they cannot disagree. */
139
+ export declare function displayVersion(entry: PluginEntry): string | undefined;
140
+ /** `<count> <noun>`, with the noun agreeing with the count. Every count this
141
+ * page renders goes through here so no corner of it reads "1 docs". */
142
+ export declare function plural(count: number, one: string, many: string): string;
143
+ /** What a plugin adds to this machine — read from its directory on demand. */
144
+ export interface PluginCapability {
145
+ /** Directory these facts were read from. */
146
+ root: string;
147
+ /** How crtr invokes its commands; null for a docs-only plugin. */
148
+ transport: "exec" | "http" | null;
149
+ /** Local executable (exec) or remote endpoint (http) — the trust-relevant fact. */
150
+ transportTarget?: string;
151
+ /** The command forest this plugin mounts, as a tree. */
152
+ commands: CommandNode[];
153
+ /** Static validation problems that stop a declared command from mounting. */
154
+ commandIssues: {
155
+ message: string;
156
+ next?: string;
157
+ }[];
158
+ docs: DocDetail[];
159
+ /** Total bytes on disk, when cheap to measure. */
160
+ sizeBytes?: number;
161
+ }
162
+ export interface MarketplaceDetail {
163
+ name: string;
164
+ scope: WritableScope;
165
+ url: string;
166
+ ref: string;
167
+ /** Absolute path to the clone. */
168
+ path: string;
169
+ owner?: OwnerRef;
170
+ version?: string;
171
+ pluginCount: number;
172
+ installedCount: number;
173
+ /** Installed plugins from this marketplace with a newer version offered. */
174
+ updatableCount: number;
175
+ /** Short sha of the clone's checked-out commit. */
176
+ revision?: string;
177
+ /** ISO date of that commit. */
178
+ revisionDate?: string;
179
+ }
180
+ export interface CatalogSource {
181
+ id: SourceId;
182
+ kind: "installed" | "marketplace";
183
+ label: string;
184
+ /** Present exactly when `kind === 'marketplace'`. */
185
+ marketplace?: MarketplaceDetail;
186
+ entries: PluginEntry[];
187
+ }
188
+ export interface Catalog {
189
+ sources: CatalogSource[];
190
+ /** Scopes a plugin can be installed into on this machine, in offer order. */
191
+ installScopes: WritableScope[];
192
+ /** Non-fatal problems loading the catalog — a broken clone, an unreadable
193
+ * manifest. Surfaced in the page rather than thrown, so one bad marketplace
194
+ * never takes the browser down. */
195
+ warnings: string[];
196
+ }
197
+ export type ActionKind = "install" | "remove" | "enable" | "disable" | "update" | "market-add" | "market-update" | "market-remove";
198
+ export interface ActionRequest {
199
+ kind: ActionKind;
200
+ /** Required for every plugin-scoped action. */
201
+ entry?: PluginEntry;
202
+ /** Required for every marketplace-scoped action on an already-registered
203
+ * marketplace (`market-update`, `market-remove`). */
204
+ marketplace?: string;
205
+ /** Git URL of the marketplace repo. Required by `market-add` only. */
206
+ url?: string;
207
+ /** Git ref the added marketplace tracks. `market-add` only; defaults to main. */
208
+ ref?: string;
209
+ /** The scope this action operates in — where `install` and `market-add` put
210
+ * their result, and which of two same-named marketplaces `market-remove`
211
+ * destroys. Ignored by actions that do not resolve a scope. */
212
+ scope?: WritableScope;
213
+ }
214
+ export interface ActionOutcome {
215
+ ok: boolean;
216
+ /** One line for the status row. */
217
+ message: string;
218
+ /** Extra lines for the detail pane — mounted commands, issues, git output. */
219
+ detail?: string[];
220
+ }
221
+ /** How an action presents before it runs: the progress line, and the question
222
+ * to put in a confirm modal when the action destroys something. */
223
+ export interface ActionPresentation {
224
+ /** Imperative label for the footer/menu, e.g. "Install into user scope". */
225
+ label: string;
226
+ /** Present-tense progress line, e.g. "Installing capture…". */
227
+ progress: string;
228
+ /** Non-empty only for destructive actions; the shell must confirm first. */
229
+ confirm?: string;
230
+ }
@@ -0,0 +1,36 @@
1
+ // The read model behind `crtr pkg browse` — one shape the catalog produces, the
2
+ // action layer consumes, and the panels render.
3
+ //
4
+ // The browser answers one question the flat `pkg market browse` listing cannot:
5
+ // *what does installing this actually do to my crtr?* So an entry carries its
6
+ // marketplace metadata (cheap, from the manifest index) separately from its
7
+ // capability surface (commands mounted, memory docs provided, transport), which
8
+ // costs a directory read and is resolved on demand for the focused row only.
9
+ export const INSTALLED_SOURCE_ID = "installed";
10
+ export function marketplaceSourceId(name, scope) {
11
+ return `market:${scope}:${name}`;
12
+ }
13
+ /** `crtr search web` — how a user types the command a node names. The one place
14
+ * a command path becomes a label, so no corner of the page invents its own. */
15
+ export function commandLabel(node) {
16
+ return `crtr ${node.path.join(" ")}`;
17
+ }
18
+ /** Every node in a command forest, parents before their children. */
19
+ export function flattenCommands(nodes) {
20
+ return nodes.flatMap((node) => [node, ...flattenCommands(node.children)]);
21
+ }
22
+ /** The version to show beside a plugin's name, anywhere in the page.
23
+ *
24
+ * One rule, one place: the version beside a name is always the one in play on
25
+ * THIS machine — what is installed, or what installing would get you. The
26
+ * marketplace's offer for an already-installed plugin is a different fact, and
27
+ * it appears only as the right-hand side of the update arrow in the status
28
+ * line. Headline and list row both call this so they cannot disagree. */
29
+ export function displayVersion(entry) {
30
+ return entry.install.kind === "installed" ? entry.install.version ?? entry.version : entry.version;
31
+ }
32
+ /** `<count> <noun>`, with the noun agreeing with the count. Every count this
33
+ * page renders goes through here so no corner of it reads "1 docs". */
34
+ export function plural(count, one, many) {
35
+ return `${count} ${count === 1 ? one : many}`;
36
+ }
@@ -0,0 +1,93 @@
1
+ import { type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
2
+ import { type BindingId, type BindingResolution } from "../../../core/keybindings/index.js";
3
+ /** Full-screen plugin browser. The data and mutation boundaries stay in the
4
+ * catalog/actions collaborators, leaving this class responsible for movement,
5
+ * disclosure, and the rendered page grammar. */
6
+ export declare class PkgBrowseShell implements Component, Focusable {
7
+ private readonly tui;
8
+ private readonly terminalRows;
9
+ private readonly bindings;
10
+ private catalog;
11
+ private sourceIndex;
12
+ private zone;
13
+ private readonly list;
14
+ private readonly detail;
15
+ private readonly capabilities;
16
+ private modal;
17
+ private busy;
18
+ private notice;
19
+ private _focused;
20
+ onCancel?: () => void;
21
+ constructor(tui: TUI, terminalRows: () => number, bindings: BindingResolution<BindingId>);
22
+ /** The page follows the user's own `crtr.setup.*` navigation gestures, the
23
+ * same ones `crtr sys settings` resolves, alongside its literal defaults —
24
+ * a remap must not leave one full-screen page moving and the other stuck. */
25
+ private matches;
26
+ /** The footer names keys, so it names the gestures actually bound. */
27
+ private label;
28
+ private isDown;
29
+ private isUp;
30
+ /** Move one zone to the right, or open the focused row. */
31
+ private isDescend;
32
+ /** Move one zone to the left. Escape is deliberately separate: in the dossier
33
+ * it pops the whole view stack where `h` folds one branch. */
34
+ private isAscend;
35
+ /** Tab cycles rail sources from anywhere. The resolved `tab-next` gesture
36
+ * also carries the horizontal arrows, which mean "into the panel" here, so
37
+ * those are excluded exactly as the settings shell excludes them. */
38
+ private cyclesSource;
39
+ set focused(value: boolean);
40
+ get focused(): boolean;
41
+ /** Required by pi-tui's `Component`, and deliberately a no-op: this page
42
+ * holds no render cache to drop. Every panel is recomputed each frame
43
+ * because capability reads and action results land between input events. */
44
+ invalidate(): void;
45
+ dispose(): void;
46
+ private sources;
47
+ private get source();
48
+ private syncSource;
49
+ private syncDetail;
50
+ private reload;
51
+ private goSource;
52
+ private moveList;
53
+ private clearActionNotice;
54
+ /** A live filter, an open modal, or a running action holds every key — the
55
+ * one predicate both `handleInput` and the footer consult, so the footer can
56
+ * never advertise a key the filter is swallowing. */
57
+ private panelOwnsKeyboard;
58
+ handleInput(data: string): void;
59
+ private enterRail;
60
+ private enterList;
61
+ private enterDetail;
62
+ private jumpToInitial;
63
+ /** The focused rail row as an action subject. Carries the marketplace's scope
64
+ * as well as its name: two marketplaces can share a name across the user and
65
+ * project scopes, and without the scope `availableActions` cannot tell which
66
+ * one a remove would destroy, so it offers nothing at all. */
67
+ private marketplaceSubject;
68
+ private triggerAction;
69
+ private openAdd;
70
+ private handleModal;
71
+ private submitAdd;
72
+ /** Where `a` registers a marketplace. Named once so the modal can state
73
+ * exactly what the action will do. */
74
+ private addScope;
75
+ private execute;
76
+ private beginBusy;
77
+ private finishAction;
78
+ /** The source rail. It scrolls like the list does: marketplace count is user
79
+ * controlled, so a fixed slice would let the selection sit below the last
80
+ * drawn row, moving invisibly while the badge the user is aiming for never
81
+ * appears. Group headings scroll with their group. */
82
+ private renderRail;
83
+ private renderPanel;
84
+ private renderModal;
85
+ private wrapModal;
86
+ private footer;
87
+ /** The footer names KEYS, not actions. An absent plugin offers a user-scope
88
+ * and a project-scope install — two actions behind one `i`, which the scope
89
+ * modal then chooses between — so the hints are deduped before joining. */
90
+ private availableActionHints;
91
+ render(width: number): string[];
92
+ }
93
+ export declare function runPkgBrowse(): Promise<void>;