@bacnh85/pi-plan 0.1.2 → 0.1.4
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 +7 -4
- package/index.ts +142 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Pi extension that adds a lightweight plan mode inspired by Codex and Claude Code:
|
|
4
4
|
|
|
5
5
|
- Toggle plan mode with `/plan` or `Ctrl+Alt+P`.
|
|
6
|
-
- Remembers separate thinking/reasoning levels for planning and normal execution based on the active mode when you change Pi's reasoning level.
|
|
7
|
-
- Keeps planning read-only by
|
|
6
|
+
- Remembers separate thinking/reasoning levels for planning and normal execution across sessions based on the active mode when you change Pi's reasoning level.
|
|
7
|
+
- Keeps planning read-only by limiting active tools to planning-safe tools and blocking destructive shell commands.
|
|
8
8
|
- Provides a `write_plan` tool so the agent writes reviewable Markdown plans into `.agents/plans/` in the current workspace.
|
|
9
9
|
- Provides an `ask_plan_question` tool so the agent can ask selection-style clarifying questions during planning, with an option for free-form user input.
|
|
10
10
|
- Prompts after a plan is written so you can approve execution, approve only, keep planning, or refine with feedback.
|
|
@@ -50,11 +50,14 @@ pi --plan
|
|
|
50
50
|
4. The model calls `write_plan`.
|
|
51
51
|
5. The plan is saved under `.agents/plans/<timestamp>-<title>.md`.
|
|
52
52
|
6. Choose one of the approval options:
|
|
53
|
-
- **
|
|
53
|
+
- **Execute in current session**: exits plan mode, restores tools, applies normal thinking level, shows current context usage in the option label, and sends a follow-up execution prompt.
|
|
54
|
+
- **Execute in new session**: exits plan mode and prepares a fresh session that uses the saved Markdown plan file as the handoff artifact.
|
|
54
55
|
- **Approve only**: exits plan mode without starting work.
|
|
55
56
|
- **Keep planning**: remains in plan mode.
|
|
56
57
|
- **Refine with feedback**: sends your feedback as a follow-up planning prompt.
|
|
57
58
|
|
|
59
|
+
Agents should ask blocking, user-answerable planning questions with `ask_plan_question` before finalizing a plan. Final plans may still list non-blocking uncertainties or implementation-time checks, but should not leave consequential user decisions unresolved.
|
|
60
|
+
|
|
58
61
|
## Reasoning levels
|
|
59
62
|
|
|
60
63
|
pi-plan remembers two reasoning levels:
|
|
@@ -62,7 +65,7 @@ pi-plan remembers two reasoning levels:
|
|
|
62
65
|
- Change Pi's active reasoning level while plan mode is active to update the planning level.
|
|
63
66
|
- Change Pi's active reasoning level in normal or execution mode to update the normal/execution level.
|
|
64
67
|
|
|
65
|
-
The remembered levels are restored when switching modes and across resumed sessions.
|
|
68
|
+
The remembered levels are restored when switching modes and across independent or resumed sessions. pi-plan stores only these non-sensitive preferences under your user Pi agent directory.
|
|
66
69
|
|
|
67
70
|
## Design notes
|
|
68
71
|
|
package/index.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { isToolCallEventType, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
|
|
7
8
|
const STATUS_KEY = "pi-plan";
|
|
8
9
|
const PLAN_DIR = path.join(".agents", "plans");
|
|
9
10
|
const PLAN_TOOL = "write_plan";
|
|
10
11
|
const PLAN_QUESTION_TOOL = "ask_plan_question";
|
|
11
|
-
const
|
|
12
|
+
const PLAN_EXECUTE_COMMAND = "plan-execute";
|
|
13
|
+
const PREFERENCES_FILE = path.join(os.homedir(), ".pi", "agent", "pi-plan", "preferences.json");
|
|
12
14
|
const DEFAULT_PLAN_TOOLS = ["read", "bash", "grep", "find", "ls", PLAN_TOOL, PLAN_QUESTION_TOOL];
|
|
15
|
+
const PLAN_ALLOWED_TOOLS = new Set(DEFAULT_PLAN_TOOLS);
|
|
13
16
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
14
17
|
|
|
15
18
|
type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
@@ -26,6 +29,12 @@ interface PlanState {
|
|
|
26
29
|
lastPlanStatus?: PlanStatus;
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
interface PlanPreferences {
|
|
33
|
+
version: 1;
|
|
34
|
+
planThinking: ThinkingLevel;
|
|
35
|
+
normalThinking: ThinkingLevel;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
interface WritePlanParams {
|
|
30
39
|
title: string;
|
|
31
40
|
content: string;
|
|
@@ -102,6 +111,43 @@ function relativeToCwd(cwd: string, absolutePath: string): string {
|
|
|
102
111
|
return path.relative(cwd, absolutePath).split(path.sep).join("/");
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
function formatContextUsage(ctx: ExtensionContext): string {
|
|
115
|
+
const usage = ctx.getContextUsage();
|
|
116
|
+
if (!usage || usage.percent === null) return "context usage unknown";
|
|
117
|
+
const percent = `${Math.round(usage.percent)}% context used`;
|
|
118
|
+
if (usage.tokens === null) return percent;
|
|
119
|
+
return `${percent} (${Math.round(usage.tokens / 1000)}k / ${Math.round(usage.contextWindow / 1000)}k tokens)`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildExecutionPrompt(relativePlan: string, mode: "current" | "new"): string {
|
|
123
|
+
const prefix = mode === "new" ? "This is a fresh session created from an approved pi-plan. " : "";
|
|
124
|
+
return `${prefix}Execute the approved plan in ${relativePlan}. Read the plan file if needed, keep the implementation scoped to the plan, update the plan if reality differs materially, and run the verification described there.`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasOpenQuestionWarning(content: string): boolean {
|
|
128
|
+
return /(^|\n)#{1,6}\s+.*open questions?.*\n[\s\S]*\?/i.test(content);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadPreferences(): Promise<PlanPreferences | undefined> {
|
|
132
|
+
try {
|
|
133
|
+
const raw = await readFile(PREFERENCES_FILE, "utf8");
|
|
134
|
+
const parsed = JSON.parse(raw) as Partial<PlanPreferences>;
|
|
135
|
+
const savedPlanThinking = parsed.planThinking;
|
|
136
|
+
const savedNormalThinking = parsed.normalThinking;
|
|
137
|
+
if (parsed.version !== 1 || !savedPlanThinking || !savedNormalThinking || !isThinkingLevel(savedPlanThinking) || !isThinkingLevel(savedNormalThinking)) return undefined;
|
|
138
|
+
return { version: 1, planThinking: savedPlanThinking, normalThinking: savedNormalThinking };
|
|
139
|
+
} catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function savePreferences(preferences: PlanPreferences): Promise<void> {
|
|
145
|
+
await mkdir(path.dirname(PREFERENCES_FILE), { recursive: true });
|
|
146
|
+
const temporaryPath = `${PREFERENCES_FILE}.${process.pid}.tmp`;
|
|
147
|
+
await writeFile(temporaryPath, `${JSON.stringify(preferences, null, 2)}\n`, "utf8");
|
|
148
|
+
await rename(temporaryPath, PREFERENCES_FILE);
|
|
149
|
+
}
|
|
150
|
+
|
|
105
151
|
function isDestructiveBash(command: string): boolean {
|
|
106
152
|
return DESTRUCTIVE_BASH_PATTERNS.some((pattern) => pattern.test(command));
|
|
107
153
|
}
|
|
@@ -146,10 +192,14 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
146
192
|
} satisfies PlanState);
|
|
147
193
|
}
|
|
148
194
|
|
|
195
|
+
function persistPreferences(): void {
|
|
196
|
+
void savePreferences({ version: 1, planThinking, normalThinking }).catch(() => undefined);
|
|
197
|
+
}
|
|
198
|
+
|
|
149
199
|
function enablePlanTools(): void {
|
|
150
200
|
const baseline = toolsBeforePlan ?? pi.getActiveTools();
|
|
151
201
|
toolsBeforePlan = baseline;
|
|
152
|
-
pi.setActiveTools(unique([...baseline.filter((tool) =>
|
|
202
|
+
pi.setActiveTools(unique([...baseline.filter((tool) => PLAN_ALLOWED_TOOLS.has(tool)), ...DEFAULT_PLAN_TOOLS]));
|
|
153
203
|
}
|
|
154
204
|
|
|
155
205
|
function restoreTools(): void {
|
|
@@ -175,6 +225,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
175
225
|
}
|
|
176
226
|
setStatus(ctx);
|
|
177
227
|
persistState();
|
|
228
|
+
persistPreferences();
|
|
178
229
|
}
|
|
179
230
|
|
|
180
231
|
function enterPlanMode(ctx: ExtensionContext): void {
|
|
@@ -208,6 +259,50 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
208
259
|
else enterPlanMode(ctx);
|
|
209
260
|
}
|
|
210
261
|
|
|
262
|
+
function beginCurrentSessionExecution(ctx: ExtensionContext, relativePlan: string): void {
|
|
263
|
+
planModeEnabled = false;
|
|
264
|
+
executionMode = true;
|
|
265
|
+
lastPlanStatus = "approved";
|
|
266
|
+
restoreTools();
|
|
267
|
+
applyThinking(normalThinking);
|
|
268
|
+
setStatus(ctx);
|
|
269
|
+
clearPlanWidget(ctx);
|
|
270
|
+
persistState();
|
|
271
|
+
persistPreferences();
|
|
272
|
+
pi.sendUserMessage(buildExecutionPrompt(relativePlan, "current"), { deliverAs: "followUp" });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function beginNewSessionExecution(ctx: ExtensionCommandContext): Promise<void> {
|
|
276
|
+
if (!lastPlanPath) {
|
|
277
|
+
ctx.ui.notify("No approved plan is available to execute.", "error");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await ctx.waitForIdle();
|
|
281
|
+
const planPathToExecute = lastPlanPath;
|
|
282
|
+
const planTitleToExecute = lastPlanTitle;
|
|
283
|
+
const relativePlan = relativeToCwd(ctx.cwd, planPathToExecute);
|
|
284
|
+
const parentSession = ctx.sessionManager.getSessionFile();
|
|
285
|
+
const state: PlanState = {
|
|
286
|
+
enabled: false,
|
|
287
|
+
executing: true,
|
|
288
|
+
planThinking,
|
|
289
|
+
normalThinking,
|
|
290
|
+
lastPlanPath: planPathToExecute,
|
|
291
|
+
lastPlanTitle: planTitleToExecute,
|
|
292
|
+
lastPlanStatus: "approved",
|
|
293
|
+
};
|
|
294
|
+
const result = await ctx.newSession({
|
|
295
|
+
parentSession,
|
|
296
|
+
setup: async (sessionManager) => {
|
|
297
|
+
sessionManager.appendCustomEntry("pi-plan", state);
|
|
298
|
+
},
|
|
299
|
+
withSession: async (replacementCtx) => {
|
|
300
|
+
await replacementCtx.sendUserMessage(buildExecutionPrompt(relativePlan, "new"));
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
if (result.cancelled) ctx.ui.notify("New-session execution cancelled.", "info");
|
|
304
|
+
}
|
|
305
|
+
|
|
211
306
|
pi.registerFlag("plan", {
|
|
212
307
|
description: "Start in pi-plan read-only planning mode",
|
|
213
308
|
type: "boolean",
|
|
@@ -219,7 +314,10 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
219
314
|
label: "Write Plan",
|
|
220
315
|
description: `Write or replace the current implementation plan as Markdown under ${PLAN_DIR}/. Use in plan mode when the plan is ready for user review.`,
|
|
221
316
|
promptSnippet: `Write the implementation plan to ${PLAN_DIR}/ as a Markdown file for user review`,
|
|
222
|
-
promptGuidelines: [
|
|
317
|
+
promptGuidelines: [
|
|
318
|
+
`Use ${PLAN_TOOL} in plan mode after repository exploration; do not use edit/write for implementation until the user approves the plan.`,
|
|
319
|
+
`Do not call ${PLAN_TOOL} while blocking user-answerable questions remain; use ${PLAN_QUESTION_TOOL} first.`,
|
|
320
|
+
],
|
|
223
321
|
parameters: Type.Object({
|
|
224
322
|
title: Type.String({ description: "Short human-readable title for the plan" }),
|
|
225
323
|
content: Type.String({ description: "Complete Markdown plan content" }),
|
|
@@ -237,8 +335,11 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
237
335
|
lastPlanTitle = typedParams.title.trim() || "Plan";
|
|
238
336
|
lastPlanStatus = isPlanStatus(typedParams.status) ? typedParams.status : "draft";
|
|
239
337
|
persistState();
|
|
338
|
+
const warning = hasOpenQuestionWarning(content)
|
|
339
|
+
? ` If the plan contains blocking user-answerable open questions, call ${PLAN_QUESTION_TOOL} before requesting approval.`
|
|
340
|
+
: "";
|
|
240
341
|
return {
|
|
241
|
-
content: [{ type: "text", text: `Plan written to ${relativeToCwd(ctx.cwd, destination)}.
|
|
342
|
+
content: [{ type: "text", text: `Plan written to ${relativeToCwd(ctx.cwd, destination)}. If no blocking user-answerable questions remain, ask the user to approve, refine, execute in current session, execute in a new session, or keep planning.${warning}` }],
|
|
242
343
|
details: { path: destination, title: lastPlanTitle, status: lastPlanStatus },
|
|
243
344
|
};
|
|
244
345
|
},
|
|
@@ -324,6 +425,17 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
324
425
|
handler: async (args, ctx) => handlePlanCommand(args, ctx),
|
|
325
426
|
});
|
|
326
427
|
|
|
428
|
+
pi.registerCommand(PLAN_EXECUTE_COMMAND, {
|
|
429
|
+
description: "Internal pi-plan execution bridge",
|
|
430
|
+
handler: async (args, ctx) => {
|
|
431
|
+
if (args.trim() !== "new") {
|
|
432
|
+
ctx.ui.notify(`Usage: /${PLAN_EXECUTE_COMMAND} new`, "warning");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
await beginNewSessionExecution(ctx);
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
327
439
|
pi.registerShortcut("ctrl+alt+p", {
|
|
328
440
|
description: "Toggle pi-plan mode",
|
|
329
441
|
handler: async (ctx) => {
|
|
@@ -333,24 +445,32 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
333
445
|
});
|
|
334
446
|
|
|
335
447
|
pi.on("session_start", async (_event, ctx) => {
|
|
448
|
+
const preferences = await loadPreferences();
|
|
336
449
|
const entries = ctx.sessionManager.getEntries();
|
|
337
450
|
const saved = entries
|
|
338
451
|
.filter((entry: { type: string; customType?: string }) => entry.type === "custom" && entry.customType === "pi-plan")
|
|
339
452
|
.pop() as { data?: PlanState } | undefined;
|
|
453
|
+
if (preferences) {
|
|
454
|
+
planThinking = preferences.planThinking;
|
|
455
|
+
normalThinking = preferences.normalThinking;
|
|
456
|
+
}
|
|
340
457
|
if (saved?.data) {
|
|
341
458
|
planModeEnabled = saved.data.enabled ?? planModeEnabled;
|
|
342
459
|
executionMode = saved.data.executing ?? executionMode;
|
|
343
|
-
|
|
344
|
-
|
|
460
|
+
if (!preferences && isThinkingLevel(saved.data.planThinking)) planThinking = saved.data.planThinking;
|
|
461
|
+
if (!preferences && isThinkingLevel(saved.data.normalThinking)) normalThinking = saved.data.normalThinking;
|
|
345
462
|
toolsBeforePlan = saved.data.toolsBeforePlan ?? toolsBeforePlan;
|
|
346
463
|
lastPlanPath = saved.data.lastPlanPath ?? lastPlanPath;
|
|
347
464
|
lastPlanTitle = saved.data.lastPlanTitle ?? lastPlanTitle;
|
|
348
465
|
lastPlanStatus = saved.data.lastPlanStatus ?? lastPlanStatus;
|
|
349
466
|
}
|
|
467
|
+
if (!preferences) persistPreferences();
|
|
350
468
|
if (pi.getFlag("plan") === true) planModeEnabled = true;
|
|
351
469
|
if (planModeEnabled) {
|
|
352
470
|
enablePlanTools();
|
|
353
471
|
applyThinking(planThinking);
|
|
472
|
+
} else if (executionMode) {
|
|
473
|
+
applyThinking(normalThinking);
|
|
354
474
|
}
|
|
355
475
|
setStatus(ctx);
|
|
356
476
|
clearPlanWidget(ctx);
|
|
@@ -364,8 +484,8 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
364
484
|
|
|
365
485
|
pi.on("tool_call", async (event) => {
|
|
366
486
|
if (!planModeEnabled) return;
|
|
367
|
-
if (
|
|
368
|
-
return { block: true, reason: `pi-plan: ${event.toolName} is disabled
|
|
487
|
+
if (!PLAN_ALLOWED_TOOLS.has(event.toolName)) {
|
|
488
|
+
return { block: true, reason: `pi-plan: ${event.toolName} is disabled in read-only plan mode. Use ${PLAN_TOOL} to write the plan file.` };
|
|
369
489
|
}
|
|
370
490
|
if (!isToolCallEventType("bash", event)) return;
|
|
371
491
|
if (isDestructiveBash(event.input.command)) {
|
|
@@ -379,7 +499,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
379
499
|
return {
|
|
380
500
|
message: {
|
|
381
501
|
customType: "pi-plan-context",
|
|
382
|
-
content: `[PI-PLAN MODE ACTIVE]\nYou are in read-only planning mode. Research the codebase and produce a reviewable implementation plan before making changes.\n\nRules:\n- Do not edit source files, configs, lockfiles, or git state.\n- You may read files, search, inspect git state, and run read-only shell commands.\n- Ask concise clarifying questions if requirements are ambiguous. Use ${PLAN_QUESTION_TOOL} for consequential open decisions with 2-4 clear options and an Other/user-opinion path.\n- Do not ask about details you can discover from repository evidence. If the user already gave an opinion, incorporate it instead of asking again.\n- When the plan is ready, call ${PLAN_TOOL} with a complete Markdown plan.\n- The plan file must live in ${PLAN_DIR}/. Current/next plan path: ${relativePlan}\n\nPlan content should include:\n1. Goal and assumptions.\n2. Key findings with durable file/symbol paths.\n3. Proposed implementation steps.\n4. Verification plan.\n5. Risks, open questions, and rejected alternatives if relevant.`,
|
|
502
|
+
content: `[PI-PLAN MODE ACTIVE]\nYou are in read-only planning mode. Research the codebase and produce a reviewable implementation plan before making changes.\n\nRules:\n- Do not edit source files, configs, lockfiles, or git state.\n- You may read files, search, inspect git state, and run read-only shell commands.\n- Ask concise clarifying questions if requirements are ambiguous. Use ${PLAN_QUESTION_TOOL} for consequential open decisions with 2-4 clear options and an Other/user-opinion path.\n- Do not ask about details you can discover from repository evidence. If the user already gave an opinion, incorporate it instead of asking again.\n- Before calling ${PLAN_TOOL}, if any consequential, user-answerable decision remains, call ${PLAN_QUESTION_TOOL} and wait for the answer. Do not place blocking user decisions in the final plan as open questions.\n- When the plan is ready, call ${PLAN_TOOL} with a complete Markdown plan.\n- The plan file must live in ${PLAN_DIR}/. Current/next plan path: ${relativePlan}\n\nPlan content should include:\n1. Goal and assumptions.\n2. Key findings with durable file/symbol paths.\n3. Proposed implementation steps.\n4. Verification plan.\n5. Risks, non-blocking open questions, and rejected alternatives if relevant.`,
|
|
383
503
|
display: false,
|
|
384
504
|
},
|
|
385
505
|
};
|
|
@@ -405,22 +525,29 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
|
|
|
405
525
|
pi.on("agent_end", async (_event, ctx) => {
|
|
406
526
|
if (!planModeEnabled || !lastPlanPath || !ctx.hasUI) return;
|
|
407
527
|
const relativePlan = relativeToCwd(ctx.cwd, lastPlanPath);
|
|
528
|
+
const currentSessionChoice = `Execute in current session (${formatContextUsage(ctx)})`;
|
|
529
|
+
const newSessionChoice = "Execute in new session (fresh context)";
|
|
408
530
|
const choice = await ctx.ui.select(`Plan written: ${relativePlan}`, [
|
|
409
|
-
|
|
531
|
+
currentSessionChoice,
|
|
532
|
+
newSessionChoice,
|
|
410
533
|
"Approve only (exit plan mode)",
|
|
411
534
|
"Keep planning",
|
|
412
535
|
"Refine with feedback",
|
|
413
536
|
]);
|
|
414
|
-
if (choice ===
|
|
537
|
+
if (choice === currentSessionChoice) {
|
|
538
|
+
beginCurrentSessionExecution(ctx, relativePlan);
|
|
539
|
+
} else if (choice === newSessionChoice) {
|
|
415
540
|
planModeEnabled = false;
|
|
416
|
-
executionMode =
|
|
541
|
+
executionMode = false;
|
|
417
542
|
lastPlanStatus = "approved";
|
|
418
543
|
restoreTools();
|
|
419
544
|
applyThinking(normalThinking);
|
|
420
545
|
setStatus(ctx);
|
|
421
546
|
clearPlanWidget(ctx);
|
|
422
547
|
persistState();
|
|
423
|
-
|
|
548
|
+
persistPreferences();
|
|
549
|
+
ctx.ui.setEditorText(`/${PLAN_EXECUTE_COMMAND} new`);
|
|
550
|
+
ctx.ui.notify("Press Enter to start execution in a new session.", "info");
|
|
424
551
|
} else if (choice === "Approve only (exit plan mode)") {
|
|
425
552
|
lastPlanStatus = "approved";
|
|
426
553
|
leavePlanMode(ctx);
|