@diegopetrucci/pi-extensions 0.1.73 → 0.1.76
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/LICENSE +2 -0
- package/README.md +9 -4
- package/extensions/agent-workflow-audit/index.ts +1398 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +853 -767
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +1 -1
- package/extensions/code-reviewer/index.ts +1598 -1350
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +55 -51
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/index.ts +312 -304
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1854 -1735
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +41 -41
- package/extensions/dynamic-context-pruning/README.md +6 -38
- package/extensions/dynamic-context-pruning/index.ts +2124 -1980
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +71 -21
- package/extensions/fast/index.ts +513 -313
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +88 -88
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +1669 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +994 -1012
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/index.ts +1731 -1625
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +658 -608
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/index.ts +282 -264
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +284 -265
- package/extensions/triage-comments/README.md +2 -2
- package/extensions/triage-comments/index.ts +2294 -1882
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -3
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -2,1219 +2,1362 @@ import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
CONFIG_DIR_NAME,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionCommandContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
10
|
|
|
7
|
-
const EXTENSION_ID = "context-inspector";
|
|
8
11
|
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
9
12
|
const DETAIL_TEXT_LIMIT = 16_000;
|
|
10
13
|
const PREVIEW_TEXT_LIMIT = 700;
|
|
11
|
-
const COMPACTION_SUMMARY_PREFIX =
|
|
14
|
+
const COMPACTION_SUMMARY_PREFIX =
|
|
15
|
+
"The conversation history before this point was compacted into the following summary:\n\n<summary>\n";
|
|
12
16
|
const COMPACTION_SUMMARY_SUFFIX = "\n</summary>";
|
|
13
|
-
const BRANCH_SUMMARY_PREFIX =
|
|
17
|
+
const BRANCH_SUMMARY_PREFIX =
|
|
18
|
+
"The following is a summary of a branch that this conversation came back from:\n\n<summary>\n";
|
|
14
19
|
const BRANCH_SUMMARY_SUFFIX = "</summary>";
|
|
15
20
|
|
|
16
21
|
const CATEGORY_META = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
22
|
+
system: {
|
|
23
|
+
label: "System prompt",
|
|
24
|
+
shortLabel: "System",
|
|
25
|
+
color: "#8b5cf6",
|
|
26
|
+
description:
|
|
27
|
+
"Current system prompt, project instructions, loaded guidance, and extension-added prompt text.",
|
|
28
|
+
},
|
|
29
|
+
toolSchemas: {
|
|
30
|
+
label: "Tool schemas",
|
|
31
|
+
shortLabel: "Tools",
|
|
32
|
+
color: "#06b6d4",
|
|
33
|
+
description: "Active tool definitions, descriptions, and JSON schemas sent to the provider.",
|
|
34
|
+
},
|
|
35
|
+
user: {
|
|
36
|
+
label: "User messages",
|
|
37
|
+
shortLabel: "User",
|
|
38
|
+
color: "#22c55e",
|
|
39
|
+
description: "Your prompts and user-role text that stays in the active conversation.",
|
|
40
|
+
},
|
|
41
|
+
assistant: {
|
|
42
|
+
label: "Assistant responses",
|
|
43
|
+
shortLabel: "Assistant",
|
|
44
|
+
color: "#60a5fa",
|
|
45
|
+
description: "Visible assistant text responses kept in context.",
|
|
46
|
+
},
|
|
47
|
+
thinking: {
|
|
48
|
+
label: "Assistant thinking",
|
|
49
|
+
shortLabel: "Thinking",
|
|
50
|
+
color: "#f97316",
|
|
51
|
+
description:
|
|
52
|
+
"Reasoning/thinking blocks returned by reasoning models when present in the session.",
|
|
53
|
+
},
|
|
54
|
+
toolCalls: {
|
|
55
|
+
label: "Tool calls",
|
|
56
|
+
shortLabel: "Calls",
|
|
57
|
+
color: "#facc15",
|
|
58
|
+
description: "Assistant tool-call arguments, including paths, commands, and edit payloads.",
|
|
59
|
+
},
|
|
60
|
+
toolResults: {
|
|
61
|
+
label: "Tool results",
|
|
62
|
+
shortLabel: "Results",
|
|
63
|
+
color: "#ef4444",
|
|
64
|
+
description:
|
|
65
|
+
"Tool output returned to the model, usually the largest contributor in coding sessions.",
|
|
66
|
+
},
|
|
67
|
+
bash: {
|
|
68
|
+
label: "User bash",
|
|
69
|
+
shortLabel: "Bash",
|
|
70
|
+
color: "#a3e635",
|
|
71
|
+
description: "User-run ! bash commands and their outputs when included in model context.",
|
|
72
|
+
},
|
|
73
|
+
summaries: {
|
|
74
|
+
label: "Summaries",
|
|
75
|
+
shortLabel: "Summaries",
|
|
76
|
+
color: "#ec4899",
|
|
77
|
+
description: "Compaction and branch summaries replacing earlier history.",
|
|
78
|
+
},
|
|
79
|
+
custom: {
|
|
80
|
+
label: "Custom context",
|
|
81
|
+
shortLabel: "Custom",
|
|
82
|
+
color: "#14b8a6",
|
|
83
|
+
description: "Extension-injected custom messages that participate in context.",
|
|
84
|
+
},
|
|
85
|
+
images: {
|
|
86
|
+
label: "Images",
|
|
87
|
+
shortLabel: "Images",
|
|
88
|
+
color: "#f59e0b",
|
|
89
|
+
description:
|
|
90
|
+
"Image blocks, estimated conservatively because providers tokenize images differently.",
|
|
91
|
+
},
|
|
92
|
+
providerDelta: {
|
|
93
|
+
label: "Provider / serialization delta",
|
|
94
|
+
shortLabel: "Delta",
|
|
95
|
+
color: "#64748b",
|
|
96
|
+
description:
|
|
97
|
+
"Unattributed difference between local estimates and pi's footer-compatible provider total.",
|
|
98
|
+
},
|
|
89
99
|
} as const;
|
|
90
100
|
|
|
91
101
|
type CategoryId = keyof typeof CATEGORY_META;
|
|
92
102
|
|
|
93
103
|
type CommandOptions = {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
open: boolean;
|
|
105
|
+
keep: boolean;
|
|
106
|
+
redact: boolean;
|
|
107
|
+
defaultDataset: DatasetId;
|
|
108
|
+
help: boolean;
|
|
99
109
|
};
|
|
100
110
|
|
|
101
111
|
type DatasetId = "current" | "full";
|
|
102
112
|
|
|
103
113
|
type MinimalContentBlock = {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
type?: string;
|
|
115
|
+
text?: string;
|
|
116
|
+
thinking?: string;
|
|
117
|
+
name?: string;
|
|
118
|
+
id?: string;
|
|
119
|
+
arguments?: Record<string, unknown>;
|
|
120
|
+
data?: string;
|
|
121
|
+
mimeType?: string;
|
|
122
|
+
source?: {
|
|
123
|
+
type?: string;
|
|
124
|
+
mediaType?: string;
|
|
125
|
+
data?: string;
|
|
126
|
+
};
|
|
117
127
|
};
|
|
118
128
|
|
|
119
129
|
type MinimalMessage = {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
130
|
+
role?: string;
|
|
131
|
+
content?: unknown;
|
|
132
|
+
timestamp?: number;
|
|
133
|
+
provider?: string;
|
|
134
|
+
model?: string;
|
|
135
|
+
api?: string;
|
|
136
|
+
stopReason?: string;
|
|
137
|
+
usage?: unknown;
|
|
138
|
+
toolCallId?: string;
|
|
139
|
+
toolName?: string;
|
|
140
|
+
details?: unknown;
|
|
141
|
+
/** Pi keeps nested tool calls here as bounded metadata; they are not transcript content. */
|
|
142
|
+
nestedCalls?: unknown;
|
|
143
|
+
customType?: string;
|
|
144
|
+
display?: boolean;
|
|
145
|
+
summary?: string;
|
|
146
|
+
fromId?: string;
|
|
147
|
+
tokensBefore?: number;
|
|
148
|
+
command?: string;
|
|
149
|
+
output?: string;
|
|
150
|
+
exitCode?: number;
|
|
151
|
+
cancelled?: boolean;
|
|
152
|
+
truncated?: boolean;
|
|
153
|
+
fullOutputPath?: string;
|
|
154
|
+
excludeFromContext?: boolean;
|
|
145
155
|
};
|
|
146
156
|
|
|
147
157
|
type MinimalEntry = {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
type?: string;
|
|
159
|
+
id?: string;
|
|
160
|
+
parentId?: string | null;
|
|
161
|
+
timestamp?: string;
|
|
162
|
+
message?: MinimalMessage;
|
|
163
|
+
customType?: string;
|
|
164
|
+
content?: unknown;
|
|
165
|
+
display?: boolean;
|
|
166
|
+
details?: unknown;
|
|
167
|
+
summary?: string;
|
|
168
|
+
fromId?: string;
|
|
169
|
+
tokensBefore?: number;
|
|
170
|
+
firstKeptEntryId?: string;
|
|
161
171
|
};
|
|
162
172
|
|
|
163
173
|
type MinimalProjectionEntry = {
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
sourceEntry: MinimalEntry;
|
|
175
|
+
messages: MinimalMessage[];
|
|
166
176
|
};
|
|
167
177
|
|
|
168
178
|
type MinimalSessionProjection = {
|
|
169
|
-
|
|
170
|
-
|
|
179
|
+
entries: MinimalProjectionEntry[];
|
|
180
|
+
messages?: MinimalMessage[];
|
|
171
181
|
};
|
|
172
182
|
|
|
173
183
|
type AnalysisResult = {
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
segments: Segment[];
|
|
185
|
+
messageCount: number;
|
|
176
186
|
};
|
|
177
187
|
|
|
178
188
|
type Segment = {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
189
|
+
id: string;
|
|
190
|
+
category: CategoryId;
|
|
191
|
+
label: string;
|
|
192
|
+
source: string;
|
|
193
|
+
role: string;
|
|
194
|
+
tokens: number;
|
|
195
|
+
chars: number;
|
|
196
|
+
turn: number;
|
|
197
|
+
sequence: number;
|
|
198
|
+
entryId?: string;
|
|
199
|
+
timestamp?: string;
|
|
200
|
+
toolName?: string;
|
|
201
|
+
toolCallId?: string;
|
|
202
|
+
path?: string;
|
|
203
|
+
command?: string;
|
|
204
|
+
excluded?: boolean;
|
|
205
|
+
displayOnly?: boolean;
|
|
206
|
+
preview: string;
|
|
207
|
+
detail: string;
|
|
208
|
+
note?: string;
|
|
199
209
|
};
|
|
200
210
|
|
|
201
211
|
type CategoryStat = {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
id: CategoryId;
|
|
213
|
+
label: string;
|
|
214
|
+
shortLabel: string;
|
|
215
|
+
color: string;
|
|
216
|
+
description: string;
|
|
217
|
+
tokens: number;
|
|
218
|
+
displayTokens: number;
|
|
219
|
+
percent: number;
|
|
220
|
+
segments: number;
|
|
211
221
|
};
|
|
212
222
|
|
|
213
223
|
type AggregateStat = {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
224
|
+
key: string;
|
|
225
|
+
label: string;
|
|
226
|
+
tokens: number;
|
|
227
|
+
segments: number;
|
|
228
|
+
percent: number;
|
|
219
229
|
};
|
|
220
230
|
|
|
221
231
|
type DatasetStats = {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
tokens: number;
|
|
233
|
+
rawTokens: number;
|
|
234
|
+
visibleRawTokens: number;
|
|
235
|
+
excludedTokens: number;
|
|
236
|
+
providerDeltaTokens: number;
|
|
237
|
+
estimatorOverageTokens: number;
|
|
238
|
+
reconciliationScale: number;
|
|
239
|
+
segmentCount: number;
|
|
240
|
+
messageCount: number;
|
|
241
|
+
categories: CategoryStat[];
|
|
242
|
+
topSegments: Segment[];
|
|
243
|
+
topTools: AggregateStat[];
|
|
244
|
+
topPaths: AggregateStat[];
|
|
245
|
+
topTurns: AggregateStat[];
|
|
236
246
|
};
|
|
237
247
|
|
|
238
248
|
type Dataset = {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
id: DatasetId;
|
|
250
|
+
label: string;
|
|
251
|
+
description: string;
|
|
252
|
+
segments: Segment[];
|
|
253
|
+
stats: DatasetStats;
|
|
244
254
|
};
|
|
245
255
|
|
|
246
256
|
type RoutedModel = {
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
provider: string;
|
|
258
|
+
id: string;
|
|
249
259
|
};
|
|
250
260
|
|
|
251
261
|
type ReportData = {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
262
|
+
generatedAt: string;
|
|
263
|
+
cwd: string;
|
|
264
|
+
session: {
|
|
265
|
+
id?: string;
|
|
266
|
+
name?: string;
|
|
267
|
+
file?: string;
|
|
268
|
+
};
|
|
269
|
+
model: {
|
|
270
|
+
provider?: string;
|
|
271
|
+
id?: string;
|
|
272
|
+
contextWindow?: number;
|
|
273
|
+
thinkingLevel?: string;
|
|
274
|
+
virtual?: boolean;
|
|
275
|
+
routed?: RoutedModel;
|
|
276
|
+
};
|
|
277
|
+
contextUsage: {
|
|
278
|
+
tokens: number | null;
|
|
279
|
+
contextWindow: number | null;
|
|
280
|
+
percent: number | null;
|
|
281
|
+
};
|
|
282
|
+
options: {
|
|
283
|
+
redacted: boolean;
|
|
284
|
+
defaultDataset: DatasetId;
|
|
285
|
+
};
|
|
286
|
+
categoryMeta: typeof CATEGORY_META;
|
|
287
|
+
datasets: Record<DatasetId, Dataset>;
|
|
288
|
+
notes: string[];
|
|
279
289
|
};
|
|
280
290
|
|
|
281
291
|
type AnalyzerState = {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
292
|
+
segments: Segment[];
|
|
293
|
+
sequence: number;
|
|
294
|
+
turn: number;
|
|
295
|
+
redact: boolean;
|
|
286
296
|
};
|
|
287
297
|
|
|
288
298
|
function parseArgs(args: string): CommandOptions {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
299
|
+
const options: CommandOptions = {
|
|
300
|
+
open: true,
|
|
301
|
+
keep: false,
|
|
302
|
+
redact: false,
|
|
303
|
+
defaultDataset: "current",
|
|
304
|
+
help: false,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
for (const token of args
|
|
308
|
+
.split(/\s+/)
|
|
309
|
+
.map((part) => part.trim())
|
|
310
|
+
.filter(Boolean)) {
|
|
311
|
+
switch (token) {
|
|
312
|
+
case "--no-open":
|
|
313
|
+
case "--no-browser":
|
|
314
|
+
options.open = false;
|
|
315
|
+
break;
|
|
316
|
+
case "--keep":
|
|
317
|
+
options.keep = true;
|
|
318
|
+
break;
|
|
319
|
+
case "--redact":
|
|
320
|
+
options.redact = true;
|
|
321
|
+
break;
|
|
322
|
+
case "--full":
|
|
323
|
+
options.defaultDataset = "full";
|
|
324
|
+
break;
|
|
325
|
+
case "--current":
|
|
326
|
+
options.defaultDataset = "current";
|
|
327
|
+
break;
|
|
328
|
+
case "--help":
|
|
329
|
+
case "-h":
|
|
330
|
+
helpOption(options);
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return options;
|
|
323
336
|
}
|
|
324
337
|
|
|
325
338
|
function helpOption(options: CommandOptions): void {
|
|
326
|
-
|
|
339
|
+
options.help = true;
|
|
327
340
|
}
|
|
328
341
|
|
|
329
342
|
function usageText(): string {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
343
|
+
return [
|
|
344
|
+
"Usage: /context [--no-open] [--keep] [--redact] [--full]",
|
|
345
|
+
"",
|
|
346
|
+
"Opens a local HTML report showing where the current session context is going.",
|
|
347
|
+
"",
|
|
348
|
+
"Options:",
|
|
349
|
+
" --no-open Write the report but do not open a browser.",
|
|
350
|
+
" --keep Save under " +
|
|
351
|
+
CONFIG_DIR_NAME +
|
|
352
|
+
"/context-reports/ instead of the OS temp directory.",
|
|
353
|
+
" --redact Hide message/tool contents while keeping token attribution.",
|
|
354
|
+
" --full Open the report on the full active branch tab instead of current context.",
|
|
355
|
+
].join("\n");
|
|
341
356
|
}
|
|
342
357
|
|
|
343
358
|
function formatTokens(tokens: number | null | undefined): string {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
359
|
+
if (tokens == null || !Number.isFinite(tokens)) return "?";
|
|
360
|
+
const rounded = Math.round(tokens);
|
|
361
|
+
if (Math.abs(rounded) < 1000) return rounded.toLocaleString();
|
|
362
|
+
if (Math.abs(rounded) < 10_000) return `${(rounded / 1000).toFixed(1)}k`;
|
|
363
|
+
if (Math.abs(rounded) < 1_000_000) return `${Math.round(rounded / 1000).toLocaleString()}k`;
|
|
364
|
+
return `${(rounded / 1_000_000).toFixed(1)}M`;
|
|
350
365
|
}
|
|
351
366
|
|
|
352
367
|
function estimateTextTokens(text: string): number {
|
|
353
|
-
|
|
354
|
-
|
|
368
|
+
if (!text) return 0;
|
|
369
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
355
370
|
}
|
|
356
371
|
|
|
357
372
|
function makeDetail(text: string, redact: boolean): string {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
if (redact) return text ? `[redacted ${text.length.toLocaleString()} characters]` : "";
|
|
374
|
+
if (text.length <= DETAIL_TEXT_LIMIT) return text;
|
|
375
|
+
return `${text.slice(0, DETAIL_TEXT_LIMIT)}\n\n[… ${(
|
|
376
|
+
text.length - DETAIL_TEXT_LIMIT
|
|
377
|
+
).toLocaleString()} more characters omitted from the report]`;
|
|
363
378
|
}
|
|
364
379
|
|
|
365
380
|
function makePreview(text: string, redact: boolean): string {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
381
|
+
if (redact) return text ? `[redacted ${text.length.toLocaleString()} chars]` : "";
|
|
382
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
383
|
+
if (compact.length <= PREVIEW_TEXT_LIMIT) return compact;
|
|
384
|
+
return `${compact.slice(0, PREVIEW_TEXT_LIMIT)}…`;
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
function safeJson(value: unknown): string {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
388
|
+
try {
|
|
389
|
+
return JSON.stringify(value, null, 2) ?? "";
|
|
390
|
+
} catch {
|
|
391
|
+
return String(value);
|
|
392
|
+
}
|
|
378
393
|
}
|
|
379
394
|
|
|
380
395
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
381
|
-
|
|
396
|
+
return typeof value === "object" && value !== null;
|
|
382
397
|
}
|
|
383
398
|
|
|
384
399
|
function isKnownPositiveNumber(value: unknown): value is number {
|
|
385
|
-
|
|
400
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
386
401
|
}
|
|
387
402
|
|
|
388
403
|
function isKnownNonNegativeNumber(value: unknown): value is number {
|
|
389
|
-
|
|
404
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
390
405
|
}
|
|
391
406
|
|
|
392
407
|
function isVirtualModel(model: unknown): boolean {
|
|
393
|
-
|
|
408
|
+
return isRecord(model) && model.api === "pi-virtual";
|
|
394
409
|
}
|
|
395
410
|
|
|
396
411
|
function getContentBlocks(content: unknown): MinimalContentBlock[] {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
412
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
413
|
+
if (!Array.isArray(content)) return [];
|
|
414
|
+
return content.filter(isRecord) as MinimalContentBlock[];
|
|
400
415
|
}
|
|
401
416
|
|
|
402
417
|
function contentText(content: unknown): string {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
418
|
+
if (typeof content === "string") return content;
|
|
419
|
+
if (!Array.isArray(content)) return "";
|
|
420
|
+
const parts: string[] = [];
|
|
421
|
+
for (const block of content) {
|
|
422
|
+
if (!isRecord(block)) continue;
|
|
423
|
+
if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
424
|
+
}
|
|
425
|
+
return parts.join("\n");
|
|
411
426
|
}
|
|
412
427
|
|
|
413
428
|
function isoTimestamp(message: MinimalMessage, entry?: MinimalEntry): string | undefined {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
429
|
+
if (typeof message.timestamp === "number" && Number.isFinite(message.timestamp)) {
|
|
430
|
+
return new Date(message.timestamp).toISOString();
|
|
431
|
+
}
|
|
432
|
+
return entry?.timestamp;
|
|
418
433
|
}
|
|
419
434
|
|
|
420
435
|
function extractLikelyPath(args: Record<string, unknown> | undefined): string | undefined {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
436
|
+
if (!args) return undefined;
|
|
437
|
+
for (const key of ["path", "file", "filePath", "target", "cwd", "glob"] as const) {
|
|
438
|
+
const value = args[key];
|
|
439
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
427
442
|
}
|
|
428
443
|
|
|
429
444
|
function extractLikelyCommand(args: Record<string, unknown> | undefined): string | undefined {
|
|
430
|
-
|
|
431
|
-
|
|
445
|
+
const value = args?.command;
|
|
446
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
432
447
|
}
|
|
433
448
|
|
|
434
449
|
function addSegment(
|
|
435
|
-
|
|
436
|
-
|
|
450
|
+
state: AnalyzerState,
|
|
451
|
+
segment: Omit<Segment, "id" | "sequence" | "preview" | "detail"> & { text: string },
|
|
437
452
|
): void {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function redactSegmentMetadata<T extends Omit<Segment, "id" | "sequence" | "preview" | "detail">>(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
453
|
+
const sequence = state.sequence++;
|
|
454
|
+
const detail = makeDetail(segment.text, state.redact);
|
|
455
|
+
const preview = makePreview(segment.text, state.redact);
|
|
456
|
+
const { text: _text, ...rest } = segment;
|
|
457
|
+
const safeRest = state.redact ? redactSegmentMetadata(rest) : rest;
|
|
458
|
+
state.segments.push({
|
|
459
|
+
...safeRest,
|
|
460
|
+
id: `seg-${sequence}`,
|
|
461
|
+
sequence,
|
|
462
|
+
preview,
|
|
463
|
+
detail,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function redactSegmentMetadata<T extends Omit<Segment, "id" | "sequence" | "preview" | "detail">>(
|
|
468
|
+
segment: T,
|
|
469
|
+
): T {
|
|
470
|
+
return {
|
|
471
|
+
...segment,
|
|
472
|
+
entryId: segment.entryId ? "[redacted entry]" : undefined,
|
|
473
|
+
timestamp: segment.timestamp ? "[redacted timestamp]" : undefined,
|
|
474
|
+
toolCallId: segment.toolCallId ? "[redacted tool call]" : undefined,
|
|
475
|
+
path: segment.path ? "[redacted path]" : undefined,
|
|
476
|
+
command: segment.command ? "[redacted command]" : undefined,
|
|
477
|
+
note: segment.note ? "[redacted note]" : undefined,
|
|
478
|
+
} as T;
|
|
462
479
|
}
|
|
463
480
|
|
|
464
481
|
function addTextSegment(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
482
|
+
state: AnalyzerState,
|
|
483
|
+
category: CategoryId,
|
|
484
|
+
label: string,
|
|
485
|
+
text: string,
|
|
486
|
+
base: {
|
|
487
|
+
source: string;
|
|
488
|
+
role: string;
|
|
489
|
+
entryId?: string;
|
|
490
|
+
timestamp?: string;
|
|
491
|
+
toolName?: string;
|
|
492
|
+
toolCallId?: string;
|
|
493
|
+
path?: string;
|
|
494
|
+
command?: string;
|
|
495
|
+
excluded?: boolean;
|
|
496
|
+
displayOnly?: boolean;
|
|
497
|
+
note?: string;
|
|
498
|
+
turn?: number;
|
|
499
|
+
},
|
|
483
500
|
): void {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
501
|
+
const chars = text.length;
|
|
502
|
+
const tokens = estimateTextTokens(text);
|
|
503
|
+
if (tokens <= 0 && chars <= 0) return;
|
|
504
|
+
addSegment(state, {
|
|
505
|
+
category,
|
|
506
|
+
label,
|
|
507
|
+
source: base.source,
|
|
508
|
+
role: base.role,
|
|
509
|
+
tokens,
|
|
510
|
+
chars,
|
|
511
|
+
turn: base.turn ?? state.turn,
|
|
512
|
+
entryId: base.entryId,
|
|
513
|
+
timestamp: base.timestamp,
|
|
514
|
+
toolName: base.toolName,
|
|
515
|
+
toolCallId: base.toolCallId,
|
|
516
|
+
path: base.path,
|
|
517
|
+
command: base.command,
|
|
518
|
+
excluded: base.excluded,
|
|
519
|
+
displayOnly: base.displayOnly,
|
|
520
|
+
note: base.note,
|
|
521
|
+
text,
|
|
522
|
+
});
|
|
506
523
|
}
|
|
507
524
|
|
|
508
525
|
function addImageSegment(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
526
|
+
state: AnalyzerState,
|
|
527
|
+
label: string,
|
|
528
|
+
base: {
|
|
529
|
+
source: string;
|
|
530
|
+
role: string;
|
|
531
|
+
entryId?: string;
|
|
532
|
+
timestamp?: string;
|
|
533
|
+
toolName?: string;
|
|
534
|
+
toolCallId?: string;
|
|
535
|
+
excluded?: boolean;
|
|
536
|
+
turn?: number;
|
|
537
|
+
mimeType?: string;
|
|
538
|
+
},
|
|
522
539
|
): void {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
540
|
+
addSegment(state, {
|
|
541
|
+
category: "images",
|
|
542
|
+
label,
|
|
543
|
+
source: base.source,
|
|
544
|
+
role: base.role,
|
|
545
|
+
tokens: IMAGE_TOKEN_ESTIMATE,
|
|
546
|
+
chars: IMAGE_TOKEN_ESTIMATE * 4,
|
|
547
|
+
turn: base.turn ?? state.turn,
|
|
548
|
+
entryId: base.entryId,
|
|
549
|
+
timestamp: base.timestamp,
|
|
550
|
+
toolName: base.toolName,
|
|
551
|
+
toolCallId: base.toolCallId,
|
|
552
|
+
excluded: base.excluded,
|
|
553
|
+
note: base.mimeType
|
|
554
|
+
? `Image block (${base.mimeType}); token count is a conservative estimate.`
|
|
555
|
+
: "Image block; token count is a conservative estimate.",
|
|
556
|
+
text: base.mimeType ? `[image: ${base.mimeType}]` : "[image]",
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function analyzeUserContent(
|
|
561
|
+
state: AnalyzerState,
|
|
562
|
+
message: MinimalMessage,
|
|
563
|
+
entry?: MinimalEntry,
|
|
564
|
+
): void {
|
|
565
|
+
state.turn++;
|
|
566
|
+
const timestamp = isoTimestamp(message, entry);
|
|
567
|
+
const entryId = entry?.id;
|
|
568
|
+
const blocks = getContentBlocks(message.content);
|
|
569
|
+
for (const block of blocks) {
|
|
570
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
571
|
+
addTextSegment(state, "user", "User message", block.text, {
|
|
572
|
+
source: "message",
|
|
573
|
+
role: "user",
|
|
574
|
+
entryId,
|
|
575
|
+
timestamp,
|
|
576
|
+
});
|
|
577
|
+
} else if (block.type === "image") {
|
|
578
|
+
addImageSegment(state, "User image", {
|
|
579
|
+
source: "message",
|
|
580
|
+
role: "user",
|
|
581
|
+
entryId,
|
|
582
|
+
timestamp,
|
|
583
|
+
mimeType: block.mimeType ?? block.source?.mediaType,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function analyzeAssistantContent(
|
|
590
|
+
state: AnalyzerState,
|
|
591
|
+
message: MinimalMessage,
|
|
592
|
+
entry?: MinimalEntry,
|
|
593
|
+
): void {
|
|
594
|
+
const timestamp = isoTimestamp(message, entry);
|
|
595
|
+
const entryId = entry?.id;
|
|
596
|
+
const blocks = getContentBlocks(message.content);
|
|
597
|
+
for (const block of blocks) {
|
|
598
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
599
|
+
addTextSegment(state, "assistant", "Assistant response", block.text, {
|
|
600
|
+
source: "message",
|
|
601
|
+
role: "assistant",
|
|
602
|
+
entryId,
|
|
603
|
+
timestamp,
|
|
604
|
+
});
|
|
605
|
+
} else if (block.type === "thinking" && typeof block.thinking === "string") {
|
|
606
|
+
addTextSegment(state, "thinking", "Assistant thinking", block.thinking, {
|
|
607
|
+
source: "message",
|
|
608
|
+
role: "assistant",
|
|
609
|
+
entryId,
|
|
610
|
+
timestamp,
|
|
611
|
+
});
|
|
612
|
+
} else if (block.type === "toolCall" && typeof block.name === "string") {
|
|
613
|
+
const args = isRecord(block.arguments) ? block.arguments : {};
|
|
614
|
+
const toolCallText = `${block.name}(${safeJson(args)})`;
|
|
615
|
+
addTextSegment(state, "toolCalls", `Tool call: ${block.name}`, toolCallText, {
|
|
616
|
+
source: "tool-call",
|
|
617
|
+
role: "assistant",
|
|
618
|
+
entryId,
|
|
619
|
+
timestamp,
|
|
620
|
+
toolName: block.name,
|
|
621
|
+
toolCallId: block.id,
|
|
622
|
+
path: extractLikelyPath(args),
|
|
623
|
+
command: extractLikelyCommand(args),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
600
627
|
}
|
|
601
628
|
|
|
602
629
|
function nestedCallsNote(message: MinimalMessage): string | undefined {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function analyzeToolResult(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
630
|
+
const nested = message.nestedCalls;
|
|
631
|
+
if (!isRecord(nested) || !Array.isArray(nested.calls)) return undefined;
|
|
632
|
+
const count = nested.calls.length;
|
|
633
|
+
if (nested.complete === false) {
|
|
634
|
+
return `Pi recorded ${count} bounded nested tool call${count === 1 ? "" : "s"} incompletely; nested results are not transcript context and are excluded from attribution.`;
|
|
635
|
+
}
|
|
636
|
+
return `Pi recorded ${count} bounded nested tool call${count === 1 ? "" : "s"}; nested results are not transcript context and are excluded from attribution.`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function analyzeToolResult(
|
|
640
|
+
state: AnalyzerState,
|
|
641
|
+
message: MinimalMessage,
|
|
642
|
+
entry?: MinimalEntry,
|
|
643
|
+
): void {
|
|
644
|
+
const timestamp = isoTimestamp(message, entry);
|
|
645
|
+
const entryId = entry?.id;
|
|
646
|
+
const toolName = message.toolName ?? "tool";
|
|
647
|
+
const blocks = getContentBlocks(message.content);
|
|
648
|
+
const nestedNote = nestedCallsNote(message);
|
|
649
|
+
for (const block of blocks) {
|
|
650
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
651
|
+
const noteParts: string[] = [];
|
|
652
|
+
if (message.details && isRecord(message.details)) {
|
|
653
|
+
if (isRecord(message.details.truncation) && message.details.truncation.truncated)
|
|
654
|
+
noteParts.push("Result was truncated before entering context.");
|
|
655
|
+
if (typeof message.details.fullOutputPath === "string")
|
|
656
|
+
noteParts.push(`Full output: ${message.details.fullOutputPath}`);
|
|
657
|
+
}
|
|
658
|
+
if (nestedNote) noteParts.push(nestedNote);
|
|
659
|
+
addTextSegment(state, "toolResults", `Tool result: ${toolName}`, block.text, {
|
|
660
|
+
source: "tool-result",
|
|
661
|
+
role: "toolResult",
|
|
662
|
+
entryId,
|
|
663
|
+
timestamp,
|
|
664
|
+
toolName,
|
|
665
|
+
toolCallId: message.toolCallId,
|
|
666
|
+
note: noteParts.join(" ") || undefined,
|
|
667
|
+
});
|
|
668
|
+
} else if (block.type === "image") {
|
|
669
|
+
addImageSegment(state, `Tool image: ${toolName}`, {
|
|
670
|
+
source: "tool-result",
|
|
671
|
+
role: "toolResult",
|
|
672
|
+
entryId,
|
|
673
|
+
timestamp,
|
|
674
|
+
toolName,
|
|
675
|
+
toolCallId: message.toolCallId,
|
|
676
|
+
mimeType: block.mimeType ?? block.source?.mediaType,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
647
680
|
}
|
|
648
681
|
|
|
649
682
|
function bashExecutionToModelText(message: MinimalMessage): string {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function analyzeBashExecution(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
683
|
+
let text = `Ran \`${message.command ?? ""}\`\n`;
|
|
684
|
+
if (message.output) text += `\`\`\`\n${message.output}\n\`\`\``;
|
|
685
|
+
else text += "(no output)";
|
|
686
|
+
if (message.cancelled) text += "\n\n(command cancelled)";
|
|
687
|
+
else if (message.exitCode !== null && message.exitCode !== undefined && message.exitCode !== 0) {
|
|
688
|
+
text += `\n\nCommand exited with code ${message.exitCode}`;
|
|
689
|
+
}
|
|
690
|
+
if (message.truncated && message.fullOutputPath) {
|
|
691
|
+
text += `\n\n[Output truncated. Full output: ${message.fullOutputPath}]`;
|
|
692
|
+
}
|
|
693
|
+
return text;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function analyzeBashExecution(
|
|
697
|
+
state: AnalyzerState,
|
|
698
|
+
message: MinimalMessage,
|
|
699
|
+
entry?: MinimalEntry,
|
|
700
|
+
): void {
|
|
701
|
+
state.turn++;
|
|
702
|
+
const timestamp = isoTimestamp(message, entry);
|
|
703
|
+
const text = bashExecutionToModelText(message);
|
|
704
|
+
const excluded = message.excludeFromContext === true;
|
|
705
|
+
addTextSegment(state, "bash", "User bash execution", text, {
|
|
706
|
+
source: "bash-execution",
|
|
707
|
+
role: "bashExecution",
|
|
708
|
+
entryId: entry?.id,
|
|
709
|
+
timestamp,
|
|
710
|
+
command: message.command,
|
|
711
|
+
excluded,
|
|
712
|
+
note: excluded
|
|
713
|
+
? "This was marked excludeFromContext by pi (!! command). It is shown as session-only context."
|
|
714
|
+
: undefined,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function analyzeCustomMessage(
|
|
719
|
+
state: AnalyzerState,
|
|
720
|
+
message: MinimalMessage,
|
|
721
|
+
entry?: MinimalEntry,
|
|
722
|
+
): void {
|
|
723
|
+
state.turn++;
|
|
724
|
+
const timestamp = isoTimestamp(message, entry);
|
|
725
|
+
const text = contentText(message.content);
|
|
726
|
+
addTextSegment(
|
|
727
|
+
state,
|
|
728
|
+
"custom",
|
|
729
|
+
message.customType ? `Custom context: ${message.customType}` : "Custom context",
|
|
730
|
+
text,
|
|
731
|
+
{
|
|
732
|
+
source: "custom-message",
|
|
733
|
+
role: "custom",
|
|
734
|
+
entryId: entry?.id,
|
|
735
|
+
timestamp,
|
|
736
|
+
displayOnly: message.display === false,
|
|
737
|
+
note:
|
|
738
|
+
message.display === false
|
|
739
|
+
? "Hidden in TUI, but sent to the model as custom context."
|
|
740
|
+
: undefined,
|
|
741
|
+
},
|
|
742
|
+
);
|
|
743
|
+
for (const block of getContentBlocks(message.content)) {
|
|
744
|
+
if (block.type === "image") {
|
|
745
|
+
addImageSegment(
|
|
746
|
+
state,
|
|
747
|
+
message.customType ? `Custom image: ${message.customType}` : "Custom image",
|
|
748
|
+
{
|
|
749
|
+
source: "custom-message",
|
|
750
|
+
role: "custom",
|
|
751
|
+
entryId: entry?.id,
|
|
752
|
+
timestamp,
|
|
753
|
+
mimeType: block.mimeType ?? block.source?.mediaType,
|
|
754
|
+
},
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function analyzeSummaryMessage(
|
|
761
|
+
state: AnalyzerState,
|
|
762
|
+
message: MinimalMessage,
|
|
763
|
+
entry?: MinimalEntry,
|
|
764
|
+
): void {
|
|
765
|
+
const isCompaction = message.role === "compactionSummary" || entry?.type === "compaction";
|
|
766
|
+
const label = isCompaction ? "Compaction summary" : "Branch summary";
|
|
767
|
+
const summary = message.summary ?? entry?.summary ?? "";
|
|
768
|
+
const modelText = isCompaction
|
|
769
|
+
? `${COMPACTION_SUMMARY_PREFIX}${summary}${COMPACTION_SUMMARY_SUFFIX}`
|
|
770
|
+
: `${BRANCH_SUMMARY_PREFIX}${summary}${BRANCH_SUMMARY_SUFFIX}`;
|
|
771
|
+
addTextSegment(state, "summaries", label, modelText, {
|
|
772
|
+
source: isCompaction ? "compaction" : "branch-summary",
|
|
773
|
+
role: message.role ?? (isCompaction ? "compactionSummary" : "branchSummary"),
|
|
774
|
+
entryId: entry?.id,
|
|
775
|
+
timestamp: isoTimestamp(message, entry),
|
|
776
|
+
note:
|
|
777
|
+
isCompaction && typeof message.tokensBefore === "number"
|
|
778
|
+
? `This summary replaced about ${formatTokens(message.tokensBefore)} earlier tokens before compaction.`
|
|
779
|
+
: undefined,
|
|
780
|
+
});
|
|
722
781
|
}
|
|
723
782
|
|
|
724
783
|
function analyzeMessage(state: AnalyzerState, message: MinimalMessage, entry?: MinimalEntry): void {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
784
|
+
switch (message.role) {
|
|
785
|
+
case "user":
|
|
786
|
+
analyzeUserContent(state, message, entry);
|
|
787
|
+
break;
|
|
788
|
+
case "assistant":
|
|
789
|
+
analyzeAssistantContent(state, message, entry);
|
|
790
|
+
break;
|
|
791
|
+
case "toolResult":
|
|
792
|
+
analyzeToolResult(state, message, entry);
|
|
793
|
+
break;
|
|
794
|
+
case "bashExecution":
|
|
795
|
+
analyzeBashExecution(state, message, entry);
|
|
796
|
+
break;
|
|
797
|
+
case "custom":
|
|
798
|
+
analyzeCustomMessage(state, message, entry);
|
|
799
|
+
break;
|
|
800
|
+
case "branchSummary":
|
|
801
|
+
case "compactionSummary":
|
|
802
|
+
analyzeSummaryMessage(state, message, entry);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
746
805
|
}
|
|
747
806
|
|
|
748
807
|
function contextEntryToMessage(entry: MinimalEntry): MinimalMessage | undefined {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
808
|
+
if (entry.type === "message") return entry.message;
|
|
809
|
+
if (entry.type === "custom_message") {
|
|
810
|
+
return {
|
|
811
|
+
role: "custom",
|
|
812
|
+
customType: entry.customType,
|
|
813
|
+
content: entry.content,
|
|
814
|
+
display: entry.display,
|
|
815
|
+
details: entry.details,
|
|
816
|
+
timestamp: entry.timestamp ? new Date(entry.timestamp).getTime() : undefined,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
if (entry.type === "branch_summary") {
|
|
820
|
+
return {
|
|
821
|
+
role: "branchSummary",
|
|
822
|
+
summary: entry.summary,
|
|
823
|
+
fromId: entry.fromId,
|
|
824
|
+
timestamp: entry.timestamp ? new Date(entry.timestamp).getTime() : undefined,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
if (entry.type === "compaction") {
|
|
828
|
+
return {
|
|
829
|
+
role: "compactionSummary",
|
|
830
|
+
summary: entry.summary,
|
|
831
|
+
tokensBefore: entry.tokensBefore,
|
|
832
|
+
timestamp: entry.timestamp ? new Date(entry.timestamp).getTime() : undefined,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
return undefined;
|
|
777
836
|
}
|
|
778
837
|
|
|
779
838
|
function isContextEntry(entry: MinimalEntry): boolean {
|
|
780
|
-
|
|
839
|
+
return (
|
|
840
|
+
entry.type === "message" || entry.type === "custom_message" || entry.type === "branch_summary"
|
|
841
|
+
);
|
|
781
842
|
}
|
|
782
843
|
|
|
783
844
|
function collectCurrentContextEntries(branchEntries: MinimalEntry[]): MinimalEntry[] {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
845
|
+
let compactionIndex = -1;
|
|
846
|
+
for (let i = branchEntries.length - 1; i >= 0; i--) {
|
|
847
|
+
if (branchEntries[i]?.type === "compaction") {
|
|
848
|
+
compactionIndex = i;
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (compactionIndex === -1) return branchEntries.filter(isContextEntry);
|
|
854
|
+
|
|
855
|
+
const result: MinimalEntry[] = [branchEntries[compactionIndex]];
|
|
856
|
+
const compaction = branchEntries[compactionIndex];
|
|
857
|
+
let foundFirstKept = false;
|
|
858
|
+
for (let i = 0; i < compactionIndex; i++) {
|
|
859
|
+
const entry = branchEntries[i];
|
|
860
|
+
if (entry?.id === compaction?.firstKeptEntryId) foundFirstKept = true;
|
|
861
|
+
if (foundFirstKept && isContextEntry(entry)) result.push(entry);
|
|
862
|
+
}
|
|
863
|
+
for (let i = compactionIndex + 1; i < branchEntries.length; i++) {
|
|
864
|
+
const entry = branchEntries[i];
|
|
865
|
+
if (isContextEntry(entry)) result.push(entry);
|
|
866
|
+
}
|
|
867
|
+
return result;
|
|
807
868
|
}
|
|
808
869
|
|
|
809
870
|
function isProjectionSourceEntry(value: unknown): value is MinimalEntry {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
871
|
+
return (
|
|
872
|
+
isRecord(value) &&
|
|
873
|
+
typeof value.type === "string" &&
|
|
874
|
+
value.type.length > 0 &&
|
|
875
|
+
typeof value.id === "string" &&
|
|
876
|
+
value.id.length > 0
|
|
877
|
+
);
|
|
815
878
|
}
|
|
816
879
|
|
|
817
880
|
function isProjectionMessage(value: unknown): value is MinimalMessage {
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function resolveSessionProjection(
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
881
|
+
return isRecord(value) && typeof value.role === "string" && value.role.length > 0;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function resolveSessionProjection(
|
|
885
|
+
ctx: ExtensionCommandContext,
|
|
886
|
+
): MinimalSessionProjection | undefined {
|
|
887
|
+
const sessionManager = ctx.sessionManager as unknown as {
|
|
888
|
+
buildSessionProjection?: () => unknown;
|
|
889
|
+
};
|
|
890
|
+
const buildProjection = sessionManager.buildSessionProjection;
|
|
891
|
+
if (typeof buildProjection !== "function") return undefined;
|
|
892
|
+
|
|
893
|
+
const projection = safeCall(() => buildProjection.call(sessionManager));
|
|
894
|
+
if (!isRecord(projection) || !Array.isArray(projection.entries)) return undefined;
|
|
895
|
+
|
|
896
|
+
const entries: MinimalProjectionEntry[] = [];
|
|
897
|
+
for (const candidate of projection.entries) {
|
|
898
|
+
if (
|
|
899
|
+
!isRecord(candidate) ||
|
|
900
|
+
!isProjectionSourceEntry(candidate.sourceEntry) ||
|
|
901
|
+
!Array.isArray(candidate.messages)
|
|
902
|
+
)
|
|
903
|
+
return undefined;
|
|
904
|
+
if (!candidate.messages.every(isProjectionMessage)) return undefined;
|
|
905
|
+
entries.push({
|
|
906
|
+
sourceEntry: candidate.sourceEntry,
|
|
907
|
+
messages: candidate.messages,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
// `entries` is the provenance-bearing canonical projection. Keep validating
|
|
911
|
+
// the optional flattened field for compatibility, but derive the list used by
|
|
912
|
+
// the report from entries so a stale/invented flattened list cannot reintroduce
|
|
913
|
+
// omitted history or nested metadata as transcript messages.
|
|
914
|
+
if (
|
|
915
|
+
"messages" in projection &&
|
|
916
|
+
(!Array.isArray(projection.messages) || !projection.messages.every(isProjectionMessage))
|
|
917
|
+
)
|
|
918
|
+
return undefined;
|
|
919
|
+
return {
|
|
920
|
+
entries,
|
|
921
|
+
messages: entries.flatMap((entry) => entry.messages),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function resolveCurrentContextEntries(
|
|
926
|
+
ctx: ExtensionCommandContext,
|
|
927
|
+
branchEntries: MinimalEntry[],
|
|
928
|
+
): MinimalEntry[] {
|
|
929
|
+
const sessionManager = ctx.sessionManager as unknown as { buildContextEntries?: () => unknown };
|
|
930
|
+
const buildEntries = sessionManager.buildContextEntries;
|
|
931
|
+
const currentEntries =
|
|
932
|
+
typeof buildEntries === "function"
|
|
933
|
+
? safeCall(() => buildEntries.call(sessionManager))
|
|
934
|
+
: undefined;
|
|
935
|
+
if (Array.isArray(currentEntries)) return currentEntries as MinimalEntry[];
|
|
936
|
+
return collectCurrentContextEntries(branchEntries);
|
|
857
937
|
}
|
|
858
938
|
|
|
859
939
|
function analyzeEntries(entries: MinimalEntry[], redact: boolean): AnalysisResult {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
940
|
+
const state: AnalyzerState = { segments: [], sequence: 0, turn: 0, redact };
|
|
941
|
+
let messageCount = 0;
|
|
942
|
+
for (const entry of entries) {
|
|
943
|
+
const message = contextEntryToMessage(entry);
|
|
944
|
+
if (!message) continue;
|
|
945
|
+
messageCount++;
|
|
946
|
+
analyzeMessage(state, message, entry);
|
|
947
|
+
}
|
|
948
|
+
return { segments: state.segments, messageCount };
|
|
869
949
|
}
|
|
870
950
|
|
|
871
951
|
function analyzeProjection(projection: MinimalSessionProjection, redact: boolean): AnalysisResult {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function resolveCurrentContextAnalysis(
|
|
887
|
-
ctx: ExtensionCommandContext,
|
|
888
|
-
branchEntries: MinimalEntry[],
|
|
889
|
-
redact: boolean,
|
|
890
|
-
): AnalysisResult {
|
|
891
|
-
const projection = resolveSessionProjection(ctx);
|
|
892
|
-
if (projection) return analyzeProjection(projection, redact);
|
|
893
|
-
return analyzeEntries(resolveCurrentContextEntries(ctx, branchEntries), redact);
|
|
952
|
+
const state: AnalyzerState = { segments: [], sequence: 0, turn: 0, redact };
|
|
953
|
+
let messageCount = 0;
|
|
954
|
+
for (const projectedEntry of projection.entries) {
|
|
955
|
+
for (const message of projectedEntry.messages) {
|
|
956
|
+
// Pi's projection may carry the persisted system message on a compaction
|
|
957
|
+
// entry; the active system prompt is accounted for separately as overhead.
|
|
958
|
+
if (message.role === "system") continue;
|
|
959
|
+
messageCount++;
|
|
960
|
+
analyzeMessage(state, message, projectedEntry.sourceEntry);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return { segments: state.segments, messageCount };
|
|
894
964
|
}
|
|
895
965
|
|
|
896
966
|
function collectToolSchemaText(pi: ExtensionAPI): string {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
967
|
+
try {
|
|
968
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
969
|
+
const tools = pi.getAllTools().filter((tool) => activeToolNames.has(tool.name));
|
|
970
|
+
return tools
|
|
971
|
+
.map((tool) =>
|
|
972
|
+
safeJson({
|
|
973
|
+
name: tool.name,
|
|
974
|
+
description: tool.description,
|
|
975
|
+
parameters: tool.parameters,
|
|
976
|
+
source: tool.sourceInfo,
|
|
977
|
+
}),
|
|
978
|
+
)
|
|
979
|
+
.join("\n\n");
|
|
980
|
+
} catch {
|
|
981
|
+
return "";
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function buildOverheadSegments(
|
|
986
|
+
pi: ExtensionAPI,
|
|
987
|
+
ctx: ExtensionCommandContext,
|
|
988
|
+
redact: boolean,
|
|
989
|
+
): Segment[] {
|
|
990
|
+
const state: AnalyzerState = { segments: [], sequence: -2, turn: 0, redact };
|
|
991
|
+
const systemPrompt = safeCall(() => ctx.getSystemPrompt()) ?? "";
|
|
992
|
+
addTextSegment(state, "system", "Current system prompt", systemPrompt, {
|
|
993
|
+
source: "system-prompt",
|
|
994
|
+
role: "system",
|
|
995
|
+
turn: 0,
|
|
996
|
+
});
|
|
997
|
+
const toolSchemaText = collectToolSchemaText(pi);
|
|
998
|
+
addTextSegment(state, "toolSchemas", "Active tool schemas", toolSchemaText, {
|
|
999
|
+
source: "tool-schema",
|
|
1000
|
+
role: "system",
|
|
1001
|
+
turn: 0,
|
|
1002
|
+
});
|
|
1003
|
+
return state.segments.map((segment, index) => ({
|
|
1004
|
+
...segment,
|
|
1005
|
+
id: `overhead-${index}`,
|
|
1006
|
+
sequence: -2 + index,
|
|
1007
|
+
}));
|
|
928
1008
|
}
|
|
929
1009
|
|
|
930
1010
|
function safeCall<T>(fn: () => T): T | undefined {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1011
|
+
try {
|
|
1012
|
+
return fn();
|
|
1013
|
+
} catch {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
936
1016
|
}
|
|
937
1017
|
|
|
938
1018
|
function aggregateBy(
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1019
|
+
segments: Segment[],
|
|
1020
|
+
keyFn: (segment: Segment) => string | undefined,
|
|
1021
|
+
labelFn: (key: string) => string = (key) => key,
|
|
942
1022
|
): AggregateStat[] {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1023
|
+
const map = new Map<string, { tokens: number; segments: number }>();
|
|
1024
|
+
for (const segment of segments) {
|
|
1025
|
+
if (segment.excluded) continue;
|
|
1026
|
+
const key = keyFn(segment);
|
|
1027
|
+
if (!key) continue;
|
|
1028
|
+
const current = map.get(key) ?? { tokens: 0, segments: 0 };
|
|
1029
|
+
current.tokens += segment.tokens;
|
|
1030
|
+
current.segments++;
|
|
1031
|
+
map.set(key, current);
|
|
1032
|
+
}
|
|
1033
|
+
const total = Array.from(map.values()).reduce((sum, stat) => sum + stat.tokens, 0);
|
|
1034
|
+
return Array.from(map.entries())
|
|
1035
|
+
.map(([key, stat]) => ({
|
|
1036
|
+
key,
|
|
1037
|
+
label: labelFn(key),
|
|
1038
|
+
tokens: Math.round(stat.tokens),
|
|
1039
|
+
segments: stat.segments,
|
|
1040
|
+
percent: total > 0 ? (stat.tokens / total) * 100 : 0,
|
|
1041
|
+
}))
|
|
1042
|
+
.sort((a, b) => b.tokens - a.tokens)
|
|
1043
|
+
.slice(0, 12);
|
|
964
1044
|
}
|
|
965
1045
|
|
|
966
1046
|
function finalizeDataset(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1047
|
+
id: DatasetId,
|
|
1048
|
+
label: string,
|
|
1049
|
+
description: string,
|
|
1050
|
+
segments: Segment[],
|
|
1051
|
+
messageCount: number,
|
|
1052
|
+
authoritativeTokens: number | null,
|
|
973
1053
|
): Dataset {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1054
|
+
const visibleSegments = segments.filter((segment) => !segment.excluded);
|
|
1055
|
+
const visibleRawTokens = visibleSegments.reduce((sum, segment) => sum + segment.tokens, 0);
|
|
1056
|
+
const excludedTokens = segments
|
|
1057
|
+
.filter((segment) => segment.excluded)
|
|
1058
|
+
.reduce((sum, segment) => sum + segment.tokens, 0);
|
|
1059
|
+
const providerDeltaTokens =
|
|
1060
|
+
authoritativeTokens != null
|
|
1061
|
+
? Math.max(0, Math.round(authoritativeTokens - visibleRawTokens))
|
|
1062
|
+
: 0;
|
|
1063
|
+
const estimatorOverageTokens =
|
|
1064
|
+
authoritativeTokens != null
|
|
1065
|
+
? Math.max(0, Math.round(visibleRawTokens - authoritativeTokens))
|
|
1066
|
+
: 0;
|
|
1067
|
+
const reconciliationScale =
|
|
1068
|
+
authoritativeTokens != null && visibleRawTokens > authoritativeTokens && visibleRawTokens > 0
|
|
1069
|
+
? authoritativeTokens / visibleRawTokens
|
|
1070
|
+
: 1;
|
|
1071
|
+
const displayTotal = authoritativeTokens ?? visibleRawTokens;
|
|
1072
|
+
|
|
1073
|
+
const byCategory = new Map<CategoryId, { tokens: number; segments: number }>();
|
|
1074
|
+
for (const segment of visibleSegments) {
|
|
1075
|
+
const current = byCategory.get(segment.category) ?? { tokens: 0, segments: 0 };
|
|
1076
|
+
current.tokens += segment.tokens;
|
|
1077
|
+
current.segments++;
|
|
1078
|
+
byCategory.set(segment.category, current);
|
|
1079
|
+
}
|
|
1080
|
+
if (providerDeltaTokens > 0) {
|
|
1081
|
+
byCategory.set("providerDelta", { tokens: providerDeltaTokens, segments: 1 });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const categories = (Object.keys(CATEGORY_META) as CategoryId[])
|
|
1085
|
+
.map((categoryId) => {
|
|
1086
|
+
const current = byCategory.get(categoryId) ?? { tokens: 0, segments: 0 };
|
|
1087
|
+
const rawTokens = current.tokens;
|
|
1088
|
+
const displayTokens =
|
|
1089
|
+
categoryId === "providerDelta" ? rawTokens : Math.round(rawTokens * reconciliationScale);
|
|
1090
|
+
const meta = CATEGORY_META[categoryId];
|
|
1091
|
+
return {
|
|
1092
|
+
id: categoryId,
|
|
1093
|
+
label: meta.label,
|
|
1094
|
+
shortLabel: meta.shortLabel,
|
|
1095
|
+
color: meta.color,
|
|
1096
|
+
description: meta.description,
|
|
1097
|
+
tokens: Math.round(rawTokens),
|
|
1098
|
+
displayTokens,
|
|
1099
|
+
percent: displayTotal > 0 ? (displayTokens / displayTotal) * 100 : 0,
|
|
1100
|
+
segments: current.segments,
|
|
1101
|
+
};
|
|
1102
|
+
})
|
|
1103
|
+
.filter((stat) => stat.tokens > 0 || stat.displayTokens > 0)
|
|
1104
|
+
.sort((a, b) => b.displayTokens - a.displayTokens);
|
|
1105
|
+
|
|
1106
|
+
return {
|
|
1107
|
+
id,
|
|
1108
|
+
label,
|
|
1109
|
+
description,
|
|
1110
|
+
segments,
|
|
1111
|
+
stats: {
|
|
1112
|
+
tokens: Math.round(displayTotal),
|
|
1113
|
+
rawTokens: Math.round(segments.reduce((sum, segment) => sum + segment.tokens, 0)),
|
|
1114
|
+
visibleRawTokens: Math.round(visibleRawTokens),
|
|
1115
|
+
excludedTokens: Math.round(excludedTokens),
|
|
1116
|
+
providerDeltaTokens,
|
|
1117
|
+
estimatorOverageTokens,
|
|
1118
|
+
reconciliationScale,
|
|
1119
|
+
segmentCount: segments.length,
|
|
1120
|
+
messageCount,
|
|
1121
|
+
categories,
|
|
1122
|
+
topSegments: [...visibleSegments].sort((a, b) => b.tokens - a.tokens).slice(0, 24),
|
|
1123
|
+
topTools: aggregateBy(visibleSegments, (segment) => segment.toolName),
|
|
1124
|
+
topPaths: aggregateBy(visibleSegments, (segment) => segment.path),
|
|
1125
|
+
topTurns: aggregateBy(
|
|
1126
|
+
visibleSegments,
|
|
1127
|
+
(segment) => (segment.turn > 0 ? String(segment.turn) : undefined),
|
|
1128
|
+
(key) => `Turn ${key}`,
|
|
1129
|
+
),
|
|
1130
|
+
},
|
|
1131
|
+
};
|
|
1040
1132
|
}
|
|
1041
1133
|
|
|
1042
1134
|
function getSessionName(ctx: ExtensionCommandContext): string | undefined {
|
|
1043
|
-
|
|
1135
|
+
return safeCall(() => ctx.sessionManager.getSessionName());
|
|
1044
1136
|
}
|
|
1045
1137
|
|
|
1046
1138
|
function latestRoutedModel(messages: MinimalMessage[]): RoutedModel | undefined {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1139
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1140
|
+
const message = messages[i];
|
|
1141
|
+
if (message?.role !== "assistant") continue;
|
|
1142
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") continue;
|
|
1143
|
+
if (message.api === "pi-virtual") continue;
|
|
1144
|
+
if (
|
|
1145
|
+
typeof message.provider !== "string" ||
|
|
1146
|
+
!message.provider ||
|
|
1147
|
+
typeof message.model !== "string" ||
|
|
1148
|
+
!message.model
|
|
1149
|
+
)
|
|
1150
|
+
continue;
|
|
1151
|
+
return { provider: message.provider, id: message.model };
|
|
1152
|
+
}
|
|
1153
|
+
return undefined;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function buildReportData(
|
|
1157
|
+
pi: ExtensionAPI,
|
|
1158
|
+
ctx: ExtensionCommandContext,
|
|
1159
|
+
options: CommandOptions,
|
|
1160
|
+
): ReportData {
|
|
1161
|
+
const branchEntries = ctx.sessionManager.getBranch() as MinimalEntry[];
|
|
1162
|
+
const projection = resolveSessionProjection(ctx);
|
|
1163
|
+
const overhead = buildOverheadSegments(pi, ctx, options.redact);
|
|
1164
|
+
const currentAnalysis = projection
|
|
1165
|
+
? analyzeProjection(projection, options.redact)
|
|
1166
|
+
: analyzeEntries(resolveCurrentContextEntries(ctx, branchEntries), options.redact);
|
|
1167
|
+
const fullAnalysis = analyzeEntries(branchEntries, options.redact);
|
|
1168
|
+
const usage = ctx.getContextUsage();
|
|
1169
|
+
const contextTokens = isKnownNonNegativeNumber(usage?.tokens) ? usage.tokens : null;
|
|
1170
|
+
const reportedContextWindow = isKnownPositiveNumber(usage?.contextWindow)
|
|
1171
|
+
? usage.contextWindow
|
|
1172
|
+
: undefined;
|
|
1173
|
+
const selectedContextWindow = isKnownPositiveNumber(ctx.model?.contextWindow)
|
|
1174
|
+
? ctx.model.contextWindow
|
|
1175
|
+
: undefined;
|
|
1176
|
+
const virtualSelection = isVirtualModel(ctx.model);
|
|
1177
|
+
const contextWindow =
|
|
1178
|
+
reportedContextWindow ?? (virtualSelection ? null : (selectedContextWindow ?? null));
|
|
1179
|
+
const reportedPercent = isKnownNonNegativeNumber(usage?.percent) ? usage.percent : undefined;
|
|
1180
|
+
const contextPercent =
|
|
1181
|
+
reportedPercent ??
|
|
1182
|
+
(contextTokens != null && contextWindow ? (contextTokens / contextWindow) * 100 : null);
|
|
1183
|
+
const projectedMessages = projection
|
|
1184
|
+
? projection.entries.flatMap((entry) => entry.messages)
|
|
1185
|
+
: branchEntries.flatMap((entry) => {
|
|
1186
|
+
const message = contextEntryToMessage(entry);
|
|
1187
|
+
return message ? [message] : [];
|
|
1188
|
+
});
|
|
1189
|
+
const routed = virtualSelection ? latestRoutedModel(projectedMessages) : undefined;
|
|
1190
|
+
|
|
1191
|
+
const currentSegments = [...overhead, ...currentAnalysis.segments];
|
|
1192
|
+
const fullSegments = [...overhead, ...fullAnalysis.segments];
|
|
1193
|
+
const currentDataset = finalizeDataset(
|
|
1194
|
+
"current",
|
|
1195
|
+
"Current model context",
|
|
1196
|
+
"What pi is expected to send to the model on the next turn: latest compaction summary plus unsummarized messages, system prompt, and active tool schemas.",
|
|
1197
|
+
currentSegments,
|
|
1198
|
+
currentAnalysis.messageCount,
|
|
1199
|
+
contextTokens,
|
|
1200
|
+
);
|
|
1201
|
+
const fullDataset = finalizeDataset(
|
|
1202
|
+
"full",
|
|
1203
|
+
"Full active branch history",
|
|
1204
|
+
"Every context-bearing entry on the active branch, including pre-compaction history that may no longer be sent verbatim.",
|
|
1205
|
+
fullSegments,
|
|
1206
|
+
fullAnalysis.messageCount,
|
|
1207
|
+
null,
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
const notes: string[] = [
|
|
1211
|
+
"Per-component token counts are local estimates based mostly on characters/4. Providers expose aggregate usage, not exact per-message attribution.",
|
|
1212
|
+
"The current-context chart reconciles to pi's footer-compatible context total when that total is known.",
|
|
1213
|
+
];
|
|
1214
|
+
if (options.redact) {
|
|
1215
|
+
notes.push(
|
|
1216
|
+
"Redaction is enabled: message contents, paths, commands, session identifiers, and timestamps are hidden in this report.",
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
if (contextTokens == null) {
|
|
1220
|
+
notes.push(
|
|
1221
|
+
"Current context usage is unknown, usually because compaction just ran and no model response has arrived yet.",
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
if (virtualSelection) {
|
|
1225
|
+
const selected =
|
|
1226
|
+
[ctx.model?.provider, ctx.model?.id].filter(Boolean).join("/") || "virtual selection";
|
|
1227
|
+
if (routed) {
|
|
1228
|
+
notes.push(
|
|
1229
|
+
`Model selection ${selected} is virtual. Latest routed physical response: ${routed.provider}/${routed.id}. The context window shown below is provider-reported usage when available; it is not inferred from the virtual selection.`,
|
|
1230
|
+
);
|
|
1231
|
+
} else {
|
|
1232
|
+
notes.push(
|
|
1233
|
+
`Model selection ${selected} is virtual. Pi has not exposed a routed physical response here, so no provider context window is inferred.`,
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
} else if (contextWindow == null) {
|
|
1237
|
+
notes.push(
|
|
1238
|
+
"The selected model's context window is unknown because pi did not provide a finite positive limit.",
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
if (currentDataset.stats.estimatorOverageTokens > 0) {
|
|
1242
|
+
notes.push(
|
|
1243
|
+
`Local component estimates exceed pi's footer total by ${formatTokens(currentDataset.stats.estimatorOverageTokens)} tokens, so chart slices are scaled down proportionally.`,
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
if (currentDataset.stats.providerDeltaTokens > 0) {
|
|
1247
|
+
notes.push(
|
|
1248
|
+
`${formatTokens(currentDataset.stats.providerDeltaTokens)} tokens are unattributed provider/serialization delta: system serialization, provider tokenization, cache accounting, or schema overhead not explained by local estimates.`,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
return {
|
|
1253
|
+
generatedAt: new Date().toISOString(),
|
|
1254
|
+
cwd: options.redact ? "[redacted cwd]" : ctx.cwd,
|
|
1255
|
+
session: {
|
|
1256
|
+
id: options.redact ? "[redacted session]" : safeCall(() => ctx.sessionManager.getSessionId()),
|
|
1257
|
+
name: options.redact ? "[redacted session name]" : getSessionName(ctx),
|
|
1258
|
+
file: options.redact
|
|
1259
|
+
? "[redacted session file]"
|
|
1260
|
+
: safeCall(() => ctx.sessionManager.getSessionFile()),
|
|
1261
|
+
},
|
|
1262
|
+
model: {
|
|
1263
|
+
provider: ctx.model?.provider,
|
|
1264
|
+
id: ctx.model?.id,
|
|
1265
|
+
contextWindow: selectedContextWindow,
|
|
1266
|
+
thinkingLevel: safeCall(() => pi.getThinkingLevel()),
|
|
1267
|
+
virtual: virtualSelection,
|
|
1268
|
+
routed,
|
|
1269
|
+
},
|
|
1270
|
+
contextUsage: {
|
|
1271
|
+
tokens: contextTokens,
|
|
1272
|
+
contextWindow,
|
|
1273
|
+
percent: contextPercent,
|
|
1274
|
+
},
|
|
1275
|
+
options: {
|
|
1276
|
+
redacted: options.redact,
|
|
1277
|
+
defaultDataset: options.defaultDataset,
|
|
1278
|
+
},
|
|
1279
|
+
categoryMeta: CATEGORY_META,
|
|
1280
|
+
datasets: {
|
|
1281
|
+
current: currentDataset,
|
|
1282
|
+
full: fullDataset,
|
|
1283
|
+
},
|
|
1284
|
+
notes,
|
|
1285
|
+
};
|
|
1160
1286
|
}
|
|
1161
1287
|
|
|
1162
1288
|
function sanitizeFilePart(value: string): string {
|
|
1163
|
-
|
|
1289
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80) || "session";
|
|
1164
1290
|
}
|
|
1165
1291
|
|
|
1166
1292
|
function reportPath(ctx: ExtensionCommandContext, options: CommandOptions): string {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1293
|
+
const rawSessionId = options.redact
|
|
1294
|
+
? "redacted"
|
|
1295
|
+
: (safeCall(() => ctx.sessionManager.getSessionId()) ?? "session");
|
|
1296
|
+
const fileName = `pi-context-${sanitizeFilePart(rawSessionId)}-${Date.now()}.html`;
|
|
1297
|
+
if (options.keep) {
|
|
1298
|
+
const dir = join(ctx.cwd, CONFIG_DIR_NAME, "context-reports");
|
|
1299
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1300
|
+
try {
|
|
1301
|
+
chmodSync(dir, 0o700);
|
|
1302
|
+
} catch {
|
|
1303
|
+
/* best effort */
|
|
1304
|
+
}
|
|
1305
|
+
return join(dir, fileName);
|
|
1306
|
+
}
|
|
1307
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-context-"));
|
|
1308
|
+
try {
|
|
1309
|
+
chmodSync(dir, 0o700);
|
|
1310
|
+
} catch {
|
|
1311
|
+
/* best effort */
|
|
1312
|
+
}
|
|
1313
|
+
return join(dir, fileName);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async function openReport(
|
|
1317
|
+
pi: ExtensionAPI,
|
|
1318
|
+
filePath: string,
|
|
1319
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
1320
|
+
const url = pathToFileURL(filePath).href;
|
|
1321
|
+
try {
|
|
1322
|
+
let result: { code?: number | null; stderr?: string };
|
|
1323
|
+
if (process.platform === "darwin") {
|
|
1324
|
+
result = await pi.exec("open", [url], { timeout: 5000 });
|
|
1325
|
+
} else if (process.platform === "win32") {
|
|
1326
|
+
result = await pi.exec("cmd", ["/c", "start", "", url], { timeout: 5000 });
|
|
1327
|
+
} else {
|
|
1328
|
+
result = await pi.exec("xdg-open", [url], { timeout: 5000 });
|
|
1329
|
+
}
|
|
1330
|
+
if (result.code === 0) return { ok: true };
|
|
1331
|
+
return { ok: false, error: result.stderr || `open command exited with code ${result.code}` };
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function notify(
|
|
1338
|
+
ctx: ExtensionCommandContext,
|
|
1339
|
+
message: string,
|
|
1340
|
+
type: "info" | "warning" | "error" = "info",
|
|
1341
|
+
): void {
|
|
1342
|
+
if (ctx.hasUI) ctx.ui.notify(message, type);
|
|
1343
|
+
else console.log(message);
|
|
1201
1344
|
}
|
|
1202
1345
|
|
|
1203
1346
|
function writeReport(data: ReportData, filePath: string): void {
|
|
1204
|
-
|
|
1347
|
+
writeFileSync(filePath, buildHtml(data), { encoding: "utf8", mode: 0o600 });
|
|
1205
1348
|
}
|
|
1206
1349
|
|
|
1207
1350
|
function scriptJson(data: ReportData): string {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1351
|
+
return JSON.stringify(data)
|
|
1352
|
+
.replace(/</g, "\\u003c")
|
|
1353
|
+
.replace(/>/g, "\\u003e")
|
|
1354
|
+
.replace(/&/g, "\\u0026")
|
|
1355
|
+
.replace(/\u2028/g, "\\u2028")
|
|
1356
|
+
.replace(/\u2029/g, "\\u2029");
|
|
1214
1357
|
}
|
|
1215
1358
|
|
|
1216
1359
|
function buildHtml(data: ReportData): string {
|
|
1217
|
-
|
|
1360
|
+
return `<!doctype html>
|
|
1218
1361
|
<html lang="en">
|
|
1219
1362
|
<head>
|
|
1220
1363
|
<meta charset="utf-8" />
|
|
@@ -1614,43 +1757,47 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cbd5e1;
|
|
|
1614
1757
|
}
|
|
1615
1758
|
|
|
1616
1759
|
export const __testing = {
|
|
1617
|
-
|
|
1618
|
-
|
|
1760
|
+
analyzeEntries,
|
|
1761
|
+
buildReportData,
|
|
1619
1762
|
};
|
|
1620
1763
|
|
|
1621
1764
|
export default function contextInspectorExtension(pi: ExtensionAPI) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1765
|
+
pi.registerCommand("context", {
|
|
1766
|
+
description: "Open a local HTML breakdown of where this session's context is going",
|
|
1767
|
+
getArgumentCompletions: (prefix) => {
|
|
1768
|
+
const flags = ["--no-open", "--keep", "--redact", "--full", "--current", "--help"];
|
|
1769
|
+
const trimmed = prefix.trimStart();
|
|
1770
|
+
const last = trimmed.split(/\s+/).pop() ?? "";
|
|
1771
|
+
const matches = flags.filter((flag) => flag.startsWith(last));
|
|
1772
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
1773
|
+
},
|
|
1774
|
+
handler: async (args, ctx) => {
|
|
1775
|
+
const options = parseArgs(args);
|
|
1776
|
+
if (options.help) {
|
|
1777
|
+
notify(ctx, usageText(), "info");
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
await ctx.waitForIdle();
|
|
1782
|
+
const data = buildReportData(pi, ctx, options);
|
|
1783
|
+
const filePath = reportPath(ctx, options);
|
|
1784
|
+
writeReport(data, filePath);
|
|
1785
|
+
|
|
1786
|
+
if (!options.open) {
|
|
1787
|
+
notify(ctx, `Context report written: ${filePath}`, "info");
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
const result = await openReport(pi, filePath);
|
|
1792
|
+
if (result.ok) {
|
|
1793
|
+
notify(ctx, `Opened context report: ${filePath}`, "info");
|
|
1794
|
+
} else {
|
|
1795
|
+
notify(
|
|
1796
|
+
ctx,
|
|
1797
|
+
`Context report written, but browser open failed: ${filePath}\n${result.error ?? ""}`,
|
|
1798
|
+
"warning",
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
1656
1803
|
}
|