@oh-my-pi/pi-coding-agent 12.19.2 → 13.0.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/CHANGELOG.md +53 -0
- package/package.json +7 -7
- package/src/commit/prompts/analysis-system.md +3 -3
- package/src/commit/prompts/analysis-user.md +14 -14
- package/src/commit/prompts/changelog-system.md +4 -4
- package/src/commit/prompts/changelog-user.md +4 -4
- package/src/commit/prompts/file-observer-system.md +2 -2
- package/src/commit/prompts/file-observer-user.md +2 -2
- package/src/commit/prompts/reduce-system.md +4 -4
- package/src/commit/prompts/reduce-user.md +6 -6
- package/src/commit/prompts/summary-system.md +4 -4
- package/src/commit/prompts/summary-user.md +6 -6
- package/src/config/settings-schema.ts +0 -11
- package/src/discovery/helpers.ts +13 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/index.ts +8 -3
- package/src/internal-urls/local-protocol.ts +223 -0
- package/src/internal-urls/{docs-protocol.ts → pi-protocol.ts} +12 -12
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/ipy/executor.ts +4 -32
- package/src/main.ts +0 -1
- package/src/memories/index.ts +1 -1
- package/src/modes/components/settings-defs.ts +0 -5
- package/src/modes/controllers/event-controller.ts +4 -4
- package/src/modes/interactive-mode.ts +84 -64
- package/src/modes/types.ts +11 -3
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/patch/hashline.ts +42 -42
- package/src/patch/index.ts +24 -21
- package/src/patch/shared.ts +21 -43
- package/src/plan-mode/approved-plan.ts +55 -0
- package/src/prompts/agents/designer.md +6 -6
- package/src/prompts/agents/explore.md +4 -4
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/init.md +10 -10
- package/src/prompts/agents/plan.md +6 -6
- package/src/prompts/agents/reviewer.md +4 -3
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +5 -5
- package/src/prompts/memories/read-path.md +11 -0
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +6 -6
- package/src/prompts/system/plan-mode-active.md +20 -20
- package/src/prompts/system/plan-mode-approved.md +9 -7
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/subagent-submit-reminder.md +5 -5
- package/src/prompts/system/subagent-system-prompt.md +9 -9
- package/src/prompts/system/subagent-user-prompt.md +3 -5
- package/src/prompts/system/summarization-system.md +1 -1
- package/src/prompts/system/system-prompt.md +109 -84
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-interrupt.md +2 -2
- package/src/prompts/system/web-search.md +16 -16
- package/src/prompts/tools/ask.md +6 -6
- package/src/prompts/tools/bash.md +9 -9
- package/src/prompts/tools/browser.md +5 -5
- package/src/prompts/tools/cancel-job.md +2 -2
- package/src/prompts/tools/exit-plan-mode.md +13 -10
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/gemini-image.md +7 -7
- package/src/prompts/tools/grep.md +4 -3
- package/src/prompts/tools/hashline.md +37 -39
- package/src/prompts/tools/patch.md +5 -5
- package/src/prompts/tools/poll-jobs.md +1 -1
- package/src/prompts/tools/python.md +8 -10
- package/src/prompts/tools/read.md +2 -12
- package/src/prompts/tools/replace.md +6 -6
- package/src/prompts/tools/ssh.md +2 -7
- package/src/prompts/tools/task.md +34 -23
- package/src/prompts/tools/todo-write.md +65 -49
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +4 -3
- package/src/sdk.ts +11 -9
- package/src/session/agent-session.ts +92 -51
- package/src/session/artifacts.ts +1 -1
- package/src/session/messages.ts +1 -0
- package/src/task/agents.ts +1 -0
- package/src/task/index.ts +2 -1
- package/src/task/render.ts +2 -2
- package/src/task/types.ts +1 -0
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +3 -2
- package/src/tools/bash.ts +38 -19
- package/src/tools/exit-plan-mode.ts +30 -2
- package/src/tools/grep.ts +131 -75
- package/src/tools/index.ts +13 -3
- package/src/tools/path-utils.ts +2 -1
- package/src/tools/plan-mode-guard.ts +8 -8
- package/src/tools/python.ts +0 -2
- package/src/tools/read.ts +2 -2
- package/src/tools/todo-write.ts +276 -146
- package/src/internal-urls/plan-protocol.ts +0 -95
- package/src/modes/components/todo-display.ts +0 -114
- package/src/prompts/memories/read_path.md +0 -11
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Protocol handler for
|
|
2
|
+
* Protocol handler for pi:// URLs.
|
|
3
3
|
*
|
|
4
4
|
* Serves statically embedded documentation files bundled at build time.
|
|
5
5
|
*
|
|
6
6
|
* URL forms:
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
7
|
+
* - pi:// - Lists all available documentation files
|
|
8
|
+
* - pi://<file>.md - Reads a specific documentation file
|
|
9
9
|
*/
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
|
|
12
12
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Handler for
|
|
15
|
+
* Handler for pi:// URLs.
|
|
16
16
|
*
|
|
17
17
|
* Resolves documentation file names to their content, or lists available docs.
|
|
18
18
|
*/
|
|
19
|
-
export class
|
|
20
|
-
readonly scheme = "
|
|
19
|
+
export class PiProtocolHandler implements ProtocolHandler {
|
|
20
|
+
readonly scheme = "pi";
|
|
21
21
|
|
|
22
22
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
23
23
|
// Extract filename from host + path
|
|
@@ -37,7 +37,7 @@ export class DocsProtocolHandler implements ProtocolHandler {
|
|
|
37
37
|
throw new Error("No documentation files found");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const listing = EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](
|
|
40
|
+
const listing = EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](pi://${f})`).join("\n");
|
|
41
41
|
const content = `# Documentation\n\n${EMBEDDED_DOC_FILENAMES.length} files available:\n\n${listing}\n`;
|
|
42
42
|
|
|
43
43
|
return {
|
|
@@ -45,19 +45,19 @@ export class DocsProtocolHandler implements ProtocolHandler {
|
|
|
45
45
|
content,
|
|
46
46
|
contentType: "text/markdown",
|
|
47
47
|
size: Buffer.byteLength(content, "utf-8"),
|
|
48
|
-
sourcePath: "
|
|
48
|
+
sourcePath: "pi://",
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
async #readDoc(filename: string, url: InternalUrl): Promise<InternalResource> {
|
|
53
53
|
// Validate: no traversal, no absolute paths
|
|
54
54
|
if (path.isAbsolute(filename)) {
|
|
55
|
-
throw new Error("Absolute paths are not allowed in
|
|
55
|
+
throw new Error("Absolute paths are not allowed in pi:// URLs");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const normalized = path.posix.normalize(filename.replaceAll("\\", "/"));
|
|
59
59
|
if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
60
|
-
throw new Error("Path traversal (..) is not allowed in
|
|
60
|
+
throw new Error("Path traversal (..) is not allowed in pi:// URLs");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const content = EMBEDDED_DOCS[normalized];
|
|
@@ -69,7 +69,7 @@ export class DocsProtocolHandler implements ProtocolHandler {
|
|
|
69
69
|
const suffix =
|
|
70
70
|
suggestions.length > 0
|
|
71
71
|
? `\nDid you mean: ${suggestions.join(", ")}`
|
|
72
|
-
: "\nUse
|
|
72
|
+
: "\nUse pi:// to list available files.";
|
|
73
73
|
throw new Error(`Documentation file not found: ${filename}${suffix}`);
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -78,7 +78,7 @@ export class DocsProtocolHandler implements ProtocolHandler {
|
|
|
78
78
|
content,
|
|
79
79
|
contentType: "text/markdown",
|
|
80
80
|
size: Buffer.byteLength(content, "utf-8"),
|
|
81
|
-
sourcePath: `
|
|
81
|
+
sourcePath: `pi://${normalized}`,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal URL router for internal protocols (agent://, artifact://,
|
|
2
|
+
* Internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, pi://, local://).
|
|
3
3
|
*/
|
|
4
4
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the internal URL routing system.
|
|
3
3
|
*
|
|
4
|
-
* Internal URLs (agent://, artifact://,
|
|
4
|
+
* Internal URLs (agent://, artifact://, memory://, skill://, rule://, pi://, local://) are resolved by tools like fetch and read,
|
|
5
5
|
* providing access to agent outputs and skill files without exposing filesystem paths.
|
|
6
6
|
*/
|
|
7
7
|
|
package/src/ipy/executor.ts
CHANGED
|
@@ -39,8 +39,6 @@ export interface PythonExecutorOptions {
|
|
|
39
39
|
useSharedGateway?: boolean;
|
|
40
40
|
/** Session file path for accessing task outputs */
|
|
41
41
|
sessionFile?: string;
|
|
42
|
-
/** Artifacts directory for $ARTIFACTS env var and artifact storage */
|
|
43
|
-
artifactsDir?: string;
|
|
44
42
|
/** Artifact path/id for full output storage */
|
|
45
43
|
artifactPath?: string;
|
|
46
44
|
artifactId?: string;
|
|
@@ -311,16 +309,9 @@ async function createKernelSession(
|
|
|
311
309
|
cwd: string,
|
|
312
310
|
useSharedGateway?: boolean,
|
|
313
311
|
sessionFile?: string,
|
|
314
|
-
artifactsDir?: string,
|
|
315
312
|
isRetry?: boolean,
|
|
316
313
|
): Promise<KernelSession> {
|
|
317
|
-
const env: Record<string, string> | undefined =
|
|
318
|
-
sessionFile || artifactsDir
|
|
319
|
-
? {
|
|
320
|
-
...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
|
|
321
|
-
...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
|
|
322
|
-
}
|
|
323
|
-
: undefined;
|
|
314
|
+
const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
|
|
324
315
|
|
|
325
316
|
let kernel: PythonKernel;
|
|
326
317
|
try {
|
|
@@ -330,7 +321,7 @@ async function createKernelSession(
|
|
|
330
321
|
} catch (err) {
|
|
331
322
|
if (!isRetry && isResourceExhaustionError(err)) {
|
|
332
323
|
await recoverFromResourceExhaustion();
|
|
333
|
-
return createKernelSession(sessionId, cwd, useSharedGateway, sessionFile,
|
|
324
|
+
return createKernelSession(sessionId, cwd, useSharedGateway, sessionFile, true);
|
|
334
325
|
}
|
|
335
326
|
throw err;
|
|
336
327
|
}
|
|
@@ -359,7 +350,6 @@ async function restartKernelSession(
|
|
|
359
350
|
cwd: string,
|
|
360
351
|
useSharedGateway?: boolean,
|
|
361
352
|
sessionFile?: string,
|
|
362
|
-
artifactsDir?: string,
|
|
363
353
|
): Promise<void> {
|
|
364
354
|
session.restartCount += 1;
|
|
365
355
|
if (session.restartCount > 1) {
|
|
@@ -370,13 +360,7 @@ async function restartKernelSession(
|
|
|
370
360
|
} catch (err) {
|
|
371
361
|
logger.warn("Failed to shutdown crashed kernel", { error: err instanceof Error ? err.message : String(err) });
|
|
372
362
|
}
|
|
373
|
-
const env: Record<string, string> | undefined =
|
|
374
|
-
sessionFile || artifactsDir
|
|
375
|
-
? {
|
|
376
|
-
...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
|
|
377
|
-
...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
|
|
378
|
-
}
|
|
379
|
-
: undefined;
|
|
363
|
+
const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
|
|
380
364
|
const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
|
|
381
365
|
session.kernel = kernel;
|
|
382
366
|
session.dead = false;
|
|
@@ -401,7 +385,6 @@ async function withKernelSession<T>(
|
|
|
401
385
|
handler: (kernel: PythonKernel) => Promise<T>,
|
|
402
386
|
useSharedGateway?: boolean,
|
|
403
387
|
sessionFile?: string,
|
|
404
|
-
artifactsDir?: string,
|
|
405
388
|
): Promise<T> {
|
|
406
389
|
let session = kernelSessions.get(sessionId);
|
|
407
390
|
if (!session) {
|
|
@@ -416,7 +399,6 @@ async function withKernelSession<T>(
|
|
|
416
399
|
cwd,
|
|
417
400
|
useSharedGateway,
|
|
418
401
|
sessionFile,
|
|
419
|
-
artifactsDir,
|
|
420
402
|
);
|
|
421
403
|
kernelSessions.set(sessionId, session);
|
|
422
404
|
startCleanupTimer();
|
|
@@ -432,7 +414,6 @@ async function withKernelSession<T>(
|
|
|
432
414
|
cwd,
|
|
433
415
|
useSharedGateway,
|
|
434
416
|
sessionFile,
|
|
435
|
-
artifactsDir,
|
|
436
417
|
);
|
|
437
418
|
}
|
|
438
419
|
try {
|
|
@@ -450,7 +431,6 @@ async function withKernelSession<T>(
|
|
|
450
431
|
cwd,
|
|
451
432
|
useSharedGateway,
|
|
452
433
|
sessionFile,
|
|
453
|
-
artifactsDir,
|
|
454
434
|
);
|
|
455
435
|
const result = await logger.timeAsync("kernel:postRestart:handler", handler, session!.kernel);
|
|
456
436
|
session!.restartCount = 0;
|
|
@@ -539,16 +519,9 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
539
519
|
const kernelMode = options?.kernelMode ?? "session";
|
|
540
520
|
const useSharedGateway = options?.useSharedGateway;
|
|
541
521
|
const sessionFile = options?.sessionFile;
|
|
542
|
-
const artifactsDir = options?.artifactsDir;
|
|
543
522
|
|
|
544
523
|
if (kernelMode === "per-call") {
|
|
545
|
-
const env: Record<string, string> | undefined =
|
|
546
|
-
sessionFile || artifactsDir
|
|
547
|
-
? {
|
|
548
|
-
...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
|
|
549
|
-
...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
|
|
550
|
-
}
|
|
551
|
-
: undefined;
|
|
524
|
+
const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
|
|
552
525
|
const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
|
|
553
526
|
try {
|
|
554
527
|
return await executeWithKernel(kernel, code, options);
|
|
@@ -570,6 +543,5 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
570
543
|
async kernel => executeWithKernel(kernel, code, options),
|
|
571
544
|
useSharedGateway,
|
|
572
545
|
sessionFile,
|
|
573
|
-
artifactsDir,
|
|
574
546
|
);
|
|
575
547
|
}
|
package/src/main.ts
CHANGED
|
@@ -538,7 +538,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
538
538
|
const cwd = getProjectDir();
|
|
539
539
|
await logger.timeAsync("settings:init", () => Settings.init({ cwd }));
|
|
540
540
|
if (parsedArgs.noPty) {
|
|
541
|
-
settings.override("bash.virtualTerminal", "off");
|
|
542
541
|
Bun.env.PI_NO_PTY = "1";
|
|
543
542
|
}
|
|
544
543
|
const {
|
package/src/memories/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { parseModelString } from "../config/model-resolver";
|
|
|
11
11
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
12
12
|
import type { Settings } from "../config/settings";
|
|
13
13
|
import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
|
|
14
|
-
import readPathTemplate from "../prompts/memories/
|
|
14
|
+
import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
|
|
15
15
|
import stageOneInputTemplate from "../prompts/memories/stage_one_input.md" with { type: "text" };
|
|
16
16
|
import stageOneSystemTemplate from "../prompts/memories/stage_one_system.md" with { type: "text" };
|
|
17
17
|
import type { AgentSession } from "../session/agent-session";
|
|
@@ -154,11 +154,6 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
154
154
|
{ value: "tool-only", label: "tool-only", description: "Interrupt only on tool-call argument matches" },
|
|
155
155
|
{ value: "never", label: "never", description: "Never interrupt; inject warning after completion" },
|
|
156
156
|
],
|
|
157
|
-
// Virtual terminal
|
|
158
|
-
"bash.virtualTerminal": [
|
|
159
|
-
{ value: "on", label: "On", description: "PTY-backed interactive execution" },
|
|
160
|
-
{ value: "off", label: "Off", description: "Standard non-interactive execution" },
|
|
161
|
-
],
|
|
162
157
|
// Provider options
|
|
163
158
|
"providers.webSearch": [
|
|
164
159
|
{
|
|
@@ -7,7 +7,7 @@ import { TodoReminderComponent } from "../../modes/components/todo-reminder";
|
|
|
7
7
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
8
8
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
9
9
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
10
|
-
import type { InteractiveModeContext,
|
|
10
|
+
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
11
11
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
12
12
|
import type { ExitPlanModeDetails } from "../../tools";
|
|
13
13
|
|
|
@@ -288,9 +288,9 @@ export class EventController {
|
|
|
288
288
|
}
|
|
289
289
|
// Update todo display when todo_write tool completes
|
|
290
290
|
if (event.toolName === "todo_write" && !event.isError) {
|
|
291
|
-
const details = event.result.details as {
|
|
292
|
-
if (details?.
|
|
293
|
-
this.ctx.setTodos(details.
|
|
291
|
+
const details = event.result.details as { phases?: TodoPhase[] } | undefined;
|
|
292
|
+
if (details?.phases) {
|
|
293
|
+
this.ctx.setTodos(details.phases);
|
|
294
294
|
}
|
|
295
295
|
} else if (event.toolName === "todo_write" && event.isError) {
|
|
296
296
|
const textContent = event.result.content.find(
|
|
@@ -24,7 +24,8 @@ import { type Settings, settings } from "../config/settings";
|
|
|
24
24
|
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
|
|
25
25
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
26
26
|
import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
|
|
27
|
-
import {
|
|
27
|
+
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
28
|
+
import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
|
|
28
29
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
29
30
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
30
31
|
import { HistoryStorage } from "../session/history-storage";
|
|
@@ -54,10 +55,9 @@ import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
|
54
55
|
import { setMermaidRenderCallback } from "./theme/mermaid-cache";
|
|
55
56
|
import type { Theme } from "./theme/theme";
|
|
56
57
|
import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
|
|
57
|
-
import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem } from "./types";
|
|
58
|
+
import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem, TodoPhase } from "./types";
|
|
58
59
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
59
60
|
|
|
60
|
-
const TODO_FILE_NAME = "todos.json";
|
|
61
61
|
const EDITOR_MAX_HEIGHT_MIN = 6;
|
|
62
62
|
const EDITOR_MAX_HEIGHT_MAX = 18;
|
|
63
63
|
const EDITOR_RESERVED_ROWS = 12;
|
|
@@ -102,7 +102,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
102
102
|
planModeEnabled = false;
|
|
103
103
|
planModePaused = false;
|
|
104
104
|
planModePlanFilePath: string | undefined = undefined;
|
|
105
|
-
|
|
105
|
+
todoPhases: TodoPhase[] = [];
|
|
106
106
|
hideThinkingBlock = false;
|
|
107
107
|
pendingImages: ImageContent[] = [];
|
|
108
108
|
compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
|
@@ -436,92 +436,82 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
436
436
|
|
|
437
437
|
#formatTodoLine(todo: TodoItem, prefix: string): string {
|
|
438
438
|
const checkbox = theme.checkbox;
|
|
439
|
-
const label = todo.content;
|
|
440
439
|
switch (todo.status) {
|
|
441
440
|
case "completed":
|
|
442
441
|
return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(todo.content)}`);
|
|
443
442
|
case "in_progress":
|
|
444
|
-
return theme.fg("accent", `${prefix}${checkbox.unchecked} ${
|
|
443
|
+
return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`);
|
|
444
|
+
case "abandoned":
|
|
445
|
+
return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`);
|
|
445
446
|
default:
|
|
446
|
-
return theme.fg("dim", `${prefix}${checkbox.unchecked} ${
|
|
447
|
+
return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`);
|
|
447
448
|
}
|
|
448
449
|
}
|
|
449
450
|
|
|
450
|
-
#
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return todos.slice(startIndex, startIndex + 5);
|
|
451
|
+
#getActivePhase(phases: TodoPhase[]): TodoPhase | undefined {
|
|
452
|
+
const nonEmpty = phases.filter(phase => phase.tasks.length > 0);
|
|
453
|
+
const active = nonEmpty.find(phase =>
|
|
454
|
+
phase.tasks.some(task => task.status === "pending" || task.status === "in_progress"),
|
|
455
|
+
);
|
|
456
|
+
return active ?? nonEmpty[nonEmpty.length - 1];
|
|
459
457
|
}
|
|
460
458
|
|
|
461
459
|
#renderTodoList(): void {
|
|
462
460
|
this.todoContainer.clear();
|
|
463
|
-
|
|
461
|
+
const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
|
|
462
|
+
if (phases.length === 0) {
|
|
464
463
|
return;
|
|
465
464
|
}
|
|
466
465
|
|
|
467
|
-
const visibleTodos = this.todoExpanded ? this.todoItems : this.#getCollapsedTodos(this.todoItems);
|
|
468
466
|
const indent = " ";
|
|
469
467
|
const hook = theme.tree.hook;
|
|
470
468
|
const lines = [indent + theme.bold(theme.fg("accent", "Todos"))];
|
|
471
469
|
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
470
|
+
if (!this.todoExpanded) {
|
|
471
|
+
const activePhase = this.#getActivePhase(phases);
|
|
472
|
+
if (!activePhase) return;
|
|
473
|
+
lines.push(`${indent}${theme.fg("accent", `${hook} ${activePhase.name}`)}`);
|
|
474
|
+
const visibleTasks = activePhase.tasks.slice(0, 5);
|
|
475
|
+
visibleTasks.forEach((todo, index) => {
|
|
476
|
+
const prefix = `${indent}${index === 0 ? hook : " "} `;
|
|
477
|
+
lines.push(this.#formatTodoLine(todo, prefix));
|
|
478
|
+
});
|
|
479
|
+
if (visibleTasks.length < activePhase.tasks.length) {
|
|
480
|
+
const remaining = activePhase.tasks.length - visibleTasks.length;
|
|
481
|
+
lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more (Ctrl+T to expand)`));
|
|
482
|
+
}
|
|
483
|
+
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
476
486
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
487
|
+
for (const phase of phases) {
|
|
488
|
+
lines.push(`${indent}${theme.fg("accent", `${hook} ${phase.name}`)}`);
|
|
489
|
+
phase.tasks.forEach((todo, index) => {
|
|
490
|
+
const prefix = `${indent}${index === 0 ? hook : " "} `;
|
|
491
|
+
lines.push(this.#formatTodoLine(todo, prefix));
|
|
492
|
+
});
|
|
480
493
|
}
|
|
481
494
|
|
|
482
495
|
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
483
496
|
}
|
|
484
497
|
|
|
485
498
|
async #loadTodoList(): Promise<void> {
|
|
486
|
-
|
|
487
|
-
if (!sessionFile) {
|
|
488
|
-
this.todoItems = [];
|
|
489
|
-
this.#renderTodoList();
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
const artifactsDir = sessionFile.slice(0, -6);
|
|
493
|
-
const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
|
|
494
|
-
try {
|
|
495
|
-
const data = (await Bun.file(todoPath).json()) as { todos?: TodoItem[] };
|
|
496
|
-
if (data?.todos && Array.isArray(data.todos)) {
|
|
497
|
-
this.todoItems = data.todos;
|
|
498
|
-
} else {
|
|
499
|
-
this.todoItems = [];
|
|
500
|
-
}
|
|
501
|
-
} catch (error) {
|
|
502
|
-
if (isEnoent(error)) {
|
|
503
|
-
this.todoItems = [];
|
|
504
|
-
this.#renderTodoList();
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
logger.warn("Failed to load todos", { path: todoPath, error: String(error) });
|
|
508
|
-
}
|
|
499
|
+
this.todoPhases = this.session.getTodoPhases();
|
|
509
500
|
this.#renderTodoList();
|
|
510
501
|
}
|
|
511
502
|
|
|
512
|
-
#getPlanFilePath(): string {
|
|
513
|
-
|
|
514
|
-
return `plan://${sessionId}/plan.md`;
|
|
503
|
+
async #getPlanFilePath(): Promise<string> {
|
|
504
|
+
return "local://PLAN.md";
|
|
515
505
|
}
|
|
516
506
|
|
|
517
507
|
#resolvePlanFilePath(planFilePath: string): string {
|
|
518
|
-
if (planFilePath.startsWith("
|
|
519
|
-
return
|
|
520
|
-
|
|
521
|
-
|
|
508
|
+
if (planFilePath.startsWith("local://")) {
|
|
509
|
+
return resolveLocalUrlToPath(planFilePath, {
|
|
510
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
511
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
522
512
|
});
|
|
523
513
|
}
|
|
524
|
-
return planFilePath;
|
|
514
|
+
return path.resolve(this.sessionManager.getCwd(), planFilePath);
|
|
525
515
|
}
|
|
526
516
|
|
|
527
517
|
#updatePlanModeStatus(): void {
|
|
@@ -592,7 +582,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
592
582
|
|
|
593
583
|
this.planModePaused = false;
|
|
594
584
|
|
|
595
|
-
const planFilePath = options?.planFilePath ?? this.#getPlanFilePath();
|
|
585
|
+
const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
|
|
596
586
|
const previousTools = this.session.getActiveToolNames();
|
|
597
587
|
const hasExitTool = this.session.getToolByName("exit_plan_mode") !== undefined;
|
|
598
588
|
const planTools = hasExitTool ? [...previousTools, "exit_plan_mode"] : previousTools;
|
|
@@ -672,16 +662,29 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
672
662
|
this.ui.requestRender();
|
|
673
663
|
}
|
|
674
664
|
|
|
675
|
-
async #approvePlan(
|
|
665
|
+
async #approvePlan(
|
|
666
|
+
planContent: string,
|
|
667
|
+
options: { planFilePath: string; finalPlanFilePath: string },
|
|
668
|
+
): Promise<void> {
|
|
669
|
+
await renameApprovedPlanFile({
|
|
670
|
+
planFilePath: options.planFilePath,
|
|
671
|
+
finalPlanFilePath: options.finalPlanFilePath,
|
|
672
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
673
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
674
|
+
});
|
|
676
675
|
const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
|
|
677
676
|
await this.#exitPlanMode({ silent: true, paused: false });
|
|
678
677
|
await this.handleClearCommand();
|
|
679
678
|
if (previousTools.length > 0) {
|
|
680
679
|
await this.session.setActiveToolsByName(previousTools);
|
|
681
680
|
}
|
|
681
|
+
this.session.setPlanReferencePath(options.finalPlanFilePath);
|
|
682
682
|
this.session.markPlanReferenceSent();
|
|
683
|
-
const prompt = renderPromptTemplate(planModeApprovedPrompt, {
|
|
684
|
-
|
|
683
|
+
const prompt = renderPromptTemplate(planModeApprovedPrompt, {
|
|
684
|
+
planContent,
|
|
685
|
+
finalPlanFilePath: options.finalPlanFilePath,
|
|
686
|
+
});
|
|
687
|
+
await this.session.prompt(prompt, { synthetic: true });
|
|
685
688
|
}
|
|
686
689
|
|
|
687
690
|
async handlePlanModeCommand(): Promise<void> {
|
|
@@ -703,7 +706,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
703
706
|
return;
|
|
704
707
|
}
|
|
705
708
|
|
|
706
|
-
const planFilePath = details.planFilePath || this.planModePlanFilePath || this.#getPlanFilePath();
|
|
709
|
+
const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
|
|
707
710
|
this.planModePlanFilePath = planFilePath;
|
|
708
711
|
const planContent = await this.#readPlanFile(planFilePath);
|
|
709
712
|
if (!planContent) {
|
|
@@ -719,7 +722,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
719
722
|
]);
|
|
720
723
|
|
|
721
724
|
if (choice === "Approve and execute") {
|
|
722
|
-
|
|
725
|
+
const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
|
|
726
|
+
try {
|
|
727
|
+
await this.#approvePlan(planContent, { planFilePath, finalPlanFilePath });
|
|
728
|
+
} catch (error) {
|
|
729
|
+
this.showError(
|
|
730
|
+
`Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
723
733
|
return;
|
|
724
734
|
}
|
|
725
735
|
if (choice === "Refine plan") {
|
|
@@ -1170,8 +1180,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1170
1180
|
this.ui.requestRender();
|
|
1171
1181
|
}
|
|
1172
1182
|
|
|
1173
|
-
setTodos(todos: TodoItem[]): void {
|
|
1174
|
-
|
|
1183
|
+
setTodos(todos: TodoItem[] | TodoPhase[]): void {
|
|
1184
|
+
if (todos.length > 0 && "tasks" in todos[0]) {
|
|
1185
|
+
this.todoPhases = todos as TodoPhase[];
|
|
1186
|
+
} else {
|
|
1187
|
+
this.todoPhases = [
|
|
1188
|
+
{
|
|
1189
|
+
id: "default",
|
|
1190
|
+
name: "Todos",
|
|
1191
|
+
tasks: todos as TodoItem[],
|
|
1192
|
+
},
|
|
1193
|
+
];
|
|
1194
|
+
}
|
|
1175
1195
|
this.#renderTodoList();
|
|
1176
1196
|
this.ui.requestRender();
|
|
1177
1197
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -26,10 +26,18 @@ export type CompactionQueuedMessage = {
|
|
|
26
26
|
mode: "steer" | "followUp";
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
|
|
30
|
+
|
|
29
31
|
export type TodoItem = {
|
|
30
32
|
id: string;
|
|
31
33
|
content: string;
|
|
32
|
-
status:
|
|
34
|
+
status: TodoStatus;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type TodoPhase = {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
tasks: TodoItem[];
|
|
33
41
|
};
|
|
34
42
|
|
|
35
43
|
export interface InteractiveModeContext {
|
|
@@ -89,7 +97,7 @@ export interface InteractiveModeContext {
|
|
|
89
97
|
lastStatusText: Text | undefined;
|
|
90
98
|
fileSlashCommands: Set<string>;
|
|
91
99
|
skillCommands: Map<string, string>;
|
|
92
|
-
|
|
100
|
+
todoPhases: TodoPhase[];
|
|
93
101
|
|
|
94
102
|
// Lifecycle
|
|
95
103
|
init(): Promise<void>;
|
|
@@ -130,7 +138,7 @@ export interface InteractiveModeContext {
|
|
|
130
138
|
updateEditorTopBorder(): void;
|
|
131
139
|
updateEditorBorderColor(): void;
|
|
132
140
|
rebuildChatFromMessages(): void;
|
|
133
|
-
setTodos(todos: TodoItem[]): void;
|
|
141
|
+
setTodos(todos: TodoItem[] | TodoPhase[]): void;
|
|
134
142
|
reloadTodos(): Promise<void>;
|
|
135
143
|
toggleTodoExpansion(): void;
|
|
136
144
|
|
|
@@ -171,12 +171,14 @@ export class UiHelpers {
|
|
|
171
171
|
}
|
|
172
172
|
break;
|
|
173
173
|
}
|
|
174
|
-
case "user":
|
|
174
|
+
case "user":
|
|
175
|
+
case "developer": {
|
|
175
176
|
const textContent = this.ctx.getUserMessageText(message);
|
|
176
177
|
if (textContent) {
|
|
177
|
-
const
|
|
178
|
+
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
179
|
+
const userComponent = new UserMessageComponent(textContent, isSynthetic);
|
|
178
180
|
this.ctx.chatContainer.addChild(userComponent);
|
|
179
|
-
if (options?.populateHistory &&
|
|
181
|
+
if (options?.populateHistory && message.role === "user" && !isSynthetic) {
|
|
180
182
|
this.ctx.editor.addToHistory(textContent);
|
|
181
183
|
}
|
|
182
184
|
}
|