@astralyn/pi 0.82.7 → 0.83.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +34 -5099
  2. package/README.md +12 -111
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +9 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/credential-print.d.ts +23 -0
  7. package/dist/cli/credential-print.d.ts.map +1 -0
  8. package/dist/cli/credential-print.js +116 -0
  9. package/dist/cli/credential-print.js.map +1 -0
  10. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  11. package/dist/core/agent-session-runtime.js +3 -0
  12. package/dist/core/agent-session-runtime.js.map +1 -1
  13. package/dist/core/agent-session.d.ts +1 -1
  14. package/dist/core/agent-session.d.ts.map +1 -1
  15. package/dist/core/agent-session.js +13 -6
  16. package/dist/core/agent-session.js.map +1 -1
  17. package/dist/core/extensions/runner.d.ts +1 -0
  18. package/dist/core/extensions/runner.d.ts.map +1 -1
  19. package/dist/core/extensions/runner.js +7 -0
  20. package/dist/core/extensions/runner.js.map +1 -1
  21. package/dist/core/extensions/types.d.ts +7 -0
  22. package/dist/core/extensions/types.d.ts.map +1 -1
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/core/footer-data-provider.d.ts +10 -0
  25. package/dist/core/footer-data-provider.d.ts.map +1 -1
  26. package/dist/core/footer-data-provider.js +1 -1
  27. package/dist/core/footer-data-provider.js.map +1 -1
  28. package/dist/core/model-runtime.d.ts +2 -0
  29. package/dist/core/model-runtime.d.ts.map +1 -1
  30. package/dist/core/model-runtime.js.map +1 -1
  31. package/dist/core/package-manager.d.ts.map +1 -1
  32. package/dist/core/package-manager.js +13 -6
  33. package/dist/core/package-manager.js.map +1 -1
  34. package/dist/core/resource-loader.d.ts +15 -0
  35. package/dist/core/resource-loader.d.ts.map +1 -1
  36. package/dist/core/resource-loader.js +70 -9
  37. package/dist/core/resource-loader.js.map +1 -1
  38. package/dist/core/tools/find.d.ts.map +1 -1
  39. package/dist/core/tools/find.js +34 -4
  40. package/dist/core/tools/find.js.map +1 -1
  41. package/dist/core/trust-manager.d.ts.map +1 -1
  42. package/dist/core/trust-manager.js +48 -9
  43. package/dist/core/trust-manager.js.map +1 -1
  44. package/dist/extensions/index.d.ts.map +1 -1
  45. package/dist/extensions/index.js +0 -2
  46. package/dist/extensions/index.js.map +1 -1
  47. package/dist/extensions/llama/provider.d.ts.map +1 -1
  48. package/dist/extensions/llama/provider.js +1 -1
  49. package/dist/extensions/llama/provider.js.map +1 -1
  50. package/dist/main.d.ts.map +1 -1
  51. package/dist/main.js +43 -0
  52. package/dist/main.js.map +1 -1
  53. package/dist/modes/interactive/components/extension-selector.d.ts +1 -1
  54. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  56. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/model-selector.js +4 -1
  58. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  59. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  60. package/dist/modes/interactive/external-editor.js +40 -3
  61. package/dist/modes/interactive/external-editor.js.map +1 -1
  62. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  63. package/dist/modes/interactive/interactive-mode.js +20 -6
  64. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  65. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  66. package/dist/modes/rpc/rpc-mode.js +13 -0
  67. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  68. package/dist/utils/paths.d.ts +2 -3
  69. package/dist/utils/paths.d.ts.map +1 -1
  70. package/dist/utils/paths.js +2 -3
  71. package/dist/utils/paths.js.map +1 -1
  72. package/docs/bundled/README.md +13 -16
  73. package/docs/bundled/themes.md +1 -1
  74. package/docs/compaction.md +25 -30
  75. package/docs/custom-provider.md +298 -583
  76. package/docs/docs.json +0 -8
  77. package/docs/extensions.md +112 -116
  78. package/docs/index.md +18 -74
  79. package/docs/json.md +20 -14
  80. package/docs/models.md +193 -151
  81. package/docs/providers.md +2 -1
  82. package/docs/quickstart.md +3 -3
  83. package/docs/rpc.md +165 -123
  84. package/docs/sdk.md +119 -178
  85. package/docs/security.md +1 -1
  86. package/docs/session-format.md +139 -119
  87. package/docs/skills.md +0 -1
  88. package/docs/themes.md +2 -2
  89. package/docs/tui.md +411 -549
  90. package/docs/usage.md +3 -11
  91. package/examples/README.md +6 -1
  92. package/examples/extensions/README.md +13 -33
  93. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  94. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  95. package/examples/extensions/truncated-tool.ts +2 -2
  96. package/examples/sdk/12-full-control.ts +2 -0
  97. package/npm-shrinkwrap.json +21 -21
  98. package/package.json +5 -5
  99. package/dist/extensions/plan/constants.d.ts +0 -68
  100. package/dist/extensions/plan/constants.d.ts.map +0 -1
  101. package/dist/extensions/plan/constants.js +0 -100
  102. package/dist/extensions/plan/constants.js.map +0 -1
  103. package/dist/extensions/plan/index.d.ts +0 -30
  104. package/dist/extensions/plan/index.d.ts.map +0 -1
  105. package/dist/extensions/plan/index.js +0 -429
  106. package/dist/extensions/plan/index.js.map +0 -1
  107. package/dist/extensions/plan/schema.d.ts +0 -20
  108. package/dist/extensions/plan/schema.d.ts.map +0 -1
  109. package/dist/extensions/plan/schema.js +0 -13
  110. package/dist/extensions/plan/schema.js.map +0 -1
  111. package/dist/extensions/plan/state.d.ts +0 -38
  112. package/dist/extensions/plan/state.d.ts.map +0 -1
  113. package/dist/extensions/plan/state.js +0 -96
  114. package/dist/extensions/plan/state.js.map +0 -1
  115. package/dist/extensions/plan/storage.d.ts +0 -26
  116. package/dist/extensions/plan/storage.d.ts.map +0 -1
  117. package/dist/extensions/plan/storage.js +0 -83
  118. package/dist/extensions/plan/storage.js.map +0 -1
  119. package/dist/extensions/plan/view.d.ts +0 -44
  120. package/dist/extensions/plan/view.d.ts.map +0 -1
  121. package/dist/extensions/plan/view.js +0 -149
  122. package/dist/extensions/plan/view.js.map +0 -1
  123. package/docs/bundled/extensions/README.md +0 -19
  124. package/docs/bundled/extensions/plan.md +0 -34
  125. package/examples/extensions/plan-mode/README.md +0 -66
  126. package/examples/extensions/plan-mode/index.ts +0 -390
  127. package/examples/extensions/plan-mode/utils.ts +0 -168
