pi-context 1.0.1 → 1.0.3

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 CHANGED
@@ -4,6 +4,8 @@ A Git-like context management tool that allows AI agents to proactively manage t
4
4
 
5
5
  Inspired by kimi-cli d-mail, implementing lossless time travel on the Pi session tree.
6
6
 
7
+ Blog: [中文](https://blog.xlab.app/p/6a966aeb/) [EN](https://blog.xlab.app/p/51d26495/)
8
+
7
9
  ## Installation
8
10
 
9
11
  ```bash
package/dist/index.js ADDED
@@ -0,0 +1,443 @@
1
+ import { DynamicBorder, } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { Container, Text, Spacer } from "@mariozechner/pi-tui";
4
+ const ContextLogParams = Type.Object({
5
+ limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
6
+ 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." })),
7
+ });
8
+ const ContextCheckoutParams = Type.Object({
9
+ 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." }),
10
+ 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]'" }),
11
+ 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." })),
12
+ });
13
+ const ContextTagParams = Type.Object({
14
+ name: Type.String({ description: "The tag/milestone name. Use meaningful names." }),
15
+ target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
16
+ });
17
+ const isInternal = (name) => ["context_tag", "context_log", "context_checkout"].includes(name);
18
+ const resolveTargetId = (sm, target) => {
19
+ if (target.toLowerCase() === "root") {
20
+ const tree = sm.getTree();
21
+ return tree.length > 0 ? tree[0].entry.id : target;
22
+ }
23
+ if (/^[0-9a-f]{8,}$/i.test(target))
24
+ return target;
25
+ const find = (nodes) => {
26
+ for (const n of nodes) {
27
+ if (sm.getLabel(n.entry.id) === target)
28
+ return n.entry.id;
29
+ const r = find(n.children);
30
+ if (r)
31
+ return r;
32
+ }
33
+ return null;
34
+ };
35
+ // sm.getTree() returns the SDK's SessionTreeNode[], which is structurally compatible
36
+ return find(sm.getTree()) || target;
37
+ };
38
+ const formatTokens = (n) => {
39
+ if (n >= 1_000_000)
40
+ return (n / 1_000_000).toFixed(1) + "M";
41
+ if (n >= 1_000)
42
+ return Math.round(n / 1_000) + "k";
43
+ return n.toString();
44
+ };
45
+ export default function (pi) {
46
+ pi.registerTool({
47
+ name: "context_tag",
48
+ label: "Context Tag",
49
+ 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'.",
50
+ parameters: ContextTagParams,
51
+ async execute(_id, params, _signal, _onUpdate, ctx) {
52
+ const sm = ctx.sessionManager;
53
+ let id = params.target ? resolveTargetId(sm, params.target) : undefined;
54
+ if (!id) {
55
+ // Auto-resolve: Find the last "interesting" node to tag.
56
+ // We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
57
+ const branch = sm.getBranch();
58
+ for (let i = branch.length - 1; i >= 0; i--) {
59
+ const entry = branch[i];
60
+ // 1. Check ToolResults
61
+ if (entry.type === 'message' && entry.message.role === 'toolResult') {
62
+ const tr = entry.message;
63
+ if (isInternal(tr.toolName))
64
+ continue;
65
+ // Public tool result is a valid target
66
+ id = entry.id;
67
+ break;
68
+ }
69
+ // 2. Check Assistant messages for visibility
70
+ if (entry.type === 'message' && entry.message.role === 'assistant') {
71
+ const m = entry.message;
72
+ let hasText = false;
73
+ let hasPublicTools = false;
74
+ const content = m.content;
75
+ if (typeof content === 'string') {
76
+ hasText = content.trim().length > 0;
77
+ }
78
+ else if (Array.isArray(content)) {
79
+ hasText = content.some((c) => c.type === 'text' && typeof c.text === 'string' && c.text.trim().length > 0);
80
+ const toolCalls = content.filter((c) => c.type === 'toolCall');
81
+ hasPublicTools = toolCalls.some((tc) => !isInternal(tc.name));
82
+ }
83
+ // If it has visible text or public tools, it's a valid target
84
+ if (hasText || hasPublicTools) {
85
+ id = entry.id;
86
+ break;
87
+ }
88
+ // Otherwise (only internal tools like context_tag), skip it
89
+ continue;
90
+ }
91
+ // 3. User messages, Summaries, etc. are always valid targets
92
+ id = entry.id;
93
+ break;
94
+ }
95
+ // Fallback to leaf if search failed
96
+ if (!id)
97
+ id = sm.getLeafId() ?? "";
98
+ }
99
+ pi.setLabel(id, params.name);
100
+ return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id.slice(0, 7)}` }], details: {} };
101
+ },
102
+ });
103
+ pi.registerTool({
104
+ name: "context_log",
105
+ label: "Context Log",
106
+ description: "Show the entire history structure (status, message, tags, milestones). Analogous to 'git log --graph --oneline --decorate'",
107
+ parameters: ContextLogParams,
108
+ async execute(_id, params, _signal, _onUpdate, ctx) {
109
+ const sm = ctx.sessionManager;
110
+ const branch = sm.getBranch();
111
+ const currentLeafId = sm.getLeafId();
112
+ const verbose = params.verbose ?? false;
113
+ const limit = params.limit ?? 50;
114
+ const backboneIds = new Set(branch.map((e) => e.id));
115
+ const sequence = [];
116
+ branch.forEach((entry) => {
117
+ sequence.push(entry);
118
+ // Preserve side-summary logic: Show branch summaries/compactions that are off-path
119
+ const children = sm.getChildren(entry.id);
120
+ children.forEach((child) => {
121
+ if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
122
+ sequence.push(child);
123
+ }
124
+ });
125
+ });
126
+ const getMsgContent = (entry) => {
127
+ if (entry.type === "branch_summary" || entry.type === "compaction") {
128
+ const e = entry;
129
+ return e.summary || "[No summary provided]";
130
+ }
131
+ if (entry.type === "label") {
132
+ return `tag: ${entry.label}`;
133
+ }
134
+ if (entry.type === "message") {
135
+ const msg = entry.message;
136
+ if (msg.role === "toolResult") {
137
+ const tr = msg;
138
+ if (!verbose && isInternal(tr.toolName))
139
+ return "";
140
+ const extractText = (content) => {
141
+ return content
142
+ .map((p) => (p.type === "text" ? p.text : ""))
143
+ .join(" ")
144
+ .trim();
145
+ };
146
+ let resText = extractText(tr.content);
147
+ const details = tr.details;
148
+ if ((tr.toolName === "read" || tr.toolName === "edit") && details && "path" in details && typeof details.path === "string") {
149
+ resText = `${details.path}: ${resText}`;
150
+ }
151
+ return `(${tr.toolName}) ${resText}`;
152
+ }
153
+ if (msg.role === "bashExecution") {
154
+ return `[Bash] ${msg.command}`;
155
+ }
156
+ if (msg.role === "user" || msg.role === "assistant") {
157
+ let text = "";
158
+ if (typeof msg.content === "string") {
159
+ text = msg.content;
160
+ }
161
+ else if (Array.isArray(msg.content)) {
162
+ text = msg.content
163
+ .map((p) => {
164
+ if (typeof p === "object" && p !== null && "text" in p)
165
+ return p.text;
166
+ return "";
167
+ })
168
+ .join(" ")
169
+ .trim();
170
+ }
171
+ let toolCallsText = "";
172
+ if (msg.role === "assistant") {
173
+ const toolCalls = msg.content.filter((c) => c.type === "toolCall");
174
+ toolCallsText = toolCalls
175
+ .filter((tc) => verbose || !isInternal(tc.name))
176
+ .map((tc) => `call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
177
+ .join("; ");
178
+ }
179
+ return [text, toolCallsText].filter(Boolean).join(" ");
180
+ }
181
+ }
182
+ return "";
183
+ };
184
+ const isInteresting = (entry) => {
185
+ // 1. HEAD and Root
186
+ if (entry.id === currentLeafId)
187
+ return true;
188
+ if (branch.length > 0 && entry.id === branch[0].id)
189
+ return true;
190
+ // 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
191
+ if (sm.getLabel(entry.id))
192
+ return true;
193
+ if (entry.type === 'label')
194
+ return false; // Hide label nodes, they are redundant
195
+ // 3. Structural Milestones (Summaries)
196
+ if (entry.type === 'branch_summary' || entry.type === 'compaction')
197
+ return true;
198
+ // 4. Branch Points (Forks)
199
+ if (sm.getChildren(entry.id).length > 1)
200
+ return true;
201
+ // 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
202
+ if (entry.type === 'message' && entry.message.role === 'user')
203
+ return true;
204
+ return false;
205
+ };
206
+ const visibleSequenceIds = new Set();
207
+ sequence.forEach(e => {
208
+ if (verbose || isInteresting(e)) {
209
+ visibleSequenceIds.add(e.id);
210
+ }
211
+ });
212
+ let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
213
+ if (visibleEntries.length > limit) {
214
+ const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
215
+ visibleSequenceIds.clear();
216
+ allowedIds.forEach(id => visibleSequenceIds.add(id));
217
+ }
218
+ const lines = [];
219
+ let hiddenCount = 0;
220
+ sequence.forEach((entry) => {
221
+ if (!visibleSequenceIds.has(entry.id)) {
222
+ hiddenCount++;
223
+ return;
224
+ }
225
+ if (hiddenCount > 0) {
226
+ lines.push(` : ... (${hiddenCount} hidden messages) ...`);
227
+ hiddenCount = 0;
228
+ }
229
+ const isHead = entry.id === currentLeafId;
230
+ const label = sm.getLabel(entry.id);
231
+ const content = getMsgContent(entry).replace(/\s+/g, " ");
232
+ let role = entry.type.toUpperCase();
233
+ if (entry.type === "message") {
234
+ const m = entry.message;
235
+ role =
236
+ m.role === "assistant"
237
+ ? "AI"
238
+ : m.role === "user"
239
+ ? "USER"
240
+ : m.role === "bashExecution"
241
+ ? "BASH"
242
+ : "TOOL";
243
+ }
244
+ else if (entry.type === "branch_summary" || entry.type === "compaction") {
245
+ role = "SUMMARY";
246
+ }
247
+ const id = entry.id;
248
+ const isRoot = branch.length > 0 && entry.id === branch[0].id;
249
+ const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
250
+ const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
251
+ const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
252
+ lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
253
+ });
254
+ if (hiddenCount > 0) {
255
+ lines.push(` : ... (${hiddenCount} hidden messages) ...`);
256
+ }
257
+ // --- Context Dashboard (HUD) ---
258
+ const usage = await ctx.getContextUsage();
259
+ let usageStr = "Unknown";
260
+ if (usage) {
261
+ usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
262
+ }
263
+ // Find the distance to the nearest tag
264
+ let stepsSinceTag = 0;
265
+ let nearestTagName = "None";
266
+ for (let i = branch.length - 1; i >= 0; i--) {
267
+ const id = branch[i].id;
268
+ const label = sm.getLabel(id);
269
+ if (label) {
270
+ nearestTagName = label;
271
+ break;
272
+ }
273
+ stepsSinceTag++;
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
+ return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
282
+ },
283
+ });
284
+ pi.registerTool({
285
+ name: "context_checkout",
286
+ label: "Context Checkout",
287
+ 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.",
288
+ parameters: ContextCheckoutParams,
289
+ async execute(_id, params, _signal, _onUpdate, ctx) {
290
+ const sm = ctx.sessionManager;
291
+ const tid = resolveTargetId(sm, params.target);
292
+ const currentLeaf = sm.getLeafId();
293
+ if (currentLeaf === tid) {
294
+ return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
295
+ }
296
+ if (params.backupTag && currentLeaf) {
297
+ pi.setLabel(currentLeaf, params.backupTag);
298
+ }
299
+ const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
300
+ const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
301
+ const enrichedMessage = `(summary from ${origin})\n${params.message}`;
302
+ await sm.branchWithSummary(tid, enrichedMessage);
303
+ return { content: [{ type: "text", text: `Checked out ${tid}\nBackup tag created: ${params.backupTag || "none"}\nmessage: ${enrichedMessage}` }], details: {} };
304
+ },
305
+ });
306
+ pi.registerCommand("context", {
307
+ description: "Show context usage visualization",
308
+ handler: async (args, ctx) => {
309
+ const usage = await ctx.getContextUsage();
310
+ if (!usage) {
311
+ ctx.ui.notify("Context usage info not available.", "warning");
312
+ return;
313
+ }
314
+ const sm = ctx.sessionManager;
315
+ const branch = sm.getBranch();
316
+ const systemPrompt = ctx.getSystemPrompt();
317
+ const tools = pi.getActiveTools();
318
+ const allTools = pi.getAllTools();
319
+ const activeToolDefs = allTools.filter(t => tools.includes(t.name));
320
+ const estimateTokens = (text) => Math.ceil(text.length / 4);
321
+ let msgTokensRaw = 0;
322
+ let toolUseTokensRaw = 0;
323
+ let toolResultTokensRaw = 0;
324
+ for (const entry of branch) {
325
+ if (entry.type === "message") {
326
+ const m = entry.message;
327
+ if (m.role === "user") {
328
+ if (typeof m.content === "string")
329
+ msgTokensRaw += estimateTokens(m.content);
330
+ else if (Array.isArray(m.content)) {
331
+ for (const p of m.content)
332
+ if (p.type === "text")
333
+ msgTokensRaw += estimateTokens(p.text);
334
+ }
335
+ }
336
+ else if (m.role === "assistant") {
337
+ if (typeof m.content === "string")
338
+ msgTokensRaw += estimateTokens(m.content);
339
+ else if (Array.isArray(m.content)) {
340
+ for (const p of m.content) {
341
+ if (p.type === "text")
342
+ msgTokensRaw += estimateTokens(p.text);
343
+ if (p.type === "toolCall")
344
+ toolUseTokensRaw += estimateTokens(JSON.stringify(p));
345
+ }
346
+ }
347
+ }
348
+ else if (m.role === "toolResult") {
349
+ if (Array.isArray(m.content)) {
350
+ for (const p of m.content)
351
+ if (p.type === "text")
352
+ toolResultTokensRaw += estimateTokens(p.text);
353
+ }
354
+ }
355
+ else if (m.role === "bashExecution") {
356
+ toolUseTokensRaw += estimateTokens(m.command || "");
357
+ }
358
+ }
359
+ else if (entry.type === "branch_summary" || entry.type === "compaction") {
360
+ msgTokensRaw += estimateTokens(entry.summary || "");
361
+ }
362
+ }
363
+ const systemTokensRaw = estimateTokens(systemPrompt);
364
+ const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
365
+ const totalActual = usage.tokens;
366
+ const limit = usage.contextWindow;
367
+ const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
368
+ const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
369
+ const systemTokens = Math.round(systemTokensRaw * ratio);
370
+ const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
371
+ const msgTokens = Math.round(msgTokensRaw * ratio);
372
+ const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
373
+ const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
374
+ await ctx.ui.custom((tui, theme, kb, done) => {
375
+ const container = new Container();
376
+ container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
377
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
378
+ container.addChild(new Spacer(1));
379
+ // Grouped by function and color
380
+ const categories = [
381
+ { label: "System Prompt", value: systemTokens, color: "muted" },
382
+ { label: "System Tools", value: toolDefTokens, color: "dim" },
383
+ { label: "Tool Call", value: toolUseTokens + toolResultTokens, color: "success" },
384
+ { label: "Messages", value: msgTokens, color: "accent" },
385
+ ];
386
+ const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
387
+ if (otherTokens > 10)
388
+ categories.push({ label: "Other", value: otherTokens, color: "dim" });
389
+ categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
390
+ const gridWidth = 10;
391
+ const gridHeight = 5;
392
+ const totalBlocks = gridWidth * gridHeight;
393
+ const blocks = [];
394
+ categories.forEach((cat) => {
395
+ if (cat.label === "Available")
396
+ return;
397
+ let count = Math.round((cat.value / limit) * totalBlocks);
398
+ if (count === 0 && cat.value > 0)
399
+ count = 1;
400
+ for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
401
+ blocks.push({ color: cat.color, filled: true });
402
+ }
403
+ });
404
+ while (blocks.length < totalBlocks) {
405
+ blocks.push({ color: "borderMuted", filled: false });
406
+ }
407
+ const gridLines = [];
408
+ for (let r = 0; r < gridHeight; r++) {
409
+ let rowStr = "";
410
+ for (let c = 0; c < gridWidth; c++) {
411
+ const b = blocks[r * gridWidth + c];
412
+ rowStr += theme.fg(b.color, b.filled ? "■ " : "□ ");
413
+ }
414
+ gridLines.push(rowStr.trimEnd());
415
+ }
416
+ 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)}%)`))}`;
417
+ const catDetailLines = categories.map(cat => {
418
+ const labelStr = cat.label.padEnd(14);
419
+ const valStr = formatTokens(cat.value).padStart(7);
420
+ const rowPercent = ((cat.value / limit) * 100).toFixed(1).padStart(5);
421
+ const icon = cat.label === "Available" ? "□" : "■";
422
+ return `${theme.fg(cat.color, icon)} ${theme.fg("text", labelStr)} ${theme.fg("accent", valStr)} (${rowPercent}%)`;
423
+ });
424
+ const allDetailLines = [totalUsageTitle, "", ...catDetailLines];
425
+ const leftSideWidth = 20;
426
+ const maxH = Math.max(gridLines.length, allDetailLines.length);
427
+ for (let i = 0; i < maxH; i++) {
428
+ const left = (gridLines[i] || "").padEnd(leftSideWidth);
429
+ const right = allDetailLines[i] || "";
430
+ container.addChild(new Text(` ${left} ${right}`, 1, 0));
431
+ }
432
+ container.addChild(new Spacer(1));
433
+ container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
434
+ container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
435
+ return {
436
+ render: (w) => container.render(w),
437
+ invalidate: () => container.invalidate(),
438
+ handleInput: (data) => done(undefined),
439
+ };
440
+ }, { overlay: true });
441
+ }
442
+ });
443
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-context",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "agent-driven context management tools",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,27 +16,23 @@
16
16
  "context-management",
17
17
  "context-optimization",
18
18
  "time-travel",
19
- "checkpoint"
19
+ "checkpoint",
20
+ "pi-skill"
20
21
  ],
21
22
  "type": "module",
22
23
  "author": "",
23
24
  "license": "MIT",
24
- "peerDependencies": {
25
- "@mariozechner/pi-coding-agent": "*"
26
- },
27
- "dependencies": {
28
- "@sinclair/typebox": "^0.34.13"
29
- },
30
25
  "devDependencies": {
26
+ "@sinclair/typebox": "^0.34.13",
31
27
  "typescript": "^5.7.2",
32
28
  "@mariozechner/pi-coding-agent": "latest"
33
29
  },
34
30
  "pi": {
35
31
  "extensions": [
36
- "src/index.ts"
32
+ "./src/index.ts"
37
33
  ],
38
34
  "skills": [
39
- "skills/context-management/SKILL.md"
35
+ "./skills"
40
36
  ],
41
37
  "image": "https://github.com/ttttmr/pi-context/raw/refs/heads/main/img/context.png"
42
38
  },
@@ -5,92 +5,124 @@ description: Strategies for efficient context management using context_log, cont
5
5
 
6
6
  # Context Management
7
7
 
8
- **CRITICAL: THIS SKILL MANAGES YOUR MEMORY (CONVERSATION HISTORY), NOT THE USER'S FILES.**
8
+ **CRITICAL: THIS SKILL MANAGES YOUR MEMORY. WITHOUT IT, YOU WILL FORGET.**
9
9
 
10
- Your context window is a limited resource. As the conversation grows, "pollution" (noise, failed attempts, intermediate logs) degrades your reasoning.
10
+ Your context window is limited. As conversations grow, "pollution" (noise, failed attempts) degrades your reasoning.
11
11
 
12
- **CRITICAL: YOU ARE RESPONSIBLE FOR YOUR CONTEXT STRUCTURE.**
13
- **DO NOT WAIT FOR THE USER TO TELL YOU TO MANAGE CONTEXT.**
12
+ **YOU MUST PROACTIVELY MANAGE YOUR HISTORY.**
13
+ Do not wait for the user to tell you.
14
14
 
15
- You must proactively use these tools to keep your reasoning sharp and your history structured.
15
+ ## The Core Philosophy: Build, Perceive, Navigate
16
16
 
17
- **Use this skill to optimize your "Context Economy":**
18
- 1. **Structure (Save Points)**: Use `context_tag` to bookmark stable states. **Untagged progress is risky.** Always tag before a major step.
19
- 2. **Monitor (Awareness)**: Use `context_log` to check your "Usage" and "Segment Size". **Don't fly blind.** High usage degrades your reasoning.
20
- 3. **Compress (Hygiene)**: Use `context_checkout` to squash history. **Finished tasks are noise.** Summarize them to reclaim memory.
21
-
22
- ## The Context Dashboard
23
-
24
- Call `context_log` to see your status. It provides a **HUD** (Health Metrics) and a **Graph** (Map).
25
-
26
- ### 1. The HUD (Health)
27
- ```text
28
- • Context Usage: 85% (108k/128k)
29
- • Segment Size: 84 steps since last tag
30
17
  ```
18
+ Context Window = RAM (Expensive, volatile, limited)
19
+ Context Graph = Disk (Cheap, persistent, unlimited)
31
20
 
32
- ### 2. The Graph (Map)
33
- The log tree shows where you are and where you've been.
34
- ```text
35
- | 1a2b3c4 (ROOT, tag: start) [USER] Start the project.
36
- | ... (50 hidden messages) ...
37
- | f9e8d7c (tag: task-a-done) [SUMMARY] Task A completed.
38
- | ... (5 hidden messages) ...
39
- * a1b2c3d (HEAD, tag: task-b-start) [AI] I have switched to Task B.
21
+ Move finished tasks from RAM to the Graph.
40
22
  ```
41
- - **`*` (Asterisk)**: Your current location (HEAD).
42
- - **`... (hidden)`**: Low-value steps (thinking/tools) are auto-collapsed.
43
- - **`tag: name`**: Safe checkpoints to jump back to.
44
- - **`[SUMMARY]`**: A compressed history node (squashed context).
45
23
 
46
- ## Decision Framework: When to Act?
47
-
48
- ### 1. Resource Perspective (The Dashboard)
49
- *Check the `context_log` HUD.*
50
-
51
- | Signal | Diagnosis | Prescription |
24
+ Manage your context window like a Git repository. You are the maintainer.
25
+
26
+ 1. **BUILD the Skeleton (`context_tag`)**:
27
+ * Raw conversation is a flat list. **Tags create structure.**
28
+ * Without tags, `context_log` is just a list of IDs. With tags, it is a **Map**.
29
+ 2. **PERCEIVE the State (`context_log`)**:
30
+ * Check the HUD: Is "Segment Size" too big? You are drifting.
31
+ * Check the Graph: Where are you? Are you in a deep branch?
32
+ 3. **NAVIGATE & MERGE (`context_checkout`)**:
33
+ * **Squash:** Convert a messy "feature branch" (thinking process) into a single "merge commit" (summary).
34
+ * **Jump:** Move between tasks or retry paths without carrying baggage.
35
+
36
+ ## Quick Start: The Loop
37
+
38
+ Follow this cycle for every major task:
39
+
40
+ 1. **CHECK:** Verify state.
41
+ `context_log`
42
+ 2. **START:** Tag the beginning.
43
+ `context_tag({ name: "task-start" })`
44
+ 3. **WORK:** Execute steps.
45
+ 4. **MILESTONE:** Tag intermediate stable states (e.g., "Plan done", "Part 1 done").
46
+ `context_tag({ name: "task-plan-done" })`
47
+ 5. **SQUASH (Autonomous):** If history becomes noisy or low-density, **Squash with Backup**.
48
+ * *Action:* `context_checkout({ target: "task-start", message: "Summarized debugging steps...", backupTag: "pre-squash-backup" })`
49
+ * *Action (Optional):* `context_tag({ name: "clean-state" })`
50
+ * *Safety:* If you need the details later, you can checkout `pre-squash-backup`.
51
+
52
+ ## Tool Reference
53
+
54
+ | Tool | Analog | Purpose | When to Use |
55
+ | :--- | :--- | :--- | :--- |
56
+ | `context_tag` | `git tag` | Bookmark a stable state. | Before risky changes. Before starting a new task. |
57
+ | `context_log` | `git log` | See where you are. | When you feel lost. To find IDs for checkout. |
58
+ | `context_checkout`| `git reset --soft` | **Time Travel / Squash.** | To undo mistakes. To compress history. |
59
+
60
+ ## Critical Rules
61
+
62
+ ### Tag Wisely (Build The Skeleton)
63
+ Tags are the "Table of Contents". Use them to mark **Start** and **Backups**.
64
+ * **Start:** `task-start`
65
+ * **Backup:** `task-raw-history` (Created automatically by `backupTag`)
66
+ * **Milestone:** `phase-1-done`
67
+
68
+
69
+ ### Squash Noise, Keep Signal, Focus on Goal (Context Hygiene)
70
+ Think of your conversation as a "Feature Branch" full of messy thoughts.
71
+ **You must distinguish Signal from Noise.**
72
+
73
+ * **Signal (High Value):** Design decisions, user constraints, final working code. -> **KEEP.**
74
+ * **Noise (Low Value):** Failed attempts, long tool outputs, "thinking" steps. -> **SQUASH.**
75
+ * **Focus on Goal:** Ask yourself: "Does this message help me achieve the current goal?" -> **KEEP.**
76
+
77
+ **When to Squash:**
78
+ 1. **Task Done:** Convert the messy process into one clean summary.
79
+ 2. **Low Density:** You read 2000 lines but only found 1 error.
80
+
81
+ **Safety:** Squashing is **LOSSLESS**.
82
+ By using `backupTag`, you save the "Messy Branch" forever. You can always checkout the backup tag if the summary isn't enough.
83
+ * **Main Trunk:** Jump back to the summary.
84
+ * **Backup Tag:** Jump back to the raw details.
85
+
86
+ ### Fail Fast, Revert Faster
87
+ If you fail 3 times:
88
+ 1. **STOP.** Don't try a 4th time.
89
+ 2. `context_checkout` back to the last safe tag.
90
+ 3. Summarize the failure in the checkout message ("Tried X, failed because Y").
91
+ 4. Try a new approach from the clean state.
92
+
93
+ ## Decision Matrix: When to Act
94
+
95
+ | Situation | Action | Reason |
52
96
  | :--- | :--- | :--- |
53
- | **Usage > 50%** | **ATTENTION DECAY**: Recall fades, instruction adherence drops, and reasoning quality suffers. | **COMPRESS**: `context_checkout({ target: "task-start", message: "Consolidated progress...", tagName: "clean-up" })` |
54
- | **Segment > 10** | **LINEAR FATIGUE**: Hard to distinguish signal from noise in long threads. | **TAG**: `context_tag({ name: "safe-point" })` |
55
- | **Reading Huge Files / Web Search** | **LOW INFO DENSITY**: You read 500 lines but only need 5 lines. | **EXTRACT & REWIND**: `context_checkout({ target: "pre-read", message: "File content summary: [Key Configs...]", tagName: "extracted-info" })` |
97
+ | **Starting Task** | `context_tag({ name: "task-X-start" })` | Create a rollback point. |
98
+ | **Research / Logs** | `context_checkout` (Squash) | **Process is Noise.** Read 2000 lines -> Keep result. |
99
+ | **Messy Debugging** | **Squash w/ Backup** | **Cleanup.** The error logs are noise once fixed. |
100
+ | **Task Done (Candidate)**| **Squash w/ Backup** | **Assume Success.** Summary is usually enough. Backup exists if not. |
101
+ | **Goal Shift** | `context_checkout` (Squash) | Old context is irrelevant. |
102
+ | **Drift (some steps w/o tag)** | **Tag (Milestone)** | Maintain the skeleton. Don't fly blind. |
56
103
 
57
- ### 2. Task Perspective (The Workflow)
58
- *Reflect on your current progress.*
104
+ ## The "Context Health" Check
59
105
 
60
- | Signal | Diagnosis | Prescription |
61
- | :--- | :--- | :--- |
62
- | **"I tried 3 times and failed"** | **POISONED CONTEXT**: Previous failures bias future attempts and eat up token budget. | **BACKTRACK**: `context_checkout({ target: "last-working-tag", message: "Approach A failed..." })` |
63
- | **"I think I finished the task"** | **PENDING**: User might ask for tweaks. | **TAG ONLY**: `context_tag({ name: "feature-x-candidate" })`. **Do not squash yet.** |
64
- | **"User starts a NEW task"** | **SAFE TO CLOSE**: Old context is now clutter. | **SQUASH**: `context_checkout({ target: "root", message: "Prev task done. Summary...", tagName: "clean-slate" })` |
106
+ If you cannot answer these, run `context_log`:
65
107
 
66
- ## Workflow Best Practices
108
+ | Question | Answer Source |
109
+ | :--- | :--- |
110
+ | **Where is the skeleton?** | The sequence of `tag`s in the log. |
111
+ | **Is this history useful?** | If "No" -> **SQUASH IT.** |
112
+ | **Am I in a loop?** | Repeated entries in the graph. |
67
113
 
68
- ### 1. Build the ToC (`context_tag`)
69
- Don't just work blindly. Tag your milestones so you (and the user) can see the structure.
70
- - *Good*: `start` -> `plan-v1` -> `impl-v1` -> `test-pass`
71
- - *Bad*: `start` -> (100 messages) -> `done`
72
114
 
73
- ### 2. View the Map (`context_log`)
74
- Check where you are and how expensive the path is.
75
- - Use `verbose: false` (default) to see the high-level "ToC" (Milestones).
76
-
77
- ### 3. Squash & Merge (`context_checkout`)
78
- **This is your Garbage Collector.** Use it to delete low-value history but keep high-value insights.
79
-
80
- > **CRITICAL REMINDER: CONTEXT IS NOT CODEBASE**
81
- > Checking out a new context branch **DOES NOT DELETE FILES**. It only resets your *conversation memory*. Your code on disk is safe.
82
-
83
- **Checkout Messages**
115
+ ## Good Checkout Messages
84
116
 
85
117
  The `message` is your lifeline to your past self.
86
118
  A good message preserves critical context that would otherwise be lost.
87
119
 
88
- Structure: `[Status] + [Reason] + [Important Changes] + [Carryover Data]`
120
+ Structure: `[Status] + [Reason] + [Carryover Data] + [Important Changes]`
89
121
 
90
122
  * **Status**: What did you just finish or stop doing?
91
123
  * **Reason**: Why are you branching/moving? (e.g., "Too much noise", "Task complete", "Failed attempt")
124
+ * **Carryover**: What specific details (e.g., IDs, file paths, user constraints) must be remembered?
92
125
  * **Important Changes**: What files or logic have been modified? (This checkout only resets *conversation history*, NOT disk files, so you must remember what changed.)
93
- * **Carryover**: What specific details (IDs, file paths, user constraints) must be remembered?
94
126
 
95
127
  Examples:
96
128
 
@@ -99,82 +131,89 @@ Examples:
99
131
  * *Bad*: "Switching context." (Too vague - you will forget why)
100
132
  * *Bad*: "Done." (What is done? What did we learn?)
101
133
 
102
- **Safety Net (Undo Button):**
103
- `context_checkout` is non-destructive. If you squash too much, use `context_log` to find the old branch ID and checkout back to it.
134
+ ## Anti-Patterns
104
135
 
105
- **Checkout Failures**
106
- If `context_checkout` fails with "Target not found":
107
- 1. Run `context_log` immediately to see available commit IDs and tags.
108
- 2. Select a valid ID from the log.
109
- 3. Retry the checkout.
136
+ | Don't | Do Instead |
137
+ | :--- | :--- |
138
+ | **Blind Tagging** (Tagging without looking) | **Check** (`context_log`) to avoid duplicates or tagging noise. |
139
+ | **Over-Tagging** (Tagging every step) | **Tag** only major phase changes (`start`, `milestone`). |
140
+ | **Hoard** (Keep all history "just in case") | **Squash** low-density history (research, logs). |
141
+ | **Panic** (Apologize repeatedly for errors) | **Revert** (`context_checkout`) to before the error. |
142
+ | **Blind Checkout** (Guessing IDs) | **Look** (`context_log`) first to get valid IDs. |
143
+ | **Vague Summaries** ("Done", "Fixed") | **Detailed Summaries** ("Found bug in line 40. Fixed with patch X.") |
110
144
 
111
- ## Scenarios
145
+ ## Recipes (Copy-Paste)
112
146
 
113
- **Scenario: Task Complete & Confirmed**
114
- You finished Feature A. **Wait for user confirmation.**
115
- *User*: "Looks good. Now let's work on Feature B."
116
- *Reasoning*: Feature A details are now noise. Feature B needs a clean slate.
147
+ ### 1. The "Miner" (Immediate Squash)
148
+ **Goal:** Pure information gathering (Reading files, Searching web).
149
+ **Why:** The *process* of searching is irrelevant. Only the *result* matters.
117
150
 
118
151
  ```javascript
119
- // 1. Tag the raw history (just in case)
120
- context_tag({ name: "feature-a-raw" });
152
+ // 1. Tag BEFORE starting the noisy work
153
+ context_tag({ name: "pre-research" });
154
+
155
+ // ... (Read 5 files, search 3 sites, find 1 key fact) ...
121
156
 
122
- // 2. Squash to Root (or previous milestone)
157
+ // 2. Squash IMMEDIATELY. Do not wait for user.
123
158
  context_checkout({
124
- target: "root", // or "project-start"
125
- message: "Feature A completed. All tests passed. Key files created: X, Y, Z.",
126
- tagName: "feature-a-done"
159
+ target: "pre-research",
160
+ message: "Researched logs. Found root cause: DB timeout. Irrelevant logs discarded.",
161
+ backupTag: "raw-research-logs" // Safety backup
127
162
  });
163
+ context_tag({ name: "research-done" });
128
164
  ```
129
- *Result*: You are now at a clean state with just the summary of Feature A. Cost dropped from 50 msgs to 1 msg.
130
165
 
131
- **Scenario: Pivot / Retry**
132
- You tried approach A and it failed 5 times (Pollution).
133
- *Solution*: Backtrack and summarize the failure.
166
+ ### 2. The "Candidate" (Wait for Confirmation)
167
+ **Goal:** You finished a complex task.
168
+ **Why:** The history is noisy. The result is clean.
169
+ **Safety:** We create a backup tag automatically.
134
170
 
135
171
  ```javascript
136
- // Return to the state BEFORE the failed attempts
172
+ // Squash to Summary (Optimistic Cleanup)
137
173
  context_checkout({
138
- target: "task-start",
139
- message: "Approach A failed due to library incompatibility. Context clean. Starting Approach B.",
140
- tagName: "retry-approach-b"
174
+ target: "feature-a-start", // Squash range: Start -> Now
175
+ message: "Feature A implemented. 5 files changed. Tests passed. Revert to 'feature-a-raw-history' for full logs.",
176
+ backupTag: "feature-a-raw-history"
141
177
  });
178
+ context_tag({ name: "feature-a-candidate" });
142
179
  ```
143
180
 
144
- **Scenario: Data Extraction / Research**
145
- You read a huge file (e.g., 2000 lines) or searched the web, but only found 3 relevant lines.
146
- *Problem*: The context is now polluted with 1997 lines of noise.
147
- *Solution*: Send the 3 lines back to your past self.
181
+ ### 3. The "Undo" (Revert Squash)
182
+ **Goal:** User asks about a detail you squashed away.
183
+ **Action:** Jump back to the backup tag.
148
184
 
149
185
  ```javascript
150
- // 1. Tag BEFORE you start reading/searching (proactive)
151
- context_tag({ name: "pre-research" });
186
+ // Jump back to the raw history
187
+ context_checkout({
188
+ target: "feature-a-raw-history",
189
+ message: "Reverting to raw history to check specific error logs."
190
+ });
191
+ context_tag({ name: "restored-history" });
192
+ ```
152
193
 
153
- // ... (You read the huge file and find the key config) ...
194
+ ### 4. Branching (Alternative Approach)
195
+ **Scenario:** Method A failed (and was squashed). You want to try Method B from the clean state.
196
+ **Action:** Checkout the start point.
154
197
 
155
- // 2. Checkout back to the tag, bringing ONLY the key info
198
+ ```javascript
199
+ // Jump back to start
156
200
  context_checkout({
157
- target: "pre-research",
158
- message: "Found the key configuration in line 150: 'MAX_CONNECTIONS=5'. The rest of the file was irrelevant.",
159
- tagName: "research-complete"
201
+ target: "task-start",
202
+ message: "Method A failed (see summary in 'method-a-fail'). Starting Method B from clean state."
160
203
  });
204
+ context_tag({ name: "method-b-start" });
161
205
  ```
162
- *Result*: Your context now contains the prompt, the tag, and the single message with the key config. The 2000 lines of noise are gone.
163
206
 
164
- **Scenario: Code Fix / "Time Travel" Debugging**
165
- You wrote code, it failed, you fixed it after 10 tries.
166
- *Problem*: Context has 10 failed attempts and error logs.
167
- *Solution*: Send the *final working code* back to the moment before you started coding.
207
+ ### 5. The "Undo" (Failed Attempt)
208
+ You tried to fix a bug but broke everything.
209
+ **Goal:** Clean up a failed path.
168
210
 
169
211
  ```javascript
170
- // 1. You realize you are in a messy debug loop.
171
- // 2. You finally get the code working.
172
- // 3. Checkout back to BEFORE you started the implementation.
173
-
212
+ // Jump back to safety.
174
213
  context_checkout({
175
- target: "impl-start",
176
- message: "Implementation successful. Here is the working code: \n```javascript\n...\n```\nSkipped the debugging steps.",
177
- tagName: "impl-done"
214
+ target: "pre-debug-tag",
215
+ message: "Attempted fix using Method A failed. Error: 'Timeout'.",
216
+ backupTag: "failed-attempt-1" // Save the failure just in case
178
217
  });
218
+ context_tag({ name: "debug-retry" });
179
219
  ```
180
- *Result*: It looks like you got it right on the first try. Context is clean and focused.
package/src/index.ts CHANGED
@@ -27,7 +27,7 @@ const ContextLogParams = Type.Object({
27
27
  const ContextCheckoutParams = Type.Object({
28
28
  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." }),
29
29
  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]'" }),
30
- tagName: Type.Optional(Type.String({ description: "Optional tag name to apply to the target state immediately after checking out." })),
30
+ 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
31
  });
32
32
 
33
33
  const ContextTagParams = Type.Object({
@@ -35,7 +35,7 @@ const ContextTagParams = Type.Object({
35
35
  target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
36
36
  });
37
37
 
38
- const isInternal = (name: string) => ["context_log", "context_checkout", "context_tag"].includes(name);
38
+ const isInternal = (name: string) => ["context_tag", "context_log", "context_checkout"].includes(name);
39
39
 
40
40
  const resolveTargetId = (sm: SessionManager, target: string): string => {
41
41
  if (target.toLowerCase() === "root") {
@@ -62,6 +62,55 @@ const formatTokens = (n: number) => {
62
62
  };
63
63
 
64
64
  export default function (pi: ExtensionAPI) {
65
+ pi.registerTool({
66
+ name: "context_tag",
67
+ label: "Context Tag",
68
+ 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'.",
69
+ parameters: ContextTagParams,
70
+ async execute(_id, params: Static<typeof ContextTagParams>, _signal, _onUpdate, ctx) {
71
+ const sm = ctx.sessionManager as SessionManager;
72
+ let id = params.target ? resolveTargetId(sm, params.target) : undefined;
73
+
74
+ if (!id) {
75
+ // Auto-resolve: Find the last "interesting" node to tag.
76
+ // We skip ToolResults (which look ugly tagged) and internal-only Assistant messages (which look empty).
77
+ const branch = sm.getBranch();
78
+ for (let i = branch.length - 1; i >= 0; i--) {
79
+ const entry = branch[i];
80
+
81
+ // 1. Check ToolResults
82
+ if (entry.type === 'message' && entry.message.role === 'toolResult') {
83
+ const tr = entry.message as any;
84
+ if (isInternal(tr.toolName)) continue;
85
+
86
+ // Public tool result is a valid target
87
+ id = entry.id;
88
+ break;
89
+ }
90
+
91
+ // 2. Check Assistant messages for visibility
92
+ if (entry.type === 'message' && entry.message.role === 'assistant') {
93
+ const m = entry.message;
94
+ const hasInternalTool = m.content.some(c => c.type === 'toolCall' && isInternal(c.name));
95
+
96
+ if (!hasInternalTool) {
97
+ id = entry.id;
98
+ break;
99
+ }
100
+ }
101
+
102
+ id = entry.id;
103
+ break;
104
+ }
105
+ // Fallback to leaf if search failed
106
+ if (!id) id = sm.getLeafId() ?? "";
107
+ }
108
+
109
+ pi.setLabel(id, params.name);
110
+ return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id}` }], details: {} };
111
+ },
112
+ });
113
+
65
114
  pi.registerTool({
66
115
  name: "context_log",
67
116
  label: "Context Log",
@@ -69,28 +118,25 @@ export default function (pi: ExtensionAPI) {
69
118
  parameters: ContextLogParams,
70
119
  async execute(_id, params: Static<typeof ContextLogParams>, _signal, _onUpdate, ctx) {
71
120
  const sm = ctx.sessionManager as SessionManager;
72
- const tree = sm.getTree();
121
+ const branch = sm.getBranch();
73
122
  const currentLeafId = sm.getLeafId();
74
123
  const verbose = params.verbose ?? false;
75
124
  const limit = params.limit ?? 50;
76
125
 
77
- const parents = new Map<string, string>();
78
- const entryMap = new Map<string, SessionEntry>();
79
- const childrenMap = new Map<string, SessionEntry[]>();
80
-
81
- const walk = (nodes: SessionTreeNode[], pId?: string) => {
82
- for (const n of nodes) {
83
- entryMap.set(n.entry.id, n.entry);
84
- if (pId) {
85
- parents.set(n.entry.id, pId);
86
- const siblings = childrenMap.get(pId) || [];
87
- siblings.push(n.entry);
88
- childrenMap.set(pId, siblings);
126
+ const backboneIds = new Set(branch.map((e) => e.id));
127
+ const sequence: SessionEntry[] = [];
128
+
129
+ branch.forEach((entry) => {
130
+ sequence.push(entry);
131
+
132
+ // Preserve side-summary logic: Show branch summaries/compactions that are off-path
133
+ const children = sm.getChildren(entry.id);
134
+ children.forEach((child) => {
135
+ if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.has(child.id)) {
136
+ sequence.push(child);
89
137
  }
90
- walk(n.children, n.entry.id);
91
- }
92
- };
93
- walk(tree);
138
+ });
139
+ });
94
140
 
95
141
  const getMsgContent = (entry: SessionEntry): string => {
96
142
  if (entry.type === "branch_summary" || entry.type === "compaction") {
@@ -157,40 +203,22 @@ export default function (pi: ExtensionAPI) {
157
203
  return "";
158
204
  };
159
205
 
160
- const backboneIds: string[] = [];
161
- let currId: string | undefined = currentLeafId ?? undefined;
162
- while (currId) {
163
- backboneIds.unshift(currId);
164
- currId = parents.get(currId);
165
- }
166
-
167
- const sequence: SessionEntry[] = [];
168
- backboneIds.forEach((id) => {
169
- const entry = entryMap.get(id);
170
- if (!entry) return;
171
- sequence.push(entry);
172
- const children = childrenMap.get(id) || [];
173
- children.forEach((child) => {
174
- if ((child.type === "branch_summary" || child.type === "compaction") && !backboneIds.includes(child.id)) {
175
- sequence.push(child);
176
- }
177
- });
178
- });
179
-
180
206
  const isInteresting = (entry: SessionEntry): boolean => {
181
207
  // 1. HEAD and Root
182
208
  if (entry.id === currentLeafId) return true;
183
- if (!parents.has(entry.id)) return true;
209
+ if (branch.length > 0 && entry.id === branch[0].id) return true;
184
210
 
185
211
  // 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
186
212
  if (sm.getLabel(entry.id)) return true;
187
213
  if (entry.type === 'label') return false; // Hide label nodes, they are redundant
188
214
 
189
- // 3. Structural Milestones (Summaries, Forks)
215
+ // 3. Structural Milestones (Summaries)
190
216
  if (entry.type === 'branch_summary' || entry.type === 'compaction') return true;
191
- if ((childrenMap.get(entry.id)?.length ?? 0) > 1) return true;
192
217
 
193
- // 4. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
218
+ // 4. Branch Points (Forks)
219
+ if (sm.getChildren(entry.id).length > 1) return true;
220
+
221
+ // 5. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
194
222
  if (entry.type === 'message' && entry.message.role === 'user') return true;
195
223
 
196
224
  return false;
@@ -243,8 +271,8 @@ export default function (pi: ExtensionAPI) {
243
271
  role = "SUMMARY";
244
272
  }
245
273
 
246
- const id = entry.id.slice(0, 8);
247
- const isRoot = !parents.has(entry.id);
274
+ const id = entry.id;
275
+ const isRoot = branch.length > 0 && entry.id === branch[0].id;
248
276
  const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
249
277
 
250
278
  const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
@@ -259,9 +287,6 @@ export default function (pi: ExtensionAPI) {
259
287
  }
260
288
 
261
289
  // --- Context Dashboard (HUD) ---
262
- const totalNodes = entryMap.size;
263
- const currentDepth = backboneIds.length;
264
-
265
290
  const usage = await ctx.getContextUsage();
266
291
  let usageStr = "Unknown";
267
292
  if (usage) {
@@ -271,8 +296,8 @@ export default function (pi: ExtensionAPI) {
271
296
  // Find the distance to the nearest tag
272
297
  let stepsSinceTag = 0;
273
298
  let nearestTagName = "None";
274
- for (let i = backboneIds.length - 1; i >= 0; i--) {
275
- const id = backboneIds[i];
299
+ for (let i = branch.length - 1; i >= 0; i--) {
300
+ const id = branch[i].id;
276
301
  const label = sm.getLabel(id);
277
302
  if (label) {
278
303
  nearestTagName = label;
@@ -299,40 +324,26 @@ export default function (pi: ExtensionAPI) {
299
324
  parameters: ContextCheckoutParams,
300
325
  async execute(_id, params: Static<typeof ContextCheckoutParams>, _signal, _onUpdate, ctx) {
301
326
  const sm = ctx.sessionManager as SessionManager;
327
+
302
328
  const tid = resolveTargetId(sm, params.target);
303
329
 
304
330
  const currentLeaf = sm.getLeafId();
305
331
  if (currentLeaf === tid) {
306
- return { content: [{ type: "text", text: `Already at target ${tid.slice(0, 7)}` }], details: {} };
332
+ return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
333
+ }
334
+ if (params.backupTag && currentLeaf) {
335
+ pi.setLabel(currentLeaf, params.backupTag);
307
336
  }
308
-
309
337
  const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
310
- const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf ? currentLeaf.slice(0, 8) : "unknown");
338
+ const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf || "unknown");
311
339
 
312
- const enrichedMessage = `${params.message} (branched from ${origin})`;
340
+ const enrichedMessage = `(summary from ${origin})\n${params.message}`;
313
341
  await sm.branchWithSummary(tid, enrichedMessage);
314
342
 
315
- // Fix: Label the NEW leaf (the summary node or the checkout target), not necessarily the old target ID.
316
- // This ensures 'tagName' acts like naming the NEW branch tip.
317
- const newLeaf = sm.getLeafId();
318
- if (params.tagName && newLeaf) pi.setLabel(newLeaf, params.tagName);
319
-
320
- return { content: [{ type: "text", text: `Checked out ${tid.slice(0, 7)}` }], details: {} };
343
+ return { content: [{ type: "text", text: `Checked out ${tid}\nBackup tag created: ${params.backupTag || "none"}\nmessage: ${enrichedMessage}` }], details: {} };
321
344
  },
322
345
  });
323
346
 
324
- pi.registerTool({
325
- name: "context_tag",
326
- label: "Context Tag",
327
- 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'.",
328
- parameters: ContextTagParams,
329
- async execute(_id, params: Static<typeof ContextTagParams>, _signal, _onUpdate, ctx) {
330
- const sm = ctx.sessionManager as SessionManager;
331
- const id = params.target ? resolveTargetId(sm, params.target) : (sm.getLeafId() ?? "");
332
- pi.setLabel(id, params.name);
333
- return { content: [{ type: "text", text: `Created tag '${params.name}' at ${id.slice(0, 7)}` }], details: {} };
334
- },
335
- });
336
347
 
337
348
  pi.registerCommand("context", {
338
349
  description: "Show context usage visualization",