pi-context 1.0.0 → 1.0.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/package.json +11 -5
- package/skills/context-management/SKILL.md +180 -0
- package/src/index.ts +480 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -215
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "agent-driven context management tools",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
8
|
+
"src",
|
|
9
|
+
"skills",
|
|
10
|
+
"README.md"
|
|
9
11
|
],
|
|
10
12
|
"scripts": {},
|
|
11
13
|
"keywords": [
|
|
@@ -31,11 +33,15 @@
|
|
|
31
33
|
},
|
|
32
34
|
"pi": {
|
|
33
35
|
"extensions": [
|
|
34
|
-
"
|
|
36
|
+
"src/index.ts"
|
|
35
37
|
],
|
|
36
38
|
"skills": [
|
|
37
|
-
"
|
|
39
|
+
"skills/context-management/SKILL.md"
|
|
38
40
|
],
|
|
39
41
|
"image": "https://github.com/ttttmr/pi-context/raw/refs/heads/main/img/context.png"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/ttttmr/pi-context.git"
|
|
40
46
|
}
|
|
41
|
-
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: context-management
|
|
3
|
+
description: Strategies for efficient context management using context_log, context_tag, and context_checkout. Learn when to tag, how to visualize the graph, and safe ways to squash history. Use for complex refactoring, debugging, and long conversations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Context Management
|
|
7
|
+
|
|
8
|
+
**CRITICAL: THIS SKILL MANAGES YOUR MEMORY (CONVERSATION HISTORY), NOT THE USER'S FILES.**
|
|
9
|
+
|
|
10
|
+
Your context window is a limited resource. As the conversation grows, "pollution" (noise, failed attempts, intermediate logs) degrades your reasoning.
|
|
11
|
+
|
|
12
|
+
**CRITICAL: YOU ARE RESPONSIBLE FOR YOUR CONTEXT STRUCTURE.**
|
|
13
|
+
**DO NOT WAIT FOR THE USER TO TELL YOU TO MANAGE CONTEXT.**
|
|
14
|
+
|
|
15
|
+
You must proactively use these tools to keep your reasoning sharp and your history structured.
|
|
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
|
+
```
|
|
31
|
+
|
|
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.
|
|
40
|
+
```
|
|
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
|
+
|
|
46
|
+
## Decision Framework: When to Act?
|
|
47
|
+
|
|
48
|
+
### 1. Resource Perspective (The Dashboard)
|
|
49
|
+
*Check the `context_log` HUD.*
|
|
50
|
+
|
|
51
|
+
| Signal | Diagnosis | Prescription |
|
|
52
|
+
| :--- | :--- | :--- |
|
|
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" })` |
|
|
56
|
+
|
|
57
|
+
### 2. Task Perspective (The Workflow)
|
|
58
|
+
*Reflect on your current progress.*
|
|
59
|
+
|
|
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" })` |
|
|
65
|
+
|
|
66
|
+
## Workflow Best Practices
|
|
67
|
+
|
|
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
|
+
|
|
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**
|
|
84
|
+
|
|
85
|
+
The `message` is your lifeline to your past self.
|
|
86
|
+
A good message preserves critical context that would otherwise be lost.
|
|
87
|
+
|
|
88
|
+
Structure: `[Status] + [Reason] + [Important Changes] + [Carryover Data]`
|
|
89
|
+
|
|
90
|
+
* **Status**: What did you just finish or stop doing?
|
|
91
|
+
* **Reason**: Why are you branching/moving? (e.g., "Too much noise", "Task complete", "Failed attempt")
|
|
92
|
+
* **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
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
|
|
97
|
+
* *Good (Resetting after failure)*: "Abandoning the recursive approach (infinite loop). Switching back to iterative. **Important Changes**: Modified `utils/recursion.ts`. **Carryover**: The test case `test_retry_logic` is the one failing."
|
|
98
|
+
* *Good (Cleaning up)*: "Completed authentication module. All tests passed. **Important Changes**: Created `auth/` directory and updated `routes.ts`. **Carryover**: The user ID is stored in `localStorage` under `auth_token`. Moving to Dashboard UI."
|
|
99
|
+
* *Bad*: "Switching context." (Too vague - you will forget why)
|
|
100
|
+
* *Bad*: "Done." (What is done? What did we learn?)
|
|
101
|
+
|
|
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.
|
|
104
|
+
|
|
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.
|
|
110
|
+
|
|
111
|
+
## Scenarios
|
|
112
|
+
|
|
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.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
// 1. Tag the raw history (just in case)
|
|
120
|
+
context_tag({ name: "feature-a-raw" });
|
|
121
|
+
|
|
122
|
+
// 2. Squash to Root (or previous milestone)
|
|
123
|
+
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"
|
|
127
|
+
});
|
|
128
|
+
```
|
|
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
|
+
|
|
131
|
+
**Scenario: Pivot / Retry**
|
|
132
|
+
You tried approach A and it failed 5 times (Pollution).
|
|
133
|
+
*Solution*: Backtrack and summarize the failure.
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
// Return to the state BEFORE the failed attempts
|
|
137
|
+
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"
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
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.
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
// 1. Tag BEFORE you start reading/searching (proactive)
|
|
151
|
+
context_tag({ name: "pre-research" });
|
|
152
|
+
|
|
153
|
+
// ... (You read the huge file and find the key config) ...
|
|
154
|
+
|
|
155
|
+
// 2. Checkout back to the tag, bringing ONLY the key info
|
|
156
|
+
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"
|
|
160
|
+
});
|
|
161
|
+
```
|
|
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
|
+
|
|
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.
|
|
168
|
+
|
|
169
|
+
```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
|
+
|
|
174
|
+
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"
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
*Result*: It looks like you got it right on the first try. Context is clean and focused.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type SessionEntry,
|
|
4
|
+
type SessionManager,
|
|
5
|
+
DynamicBorder,
|
|
6
|
+
} from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import type {
|
|
8
|
+
ToolCall,
|
|
9
|
+
TextContent,
|
|
10
|
+
ImageContent,
|
|
11
|
+
} from "@mariozechner/pi-ai";
|
|
12
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
13
|
+
import { Container, Text, Spacer } from "@mariozechner/pi-tui";
|
|
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 ContextLogParams = Type.Object({
|
|
23
|
+
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
24
|
+
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
|
+
});
|
|
26
|
+
|
|
27
|
+
const ContextCheckoutParams = Type.Object({
|
|
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
|
+
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." })),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ContextTagParams = Type.Object({
|
|
34
|
+
name: Type.String({ description: "The tag/milestone name. Use meaningful names." }),
|
|
35
|
+
target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const isInternal = (name: string) => ["context_log", "context_checkout", "context_tag"].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
|
+
export default function (pi: ExtensionAPI) {
|
|
65
|
+
pi.registerTool({
|
|
66
|
+
name: "context_log",
|
|
67
|
+
label: "Context Log",
|
|
68
|
+
description: "Show the entire history structure (status, message, tags, milestones). Analogous to 'git log --graph --oneline --decorate'",
|
|
69
|
+
parameters: ContextLogParams,
|
|
70
|
+
async execute(_id, params: Static<typeof ContextLogParams>, _signal, _onUpdate, ctx) {
|
|
71
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
72
|
+
const tree = sm.getTree();
|
|
73
|
+
const currentLeafId = sm.getLeafId();
|
|
74
|
+
const verbose = params.verbose ?? false;
|
|
75
|
+
const limit = params.limit ?? 50;
|
|
76
|
+
|
|
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);
|
|
89
|
+
}
|
|
90
|
+
walk(n.children, n.entry.id);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
walk(tree);
|
|
94
|
+
|
|
95
|
+
const getMsgContent = (entry: SessionEntry): string => {
|
|
96
|
+
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
97
|
+
const e = entry;
|
|
98
|
+
return e.summary || "[No summary provided]";
|
|
99
|
+
}
|
|
100
|
+
if (entry.type === "label") {
|
|
101
|
+
return `tag: ${entry.label}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (entry.type === "message") {
|
|
105
|
+
const msg = entry.message;
|
|
106
|
+
|
|
107
|
+
if (msg.role === "toolResult") {
|
|
108
|
+
const tr = msg;
|
|
109
|
+
if (!verbose && isInternal(tr.toolName)) return "";
|
|
110
|
+
|
|
111
|
+
const extractText = (content: (TextContent | ImageContent)[]): string => {
|
|
112
|
+
return content
|
|
113
|
+
.map((p) => (p.type === "text" ? p.text : ""))
|
|
114
|
+
.join(" ")
|
|
115
|
+
.trim();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let resText = extractText(tr.content);
|
|
119
|
+
const details = tr.details as Record<string, unknown> | undefined;
|
|
120
|
+
if ((tr.toolName === "read" || tr.toolName === "edit") && details && "path" in details && typeof details.path === "string") {
|
|
121
|
+
resText = `${details.path}: ${resText}`;
|
|
122
|
+
}
|
|
123
|
+
return `(${tr.toolName}) ${resText}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (msg.role === "bashExecution") {
|
|
127
|
+
return `[Bash] ${msg.command}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
131
|
+
let text = "";
|
|
132
|
+
if (typeof msg.content === "string") {
|
|
133
|
+
text = msg.content;
|
|
134
|
+
} else if (Array.isArray(msg.content)) {
|
|
135
|
+
text = msg.content
|
|
136
|
+
.map((p: any) => {
|
|
137
|
+
if (typeof p === "object" && p !== null && "text" in p) return (p as TextContent).text;
|
|
138
|
+
return "";
|
|
139
|
+
})
|
|
140
|
+
.join(" ")
|
|
141
|
+
.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let toolCallsText = "";
|
|
145
|
+
if (msg.role === "assistant") {
|
|
146
|
+
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
147
|
+
|
|
148
|
+
toolCallsText = toolCalls
|
|
149
|
+
.filter((tc) => verbose || !isInternal(tc.name))
|
|
150
|
+
.map((tc) => `call: ${tc.name}(${JSON.stringify(tc.arguments)})`)
|
|
151
|
+
.join("; ");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [text, toolCallsText].filter(Boolean).join(" ");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return "";
|
|
158
|
+
};
|
|
159
|
+
|
|
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
|
+
const isInteresting = (entry: SessionEntry): boolean => {
|
|
181
|
+
// 1. HEAD and Root
|
|
182
|
+
if (entry.id === currentLeafId) return true;
|
|
183
|
+
if (!parents.has(entry.id)) return true;
|
|
184
|
+
|
|
185
|
+
// 2. Explicit Tags (Labels) - Only show the TAGGED node, not the label node itself
|
|
186
|
+
if (sm.getLabel(entry.id)) return true;
|
|
187
|
+
if (entry.type === 'label') return false; // Hide label nodes, they are redundant
|
|
188
|
+
|
|
189
|
+
// 3. Structural Milestones (Summaries, Forks)
|
|
190
|
+
if (entry.type === 'branch_summary' || entry.type === 'compaction') return true;
|
|
191
|
+
if ((childrenMap.get(entry.id)?.length ?? 0) > 1) return true;
|
|
192
|
+
|
|
193
|
+
// 4. Natural Milestones (User Messages) - This is the key auto-tagging mechanism
|
|
194
|
+
if (entry.type === 'message' && entry.message.role === 'user') return true;
|
|
195
|
+
|
|
196
|
+
return false;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const visibleSequenceIds = new Set<string>();
|
|
200
|
+
sequence.forEach(e => {
|
|
201
|
+
if (verbose || isInteresting(e)) {
|
|
202
|
+
visibleSequenceIds.add(e.id);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
let visibleEntries = sequence.filter(e => visibleSequenceIds.has(e.id));
|
|
207
|
+
if (visibleEntries.length > limit) {
|
|
208
|
+
const allowedIds = new Set(visibleEntries.slice(-limit).map(e => e.id));
|
|
209
|
+
visibleSequenceIds.clear();
|
|
210
|
+
allowedIds.forEach(id => visibleSequenceIds.add(id));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
let hiddenCount = 0;
|
|
215
|
+
|
|
216
|
+
sequence.forEach((entry) => {
|
|
217
|
+
if (!visibleSequenceIds.has(entry.id)) {
|
|
218
|
+
hiddenCount++;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (hiddenCount > 0) {
|
|
223
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
224
|
+
hiddenCount = 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const isHead = entry.id === currentLeafId;
|
|
228
|
+
const label = sm.getLabel(entry.id);
|
|
229
|
+
const content = getMsgContent(entry).replace(/\s+/g, " ");
|
|
230
|
+
|
|
231
|
+
let role = entry.type.toUpperCase();
|
|
232
|
+
if (entry.type === "message") {
|
|
233
|
+
const m = entry.message;
|
|
234
|
+
role =
|
|
235
|
+
m.role === "assistant"
|
|
236
|
+
? "AI"
|
|
237
|
+
: m.role === "user"
|
|
238
|
+
? "USER"
|
|
239
|
+
: m.role === "bashExecution"
|
|
240
|
+
? "BASH"
|
|
241
|
+
: "TOOL";
|
|
242
|
+
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
243
|
+
role = "SUMMARY";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const id = entry.id.slice(0, 8);
|
|
247
|
+
const isRoot = !parents.has(entry.id);
|
|
248
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `tag: ${label}` : null].filter(Boolean).join(", ");
|
|
249
|
+
|
|
250
|
+
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
251
|
+
|
|
252
|
+
const marker = isHead ? "*" : (role === "USER" ? "•" : "|");
|
|
253
|
+
|
|
254
|
+
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (hiddenCount > 0) {
|
|
258
|
+
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Context Dashboard (HUD) ---
|
|
262
|
+
const totalNodes = entryMap.size;
|
|
263
|
+
const currentDepth = backboneIds.length;
|
|
264
|
+
|
|
265
|
+
const usage = await ctx.getContextUsage();
|
|
266
|
+
let usageStr = "Unknown";
|
|
267
|
+
if (usage) {
|
|
268
|
+
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Find the distance to the nearest tag
|
|
272
|
+
let stepsSinceTag = 0;
|
|
273
|
+
let nearestTagName = "None";
|
|
274
|
+
for (let i = backboneIds.length - 1; i >= 0; i--) {
|
|
275
|
+
const id = backboneIds[i];
|
|
276
|
+
const label = sm.getLabel(id);
|
|
277
|
+
if (label) {
|
|
278
|
+
nearestTagName = label;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
stepsSinceTag++;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const hud = [
|
|
285
|
+
`[Context Dashboard]`,
|
|
286
|
+
`• Context Usage: ${usageStr}`,
|
|
287
|
+
`• Segment Size: ${stepsSinceTag} steps since last tag '${nearestTagName}'`,
|
|
288
|
+
`---------------------------------------------------`
|
|
289
|
+
].join("\n");
|
|
290
|
+
|
|
291
|
+
return { content: [{ type: "text", text: hud + "\n" + (lines.join("\n") || "(Root Path Only)") }], details: {} };
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
pi.registerTool({
|
|
296
|
+
name: "context_checkout",
|
|
297
|
+
label: "Context Checkout",
|
|
298
|
+
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.",
|
|
299
|
+
parameters: ContextCheckoutParams,
|
|
300
|
+
async execute(_id, params: Static<typeof ContextCheckoutParams>, _signal, _onUpdate, ctx) {
|
|
301
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
302
|
+
const tid = resolveTargetId(sm, params.target);
|
|
303
|
+
|
|
304
|
+
const currentLeaf = sm.getLeafId();
|
|
305
|
+
if (currentLeaf === tid) {
|
|
306
|
+
return { content: [{ type: "text", text: `Already at target ${tid.slice(0, 7)}` }], details: {} };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
310
|
+
const origin = currentLabel ? `tag: ${currentLabel}` : (currentLeaf ? currentLeaf.slice(0, 8) : "unknown");
|
|
311
|
+
|
|
312
|
+
const enrichedMessage = `${params.message} (branched from ${origin})`;
|
|
313
|
+
await sm.branchWithSummary(tid, enrichedMessage);
|
|
314
|
+
|
|
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: {} };
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
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
|
+
|
|
337
|
+
pi.registerCommand("context", {
|
|
338
|
+
description: "Show context usage visualization",
|
|
339
|
+
handler: async (args, ctx) => {
|
|
340
|
+
const usage = await ctx.getContextUsage();
|
|
341
|
+
if (!usage) {
|
|
342
|
+
ctx.ui.notify("Context usage info not available.", "warning");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
347
|
+
const branch = sm.getBranch();
|
|
348
|
+
const systemPrompt = ctx.getSystemPrompt();
|
|
349
|
+
const tools = pi.getActiveTools();
|
|
350
|
+
const allTools = pi.getAllTools();
|
|
351
|
+
const activeToolDefs = allTools.filter(t => tools.includes(t.name));
|
|
352
|
+
|
|
353
|
+
const estimateTokens = (text: string) => Math.ceil(text.length / 4);
|
|
354
|
+
|
|
355
|
+
let msgTokensRaw = 0;
|
|
356
|
+
let toolUseTokensRaw = 0;
|
|
357
|
+
let toolResultTokensRaw = 0;
|
|
358
|
+
|
|
359
|
+
for (const entry of branch) {
|
|
360
|
+
if (entry.type === "message") {
|
|
361
|
+
const m = entry.message;
|
|
362
|
+
if (m.role === "user") {
|
|
363
|
+
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
364
|
+
else if (Array.isArray(m.content)) {
|
|
365
|
+
for (const p of m.content) if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
366
|
+
}
|
|
367
|
+
} else if (m.role === "assistant") {
|
|
368
|
+
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
369
|
+
else if (Array.isArray(m.content)) {
|
|
370
|
+
for (const p of m.content) {
|
|
371
|
+
if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
372
|
+
if (p.type === "toolCall") toolUseTokensRaw += estimateTokens(JSON.stringify(p));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} else if (m.role === "toolResult") {
|
|
376
|
+
if (Array.isArray(m.content)) {
|
|
377
|
+
for (const p of m.content) if (p.type === "text") toolResultTokensRaw += estimateTokens(p.text);
|
|
378
|
+
}
|
|
379
|
+
} else if (m.role === "bashExecution") {
|
|
380
|
+
toolUseTokensRaw += estimateTokens(m.command || "");
|
|
381
|
+
}
|
|
382
|
+
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
383
|
+
msgTokensRaw += estimateTokens(entry.summary || "");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const systemTokensRaw = estimateTokens(systemPrompt);
|
|
388
|
+
const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
389
|
+
const totalActual = usage.tokens;
|
|
390
|
+
const limit = usage.contextWindow;
|
|
391
|
+
|
|
392
|
+
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
393
|
+
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
394
|
+
|
|
395
|
+
const systemTokens = Math.round(systemTokensRaw * ratio);
|
|
396
|
+
const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
|
|
397
|
+
const msgTokens = Math.round(msgTokensRaw * ratio);
|
|
398
|
+
const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
|
|
399
|
+
const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
|
|
400
|
+
|
|
401
|
+
await ctx.ui.custom((tui, theme, kb, done) => {
|
|
402
|
+
const container = new Container();
|
|
403
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
404
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
|
|
405
|
+
container.addChild(new Spacer(1));
|
|
406
|
+
|
|
407
|
+
// Grouped by function and color
|
|
408
|
+
const categories = [
|
|
409
|
+
{ label: "System Prompt", value: systemTokens, color: "muted" },
|
|
410
|
+
{ label: "System Tools", value: toolDefTokens, color: "dim" },
|
|
411
|
+
{ label: "Tool Call", value: toolUseTokens + toolResultTokens, color: "success" },
|
|
412
|
+
{ label: "Messages", value: msgTokens, color: "accent" },
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
|
|
416
|
+
if (otherTokens > 10) categories.push({ label: "Other", value: otherTokens, color: "dim" });
|
|
417
|
+
|
|
418
|
+
categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
|
|
419
|
+
|
|
420
|
+
const gridWidth = 10;
|
|
421
|
+
const gridHeight = 5;
|
|
422
|
+
const totalBlocks = gridWidth * gridHeight;
|
|
423
|
+
|
|
424
|
+
const blocks: { color: string, filled: boolean }[] = [];
|
|
425
|
+
categories.forEach((cat) => {
|
|
426
|
+
if (cat.label === "Available") return;
|
|
427
|
+
let count = Math.round((cat.value / limit) * totalBlocks);
|
|
428
|
+
if (count === 0 && cat.value > 0) count = 1;
|
|
429
|
+
for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
|
|
430
|
+
blocks.push({ color: cat.color, filled: true });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
while (blocks.length < totalBlocks) {
|
|
435
|
+
blocks.push({ color: "borderMuted", filled: false });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const gridLines: string[] = [];
|
|
439
|
+
for (let r = 0; r < gridHeight; r++) {
|
|
440
|
+
let rowStr = "";
|
|
441
|
+
for (let c = 0; c < gridWidth; c++) {
|
|
442
|
+
const b = blocks[r * gridWidth + c];
|
|
443
|
+
rowStr += theme.fg(b.color as any, b.filled ? "■ " : "□ ");
|
|
444
|
+
}
|
|
445
|
+
gridLines.push(rowStr.trimEnd());
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
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)}%)`))}`;
|
|
449
|
+
|
|
450
|
+
const catDetailLines = categories.map(cat => {
|
|
451
|
+
const labelStr = cat.label.padEnd(14);
|
|
452
|
+
const valStr = formatTokens(cat.value).padStart(7);
|
|
453
|
+
const rowPercent = ((cat.value / limit) * 100).toFixed(1).padStart(5);
|
|
454
|
+
const icon = cat.label === "Available" ? "□" : "■";
|
|
455
|
+
return `${theme.fg(cat.color as any, icon)} ${theme.fg("text", labelStr)} ${theme.fg("accent", valStr)} (${rowPercent}%)`;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const allDetailLines = [totalUsageTitle, "", ...catDetailLines];
|
|
459
|
+
|
|
460
|
+
const leftSideWidth = 20;
|
|
461
|
+
const maxH = Math.max(gridLines.length, allDetailLines.length);
|
|
462
|
+
for (let i = 0; i < maxH; i++) {
|
|
463
|
+
const left = (gridLines[i] || "").padEnd(leftSideWidth);
|
|
464
|
+
const right = allDetailLines[i] || "";
|
|
465
|
+
container.addChild(new Text(` ${left} ${right}`, 1, 0));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
container.addChild(new Spacer(1));
|
|
469
|
+
container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
|
|
470
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
render: (w) => container.render(w),
|
|
474
|
+
invalidate: () => container.invalidate(),
|
|
475
|
+
handleInput: (data) => done(undefined),
|
|
476
|
+
};
|
|
477
|
+
}, { overlay: true });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
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, collapses intermediate steps." })),
|
|
7
|
-
});
|
|
8
|
-
const ContextCheckoutParams = Type.Object({
|
|
9
|
-
target: Type.String({ description: "Tag name or ID." }),
|
|
10
|
-
message: Type.String({ description: "Carryover message." }),
|
|
11
|
-
tagName: Type.Optional(Type.String()),
|
|
12
|
-
});
|
|
13
|
-
const ContextTagParams = Type.Object({
|
|
14
|
-
name: Type.String(),
|
|
15
|
-
target: Type.Optional(Type.String()),
|
|
16
|
-
});
|
|
17
|
-
const formatTokens = (n) => {
|
|
18
|
-
if (n >= 1_000_000)
|
|
19
|
-
return (n / 1_000_000).toFixed(1) + "M";
|
|
20
|
-
if (n >= 1_000)
|
|
21
|
-
return Math.round(n / 1_000) + "k";
|
|
22
|
-
return n.toString();
|
|
23
|
-
};
|
|
24
|
-
const resolveTargetId = (sm, target) => {
|
|
25
|
-
if (target.toLowerCase() === "root") {
|
|
26
|
-
const tree = sm.getTree();
|
|
27
|
-
return tree.length > 0 ? tree[0].entry.id : target;
|
|
28
|
-
}
|
|
29
|
-
if (/^[0-9a-f]{8,}$/i.test(target))
|
|
30
|
-
return target;
|
|
31
|
-
const find = (nodes) => {
|
|
32
|
-
for (const n of nodes) {
|
|
33
|
-
if (sm.getLabel(n.entry.id) === target)
|
|
34
|
-
return n.entry.id;
|
|
35
|
-
const r = find(n.children);
|
|
36
|
-
if (r)
|
|
37
|
-
return r;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
};
|
|
41
|
-
return find(sm.getTree()) || target;
|
|
42
|
-
};
|
|
43
|
-
export default function (pi) {
|
|
44
|
-
pi.registerTool({
|
|
45
|
-
name: "context_log",
|
|
46
|
-
label: "Context Log",
|
|
47
|
-
description: "Show history structure.",
|
|
48
|
-
parameters: ContextLogParams,
|
|
49
|
-
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
50
|
-
return { content: [{ type: "text", text: "Use /tree or context_log for history." }], details: {} };
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
pi.registerTool({
|
|
54
|
-
name: "context_checkout",
|
|
55
|
-
label: "Context Checkout",
|
|
56
|
-
description: "Navigate history.",
|
|
57
|
-
parameters: ContextCheckoutParams,
|
|
58
|
-
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
59
|
-
const sm = ctx.sessionManager;
|
|
60
|
-
const tid = resolveTargetId(sm, params.target);
|
|
61
|
-
await sm.branchWithSummary(tid, params.message);
|
|
62
|
-
return { content: [{ type: "text", text: `Jumped to ${tid.slice(0, 7)}` }], details: {} };
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
pi.registerTool({
|
|
66
|
-
name: "context_tag",
|
|
67
|
-
label: "Context Tag",
|
|
68
|
-
description: "Create checkpoint.",
|
|
69
|
-
parameters: ContextTagParams,
|
|
70
|
-
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
71
|
-
const sm = ctx.sessionManager;
|
|
72
|
-
const id = params.target ? resolveTargetId(sm, params.target) : (sm.getLeafId() ?? "");
|
|
73
|
-
pi.setLabel(id, params.name);
|
|
74
|
-
return { content: [{ type: "text", text: `Tagged ${params.name}` }], details: {} };
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
pi.registerCommand("context", {
|
|
78
|
-
description: "Show context usage visualization (Claude Code style)",
|
|
79
|
-
handler: async (args, ctx) => {
|
|
80
|
-
const usage = await ctx.getContextUsage();
|
|
81
|
-
if (!usage) {
|
|
82
|
-
ctx.ui.notify("Context usage info not available.", "warning");
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const sm = ctx.sessionManager;
|
|
86
|
-
const branch = sm.getBranch();
|
|
87
|
-
const systemPrompt = ctx.getSystemPrompt();
|
|
88
|
-
const tools = pi.getActiveTools();
|
|
89
|
-
const allTools = pi.getAllTools();
|
|
90
|
-
const activeToolDefs = allTools.filter(t => tools.includes(t.name));
|
|
91
|
-
const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
92
|
-
let msgTokensRaw = 0;
|
|
93
|
-
let toolUseTokensRaw = 0;
|
|
94
|
-
let toolResultTokensRaw = 0;
|
|
95
|
-
for (const entry of branch) {
|
|
96
|
-
if (entry.type === "message") {
|
|
97
|
-
const m = entry.message;
|
|
98
|
-
if (m.role === "user") {
|
|
99
|
-
if (typeof m.content === "string")
|
|
100
|
-
msgTokensRaw += estimateTokens(m.content);
|
|
101
|
-
else if (Array.isArray(m.content)) {
|
|
102
|
-
for (const p of m.content)
|
|
103
|
-
if (p.type === "text")
|
|
104
|
-
msgTokensRaw += estimateTokens(p.text);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
else if (m.role === "assistant") {
|
|
108
|
-
if (typeof m.content === "string")
|
|
109
|
-
msgTokensRaw += estimateTokens(m.content);
|
|
110
|
-
else if (Array.isArray(m.content)) {
|
|
111
|
-
for (const p of m.content) {
|
|
112
|
-
if (p.type === "text")
|
|
113
|
-
msgTokensRaw += estimateTokens(p.text);
|
|
114
|
-
if (p.type === "toolCall")
|
|
115
|
-
toolUseTokensRaw += estimateTokens(JSON.stringify(p));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
else if (m.role === "toolResult") {
|
|
120
|
-
if (Array.isArray(m.content)) {
|
|
121
|
-
for (const p of m.content)
|
|
122
|
-
if (p.type === "text")
|
|
123
|
-
toolResultTokensRaw += estimateTokens(p.text);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else if (m.role === "bashExecution") {
|
|
127
|
-
toolUseTokensRaw += estimateTokens(m.command || "");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
131
|
-
msgTokensRaw += estimateTokens(entry.summary || "");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
const systemTokensRaw = estimateTokens(systemPrompt);
|
|
135
|
-
const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
136
|
-
const totalActual = usage.tokens;
|
|
137
|
-
const limit = usage.contextWindow;
|
|
138
|
-
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
139
|
-
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
140
|
-
const systemTokens = Math.round(systemTokensRaw * ratio);
|
|
141
|
-
const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
|
|
142
|
-
const msgTokens = Math.round(msgTokensRaw * ratio);
|
|
143
|
-
const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
|
|
144
|
-
const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
|
|
145
|
-
await ctx.ui.custom((tui, theme, kb, done) => {
|
|
146
|
-
const container = new Container();
|
|
147
|
-
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
148
|
-
container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
|
|
149
|
-
container.addChild(new Spacer(1));
|
|
150
|
-
// Grouped by function and color
|
|
151
|
-
const categories = [
|
|
152
|
-
{ label: "System", value: systemTokens, color: "dim" },
|
|
153
|
-
{ label: "Tools", value: toolDefTokens, color: "dim" },
|
|
154
|
-
{ label: "Messages", value: msgTokens, color: "accent" },
|
|
155
|
-
{ label: "Tool Use", value: toolUseTokens, color: "success" },
|
|
156
|
-
{ label: "Tool Results", value: toolResultTokens, color: "success" },
|
|
157
|
-
];
|
|
158
|
-
const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
|
|
159
|
-
if (otherTokens > 10)
|
|
160
|
-
categories.push({ label: "Other", value: otherTokens, color: "dim" });
|
|
161
|
-
categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
|
|
162
|
-
const gridWidth = 10;
|
|
163
|
-
const gridHeight = 5;
|
|
164
|
-
const totalBlocks = gridWidth * gridHeight;
|
|
165
|
-
const blocks = [];
|
|
166
|
-
categories.forEach((cat) => {
|
|
167
|
-
if (cat.label === "Available")
|
|
168
|
-
return;
|
|
169
|
-
let count = Math.round((cat.value / limit) * totalBlocks);
|
|
170
|
-
if (count === 0 && cat.value > 0)
|
|
171
|
-
count = 1;
|
|
172
|
-
for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
|
|
173
|
-
blocks.push({ color: cat.color, filled: true });
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
while (blocks.length < totalBlocks) {
|
|
177
|
-
blocks.push({ color: "borderMuted", filled: false });
|
|
178
|
-
}
|
|
179
|
-
const gridLines = [];
|
|
180
|
-
for (let r = 0; r < gridHeight; r++) {
|
|
181
|
-
let rowStr = "";
|
|
182
|
-
for (let c = 0; c < gridWidth; c++) {
|
|
183
|
-
const b = blocks[r * gridWidth + c];
|
|
184
|
-
rowStr += theme.fg(b.color, b.filled ? "■ " : "□ ");
|
|
185
|
-
}
|
|
186
|
-
gridLines.push(rowStr.trimEnd());
|
|
187
|
-
}
|
|
188
|
-
const totalUsageTitle = `${theme.fg("text", theme.bold("Total Usage".padEnd(16)))} ${theme.fg("accent", theme.bold(formatTokens(totalActual).padStart(7)))} ${theme.fg("accent", theme.bold(`(${usage.percent.toFixed(1).padStart(5)}%)`))}`;
|
|
189
|
-
const catDetailLines = categories.map(cat => {
|
|
190
|
-
const labelStr = cat.label.padEnd(14);
|
|
191
|
-
const valStr = formatTokens(cat.value).padStart(7);
|
|
192
|
-
const rowPercent = ((cat.value / limit) * 100).toFixed(1).padStart(5);
|
|
193
|
-
const icon = cat.label === "Available" ? "□" : "■";
|
|
194
|
-
return `${theme.fg(cat.color, icon)} ${theme.fg("text", labelStr)} ${theme.fg("accent", valStr)} (${rowPercent}%)`;
|
|
195
|
-
});
|
|
196
|
-
const allDetailLines = [totalUsageTitle, "", ...catDetailLines];
|
|
197
|
-
const leftSideWidth = 20;
|
|
198
|
-
const maxH = Math.max(gridLines.length, allDetailLines.length);
|
|
199
|
-
for (let i = 0; i < maxH; i++) {
|
|
200
|
-
const left = (gridLines[i] || "").padEnd(leftSideWidth);
|
|
201
|
-
const right = allDetailLines[i] || "";
|
|
202
|
-
container.addChild(new Text(` ${left} ${right}`, 1, 0));
|
|
203
|
-
}
|
|
204
|
-
container.addChild(new Spacer(1));
|
|
205
|
-
container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
|
|
206
|
-
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
207
|
-
return {
|
|
208
|
-
render: (w) => container.render(w),
|
|
209
|
-
invalidate: () => container.invalidate(),
|
|
210
|
-
handleInput: (data) => done(undefined),
|
|
211
|
-
};
|
|
212
|
-
}, { overlay: true });
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
}
|