@@ -1 +0,0 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/state.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,gBAAgB;IAChC,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6FAA6F;IAC7F,eAAe,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,EAAE,gBAK9B,CAAC;AAKF,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,YAAY,IAAI,gBAAgB,CAE/C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAE7D;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAOzE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAGrE;AAqBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACzC,cAAc,EAAE;QAAE,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CACnD,GAAG,gBAAgB,GAAG,SAAS,CAU/B","sourcesContent":["/**\n * plan/state.ts — per-session plan-mode state, persisted as custom entries.\n *\n * State is conversation-backed like the todo extension: every transition is\n * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle\n * handlers replay the current branch tail → head to restore. Custom entries are\n * copied on fork/clone, so `planFiles` keeps pointing at the right plan files\n * even though the forked session gets a new session id.\n *\n * State is keyed per session id: one Pi process can host several sessions over\n * its lifetime (resume, /tree switches), so callers re-point the active bucket\n * wherever ctx is in hand.\n */\nimport { PLAN_ENTRY_TYPE } from \"./constants.ts\";\n\nexport interface PlanSessionState {\n\t/** True from entry until the exit is fully applied (including a pending compaction). */\n\tplanning: boolean;\n\t/**\n\t * Full active-tool set snapshotted on entry; restored verbatim on exit.\n\t * Empty for legacy entries (pre-snapshot `removedTools` shape) — replay\n\t * re-derives a fresh snapshot for active planning, and an inactive legacy\n\t * entry needs no restore beyond dropping exit_plan.\n\t */\n\ttoolSnapshot: string[];\n\t/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */\n\tawaitingCompact: boolean;\n\t/** Absolute paths of plan files saved on this conversation branch. */\n\tplanFiles: string[];\n}\n\nexport const EMPTY_PLAN_STATE: PlanSessionState = {\n\tplanning: false,\n\ttoolSnapshot: [],\n\tawaitingCompact: false,\n\tplanFiles: [],\n};\n\nconst states = new Map<string, PlanSessionState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActivePlanSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getPlanState(): PlanSessionState {\n\treturn states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);\n}\n\nexport function replacePlanState(next: PlanSessionState): void {\n\tstates.set(activeSid, clonePlanState(next));\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposePlanSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function clonePlanState(source: PlanSessionState): PlanSessionState {\n\treturn {\n\t\tplanning: source.planning,\n\t\ttoolSnapshot: [...source.toolSnapshot],\n\t\tawaitingCompact: source.awaitingCompact,\n\t\tplanFiles: [...source.planFiles],\n\t};\n}\n\n/**\n * Accepts both the current shape (`toolSnapshot`) and the legacy one\n * (`removedTools`), so branches recorded before the snapshot change replay.\n */\nexport function isPlanState(value: unknown): value is PlanSessionState {\n\tconst parsed = parsePlanState(value);\n\treturn parsed !== undefined;\n}\n\nfunction parsePlanState(value: unknown): PlanSessionState | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (typeof record.planning !== \"boolean\" || typeof record.awaitingCompact !== \"boolean\") return undefined;\n\tif (!Array.isArray(record.planFiles)) return undefined;\n\tconst snapshot = Array.isArray(record.toolSnapshot)\n\t\t? record.toolSnapshot\n\t\t: Array.isArray(record.removedTools)\n\t\t\t? [] // Legacy entries recorded removals, not a snapshot; nothing to restore from.\n\t\t\t: undefined;\n\tif (!snapshot) return undefined;\n\treturn {\n\t\tplanning: record.planning,\n\t\ttoolSnapshot: snapshot.filter((tool): tool is string => typeof tool === \"string\"),\n\t\tawaitingCompact: record.awaitingCompact,\n\t\tplanFiles: record.planFiles.filter((path): path is string => typeof path === \"string\"),\n\t};\n}\n\n/**\n * Latest plan-mode entry on the current branch, or undefined when the branch\n * has none (lets callers distinguish \"never used\" from \"explicitly exited\").\n */\nexport function replayPlanFromBranch(ctx: {\n\tsessionManager: { getBranch(): Iterable<unknown> };\n}): PlanSessionState | undefined {\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as { type?: string; customType?: string; data?: unknown };\n\t\tif (item.type !== \"custom\" || item.customType !== PLAN_ENTRY_TYPE) continue;\n\t\tconst parsed = parsePlanState(item.data);\n\t\tif (!parsed) continue;\n\t\treturn parsed;\n\t}\n\treturn undefined;\n}\n"]}
@@ -1,96 +0,0 @@
1
- /**
2
- * plan/state.ts — per-session plan-mode state, persisted as custom entries.
3
- *
4
- * State is conversation-backed like the todo extension: every transition is
5
- * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle
6
- * handlers replay the current branch tail → head to restore. Custom entries are
7
- * copied on fork/clone, so `planFiles` keeps pointing at the right plan files
8
- * even though the forked session gets a new session id.
9
- *
10
- * State is keyed per session id: one Pi process can host several sessions over
11
- * its lifetime (resume, /tree switches), so callers re-point the active bucket
12
- * wherever ctx is in hand.
13
- */
14
- import { PLAN_ENTRY_TYPE } from "./constants.js";
15
- export const EMPTY_PLAN_STATE = {
16
- planning: false,
17
- toolSnapshot: [],
18
- awaitingCompact: false,
19
- planFiles: [],
20
- };
21
- const states = new Map();
22
- let activeSid = "";
23
- /** Point the module at a session's state bucket. Call wherever ctx is in hand. */
24
- export function setActivePlanSession(sid) {
25
- activeSid = sid;
26
- }
27
- export function getPlanState() {
28
- return states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);
29
- }
30
- export function replacePlanState(next) {
31
- states.set(activeSid, clonePlanState(next));
32
- }
33
- /** Drop in-memory state for a session (call from session_shutdown). */
34
- export function disposePlanSession(sid) {
35
- if (!sid)
36
- return;
37
- states.delete(sid);
38
- if (activeSid === sid)
39
- activeSid = "";
40
- }
41
- export function clonePlanState(source) {
42
- return {
43
- planning: source.planning,
44
- toolSnapshot: [...source.toolSnapshot],
45
- awaitingCompact: source.awaitingCompact,
46
- planFiles: [...source.planFiles],
47
- };
48
- }
49
- /**
50
- * Accepts both the current shape (`toolSnapshot`) and the legacy one
51
- * (`removedTools`), so branches recorded before the snapshot change replay.
52
- */
53
- export function isPlanState(value) {
54
- const parsed = parsePlanState(value);
55
- return parsed !== undefined;
56
- }
57
- function parsePlanState(value) {
58
- if (!value || typeof value !== "object")
59
- return undefined;
60
- const record = value;
61
- if (typeof record.planning !== "boolean" || typeof record.awaitingCompact !== "boolean")
62
- return undefined;
63
- if (!Array.isArray(record.planFiles))
64
- return undefined;
65
- const snapshot = Array.isArray(record.toolSnapshot)
66
- ? record.toolSnapshot
67
- : Array.isArray(record.removedTools)
68
- ? [] // Legacy entries recorded removals, not a snapshot; nothing to restore from.
69
- : undefined;
70
- if (!snapshot)
71
- return undefined;
72
- return {
73
- planning: record.planning,
74
- toolSnapshot: snapshot.filter((tool) => typeof tool === "string"),
75
- awaitingCompact: record.awaitingCompact,
76
- planFiles: record.planFiles.filter((path) => typeof path === "string"),
77
- };
78
- }
79
- /**
80
- * Latest plan-mode entry on the current branch, or undefined when the branch
81
- * has none (lets callers distinguish "never used" from "explicitly exited").
82
- */
83
- export function replayPlanFromBranch(ctx) {
84
- const branch = Array.from(ctx.sessionManager.getBranch());
85
- for (let i = branch.length - 1; i >= 0; i--) {
86
- const item = branch[i];
87
- if (item.type !== "custom" || item.customType !== PLAN_ENTRY_TYPE)
88
- continue;
89
- const parsed = parsePlanState(item.data);
90
- if (!parsed)
91
- continue;
92
- return parsed;
93
- }
94
- return undefined;
95
- }
96
- //# sourceMappingURL=state.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/extensions/plan/state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan/state.ts — per-session plan-mode state, persisted as custom entries.\n *\n * State is conversation-backed like the todo extension: every transition is\n * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle\n * handlers replay the current branch tail → head to restore. Custom entries are\n * copied on fork/clone, so `planFiles` keeps pointing at the right plan files\n * even though the forked session gets a new session id.\n *\n * State is keyed per session id: one Pi process can host several sessions over\n * its lifetime (resume, /tree switches), so callers re-point the active bucket\n * wherever ctx is in hand.\n */\nimport { PLAN_ENTRY_TYPE } from \"./constants.ts\";\n\nexport interface PlanSessionState {\n\t/** True from entry until the exit is fully applied (including a pending compaction). */\n\tplanning: boolean;\n\t/**\n\t * Full active-tool set snapshotted on entry; restored verbatim on exit.\n\t * Empty for legacy entries (pre-snapshot `removedTools` shape) — replay\n\t * re-derives a fresh snapshot for active planning, and an inactive legacy\n\t * entry needs no restore beyond dropping exit_plan.\n\t */\n\ttoolSnapshot: string[];\n\t/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */\n\tawaitingCompact: boolean;\n\t/** Absolute paths of plan files saved on this conversation branch. */\n\tplanFiles: string[];\n}\n\nexport const EMPTY_PLAN_STATE: PlanSessionState = {\n\tplanning: false,\n\ttoolSnapshot: [],\n\tawaitingCompact: false,\n\tplanFiles: [],\n};\n\nconst states = new Map<string, PlanSessionState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActivePlanSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getPlanState(): PlanSessionState {\n\treturn states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);\n}\n\nexport function replacePlanState(next: PlanSessionState): void {\n\tstates.set(activeSid, clonePlanState(next));\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposePlanSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function clonePlanState(source: PlanSessionState): PlanSessionState {\n\treturn {\n\t\tplanning: source.planning,\n\t\ttoolSnapshot: [...source.toolSnapshot],\n\t\tawaitingCompact: source.awaitingCompact,\n\t\tplanFiles: [...source.planFiles],\n\t};\n}\n\n/**\n * Accepts both the current shape (`toolSnapshot`) and the legacy one\n * (`removedTools`), so branches recorded before the snapshot change replay.\n */\nexport function isPlanState(value: unknown): value is PlanSessionState {\n\tconst parsed = parsePlanState(value);\n\treturn parsed !== undefined;\n}\n\nfunction parsePlanState(value: unknown): PlanSessionState | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (typeof record.planning !== \"boolean\" || typeof record.awaitingCompact !== \"boolean\") return undefined;\n\tif (!Array.isArray(record.planFiles)) return undefined;\n\tconst snapshot = Array.isArray(record.toolSnapshot)\n\t\t? record.toolSnapshot\n\t\t: Array.isArray(record.removedTools)\n\t\t\t? [] // Legacy entries recorded removals, not a snapshot; nothing to restore from.\n\t\t\t: undefined;\n\tif (!snapshot) return undefined;\n\treturn {\n\t\tplanning: record.planning,\n\t\ttoolSnapshot: snapshot.filter((tool): tool is string => typeof tool === \"string\"),\n\t\tawaitingCompact: record.awaitingCompact,\n\t\tplanFiles: record.planFiles.filter((path): path is string => typeof path === \"string\"),\n\t};\n}\n\n/**\n * Latest plan-mode entry on the current branch, or undefined when the branch\n * has none (lets callers distinguish \"never used\" from \"explicitly exited\").\n */\nexport function replayPlanFromBranch(ctx: {\n\tsessionManager: { getBranch(): Iterable<unknown> };\n}): PlanSessionState | undefined {\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as { type?: string; customType?: string; data?: unknown };\n\t\tif (item.type !== \"custom\" || item.customType !== PLAN_ENTRY_TYPE) continue;\n\t\tconst parsed = parsePlanState(item.data);\n\t\tif (!parsed) continue;\n\t\treturn parsed;\n\t}\n\treturn undefined;\n}\n"]}
@@ -1,26 +0,0 @@
1
- export interface SavePlanOptions {
2
- sessionId: string;
3
- title: string;
4
- plan: string;
5
- cwd: string;
6
- revises?: string;
7
- }
8
- /** Per-project plans directory for a cwd (same encoding as sessions). */
9
- export declare function getPlansDir(cwd: string): string;
10
- /** File-name slug from a plan title: lowercase, ascii-ish, bounded. */
11
- export declare function slugifyTitle(title: string): string;
12
- /** Local-time `YYYYMMDD-HHmm` prefix: sortable and collision-poor per project. */
13
- export declare function formatPlanFileStamp(date: Date): string;
14
- export declare function buildPlanDocument(options: SavePlanOptions & {
15
- createdIso: string;
16
- }): string;
17
- /** First non-existing `<base>.md`, `<base>-2.md`, ... path in dir. */
18
- export declare function resolvePlanFileName(dir: string, base: string): string;
19
- /**
20
- * Absolute paths of plans saved for this project, newest first (the
21
- * `YYYYMMDD-HHmm` prefix makes file names sort chronologically).
22
- */
23
- export declare function listProjectPlanFiles(cwd: string): string[];
24
- /** Write the plan to disk and return its absolute path. */
25
- export declare function savePlanFile(options: SavePlanOptions): string;
26
- //# sourceMappingURL=storage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/storage.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAGtD;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAW3F;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAY1D;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAQ7D","sourcesContent":["/**\n * plan/storage.ts — plan file layout and writing.\n *\n * Plans live under `<agentDir>/plans/<safeCwdDir>/<YYYYMMDD-HHmm>-<slug>.md`,\n * grouped by project the same way sessions are (cwdToSafeDirName). The\n * directory is only the initial storage location — the authoritative index is\n * the `planFiles` list in the session's plan-mode entries, which follows\n * fork/clone. Plans saved before the per-project layout stay in their legacy\n * `plans/<sessionId>/` directories and remain readable via those entries.\n */\nimport { existsSync, mkdirSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { cwdToSafeDirName, resolvePath } from \"../../utils/paths.ts\";\n\nexport interface SavePlanOptions {\n\tsessionId: string;\n\ttitle: string;\n\tplan: string;\n\tcwd: string;\n\trevises?: string;\n}\n\n/** Per-project plans directory for a cwd (same encoding as sessions). */\nexport function getPlansDir(cwd: string): string {\n\treturn join(getAgentDir(), \"plans\", cwdToSafeDirName(resolvePath(cwd)));\n}\n\n/** File-name slug from a plan title: lowercase, ascii-ish, bounded. */\nexport function slugifyTitle(title: string): string {\n\tconst slug = title\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 40)\n\t\t.replace(/-+$/, \"\");\n\treturn slug || \"plan\";\n}\n\n/** Local-time `YYYYMMDD-HHmm` prefix: sortable and collision-poor per project. */\nexport function formatPlanFileStamp(date: Date): string {\n\tconst pad = (value: number) => String(value).padStart(2, \"0\");\n\treturn `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;\n}\n\nexport function buildPlanDocument(options: SavePlanOptions & { createdIso: string }): string {\n\tconst lines = [\n\t\t\"---\",\n\t\t`title: ${JSON.stringify(options.title)}`,\n\t\t`created: ${options.createdIso}`,\n\t\t`cwd: ${JSON.stringify(options.cwd)}`,\n\t\t`session: ${options.sessionId}`,\n\t];\n\tif (options.revises) lines.push(`revises: ${JSON.stringify(options.revises)}`);\n\tlines.push(\"---\", \"\", options.plan.trimEnd(), \"\");\n\treturn lines.join(\"\\n\");\n}\n\n/** First non-existing `<base>.md`, `<base>-2.md`, ... path in dir. */\nexport function resolvePlanFileName(dir: string, base: string): string {\n\tlet candidate = `${base}.md`;\n\tfor (let attempt = 2; existsSync(join(dir, candidate)); attempt++) {\n\t\tcandidate = `${base}-${attempt}.md`;\n\t}\n\treturn candidate;\n}\n\n/**\n * Absolute paths of plans saved for this project, newest first (the\n * `YYYYMMDD-HHmm` prefix makes file names sort chronologically).\n */\nexport function listProjectPlanFiles(cwd: string): string[] {\n\tconst dir = getPlansDir(cwd);\n\tlet names: string[];\n\ttry {\n\t\tnames = readdirSync(dir);\n\t} catch {\n\t\treturn [];\n\t}\n\treturn names\n\t\t.filter((name) => name.endsWith(\".md\"))\n\t\t.sort((left, right) => right.localeCompare(left))\n\t\t.map((name) => join(dir, name));\n}\n\n/** Write the plan to disk and return its absolute path. */\nexport function savePlanFile(options: SavePlanOptions): string {\n\tconst dir = getPlansDir(options.cwd);\n\tmkdirSync(dir, { recursive: true });\n\tconst now = new Date();\n\tconst fileName = resolvePlanFileName(dir, `${formatPlanFileStamp(now)}-${slugifyTitle(options.title)}`);\n\tconst path = join(dir, fileName);\n\twriteFileSync(path, buildPlanDocument({ ...options, createdIso: now.toISOString() }), \"utf-8\");\n\treturn path;\n}\n"]}
@@ -1,83 +0,0 @@
1
- /**
2
- * plan/storage.ts — plan file layout and writing.
3
- *
4
- * Plans live under `<agentDir>/plans/<safeCwdDir>/<YYYYMMDD-HHmm>-<slug>.md`,
5
- * grouped by project the same way sessions are (cwdToSafeDirName). The
6
- * directory is only the initial storage location — the authoritative index is
7
- * the `planFiles` list in the session's plan-mode entries, which follows
8
- * fork/clone. Plans saved before the per-project layout stay in their legacy
9
- * `plans/<sessionId>/` directories and remain readable via those entries.
10
- */
11
- import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
12
- import { join } from "node:path";
13
- import { getAgentDir } from "../../config.js";
14
- import { cwdToSafeDirName, resolvePath } from "../../utils/paths.js";
15
- /** Per-project plans directory for a cwd (same encoding as sessions). */
16
- export function getPlansDir(cwd) {
17
- return join(getAgentDir(), "plans", cwdToSafeDirName(resolvePath(cwd)));
18
- }
19
- /** File-name slug from a plan title: lowercase, ascii-ish, bounded. */
20
- export function slugifyTitle(title) {
21
- const slug = title
22
- .toLowerCase()
23
- .replace(/[^a-z0-9]+/g, "-")
24
- .replace(/^-+|-+$/g, "")
25
- .slice(0, 40)
26
- .replace(/-+$/, "");
27
- return slug || "plan";
28
- }
29
- /** Local-time `YYYYMMDD-HHmm` prefix: sortable and collision-poor per project. */
30
- export function formatPlanFileStamp(date) {
31
- const pad = (value) => String(value).padStart(2, "0");
32
- return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
33
- }
34
- export function buildPlanDocument(options) {
35
- const lines = [
36
- "---",
37
- `title: ${JSON.stringify(options.title)}`,
38
- `created: ${options.createdIso}`,
39
- `cwd: ${JSON.stringify(options.cwd)}`,
40
- `session: ${options.sessionId}`,
41
- ];
42
- if (options.revises)
43
- lines.push(`revises: ${JSON.stringify(options.revises)}`);
44
- lines.push("---", "", options.plan.trimEnd(), "");
45
- return lines.join("\n");
46
- }
47
- /** First non-existing `<base>.md`, `<base>-2.md`, ... path in dir. */
48
- export function resolvePlanFileName(dir, base) {
49
- let candidate = `${base}.md`;
50
- for (let attempt = 2; existsSync(join(dir, candidate)); attempt++) {
51
- candidate = `${base}-${attempt}.md`;
52
- }
53
- return candidate;
54
- }
55
- /**
56
- * Absolute paths of plans saved for this project, newest first (the
57
- * `YYYYMMDD-HHmm` prefix makes file names sort chronologically).
58
- */
59
- export function listProjectPlanFiles(cwd) {
60
- const dir = getPlansDir(cwd);
61
- let names;
62
- try {
63
- names = readdirSync(dir);
64
- }
65
- catch {
66
- return [];
67
- }
68
- return names
69
- .filter((name) => name.endsWith(".md"))
70
- .sort((left, right) => right.localeCompare(left))
71
- .map((name) => join(dir, name));
72
- }
73
- /** Write the plan to disk and return its absolute path. */
74
- export function savePlanFile(options) {
75
- const dir = getPlansDir(options.cwd);
76
- mkdirSync(dir, { recursive: true });
77
- const now = new Date();
78
- const fileName = resolvePlanFileName(dir, `${formatPlanFileStamp(now)}-${slugifyTitle(options.title)}`);
79
- const path = join(dir, fileName);
80
- writeFileSync(path, buildPlanDocument({ ...options, createdIso: now.toISOString() }), "utf-8");
81
- return path;
82
- }
83
- //# sourceMappingURL=storage.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../../src/extensions/plan/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan/storage.ts — plan file layout and writing.\n *\n * Plans live under `<agentDir>/plans/<safeCwdDir>/<YYYYMMDD-HHmm>-<slug>.md`,\n * grouped by project the same way sessions are (cwdToSafeDirName). The\n * directory is only the initial storage location — the authoritative index is\n * the `planFiles` list in the session's plan-mode entries, which follows\n * fork/clone. Plans saved before the per-project layout stay in their legacy\n * `plans/<sessionId>/` directories and remain readable via those entries.\n */\nimport { existsSync, mkdirSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { cwdToSafeDirName, resolvePath } from \"../../utils/paths.ts\";\n\nexport interface SavePlanOptions {\n\tsessionId: string;\n\ttitle: string;\n\tplan: string;\n\tcwd: string;\n\trevises?: string;\n}\n\n/** Per-project plans directory for a cwd (same encoding as sessions). */\nexport function getPlansDir(cwd: string): string {\n\treturn join(getAgentDir(), \"plans\", cwdToSafeDirName(resolvePath(cwd)));\n}\n\n/** File-name slug from a plan title: lowercase, ascii-ish, bounded. */\nexport function slugifyTitle(title: string): string {\n\tconst slug = title\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 40)\n\t\t.replace(/-+$/, \"\");\n\treturn slug || \"plan\";\n}\n\n/** Local-time `YYYYMMDD-HHmm` prefix: sortable and collision-poor per project. */\nexport function formatPlanFileStamp(date: Date): string {\n\tconst pad = (value: number) => String(value).padStart(2, \"0\");\n\treturn `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;\n}\n\nexport function buildPlanDocument(options: SavePlanOptions & { createdIso: string }): string {\n\tconst lines = [\n\t\t\"---\",\n\t\t`title: ${JSON.stringify(options.title)}`,\n\t\t`created: ${options.createdIso}`,\n\t\t`cwd: ${JSON.stringify(options.cwd)}`,\n\t\t`session: ${options.sessionId}`,\n\t];\n\tif (options.revises) lines.push(`revises: ${JSON.stringify(options.revises)}`);\n\tlines.push(\"---\", \"\", options.plan.trimEnd(), \"\");\n\treturn lines.join(\"\\n\");\n}\n\n/** First non-existing `<base>.md`, `<base>-2.md`, ... path in dir. */\nexport function resolvePlanFileName(dir: string, base: string): string {\n\tlet candidate = `${base}.md`;\n\tfor (let attempt = 2; existsSync(join(dir, candidate)); attempt++) {\n\t\tcandidate = `${base}-${attempt}.md`;\n\t}\n\treturn candidate;\n}\n\n/**\n * Absolute paths of plans saved for this project, newest first (the\n * `YYYYMMDD-HHmm` prefix makes file names sort chronologically).\n */\nexport function listProjectPlanFiles(cwd: string): string[] {\n\tconst dir = getPlansDir(cwd);\n\tlet names: string[];\n\ttry {\n\t\tnames = readdirSync(dir);\n\t} catch {\n\t\treturn [];\n\t}\n\treturn names\n\t\t.filter((name) => name.endsWith(\".md\"))\n\t\t.sort((left, right) => right.localeCompare(left))\n\t\t.map((name) => join(dir, name));\n}\n\n/** Write the plan to disk and return its absolute path. */\nexport function savePlanFile(options: SavePlanOptions): string {\n\tconst dir = getPlansDir(options.cwd);\n\tmkdirSync(dir, { recursive: true });\n\tconst now = new Date();\n\tconst fileName = resolvePlanFileName(dir, `${formatPlanFileStamp(now)}-${slugifyTitle(options.title)}`);\n\tconst path = join(dir, fileName);\n\twriteFileSync(path, buildPlanDocument({ ...options, createdIso: now.toISOString() }), \"utf-8\");\n\treturn path;\n}\n"]}
@@ -1,44 +0,0 @@
1
- /**
2
- * plan/view.ts — approval-dialog context and exit_plan tool rendering.
3
- *
4
- * `exit_plan` carries the whole plan as a parameter, and the default renderer
5
- * echoes parameters verbatim: the row would repeat, escaped onto one line, the
6
- * markdown the model just printed as response text. Collapsed rows therefore
7
- * carry the title only, and the plan body is available on expand.
8
- *
9
- * The result text is written for the model (it restates tool-state precedence
10
- * rules), so the result row renders from `details` instead.
11
- */
12
- import { type Component } from "@earendil-works/pi-tui";
13
- import type { AgentToolResult, ExtensionContext } from "../../core/extensions/types.ts";
14
- import type { CustomMessage } from "../../core/messages.ts";
15
- import { type Theme } from "../../modes/interactive/theme/theme.ts";
16
- import type { ExitPlanDetails, PlanKickoffDetails } from "./schema.ts";
17
- /** Collapse the home prefix so a plan path stays on one line. */
18
- export declare function shortenPlanPath(path: string): string;
19
- export declare function formatApprovalSubtitle(ctx: ExtensionContext): string | undefined;
20
- /**
21
- * The kickoff message body repeats the whole plan for the LLM, so the default
22
- * renderer would flood the chat with markdown the user already approved.
23
- * Collapsed: a two-line card (title + saved path with the expand hint).
24
- * Expanded: the complete kickoff markdown, styled like the exit_plan body.
25
- */
26
- export declare function renderPlanKickoffMessage(message: CustomMessage<PlanKickoffDetails>, options: {
27
- expanded: boolean;
28
- outputPad: number;
29
- }, theme: Theme): Component;
30
- type ExitPlanCallArgs = {
31
- title?: unknown;
32
- plan?: unknown;
33
- revises?: unknown;
34
- };
35
- /** Title-only headline when collapsed; Markdown plan body and revision target on expand. */
36
- export declare function renderExitPlanCall(args: ExitPlanCallArgs | undefined, theme: Theme, expanded: boolean): Component;
37
- /**
38
- * What the user needs to know: where the plan landed and what happens next.
39
- * Falls back to the model-facing text when details are missing — a thrown
40
- * approval (interrupt) has no details to render from.
41
- */
42
- export declare function formatExitPlanResult(result: AgentToolResult<ExitPlanDetails>, theme: Theme): string;
43
- export {};
44
- //# sourceMappingURL=view.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,SAAS,EAAqC,MAAM,wBAAwB,CAAC;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,OAAO,EAAoB,KAAK,KAAK,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAoB,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEzF,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOpD;AAcD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAKhF;AAID;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAC1C,OAAO,EAAE;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EACjD,KAAK,EAAE,KAAK,GACV,SAAS,CAsCX;AAKD,KAAK,gBAAgB,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAM/E,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,GAAG,SAAS,CAsBjH;AAsBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,CAgBnG","sourcesContent":["/**\n * plan/view.ts — approval-dialog context and exit_plan tool rendering.\n *\n * `exit_plan` carries the whole plan as a parameter, and the default renderer\n * echoes parameters verbatim: the row would repeat, escaped onto one line, the\n * markdown the model just printed as response text. Collapsed rows therefore\n * carry the title only, and the plan body is available on expand.\n *\n * The result text is written for the model (it restates tool-state precedence\n * rules), so the result row renders from `details` instead.\n */\nimport { type Component, Container, Markdown, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { AgentToolResult, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport type { CustomMessage } from \"../../core/messages.ts\";\nimport { keyText } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { getMarkdownTheme, type Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ExitPlanDecision, ExitPlanDetails, PlanKickoffDetails } from \"./schema.ts\";\n\n/** Collapse the home prefix so a plan path stays on one line. */\nexport function shortenPlanPath(path: string): string {\n\tconst rawHome = process.env.USERPROFILE || process.env.HOME || \"\";\n\tconst home = rawHome.replace(/[\\\\/]+$/, \"\");\n\tif (!home) return path;\n\tconst normalize = (value: string) => value.replace(/\\\\/g, \"/\").toLowerCase();\n\tif (!normalize(path).startsWith(`${normalize(home)}/`)) return path;\n\treturn `~${path.slice(home.length).replace(/\\\\/g, \"/\")}`;\n}\n\n/**\n * Context pressure is the fact the compact-or-not decision turns on, so the\n * dialog states it instead of making the user leave and check the footer.\n */\nfunction formatTokenCount(tokenCount: number): string {\n\tif (tokenCount < 1000) return `${tokenCount}`;\n\tif (tokenCount < 10_000) return `${(tokenCount / 1000).toFixed(1)}k`;\n\tif (tokenCount < 1_000_000) return `${Math.round(tokenCount / 1000)}k`;\n\tif (tokenCount < 10_000_000) return `${(tokenCount / 1_000_000).toFixed(1)}M`;\n\treturn `${Math.round(tokenCount / 1_000_000)}M`;\n}\n\nexport function formatApprovalSubtitle(ctx: ExtensionContext): string | undefined {\n\tconst usage = ctx.getContextUsage();\n\tif (usage?.percent == null) return undefined;\n\tconst window = usage.contextWindow > 0 ? ` of ${formatTokenCount(usage.contextWindow)}` : \"\";\n\treturn `Context ${usage.percent.toFixed(1)}%${window}`;\n}\n\n// ---- kickoff message rendering ----------------------------------------------\n\n/**\n * The kickoff message body repeats the whole plan for the LLM, so the default\n * renderer would flood the chat with markdown the user already approved.\n * Collapsed: a two-line card (title + saved path with the expand hint).\n * Expanded: the complete kickoff markdown, styled like the exit_plan body.\n */\nexport function renderPlanKickoffMessage(\n\tmessage: CustomMessage<PlanKickoffDetails>,\n\toptions: { expanded: boolean; outputPad: number },\n\ttheme: Theme,\n): Component {\n\tconst details = (message.details ?? {}) as Partial<PlanKickoffDetails>;\n\tconst title = typeof details.title === \"string\" && details.title.trim() ? details.title.trim() : \"plan\";\n\tconst planPath = typeof details.planPath === \"string\" ? details.planPath.trim() : \"\";\n\n\tconst container = new Container();\n\tcontainer.addChild(\n\t\tnew Text(\n\t\t\t[theme.fg(\"toolTitle\", theme.bold(\"plan\")), theme.fg(\"text\", `Executing \"${title}\"`)].join(\" \"),\n\t\t\toptions.outputPad,\n\t\t\t0,\n\t\t),\n\t);\n\tconst pathText = planPath ? `${shortenPlanPath(planPath)} ` : \"\";\n\tconst secondLine = options.expanded\n\t\t? planPath\n\t\t\t? theme.fg(\"dim\", shortenPlanPath(planPath))\n\t\t\t: undefined\n\t\t: theme.fg(\"dim\", `${pathText}(${keyText(\"app.tools.expand\")} to expand)`);\n\tif (secondLine) container.addChild(new Text(secondLine, options.outputPad, 0));\n\tif (!options.expanded) return container;\n\n\tconst body =\n\t\ttypeof message.content === \"string\"\n\t\t\t? message.content\n\t\t\t: message.content\n\t\t\t\t\t.filter((block): block is { type: \"text\"; text: string } => block.type === \"text\")\n\t\t\t\t\t.map((block) => block.text)\n\t\t\t\t\t.join(\"\\n\");\n\tif (body.trim()) {\n\t\tcontainer.addChild(new Spacer(1));\n\t\tcontainer.addChild(\n\t\t\tnew Markdown(body, options.outputPad, 0, getMarkdownTheme(), {\n\t\t\t\tcolor: (text) => theme.fg(\"toolOutput\", text),\n\t\t\t}),\n\t\t);\n\t}\n\treturn container;\n}\n\n// ---- tool call rendering ----------------------------------------------------\n// Args arrive off the wire and stream in, so every field is read defensively.\n\ntype ExitPlanCallArgs = { title?: unknown; plan?: unknown; revises?: unknown };\n\nfunction callText(value: unknown): string {\n\treturn typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Title-only headline when collapsed; Markdown plan body and revision target on expand. */\nexport function renderExitPlanCall(args: ExitPlanCallArgs | undefined, theme: Theme, expanded: boolean): Component {\n\tconst title = callText(args?.title);\n\tconst headline = [\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"exit_plan\")),\n\t\ttitle ? theme.fg(\"text\", title) : theme.fg(\"dim\", \"…\"),\n\t].join(\" \");\n\tif (!expanded) return new Text(headline, 0, 0);\n\n\tconst container = new Container();\n\tcontainer.addChild(new Text(headline, 0, 0));\n\tconst revises = callText(args?.revises);\n\tif (revises) container.addChild(new Text(theme.fg(\"dim\", `revises ${revises}`), 0, 0));\n\tconst plan = callText(args?.plan);\n\tif (plan) {\n\t\tcontainer.addChild(new Spacer(1));\n\t\tcontainer.addChild(\n\t\t\tnew Markdown(plan, 0, 0, getMarkdownTheme(), {\n\t\t\t\tcolor: (text) => theme.fg(\"toolOutput\", text),\n\t\t\t}),\n\t\t);\n\t}\n\treturn container;\n}\n\n// ---- tool result rendering --------------------------------------------------\n\nconst DECISION_SUMMARY: Record<ExitPlanDecision, string> = {\n\texecute: \"Executing with full context\",\n\tcompactAndExecute: \"Compacting context, then executing\",\n\tkeepPlanning: \"Still planning\",\n\tcancelled: \"No approval was pending\",\n};\n\nfunction isExitPlanDecision(value: unknown): value is ExitPlanDecision {\n\treturn value === \"execute\" || value === \"compactAndExecute\" || value === \"keepPlanning\" || value === \"cancelled\";\n}\n\nfunction resultText(result: AgentToolResult<ExitPlanDetails>): string {\n\treturn result.content\n\t\t.filter((block): block is { type: \"text\"; text: string } => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\n/**\n * What the user needs to know: where the plan landed and what happens next.\n * Falls back to the model-facing text when details are missing — a thrown\n * approval (interrupt) has no details to render from.\n */\nexport function formatExitPlanResult(result: AgentToolResult<ExitPlanDetails>, theme: Theme): string {\n\tconst details = result.details as unknown;\n\tif (!details || typeof details !== \"object\" || Array.isArray(details)) {\n\t\treturn theme.fg(\"dim\", resultText(result));\n\t}\n\tconst decision = (details as Record<string, unknown>).decision;\n\tif (!isExitPlanDecision(decision)) return theme.fg(\"dim\", resultText(result));\n\tconst rawPlanPath = (details as Record<string, unknown>).planPath;\n\n\tconst lines: string[] = [];\n\tif (typeof rawPlanPath === \"string\" && rawPlanPath) {\n\t\tlines.push(theme.fg(\"dim\", `Saved to ${shortenPlanPath(rawPlanPath)}`));\n\t}\n\tconst decisionColor = decision === \"cancelled\" ? \"warning\" : \"text\";\n\tlines.push(theme.fg(decisionColor, DECISION_SUMMARY[decision]));\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -1,149 +0,0 @@
1
- /**
2
- * plan/view.ts — approval-dialog context and exit_plan tool rendering.
3
- *
4
- * `exit_plan` carries the whole plan as a parameter, and the default renderer
5
- * echoes parameters verbatim: the row would repeat, escaped onto one line, the
6
- * markdown the model just printed as response text. Collapsed rows therefore
7
- * carry the title only, and the plan body is available on expand.
8
- *
9
- * The result text is written for the model (it restates tool-state precedence
10
- * rules), so the result row renders from `details` instead.
11
- */
12
- import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
13
- import { keyText } from "../../modes/interactive/components/keybinding-hints.js";
14
- import { getMarkdownTheme } from "../../modes/interactive/theme/theme.js";
15
- /** Collapse the home prefix so a plan path stays on one line. */
16
- export function shortenPlanPath(path) {
17
- const rawHome = process.env.USERPROFILE || process.env.HOME || "";
18
- const home = rawHome.replace(/[\\/]+$/, "");
19
- if (!home)
20
- return path;
21
- const normalize = (value) => value.replace(/\\/g, "/").toLowerCase();
22
- if (!normalize(path).startsWith(`${normalize(home)}/`))
23
- return path;
24
- return `~${path.slice(home.length).replace(/\\/g, "/")}`;
25
- }
26
- /**
27
- * Context pressure is the fact the compact-or-not decision turns on, so the
28
- * dialog states it instead of making the user leave and check the footer.
29
- */
30
- function formatTokenCount(tokenCount) {
31
- if (tokenCount < 1000)
32
- return `${tokenCount}`;
33
- if (tokenCount < 10_000)
34
- return `${(tokenCount / 1000).toFixed(1)}k`;
35
- if (tokenCount < 1_000_000)
36
- return `${Math.round(tokenCount / 1000)}k`;
37
- if (tokenCount < 10_000_000)
38
- return `${(tokenCount / 1_000_000).toFixed(1)}M`;
39
- return `${Math.round(tokenCount / 1_000_000)}M`;
40
- }
41
- export function formatApprovalSubtitle(ctx) {
42
- const usage = ctx.getContextUsage();
43
- if (usage?.percent == null)
44
- return undefined;
45
- const window = usage.contextWindow > 0 ? ` of ${formatTokenCount(usage.contextWindow)}` : "";
46
- return `Context ${usage.percent.toFixed(1)}%${window}`;
47
- }
48
- // ---- kickoff message rendering ----------------------------------------------
49
- /**
50
- * The kickoff message body repeats the whole plan for the LLM, so the default
51
- * renderer would flood the chat with markdown the user already approved.
52
- * Collapsed: a two-line card (title + saved path with the expand hint).
53
- * Expanded: the complete kickoff markdown, styled like the exit_plan body.
54
- */
55
- export function renderPlanKickoffMessage(message, options, theme) {
56
- const details = (message.details ?? {});
57
- const title = typeof details.title === "string" && details.title.trim() ? details.title.trim() : "plan";
58
- const planPath = typeof details.planPath === "string" ? details.planPath.trim() : "";
59
- const container = new Container();
60
- container.addChild(new Text([theme.fg("toolTitle", theme.bold("plan")), theme.fg("text", `Executing "${title}"`)].join(" "), options.outputPad, 0));
61
- const pathText = planPath ? `${shortenPlanPath(planPath)} ` : "";
62
- const secondLine = options.expanded
63
- ? planPath
64
- ? theme.fg("dim", shortenPlanPath(planPath))
65
- : undefined
66
- : theme.fg("dim", `${pathText}(${keyText("app.tools.expand")} to expand)`);
67
- if (secondLine)
68
- container.addChild(new Text(secondLine, options.outputPad, 0));
69
- if (!options.expanded)
70
- return container;
71
- const body = typeof message.content === "string"
72
- ? message.content
73
- : message.content
74
- .filter((block) => block.type === "text")
75
- .map((block) => block.text)
76
- .join("\n");
77
- if (body.trim()) {
78
- container.addChild(new Spacer(1));
79
- container.addChild(new Markdown(body, options.outputPad, 0, getMarkdownTheme(), {
80
- color: (text) => theme.fg("toolOutput", text),
81
- }));
82
- }
83
- return container;
84
- }
85
- function callText(value) {
86
- return typeof value === "string" ? value.trim() : "";
87
- }
88
- /** Title-only headline when collapsed; Markdown plan body and revision target on expand. */
89
- export function renderExitPlanCall(args, theme, expanded) {
90
- const title = callText(args?.title);
91
- const headline = [
92
- theme.fg("toolTitle", theme.bold("exit_plan")),
93
- title ? theme.fg("text", title) : theme.fg("dim", "…"),
94
- ].join(" ");
95
- if (!expanded)
96
- return new Text(headline, 0, 0);
97
- const container = new Container();
98
- container.addChild(new Text(headline, 0, 0));
99
- const revises = callText(args?.revises);
100
- if (revises)
101
- container.addChild(new Text(theme.fg("dim", `revises ${revises}`), 0, 0));
102
- const plan = callText(args?.plan);
103
- if (plan) {
104
- container.addChild(new Spacer(1));
105
- container.addChild(new Markdown(plan, 0, 0, getMarkdownTheme(), {
106
- color: (text) => theme.fg("toolOutput", text),
107
- }));
108
- }
109
- return container;
110
- }
111
- // ---- tool result rendering --------------------------------------------------
112
- const DECISION_SUMMARY = {
113
- execute: "Executing with full context",
114
- compactAndExecute: "Compacting context, then executing",
115
- keepPlanning: "Still planning",
116
- cancelled: "No approval was pending",
117
- };
118
- function isExitPlanDecision(value) {
119
- return value === "execute" || value === "compactAndExecute" || value === "keepPlanning" || value === "cancelled";
120
- }
121
- function resultText(result) {
122
- return result.content
123
- .filter((block) => block.type === "text")
124
- .map((block) => block.text)
125
- .join("\n");
126
- }
127
- /**
128
- * What the user needs to know: where the plan landed and what happens next.
129
- * Falls back to the model-facing text when details are missing — a thrown
130
- * approval (interrupt) has no details to render from.
131
- */
132
- export function formatExitPlanResult(result, theme) {
133
- const details = result.details;
134
- if (!details || typeof details !== "object" || Array.isArray(details)) {
135
- return theme.fg("dim", resultText(result));
136
- }
137
- const decision = details.decision;
138
- if (!isExitPlanDecision(decision))
139
- return theme.fg("dim", resultText(result));
140
- const rawPlanPath = details.planPath;
141
- const lines = [];
142
- if (typeof rawPlanPath === "string" && rawPlanPath) {
143
- lines.push(theme.fg("dim", `Saved to ${shortenPlanPath(rawPlanPath)}`));
144
- }
145
- const decisionColor = decision === "cancelled" ? "warning" : "text";
146
- lines.push(theme.fg(decisionColor, DECISION_SUMMARY[decision]));
147
- return lines.join("\n");
148
- }
149
- //# sourceMappingURL=view.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"view.js","sourceRoot":"","sources":["../../../src/extensions/plan/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAkB,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAG3F,OAAO,EAAE,OAAO,EAAE,MAAM,wDAAwD,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAc,MAAM,wCAAwC,CAAC;AAGtF,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,IAAY,EAAU;IACrD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAClE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,SAAS,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7E,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACpE,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;AAAA,CACzD;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,UAAkB,EAAU;IACrD,IAAI,UAAU,GAAG,IAAI;QAAE,OAAO,GAAG,UAAU,EAAE,CAAC;IAC9C,IAAI,UAAU,GAAG,MAAM;QAAE,OAAO,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACrE,IAAI,UAAU,GAAG,SAAS;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC;IACvE,IAAI,UAAU,GAAG,UAAU;QAAE,OAAO,GAAG,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9E,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC;AAAA,CAChD;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAqB,EAAsB;IACjF,MAAM,KAAK,GAAG,GAAG,CAAC,eAAe,EAAE,CAAC;IACpC,IAAI,KAAK,EAAE,OAAO,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,gBAAgB,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7F,OAAO,WAAW,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC;AAAA,CACvD;AAED,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CACvC,OAA0C,EAC1C,OAAiD,EACjD,KAAY,EACA;IACZ,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAgC,CAAC;IACvE,MAAM,KAAK,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IACxG,MAAM,QAAQ,GAAG,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAErF,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;IAClC,SAAS,CAAC,QAAQ,CACjB,IAAI,IAAI,CACP,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAC/F,OAAO,CAAC,SAAS,EACjB,CAAC,CACD,CACD,CAAC;IACF,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ;QAClC,CAAC,CAAC,QAAQ;YACT,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;YAC5C,CAAC,CAAC,SAAS;QACZ,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,QAAQ,IAAI,OAAO,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IAC5E,IAAI,UAAU;QAAE,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/E,IAAI,CAAC,OAAO,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IAExC,MAAM,IAAI,GACT,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;QAClC,CAAC,CAAC,OAAO,CAAC,OAAO;QACjB,CAAC,CAAC,OAAO,CAAC,OAAO;aACd,MAAM,CAAC,CAAC,KAAK,EAA2C,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC;aACjF,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,IAAI,CAAC,IAAI,CAAC,CAAC;IAChB,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACjB,SAAS,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,SAAS,CAAC,QAAQ,CACjB,IAAI,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE;YAC5D,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC;SAC7C,CAAC,CACF,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAOD,SAAS,QAAQ,CAAC,KAAc,EAAU;IACzC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACrD;AAED,4FAA4F;AAC5F,MAAM,UAAU,kBAAkB,CAAC,IAAkC,EAAE,KAAY,EAAE,QAAiB,EAAa;IAClH,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG;QAChB,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC9C,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAG,CAAC;KACtD,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAE/C,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;IAClC,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACxC,IAAI,OAAO;QAAE,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvF,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAClC,IAAI,IAAI,EAAE,CAAC;QACV,SAAS,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,SAAS,CAAC,QAAQ,CACjB,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE;YAC5C,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC;SAC7C,CAAC,CACF,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,gFAAgF;AAEhF,MAAM,gBAAgB,GAAqC;IAC1D,OAAO,EAAE,6BAA6B;IACtC,iBAAiB,EAAE,oCAAoC;IACvD,YAAY,EAAE,gBAAgB;IAC9B,SAAS,EAAE,yBAAyB;CACpC,CAAC;AAEF,SAAS,kBAAkB,CAAC,KAAc,EAA6B;IACtE,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,mBAAmB,IAAI,KAAK,KAAK,cAAc,IAAI,KAAK,KAAK,WAAW,CAAC;AAAA,CACjH;AAED,SAAS,UAAU,CAAC,MAAwC,EAAU;IACrE,OAAO,MAAM,CAAC,OAAO;SACnB,MAAM,CAAC,CAAC,KAAK,EAA2C,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC;SACjF,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;SAC1B,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACb;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAwC,EAAE,KAAY,EAAU;IACpG,MAAM,OAAO,GAAG,MAAM,CAAC,OAAkB,CAAC;IAC1C,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACvE,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,QAAQ,GAAI,OAAmC,CAAC,QAAQ,CAAC;IAC/D,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9E,MAAM,WAAW,GAAI,OAAmC,CAAC,QAAQ,CAAC;IAElE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,EAAE,CAAC;QACpD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,eAAe,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,aAAa,GAAG,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;IACpE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAChE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB","sourcesContent":["/**\n * plan/view.ts — approval-dialog context and exit_plan tool rendering.\n *\n * `exit_plan` carries the whole plan as a parameter, and the default renderer\n * echoes parameters verbatim: the row would repeat, escaped onto one line, the\n * markdown the model just printed as response text. Collapsed rows therefore\n * carry the title only, and the plan body is available on expand.\n *\n * The result text is written for the model (it restates tool-state precedence\n * rules), so the result row renders from `details` instead.\n */\nimport { type Component, Container, Markdown, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { AgentToolResult, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport type { CustomMessage } from \"../../core/messages.ts\";\nimport { keyText } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { getMarkdownTheme, type Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ExitPlanDecision, ExitPlanDetails, PlanKickoffDetails } from \"./schema.ts\";\n\n/** Collapse the home prefix so a plan path stays on one line. */\nexport function shortenPlanPath(path: string): string {\n\tconst rawHome = process.env.USERPROFILE || process.env.HOME || \"\";\n\tconst home = rawHome.replace(/[\\\\/]+$/, \"\");\n\tif (!home) return path;\n\tconst normalize = (value: string) => value.replace(/\\\\/g, \"/\").toLowerCase();\n\tif (!normalize(path).startsWith(`${normalize(home)}/`)) return path;\n\treturn `~${path.slice(home.length).replace(/\\\\/g, \"/\")}`;\n}\n\n/**\n * Context pressure is the fact the compact-or-not decision turns on, so the\n * dialog states it instead of making the user leave and check the footer.\n */\nfunction formatTokenCount(tokenCount: number): string {\n\tif (tokenCount < 1000) return `${tokenCount}`;\n\tif (tokenCount < 10_000) return `${(tokenCount / 1000).toFixed(1)}k`;\n\tif (tokenCount < 1_000_000) return `${Math.round(tokenCount / 1000)}k`;\n\tif (tokenCount < 10_000_000) return `${(tokenCount / 1_000_000).toFixed(1)}M`;\n\treturn `${Math.round(tokenCount / 1_000_000)}M`;\n}\n\nexport function formatApprovalSubtitle(ctx: ExtensionContext): string | undefined {\n\tconst usage = ctx.getContextUsage();\n\tif (usage?.percent == null) return undefined;\n\tconst window = usage.contextWindow > 0 ? ` of ${formatTokenCount(usage.contextWindow)}` : \"\";\n\treturn `Context ${usage.percent.toFixed(1)}%${window}`;\n}\n\n// ---- kickoff message rendering ----------------------------------------------\n\n/**\n * The kickoff message body repeats the whole plan for the LLM, so the default\n * renderer would flood the chat with markdown the user already approved.\n * Collapsed: a two-line card (title + saved path with the expand hint).\n * Expanded: the complete kickoff markdown, styled like the exit_plan body.\n */\nexport function renderPlanKickoffMessage(\n\tmessage: CustomMessage<PlanKickoffDetails>,\n\toptions: { expanded: boolean; outputPad: number },\n\ttheme: Theme,\n): Component {\n\tconst details = (message.details ?? {}) as Partial<PlanKickoffDetails>;\n\tconst title = typeof details.title === \"string\" && details.title.trim() ? details.title.trim() : \"plan\";\n\tconst planPath = typeof details.planPath === \"string\" ? details.planPath.trim() : \"\";\n\n\tconst container = new Container();\n\tcontainer.addChild(\n\t\tnew Text(\n\t\t\t[theme.fg(\"toolTitle\", theme.bold(\"plan\")), theme.fg(\"text\", `Executing \"${title}\"`)].join(\" \"),\n\t\t\toptions.outputPad,\n\t\t\t0,\n\t\t),\n\t);\n\tconst pathText = planPath ? `${shortenPlanPath(planPath)} ` : \"\";\n\tconst secondLine = options.expanded\n\t\t? planPath\n\t\t\t? theme.fg(\"dim\", shortenPlanPath(planPath))\n\t\t\t: undefined\n\t\t: theme.fg(\"dim\", `${pathText}(${keyText(\"app.tools.expand\")} to expand)`);\n\tif (secondLine) container.addChild(new Text(secondLine, options.outputPad, 0));\n\tif (!options.expanded) return container;\n\n\tconst body =\n\t\ttypeof message.content === \"string\"\n\t\t\t? message.content\n\t\t\t: message.content\n\t\t\t\t\t.filter((block): block is { type: \"text\"; text: string } => block.type === \"text\")\n\t\t\t\t\t.map((block) => block.text)\n\t\t\t\t\t.join(\"\\n\");\n\tif (body.trim()) {\n\t\tcontainer.addChild(new Spacer(1));\n\t\tcontainer.addChild(\n\t\t\tnew Markdown(body, options.outputPad, 0, getMarkdownTheme(), {\n\t\t\t\tcolor: (text) => theme.fg(\"toolOutput\", text),\n\t\t\t}),\n\t\t);\n\t}\n\treturn container;\n}\n\n// ---- tool call rendering ----------------------------------------------------\n// Args arrive off the wire and stream in, so every field is read defensively.\n\ntype ExitPlanCallArgs = { title?: unknown; plan?: unknown; revises?: unknown };\n\nfunction callText(value: unknown): string {\n\treturn typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Title-only headline when collapsed; Markdown plan body and revision target on expand. */\nexport function renderExitPlanCall(args: ExitPlanCallArgs | undefined, theme: Theme, expanded: boolean): Component {\n\tconst title = callText(args?.title);\n\tconst headline = [\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"exit_plan\")),\n\t\ttitle ? theme.fg(\"text\", title) : theme.fg(\"dim\", \"…\"),\n\t].join(\" \");\n\tif (!expanded) return new Text(headline, 0, 0);\n\n\tconst container = new Container();\n\tcontainer.addChild(new Text(headline, 0, 0));\n\tconst revises = callText(args?.revises);\n\tif (revises) container.addChild(new Text(theme.fg(\"dim\", `revises ${revises}`), 0, 0));\n\tconst plan = callText(args?.plan);\n\tif (plan) {\n\t\tcontainer.addChild(new Spacer(1));\n\t\tcontainer.addChild(\n\t\t\tnew Markdown(plan, 0, 0, getMarkdownTheme(), {\n\t\t\t\tcolor: (text) => theme.fg(\"toolOutput\", text),\n\t\t\t}),\n\t\t);\n\t}\n\treturn container;\n}\n\n// ---- tool result rendering --------------------------------------------------\n\nconst DECISION_SUMMARY: Record<ExitPlanDecision, string> = {\n\texecute: \"Executing with full context\",\n\tcompactAndExecute: \"Compacting context, then executing\",\n\tkeepPlanning: \"Still planning\",\n\tcancelled: \"No approval was pending\",\n};\n\nfunction isExitPlanDecision(value: unknown): value is ExitPlanDecision {\n\treturn value === \"execute\" || value === \"compactAndExecute\" || value === \"keepPlanning\" || value === \"cancelled\";\n}\n\nfunction resultText(result: AgentToolResult<ExitPlanDetails>): string {\n\treturn result.content\n\t\t.filter((block): block is { type: \"text\"; text: string } => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\n/**\n * What the user needs to know: where the plan landed and what happens next.\n * Falls back to the model-facing text when details are missing — a thrown\n * approval (interrupt) has no details to render from.\n */\nexport function formatExitPlanResult(result: AgentToolResult<ExitPlanDetails>, theme: Theme): string {\n\tconst details = result.details as unknown;\n\tif (!details || typeof details !== \"object\" || Array.isArray(details)) {\n\t\treturn theme.fg(\"dim\", resultText(result));\n\t}\n\tconst decision = (details as Record<string, unknown>).decision;\n\tif (!isExitPlanDecision(decision)) return theme.fg(\"dim\", resultText(result));\n\tconst rawPlanPath = (details as Record<string, unknown>).planPath;\n\n\tconst lines: string[] = [];\n\tif (typeof rawPlanPath === \"string\" && rawPlanPath) {\n\t\tlines.push(theme.fg(\"dim\", `Saved to ${shortenPlanPath(rawPlanPath)}`));\n\t}\n\tconst decisionColor = decision === \"cancelled\" ? \"warning\" : \"text\";\n\tlines.push(theme.fg(decisionColor, DECISION_SUMMARY[decision]));\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -1,19 +0,0 @@
1
- # Bundled extensions
2
-
3
- These are extensions shipped in the `@astralyn/pi` npm package. For the public API used to write external extensions, see the [Extension API documentation](../../extensions.md).
4
-
5
- They are registered as hidden built-in extensions and use the same public Extension API available to external extensions. Their implementation details are internal to the package and are not required for installation or configuration.
6
-
7
- | Extension | Tool or command | Documentation |
8
- |---|---|---|
9
- | `llama.cpp` | `/llama` | [llama.cpp](../../llama-cpp.md) |
10
- | `deepwiki` | `deepwiki` | [deepwiki.md](deepwiki.md) |
11
- | `plan` | `exit_plan`, `/plan` | [plan.md](plan.md) |
12
- | `question` | `question` | [question.md](question.md) |
13
- | `todo` | `todo`, `/todos` | [todo.md](todo.md) |
14
- | `rewind` | `/rewind`, `/tree` lifecycle | [rewind.md](rewind.md) |
15
- | `router` | `/router` | [router.md](router.md) |
16
- | `statusline` | Footer | [statusline.md](statusline.md) |
17
- | `subagent` | `subagent`, `/agents` | [subagent.md](subagent.md) |
18
-
19
- The catalog documents installed-package behavior and configuration; it does not require access to the package's internal source.
@@ -1,34 +0,0 @@
1
- # plan — read-only plan mode
2
-
3
- Adds `/plan` for a Claude-Code-style planning phase: the model explores with read-only tooling, interviews the user, and leaves the mode through a user-approved `exit_plan` tool call that saves the plan to disk and optionally compacts the context before execution starts.
4
-
5
- ## Behavior
6
-
7
- - `/plan` (or the `--plan` CLI flag) enters plan mode. On entry the active tool set is snapshotted and swapped to the exploration set: `edit` and `write` are deactivated, the standalone `grep`/`find`/`ls` search tools (registered but not active by default) are activated alongside `read`, and `bash` stays available but is constrained to read-only inspection by the plan-mode system prompt (git log/diff/blame/show/status, ls, wc, head, tail, cat, …) — the same contract as the explorer subagent. The bundled `subagent` tool stays active but only the built-in read-only `explorer` profile is allowed. Other extension tools (`question`, `todo`, `deepwiki`, …) stay available. A `tool_call` guard also covers requests already in flight when the mode toggles: it blocks `edit`/`write` and any `subagent` call whose profile (including the omitted-default `general`, and every task in `tasks` mode) is not `explorer`.
8
- - While planning, the system prompt gains a plan-mode block: explore before asking, interview one decision at a time with the `question` tool (with a recommendation and trade-offs), skip the interview for small clear tasks, then present the full plan as response text before calling `exit_plan`.
9
- - `exit_plan(title, plan, revises?)` opens the approval menu. Each option carries a muted description, and the dialog subtitle reports context usage as `Context 29.4% of 200k` when the window is known (or just the percentage otherwise) — the fact the compact-or-not decision turns on:
10
- - **Start executing** *(keep full context)* — saves the plan, restores tools, and execution continues in the same run.
11
- - **Compact context, then execute** *(best for long tasks)* — saves the plan, ends the run (`terminate`), compacts once the run has settled, and only then restores tools; execution restarts via a kickoff message that embeds the plan text, so continuation never depends on the compaction summary. The kickoff is a custom message: the LLM receives the full text, while the TUI renders a collapsed two-line card (title + saved path) that expands to the complete plan with the tools-expand key. If compaction fails, tools are restored and execution proceeds with full context.
12
- - **Keep planning** *(refine before running)* — optionally collects feedback (multi-line editor in the TUI, input dialog in RPC) and returns it to the model; plan mode stays active. `Esc` and dialog timeouts map here, never to an executing option, so the dismiss hint reads "keep planning" rather than "cancel".
13
- - Both executing options save the plan file. If the session has no name yet, the plan title becomes the session name.
14
- - `/plan` while planning opens the plan panel (TUI): the first option exits plan mode, the rest are saved plan files — this branch's plans first, then the project's, newest first, bounded at 20. Picking a file pastes its absolute path into the editor so the user can add an instruction around it; `Esc` keeps planning with no side effects. Outside the TUI the command keeps the direct-toggle semantic. During a pending compaction the command is refused until the compaction settles.
15
- - Plan files live under `<agentDir>/plans/<safeCwdDir>/<YYYYMMDD-HHmm>-<slug>.md` (`~/.pi/agent/plans/…` by default, `PI_CODING_AGENT_DIR` respected), grouped per project with the same cwd encoding as sessions, with `title`, `created`, `cwd`, `session`, and optional `revises` frontmatter. The local-time stamp keeps names chronologically sortable; same-minute collisions append `-2`, `-3`, …. Revisions are new files that point back via `revises`. Plans saved before the per-project layout stay in their legacy `plans/<sessionId>/` directories and remain reachable through the branch's recorded paths.
16
- - To revise a plan mid-execution, re-enter with `/plan`: the system prompt lists the branch's saved plans so the model reads the latest one, accounts for completed work, and produces a revision.
17
-
18
- ## Limits
19
-
20
- - The kickoff message embeds at most 24 000 characters of plan markdown; longer plans are truncated with a pointer to the full file.
21
- - In `json`/`print` modes `exit_plan` cannot ask, so it saves the plan and exits with context kept (write access is restored without a menu).
22
- - Approving with **Compact context, then execute** and then interrupting before compaction completes (quit, resume, branch switch) degrades to the keep-context exit on the next session start.
23
- - The dialog subtitle is omitted when context usage is not yet known (no assistant turn has reported usage).
24
- - The read-only bash constraint is prompt-enforced, not sandboxed — the same trust model as the explorer subagent.
25
-
26
- ## Implementation notes
27
-
28
- - **Conversation-backed state.** Every transition appends a `plan-mode` custom entry (never sent to the LLM); lifecycle handlers replay the current branch tail → head, so `/reload`, `/tree`, resume, and fork restore the mode. Fork and clone copy entries, so the recorded plan-file paths follow the new session even though it gets a fresh session id — the plans directory is only the initial storage location, not the index.
29
- - **State is keyed per session id** (todo pattern): resume and `/tree` can switch sessions within one process, and handlers re-point the active bucket before touching state.
30
- - **Tool restore is a snapshot, not an inverse delta.** Entry records the full pre-entry active set and exit restores it verbatim — necessary because entry both removes (`edit`/`write`) and adds (`grep`/`find`/`ls`) tools. Replay keeps the in-memory snapshot across branch switches while planning (re-snapshotting would capture the restricted set) and treats legacy `removedTools` entries as an empty snapshot, which degrades to dropping `exit_plan` only.
31
- - **No write access is ever needed for the plan itself** — the plan text travels as an `exit_plan` parameter and the extension writes the file.
32
- - **The compact-then-execute window is closed by construction**: plan mode (and its tool restrictions) persists until compaction completes; the run is terminated by the tool result and the compaction starts on `agent_settled`, which fires once per fully settled run.
33
- - The `exit_plan` result text authoritatively announces mode changes because the per-run system prompt override is not rebuilt mid-run.
34
- - **The tool row renders from the title and `details`, not from the parameters.** `exit_plan` carries the whole plan as a parameter, and its result text is written for the model, so the default renderer would echo the plan markdown escaped onto one line and then restate tool-state rules at the user. The collapsed call shows the title; expanding renders the complete plan with Pi's standard Markdown theme while keeping `revises` metadata muted. Results give the decision normal emphasis, keep `Saved to` secondary, and warn when historical state says no approval was pending. Malformed historical decision/path values fall back safely to the bounded model text.