@huseynsnmz/pi-suite 0.1.0 → 0.2.1
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/CHANGELOG.md +23 -0
- package/README.md +2 -1
- package/package.json +6 -7
- package/packages/board/README.md +1 -1
- package/packages/board/package.json +1 -3
- package/packages/board/src/core/rows.mjs +21 -5
- package/packages/board/src/ui/dashboard.ts +36 -20
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +20 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +141 -0
- package/packages/frame/README.md +2 -2
- package/packages/frame/package.json +1 -4
- package/packages/frame/src/data/github.ts +1 -1
- package/packages/frame/src/data/mode.ts +28 -2
- package/packages/frame/src/data/snapshot.ts +1 -1
- package/packages/frame/src/data/start-notes.ts +1 -1
- package/packages/frame/src/data/totals.ts +2 -2
- package/packages/frame/src/data/visits.ts +1 -1
- package/packages/frame/src/index.ts +25 -3
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +55 -2
- package/packages/frame/src/render/fit.ts +1 -1
- package/packages/frame/src/render/format.ts +1 -1
- package/packages/frame/src/render/segments.ts +1 -1
- package/packages/frame/src/render/streaming.ts +1 -1
- package/packages/frame/src/render/tools.ts +1 -1
- package/packages/frame/src/settings.ts +1 -1
- package/packages/frame/src/ui/editor.ts +1 -1
- package/packages/frame/src/ui/effort.ts +1 -1
- package/packages/frame/src/ui/header.ts +1 -1
- package/packages/frame/src/ui/outputs.ts +1 -1
- package/packages/frame/src/ui/pi-entries.ts +28 -6
- package/packages/frame/src/ui/previews.ts +1 -1
- package/packages/frame/src/ui/queue.ts +1 -1
- package/packages/frame/src/ui/todos.ts +1 -1
- package/packages/frame/src/ui/tool-frame.ts +1 -1
- package/packages/frame/src/view/state.ts +1 -1
- package/packages/frame/src/view/transcript.ts +1 -1
- package/packages/frame/src/view/view.ts +1 -1
- package/packages/memory/README.md +1 -1
- package/packages/modes/README.md +1 -1
- package/packages/router/README.md +1 -1
- package/packages/web/README.md +1 -1
- package/packages/web/package.json +1 -3
- package/packages/web/src/extract.ts +2 -2
- package/packages/web/src/markdown.ts +156 -0
- package/src/index.ts +1 -1
- package/src/settings.ts +1 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Each app answers the screen for the features it runs. A change goes through the setting's own
|
|
2
|
+
// decoder, like a value read from the file.
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
APPLY,
|
|
7
|
+
type ApplyRequest,
|
|
8
|
+
DONE,
|
|
9
|
+
LIST,
|
|
10
|
+
type RowView,
|
|
11
|
+
RUN,
|
|
12
|
+
type RunDone,
|
|
13
|
+
type RunRequest,
|
|
14
|
+
type TabView,
|
|
15
|
+
} from "../contracts/screen.ts";
|
|
16
|
+
import type { Json } from "../control.ts";
|
|
17
|
+
import { formatProblems, isObject } from "../decode.ts";
|
|
18
|
+
import { controlOf, type Setting } from "../settings/setting.ts";
|
|
19
|
+
import type { SettingsStore } from "../settings/store.ts";
|
|
20
|
+
import type { Feature, ScreenEntry, ScreenGroup } from "./feature.ts";
|
|
21
|
+
|
|
22
|
+
export interface Host {
|
|
23
|
+
pi: ExtensionAPI;
|
|
24
|
+
settings: SettingsStore;
|
|
25
|
+
features: () => readonly Feature[];
|
|
26
|
+
screen: ReadonlyMap<string, ScreenEntry[]>;
|
|
27
|
+
screenGroups: ReadonlyMap<string, ScreenGroup[]>;
|
|
28
|
+
session: () => ExtensionContext | undefined;
|
|
29
|
+
isLive: () => boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function serveScreen(host: Host): void {
|
|
33
|
+
host.pi.events.on(LIST, (data) => {
|
|
34
|
+
if (!host.isLive() || !isObject(data) || !Array.isArray(data.tabs)) return;
|
|
35
|
+
const ctx = host.session();
|
|
36
|
+
for (const feature of host.features()) data.tabs.push(tabOf(host, feature, ctx));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
host.pi.events.on(APPLY, (data) => {
|
|
40
|
+
const request = asApply(data);
|
|
41
|
+
if (request === undefined || !host.isLive()) return;
|
|
42
|
+
const feature = host.features().find((entry) => entry.id === request.feature);
|
|
43
|
+
if (feature === undefined) return;
|
|
44
|
+
request.answer = { error: apply(host, feature, request) };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
host.pi.events.on(RUN, (data) => {
|
|
48
|
+
const request = asRun(data);
|
|
49
|
+
if (request === undefined || !host.isLive()) return;
|
|
50
|
+
if (!host.features().some((entry) => entry.id === request.feature)) return;
|
|
51
|
+
const ctx = host.session();
|
|
52
|
+
const row = findRow(host, request.feature, request.id, ctx);
|
|
53
|
+
if (row === undefined) return;
|
|
54
|
+
|
|
55
|
+
if (row.kind !== "action" || ctx === undefined) {
|
|
56
|
+
request.answer = { error: "nothing to run" };
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
request.answer = {};
|
|
60
|
+
const id = request.request;
|
|
61
|
+
void runAction(row, ctx).then((done) => host.pi.events.emit(DONE, { request: id, ...done }));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tabOf(host: Host, feature: Feature, ctx: ExtensionContext | undefined): TabView {
|
|
66
|
+
const rows: RowView[] = [];
|
|
67
|
+
for (const entry of feature.settings ?? []) {
|
|
68
|
+
const row = settingRow(host.settings, feature, entry, ctx);
|
|
69
|
+
if (row !== undefined) rows.push(row);
|
|
70
|
+
}
|
|
71
|
+
if (ctx !== undefined) {
|
|
72
|
+
for (const entry of entriesOf(host, feature.id, ctx)) rows.push(screenRow(feature, entry, ctx));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const listed = typeof feature.sections === "function" ? feature.sections(ctx) : feature.sections;
|
|
76
|
+
const sections = [...(listed ?? [])];
|
|
77
|
+
for (const row of rows) if (!sections.includes(row.section)) sections.push(row.section);
|
|
78
|
+
return { title: feature.tab ?? capitalize(feature.id), sections, rows };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function settingRow(
|
|
82
|
+
settings: SettingsStore,
|
|
83
|
+
feature: Feature,
|
|
84
|
+
entry: Setting<unknown>,
|
|
85
|
+
ctx: ExtensionContext | undefined,
|
|
86
|
+
): RowView | undefined {
|
|
87
|
+
const ui = entry.ui;
|
|
88
|
+
if (ui === undefined) return undefined;
|
|
89
|
+
|
|
90
|
+
const control = controlOf(entry, ctx);
|
|
91
|
+
const value = toJson(settings.get(entry));
|
|
92
|
+
const layer = settings.layer(entry);
|
|
93
|
+
const row: RowView = {
|
|
94
|
+
feature: feature.id,
|
|
95
|
+
id: entry.id,
|
|
96
|
+
kind: control === undefined ? "info" : "setting",
|
|
97
|
+
section: ui.section,
|
|
98
|
+
label: ui.label,
|
|
99
|
+
description: ui.description,
|
|
100
|
+
value,
|
|
101
|
+
layer,
|
|
102
|
+
fallback: toJson(entry.default),
|
|
103
|
+
};
|
|
104
|
+
if (control === undefined) row.text = JSON.stringify(value);
|
|
105
|
+
else row.control = control;
|
|
106
|
+
if (layer === "project") row.hidden = toJson(settings.hidden(entry));
|
|
107
|
+
if (ui.restart === true) row.restart = true;
|
|
108
|
+
if (ui.preview !== undefined) row.preview = ui.preview;
|
|
109
|
+
return row;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function screenRow(feature: Feature, entry: ScreenEntry, ctx: ExtensionContext): RowView {
|
|
113
|
+
const row: RowView = {
|
|
114
|
+
feature: feature.id,
|
|
115
|
+
id: entry.id,
|
|
116
|
+
kind: entry.kind,
|
|
117
|
+
section: entry.section,
|
|
118
|
+
label: entry.label,
|
|
119
|
+
description: entry.description,
|
|
120
|
+
};
|
|
121
|
+
if (entry.indent !== undefined) row.indent = entry.indent;
|
|
122
|
+
switch (entry.kind) {
|
|
123
|
+
case "value":
|
|
124
|
+
row.control = typeof entry.control === "function" ? entry.control(ctx) : entry.control;
|
|
125
|
+
row.value = entry.get(ctx);
|
|
126
|
+
if (entry.meta !== undefined) row.meta = entry.meta;
|
|
127
|
+
const shown = entry.text?.(ctx);
|
|
128
|
+
if (shown !== undefined) row.text = shown;
|
|
129
|
+
break;
|
|
130
|
+
case "action": {
|
|
131
|
+
const text = entry.text?.(ctx);
|
|
132
|
+
if (text !== undefined) row.text = text;
|
|
133
|
+
if (entry.confirm !== undefined) row.confirm = entry.confirm;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "info":
|
|
137
|
+
row.text = entry.text(ctx);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
return row;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function apply(host: Host, feature: Feature, request: ApplyRequest): string | undefined {
|
|
144
|
+
const entry = feature.settings?.find((candidate) => candidate.id === request.id);
|
|
145
|
+
if (entry !== undefined) {
|
|
146
|
+
if (request.op === "unset") {
|
|
147
|
+
const layer = host.settings.layer(entry);
|
|
148
|
+
return layer === "default" ? undefined : host.settings.unset(entry, layer);
|
|
149
|
+
}
|
|
150
|
+
const decoded = entry.decoder.decode(request.value, entry.id);
|
|
151
|
+
if (!decoded.ok) return formatProblems(decoded.problems).join("; ");
|
|
152
|
+
return host.settings.set(entry, decoded.value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const ctx = host.session();
|
|
156
|
+
const row = findRow(host, feature.id, request.id, ctx);
|
|
157
|
+
if (row?.kind !== "value" || ctx === undefined) return "this row cannot change";
|
|
158
|
+
if (request.op === "unset") return "this row has no default to go back to";
|
|
159
|
+
return row.set(request.value ?? null, ctx);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function entriesOf(host: Host, feature: string, ctx: ExtensionContext): ScreenEntry[] {
|
|
163
|
+
return [
|
|
164
|
+
...(host.screen.get(feature) ?? []),
|
|
165
|
+
...(host.screenGroups.get(feature) ?? []).flatMap((group) => group(ctx)),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** An action can sit on a built row too, so the lookup walks the same two places. */
|
|
170
|
+
function findRow(
|
|
171
|
+
host: Host,
|
|
172
|
+
feature: string,
|
|
173
|
+
id: string,
|
|
174
|
+
ctx: ExtensionContext | undefined,
|
|
175
|
+
): ScreenEntry | undefined {
|
|
176
|
+
if (ctx === undefined) return host.screen.get(feature)?.find((candidate) => candidate.id === id);
|
|
177
|
+
return entriesOf(host, feature, ctx).find((candidate) => candidate.id === id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function runAction(
|
|
181
|
+
row: Extract<ScreenEntry, { kind: "action" }>,
|
|
182
|
+
ctx: ExtensionContext,
|
|
183
|
+
): Promise<Omit<RunDone, "request">> {
|
|
184
|
+
try {
|
|
185
|
+
const text = await row.run(ctx);
|
|
186
|
+
return text === undefined ? {} : { text };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function asApply(data: unknown): ApplyRequest | undefined {
|
|
193
|
+
if (!isObject(data) || data.answer !== undefined) return undefined;
|
|
194
|
+
const { feature, id, op } = data;
|
|
195
|
+
if (typeof feature !== "string" || typeof id !== "string") return undefined;
|
|
196
|
+
if (op !== "set" && op !== "unset") return undefined;
|
|
197
|
+
return data as unknown as ApplyRequest;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function asRun(data: unknown): RunRequest | undefined {
|
|
201
|
+
if (!isObject(data) || data.answer !== undefined) return undefined;
|
|
202
|
+
const { feature, id, request } = data;
|
|
203
|
+
if (typeof feature !== "string" || typeof id !== "string" || typeof request !== "string")
|
|
204
|
+
return undefined;
|
|
205
|
+
return data as unknown as RunRequest;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function toJson(value: unknown): Json {
|
|
209
|
+
return value === undefined ? null : (JSON.parse(JSON.stringify(value)) as Json);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function capitalize(text: string): string {
|
|
213
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
214
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The renderers the harness's features register, kept so a view can draw another session's entries
|
|
2
|
+
// the way pi draws this one's. Pi resolves its own chain but gives extensions no way to call it.
|
|
3
|
+
import type {
|
|
4
|
+
MessageRenderer,
|
|
5
|
+
ToolRendererResolver,
|
|
6
|
+
ToolRenderers,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
interface Registered {
|
|
10
|
+
tools: ToolRendererResolver[];
|
|
11
|
+
messages: Map<string, MessageRenderer>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// By feature, in the order they mount, which is the order pi resolves them in. A feature mounted again
|
|
15
|
+
// for the next session replaces what it registered for the last one.
|
|
16
|
+
const byFeature = new Map<string, Registered>();
|
|
17
|
+
|
|
18
|
+
export function forgetRenderers(feature: string): void {
|
|
19
|
+
byFeature.delete(feature);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function keepToolRenderer(feature: string, resolver: ToolRendererResolver): void {
|
|
23
|
+
registered(feature).tools.push(resolver);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function keepMessageRenderer(
|
|
27
|
+
feature: string,
|
|
28
|
+
customType: string,
|
|
29
|
+
renderer: MessageRenderer,
|
|
30
|
+
): void {
|
|
31
|
+
registered(feature).messages.set(customType, renderer);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** As pi resolves them: every feature's resolver over `base`. */
|
|
35
|
+
export function resolveToolRenderers(
|
|
36
|
+
toolName: string,
|
|
37
|
+
base: () => ToolRenderers | undefined,
|
|
38
|
+
): ToolRenderers | undefined {
|
|
39
|
+
const resolvers = [...byFeature.values()].flatMap((one) => one.tools);
|
|
40
|
+
function resolve(index: number): ToolRenderers | undefined {
|
|
41
|
+
const resolver = resolvers[index];
|
|
42
|
+
return resolver === undefined ? base() : resolver(toolName, () => resolve(index + 1));
|
|
43
|
+
}
|
|
44
|
+
return resolve(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function messageRendererFor(customType: string): MessageRenderer | undefined {
|
|
48
|
+
for (const one of byFeature.values()) {
|
|
49
|
+
const renderer = one.messages.get(customType);
|
|
50
|
+
if (renderer !== undefined) return renderer;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function registered(feature: string): Registered {
|
|
56
|
+
let one = byFeature.get(feature);
|
|
57
|
+
if (one === undefined) {
|
|
58
|
+
one = { tools: [], messages: new Map() };
|
|
59
|
+
byFeature.set(feature, one);
|
|
60
|
+
}
|
|
61
|
+
return one;
|
|
62
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ExtensionAPI, MessageRenderer } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { SettingsStore } from "../settings/store.ts";
|
|
4
|
+
import { attributed } from "./attribution.ts";
|
|
5
|
+
import { describe } from "./attribution.ts";
|
|
6
|
+
import type {
|
|
7
|
+
FeatureScope,
|
|
8
|
+
ScreenEntry,
|
|
9
|
+
ScreenGroup,
|
|
10
|
+
SessionHook,
|
|
11
|
+
ShutdownHook,
|
|
12
|
+
} from "./feature.ts";
|
|
13
|
+
import { forgetRenderers, keepMessageRenderer, keepToolRenderer } from "./renderers.ts";
|
|
14
|
+
|
|
15
|
+
export interface Hook<T> {
|
|
16
|
+
source: string;
|
|
17
|
+
run: T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AppState {
|
|
21
|
+
name: string;
|
|
22
|
+
pi: ExtensionAPI;
|
|
23
|
+
settings: SettingsStore;
|
|
24
|
+
mounted: ReadonlySet<string>;
|
|
25
|
+
commands: Map<string, string>;
|
|
26
|
+
starts: Hook<SessionHook>[];
|
|
27
|
+
shutdowns: Hook<ShutdownHook>[];
|
|
28
|
+
screen: Map<string, ScreenEntry[]>;
|
|
29
|
+
screenGroups: Map<string, ScreenGroup[]>;
|
|
30
|
+
live(): boolean;
|
|
31
|
+
/** Ends the session from inside a deferral a refused context was found in. */
|
|
32
|
+
stop(): void;
|
|
33
|
+
report(source: string, message: string): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createScope(state: AppState, featureId: string): FeatureScope {
|
|
37
|
+
const label = `${state.name}: ${featureId}`;
|
|
38
|
+
const rows: ScreenEntry[] = state.screen.get(featureId) ?? [];
|
|
39
|
+
state.screen.set(featureId, rows);
|
|
40
|
+
const groups: ScreenGroup[] = state.screenGroups.get(featureId) ?? [];
|
|
41
|
+
state.screenGroups.set(featureId, groups);
|
|
42
|
+
forgetRenderers(featureId);
|
|
43
|
+
const add = (row: ScreenEntry): void => {
|
|
44
|
+
if (rows.some((entry) => entry.id === row.id)) {
|
|
45
|
+
state.report(featureId, `the screen row "${row.id}" was added twice; keeping the first`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
rows.push(row);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...state.pi,
|
|
53
|
+
id: featureId,
|
|
54
|
+
settings: state.settings,
|
|
55
|
+
on: attributedOn(state.pi, label),
|
|
56
|
+
registerCommand(name, options) {
|
|
57
|
+
const owner = state.commands.get(name);
|
|
58
|
+
if (owner !== undefined) {
|
|
59
|
+
state.report(featureId, `/${name} is already registered by ${owner}; skipped`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
state.commands.set(name, featureId);
|
|
63
|
+
state.pi.registerCommand(name, {
|
|
64
|
+
...options,
|
|
65
|
+
handler: attributed(`${label}: /${name}`, options.handler),
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
registerToolRenderer(resolver) {
|
|
69
|
+
keepToolRenderer(featureId, resolver);
|
|
70
|
+
state.pi.registerToolRenderer(resolver);
|
|
71
|
+
},
|
|
72
|
+
registerMessageRenderer(customType, renderer) {
|
|
73
|
+
keepMessageRenderer(featureId, customType, renderer as MessageRenderer);
|
|
74
|
+
state.pi.registerMessageRenderer(customType, renderer);
|
|
75
|
+
},
|
|
76
|
+
defer(run, ms = 0) {
|
|
77
|
+
// `unref` only keeps a pending timer from holding the process open; the guard at the top
|
|
78
|
+
// of the callback is what keeps the work from running once the session is over.
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
if (!state.live()) return;
|
|
81
|
+
try {
|
|
82
|
+
run();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// The host takes the context away before it announces the shutdown, so `live` is
|
|
85
|
+
// still true here; its refusal is the only notice that the session is over.
|
|
86
|
+
if (refusedByHost(error)) {
|
|
87
|
+
state.stop();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
state.report(featureId, `deferred work failed: ${describe(error)}`);
|
|
91
|
+
}
|
|
92
|
+
}, ms).unref();
|
|
93
|
+
},
|
|
94
|
+
onSessionStart: (run) => state.starts.push({ source: featureId, run }),
|
|
95
|
+
onShutdown: (run) => state.shutdowns.push({ source: featureId, run }),
|
|
96
|
+
warn: (message) => state.report(featureId, message),
|
|
97
|
+
has: (id) => state.mounted.has(id),
|
|
98
|
+
screen: {
|
|
99
|
+
value: (row) => add({ kind: "value", ...row }),
|
|
100
|
+
action: (row) => add({ kind: "action", ...row }),
|
|
101
|
+
info: (row) => add({ kind: "info", ...row }),
|
|
102
|
+
rows: (provider) => void groups.push(provider),
|
|
103
|
+
},
|
|
104
|
+
} as FeatureScope;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Pi's message for a stale context (`dist/core/extensions/runner.js` in 1.4.0). */
|
|
108
|
+
const STALE = "stale after session replacement or reload";
|
|
109
|
+
|
|
110
|
+
/** A session that ended announces itself by the host refusing its context. */
|
|
111
|
+
function refusedByHost(error: unknown): boolean {
|
|
112
|
+
return error instanceof Error && error.message.includes(STALE);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type UntypedHandler = (...args: unknown[]) => unknown;
|
|
116
|
+
type UntypedOn = (event: string, handler: UntypedHandler) => () => void;
|
|
117
|
+
|
|
118
|
+
// pi.on has one overload per event and TypeScript cannot implement an overloaded signature
|
|
119
|
+
// generically, so this is the one place that casts. Callers keep the exact overload types.
|
|
120
|
+
function attributedOn(pi: ExtensionAPI, label: string): ExtensionAPI["on"] {
|
|
121
|
+
const on = pi.on.bind(pi) as unknown as UntypedOn;
|
|
122
|
+
const wrapped: UntypedOn = (event, handler) =>
|
|
123
|
+
on(event, attributed(`${label}: ${event}`, handler));
|
|
124
|
+
return wrapped as unknown as ExtensionAPI["on"];
|
|
125
|
+
}
|