pi-context 1.0.5 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +4 -4
- package/skills/context-management/SKILL.md +18 -10
- package/src/index.ts +436 -0
- package/src/utils.ts +0 -32
- package/src/tools.ts +0 -312
- /package/src/{commands.ts → context.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Pi Context
|
|
1
|
+
# Pi Context: Agentic Context Management for the Pi
|
|
2
2
|
|
|
3
3
|
A Git-like context management tool that allows AI agents to proactively manage their context.
|
|
4
4
|
|
|
@@ -17,10 +17,10 @@ pi install npm:pi-context
|
|
|
17
17
|
|
|
18
18
|
### For Humans
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Run the command to enable ACM (**A**gentic **C**ontext **M**anagement) for the current session.
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
/
|
|
23
|
+
/acm
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
View detailed context window usage and token distribution with a visual dashboard. (like `claude code /context`)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Agentic Context Management for the Pi",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"pi": {
|
|
31
31
|
"extensions": [
|
|
32
|
-
"./src/
|
|
33
|
-
"./src/
|
|
32
|
+
"./src/index.ts",
|
|
33
|
+
"./src/context.ts"
|
|
34
34
|
],
|
|
35
35
|
"skills": [
|
|
36
36
|
"./skills"
|
|
@@ -111,6 +111,13 @@ If you fail 3 times:
|
|
|
111
111
|
3. Summarize the failure in the checkout message ("Tried X, failed because Y").
|
|
112
112
|
4. Try a new approach from the clean state.
|
|
113
113
|
|
|
114
|
+
### After Checkout: Execute Next Step
|
|
115
|
+
|
|
116
|
+
When `context_checkout` completes and injects a summary, you are in a **new context**.
|
|
117
|
+
|
|
118
|
+
1. **READ** the injected summary carefully
|
|
119
|
+
2. **EXECUTE** the `Next Step` from the summary - this is your new task.
|
|
120
|
+
|
|
114
121
|
## Decision Matrix: When to Act
|
|
115
122
|
|
|
116
123
|
| Situation | Action | Reason |
|
|
@@ -132,24 +139,24 @@ If you cannot answer these, run `context_log`:
|
|
|
132
139
|
| **Is this history useful?** | If "No" -> **SQUASH IT.** |
|
|
133
140
|
| **Am I in a loop?** | Repeated entries in the graph. |
|
|
134
141
|
|
|
135
|
-
|
|
136
142
|
## Good Checkout Messages
|
|
137
143
|
|
|
138
144
|
The `message` is your lifeline to your past self.
|
|
139
145
|
A good message preserves critical context that would otherwise be lost.
|
|
140
146
|
|
|
141
|
-
Structure: `[Key Finding/Status] + [Reason] + [Important Changes]`
|
|
147
|
+
Structure: `[Key Finding/Status] + [Reason] + [Important Changes] + [Next Step]`
|
|
142
148
|
|
|
143
149
|
* **Key Finding/Status**: What did you discover or complete? Include specific numbers, errors, or outcomes.
|
|
144
150
|
* **Reason**: Why are you branching/moving? (e.g., "Task complete", "Approach failed", "Need raw logs")
|
|
145
151
|
* **Important Changes**: What files or logic have been modified? (This checkout only resets *conversation history*, NOT disk files, so you must remember what changed.)
|
|
152
|
+
* **Next Step**: What should you do immediately after this squash? Be specific. (e.g., "Wait for user feedback", "Implement the recommended fix", "Revert file X and try approach Y")
|
|
146
153
|
|
|
147
154
|
Examples:
|
|
148
155
|
|
|
149
|
-
* *Good (Resetting after failure)*: "Recursive parser hit stack overflow at depth 8000. Switching to iterative. **Reason**: Stack limit reached. **Important Changes**: Modified `utils/recursion.ts`."
|
|
150
|
-
* *Good (Cleaning up)*: "Auth module complete: JWT + OAuth2 + RBAC. 23 tests passing. **Reason**: Task done, cleaning context. **Important Changes**: Created `auth/`, modified `routes.ts` and `middleware.ts`."
|
|
156
|
+
* *Good (Resetting after failure)*: "Recursive parser hit stack overflow at depth 8000. Switching to iterative. **Reason**: Stack limit reached. **Important Changes**: Modified `utils/recursion.ts`. **Next Step**: Inform user of the failure and propose iterative approach."
|
|
157
|
+
* *Good (Cleaning up)*: "Auth module complete: JWT + OAuth2 + RBAC. 23 tests passing. **Reason**: Task done, cleaning context. **Important Changes**: Created `auth/`, modified `routes.ts` and `middleware.ts`. **Next Step**: Report completion to user, ask if they want to review or test."
|
|
151
158
|
* *Bad*: "Switching context." (Too vague - you will forget why)
|
|
152
|
-
* *Bad*: "Done." (What is done? What
|
|
159
|
+
* *Bad*: "Done." (What is done? What should you do next?)
|
|
153
160
|
|
|
154
161
|
## Anti-Patterns
|
|
155
162
|
|
|
@@ -162,6 +169,7 @@ Examples:
|
|
|
162
169
|
| **Blind Checkout** (Guessing IDs) | **Look** (`context_log`) first to get valid IDs. |
|
|
163
170
|
| **Vague Summaries** ("Done", "Fixed") | **Detailed Summaries** ("Found bug in line 40. Fixed with patch X.") |
|
|
164
171
|
| **Generic Tag Names** (`task-start`, `phase-1`) | **Semantic Names** (`auth-jwt-start`, `db-schema-plan`) |
|
|
172
|
+
| **Missing Next Step** in checkout message | **Always specify** what to do after squash (e.g., "Wait for user", "Implement fix X") |
|
|
165
173
|
|
|
166
174
|
## Recipes (Copy-Paste)
|
|
167
175
|
|
|
@@ -180,7 +188,7 @@ context_tag({ name: "timeout-analysis-start" });
|
|
|
180
188
|
// 2. Squash IMMEDIATELY. Do not wait for user.
|
|
181
189
|
context_checkout({
|
|
182
190
|
target: "timeout-analysis-start",
|
|
183
|
-
message: "Found DB connection pool exhaustion as root cause (pool size: 10, peak load: 1000 req/s). Recommended fix: increase to 50. **Reason**: Context cleanup after research. **Important Changes**: None (read-only).",
|
|
191
|
+
message: "Found DB connection pool exhaustion as root cause (pool size: 10, peak load: 1000 req/s). Recommended fix: increase to 50. **Reason**: Context cleanup after research. **Important Changes**: None (read-only). **Next Step**: Report findings to user and await approval to implement fix.",
|
|
184
192
|
backupTag: "timeout-analysis-raw-history" // Safety backup
|
|
185
193
|
});
|
|
186
194
|
context_tag({ name: "timeout-analysis-done" });
|
|
@@ -197,7 +205,7 @@ context_tag({ name: "timeout-analysis-done" });
|
|
|
197
205
|
// Squash to Summary (Optimistic Cleanup)
|
|
198
206
|
context_checkout({
|
|
199
207
|
target: "oauth-impl-start", // Squash range: Start -> Now
|
|
200
|
-
message: "OAuth2 flow implemented with PKCE, Google + GitHub providers. All 12 tests passing. **Reason**: Task complete, cleaning up. **Important Changes**: Created `auth/oauth.ts`, modified `routes.ts`, `config.ts`.
|
|
208
|
+
message: "OAuth2 flow implemented with PKCE, Google + GitHub providers. All 12 tests passing. **Reason**: Task complete, cleaning up. **Important Changes**: Created `auth/oauth.ts`, modified `routes.ts`, `config.ts`. **Next Step**: Report completion to user, summarize what was implemented.",
|
|
201
209
|
backupTag: "oauth-impl-raw-history"
|
|
202
210
|
});
|
|
203
211
|
context_tag({ name: "oauth-impl-candidate" });
|
|
@@ -213,7 +221,7 @@ context_tag({ name: "oauth-impl-candidate" });
|
|
|
213
221
|
// Jump back to the raw history
|
|
214
222
|
context_checkout({
|
|
215
223
|
target: "oauth-impl-raw-history",
|
|
216
|
-
message: "Reviewing token refresh logic - user reports 401 after 15 min idle. Suspect refresh token not firing. **Reason**: Need raw logs to trace the bug. **Important Changes**: None."
|
|
224
|
+
message: "Reviewing token refresh logic - user reports 401 after 15 min idle. Suspect refresh token not firing. **Reason**: Need raw logs to trace the bug. **Important Changes**: None. **Next Step**: Re-read token refresh implementation and identify the bug."
|
|
217
225
|
});
|
|
218
226
|
context_tag({ name: "oauth-review-start" });
|
|
219
227
|
```
|
|
@@ -228,7 +236,7 @@ context_tag({ name: "oauth-review-start" });
|
|
|
228
236
|
// Method A (weak references) failed, trying Method B (object pooling)
|
|
229
237
|
context_checkout({
|
|
230
238
|
target: "memory-leak-fix-start",
|
|
231
|
-
message: "WeakRef approach failed: objects GC'd within 30s (expected: 5min). Cache hit rate dropped from 95% to 12%. **Reason**: Switching to object pooling approach. **Important Changes**: `CacheManager.ts` modified (will revert)."
|
|
239
|
+
message: "WeakRef approach failed: objects GC'd within 30s (expected: 5min). Cache hit rate dropped from 95% to 12%. **Reason**: Switching to object pooling approach. **Important Changes**: `CacheManager.ts` modified (will revert). **Next Step**: Revert `CacheManager.ts` changes and implement object pooling strategy."
|
|
232
240
|
});
|
|
233
241
|
context_tag({ name: "memory-leak-pool-approach-start" });
|
|
234
242
|
```
|
|
@@ -243,7 +251,7 @@ You tried to fix a bug but broke everything.
|
|
|
243
251
|
// Attempted mutex-based fix, but introduced deadlock
|
|
244
252
|
context_checkout({
|
|
245
253
|
target: "race-condition-fix-start",
|
|
246
|
-
message: "Mutex caused deadlock: Thread A holds mutex, awaits callback; callback needs mutex held by B; B waits for A. Circular wait detected. **Reason**: Trying lock-free CAS approach next. **Important Changes**: `AsyncQueue.ts` lines 70-90 modified (backup saved).",
|
|
254
|
+
message: "Mutex caused deadlock: Thread A holds mutex, awaits callback; callback needs mutex held by B; B waits for A. Circular wait detected. **Reason**: Trying lock-free CAS approach next. **Important Changes**: `AsyncQueue.ts` lines 70-90 modified (backup saved). **Next Step**: Revert `AsyncQueue.ts` and implement lock-free compare-and-swap approach.",
|
|
247
255
|
backupTag: "race-condition-mutex-fail" // Save the failure for reference
|
|
248
256
|
});
|
|
249
257
|
context_tag({ name: "race-condition-lockfree-start" });
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type SessionManager,
|
|
4
|
+
type SessionEntry,
|
|
5
|
+
} from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import type {
|
|
7
|
+
TextContent,
|
|
8
|
+
ImageContent,
|
|
9
|
+
ToolCall,
|
|
10
|
+
} from "@mariozechner/pi-ai";
|
|
11
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
12
|
+
import { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { formatTokens } from "./utils.js";
|
|
14
|
+
|
|
15
|
+
// Define missing types locally as they are not exported from the main entry point
|
|
16
|
+
interface SessionTreeNode {
|
|
17
|
+
entry: SessionEntry;
|
|
18
|
+
children: SessionTreeNode[];
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const InternalTools = ["context_tag", "context_log", "context_checkout"];
|
|
23
|
+
let CommandCtx: ExtensionCommandContext | null = null;
|
|
24
|
+
let CheckoutParams: any = null;
|
|
25
|
+
|
|
26
|
+
const isInternal = (name: string) => InternalTools.includes(name);
|
|
27
|
+
|
|
28
|
+
const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
29
|
+
if (target.toLowerCase() === "root") {
|
|
30
|
+
const tree = sm.getTree();
|
|
31
|
+
return tree.length > 0 ? tree[0].entry.id : target;
|
|
32
|
+
}
|
|
33
|
+
if (/^[0-9a-f]{8,}$/i.test(target)) return target;
|
|
34
|
+
const find = (nodes: SessionTreeNode[]): string | null => {
|
|
35
|
+
for (const n of nodes) {
|
|
36
|
+
if (sm.getLabel(n.entry.id) === target) return n.entry.id;
|
|
37
|
+
const r = find(n.children);
|
|
38
|
+
if (r) return r;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
// sm.getTree() returns the SDK's SessionTreeNode[], which is structurally compatible
|
|
43
|
+
return find(sm.getTree()) || target;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const ContextLogParams = Type.Object({
|
|
47
|
+
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
48
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show ALL messages. If false (default), collapses intermediate AI steps and only shows 'milestones': User messages, Tags, Branch Points, and Summaries." })),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const ContextCheckoutParams = Type.Object({
|
|
52
|
+
target: Type.String({ description: "Where to jump/squash to. Can be a tag name (e.g., 'task-start'), a commit ID, or 'root'. This is the base for your new branch." }),
|
|
53
|
+
message: Type.String({ description: "The 'Carryover Message' for the new branch. A summary of your *current* progress/lessons that you want to bring with you to the new state. This ensures you don't lose key information when switching contexts. Good summary message: '[Status] + [Reason] + [Important Changes] + [Carryover Data]'" }),
|
|
54
|
+
backupTag: Type.Optional(Type.String({ description: "Optional tag name to apply to the CURRENT state before checking out. Use this to create an automatic backup of the history you are about to leave/squash." })),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const ContextTagParams = Type.Object({
|
|
58
|
+
name: Type.String({ description: "The tag/milestone name. Use meaningful names." }),
|
|
59
|
+
target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export default function (pi: ExtensionAPI) {
|
|
63
|
+
pi.registerCommand("acm", {
|
|
64
|
+
description: "Enable agentic context management for the current session",
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
CommandCtx = ctx;
|
|
67
|
+
ctx.ui.notify("Agentic Context Management enabled.", "info");
|
|
68
|
+
pi.sendMessage({
|
|
69
|
+
customType: "pi-context",
|
|
70
|
+
content: "use context-management skill",
|
|
71
|
+
display: false,
|
|
72
|
+
}, {
|
|
73
|
+
deliverAs: "followUp"
|
|
74
|
+
});
|
|
75
|
+
if (args) {
|
|
76
|
+
pi.sendUserMessage(args)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Helper: Check if a tag name already exists in the tree
|
|
82
|
+
const findTagInTree = (sm: SessionManager, nodes: SessionTreeNode[], tagName: string): string | null => {
|
|
83
|
+
for (const n of nodes) {
|
|
84
|
+
if (sm.getLabel(n.entry.id) === tagName) return n.entry.id;
|
|
85
|
+
const r = findTagInTree(sm, n.children, tagName);
|
|
86
|
+
if (r) return r;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
pi.registerTool({
|
|
92
|
+
name: "context_tag",
|
|
93
|
+
label: "Context Tag",
|
|
94
|
+
description: "Creates a 'Save Point' (Bookmark) in the history. Use this before trying risky changes or when a feature is stable. 'Untagged progress is risky'.",
|
|
95
|
+
parameters: ContextTagParams,
|
|
96
|
+
async execute(_id, params: Static<typeof ContextTagParams>, _signal, _onUpdate, ctx) {
|
|
97
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
98
|
+
|
|
99
|
+
// Deduplication check: ensure tag name is unique
|
|
100
|
+
const existingTagId = findTagInTree(sm, sm.getTree(), params.name);
|
|
101
|
+
if (existingTagId) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `Error: Tag '${params.name}' already exists at ${existingTagId}. Tag names must be unique. Use a different name or delete the existing tag first.`
|
|
106
|
+
}],
|
|
107
|
+
details: {}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
112
|
+
|
|
113
|
+
if (!id) {
|
|
114
|
+
// Auto-resolve: Find the last "interesting" node to tag.
|
|
115
|
+
// We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
|
|
116
|
+
const branch = sm.getBranch();
|
|
117
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
118
|
+
const entry = branch[i];
|
|
119
|
+
|
|
120
|
+
// 1. Check ToolResults
|
|
121
|
+
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
|
122
|
+
const tr = entry.message as any;
|
|
123
|
+
if (isInternal(tr.toolName)) continue;
|
|
124
|
+
|
|
125
|
+
// Public tool result is a valid target
|
|
126
|
+
id = entry.id;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Check Assistant messages for visibility
|
|
131
|
+
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
|
132
|
+
const m = entry.message;
|
|
133
|
+
const hasInternalTool = m.content.some(c => c.type === 'toolCall' && isInternal(c.name));
|
|
134
|
+
|
|
135
|
+
if (!hasInternalTool) {
|
|
136
|
+
id = entry.id;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
id = entry.id;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
// Fallback to leaf if search failed
|
|
145
|
+
if (!id) id = sm.getLeafId() ?? "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pi.setLabel(id, params.name);
|
|
149
|
+
return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id}` }], details: {} };
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.registerTool({
|
|
154
|
+
name: "context_log",
|
|
155
|
+
label: "Context Log",
|
|
156
|
+
description: "Show the entire history structure (status, message, tags, milestones). Analogous to 'git log --graph --oneline --decorate'",
|
|
157
|
+
parameters: ContextLogParams,
|
|
158
|
+
async execute(_id, params: Static<typeof ContextLogParams>, _signal, _onUpdate, ctx) {
|
|
159
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
160
|
+
const branch = sm.getBranch();
|
|
161
|
+
const currentLeafId = sm.getLeafId();
|
|
162
|
+
const verbose = params.verbose ?? false;
|
|
163
|
+
const limit = params.limit ?? 50;
|
|
164
|
+
|
|
165
|
+
const backboneIds = new Set(branch.map((e) => e.id));
|
|
166
|
+
const sequence: SessionEntry[] = [];
|
|
167
|
+
|
|
168
|
+
branch.forEach((entry) => {
|
|
169
|
+
sequence.push(entry);
|
|
170
|
+
|
|
171
|
+
// Preserve side-summary logic: Show branch summaries/compactions that are off-path
|
|
172
|
+
const children = sm.getChildren(entry.id);
|
|
173
|
+
children.forEach((child) => {
|
|
174
|
+
if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
|
|
175
|
+
sequence.push(child);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const getMsgContent = (entry: SessionEntry): string => {
|
|
181
|
+
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
182
|
+
const e = entry;
|
|
183
|
+
return e.summary || "[No summary provided]";
|
|
184
|
+
}
|
|
185
|
+
if (entry.type === "label") {
|
|
186
|
+
return `tag: ${entry.label}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (entry.type === "message") {
|
|
190
|
+
const msg = entry.message;
|
|
191
|
+
|
|
192
|
+
if (msg.role === "toolResult") {
|
|
193
|
+
const tr = msg;
|
|
194
|
+
if (!verbose && isInternal(tr.toolName)) return "";
|
|
195
|
+
|
|
196
|
+
const extractText = (content: (TextContent | ImageContent)[]): string => {
|
|
197
|
+
return content
|
|
198
|
+
.map((p) => (p.type === "text" ? p.text : ""))
|
|
199
|
+
.join(" ")
|
|
200
|
+
.trim();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
let resText = extractText(tr.content);
|
|
204
|
+
const details = tr.details as Record<string, unknown> | undefined;
|
|
205
|
+
if ((tr.toolName === "read" || tr.toolName === "edit") && details && "path" in details && typeof details.path === "string") {
|
|
206
|
+
resText = `${details.path}: ${resText}`;
|
|
207
|
+
}
|
|
208
|
+
return `(${tr.toolName}) ${resText}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (msg.role === "bashExecution") {
|
|
212
|
+
return `[Bash] ${msg.command}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
216
|
+
let text = "";
|
|
217
|
+
if (typeof msg.content === "string") {
|
|
218
|
+
text = msg.content;
|
|
219
|
+
} else if (Array.isArray(msg.content)) {
|
|
220
|
+
text = msg.content
|
|
221
|
+
.map((p: any) => {
|
|
222
|
+
if (typeof p === "object" && p !== null && "text" in p) return (p as TextContent).text;
|
|
223
|
+
return "";
|
|
224
|
+
})
|
|
225
|
+
.join(" ")
|
|
226
|
+
.trim();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let toolCallsText = "";
|
|
230
|
+
if (msg.role === "assistant") {
|
|
231
|
+
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
232
|
+
|
|
233
|
+
toolCallsText = toolCalls
|
|
234
|
+
.filter((tc) => verbose || !isInternal(tc.name))
|
|
235
|
+
.map((tc) => `call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
|
|
236
|
+
.join("; ");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return [text, toolCallsText].filter(Boolean).join(" ");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return "";
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const isInteresting = (entry: SessionEntry): boolean => {
|
|
246
|
+
// 1. HEAD and Root
|
|
247
|
+
if (entry.id === currentLeafId) return true;
|
|
248
|
+
if (branch.length > 0 && entry.id === branch[0].id) return true;
|
|
249
|
+
|
|
250
|
+
// 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
|
|
251
|
+
if (sm.getLabel(entry.id)) return true;
|
|
252
|
+
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
253
|
+
|
|
254
|
+
// 3. Structural Milestones (Summaries)
|
|
255
|
+
if (entry.type === 'branch_summary' || entry.type === 'compaction') return true;
|
|
256
|
+
|
|
257
|
+
// 4. Branch Points (Forks)
|
|
258
|
+
if (sm.getChildren(entry.id).length > 1) return true;
|
|
259
|
+
|
|
260
|
+
// 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
|
|
261
|
+
if (entry.type === 'message' && entry.message.role === 'user') return true;
|
|
262
|
+
|
|
263
|
+
return false;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const visibleSequenceIds = new Set<string>();
|
|
267
|
+
sequence.forEach(e => {
|
|
268
|
+
if (verbose || isInteresting(e)) {
|
|
269
|
+
visibleSequenceIds.add(e.id);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
|
|
274
|
+
if (visibleEntries.length > limit) {
|
|
275
|
+
const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
|
|
276
|
+
visibleSequenceIds.clear();
|
|
277
|
+
allowedIds.forEach(id => visibleSequenceIds.add(id));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const lines: string[] = [];
|
|
281
|
+
let hiddenCount = 0;
|
|
282
|
+
|
|
283
|
+
sequence.forEach((entry) => {
|
|
284
|
+
if (!visibleSequenceIds.has(entry.id)) {
|
|
285
|
+
hiddenCount++;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (hiddenCount > 0) {
|
|
290
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
291
|
+
hiddenCount = 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const isHead = entry.id === currentLeafId;
|
|
295
|
+
const label = sm.getLabel(entry.id);
|
|
296
|
+
const content = getMsgContent(entry).replace(/\s+/g, " ");
|
|
297
|
+
|
|
298
|
+
let role = entry.type.toUpperCase();
|
|
299
|
+
if (entry.type === "message") {
|
|
300
|
+
const m = entry.message;
|
|
301
|
+
role =
|
|
302
|
+
m.role === "assistant"
|
|
303
|
+
? "AI"
|
|
304
|
+
: m.role === "user"
|
|
305
|
+
? "USER"
|
|
306
|
+
: m.role === "bashExecution"
|
|
307
|
+
? "BASH"
|
|
308
|
+
: "TOOL";
|
|
309
|
+
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
310
|
+
role = "SUMMARY";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// hide custom messages
|
|
314
|
+
if (role === "CUSTOM_MESSAGE") {
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const id = entry.id;
|
|
319
|
+
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
320
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
321
|
+
|
|
322
|
+
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
323
|
+
|
|
324
|
+
const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
|
|
325
|
+
|
|
326
|
+
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (hiddenCount > 0) {
|
|
330
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// --- Context Dashboard (HUD) ---
|
|
334
|
+
const usage = await ctx.getContextUsage();
|
|
335
|
+
let usageStr = "Unknown";
|
|
336
|
+
if (usage) {
|
|
337
|
+
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Find the distance to the nearest tag
|
|
341
|
+
let stepsSinceTag = 0;
|
|
342
|
+
let nearestTagName = "None";
|
|
343
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
344
|
+
const id = branch[i].id;
|
|
345
|
+
const label = sm.getLabel(id);
|
|
346
|
+
if (label) {
|
|
347
|
+
nearestTagName = label;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
stepsSinceTag++;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const hud = [
|
|
354
|
+
`[Context Dashboard]`,
|
|
355
|
+
`• Context Usage: ${usageStr}`,
|
|
356
|
+
`• Segment Size: ${stepsSinceTag} steps since last tag '${nearestTagName}'`,
|
|
357
|
+
`---------------------------------------------------`
|
|
358
|
+
].join("\n");
|
|
359
|
+
|
|
360
|
+
return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
pi.registerTool({
|
|
365
|
+
name: "context_checkout",
|
|
366
|
+
label: "Context Checkout",
|
|
367
|
+
description: "Navigate to ANY point in the conversation history. This checkout only resets *conversation history*, NOT disk files. ALWAYS provide a detailed 'message' to bridge context.",
|
|
368
|
+
parameters: ContextCheckoutParams,
|
|
369
|
+
async execute(_id, params: Static<typeof ContextCheckoutParams>, _signal, _onUpdate, ctx) {
|
|
370
|
+
if (!CommandCtx) {
|
|
371
|
+
ctx.ui.setEditorText(`/acm ${ctx.ui.getEditorText() || "continue"}`)
|
|
372
|
+
return {
|
|
373
|
+
content: [{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: "Agentic context management is not enabled. Ask the user to run `/acm` in the pi to enable it, then retry."
|
|
376
|
+
}],
|
|
377
|
+
details: {}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
381
|
+
|
|
382
|
+
const tid = resolveTargetId(sm, params.target);
|
|
383
|
+
|
|
384
|
+
const currentLeaf = sm.getLeafId();
|
|
385
|
+
if (currentLeaf === tid) {
|
|
386
|
+
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
387
|
+
}
|
|
388
|
+
if (params.backupTag && currentLeaf) {
|
|
389
|
+
pi.setLabel(currentLeaf, params.backupTag);
|
|
390
|
+
}
|
|
391
|
+
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
392
|
+
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
|
|
393
|
+
|
|
394
|
+
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
395
|
+
|
|
396
|
+
const nid = await sm.branchWithSummary(tid, enrichedMessage);
|
|
397
|
+
CheckoutParams = params;
|
|
398
|
+
CheckoutParams.nid = nid;
|
|
399
|
+
CheckoutParams.tid = tid;
|
|
400
|
+
CheckoutParams.enrichedMessage = enrichedMessage;
|
|
401
|
+
|
|
402
|
+
return { content: [{ type: "text", text: "checkout start" }], details: {} };
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
407
|
+
if (!CheckoutParams) {
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
ctx.abort()
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
414
|
+
if (!CheckoutParams) {
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
if (!CommandCtx) {
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await CommandCtx.navigateTree(CheckoutParams.nid, {
|
|
422
|
+
summarize: false,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
ctx.ui.notify(`Checked out ${CheckoutParams.target}${CheckoutParams.target === CheckoutParams.tid ? "" : `(${CheckoutParams.tid})`}\nBackup tag created: ${CheckoutParams.backupTag || "none"}\nmessage: ${CheckoutParams.enrichedMessage}`, "info");
|
|
426
|
+
CheckoutParams = null;
|
|
427
|
+
|
|
428
|
+
pi.sendMessage({
|
|
429
|
+
customType: "pi-context",
|
|
430
|
+
content: "context_checkout complete. A summary of your previous branch was injected above. Read it to understand your new state. Execute the 'Next Step' from the summary",
|
|
431
|
+
display: false,
|
|
432
|
+
}, {
|
|
433
|
+
triggerTurn: true,
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type SessionEntry,
|
|
3
|
-
type SessionManager,
|
|
4
|
-
} from "@mariozechner/pi-coding-agent";
|
|
5
|
-
|
|
6
|
-
// Define missing types locally as they are not exported from the main entry point
|
|
7
|
-
export interface SessionTreeNode {
|
|
8
|
-
entry: SessionEntry;
|
|
9
|
-
children: SessionTreeNode[];
|
|
10
|
-
label?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const isInternal = (name: string) => ["context_tag", "context_log", "context_checkout"].includes(name);
|
|
14
|
-
|
|
15
|
-
export const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
16
|
-
if (target.toLowerCase() === "root") {
|
|
17
|
-
const tree = sm.getTree();
|
|
18
|
-
return tree.length > 0 ? tree[0].entry.id : target;
|
|
19
|
-
}
|
|
20
|
-
if (/^[0-9a-f]{8,}$/i.test(target)) return target;
|
|
21
|
-
const find = (nodes: SessionTreeNode[]): string | null => {
|
|
22
|
-
for (const n of nodes) {
|
|
23
|
-
if (sm.getLabel(n.entry.id) === target) return n.entry.id;
|
|
24
|
-
const r = find(n.children);
|
|
25
|
-
if (r) return r;
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
};
|
|
29
|
-
// sm.getTree() returns the SDK's SessionTreeNode[], which is structurally compatible
|
|
30
|
-
return find(sm.getTree()) || target;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
1
|
export const formatTokens = (n: number) => {
|
|
34
2
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
35
3
|
if (n >= 1_000) return Math.round(n / 1_000) + "k";
|
package/src/tools.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ExtensionAPI,
|
|
3
|
-
type SessionManager,
|
|
4
|
-
type SessionEntry,
|
|
5
|
-
} from "@mariozechner/pi-coding-agent";
|
|
6
|
-
import type {
|
|
7
|
-
TextContent,
|
|
8
|
-
ImageContent,
|
|
9
|
-
ToolCall,
|
|
10
|
-
} from "@mariozechner/pi-ai";
|
|
11
|
-
import { Type, type Static } from "@sinclair/typebox";
|
|
12
|
-
import { isInternal, resolveTargetId, formatTokens } from "./utils.js";
|
|
13
|
-
|
|
14
|
-
const ContextLogParams = Type.Object({
|
|
15
|
-
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
16
|
-
verbose: Type.Optional(Type.Boolean({ description: "If true, show ALL messages. If false (default), collapses intermediate AI steps and only shows 'milestones': User messages, Tags, Branch Points, and Summaries." })),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const ContextCheckoutParams = Type.Object({
|
|
20
|
-
target: Type.String({ description: "Where to jump/squash to. Can be a tag name (e.g., 'task-start'), a commit ID, or 'root'. This is the base for your new branch." }),
|
|
21
|
-
message: Type.String({ description: "The 'Carryover Message' for the new branch. A summary of your *current* progress/lessons that you want to bring with you to the new state. This ensures you don't lose key information when switching contexts. Good summary message: '[Status] + [Reason] + [Important Changes] + [Carryover Data]'" }),
|
|
22
|
-
backupTag: Type.Optional(Type.String({ description: "Optional tag name to apply to the CURRENT state before checking out. Use this to create an automatic backup of the history you are about to leave/squash." })),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const ContextTagParams = Type.Object({
|
|
26
|
-
name: Type.String({ description: "The tag/milestone name. Use meaningful names." }),
|
|
27
|
-
target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export default function (pi: ExtensionAPI) {
|
|
31
|
-
pi.registerTool({
|
|
32
|
-
name: "context_tag",
|
|
33
|
-
label: "Context Tag",
|
|
34
|
-
description: "Creates a 'Save Point' (Bookmark) in the history. Use this before trying risky changes or when a feature is stable. 'Untagged progress is risky'.",
|
|
35
|
-
parameters: ContextTagParams,
|
|
36
|
-
async execute(_id, params: Static<typeof ContextTagParams>, _signal, _onUpdate, ctx) {
|
|
37
|
-
const sm = ctx.sessionManager as SessionManager;
|
|
38
|
-
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
39
|
-
|
|
40
|
-
if (!id) {
|
|
41
|
-
// Auto-resolve: Find the last "interesting" node to tag.
|
|
42
|
-
// We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
|
|
43
|
-
const branch = sm.getBranch();
|
|
44
|
-
for (let i = branch.length - 1; i >= 0; i--) {
|
|
45
|
-
const entry = branch[i];
|
|
46
|
-
|
|
47
|
-
// 1. Check ToolResults
|
|
48
|
-
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
|
49
|
-
const tr = entry.message as any;
|
|
50
|
-
if (isInternal(tr.toolName)) continue;
|
|
51
|
-
|
|
52
|
-
// Public tool result is a valid target
|
|
53
|
-
id = entry.id;
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 2. Check Assistant messages for visibility
|
|
58
|
-
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
|
59
|
-
const m = entry.message;
|
|
60
|
-
const hasInternalTool = m.content.some(c => c.type === 'toolCall' && isInternal(c.name));
|
|
61
|
-
|
|
62
|
-
if (!hasInternalTool) {
|
|
63
|
-
id = entry.id;
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
id = entry.id;
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
// Fallback to leaf if search failed
|
|
72
|
-
if (!id) id = sm.getLeafId() ?? "";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
pi.setLabel(id, params.name);
|
|
76
|
-
return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id}` }], details: {} };
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
pi.registerTool({
|
|
81
|
-
name: "context_log",
|
|
82
|
-
label: "Context Log",
|
|
83
|
-
description: "Show the entire history structure (status, message, tags, milestones). Analogous to 'git log --graph --oneline --decorate'",
|
|
84
|
-
parameters: ContextLogParams,
|
|
85
|
-
async execute(_id, params: Static<typeof ContextLogParams>, _signal, _onUpdate, ctx) {
|
|
86
|
-
const sm = ctx.sessionManager as SessionManager;
|
|
87
|
-
const branch = sm.getBranch();
|
|
88
|
-
const currentLeafId = sm.getLeafId();
|
|
89
|
-
const verbose = params.verbose ?? false;
|
|
90
|
-
const limit = params.limit ?? 50;
|
|
91
|
-
|
|
92
|
-
const backboneIds = new Set(branch.map((e) => e.id));
|
|
93
|
-
const sequence: SessionEntry[] = [];
|
|
94
|
-
|
|
95
|
-
branch.forEach((entry) => {
|
|
96
|
-
sequence.push(entry);
|
|
97
|
-
|
|
98
|
-
// Preserve side-summary logic: Show branch summaries/compactions that are off-path
|
|
99
|
-
const children = sm.getChildren(entry.id);
|
|
100
|
-
children.forEach((child) => {
|
|
101
|
-
if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
|
|
102
|
-
sequence.push(child);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const getMsgContent = (entry: SessionEntry): string => {
|
|
108
|
-
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
109
|
-
const e = entry;
|
|
110
|
-
return e.summary || "[No summary provided]";
|
|
111
|
-
}
|
|
112
|
-
if (entry.type === "label") {
|
|
113
|
-
return `tag: ${entry.label}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (entry.type === "message") {
|
|
117
|
-
const msg = entry.message;
|
|
118
|
-
|
|
119
|
-
if (msg.role === "toolResult") {
|
|
120
|
-
const tr = msg;
|
|
121
|
-
if (!verbose && isInternal(tr.toolName)) return "";
|
|
122
|
-
|
|
123
|
-
const extractText = (content: (TextContent | ImageContent)[]): string => {
|
|
124
|
-
return content
|
|
125
|
-
.map((p) => (p.type === "text" ? p.text : ""))
|
|
126
|
-
.join(" ")
|
|
127
|
-
.trim();
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
let resText = extractText(tr.content);
|
|
131
|
-
const details = tr.details as Record<string, unknown> | undefined;
|
|
132
|
-
if ((tr.toolName === "read" || tr.toolName === "edit") && details && "path" in details && typeof details.path === "string") {
|
|
133
|
-
resText = `${details.path}: ${resText}`;
|
|
134
|
-
}
|
|
135
|
-
return `(${tr.toolName}) ${resText}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (msg.role === "bashExecution") {
|
|
139
|
-
return `[Bash] ${msg.command}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (msg.role === "user" || msg.role === "assistant") {
|
|
143
|
-
let text = "";
|
|
144
|
-
if (typeof msg.content === "string") {
|
|
145
|
-
text = msg.content;
|
|
146
|
-
} else if (Array.isArray(msg.content)) {
|
|
147
|
-
text = msg.content
|
|
148
|
-
.map((p: any) => {
|
|
149
|
-
if (typeof p === "object" && p !== null && "text" in p) return (p as TextContent).text;
|
|
150
|
-
return "";
|
|
151
|
-
})
|
|
152
|
-
.join(" ")
|
|
153
|
-
.trim();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let toolCallsText = "";
|
|
157
|
-
if (msg.role === "assistant") {
|
|
158
|
-
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
159
|
-
|
|
160
|
-
toolCallsText = toolCalls
|
|
161
|
-
.filter((tc) => verbose || !isInternal(tc.name))
|
|
162
|
-
.map((tc) => `call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
|
|
163
|
-
.join("; ");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return [text, toolCallsText].filter(Boolean).join(" ");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return "";
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const isInteresting = (entry: SessionEntry): boolean => {
|
|
173
|
-
// 1. HEAD and Root
|
|
174
|
-
if (entry.id === currentLeafId) return true;
|
|
175
|
-
if (branch.length > 0 && entry.id === branch[0].id) return true;
|
|
176
|
-
|
|
177
|
-
// 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
|
|
178
|
-
if (sm.getLabel(entry.id)) return true;
|
|
179
|
-
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
180
|
-
|
|
181
|
-
// 3. Structural Milestones (Summaries)
|
|
182
|
-
if (entry.type === 'branch_summary' || entry.type === 'compaction') return true;
|
|
183
|
-
|
|
184
|
-
// 4. Branch Points (Forks)
|
|
185
|
-
if (sm.getChildren(entry.id).length > 1) return true;
|
|
186
|
-
|
|
187
|
-
// 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
|
|
188
|
-
if (entry.type === 'message' && entry.message.role === 'user') return true;
|
|
189
|
-
|
|
190
|
-
return false;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const visibleSequenceIds = new Set<string>();
|
|
194
|
-
sequence.forEach(e => {
|
|
195
|
-
if (verbose || isInteresting(e)) {
|
|
196
|
-
visibleSequenceIds.add(e.id);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
|
|
201
|
-
if (visibleEntries.length > limit) {
|
|
202
|
-
const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
|
|
203
|
-
visibleSequenceIds.clear();
|
|
204
|
-
allowedIds.forEach(id => visibleSequenceIds.add(id));
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const lines: string[] = [];
|
|
208
|
-
let hiddenCount = 0;
|
|
209
|
-
|
|
210
|
-
sequence.forEach((entry) => {
|
|
211
|
-
if (!visibleSequenceIds.has(entry.id)) {
|
|
212
|
-
hiddenCount++;
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (hiddenCount > 0) {
|
|
217
|
-
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
218
|
-
hiddenCount = 0;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const isHead = entry.id === currentLeafId;
|
|
222
|
-
const label = sm.getLabel(entry.id);
|
|
223
|
-
const content = getMsgContent(entry).replace(/\s+/g, " ");
|
|
224
|
-
|
|
225
|
-
let role = entry.type.toUpperCase();
|
|
226
|
-
if (entry.type === "message") {
|
|
227
|
-
const m = entry.message;
|
|
228
|
-
role =
|
|
229
|
-
m.role === "assistant"
|
|
230
|
-
? "AI"
|
|
231
|
-
: m.role === "user"
|
|
232
|
-
? "USER"
|
|
233
|
-
: m.role === "bashExecution"
|
|
234
|
-
? "BASH"
|
|
235
|
-
: "TOOL";
|
|
236
|
-
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
237
|
-
role = "SUMMARY";
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const id = entry.id;
|
|
241
|
-
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
242
|
-
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
243
|
-
|
|
244
|
-
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
245
|
-
|
|
246
|
-
const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
|
|
247
|
-
|
|
248
|
-
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
if (hiddenCount > 0) {
|
|
252
|
-
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// --- Context Dashboard (HUD) ---
|
|
256
|
-
const usage = await ctx.getContextUsage();
|
|
257
|
-
let usageStr = "Unknown";
|
|
258
|
-
if (usage) {
|
|
259
|
-
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Find the distance to the nearest tag
|
|
263
|
-
let stepsSinceTag = 0;
|
|
264
|
-
let nearestTagName = "None";
|
|
265
|
-
for (let i = branch.length - 1; i >= 0; i--) {
|
|
266
|
-
const id = branch[i].id;
|
|
267
|
-
const label = sm.getLabel(id);
|
|
268
|
-
if (label) {
|
|
269
|
-
nearestTagName = label;
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
272
|
-
stepsSinceTag++;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const hud = [
|
|
276
|
-
`[Context Dashboard]`,
|
|
277
|
-
`• Context Usage: ${usageStr}`,
|
|
278
|
-
`• Segment Size: ${stepsSinceTag} steps since last tag '${nearestTagName}'`,
|
|
279
|
-
`---------------------------------------------------`
|
|
280
|
-
].join("\n");
|
|
281
|
-
|
|
282
|
-
return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
pi.registerTool({
|
|
287
|
-
name: "context_checkout",
|
|
288
|
-
label: "Context Checkout",
|
|
289
|
-
description: "Navigate to ANY point in the conversation history. This checkout only resets *conversation history*, NOT disk files. ALWAYS provide a detailed 'message' to bridge context.",
|
|
290
|
-
parameters: ContextCheckoutParams,
|
|
291
|
-
async execute(_id, params: Static<typeof ContextCheckoutParams>, _signal, _onUpdate, ctx) {
|
|
292
|
-
const sm = ctx.sessionManager as SessionManager;
|
|
293
|
-
|
|
294
|
-
const tid = resolveTargetId(sm, params.target);
|
|
295
|
-
|
|
296
|
-
const currentLeaf = sm.getLeafId();
|
|
297
|
-
if (currentLeaf === tid) {
|
|
298
|
-
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
299
|
-
}
|
|
300
|
-
if (params.backupTag && currentLeaf) {
|
|
301
|
-
pi.setLabel(currentLeaf, params.backupTag);
|
|
302
|
-
}
|
|
303
|
-
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
304
|
-
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
|
|
305
|
-
|
|
306
|
-
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
307
|
-
await sm.branchWithSummary(tid, enrichedMessage);
|
|
308
|
-
|
|
309
|
-
return { content: [{ type: "text", text: `Checked out ${tid}\nBackup tag created: ${params.backupTag || "none"}\nmessage: ${enrichedMessage}` }], details: {} };
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
}
|
|
File without changes
|