pi-warden 0.2.1 → 0.4.0
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 +61 -13
- package/dist/config.d.ts +19 -4
- package/dist/config.js +35 -6
- package/dist/done.d.ts +2 -2
- package/dist/done.js +11 -13
- package/dist/extension.d.ts +3 -1
- package/dist/extension.js +125 -28
- package/dist/guard.d.ts +20 -11
- package/dist/guard.js +53 -38
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -0
- package/dist/panel.d.ts +44 -0
- package/dist/panel.js +121 -0
- package/dist/prose.d.ts +57 -0
- package/dist/prose.js +98 -0
- package/dist/stuck.d.ts +6 -3
- package/dist/stuck.js +11 -14
- package/dist/tools.d.ts +16 -0
- package/dist/tools.js +37 -0
- package/dist/trace.d.ts +31 -0
- package/dist/trace.js +93 -0
- package/dist/widget.d.ts +40 -0
- package/dist/widget.js +120 -0
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { MouseRegion, Text } from "@earendil-works/pi-tui";
|
|
1
2
|
import { createTypeSafe, resolveApiKey } from "pi-typesafe";
|
|
2
3
|
import { ensureApiKey } from "pi-typesafe/ui";
|
|
3
4
|
import { applyUserOverrides, defaultConfig, isMode, loadConfig, PACKAGE_NAME, projectConfigPath, readUserConfig, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
4
5
|
import { classifyToolResult, doneNudge, emptyEvidence, evaluateDone, finalAssistantText, formatDone, needsDoneCheck, recordOutcome } from "./done.js";
|
|
5
|
-
import { evaluateAction, formatVerdict, steerReason, textApproves } from "./guard.js";
|
|
6
|
+
import { evaluateAction, formatVerdict, SLOP_LABELS, steerReason, textApproves } from "./guard.js";
|
|
7
|
+
import { evaluateProse, proseNudge, ProseTrend } from "./prose.js";
|
|
6
8
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
9
|
+
import { openTracePanel } from "./panel.js";
|
|
10
|
+
import { actionDetails, doneDetails, proseDetails, stuckDetails, Trace } from "./trace.js";
|
|
11
|
+
import { proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
7
12
|
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs; the last few tool calls and output tails when the agent keeps failing; and the agent's final message when it reports completion without running checks. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
8
13
|
const WIDGET = PACKAGE_NAME;
|
|
9
14
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
10
|
-
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, slop: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, errors: 0 });
|
|
15
|
+
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, slop: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, proseChecks: 0, proseNudges: 0, errors: 0 });
|
|
11
16
|
function latestUserPrompt(ctx) {
|
|
12
17
|
const entries = ctx.sessionManager.getBranch();
|
|
13
18
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
@@ -45,17 +50,35 @@ export function confirmMessage(verdict) {
|
|
|
45
50
|
return lines.join("\n");
|
|
46
51
|
}
|
|
47
52
|
const callKey = (tool, input) => `${tool}\0${JSON.stringify(input)}`;
|
|
53
|
+
const SLOP_FIXES = {
|
|
54
|
+
stub: "replace stubs, placeholders, and hard-coded fake data with the working implementation, or state in your reply exactly what is left unimplemented and why",
|
|
55
|
+
comments: "delete comments that restate the code; keep only those that explain intent, constraints, or non-obvious behaviour",
|
|
56
|
+
dead: "remove commented-out code, unused imports and variables, duplicated logic, and unreachable branches",
|
|
57
|
+
hedging: "replace \"should work\", \"for now\", and TODOs without a plan with a definite statement or a concrete follow-up",
|
|
58
|
+
};
|
|
59
|
+
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
60
|
+
export function slopSteer(where, symptoms, counts) {
|
|
61
|
+
const named = symptoms.map(symptom => `${SLOP_LABELS[symptom]}${counts[symptom] >= 3 ? ` (${counts[symptom]}th time this session)` : ""}`).join("; ");
|
|
62
|
+
const fixes = symptoms.map(symptom => SLOP_FIXES[symptom]).join("; ");
|
|
63
|
+
const standing = symptoms.some(symptom => counts[symptom] >= 3) ? " Treat this as a standing rule for the rest of the session." : "";
|
|
64
|
+
return `pi-warden: the content just written to ${where} has ${named}. Fix it in your next edit: ${fixes}.${standing}`;
|
|
65
|
+
}
|
|
48
66
|
/** Native Pi registration; importing the root library does not load this module. */
|
|
49
67
|
export default function wardenExtension(pi) {
|
|
50
68
|
let client;
|
|
51
69
|
let budgetExhausted = false;
|
|
52
70
|
let stats = freshStats();
|
|
53
71
|
const widget = new Map();
|
|
72
|
+
const trace = new Trace();
|
|
73
|
+
let panelOpen = false;
|
|
74
|
+
let lastUi;
|
|
54
75
|
/** Calls held in steer mode, with the user prompt current at that time; a retry after the user replies asks Jev about approval. */
|
|
55
76
|
const held = new Map();
|
|
56
77
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
57
78
|
let evidence = emptyEvidence();
|
|
58
79
|
let doneNudged = false;
|
|
80
|
+
const prose = new ProseTrend();
|
|
81
|
+
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
59
82
|
const configFor = (ctx) => loadConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
|
|
60
83
|
const consentGiven = (config) => config.typesafe || process.env.PI_WARDEN_ENABLED === "1";
|
|
61
84
|
const consentSource = (config) => config.typesafe ? "/warden enable" : process.env.PI_WARDEN_ENABLED === "1" ? "PI_WARDEN_ENABLED" : undefined;
|
|
@@ -71,21 +94,48 @@ export default function wardenExtension(pi) {
|
|
|
71
94
|
if (ctx.hasUI)
|
|
72
95
|
ctx.ui.notify(`warden: ${message}${budgetExhausted ? " Pattern checks continue without TypeSafe for the rest of this session." : ""}`, "warning");
|
|
73
96
|
};
|
|
74
|
-
const
|
|
97
|
+
const openPanel = (ui) => {
|
|
98
|
+
if (!ui || panelOpen)
|
|
99
|
+
return;
|
|
100
|
+
panelOpen = true;
|
|
101
|
+
openTracePanel(ui, trace).catch(() => undefined).finally(() => { panelOpen = false; });
|
|
102
|
+
};
|
|
103
|
+
const paint = (ctx, config) => {
|
|
104
|
+
if (!ctx.hasUI)
|
|
105
|
+
return;
|
|
106
|
+
lastUi = ctx.ui;
|
|
107
|
+
if (!config.widget.enabled || widget.size === 0) {
|
|
108
|
+
ctx.ui.setWidget(WIDGET, undefined);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const lines = [...widget.values()];
|
|
112
|
+
// A custom component so a click (fullscreen mode) opens the trace panel; plain lines otherwise look the same.
|
|
113
|
+
ctx.ui.setWidget(WIDGET, (_tui, theme) => new MouseRegion(new Text(lines.map(line => theme.fg("muted", line)).join("\n"), 0, 0), event => {
|
|
114
|
+
if (event.type !== "click" || event.button !== "left")
|
|
115
|
+
return undefined;
|
|
116
|
+
openPanel(lastUi);
|
|
117
|
+
return { handled: true };
|
|
118
|
+
}), { placement: config.widget.placement });
|
|
119
|
+
};
|
|
120
|
+
const record = (ctx, config, guard, line, details) => {
|
|
75
121
|
widget.set(guard, line);
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
trace.push({ at: Date.now(), guard, line, details });
|
|
123
|
+
paint(ctx, config);
|
|
78
124
|
};
|
|
79
|
-
const steer = (content) => pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content, display:
|
|
125
|
+
const steer = (config, content, options = { deliverAs: "steer" }) => pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content, display: config.steerVisible }, options);
|
|
80
126
|
pi.on("session_start", async (_event, ctx) => {
|
|
81
127
|
client = undefined;
|
|
82
128
|
budgetExhausted = false;
|
|
83
129
|
stats = freshStats();
|
|
84
130
|
widget.clear();
|
|
131
|
+
trace.clear();
|
|
85
132
|
held.clear();
|
|
86
133
|
attempts.reset();
|
|
87
134
|
evidence = emptyEvidence();
|
|
88
135
|
doneNudged = false;
|
|
136
|
+
prose.reset();
|
|
137
|
+
for (const symptom of Object.keys(slopCounts))
|
|
138
|
+
slopCounts[symptom] = 0;
|
|
89
139
|
if (ctx.hasUI)
|
|
90
140
|
ctx.ui.setWidget(WIDGET, undefined);
|
|
91
141
|
});
|
|
@@ -125,14 +175,18 @@ export default function wardenExtension(pi) {
|
|
|
125
175
|
stats.approved++;
|
|
126
176
|
held.delete(key);
|
|
127
177
|
}
|
|
178
|
+
const mode = activeMode(config, ctx.hasUI);
|
|
179
|
+
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(verdict, { canApprove: judge !== undefined }) : undefined;
|
|
128
180
|
if (verdict.source !== "read-only")
|
|
129
|
-
|
|
130
|
-
if (verdict.
|
|
181
|
+
record(ctx, config, "action", formatVerdict(verdict, config.widget.action), actionDetails(verdict, { mode, ...(told ? { told } : {}) }));
|
|
182
|
+
if (verdict.slopSymptoms?.length && verdict.slopReasons) {
|
|
131
183
|
stats.slop++;
|
|
184
|
+
for (const symptom of verdict.slopSymptoms)
|
|
185
|
+
slopCounts[symptom]++;
|
|
132
186
|
const where = verdict.summary.path ?? event.toolName;
|
|
133
187
|
if (ctx.hasUI)
|
|
134
188
|
ctx.ui.notify(`warden · slop · ${where}: ${verdict.slopReasons.join("; ")}`, "warning");
|
|
135
|
-
steer(
|
|
189
|
+
steer(config, slopSteer(where, verdict.slopSymptoms, slopCounts));
|
|
136
190
|
}
|
|
137
191
|
if (verdict.level === "warn") {
|
|
138
192
|
stats.warned++;
|
|
@@ -142,7 +196,6 @@ export default function wardenExtension(pi) {
|
|
|
142
196
|
}
|
|
143
197
|
if (verdict.level !== "confirm")
|
|
144
198
|
return undefined;
|
|
145
|
-
const mode = activeMode(config, ctx.hasUI);
|
|
146
199
|
const reasons = verdict.reasons.join("; ");
|
|
147
200
|
if (mode === "advise") {
|
|
148
201
|
stats.warned++;
|
|
@@ -161,15 +214,15 @@ export default function wardenExtension(pi) {
|
|
|
161
214
|
held.set(key, task);
|
|
162
215
|
if (ctx.hasUI)
|
|
163
216
|
ctx.ui.notify(`warden · held ${event.toolName}: ${reasons}. The agent was told why and asked to re-plan or ask you.`, "warning");
|
|
164
|
-
return { block: true, reason: steerReason(verdict, { canApprove: judge !== undefined }) };
|
|
217
|
+
return { block: true, reason: told ?? steerReason(verdict, { canApprove: judge !== undefined }) };
|
|
165
218
|
});
|
|
166
219
|
pi.on("tool_result", async (event, ctx) => {
|
|
167
220
|
const config = configFor(ctx);
|
|
168
221
|
if (!config.enabled)
|
|
169
222
|
return;
|
|
170
|
-
const failed = resultFailed(event.isError, event.details);
|
|
223
|
+
const failed = resultFailed(event.isError, event.details, event.content);
|
|
171
224
|
if (config.done.enabled)
|
|
172
|
-
recordOutcome(evidence, classifyToolResult(event.toolName, event.input, failed), event.input);
|
|
225
|
+
recordOutcome(evidence, classifyToolResult(event.toolName, event.input, failed), event.input, event.toolName);
|
|
173
226
|
if (!config.stuck.enabled)
|
|
174
227
|
return;
|
|
175
228
|
attempts.push(makeAttempt(event.toolName, event.input, event.content, failed));
|
|
@@ -181,41 +234,75 @@ export default function wardenExtension(pi) {
|
|
|
181
234
|
noteError(ctx, verdict.error, undefined);
|
|
182
235
|
if (verdict.source === "repeat" && !verdict.stuck)
|
|
183
236
|
return;
|
|
184
|
-
|
|
237
|
+
const nudge = verdict.stuck && config.stuck.nudge ? stuckNudge(verdict) : undefined;
|
|
238
|
+
record(ctx, config, "stuck", formatStuck(verdict, config.widget.stuck), stuckDetails(verdict, attempts.attempts, nudge));
|
|
185
239
|
if (!verdict.stuck)
|
|
186
240
|
return;
|
|
187
241
|
stats.stuck++;
|
|
188
242
|
if (ctx.hasUI)
|
|
189
|
-
ctx.ui.notify(`warden · stuck: ${verdict.reasons.join("; ")}${
|
|
190
|
-
if (
|
|
191
|
-
steer(
|
|
243
|
+
ctx.ui.notify(`warden · stuck: ${verdict.reasons.join("; ")}${nudge ? " (agent nudged)" : ""}`, "warning");
|
|
244
|
+
if (nudge)
|
|
245
|
+
steer(config, nudge);
|
|
192
246
|
});
|
|
193
247
|
pi.on("agent_end", async (event, ctx) => {
|
|
194
248
|
const config = configFor(ctx);
|
|
195
|
-
if (!config.enabled
|
|
249
|
+
if (!config.enabled)
|
|
196
250
|
return;
|
|
197
251
|
const finalMessage = finalAssistantText(event.messages);
|
|
198
252
|
const judge = judgeFor(config);
|
|
199
253
|
if (!finalMessage || !judge)
|
|
200
254
|
return;
|
|
255
|
+
if (config.slop.enabled && config.slop.prose.enabled && finalMessage.length >= config.slop.prose.minChars) {
|
|
256
|
+
stats.proseChecks++;
|
|
257
|
+
const verdict = await evaluateProse(latestUserPrompt(ctx), finalMessage, { config: config.slop.prose, judge, timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
258
|
+
if (verdict.error)
|
|
259
|
+
noteError(ctx, verdict.error, undefined);
|
|
260
|
+
else
|
|
261
|
+
prose.record(verdict.flagged);
|
|
262
|
+
const due = verdict.error ? [] : prose.due(config.slop.prose.trend);
|
|
263
|
+
const nudge = due.length ? proseNudge(due, config.slop.prose.audience, prose.counts) : undefined;
|
|
264
|
+
if (nudge) {
|
|
265
|
+
verdict.nudged = true;
|
|
266
|
+
prose.markNudged();
|
|
267
|
+
stats.proseNudges++;
|
|
268
|
+
}
|
|
269
|
+
record(ctx, config, "prose", renderTemplate(config.widget.prose, proseTokens(verdict)), proseDetails(verdict, finalMessage, config.slop.prose.audience, nudge));
|
|
270
|
+
if (nudge) {
|
|
271
|
+
if (ctx.hasUI)
|
|
272
|
+
ctx.ui.notify(`warden · prose: ${due.join(", ")} in ${config.slop.prose.trend} of the last 3 replies (agent nudged for the next reply)`, "warning");
|
|
273
|
+
steer(config, nudge, { deliverAs: "nextTurn" });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!config.done.enabled || doneNudged || !needsDoneCheck(evidence))
|
|
277
|
+
return;
|
|
201
278
|
stats.doneChecks++;
|
|
202
279
|
const verdict = await evaluateDone(latestUserPrompt(ctx), finalMessage, evidence, { config: config.done, judge, timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
203
280
|
if (verdict.error)
|
|
204
281
|
noteError(ctx, verdict.error, undefined);
|
|
205
|
-
|
|
282
|
+
const nudge = verdict.unverified && config.done.nudge ? doneNudge(verdict) : undefined;
|
|
283
|
+
record(ctx, config, "done", formatDone(verdict, config.widget.done), doneDetails(verdict, finalMessage, nudge));
|
|
206
284
|
if (!verdict.unverified)
|
|
207
285
|
return;
|
|
208
286
|
stats.unverified++;
|
|
209
287
|
if (ctx.hasUI)
|
|
210
|
-
ctx.ui.notify(`warden · done-check: ${verdict.reasons.join("; ")}${
|
|
211
|
-
if (
|
|
288
|
+
ctx.ui.notify(`warden · done-check: ${verdict.reasons.join("; ")}${nudge ? " (agent asked to verify)" : ""}`, "warning");
|
|
289
|
+
if (nudge) {
|
|
212
290
|
doneNudged = true;
|
|
213
|
-
|
|
291
|
+
steer(config, nudge, { deliverAs: "followUp", triggerTurn: true });
|
|
214
292
|
}
|
|
215
293
|
});
|
|
216
|
-
|
|
294
|
+
// Shortcuts are registered once at load; a shape check keeps a typo in the config file from being registered.
|
|
295
|
+
const shortcut = loadConfig().widget.shortcut;
|
|
296
|
+
if (/^(?:(?:ctrl|shift|alt|super)\+)+[a-z0-9]+$|^f\d{1,2}$/i.test(shortcut)) {
|
|
297
|
+
pi.registerShortcut(shortcut, {
|
|
298
|
+
description: "Open the pi-warden trace panel",
|
|
299
|
+
handler: async (ctx) => { if (ctx.hasUI)
|
|
300
|
+
openPanel(ctx.ui); },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const actions = ["status", "enable", "disable", "mode", "config", "test", "trace"];
|
|
217
304
|
pi.registerCommand("warden", {
|
|
218
|
-
description: "pi-warden status, TypeSafe consent, steer/confirm/advise mode, config editor, and a synthetic guard test",
|
|
305
|
+
description: "pi-warden status, TypeSafe consent, steer/confirm/advise mode, config editor, trace panel, and a synthetic guard test",
|
|
219
306
|
getArgumentCompletions(prefix) {
|
|
220
307
|
const matches = actions.filter(action => action.startsWith(prefix)).map(action => ({ value: action, label: action }));
|
|
221
308
|
return matches.length ? matches : null;
|
|
@@ -234,16 +321,26 @@ export default function wardenExtension(pi) {
|
|
|
234
321
|
const key = resolveApiKey();
|
|
235
322
|
const source = consentSource(config);
|
|
236
323
|
const usage = client?.getUsage();
|
|
237
|
-
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop"].filter(Boolean).join(", ");
|
|
324
|
+
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop", config.slop.enabled && config.slop.prose.enabled && `prose (${config.slop.prose.audience})`].filter(Boolean).join(", ");
|
|
238
325
|
report([
|
|
239
326
|
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `enabled via ${source}` : "disabled (run /warden enable)"}; key ${key ? key.source === "stored" ? "stored (shared with pi-typesafe)" : "from TYPESAFE_API_KEY" : "missing (run /warden enable)"}.`,
|
|
240
|
-
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.slop} slop notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests.`,
|
|
241
|
-
`Thresholds: irreversible warn ${config.action.irreversible.warn} / hold ${config.action.irreversible.confirm}; off-task warn ${config.action.offTask.warn} / hold ${config.action.offTask.confirm}; stuck same-strategy ${config.stuck.sameStrategy} after ${config.stuck.minFailures} failures; done claims ${config.done.claimsDone}; slop
|
|
327
|
+
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.slop} slop notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}.`,
|
|
328
|
+
`Thresholds: irreversible warn ${config.action.irreversible.warn} / hold ${config.action.irreversible.confirm}; off-task warn ${config.action.offTask.warn} / hold ${config.action.offTask.confirm}; stuck same-strategy ${config.stuck.sameStrategy} after ${config.stuck.minFailures} failures; done claims ${config.done.claimsDone}; slop ${config.slop.threshold}, prose ${config.slop.prose.threshold} in ${config.slop.prose.trend}/3 replies; failOpen ${config.action.failOpen}.`,
|
|
242
329
|
`Config: ${userConfigPath()}${ctx.isProjectTrusted() ? ` and ${projectConfigPath(ctx.cwd)}` : ""}.`,
|
|
243
330
|
widget.size ? `Last: ${[...widget.values()].join(" | ")}` : "No guarded activity yet this session.",
|
|
331
|
+
`Trace: ${trace.entries().length} events (/warden trace${shortcut ? `, ${shortcut}` : ""}, or click the status line in fullscreen mode). Widget templates in config.widget: action tokens ${TOKEN_NAMES.action.map(name => `{${name}}`).join(" ")}.`,
|
|
244
332
|
].join(" "));
|
|
245
333
|
return;
|
|
246
334
|
}
|
|
335
|
+
if (action === "trace") {
|
|
336
|
+
if (!ctx.hasUI) {
|
|
337
|
+
const entries = trace.entries();
|
|
338
|
+
report(entries.length ? entries.slice(-20).map(entry => `${new Date(entry.at).toTimeString().slice(0, 8)} ${entry.guard}: ${entry.line}${entry.details.length ? `\n ${entry.details.join("\n ")}` : ""}`).join("\n") : "No guarded activity yet this session.");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
openPanel(ctx.ui);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
247
344
|
if (action === "enable") {
|
|
248
345
|
if (!ctx.hasUI) {
|
|
249
346
|
report("Consent needs an interactive session. For headless runs set PI_WARDEN_ENABLED=1 and TYPESAFE_API_KEY explicitly.", "warning");
|
|
@@ -310,7 +407,7 @@ export default function wardenExtension(pi) {
|
|
|
310
407
|
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "Clean up the demo directory") goes to api.typesafe.ai and may incur charges. ${disclosure}`))
|
|
311
408
|
return;
|
|
312
409
|
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "Clean up the demo directory" }, { config: { ...config.action, enabled: true, tools: ["bash"] }, judge });
|
|
313
|
-
|
|
410
|
+
record(ctx, config, "action", formatVerdict(verdict, config.widget.action), actionDetails(verdict, { mode: activeMode(config, ctx.hasUI) }));
|
|
314
411
|
report(`${formatVerdict(verdict)}${verdict.reasons.length ? ` — ${verdict.reasons.join("; ")}` : ""}${judge ? "" : " (pattern checks only: TypeSafe judgments are not enabled or no key is configured)"}${verdict.error ? ` — ${verdict.error}` : ""}`);
|
|
315
412
|
if (verdict.level === "confirm") {
|
|
316
413
|
const mode = activeMode(config, ctx.hasUI);
|
package/dist/guard.d.ts
CHANGED
|
@@ -42,12 +42,15 @@ export interface Judgment {
|
|
|
42
42
|
model: string;
|
|
43
43
|
elapsedMs: number;
|
|
44
44
|
}
|
|
45
|
+
/** One probability per slop symptom; the steer names the symptoms above the threshold. */
|
|
45
46
|
export interface SlopJudgment {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
stub: number;
|
|
48
|
+
comments: number;
|
|
49
|
+
dead: number;
|
|
50
|
+
hedging: number;
|
|
50
51
|
}
|
|
52
|
+
export type SlopSymptom = keyof SlopJudgment;
|
|
53
|
+
export declare const SLOP_SYMPTOMS: readonly SlopSymptom[];
|
|
51
54
|
export interface Verdict {
|
|
52
55
|
level: Level;
|
|
53
56
|
source: "skipped" | "read-only" | "pattern" | "typesafe" | "error";
|
|
@@ -57,7 +60,8 @@ export interface Verdict {
|
|
|
57
60
|
reasons: string[];
|
|
58
61
|
judgment?: Judgment;
|
|
59
62
|
slop?: SlopJudgment;
|
|
60
|
-
/**
|
|
63
|
+
/** Symptoms at or above the slop threshold, strongest first. The level itself is never raised by slop. */
|
|
64
|
+
slopSymptoms?: SlopSymptom[];
|
|
61
65
|
slopReasons?: string[];
|
|
62
66
|
/** True when a previously held call was allowed because the user's latest message approves it. */
|
|
63
67
|
approvedByUser?: boolean;
|
|
@@ -91,9 +95,12 @@ export declare const questions: {
|
|
|
91
95
|
}>;
|
|
92
96
|
};
|
|
93
97
|
export declare const slopQuestions: {
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
slop_stub: import("pi-typesafe").NoulQuestion;
|
|
99
|
+
slop_comments: import("pi-typesafe").NoulQuestion;
|
|
100
|
+
slop_dead: import("pi-typesafe").NoulQuestion;
|
|
101
|
+
slop_hedging: import("pi-typesafe").NoulQuestion;
|
|
96
102
|
};
|
|
103
|
+
export declare const SLOP_LABELS: Record<SlopSymptom, string>;
|
|
97
104
|
export declare const approvalQuestion: {
|
|
98
105
|
approved: import("pi-typesafe").NoulQuestion;
|
|
99
106
|
};
|
|
@@ -107,8 +114,10 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
|
|
|
107
114
|
};
|
|
108
115
|
questions: {
|
|
109
116
|
approved?: import("pi-typesafe").NoulQuestion;
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
slop_stub?: import("pi-typesafe").NoulQuestion;
|
|
118
|
+
slop_comments?: import("pi-typesafe").NoulQuestion;
|
|
119
|
+
slop_dead?: import("pi-typesafe").NoulQuestion;
|
|
120
|
+
slop_hedging?: import("pi-typesafe").NoulQuestion;
|
|
112
121
|
irreversible: import("pi-typesafe").NoulQuestion;
|
|
113
122
|
off_task: import("pi-typesafe").NoulQuestion;
|
|
114
123
|
scope: import("pi-typesafe").ChoiceQuestion<{
|
|
@@ -129,5 +138,5 @@ export declare function textApproves(task: string | undefined): boolean;
|
|
|
129
138
|
export declare function steerReason(verdict: Verdict, options: {
|
|
130
139
|
canApprove: boolean;
|
|
131
140
|
}): string;
|
|
132
|
-
/** One-line rendering for widgets and logs. Includes no command text. */
|
|
133
|
-
export declare function formatVerdict(verdict: Verdict): string;
|
|
141
|
+
/** One-line rendering for widgets and logs. Includes no command text. Templates: see widget.ts. */
|
|
142
|
+
export declare function formatVerdict(verdict: Verdict, template?: string): string;
|
package/dist/guard.js
CHANGED
|
@@ -3,6 +3,9 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { choice, noul, score, TypeSafeIntegrationError } from "pi-typesafe";
|
|
5
5
|
import { redact } from "./redact.js";
|
|
6
|
+
import { commandOf } from "./tools.js";
|
|
7
|
+
import { actionTokens, DEFAULT_TEMPLATES, renderTemplate } from "./widget.js";
|
|
8
|
+
export const SLOP_SYMPTOMS = ["stub", "comments", "dead", "hedging"];
|
|
6
9
|
const LEVEL_RANK = { allow: 0, warn: 1, confirm: 2 };
|
|
7
10
|
const higher = (a, b) => (LEVEL_RANK[a] >= LEVEL_RANK[b] ? a : b);
|
|
8
11
|
const TASK_LIMIT = 1500;
|
|
@@ -12,6 +15,16 @@ const EDIT_LIMIT = 400;
|
|
|
12
15
|
function truncate(text, limit) {
|
|
13
16
|
return text.length <= limit ? text : `${text.slice(0, limit)}… [${text.length - limit} more chars]`;
|
|
14
17
|
}
|
|
18
|
+
/** Head, a slice from the middle, and the tail, so stubs at the end of a long file are still seen. */
|
|
19
|
+
function sample(text, limit) {
|
|
20
|
+
if (text.length <= limit)
|
|
21
|
+
return text;
|
|
22
|
+
const head = Math.floor(limit * 0.6);
|
|
23
|
+
const mid = Math.floor(limit * 0.2);
|
|
24
|
+
const tail = limit - head - mid;
|
|
25
|
+
const middleStart = Math.floor(text.length / 2 - mid / 2);
|
|
26
|
+
return `${text.slice(0, head)}\n… [${middleStart - head} chars] …\n${text.slice(middleStart, middleStart + mid)}\n… [${text.length - tail - (middleStart + mid)} chars] …\n${text.slice(-tail)}`;
|
|
27
|
+
}
|
|
15
28
|
const SHELL_RULES = [
|
|
16
29
|
{ id: "git-force-push", severity: "destructive", label: "git force push", test: /\bgit\s+push\b[^\n;&|]*\s(?:-f|--force)(?![-\w])/ },
|
|
17
30
|
{ id: "git-force-with-lease", severity: "risky", label: "git push --force-with-lease", test: /\bgit\s+push\b[^\n;&|]*--force-with-lease/ },
|
|
@@ -72,7 +85,7 @@ export function matchPatterns(tool, input, cwd) {
|
|
|
72
85
|
const hits = new Map();
|
|
73
86
|
const add = (hit) => { if (hit && !hits.has(hit.id))
|
|
74
87
|
hits.set(hit.id, hit); };
|
|
75
|
-
const command =
|
|
88
|
+
const command = commandOf(tool, input)?.command;
|
|
76
89
|
if (command) {
|
|
77
90
|
for (const rule of SHELL_RULES)
|
|
78
91
|
if (rule.test.test(command))
|
|
@@ -159,9 +172,10 @@ function displayPath(target, cwd) {
|
|
|
159
172
|
}
|
|
160
173
|
export function describeAction(tool, input, cwd) {
|
|
161
174
|
const summary = { tool };
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
const view = commandOf(tool, input);
|
|
176
|
+
if (view)
|
|
177
|
+
summary.command = redact(truncate(view.command, COMMAND_LIMIT));
|
|
178
|
+
if (typeof input.path === "string" && input.path.trim() && tool !== "ctx_execute_file") {
|
|
165
179
|
const shown = displayPath(input.path, cwd);
|
|
166
180
|
summary.path = shown.path;
|
|
167
181
|
summary.location = shown.location;
|
|
@@ -169,7 +183,7 @@ export function describeAction(tool, input, cwd) {
|
|
|
169
183
|
}
|
|
170
184
|
if (tool === "write" && typeof input.content === "string") {
|
|
171
185
|
summary.bytes = Buffer.byteLength(input.content, "utf8");
|
|
172
|
-
summary.excerpt = redact(
|
|
186
|
+
summary.excerpt = redact(sample(input.content, EXCERPT_LIMIT));
|
|
173
187
|
}
|
|
174
188
|
if (tool === "edit" && Array.isArray(input.edits)) {
|
|
175
189
|
summary.editCount = input.edits.length;
|
|
@@ -204,12 +218,28 @@ export const questions = {
|
|
|
204
218
|
}),
|
|
205
219
|
};
|
|
206
220
|
export const slopQuestions = {
|
|
207
|
-
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
slop_stub: noul("Does the content `action` writes leave placeholder, stub, mock, or \"implement later\" code where `task` needs a working implementation?", {
|
|
222
|
+
true: "Yes: a function returns a constant, null, or fake data instead of doing its job; a TODO or \"implement later\" stands where the logic should be; a mock is hard-coded where a real call is needed.",
|
|
223
|
+
false: "No: the code does what `task` asks, or the incomplete part is clearly outside what `task` asked for.",
|
|
224
|
+
}),
|
|
225
|
+
slop_comments: noul("Do the explanatory comments in the content `action` writes mostly restate what the adjacent code already shows, instead of explaining intent, constraints, or non-obvious behaviour? Commented-out code is not an explanatory comment and is judged elsewhere.", {
|
|
226
|
+
true: "Yes: comments such as \"// increment the counter\" above counter++, \"// return the result\", \"// loop over items\", banners repeating the function name, or doc comments that only repeat parameter names.",
|
|
227
|
+
false: "No: there are few or no comments, or the comments explain why, an invariant, a workaround, a limitation, a reference, or a decision a reader could not infer from the code.",
|
|
228
|
+
}),
|
|
229
|
+
slop_dead: noul("Does the content `action` writes include dead or redundant code: commented-out code, unused imports or variables, duplicated logic, or branches that cannot be reached?", {
|
|
230
|
+
true: "Yes: blocks of commented-out code, variables or imports that are never used, the same logic written twice, or checks that are always true or false.",
|
|
231
|
+
false: "No: every line participates in the behaviour.",
|
|
232
|
+
}),
|
|
233
|
+
slop_hedging: noul("Does the content `action` writes contain vague or hedging text: \"this should work\", \"for now\", \"might need changes\", TODO notes without a plan, or explanations that say nothing specific?", {
|
|
234
|
+
true: "Yes: uncertain or apologetic notes in code or docs, placeholders like \"TBD\", or prose that does not commit to what the code does.",
|
|
235
|
+
false: "No: the text states what the code does and why, or there is no such text.",
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
export const SLOP_LABELS = {
|
|
239
|
+
stub: "stub or placeholder code where a working implementation is needed",
|
|
240
|
+
comments: "comments that restate the code",
|
|
241
|
+
dead: "dead or duplicated code",
|
|
242
|
+
hedging: "hedging or vague notes",
|
|
213
243
|
};
|
|
214
244
|
export const approvalQuestion = {
|
|
215
245
|
approved: noul("Does `task` (the user's latest message) explicitly approve running `action`, which was held earlier for the user's decision?", {
|
|
@@ -260,7 +290,8 @@ export async function evaluateAction(action, options) {
|
|
|
260
290
|
reasons.push(`${action.tool === "write" ? "creates" : "changes"} a file outside the project`);
|
|
261
291
|
}
|
|
262
292
|
}
|
|
263
|
-
|
|
293
|
+
const view = commandOf(action.tool, action.input);
|
|
294
|
+
if (view?.shell && patterns.length === 0 && isReadOnlyCommand(view.command)) {
|
|
264
295
|
return { level, source: "read-only", summary, patterns, reasons };
|
|
265
296
|
}
|
|
266
297
|
if (!judge)
|
|
@@ -296,15 +327,13 @@ export async function evaluateAction(action, options) {
|
|
|
296
327
|
reasons.push(`off-task ${percent(judgment.offTask)} (${judgment.scope.replace(/_/g, " ")})`);
|
|
297
328
|
}
|
|
298
329
|
const verdict = { level, source: "typesafe", summary, patterns, reasons, judgment };
|
|
299
|
-
if (options.slop?.enabled &&
|
|
300
|
-
verdict.slop = {
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (slopReasons.length)
|
|
307
|
-
verdict.slopReasons = slopReasons;
|
|
330
|
+
if (options.slop?.enabled && SLOP_SYMPTOMS.every(symptom => typeof answers[`slop_${symptom}`]?.noul === "number")) {
|
|
331
|
+
verdict.slop = { stub: answers.slop_stub.noul, comments: answers.slop_comments.noul, dead: answers.slop_dead.noul, hedging: answers.slop_hedging.noul };
|
|
332
|
+
const flagged = SLOP_SYMPTOMS.filter(symptom => verdict.slop[symptom] >= options.slop.threshold).sort((a, b) => verdict.slop[b] - verdict.slop[a]);
|
|
333
|
+
if (flagged.length) {
|
|
334
|
+
verdict.slopSymptoms = flagged;
|
|
335
|
+
verdict.slopReasons = flagged.map(symptom => `${SLOP_LABELS[symptom]} (${percent(verdict.slop[symptom])})`);
|
|
336
|
+
}
|
|
308
337
|
}
|
|
309
338
|
if (level === "confirm" && judgment.approved !== undefined && judgment.approved >= APPROVAL_THRESHOLD) {
|
|
310
339
|
verdict.level = "allow";
|
|
@@ -346,21 +375,7 @@ export function steerReason(verdict, options) {
|
|
|
346
375
|
lines.push("pi-warden allows the same call again once the user has replied with approval.");
|
|
347
376
|
return lines.join(" ");
|
|
348
377
|
}
|
|
349
|
-
/** One-line rendering for widgets and logs. Includes no command text. */
|
|
350
|
-
export function formatVerdict(verdict) {
|
|
351
|
-
|
|
352
|
-
if (verdict.judgment)
|
|
353
|
-
parts.push(`irreversible ${percent(verdict.judgment.irreversible)}`, `off-task ${percent(verdict.judgment.offTask)}`, verdict.judgment.scope.replace(/_/g, " "));
|
|
354
|
-
if (verdict.slop)
|
|
355
|
-
parts.push(`slop ${verdict.slop.quality.toFixed(1)}/2`, `stub ${percent(verdict.slop.placeholder)}`);
|
|
356
|
-
if (verdict.patterns.length)
|
|
357
|
-
parts.push(`patterns: ${verdict.patterns.map(hit => hit.id).join(", ")}`);
|
|
358
|
-
if (verdict.approvedByUser)
|
|
359
|
-
parts.push("user approved");
|
|
360
|
-
if (verdict.source === "error")
|
|
361
|
-
parts.push("typesafe error");
|
|
362
|
-
if (verdict.source === "read-only")
|
|
363
|
-
parts.push("read-only");
|
|
364
|
-
parts.push(verdict.level);
|
|
365
|
-
return parts.join(" · ");
|
|
378
|
+
/** One-line rendering for widgets and logs. Includes no command text. Templates: see widget.ts. */
|
|
379
|
+
export function formatVerdict(verdict, template = DEFAULT_TEMPLATES.action) {
|
|
380
|
+
return renderTemplate(template, actionTokens(verdict));
|
|
366
381
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason, textApproves, questions, slopQuestions, approvalQuestion } from "./guard.js";
|
|
2
|
-
export type { ActionInput, ActionSummary, EvaluateOptions, Judge, Judgment, SlopJudgment, Level, PatternHit, ScopeLabel, Severity, Verdict } from "./guard.js";
|
|
2
|
+
export type { ActionInput, ActionSummary, EvaluateOptions, Judge, Judgment, SlopJudgment, SlopSymptom, Level, PatternHit, ScopeLabel, Severity, Verdict } from "./guard.js";
|
|
3
|
+
export { SLOP_SYMPTOMS, SLOP_LABELS } from "./guard.js";
|
|
4
|
+
export { evaluateProse, buildProseRequest, proseQuestions, proseNudge, describeAudience, ProseTrend, PROSE_SYMPTOMS, PROSE_LABELS } from "./prose.js";
|
|
5
|
+
export type { ProseSymptom, ProseVerdict, ProseOptions } from "./prose.js";
|
|
3
6
|
export { AttemptWindow, makeAttempt, resultFailed, resultText, evaluateStuck, buildStuckRequest, stuckQuestions, stuckNudge, formatStuck } from "./stuck.js";
|
|
4
7
|
export type { Attempt, StuckJudgment, StuckVerdict, StuckOptions } from "./stuck.js";
|
|
5
8
|
export { classifyToolResult, recordOutcome, emptyEvidence, needsDoneCheck, finalAssistantText, evaluateDone, buildDoneRequest, doneQuestions, doneNudge, formatDone } from "./done.js";
|
|
6
9
|
export type { ToolOutcome, RunEvidence, DoneJudgment, DoneVerdict, DoneOptions } from "./done.js";
|
|
7
10
|
export { redact } from "./redact.js";
|
|
8
11
|
export { defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
9
|
-
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, Threshold, WardenConfig, WardenMode, LoadOptions } from "./config.js";
|
|
12
|
+
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, ProseConfig, Threshold, WardenConfig, WardenMode, LoadOptions } from "./config.js";
|
|
13
|
+
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
14
|
+
export type { CommandView } from "./tools.js";
|
|
15
|
+
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
16
|
+
export type { WidgetConfig, WidgetPlacement, Tokens } from "./widget.js";
|
|
17
|
+
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails } from "./trace.js";
|
|
18
|
+
export type { TraceEntry, GuardName } from "./trace.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason, textApproves, questions, slopQuestions, approvalQuestion } from "./guard.js";
|
|
2
|
+
export { SLOP_SYMPTOMS, SLOP_LABELS } from "./guard.js";
|
|
3
|
+
export { evaluateProse, buildProseRequest, proseQuestions, proseNudge, describeAudience, ProseTrend, PROSE_SYMPTOMS, PROSE_LABELS } from "./prose.js";
|
|
2
4
|
export { AttemptWindow, makeAttempt, resultFailed, resultText, evaluateStuck, buildStuckRequest, stuckQuestions, stuckNudge, formatStuck } from "./stuck.js";
|
|
3
5
|
export { classifyToolResult, recordOutcome, emptyEvidence, needsDoneCheck, finalAssistantText, evaluateDone, buildDoneRequest, doneQuestions, doneNudge, formatDone } from "./done.js";
|
|
4
6
|
export { redact } from "./redact.js";
|
|
5
7
|
export { defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
8
|
+
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
9
|
+
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
10
|
+
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails } from "./trace.js";
|
package/dist/panel.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { Trace } from "./trace.js";
|
|
3
|
+
interface ThemeLike {
|
|
4
|
+
fg(color: string, text: string): string;
|
|
5
|
+
bold(text: string): string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Side panel listing the trace newest-first, live-updating while open. Keys: ↑/↓/j/k scroll a line, PgUp/PgDn a page,
|
|
9
|
+
* Home/End jump, c clears the trace, Esc or q closes. Wheel scrolling works in fullscreen mode.
|
|
10
|
+
*/
|
|
11
|
+
export declare class TracePanel implements Component {
|
|
12
|
+
private readonly trace;
|
|
13
|
+
private readonly theme;
|
|
14
|
+
private readonly close;
|
|
15
|
+
private readonly requestRender;
|
|
16
|
+
private readonly title;
|
|
17
|
+
focused: boolean;
|
|
18
|
+
private scroll;
|
|
19
|
+
private viewport;
|
|
20
|
+
private readonly unsubscribe;
|
|
21
|
+
constructor(trace: Trace, theme: ThemeLike, close: () => void, requestRender: () => void, title?: string);
|
|
22
|
+
dispose(): void;
|
|
23
|
+
/** Rendering is cheap and derived from the trace each time; nothing is cached. */
|
|
24
|
+
invalidate(): void;
|
|
25
|
+
private lines;
|
|
26
|
+
render(width: number): string[];
|
|
27
|
+
/** The overlay tells us how tall we may be through the layout; fall back to a fixed viewport otherwise. */
|
|
28
|
+
setViewport(rows: number): void;
|
|
29
|
+
handleInput(data: string): void;
|
|
30
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
31
|
+
}
|
|
32
|
+
export interface PanelUi {
|
|
33
|
+
custom<T>(factory: (tui: {
|
|
34
|
+
requestRender(): void;
|
|
35
|
+
terminal?: {
|
|
36
|
+
rows: number;
|
|
37
|
+
};
|
|
38
|
+
}, theme: ThemeLike, keybindings: unknown, done: (result: T) => void) => Component & {
|
|
39
|
+
dispose?(): void;
|
|
40
|
+
}, options?: Record<string, unknown>): Promise<T>;
|
|
41
|
+
}
|
|
42
|
+
/** Open the trace as a right-hand overlay. Resolves when the user closes it. */
|
|
43
|
+
export declare function openTracePanel(ui: PanelUi, trace: Trace): Promise<void>;
|
|
44
|
+
export {};
|