pi-extension-utils 0.3.1 → 0.3.3
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/README.md +91 -45
- package/dist/index.js +9 -140
- package/dist/src/client/index.d.ts +7 -0
- package/dist/src/client/index.js +25 -0
- package/dist/src/client/types.d.ts +17 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/config/index.d.ts +28 -0
- package/dist/src/config/index.js +222 -0
- package/dist/src/index.d.ts +10 -7
- package/dist/src/index.js +10 -7
- package/dist/src/logger/config.d.ts +11 -0
- package/dist/src/logger/config.js +27 -0
- package/dist/src/logger/index.d.ts +20 -0
- package/dist/src/logger/index.js +76 -0
- package/dist/src/{pane-overlay.d.ts → pane/overlay.d.ts} +1 -1
- package/dist/src/{pane-overlay.js → pane/overlay.js} +2 -2
- package/dist/src/reminders/client.d.ts +10 -0
- package/dist/src/reminders/client.js +49 -0
- package/dist/src/reminders/config.d.ts +5 -6
- package/dist/src/reminders/config.js +9 -23
- package/dist/src/reminders/debug.js +2 -2
- package/dist/src/reminders/host.d.ts +5 -0
- package/dist/src/reminders/host.js +298 -0
- package/dist/src/reminders/index.d.ts +3 -5
- package/dist/src/reminders/index.js +3 -308
- package/dist/src/reminders/types.d.ts +3 -0
- package/dist/src/reminders/types.js +3 -0
- package/dist/src/ui/client.d.ts +22 -0
- package/dist/src/ui/client.js +17 -0
- package/dist/src/utils-config.d.ts +24 -0
- package/dist/src/utils-config.js +38 -0
- package/dist/src/widgets/client.d.ts +27 -0
- package/dist/src/widgets/client.js +130 -0
- package/dist/src/widgets/host.d.ts +2 -0
- package/dist/src/widgets/host.js +143 -0
- package/dist/src/{protocol.d.ts → widgets/protocol.d.ts} +2 -6
- package/docs/README.md +11 -0
- package/docs/client.md +91 -0
- package/docs/config.md +86 -0
- package/docs/pane-overlay.md +88 -0
- package/docs/reference/reminders-spec.md +351 -0
- package/docs/reminders.md +68 -0
- package/docs/widgets.md +57 -0
- package/examples/README.md +23 -0
- package/examples/config.ts +31 -0
- package/examples/logger.ts +20 -0
- package/examples/pane-overlay.ts +47 -0
- package/examples/reminders.ts +25 -0
- package/examples/widget-coordinator.ts +18 -0
- package/package.json +12 -1
- package/dist/src/client.d.ts +0 -47
- package/dist/src/client.js +0 -194
- package/dist/src/logger.d.ts +0 -12
- package/dist/src/logger.js +0 -45
- /package/dist/src/{tui-chrome.d.ts → pane/chrome.d.ts} +0 -0
- /package/dist/src/{tui-chrome.js → pane/chrome.js} +0 -0
- /package/dist/src/{key-dispatch.d.ts → pane/key-dispatch.d.ts} +0 -0
- /package/dist/src/{key-dispatch.js → pane/key-dispatch.js} +0 -0
- /package/dist/src/{pane-state.d.ts → pane/state.d.ts} +0 -0
- /package/dist/src/{pane-state.js → pane/state.js} +0 -0
- /package/dist/src/{protocol.js → widgets/protocol.js} +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { basePayload, EVENTS, PROTOCOL_VERSION, } from "./protocol.js";
|
|
2
|
+
const HOST_CLIENT_ID = "pi-extension-utils-host";
|
|
3
|
+
const HOST_WIDGET_PREFIX = "pi-extension-utils";
|
|
4
|
+
const PLACEMENTS = ["aboveEditor", "belowEditor"];
|
|
5
|
+
export function registerWidgetHost(pi) {
|
|
6
|
+
const widgets = new Map();
|
|
7
|
+
const fullscreenStack = [];
|
|
8
|
+
let seq = 0;
|
|
9
|
+
let currentCtx;
|
|
10
|
+
for (const placement of PLACEMENTS)
|
|
11
|
+
widgets.set(placement, []);
|
|
12
|
+
function setHostWidget(placement) {
|
|
13
|
+
if (!currentCtx)
|
|
14
|
+
return;
|
|
15
|
+
const records = sortedWidgets(placement);
|
|
16
|
+
const hidden = fullscreenStack.length > 0;
|
|
17
|
+
if (records.length === 0 && !hidden) {
|
|
18
|
+
currentCtx.ui.setWidget(hostKey(placement), undefined, { placement });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
currentCtx.ui.setWidget(hostKey(placement), (tui, theme) => createHostComponent(tui, theme, records, hidden), { placement });
|
|
22
|
+
}
|
|
23
|
+
function rerenderAll() {
|
|
24
|
+
for (const placement of PLACEMENTS)
|
|
25
|
+
setHostWidget(placement);
|
|
26
|
+
}
|
|
27
|
+
pi.events.on(EVENTS.hello, (data) => {
|
|
28
|
+
const payload = readPayload(data, "hello");
|
|
29
|
+
if (!payload)
|
|
30
|
+
return;
|
|
31
|
+
pi.events.emit(EVENTS.ready, basePayload(HOST_CLIENT_ID));
|
|
32
|
+
});
|
|
33
|
+
pi.events.on(EVENTS.registerWidget, (data) => {
|
|
34
|
+
const payload = readPayload(data, "register-widget");
|
|
35
|
+
if (!payload || !isPlacement(payload.placement) || typeof payload.key !== "string" || typeof payload.order !== "number" || typeof payload.factory !== "function") {
|
|
36
|
+
warnDrop("register-widget", data);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const records = widgets.get(payload.placement);
|
|
40
|
+
const existing = records.find((record) => record.clientId === payload.clientId && record.key === payload.key);
|
|
41
|
+
if (existing) {
|
|
42
|
+
existing.order = payload.order;
|
|
43
|
+
existing.factory = payload.factory;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
records.push({ clientId: payload.clientId, key: payload.key, order: payload.order, factory: payload.factory, seq: seq++ });
|
|
47
|
+
}
|
|
48
|
+
setHostWidget(payload.placement);
|
|
49
|
+
});
|
|
50
|
+
pi.events.on(EVENTS.unregisterWidget, (data) => {
|
|
51
|
+
const payload = readPayload(data, "unregister-widget");
|
|
52
|
+
if (!payload)
|
|
53
|
+
return;
|
|
54
|
+
if (payload.all) {
|
|
55
|
+
removeClient(payload.clientId);
|
|
56
|
+
rerenderAll();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!isPlacement(payload.placement) || typeof payload.key !== "string") {
|
|
60
|
+
warnDrop("unregister-widget", data);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const records = widgets.get(payload.placement);
|
|
64
|
+
widgets.set(payload.placement, records.filter((record) => record.clientId !== payload.clientId || record.key !== payload.key));
|
|
65
|
+
setHostWidget(payload.placement);
|
|
66
|
+
});
|
|
67
|
+
pi.events.on(EVENTS.fullscreenAcquire, (data) => {
|
|
68
|
+
const payload = readPayload(data, "fullscreen-acquire");
|
|
69
|
+
if (!payload || typeof payload.token !== "string") {
|
|
70
|
+
warnDrop("fullscreen-acquire", data);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!fullscreenStack.some((lease) => lease.token === payload.token)) {
|
|
74
|
+
fullscreenStack.push({ clientId: payload.clientId, token: payload.token });
|
|
75
|
+
rerenderAll();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
pi.events.on(EVENTS.fullscreenRelease, (data) => {
|
|
79
|
+
const payload = readPayload(data, "fullscreen-release");
|
|
80
|
+
if (!payload || typeof payload.token !== "string") {
|
|
81
|
+
warnDrop("fullscreen-release", data);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const before = fullscreenStack.length;
|
|
85
|
+
for (let index = fullscreenStack.length - 1; index >= 0; index--) {
|
|
86
|
+
if (fullscreenStack[index].token === payload.token)
|
|
87
|
+
fullscreenStack.splice(index, 1);
|
|
88
|
+
}
|
|
89
|
+
if (fullscreenStack.length !== before)
|
|
90
|
+
rerenderAll();
|
|
91
|
+
});
|
|
92
|
+
pi.on("session_start", (_event, ctx) => {
|
|
93
|
+
currentCtx = ctx;
|
|
94
|
+
rerenderAll();
|
|
95
|
+
pi.events.emit(EVENTS.ready, basePayload(HOST_CLIENT_ID));
|
|
96
|
+
});
|
|
97
|
+
pi.events.emit(EVENTS.ready, basePayload(HOST_CLIENT_ID));
|
|
98
|
+
function removeClient(clientId) {
|
|
99
|
+
for (const placement of PLACEMENTS) {
|
|
100
|
+
widgets.set(placement, widgets.get(placement).filter((record) => record.clientId !== clientId));
|
|
101
|
+
}
|
|
102
|
+
for (let index = fullscreenStack.length - 1; index >= 0; index--) {
|
|
103
|
+
if (fullscreenStack[index].clientId === clientId)
|
|
104
|
+
fullscreenStack.splice(index, 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function sortedWidgets(placement) {
|
|
108
|
+
return [...widgets.get(placement)].sort((a, b) => a.order - b.order || a.seq - b.seq);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function createHostComponent(tui, theme, records, hidden) {
|
|
112
|
+
const components = hidden ? [] : records.map((record) => record.factory(tui, theme));
|
|
113
|
+
return {
|
|
114
|
+
render(width) {
|
|
115
|
+
return components.flatMap((component) => component.render(width));
|
|
116
|
+
},
|
|
117
|
+
invalidate() {
|
|
118
|
+
for (const component of components)
|
|
119
|
+
component.invalidate();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function hostKey(placement) {
|
|
124
|
+
return `${HOST_WIDGET_PREFIX}-${placement}`;
|
|
125
|
+
}
|
|
126
|
+
function readPayload(data, eventName) {
|
|
127
|
+
if (!data || typeof data !== "object") {
|
|
128
|
+
warnDrop(eventName, data);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const payload = data;
|
|
132
|
+
if (typeof payload.protocolVersion !== "number" || payload.protocolVersion > PROTOCOL_VERSION || typeof payload.clientId !== "string") {
|
|
133
|
+
warnDrop(eventName, data);
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return payload;
|
|
137
|
+
}
|
|
138
|
+
function isPlacement(value) {
|
|
139
|
+
return value === "aboveEditor" || value === "belowEditor";
|
|
140
|
+
}
|
|
141
|
+
function warnDrop(eventName, data) {
|
|
142
|
+
console.warn(`pi-extension-utils: dropping invalid ${eventName} payload`, data);
|
|
143
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
export declare const PROTOCOL_VERSION = 1;
|
|
2
3
|
export declare const EVENT_PREFIX = "pi-extension-utils";
|
|
3
4
|
export declare const HELLO_EVENT = "pi-extension-utils:hello";
|
|
@@ -15,12 +16,7 @@ export declare const EVENTS: {
|
|
|
15
16
|
readonly fullscreenRelease: "pi-extension-utils:fullscreen-release";
|
|
16
17
|
};
|
|
17
18
|
export type WidgetPlacement = "aboveEditor" | "belowEditor";
|
|
18
|
-
export
|
|
19
|
-
render(width: number): string[];
|
|
20
|
-
invalidate(): void;
|
|
21
|
-
dispose?(): void;
|
|
22
|
-
}
|
|
23
|
-
export type WidgetFactory = (tui: unknown, theme: unknown) => WidgetComponent;
|
|
19
|
+
export type WidgetFactory = NonNullable<Parameters<ExtensionContext["ui"]["setWidget"]>[1]>;
|
|
24
20
|
export interface ProtocolPayload {
|
|
25
21
|
protocolVersion: number;
|
|
26
22
|
clientId: string;
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Docs
|
|
2
|
+
|
|
3
|
+
| Doc | Covers |
|
|
4
|
+
|---|---|
|
|
5
|
+
| [client.md](client.md) | `connect()`, fullscreen, reminders, logger |
|
|
6
|
+
| [config.md](config.md) | Extension-owned JSON/JSONC config files |
|
|
7
|
+
| [widgets.md](widgets.md) | Ordered above/below editor widgets |
|
|
8
|
+
| [pane-overlay.md](pane-overlay.md) | Master/detail fullscreen overlays |
|
|
9
|
+
| [reminders.md](reminders.md) | Reminder producer API |
|
|
10
|
+
|
|
11
|
+
Reference artifacts live under [reference/](reference/).
|
package/docs/client.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Client APIs
|
|
2
|
+
|
|
3
|
+
Import from the package root:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { connect, createLogger, paneOverlay } from "pi-extension-utils";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Connect
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const client = connect(pi, { ctx, clientId: "my-extension" });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Option | Notes |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `pi` | Extension API from your extension factory |
|
|
18
|
+
| `ctx` | Current command/session context |
|
|
19
|
+
| `clientId` | Stable ID used on the shared event bus |
|
|
20
|
+
|
|
21
|
+
Call `client.dispose()` when a long-lived extension instance is shutting down.
|
|
22
|
+
|
|
23
|
+
## Widgets
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
client.widgets.set("aboveEditor", "status", (tui, theme) => component, { order: 10 });
|
|
27
|
+
client.widgets.remove("aboveEditor", "status");
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Placement | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `aboveEditor` | Widget above the editor |
|
|
33
|
+
| `belowEditor` | Widget below the editor |
|
|
34
|
+
|
|
35
|
+
The host composes widgets by `(order, insertion)`.
|
|
36
|
+
|
|
37
|
+
## Fullscreen
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
await client.ui.fullscreen((tui, theme, keybindings, done) => new MyComponent(tui, theme, done));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`ui.fullscreen()`:
|
|
44
|
+
|
|
45
|
+
- acquires a fullscreen lease
|
|
46
|
+
- blanks coordinated widgets
|
|
47
|
+
- calls `ctx.ui.custom()`
|
|
48
|
+
- releases the lease in `finally`
|
|
49
|
+
|
|
50
|
+
Use `client.fullscreen.acquire()` only when you need manual lease control.
|
|
51
|
+
|
|
52
|
+
## Pane overlay
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
await client.ui.fullscreen(paneOverlay({ primary, detail }));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
See [pane-overlay.md](pane-overlay.md).
|
|
59
|
+
|
|
60
|
+
## Reminders
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
client.reminders.upsert({ source: "my-extension", id: "state", text: "Remember this." });
|
|
64
|
+
client.reminders.remove("my-extension", "state");
|
|
65
|
+
client.reminders.clearSource("my-extension");
|
|
66
|
+
const snapshot = await client.reminders.list("my-extension");
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See [reminders.md](reminders.md).
|
|
70
|
+
|
|
71
|
+
## Logger
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const log = createLogger("my-extension", {
|
|
75
|
+
level: "info",
|
|
76
|
+
maxFiles: 5,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
log.debug("hidden at info level");
|
|
80
|
+
log.info("started");
|
|
81
|
+
log.warn("slow path");
|
|
82
|
+
log.error("failed");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`createLogger(name)` writes to `getAgentDir()/log/<name>.log` by default. `level` is typed as `"debug" | "info" | "warn" | "error" | "silent"`; `maxFiles` controls retained rotations (`.1`, `.2`, ...). If `level`, `maxFiles`, or `maxBytes` are omitted, the value comes from `getAgentDir()/config/utils.jsonc` `logging` defaults. Explicit options always win.
|
|
86
|
+
|
|
87
|
+
Override when needed:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
createLogger("my-extension", { dir: "/tmp/my-extension-logs", maxBytes: 256 * 1024 });
|
|
91
|
+
```
|
package/docs/config.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Config
|
|
2
|
+
|
|
3
|
+
`defineConfig()` manages extension-owned config files without touching Pi `settings.json`.
|
|
4
|
+
|
|
5
|
+
## Basic use
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Type, type Static } from "typebox";
|
|
10
|
+
import { defineConfig } from "pi-extension-utils";
|
|
11
|
+
|
|
12
|
+
const schema = Type.Object({
|
|
13
|
+
asyncByDefault: Type.Boolean({
|
|
14
|
+
default: false,
|
|
15
|
+
description: "Run jobs asynchronously unless the caller opts out.",
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type MyConfig = Static<typeof schema>;
|
|
20
|
+
|
|
21
|
+
const config = defineConfig({
|
|
22
|
+
name: "my-extension",
|
|
23
|
+
schema,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const cfg: MyConfig = config.get();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Default path:
|
|
30
|
+
|
|
31
|
+
```txt
|
|
32
|
+
getAgentDir()/config/<name>.jsonc
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `dir` when an extension owns a different config directory:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const config = defineConfig({
|
|
39
|
+
name: "subagent",
|
|
40
|
+
dir: getAgentDir(),
|
|
41
|
+
schema,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That resolves either:
|
|
46
|
+
|
|
47
|
+
```txt
|
|
48
|
+
<dir>/subagent.jsonc
|
|
49
|
+
<dir>/subagent.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## JSON and JSONC
|
|
53
|
+
|
|
54
|
+
Resolution is automatic:
|
|
55
|
+
|
|
56
|
+
- if only `.jsonc` exists, JSONC comments and trailing commas are allowed
|
|
57
|
+
- if only `.json` exists, strict JSON is used
|
|
58
|
+
- if neither exists, `.jsonc` is created
|
|
59
|
+
- if both exist, loading throws an ambiguity error
|
|
60
|
+
|
|
61
|
+
## Defaults and comments
|
|
62
|
+
|
|
63
|
+
Put defaults and comments on the TypeBox schema:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
Type.Boolean({
|
|
67
|
+
default: false,
|
|
68
|
+
description: "Show hidden reminders in the transcript UI for debugging.",
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`get()` creates the file when missing, applies schema defaults, validates, caches, and returns a typed value.
|
|
73
|
+
|
|
74
|
+
Generated JSONC uses `description` as comments. `update()` patches existing files through `jsonc-parser`, so existing comments and formatting are preserved where possible.
|
|
75
|
+
|
|
76
|
+
## Reload and update
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
config.reload();
|
|
80
|
+
|
|
81
|
+
config.update((cfg) => {
|
|
82
|
+
cfg.asyncByDefault = true;
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`get()` returns cached clones. Use `reload()` after external edits, such as on extension reload/session start.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Pane Overlay
|
|
2
|
+
|
|
3
|
+
`paneOverlay()` builds an opinionated fullscreen master/detail UI.
|
|
4
|
+
|
|
5
|
+
Use it when you want:
|
|
6
|
+
|
|
7
|
+
- left/right panes
|
|
8
|
+
- built-in focus, resize, cursor, scroll keys
|
|
9
|
+
- derived key legend
|
|
10
|
+
- custom actions that appear in the legend
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
await client.ui.fullscreen(
|
|
16
|
+
paneOverlay<void, Run>({
|
|
17
|
+
primary: {
|
|
18
|
+
title: "Runs",
|
|
19
|
+
mode: "cursor",
|
|
20
|
+
rows: () => runs,
|
|
21
|
+
selectionKey: (run) => run.id,
|
|
22
|
+
renderRow: (run) => run.label,
|
|
23
|
+
},
|
|
24
|
+
detail: {
|
|
25
|
+
title: (ctx) => ctx.selectedRow?.label ?? "Details",
|
|
26
|
+
rows: (ctx) => renderDetails(ctx.selectedRow),
|
|
27
|
+
},
|
|
28
|
+
customActions: [
|
|
29
|
+
{
|
|
30
|
+
keys: ["enter", "o"],
|
|
31
|
+
label: "collapse",
|
|
32
|
+
run: (ctx) => toggleCollapse(ctx.selectedKey),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Standard keys
|
|
40
|
+
|
|
41
|
+
| Keys | Action |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `esc`, `ctrl+c`, `q` | Close (`q` is configurable) |
|
|
44
|
+
| `tab`, `←`, `→` | Switch focus |
|
|
45
|
+
| `j/k`, arrows | Move cursor or scroll focused pane |
|
|
46
|
+
| `u/d` | Half-page up/down |
|
|
47
|
+
| `g/G`, `home/end` | Top/bottom |
|
|
48
|
+
| `[/]` | Resize split |
|
|
49
|
+
|
|
50
|
+
`PageUp/PageDown` are intentionally not part of this helper. Use `u/d`.
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
| Option | Purpose |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `primary` | Left pane: list or scroll content |
|
|
57
|
+
| `detail` | Right pane: content derived from selected primary row |
|
|
58
|
+
| `split` | Widths, min/max, resize step |
|
|
59
|
+
| `legendPlacement` | `footer` or `primary` |
|
|
60
|
+
| `customActions` | Extra keys with labels and handlers |
|
|
61
|
+
| `closeKeys` | Override close keys, e.g. omit `q` |
|
|
62
|
+
| `collapse` | Optional primary/sidebar collapse key |
|
|
63
|
+
| `perSelectionScroll` | Keep separate detail scroll per selected key |
|
|
64
|
+
| `stickyBottom` | Detail starts/follows at bottom until user scrolls |
|
|
65
|
+
|
|
66
|
+
## Custom actions
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
customActions: [
|
|
70
|
+
{
|
|
71
|
+
keys: "a",
|
|
72
|
+
label: "all sessions",
|
|
73
|
+
run: (ctx) => toggleAllSessions(),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
keys: "y",
|
|
77
|
+
label: "copy id",
|
|
78
|
+
when: (ctx) => ctx.detailFocus,
|
|
79
|
+
run: (ctx) => copy(ctx.selectedKey),
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Custom actions run before standard movement keys and are included in the legend by default.
|
|
85
|
+
|
|
86
|
+
## Examples
|
|
87
|
+
|
|
88
|
+
- [examples/pane-overlay.ts](../examples/pane-overlay.ts)
|