pi-context 1.0.3 → 1.0.5
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 +2 -1
- package/package.json +3 -2
- package/skills/context-management/SKILL.md +72 -41
- package/src/commands.ts +153 -0
- package/src/{index.ts → tools.ts} +3 -182
- package/src/utils.ts +37 -0
- package/dist/index.js +0 -443
package/README.md
CHANGED
|
@@ -4,7 +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
|
-
|
|
7
|
+
For the design philosophy, see the [blog post](https://blog.xlab.app/p/51d26495/)
|
|
8
|
+
([中文版本](https://blog.xlab.app/p/6a966aeb/)).
|
|
8
9
|
|
|
9
10
|
## Installation
|
|
10
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "agent-driven context management tools",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"pi": {
|
|
31
31
|
"extensions": [
|
|
32
|
-
"./src/
|
|
32
|
+
"./src/tools.ts",
|
|
33
|
+
"./src/commands.ts"
|
|
33
34
|
],
|
|
34
35
|
"skills": [
|
|
35
36
|
"./skills"
|
|
@@ -39,15 +39,15 @@ Follow this cycle for every major task:
|
|
|
39
39
|
|
|
40
40
|
1. **CHECK:** Verify state.
|
|
41
41
|
`context_log`
|
|
42
|
-
2. **START:** Tag the beginning.
|
|
43
|
-
`context_tag({ name: "task-start" })`
|
|
42
|
+
2. **START:** Tag the beginning with a semantic name.
|
|
43
|
+
`context_tag({ name: "<task-slug>-start" })` // e.g., `auth-login-start`
|
|
44
44
|
3. **WORK:** Execute steps.
|
|
45
|
-
4. **MILESTONE:** Tag intermediate stable states
|
|
46
|
-
`context_tag({ name: "task-plan
|
|
45
|
+
4. **MILESTONE:** Tag intermediate stable states.
|
|
46
|
+
`context_tag({ name: "<task-slug>-plan" })` // e.g., `auth-login-plan`
|
|
47
47
|
5. **SQUASH (Autonomous):** If history becomes noisy or low-density, **Squash with Backup**.
|
|
48
|
-
* *Action:* `context_checkout({ target: "task-start", message: "
|
|
49
|
-
* *Action (Optional):* `context_tag({ name: "
|
|
50
|
-
* *Safety:* If you need the details later,
|
|
48
|
+
* *Action:* `context_checkout({ target: "<task-slug>-start", message: "...", backupTag: "<task-slug>-raw-history" })`
|
|
49
|
+
* *Action (Optional):* `context_tag({ name: "<task-slug>-done" })`
|
|
50
|
+
* *Safety:* If you need the details later, checkout the backup tag.
|
|
51
51
|
|
|
52
52
|
## Tool Reference
|
|
53
53
|
|
|
@@ -60,11 +60,32 @@ Follow this cycle for every major task:
|
|
|
60
60
|
## Critical Rules
|
|
61
61
|
|
|
62
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
63
|
|
|
64
|
+
Tags are the "Table of Contents". Name them so you can understand the history at a glance.
|
|
65
|
+
|
|
66
|
+
**Naming Formula:** `<task-slug>-<phase>`
|
|
67
|
+
|
|
68
|
+
* **task-slug**: Short, kebab-case identifier for the task (e.g., `auth-login`, `db-migration`)
|
|
69
|
+
* **phase**: The stage of work (`start`, `plan`, `impl`, `done`, `fail`, `backup`)
|
|
70
|
+
|
|
71
|
+
| Bad (Generic) | Good (Semantic) | Why |
|
|
72
|
+
| :--- | :--- | :--- |
|
|
73
|
+
| `task-start` | `auth-oauth-start` | Describes WHAT task |
|
|
74
|
+
| `pre-research` | `error-log-analysis-start` | Future-you knows the topic |
|
|
75
|
+
| `phase-1-done` | `db-schema-plan-done` | Know which phase of which task |
|
|
76
|
+
| `debug-retry` | `null-pointer-fix-retry` | What bug? |
|
|
77
|
+
|
|
78
|
+
**Tag Categories:**
|
|
79
|
+
|
|
80
|
+
| Category | Pattern | Examples |
|
|
81
|
+
| :--- | :--- | :--- |
|
|
82
|
+
| **Start** | `<task>-start` | `auth-jwt-start`, `docker-setup-start` |
|
|
83
|
+
| **Plan** | `<task>-plan` | `api-v2-plan`, `migration-plan` |
|
|
84
|
+
| **Milestone** | `<task>-<milestone>` | `auth-jwt-impl-done`, `tests-passed` |
|
|
85
|
+
| **Backup** | `<task>-raw-history` | `auth-jwt-raw-history` |
|
|
86
|
+
| **Failure** | `<task>-fail-<reason>` | `auth-jwt-fail-timeout` |
|
|
87
|
+
|
|
88
|
+
**How to generate:** Ask yourself "What is the task?" → Extract 1-3 keywords (e.g., "fix login timeout" → `login-timeout-fix-start`)
|
|
68
89
|
|
|
69
90
|
### Squash Noise, Keep Signal, Focus on Goal (Context Hygiene)
|
|
70
91
|
Think of your conversation as a "Feature Branch" full of messy thoughts.
|
|
@@ -94,7 +115,7 @@ If you fail 3 times:
|
|
|
94
115
|
|
|
95
116
|
| Situation | Action | Reason |
|
|
96
117
|
| :--- | :--- | :--- |
|
|
97
|
-
| **Starting Task** | `context_tag({ name: "task-
|
|
118
|
+
| **Starting Task** | `context_tag({ name: "<task-slug>-start" })` | Create a rollback point. |
|
|
98
119
|
| **Research / Logs** | `context_checkout` (Squash) | **Process is Noise.** Read 2000 lines -> Keep result. |
|
|
99
120
|
| **Messy Debugging** | **Squash w/ Backup** | **Cleanup.** The error logs are noise once fixed. |
|
|
100
121
|
| **Task Done (Candidate)**| **Squash w/ Backup** | **Assume Success.** Summary is usually enough. Backup exists if not. |
|
|
@@ -117,17 +138,16 @@ If you cannot answer these, run `context_log`:
|
|
|
117
138
|
The `message` is your lifeline to your past self.
|
|
118
139
|
A good message preserves critical context that would otherwise be lost.
|
|
119
140
|
|
|
120
|
-
Structure: `[Status] + [Reason] + [
|
|
141
|
+
Structure: `[Key Finding/Status] + [Reason] + [Important Changes]`
|
|
121
142
|
|
|
122
|
-
* **Status**: What did you
|
|
123
|
-
* **Reason**: Why are you branching/moving? (e.g., "
|
|
124
|
-
* **Carryover**: What specific details (e.g., IDs, file paths, user constraints) must be remembered?
|
|
143
|
+
* **Key Finding/Status**: What did you discover or complete? Include specific numbers, errors, or outcomes.
|
|
144
|
+
* **Reason**: Why are you branching/moving? (e.g., "Task complete", "Approach failed", "Need raw logs")
|
|
125
145
|
* **Important Changes**: What files or logic have been modified? (This checkout only resets *conversation history*, NOT disk files, so you must remember what changed.)
|
|
126
146
|
|
|
127
147
|
Examples:
|
|
128
148
|
|
|
129
|
-
* *Good (Resetting after failure)*: "
|
|
130
|
-
* *Good (Cleaning up)*: "
|
|
149
|
+
* *Good (Resetting after failure)*: "Recursive parser hit stack overflow at depth 8000. Switching to iterative. **Reason**: Stack limit reached. **Important Changes**: Modified `utils/recursion.ts`."
|
|
150
|
+
* *Good (Cleaning up)*: "Auth module complete: JWT + OAuth2 + RBAC. 23 tests passing. **Reason**: Task done, cleaning context. **Important Changes**: Created `auth/`, modified `routes.ts` and `middleware.ts`."
|
|
131
151
|
* *Bad*: "Switching context." (Too vague - you will forget why)
|
|
132
152
|
* *Bad*: "Done." (What is done? What did we learn?)
|
|
133
153
|
|
|
@@ -141,6 +161,7 @@ Examples:
|
|
|
141
161
|
| **Panic** (Apologize repeatedly for errors) | **Revert** (`context_checkout`) to before the error. |
|
|
142
162
|
| **Blind Checkout** (Guessing IDs) | **Look** (`context_log`) first to get valid IDs. |
|
|
143
163
|
| **Vague Summaries** ("Done", "Fixed") | **Detailed Summaries** ("Found bug in line 40. Fixed with patch X.") |
|
|
164
|
+
| **Generic Tag Names** (`task-start`, `phase-1`) | **Semantic Names** (`auth-jwt-start`, `db-schema-plan`) |
|
|
144
165
|
|
|
145
166
|
## Recipes (Copy-Paste)
|
|
146
167
|
|
|
@@ -148,19 +169,21 @@ Examples:
|
|
|
148
169
|
**Goal:** Pure information gathering (Reading files, Searching web).
|
|
149
170
|
**Why:** The *process* of searching is irrelevant. Only the *result* matters.
|
|
150
171
|
|
|
172
|
+
**Example Task:** Analyzing error logs to find root cause of timeout
|
|
173
|
+
|
|
151
174
|
```javascript
|
|
152
|
-
// 1. Tag BEFORE starting the noisy work
|
|
153
|
-
context_tag({ name: "
|
|
175
|
+
// 1. Tag BEFORE starting the noisy work (use descriptive name)
|
|
176
|
+
context_tag({ name: "timeout-analysis-start" });
|
|
154
177
|
|
|
155
|
-
// ... (Read 5 files, search 3
|
|
178
|
+
// ... (Read 5 log files, search 3 docs, find DB connection pool exhaustion) ...
|
|
156
179
|
|
|
157
180
|
// 2. Squash IMMEDIATELY. Do not wait for user.
|
|
158
181
|
context_checkout({
|
|
159
|
-
target: "
|
|
160
|
-
message: "
|
|
161
|
-
backupTag: "raw-
|
|
182
|
+
target: "timeout-analysis-start",
|
|
183
|
+
message: "Found DB connection pool exhaustion as root cause (pool size: 10, peak load: 1000 req/s). Recommended fix: increase to 50. **Reason**: Context cleanup after research. **Important Changes**: None (read-only).",
|
|
184
|
+
backupTag: "timeout-analysis-raw-history" // Safety backup
|
|
162
185
|
});
|
|
163
|
-
context_tag({ name: "
|
|
186
|
+
context_tag({ name: "timeout-analysis-done" });
|
|
164
187
|
```
|
|
165
188
|
|
|
166
189
|
### 2. The "Candidate" (Wait for Confirmation)
|
|
@@ -168,52 +191,60 @@ context_tag({ name: "research-done" });
|
|
|
168
191
|
**Why:** The history is noisy. The result is clean.
|
|
169
192
|
**Safety:** We create a backup tag automatically.
|
|
170
193
|
|
|
194
|
+
**Example Task:** Implementing OAuth login flow
|
|
195
|
+
|
|
171
196
|
```javascript
|
|
172
197
|
// Squash to Summary (Optimistic Cleanup)
|
|
173
198
|
context_checkout({
|
|
174
|
-
target: "
|
|
175
|
-
message: "
|
|
176
|
-
backupTag: "
|
|
199
|
+
target: "oauth-impl-start", // Squash range: Start -> Now
|
|
200
|
+
message: "OAuth2 flow implemented with PKCE, Google + GitHub providers. All 12 tests passing. **Reason**: Task complete, cleaning up. **Important Changes**: Created `auth/oauth.ts`, modified `routes.ts`, `config.ts`. Backup at 'oauth-impl-raw-history'.",
|
|
201
|
+
backupTag: "oauth-impl-raw-history"
|
|
177
202
|
});
|
|
178
|
-
context_tag({ name: "
|
|
203
|
+
context_tag({ name: "oauth-impl-candidate" });
|
|
179
204
|
```
|
|
180
205
|
|
|
181
206
|
### 3. The "Undo" (Revert Squash)
|
|
182
207
|
**Goal:** User asks about a detail you squashed away.
|
|
183
208
|
**Action:** Jump back to the backup tag.
|
|
184
209
|
|
|
210
|
+
**Example Task:** Reviewing OAuth implementation details
|
|
211
|
+
|
|
185
212
|
```javascript
|
|
186
213
|
// Jump back to the raw history
|
|
187
214
|
context_checkout({
|
|
188
|
-
target: "
|
|
189
|
-
message: "
|
|
215
|
+
target: "oauth-impl-raw-history",
|
|
216
|
+
message: "Reviewing token refresh logic - user reports 401 after 15 min idle. Suspect refresh token not firing. **Reason**: Need raw logs to trace the bug. **Important Changes**: None."
|
|
190
217
|
});
|
|
191
|
-
context_tag({ name: "
|
|
218
|
+
context_tag({ name: "oauth-review-start" });
|
|
192
219
|
```
|
|
193
220
|
|
|
194
221
|
### 4. Branching (Alternative Approach)
|
|
195
222
|
**Scenario:** Method A failed (and was squashed). You want to try Method B from the clean state.
|
|
196
223
|
**Action:** Checkout the start point.
|
|
197
224
|
|
|
225
|
+
**Example Task:** Fixing memory leak - trying different approaches
|
|
226
|
+
|
|
198
227
|
```javascript
|
|
199
|
-
//
|
|
228
|
+
// Method A (weak references) failed, trying Method B (object pooling)
|
|
200
229
|
context_checkout({
|
|
201
|
-
target: "
|
|
202
|
-
message: "
|
|
230
|
+
target: "memory-leak-fix-start",
|
|
231
|
+
message: "WeakRef approach failed: objects GC'd within 30s (expected: 5min). Cache hit rate dropped from 95% to 12%. **Reason**: Switching to object pooling approach. **Important Changes**: `CacheManager.ts` modified (will revert)."
|
|
203
232
|
});
|
|
204
|
-
context_tag({ name: "
|
|
233
|
+
context_tag({ name: "memory-leak-pool-approach-start" });
|
|
205
234
|
```
|
|
206
235
|
|
|
207
236
|
### 5. The "Undo" (Failed Attempt)
|
|
208
237
|
You tried to fix a bug but broke everything.
|
|
209
238
|
**Goal:** Clean up a failed path.
|
|
210
239
|
|
|
240
|
+
**Example Task:** Fixing race condition in async handler
|
|
241
|
+
|
|
211
242
|
```javascript
|
|
212
|
-
//
|
|
243
|
+
// Attempted mutex-based fix, but introduced deadlock
|
|
213
244
|
context_checkout({
|
|
214
|
-
target: "
|
|
215
|
-
message: "
|
|
216
|
-
backupTag: "
|
|
245
|
+
target: "race-condition-fix-start",
|
|
246
|
+
message: "Mutex caused deadlock: Thread A holds mutex, awaits callback; callback needs mutex held by B; B waits for A. Circular wait detected. **Reason**: Trying lock-free CAS approach next. **Important Changes**: `AsyncQueue.ts` lines 70-90 modified (backup saved).",
|
|
247
|
+
backupTag: "race-condition-mutex-fail" // Save the failure for reference
|
|
217
248
|
});
|
|
218
|
-
context_tag({ name: "
|
|
249
|
+
context_tag({ name: "race-condition-lockfree-start" });
|
|
219
250
|
```
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type SessionManager,
|
|
4
|
+
DynamicBorder,
|
|
5
|
+
} from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { Container, Text, Spacer } from "@mariozechner/pi-tui";
|
|
7
|
+
import { formatTokens } from "./utils.js";
|
|
8
|
+
|
|
9
|
+
export default function (pi: ExtensionAPI) {
|
|
10
|
+
pi.registerCommand("context", {
|
|
11
|
+
description: "Show context usage visualization",
|
|
12
|
+
handler: async (args, ctx) => {
|
|
13
|
+
const usage = await ctx.getContextUsage();
|
|
14
|
+
if (!usage) {
|
|
15
|
+
ctx.ui.notify("Context usage info not available.", "warning");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sm = ctx.sessionManager as SessionManager;
|
|
20
|
+
const branch = sm.getBranch();
|
|
21
|
+
const systemPrompt = ctx.getSystemPrompt();
|
|
22
|
+
const tools = pi.getActiveTools();
|
|
23
|
+
const allTools = pi.getAllTools();
|
|
24
|
+
const activeToolDefs = allTools.filter(t => tools.includes(t.name));
|
|
25
|
+
|
|
26
|
+
const estimateTokens = (text: string) => Math.ceil(text.length / 4);
|
|
27
|
+
|
|
28
|
+
let msgTokensRaw = 0;
|
|
29
|
+
let toolUseTokensRaw = 0;
|
|
30
|
+
let toolResultTokensRaw = 0;
|
|
31
|
+
|
|
32
|
+
for (const entry of branch) {
|
|
33
|
+
if (entry.type === "message") {
|
|
34
|
+
const m = entry.message;
|
|
35
|
+
if (m.role === "user") {
|
|
36
|
+
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
37
|
+
else if (Array.isArray(m.content)) {
|
|
38
|
+
for (const p of m.content) if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
39
|
+
}
|
|
40
|
+
} else if (m.role === "assistant") {
|
|
41
|
+
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
42
|
+
else if (Array.isArray(m.content)) {
|
|
43
|
+
for (const p of m.content) {
|
|
44
|
+
if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
45
|
+
if (p.type === "toolCall") toolUseTokensRaw += estimateTokens(JSON.stringify(p));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else if (m.role === "toolResult") {
|
|
49
|
+
if (Array.isArray(m.content)) {
|
|
50
|
+
for (const p of m.content) if (p.type === "text") toolResultTokensRaw += estimateTokens(p.text);
|
|
51
|
+
}
|
|
52
|
+
} else if (m.role === "bashExecution") {
|
|
53
|
+
toolUseTokensRaw += estimateTokens(m.command || "");
|
|
54
|
+
}
|
|
55
|
+
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
56
|
+
msgTokensRaw += estimateTokens(entry.summary || "");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const systemTokensRaw = estimateTokens(systemPrompt);
|
|
61
|
+
const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
62
|
+
const totalActual = usage.tokens;
|
|
63
|
+
const limit = usage.contextWindow;
|
|
64
|
+
|
|
65
|
+
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
66
|
+
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
67
|
+
|
|
68
|
+
const systemTokens = Math.round(systemTokensRaw * ratio);
|
|
69
|
+
const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
|
|
70
|
+
const msgTokens = Math.round(msgTokensRaw * ratio);
|
|
71
|
+
const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
|
|
72
|
+
const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
|
|
73
|
+
|
|
74
|
+
await ctx.ui.custom((tui, theme, kb, done) => {
|
|
75
|
+
const container = new Container();
|
|
76
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
77
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
|
|
78
|
+
container.addChild(new Spacer(1));
|
|
79
|
+
|
|
80
|
+
// Grouped by function and color
|
|
81
|
+
const categories = [
|
|
82
|
+
{ label: "System Prompt", value: systemTokens, color: "muted" },
|
|
83
|
+
{ label: "System Tools", value: toolDefTokens, color: "dim" },
|
|
84
|
+
{ label: "Tool Call", value: toolUseTokens + toolResultTokens, color: "success" },
|
|
85
|
+
{ label: "Messages", value: msgTokens, color: "accent" },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
|
|
89
|
+
if (otherTokens > 10) categories.push({ label: "Other", value: otherTokens, color: "dim" });
|
|
90
|
+
|
|
91
|
+
categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
|
|
92
|
+
|
|
93
|
+
const gridWidth = 10;
|
|
94
|
+
const gridHeight = 5;
|
|
95
|
+
const totalBlocks = gridWidth * gridHeight;
|
|
96
|
+
|
|
97
|
+
const blocks: { color: string, filled: boolean }[] = [];
|
|
98
|
+
categories.forEach((cat) => {
|
|
99
|
+
if (cat.label === "Available") return;
|
|
100
|
+
let count = Math.round((cat.value / limit) * totalBlocks);
|
|
101
|
+
if (count === 0 && cat.value > 0) count = 1;
|
|
102
|
+
for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
|
|
103
|
+
blocks.push({ color: cat.color, filled: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
while (blocks.length < totalBlocks) {
|
|
108
|
+
blocks.push({ color: "borderMuted", filled: false });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const gridLines: string[] = [];
|
|
112
|
+
for (let r = 0; r < gridHeight; r++) {
|
|
113
|
+
let rowStr = "";
|
|
114
|
+
for (let c = 0; c < gridWidth; c++) {
|
|
115
|
+
const b = blocks[r * gridWidth + c];
|
|
116
|
+
rowStr += theme.fg(b.color as any, b.filled ? "■ " : "□ ");
|
|
117
|
+
}
|
|
118
|
+
gridLines.push(rowStr.trimEnd());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
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)}%)`))}`;
|
|
122
|
+
|
|
123
|
+
const catDetailLines = categories.map(cat => {
|
|
124
|
+
const labelStr = cat.label.padEnd(14);
|
|
125
|
+
const valStr = formatTokens(cat.value).padStart(7);
|
|
126
|
+
const rowPercent = ((cat.value / limit) * 100).toFixed(1).padStart(5);
|
|
127
|
+
const icon = cat.label === "Available" ? "□" : "■";
|
|
128
|
+
return `${theme.fg(cat.color as any, icon)} ${theme.fg("text", labelStr)} ${theme.fg("accent", valStr)} (${rowPercent}%)`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const allDetailLines = [totalUsageTitle, "", ...catDetailLines];
|
|
132
|
+
|
|
133
|
+
const leftSideWidth = 20;
|
|
134
|
+
const maxH = Math.max(gridLines.length, allDetailLines.length);
|
|
135
|
+
for (let i = 0; i < maxH; i++) {
|
|
136
|
+
const left = (gridLines[i] || "").padEnd(leftSideWidth);
|
|
137
|
+
const right = allDetailLines[i] || "";
|
|
138
|
+
container.addChild(new Text(` ${left} ${right}`, 1, 0));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
container.addChild(new Spacer(1));
|
|
142
|
+
container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
|
|
143
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
render: (w) => container.render(w),
|
|
147
|
+
invalidate: () => container.invalidate(),
|
|
148
|
+
handleInput: (data) => done(undefined),
|
|
149
|
+
};
|
|
150
|
+
}, { overlay: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ExtensionAPI,
|
|
3
|
-
type SessionEntry,
|
|
4
3
|
type SessionManager,
|
|
5
|
-
|
|
4
|
+
type SessionEntry,
|
|
6
5
|
} from "@mariozechner/pi-coding-agent";
|
|
7
6
|
import type {
|
|
8
|
-
ToolCall,
|
|
9
7
|
TextContent,
|
|
10
8
|
ImageContent,
|
|
9
|
+
ToolCall,
|
|
11
10
|
} from "@mariozechner/pi-ai";
|
|
12
11
|
import { Type, type Static } from "@sinclair/typebox";
|
|
13
|
-
import {
|
|
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
|
-
}
|
|
12
|
+
import { isInternal, resolveTargetId, formatTokens } from "./utils.js";
|
|
21
13
|
|
|
22
14
|
const ContextLogParams = Type.Object({
|
|
23
15
|
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
@@ -35,32 +27,6 @@ const ContextTagParams = Type.Object({
|
|
|
35
27
|
target: Type.Optional(Type.String({ description: "The commit ID to tag. Defaults to HEAD (current state)." })),
|
|
36
28
|
});
|
|
37
29
|
|
|
38
|
-
const isInternal = (name: string) => ["context_tag", "context_log", "context_checkout"].includes(name);
|
|
39
|
-
|
|
40
|
-
const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
41
|
-
if (target.toLowerCase() === "root") {
|
|
42
|
-
const tree = sm.getTree();
|
|
43
|
-
return tree.length > 0 ? tree[0].entry.id : target;
|
|
44
|
-
}
|
|
45
|
-
if (/^[0-9a-f]{8,}$/i.test(target)) return target;
|
|
46
|
-
const find = (nodes: SessionTreeNode[]): string | null => {
|
|
47
|
-
for (const n of nodes) {
|
|
48
|
-
if (sm.getLabel(n.entry.id) === target) return n.entry.id;
|
|
49
|
-
const r = find(n.children);
|
|
50
|
-
if (r) return r;
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
};
|
|
54
|
-
// sm.getTree() returns the SDK's SessionTreeNode[], which is structurally compatible
|
|
55
|
-
return find(sm.getTree()) || target;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const formatTokens = (n: number) => {
|
|
59
|
-
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
60
|
-
if (n >= 1_000) return Math.round(n / 1_000) + "k";
|
|
61
|
-
return n.toString();
|
|
62
|
-
};
|
|
63
|
-
|
|
64
30
|
export default function (pi: ExtensionAPI) {
|
|
65
31
|
pi.registerTool({
|
|
66
32
|
name: "context_tag",
|
|
@@ -343,149 +309,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
343
309
|
return { content: [{ type: "text", text: `Checked out ${tid}\nBackup tag created: ${params.backupTag || "none"}\nmessage: ${enrichedMessage}` }], details: {} };
|
|
344
310
|
},
|
|
345
311
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
pi.registerCommand("context", {
|
|
349
|
-
description: "Show context usage visualization",
|
|
350
|
-
handler: async (args, ctx) => {
|
|
351
|
-
const usage = await ctx.getContextUsage();
|
|
352
|
-
if (!usage) {
|
|
353
|
-
ctx.ui.notify("Context usage info not available.", "warning");
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const sm = ctx.sessionManager as SessionManager;
|
|
358
|
-
const branch = sm.getBranch();
|
|
359
|
-
const systemPrompt = ctx.getSystemPrompt();
|
|
360
|
-
const tools = pi.getActiveTools();
|
|
361
|
-
const allTools = pi.getAllTools();
|
|
362
|
-
const activeToolDefs = allTools.filter(t => tools.includes(t.name));
|
|
363
|
-
|
|
364
|
-
const estimateTokens = (text: string) => Math.ceil(text.length / 4);
|
|
365
|
-
|
|
366
|
-
let msgTokensRaw = 0;
|
|
367
|
-
let toolUseTokensRaw = 0;
|
|
368
|
-
let toolResultTokensRaw = 0;
|
|
369
|
-
|
|
370
|
-
for (const entry of branch) {
|
|
371
|
-
if (entry.type === "message") {
|
|
372
|
-
const m = entry.message;
|
|
373
|
-
if (m.role === "user") {
|
|
374
|
-
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
375
|
-
else if (Array.isArray(m.content)) {
|
|
376
|
-
for (const p of m.content) if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
377
|
-
}
|
|
378
|
-
} else if (m.role === "assistant") {
|
|
379
|
-
if (typeof m.content === "string") msgTokensRaw += estimateTokens(m.content);
|
|
380
|
-
else if (Array.isArray(m.content)) {
|
|
381
|
-
for (const p of m.content) {
|
|
382
|
-
if (p.type === "text") msgTokensRaw += estimateTokens(p.text);
|
|
383
|
-
if (p.type === "toolCall") toolUseTokensRaw += estimateTokens(JSON.stringify(p));
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} else if (m.role === "toolResult") {
|
|
387
|
-
if (Array.isArray(m.content)) {
|
|
388
|
-
for (const p of m.content) if (p.type === "text") toolResultTokensRaw += estimateTokens(p.text);
|
|
389
|
-
}
|
|
390
|
-
} else if (m.role === "bashExecution") {
|
|
391
|
-
toolUseTokensRaw += estimateTokens(m.command || "");
|
|
392
|
-
}
|
|
393
|
-
} else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
394
|
-
msgTokensRaw += estimateTokens(entry.summary || "");
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const systemTokensRaw = estimateTokens(systemPrompt);
|
|
399
|
-
const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
400
|
-
const totalActual = usage.tokens;
|
|
401
|
-
const limit = usage.contextWindow;
|
|
402
|
-
|
|
403
|
-
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
404
|
-
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
405
|
-
|
|
406
|
-
const systemTokens = Math.round(systemTokensRaw * ratio);
|
|
407
|
-
const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
|
|
408
|
-
const msgTokens = Math.round(msgTokensRaw * ratio);
|
|
409
|
-
const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
|
|
410
|
-
const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
|
|
411
|
-
|
|
412
|
-
await ctx.ui.custom((tui, theme, kb, done) => {
|
|
413
|
-
const container = new Container();
|
|
414
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
415
|
-
container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
|
|
416
|
-
container.addChild(new Spacer(1));
|
|
417
|
-
|
|
418
|
-
// Grouped by function and color
|
|
419
|
-
const categories = [
|
|
420
|
-
{ label: "System Prompt", value: systemTokens, color: "muted" },
|
|
421
|
-
{ label: "System Tools", value: toolDefTokens, color: "dim" },
|
|
422
|
-
{ label: "Tool Call", value: toolUseTokens + toolResultTokens, color: "success" },
|
|
423
|
-
{ label: "Messages", value: msgTokens, color: "accent" },
|
|
424
|
-
];
|
|
425
|
-
|
|
426
|
-
const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
|
|
427
|
-
if (otherTokens > 10) categories.push({ label: "Other", value: otherTokens, color: "dim" });
|
|
428
|
-
|
|
429
|
-
categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
|
|
430
|
-
|
|
431
|
-
const gridWidth = 10;
|
|
432
|
-
const gridHeight = 5;
|
|
433
|
-
const totalBlocks = gridWidth * gridHeight;
|
|
434
|
-
|
|
435
|
-
const blocks: { color: string, filled: boolean }[] = [];
|
|
436
|
-
categories.forEach((cat) => {
|
|
437
|
-
if (cat.label === "Available") return;
|
|
438
|
-
let count = Math.round((cat.value / limit) * totalBlocks);
|
|
439
|
-
if (count === 0 && cat.value > 0) count = 1;
|
|
440
|
-
for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
|
|
441
|
-
blocks.push({ color: cat.color, filled: true });
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
while (blocks.length < totalBlocks) {
|
|
446
|
-
blocks.push({ color: "borderMuted", filled: false });
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const gridLines: string[] = [];
|
|
450
|
-
for (let r = 0; r < gridHeight; r++) {
|
|
451
|
-
let rowStr = "";
|
|
452
|
-
for (let c = 0; c < gridWidth; c++) {
|
|
453
|
-
const b = blocks[r * gridWidth + c];
|
|
454
|
-
rowStr += theme.fg(b.color as any, b.filled ? "■ " : "□ ");
|
|
455
|
-
}
|
|
456
|
-
gridLines.push(rowStr.trimEnd());
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const totalUsageTitle = `${theme.fg("text", theme.bold("Total Usage".padEnd(16)))} ${theme.fg("text", theme.bold(formatTokens(totalActual).padStart(7)))} ${theme.fg("text", theme.bold(`(${usage.percent.toFixed(1).padStart(5)}%)`))}`;
|
|
460
|
-
|
|
461
|
-
const catDetailLines = categories.map(cat => {
|
|
462
|
-
const labelStr = cat.label.padEnd(14);
|
|
463
|
-
const valStr = formatTokens(cat.value).padStart(7);
|
|
464
|
-
const rowPercent = ((cat.value / limit) * 100).toFixed(1).padStart(5);
|
|
465
|
-
const icon = cat.label === "Available" ? "□" : "■";
|
|
466
|
-
return `${theme.fg(cat.color as any, icon)} ${theme.fg("text", labelStr)} ${theme.fg("accent", valStr)} (${rowPercent}%)`;
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
const allDetailLines = [totalUsageTitle, "", ...catDetailLines];
|
|
470
|
-
|
|
471
|
-
const leftSideWidth = 20;
|
|
472
|
-
const maxH = Math.max(gridLines.length, allDetailLines.length);
|
|
473
|
-
for (let i = 0; i < maxH; i++) {
|
|
474
|
-
const left = (gridLines[i] || "").padEnd(leftSideWidth);
|
|
475
|
-
const right = allDetailLines[i] || "";
|
|
476
|
-
container.addChild(new Text(` ${left} ${right}`, 1, 0));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
container.addChild(new Spacer(1));
|
|
480
|
-
container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
|
|
481
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
482
|
-
|
|
483
|
-
return {
|
|
484
|
-
render: (w) => container.render(w),
|
|
485
|
-
invalidate: () => container.invalidate(),
|
|
486
|
-
handleInput: (data) => done(undefined),
|
|
487
|
-
};
|
|
488
|
-
}, { overlay: true });
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
312
|
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SessionEntry,
|
|
3
|
+
type SessionManager,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
// Define missing types locally as they are not exported from the main entry point
|
|
7
|
+
export interface SessionTreeNode {
|
|
8
|
+
entry: SessionEntry;
|
|
9
|
+
children: SessionTreeNode[];
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const isInternal = (name: string) => ["context_tag", "context_log", "context_checkout"].includes(name);
|
|
14
|
+
|
|
15
|
+
export const resolveTargetId = (sm: SessionManager, target: string): string => {
|
|
16
|
+
if (target.toLowerCase() === "root") {
|
|
17
|
+
const tree = sm.getTree();
|
|
18
|
+
return tree.length > 0 ? tree[0].entry.id : target;
|
|
19
|
+
}
|
|
20
|
+
if (/^[0-9a-f]{8,}$/i.test(target)) return target;
|
|
21
|
+
const find = (nodes: SessionTreeNode[]): string | null => {
|
|
22
|
+
for (const n of nodes) {
|
|
23
|
+
if (sm.getLabel(n.entry.id) === target) return n.entry.id;
|
|
24
|
+
const r = find(n.children);
|
|
25
|
+
if (r) return r;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
// sm.getTree() returns the SDK's SessionTreeNode[], which is structurally compatible
|
|
30
|
+
return find(sm.getTree()) || target;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const formatTokens = (n: number) => {
|
|
34
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
35
|
+
if (n >= 1_000) return Math.round(n / 1_000) + "k";
|
|
36
|
+
return n.toString();
|
|
37
|
+
};
|
package/dist/index.js
DELETED
|
@@ -1,443 +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 (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
|
-
}
|