pi-context 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +4 -3
- package/src/context.ts +153 -0
- package/src/index.ts +358 -440
- package/src/utils.ts +5 -0
- package/dist/index.js +0 -443
package/src/index.ts
CHANGED
|
@@ -1,491 +1,409 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
DynamicBorder,
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type SessionManager,
|
|
4
|
+
type SessionEntry,
|
|
6
5
|
} from "@mariozechner/pi-coding-agent";
|
|
7
6
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
TextContent,
|
|
8
|
+
ImageContent,
|
|
9
|
+
ToolCall,
|
|
11
10
|
} from "@mariozechner/pi-ai";
|
|
12
11
|
import { Type, type Static } from "@sinclair/typebox";
|
|
13
|
-
import {
|
|
12
|
+
import { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { formatTokens } from "./utils.js";
|
|
14
14
|
|
|
15
15
|
// Define missing types locally as they are not exported from the main entry point
|
|
16
16
|
interface SessionTreeNode {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
entry: SessionEntry;
|
|
18
|
+
children: SessionTreeNode[];
|
|
19
|
+
label?: string;
|
|
20
20
|
}
|
|
21
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
|
+
|
|
22
46
|
const ContextLogParams = Type.Object({
|
|
23
|
-
|
|
24
|
-
|
|
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." })),
|
|
25
49
|
});
|
|
26
50
|
|
|
27
51
|
const ContextCheckoutParams = Type.Object({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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." })),
|
|
31
55
|
});
|
|
32
56
|
|
|
33
57
|
const ContextTagParams = Type.Object({
|
|
34
|
-
|
|
35
|
-
|
|
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)." })),
|
|
36
60
|
});
|
|
37
61
|
|
|
38
|
-
const isInternal = (name: string) => ["context_tag", "context_log", "context_checkout"].includes(name);
|
|
39
|
-
|
|
40
|
-
const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
41
|
-
if (target.toLowerCase() === "root") {
|
|
42
|
-
const tree = sm.getTree();
|
|
43
|
-
return tree.length > 0 ? tree[0].entry.id : target;
|
|
44
|
-
}
|
|
45
|
-
if (/^[0-9a-f]{8,}$/i.test(target)) return target;
|
|
46
|
-
const find = (nodes: SessionTreeNode[]): string | null => {
|
|
47
|
-
for (const n of nodes) {
|
|
48
|
-
if (sm.getLabel(n.entry.id) === target) return n.entry.id;
|
|
49
|
-
const r = find(n.children);
|
|
50
|
-
if (r) return r;
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
};
|
|
54
|
-
// sm.getTree() returns the SDK's SessionTreeNode[], which is structurally compatible
|
|
55
|
-
return find(sm.getTree()) || target;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const formatTokens = (n: number) => {
|
|
59
|
-
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
60
|
-
if (n >= 1_000) return Math.round(n / 1_000) + "k";
|
|
61
|
-
return n.toString();
|
|
62
|
-
};
|
|
63
|
-
|
|
64
62
|
export default function (pi: ExtensionAPI) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
63
|
+
pi.registerCommand("acm", {
|
|
64
|
+
description: "Enable agentic context management for the current session",
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
CommandCtx = ctx;
|
|
67
|
+
pi.setActiveTools(pi.getActiveTools().concat(InternalTools));
|
|
68
|
+
ctx.ui.notify("Agentic Context Management enabled.", "info");
|
|
69
|
+
pi.sendMessage({
|
|
70
|
+
customType: "pi-context",
|
|
71
|
+
content: "read context-management skill",
|
|
72
|
+
display: false,
|
|
73
|
+
}, {
|
|
74
|
+
deliverAs: "followUp"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
pi.registerTool({
|
|
80
|
+
name: "context_tag",
|
|
81
|
+
label: "Context Tag",
|
|
82
|
+
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'.",
|
|
83
|
+
parameters: ContextTagParams,
|
|
84
|
+
async execute(_id, params: Static<typeof ContextTagParams>, _signal, _onUpdate, ctx) {
|
|
85
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
86
|
+
let id = params.target ? resolveTargetId(sm, params.target) : undefined;
|
|
87
|
+
|
|
88
|
+
if (!id) {
|
|
89
|
+
// Auto-resolve: Find the last "interesting" node to tag.
|
|
90
|
+
// We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
|
|
91
|
+
const branch = sm.getBranch();
|
|
92
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
93
|
+
const entry = branch[i];
|
|
94
|
+
|
|
95
|
+
// 1. Check ToolResults
|
|
96
|
+
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
|
97
|
+
const tr = entry.message as any;
|
|
98
|
+
if (isInternal(tr.toolName)) continue;
|
|
99
|
+
|
|
100
|
+
// Public tool result is a valid target
|
|
101
|
+
id = entry.id;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Check Assistant messages for visibility
|
|
106
|
+
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
|
107
|
+
const m = entry.message;
|
|
108
|
+
const hasInternalTool = m.content.some(c => c.type === 'toolCall' && isInternal(c.name));
|
|
109
|
+
|
|
110
|
+
if (!hasInternalTool) {
|
|
111
|
+
id = entry.id;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
id = entry.id;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
// Fallback to leaf if search failed
|
|
120
|
+
if (!id) id = sm.getLeafId() ?? "";
|
|
99
121
|
}
|
|
100
|
-
}
|
|
101
122
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
pi.setLabel(id, params.name);
|
|
124
|
+
return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id}` }], details: {} };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
pi.registerTool({
|
|
129
|
+
name: "context_log",
|
|
130
|
+
label: "Context Log",
|
|
131
|
+
description: "Show the entire history structure (status, message, tags, milestones). Analogous to 'git log --graph --oneline --decorate'",
|
|
132
|
+
parameters: ContextLogParams,
|
|
133
|
+
async execute(_id, params: Static<typeof ContextLogParams>, _signal, _onUpdate, ctx) {
|
|
134
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
135
|
+
const branch = sm.getBranch();
|
|
136
|
+
const currentLeafId = sm.getLeafId();
|
|
137
|
+
const verbose = params.verbose ?? false;
|
|
138
|
+
const limit = params.limit ?? 50;
|
|
139
|
+
|
|
140
|
+
const backboneIds = new Set(branch.map((e) => e.id));
|
|
141
|
+
const sequence: SessionEntry[] = [];
|
|
142
|
+
|
|
143
|
+
branch.forEach((entry) => {
|
|
144
|
+
sequence.push(entry);
|
|
145
|
+
|
|
146
|
+
// Preserve side-summary logic: Show branch summaries/compactions that are off-path
|
|
147
|
+
const children = sm.getChildren(entry.id);
|
|
148
|
+
children.forEach((child) => {
|
|
149
|
+
if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
|
|
150
|
+
sequence.push(child);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const getMsgContent = (entry: SessionEntry): string => {
|
|
156
|
+
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
157
|
+
const e = entry;
|
|
158
|
+
return e.summary || "[No summary provided]";
|
|
159
|
+
}
|
|
160
|
+
if (entry.type === "label") {
|
|
161
|
+
return `tag: ${entry.label}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (entry.type === "message") {
|
|
165
|
+
const msg = entry.message;
|
|
166
|
+
|
|
167
|
+
if (msg.role === "toolResult") {
|
|
168
|
+
const tr = msg;
|
|
169
|
+
if (!verbose && isInternal(tr.toolName)) return "";
|
|
170
|
+
|
|
171
|
+
const extractText = (content: (TextContent | ImageContent)[]): string => {
|
|
172
|
+
return content
|
|
173
|
+
.map((p) => (p.type === "text" ? p.text : ""))
|
|
174
|
+
.join(" ")
|
|
175
|
+
.trim();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
let resText = extractText(tr.content);
|
|
179
|
+
const details = tr.details as Record<string, unknown> | undefined;
|
|
180
|
+
if ((tr.toolName === "read" || tr.toolName === "edit") && details && "path" in details && typeof details.path === "string") {
|
|
181
|
+
resText = `${details.path}: ${resText}`;
|
|
182
|
+
}
|
|
183
|
+
return `(${tr.toolName}) ${resText}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (msg.role === "bashExecution") {
|
|
187
|
+
return `[Bash] ${msg.command}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
191
|
+
let text = "";
|
|
192
|
+
if (typeof msg.content === "string") {
|
|
193
|
+
text = msg.content;
|
|
194
|
+
} else if (Array.isArray(msg.content)) {
|
|
195
|
+
text = msg.content
|
|
196
|
+
.map((p: any) => {
|
|
197
|
+
if (typeof p === "object" && p !== null && "text" in p) return (p as TextContent).text;
|
|
198
|
+
return "";
|
|
199
|
+
})
|
|
200
|
+
.join(" ")
|
|
201
|
+
.trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let toolCallsText = "";
|
|
205
|
+
if (msg.role === "assistant") {
|
|
206
|
+
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
207
|
+
|
|
208
|
+
toolCallsText = toolCalls
|
|
209
|
+
.filter((tc) => verbose || !isInternal(tc.name))
|
|
210
|
+
.map((tc) => `call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
|
|
211
|
+
.join("; ");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return [text, toolCallsText].filter(Boolean).join(" ");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return "";
|
|
218
|
+
};
|
|
140
219
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
220
|
+
const isInteresting = (entry: SessionEntry): boolean => {
|
|
221
|
+
// 1. HEAD and Root
|
|
222
|
+
if (entry.id === currentLeafId) return true;
|
|
223
|
+
if (branch.length > 0 && entry.id === branch[0].id) return true;
|
|
224
|
+
|
|
225
|
+
// 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
|
|
226
|
+
if (sm.getLabel(entry.id)) return true;
|
|
227
|
+
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
149
228
|
|
|
150
|
-
|
|
151
|
-
|
|
229
|
+
// 3. Structural Milestones (Summaries)
|
|
230
|
+
if (entry.type === 'branch_summary' || entry.type === 'compaction') return true;
|
|
152
231
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (!verbose && isInternal(tr.toolName)) return "";
|
|
232
|
+
// 4. Branch Points (Forks)
|
|
233
|
+
if (sm.getChildren(entry.id).length > 1) return true;
|
|
156
234
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.trim();
|
|
235
|
+
// 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
|
|
236
|
+
if (entry.type === 'message' && entry.message.role === 'user') return true;
|
|
237
|
+
|
|
238
|
+
return false;
|
|
162
239
|
};
|
|
163
240
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (msg.role === "user" || msg.role === "assistant") {
|
|
177
|
-
let text = "";
|
|
178
|
-
if (typeof msg.content === "string") {
|
|
179
|
-
text = msg.content;
|
|
180
|
-
} else if (Array.isArray(msg.content)) {
|
|
181
|
-
text = msg.content
|
|
182
|
-
.map((p: any) => {
|
|
183
|
-
if (typeof p === "object" && p !== null && "text" in p) return (p as TextContent).text;
|
|
184
|
-
return "";
|
|
185
|
-
})
|
|
186
|
-
.join(" ")
|
|
187
|
-
.trim();
|
|
241
|
+
const visibleSequenceIds = new Set<string>();
|
|
242
|
+
sequence.forEach(e => {
|
|
243
|
+
if (verbose || isInteresting(e)) {
|
|
244
|
+
visibleSequenceIds.add(e.id);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
|
|
249
|
+
if (visibleEntries.length > limit) {
|
|
250
|
+
const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
|
|
251
|
+
visibleSequenceIds.clear();
|
|
252
|
+
allowedIds.forEach(id => visibleSequenceIds.add(id));
|
|
188
253
|
}
|
|
189
254
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
255
|
+
const lines: string[] = [];
|
|
256
|
+
let hiddenCount = 0;
|
|
257
|
+
|
|
258
|
+
sequence.forEach((entry) => {
|
|
259
|
+
if (!visibleSequenceIds.has(entry.id)) {
|
|
260
|
+
hiddenCount++;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (hiddenCount > 0) {
|
|
265
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
266
|
+
hiddenCount = 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const isHead = entry.id === currentLeafId;
|
|
270
|
+
const label = sm.getLabel(entry.id);
|
|
271
|
+
const content = getMsgContent(entry).replace(/\s+/g, " ");
|
|
272
|
+
|
|
273
|
+
let role = entry.type.toUpperCase();
|
|
274
|
+
if (entry.type === "message") {
|
|
275
|
+
const m = entry.message;
|
|
276
|
+
role =
|
|
277
|
+
m.role === "assistant"
|
|
278
|
+
? "AI"
|
|
279
|
+
: m.role === "user"
|
|
280
|
+
? "USER"
|
|
281
|
+
: m.role === "bashExecution"
|
|
282
|
+
? "BASH"
|
|
283
|
+
: "TOOL";
|
|
284
|
+
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
285
|
+
role = "SUMMARY";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// hide custom messages
|
|
289
|
+
if (role === "CUSTOM_MESSAGE") {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const id = entry.id;
|
|
294
|
+
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
295
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
296
|
+
|
|
297
|
+
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
298
|
+
|
|
299
|
+
const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
|
|
300
|
+
|
|
301
|
+
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (hiddenCount > 0) {
|
|
305
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
306
|
+
}
|
|
193
307
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
308
|
+
// --- Context Dashboard (HUD) ---
|
|
309
|
+
const usage = await ctx.getContextUsage();
|
|
310
|
+
let usageStr = "Unknown";
|
|
311
|
+
if (usage) {
|
|
312
|
+
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
198
313
|
}
|
|
199
314
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
315
|
+
// Find the distance to the nearest tag
|
|
316
|
+
let stepsSinceTag = 0;
|
|
317
|
+
let nearestTagName = "None";
|
|
318
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
319
|
+
const id = branch[i].id;
|
|
320
|
+
const label = sm.getLabel(id);
|
|
321
|
+
if (label) {
|
|
322
|
+
nearestTagName = label;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
stepsSinceTag++;
|
|
326
|
+
}
|
|
205
327
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
328
|
+
const hud = [
|
|
329
|
+
`[Context Dashboard]`,
|
|
330
|
+
`• Context Usage: ${usageStr}`,
|
|
331
|
+
`• Segment Size: ${stepsSinceTag} steps since last tag '${nearestTagName}'`,
|
|
332
|
+
`---------------------------------------------------`
|
|
333
|
+
].join("\n");
|
|
334
|
+
|
|
335
|
+
return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
pi.registerTool({
|
|
340
|
+
name: "context_checkout",
|
|
341
|
+
label: "Context Checkout",
|
|
342
|
+
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.",
|
|
343
|
+
parameters: ContextCheckoutParams,
|
|
344
|
+
async execute(_id, params: Static<typeof ContextCheckoutParams>, _signal, _onUpdate, ctx) {
|
|
345
|
+
if (!CommandCtx) {
|
|
346
|
+
return { content: [{ type: "text", text: "Command context not available, require /acm command first." }], details: {} };
|
|
347
|
+
}
|
|
348
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
210
349
|
|
|
211
|
-
|
|
212
|
-
if (sm.getLabel(entry.id)) return true;
|
|
213
|
-
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
350
|
+
const tid = resolveTargetId(sm, params.target);
|
|
214
351
|
|
|
215
|
-
|
|
216
|
-
|
|
352
|
+
const currentLeaf = sm.getLeafId();
|
|
353
|
+
if (currentLeaf === tid) {
|
|
354
|
+
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
355
|
+
}
|
|
356
|
+
if (params.backupTag && currentLeaf) {
|
|
357
|
+
pi.setLabel(currentLeaf, params.backupTag);
|
|
358
|
+
}
|
|
359
|
+
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
360
|
+
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
|
|
217
361
|
|
|
218
|
-
|
|
219
|
-
if (sm.getChildren(entry.id).length > 1) return true;
|
|
362
|
+
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
220
363
|
|
|
221
|
-
|
|
222
|
-
|
|
364
|
+
const nid = await sm.branchWithSummary(tid, enrichedMessage);
|
|
365
|
+
CheckoutParams = params;
|
|
366
|
+
CheckoutParams.nid = nid;
|
|
367
|
+
CheckoutParams.tid = tid;
|
|
368
|
+
CheckoutParams.enrichedMessage = enrichedMessage;
|
|
223
369
|
|
|
224
|
-
|
|
225
|
-
|
|
370
|
+
return { content: [{ type: "text", text: "checkout start" }], details: {} };
|
|
371
|
+
},
|
|
372
|
+
});
|
|
226
373
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
visibleSequenceIds.add(e.id);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
|
|
235
|
-
if (visibleEntries.length > limit) {
|
|
236
|
-
const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
|
|
237
|
-
visibleSequenceIds.clear();
|
|
238
|
-
allowedIds.forEach(id => visibleSequenceIds.add(id));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const lines: string[] = [];
|
|
242
|
-
let hiddenCount = 0;
|
|
243
|
-
|
|
244
|
-
sequence.forEach((entry) => {
|
|
245
|
-
if (!visibleSequenceIds.has(entry.id)) {
|
|
246
|
-
hiddenCount++;
|
|
247
|
-
return;
|
|
374
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
375
|
+
if (!CheckoutParams) {
|
|
376
|
+
return
|
|
248
377
|
}
|
|
378
|
+
ctx.abort()
|
|
379
|
+
});
|
|
249
380
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
381
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
382
|
+
if (!CheckoutParams) {
|
|
383
|
+
return
|
|
253
384
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const label = sm.getLabel(entry.id);
|
|
257
|
-
const content = getMsgContent(entry).replace(/\s+/g, " ");
|
|
258
|
-
|
|
259
|
-
let role = entry.type.toUpperCase();
|
|
260
|
-
if (entry.type === "message") {
|
|
261
|
-
const m = entry.message;
|
|
262
|
-
role =
|
|
263
|
-
m.role === "assistant"
|
|
264
|
-
? "AI"
|
|
265
|
-
: m.role === "user"
|
|
266
|
-
? "USER"
|
|
267
|
-
: m.role === "bashExecution"
|
|
268
|
-
? "BASH"
|
|
269
|
-
: "TOOL";
|
|
270
|
-
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
271
|
-
role = "SUMMARY";
|
|
385
|
+
if (!CommandCtx) {
|
|
386
|
+
return
|
|
272
387
|
}
|
|
273
388
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
277
|
-
|
|
278
|
-
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
279
|
-
|
|
280
|
-
const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
|
|
281
|
-
|
|
282
|
-
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
if (hiddenCount > 0) {
|
|
286
|
-
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// --- Context Dashboard (HUD) ---
|
|
290
|
-
const usage = await ctx.getContextUsage();
|
|
291
|
-
let usageStr = "Unknown";
|
|
292
|
-
if (usage) {
|
|
293
|
-
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Find the distance to the nearest tag
|
|
297
|
-
let stepsSinceTag = 0;
|
|
298
|
-
let nearestTagName = "None";
|
|
299
|
-
for (let i = branch.length - 1; i >= 0; i--) {
|
|
300
|
-
const id = branch[i].id;
|
|
301
|
-
const label = sm.getLabel(id);
|
|
302
|
-
if (label) {
|
|
303
|
-
nearestTagName = label;
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
stepsSinceTag++;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const hud = [
|
|
310
|
-
`[Context Dashboard]`,
|
|
311
|
-
`• Context Usage: ${usageStr}`,
|
|
312
|
-
`• Segment Size: ${stepsSinceTag} steps since last tag '${nearestTagName}'`,
|
|
313
|
-
`---------------------------------------------------`
|
|
314
|
-
].join("\n");
|
|
315
|
-
|
|
316
|
-
return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
pi.registerTool({
|
|
321
|
-
name: "context_checkout",
|
|
322
|
-
label: "Context Checkout",
|
|
323
|
-
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.",
|
|
324
|
-
parameters: ContextCheckoutParams,
|
|
325
|
-
async execute(_id, params: Static<typeof ContextCheckoutParams>, _signal, _onUpdate, ctx) {
|
|
326
|
-
const sm = ctx.sessionManager as SessionManager;
|
|
327
|
-
|
|
328
|
-
const tid = resolveTargetId(sm, params.target);
|
|
329
|
-
|
|
330
|
-
const currentLeaf = sm.getLeafId();
|
|
331
|
-
if (currentLeaf === tid) {
|
|
332
|
-
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
333
|
-
}
|
|
334
|
-
if (params.backupTag && currentLeaf) {
|
|
335
|
-
pi.setLabel(currentLeaf, params.backupTag);
|
|
336
|
-
}
|
|
337
|
-
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
338
|
-
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
|
|
339
|
-
|
|
340
|
-
const enrichedMessage = `(summary from ${origin})\n${params.message}`;
|
|
341
|
-
await sm.branchWithSummary(tid, enrichedMessage);
|
|
342
|
-
|
|
343
|
-
return { content: [{ type: "text", text: `Checked out ${tid}\nBackup tag created: ${params.backupTag || "none"}\nmessage: ${enrichedMessage}` }], details: {} };
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
pi.registerCommand("context", {
|
|
349
|
-
description: "Show context usage visualization",
|
|
350
|
-
handler: async (args, ctx) => {
|
|
351
|
-
const usage = await ctx.getContextUsage();
|
|
352
|
-
if (!usage) {
|
|
353
|
-
ctx.ui.notify("Context usage info not available.", "warning");
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const sm = ctx.sessionManager as SessionManager;
|
|
358
|
-
const branch = sm.getBranch();
|
|
359
|
-
const systemPrompt = ctx.getSystemPrompt();
|
|
360
|
-
const tools = pi.getActiveTools();
|
|
361
|
-
const allTools = pi.getAllTools();
|
|
362
|
-
const activeToolDefs = allTools.filter(t => tools.includes(t.name));
|
|
363
|
-
|
|
364
|
-
const estimateTokens = (text: string) => Math.ceil(text.length / 4);
|
|
365
|
-
|
|
366
|
-
let msgTokensRaw = 0;
|
|
367
|
-
let toolUseTokensRaw = 0;
|
|
368
|
-
let toolResultTokensRaw = 0;
|
|
369
|
-
|
|
370
|
-
for (const entry of branch) {
|
|
371
|
-
if (entry.type === "message") {
|
|
372
|
-
const m = entry.message;
|
|
373
|
-
if (m.role === "user") {
|
|
374
|
-
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
375
|
-
else if (Array.isArray(m.content)) {
|
|
376
|
-
for (const p of m.content) if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
377
|
-
}
|
|
378
|
-
} else if (m.role === "assistant") {
|
|
379
|
-
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
380
|
-
else if (Array.isArray(m.content)) {
|
|
381
|
-
for (const p of m.content) {
|
|
382
|
-
if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
383
|
-
if (p.type === "toolCall") toolUseTokensRaw += estimateTokens(JSON.stringify(p));
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} else if (m.role === "toolResult") {
|
|
387
|
-
if (Array.isArray(m.content)) {
|
|
388
|
-
for (const p of m.content) if (p.type === "text") toolResultTokensRaw += estimateTokens(p.text);
|
|
389
|
-
}
|
|
390
|
-
} else if (m.role === "bashExecution") {
|
|
391
|
-
toolUseTokensRaw += estimateTokens(m.command || "");
|
|
392
|
-
}
|
|
393
|
-
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
394
|
-
msgTokensRaw += estimateTokens(entry.summary || "");
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const systemTokensRaw = estimateTokens(systemPrompt);
|
|
399
|
-
const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
400
|
-
const totalActual = usage.tokens;
|
|
401
|
-
const limit = usage.contextWindow;
|
|
402
|
-
|
|
403
|
-
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
404
|
-
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
405
|
-
|
|
406
|
-
const systemTokens = Math.round(systemTokensRaw * ratio);
|
|
407
|
-
const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
|
|
408
|
-
const msgTokens = Math.round(msgTokensRaw * ratio);
|
|
409
|
-
const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
|
|
410
|
-
const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
|
|
411
|
-
|
|
412
|
-
await ctx.ui.custom((tui, theme, kb, done) => {
|
|
413
|
-
const container = new Container();
|
|
414
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
415
|
-
container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
|
|
416
|
-
container.addChild(new Spacer(1));
|
|
417
|
-
|
|
418
|
-
// Grouped by function and color
|
|
419
|
-
const categories = [
|
|
420
|
-
{ label: "System Prompt", value: systemTokens, color: "muted" },
|
|
421
|
-
{ label: "System Tools", value: toolDefTokens, color: "dim" },
|
|
422
|
-
{ label: "Tool Call", value: toolUseTokens + toolResultTokens, color: "success" },
|
|
423
|
-
{ label: "Messages", value: msgTokens, color: "accent" },
|
|
424
|
-
];
|
|
425
|
-
|
|
426
|
-
const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
|
|
427
|
-
if (otherTokens > 10) categories.push({ label: "Other", value: otherTokens, color: "dim" });
|
|
428
|
-
|
|
429
|
-
categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
|
|
430
|
-
|
|
431
|
-
const gridWidth = 10;
|
|
432
|
-
const gridHeight = 5;
|
|
433
|
-
const totalBlocks = gridWidth * gridHeight;
|
|
434
|
-
|
|
435
|
-
const blocks: { color: string, filled: boolean }[] = [];
|
|
436
|
-
categories.forEach((cat) => {
|
|
437
|
-
if (cat.label === "Available") return;
|
|
438
|
-
let count = Math.round((cat.value / limit) * totalBlocks);
|
|
439
|
-
if (count === 0 && cat.value > 0) count = 1;
|
|
440
|
-
for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
|
|
441
|
-
blocks.push({ color: cat.color, filled: true });
|
|
442
|
-
}
|
|
389
|
+
await CommandCtx.navigateTree(CheckoutParams.nid, {
|
|
390
|
+
summarize: false,
|
|
443
391
|
});
|
|
444
392
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const gridLines: string[] = [];
|
|
450
|
-
for (let r = 0; r < gridHeight; r++) {
|
|
451
|
-
let rowStr = "";
|
|
452
|
-
for (let c = 0; c < gridWidth; c++) {
|
|
453
|
-
const b = blocks[r * gridWidth + c];
|
|
454
|
-
rowStr += theme.fg(b.color as any, b.filled ? "■ " : "□ ");
|
|
455
|
-
}
|
|
456
|
-
gridLines.push(rowStr.trimEnd());
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const totalUsageTitle = `${theme.fg("text", theme.bold("Total Usage".padEnd(16)))} ${theme.fg("text", theme.bold(formatTokens(totalActual).padStart(7)))} ${theme.fg("text", theme.bold(`(${usage.percent.toFixed(1).padStart(5)}%)`))}`;
|
|
393
|
+
ctx.ui.notify(`Checked out ${CheckoutParams.target}${CheckoutParams.target === CheckoutParams.tid ? "" : `(${CheckoutParams.tid})`}\nBackup tag created: ${CheckoutParams.backupTag || "none"}\nmessage: ${CheckoutParams.enrichedMessage}`, "info");
|
|
394
|
+
CheckoutParams = null;
|
|
460
395
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
396
|
+
pi.sendMessage({
|
|
397
|
+
customType: "pi-context",
|
|
398
|
+
content: "context_checkout done, continue",
|
|
399
|
+
display: false,
|
|
400
|
+
}, {
|
|
401
|
+
triggerTurn: true,
|
|
467
402
|
});
|
|
403
|
+
});
|
|
468
404
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
for (let i = 0; i < maxH; i++) {
|
|
474
|
-
const left = (gridLines[i] || "").padEnd(leftSideWidth);
|
|
475
|
-
const right = allDetailLines[i] || "";
|
|
476
|
-
container.addChild(new Text(` ${left} ${right}`, 1, 0));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
container.addChild(new Spacer(1));
|
|
480
|
-
container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
|
|
481
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
482
|
-
|
|
483
|
-
return {
|
|
484
|
-
render: (w) => container.render(w),
|
|
485
|
-
invalidate: () => container.invalidate(),
|
|
486
|
-
handleInput: (data) => done(undefined),
|
|
487
|
-
};
|
|
488
|
-
}, { overlay: true });
|
|
489
|
-
}
|
|
490
|
-
});
|
|
405
|
+
pi.on("session_start", () => {
|
|
406
|
+
// hide tools by default
|
|
407
|
+
pi.setActiveTools(pi.getActiveTools().filter(n => !InternalTools.includes(n)));
|
|
408
|
+
});
|
|
491
409
|
}
|