rinco-pi-hud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +18 -0
- package/README.md +204 -0
- package/docs/CONTRIBUTING.md +110 -0
- package/docs/README.zh-CN.md +204 -0
- package/docs/footer.md +139 -0
- package/extensions/hud/commands/settings.ts +765 -0
- package/extensions/hud/config/config-counts.ts +87 -0
- package/extensions/hud/config/config.ts +65 -0
- package/extensions/hud/config/file-io.ts +128 -0
- package/extensions/hud/config/normalize.ts +374 -0
- package/extensions/hud/config/persist.ts +277 -0
- package/extensions/hud/config/schema.ts +303 -0
- package/extensions/hud/footer/format.ts +279 -0
- package/extensions/hud/footer/index.ts +543 -0
- package/extensions/hud/footer/layout.ts +183 -0
- package/extensions/hud/footer/render-labels.ts +305 -0
- package/extensions/hud/index.ts +466 -0
- package/extensions/hud/segments/extension-status.ts +88 -0
- package/extensions/hud/segments/git.ts +384 -0
- package/extensions/hud/segments/mcp-status.ts +25 -0
- package/extensions/hud/segments/package-version.ts +651 -0
- package/extensions/hud/segments/project-state.ts +67 -0
- package/extensions/hud/segments/runtime-definitions.ts +695 -0
- package/extensions/hud/segments/runtime.ts +167 -0
- package/extensions/hud/segments/skill-activity.ts +152 -0
- package/extensions/hud/session/context.ts +19 -0
- package/extensions/hud/session/lifecycle.ts +60 -0
- package/extensions/hud/session/live-context.ts +75 -0
- package/extensions/hud/state/index.ts +67 -0
- package/extensions/hud/state/project-refresh.ts +104 -0
- package/extensions/hud/state/telemetry.ts +261 -0
- package/extensions/hud/telemetry/codex-usage/LICENSE +21 -0
- package/extensions/hud/telemetry/codex-usage/app-server-client.ts +231 -0
- package/extensions/hud/telemetry/codex-usage/format.ts +278 -0
- package/extensions/hud/telemetry/codex-usage/index.ts +462 -0
- package/extensions/hud/telemetry/codex-usage/normalize.ts +264 -0
- package/extensions/hud/telemetry/codex-usage/query.ts +207 -0
- package/extensions/hud/telemetry/codex-usage/safety.ts +27 -0
- package/extensions/hud/telemetry/codex-usage/types.ts +137 -0
- package/extensions/hud/telemetry/format.ts +434 -0
- package/extensions/hud/telemetry/token-switch.ts +114 -0
- package/extensions/hud/ui/icons.ts +239 -0
- package/extensions/hud/ui/style.ts +276 -0
- package/package.json +56 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, Skill } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import registerCodexUsage from "./telemetry/codex-usage/index.js";
|
|
3
|
+
import { countConfigEntries } from "./config/config-counts.js";
|
|
4
|
+
import {
|
|
5
|
+
type ColorSourcesConfig,
|
|
6
|
+
type ContextStyle,
|
|
7
|
+
type ExtensionStatusColorMode,
|
|
8
|
+
type ExtensionStatusPlacement,
|
|
9
|
+
ensureConfigExists,
|
|
10
|
+
type FooterSegmentsConfig,
|
|
11
|
+
type GitBranchConfig,
|
|
12
|
+
type IconMode,
|
|
13
|
+
loadConfig,
|
|
14
|
+
type PathDisplayConfig,
|
|
15
|
+
type PolishedTuiConfig,
|
|
16
|
+
type SeparatorStyle,
|
|
17
|
+
saveColorSourcesPatch,
|
|
18
|
+
saveContextStylePatch,
|
|
19
|
+
saveExtensionStatusColorMode,
|
|
20
|
+
saveExtensionStatusPlacement,
|
|
21
|
+
saveFooterFormatPatch,
|
|
22
|
+
saveFooterSegmentsPatch,
|
|
23
|
+
saveGitBranchPatch,
|
|
24
|
+
saveIconsModePatch,
|
|
25
|
+
savePathDisplayPatch,
|
|
26
|
+
saveSeparatorPatch,
|
|
27
|
+
saveUiFeaturesPatch,
|
|
28
|
+
type UiFeaturesConfig,
|
|
29
|
+
} from "./config/config.js";
|
|
30
|
+
import { installFooter } from "./footer/index.js";
|
|
31
|
+
import { buildSessionDurationLabel, invalidateUsageTotalsCache } from "./telemetry/format.js";
|
|
32
|
+
import { emptyGitStatus, readGitStatus } from "./segments/git.js";
|
|
33
|
+
import { LiveContextController } from "./session/live-context.js";
|
|
34
|
+
import { readPackageVersionResult } from "./segments/package-version.js";
|
|
35
|
+
import {
|
|
36
|
+
createProjectRefreshScheduler,
|
|
37
|
+
type ScheduleProjectRefreshOptions,
|
|
38
|
+
type StopProjectRefreshInterval,
|
|
39
|
+
startProjectRefreshInterval,
|
|
40
|
+
} from "./state/project-refresh.js";
|
|
41
|
+
import { applyProjectRefreshToState } from "./segments/project-state.js";
|
|
42
|
+
import { readRuntimeInfo } from "./segments/runtime.js";
|
|
43
|
+
import { isLiveExtensionContext } from "./session/context.js";
|
|
44
|
+
import { SessionLifecycle } from "./session/lifecycle.js";
|
|
45
|
+
import { SkillActivityTracker, type SkillReference } from "./segments/skill-activity.js";
|
|
46
|
+
import { registerZentuiSettingsCommand } from "./commands/settings.js";
|
|
47
|
+
import { createInitialState, type FooterState, syncState } from "./state/index.js";
|
|
48
|
+
import { findToolTarget, resetTelemetryState, updateTelemetryState } from "./state/telemetry.js";
|
|
49
|
+
|
|
50
|
+
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
51
|
+
try {
|
|
52
|
+
const mode = (ctx as ExtensionContext & { mode?: string }).mode;
|
|
53
|
+
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function (pi: ExtensionAPI) {
|
|
60
|
+
const state: FooterState = createInitialState(emptyGitStatus());
|
|
61
|
+
const sessionLifecycle = new SessionLifecycle();
|
|
62
|
+
const skillActivity = new SkillActivityTracker();
|
|
63
|
+
|
|
64
|
+
let currentConfig: PolishedTuiConfig = loadConfig();
|
|
65
|
+
let requestFooterRender: (() => void) | undefined;
|
|
66
|
+
let getActiveExtensionStatuses: () => ReadonlyMap<string, string> = () => new Map();
|
|
67
|
+
let stopRefreshInterval: StopProjectRefreshInterval = () => {};
|
|
68
|
+
let footerInstalled = false;
|
|
69
|
+
let stopSessionTimer: () => void = () => {};
|
|
70
|
+
let lastDurationLabel = "";
|
|
71
|
+
let lastProjectCwd: string | undefined;
|
|
72
|
+
|
|
73
|
+
const refresh = () => {
|
|
74
|
+
if (sessionLifecycle.isCurrent()) requestFooterRender?.();
|
|
75
|
+
};
|
|
76
|
+
// Pi hands every event handler a brand-new ctx object, so identity comparisons
|
|
77
|
+
// with the session_start ctx would silently drop later updates.
|
|
78
|
+
const isActiveSessionContext = (ctx: ExtensionContext): boolean => {
|
|
79
|
+
return sessionLifecycle.isCurrent() && isLiveExtensionContext(ctx);
|
|
80
|
+
};
|
|
81
|
+
const liveContext = new LiveContextController(sessionLifecycle, refresh);
|
|
82
|
+
const getCurrentConfig = () => currentConfig;
|
|
83
|
+
const syncFooterState = (ctx: ExtensionContext) =>
|
|
84
|
+
syncState(state, ctx, currentConfig.icons.cacheHit);
|
|
85
|
+
const availableSkillReferences = (promptSkills: readonly Skill[] = []): SkillReference[] => {
|
|
86
|
+
const byName = new Map<string, SkillReference>();
|
|
87
|
+
try {
|
|
88
|
+
for (const command of pi.getCommands()) {
|
|
89
|
+
if (command.source !== "skill" || !command.name.startsWith("skill:")) continue;
|
|
90
|
+
const name = command.name.slice("skill:".length);
|
|
91
|
+
if (name && command.sourceInfo.path) {
|
|
92
|
+
byName.set(name, { name, filePath: command.sourceInfo.path });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
for (const skill of promptSkills) {
|
|
97
|
+
byName.set(skill.name, { name: skill.name, filePath: skill.filePath });
|
|
98
|
+
}
|
|
99
|
+
return [...byName.values()];
|
|
100
|
+
};
|
|
101
|
+
const updateSkillCounts = (ctx: ExtensionContext, promptSkills: readonly Skill[] = []) => {
|
|
102
|
+
skillActivity.syncAvailable(availableSkillReferences(promptSkills), ctx.cwd);
|
|
103
|
+
state.skillCounts = skillActivity.counts();
|
|
104
|
+
};
|
|
105
|
+
const restoreSkillActivity = (ctx: ExtensionContext) => {
|
|
106
|
+
updateSkillCounts(ctx);
|
|
107
|
+
skillActivity.restoreFromEntries(ctx.sessionManager.getBranch(), ctx.cwd);
|
|
108
|
+
state.skillCounts = skillActivity.counts();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type ProjectRefreshTarget = { cwd: string; generation: number };
|
|
112
|
+
const refreshProjectState = async ({ cwd, generation }: ProjectRefreshTarget) => {
|
|
113
|
+
if (!sessionLifecycle.isCurrent(generation)) return;
|
|
114
|
+
const gitCommitConfig = currentConfig.gitCommit;
|
|
115
|
+
const gitMetricsConfig = currentConfig.gitMetrics;
|
|
116
|
+
const segments = currentConfig.footerSegments;
|
|
117
|
+
const fmt = currentConfig.footerFormat;
|
|
118
|
+
const formatNeedsTag = /\$\{?(?:git_tag|tag)\b/.test(fmt);
|
|
119
|
+
const formatNeedsCommit = /\$\{?(?:git_commit|commit)\b/.test(fmt);
|
|
120
|
+
const formatNeedsMetrics = /\$\{?(?:git_metrics|git_added|git_deleted)\b/.test(fmt);
|
|
121
|
+
const formatNeedsPackage = /\$\{?(?:package|package_version)\b/.test(fmt);
|
|
122
|
+
const wantExactTag =
|
|
123
|
+
((segments.gitCommit || formatNeedsCommit) && gitCommitConfig.showTag) || formatNeedsTag;
|
|
124
|
+
const wantMetrics = segments.gitMetrics || formatNeedsMetrics;
|
|
125
|
+
const wantPackage = segments.packageVersion || formatNeedsPackage;
|
|
126
|
+
const [git, runtime, packageVersion, configCounts] = await Promise.all([
|
|
127
|
+
readGitStatus(cwd, {
|
|
128
|
+
readExactTag: wantExactTag,
|
|
129
|
+
readMetrics: wantMetrics,
|
|
130
|
+
ignoreSubmodules: gitMetricsConfig.ignoreSubmodules,
|
|
131
|
+
}),
|
|
132
|
+
readRuntimeInfo(cwd),
|
|
133
|
+
wantPackage ? readPackageVersionResult(cwd) : Promise.resolve(undefined),
|
|
134
|
+
Promise.resolve(countConfigEntries(cwd)),
|
|
135
|
+
]);
|
|
136
|
+
if (!sessionLifecycle.isCurrent(generation)) return;
|
|
137
|
+
state.configCounts = configCounts;
|
|
138
|
+
lastProjectCwd = applyProjectRefreshToState(state, {
|
|
139
|
+
cwd,
|
|
140
|
+
previousCwd: lastProjectCwd,
|
|
141
|
+
git,
|
|
142
|
+
runtime,
|
|
143
|
+
packageVersion,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const projectRefreshScheduler = createProjectRefreshScheduler(refreshProjectState, refresh);
|
|
148
|
+
const scheduleProjectRefresh = (
|
|
149
|
+
ctx: ExtensionContext,
|
|
150
|
+
options?: ScheduleProjectRefreshOptions,
|
|
151
|
+
) => {
|
|
152
|
+
const generation = sessionLifecycle.currentGeneration();
|
|
153
|
+
if (!sessionLifecycle.isCurrent(generation)) return;
|
|
154
|
+
projectRefreshScheduler.schedule({ cwd: ctx.cwd, generation }, options);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
|
|
158
|
+
if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
|
|
159
|
+
syncFooterState(ctx);
|
|
160
|
+
if (project && currentConfig.features.statusLine) scheduleProjectRefresh(ctx);
|
|
161
|
+
refresh();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const stopProjectRefresh = () => {
|
|
165
|
+
stopRefreshInterval();
|
|
166
|
+
stopRefreshInterval = () => {};
|
|
167
|
+
projectRefreshScheduler.stop();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const startSessionTimer = () => {
|
|
171
|
+
stopSessionTimer();
|
|
172
|
+
lastDurationLabel = "";
|
|
173
|
+
const segments = currentConfig.footerSegments;
|
|
174
|
+
const format = currentConfig.footerFormat ?? "";
|
|
175
|
+
const needsWallClock = segments.time || /\$\{?time\b/.test(format);
|
|
176
|
+
const needsDuration =
|
|
177
|
+
segments.sessionDuration || /\$\{?(?:session_duration|duration)\b/.test(format);
|
|
178
|
+
const needsActivityClock =
|
|
179
|
+
segments.toolActivity ||
|
|
180
|
+
segments.agentActivity ||
|
|
181
|
+
/\$\{?(?:running_tools|active_agents|agents)\b/.test(format);
|
|
182
|
+
if (
|
|
183
|
+
!currentConfig.features.statusLine ||
|
|
184
|
+
!(needsWallClock || needsDuration || needsActivityClock)
|
|
185
|
+
) return;
|
|
186
|
+
|
|
187
|
+
const timer = setInterval(() => {
|
|
188
|
+
if (!sessionLifecycle.isCurrent()) return;
|
|
189
|
+
if (needsWallClock || needsActivityClock) {
|
|
190
|
+
refresh();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const label = state.sessionStartEpoch
|
|
194
|
+
? buildSessionDurationLabel(state.sessionStartEpoch)
|
|
195
|
+
: "";
|
|
196
|
+
if (label === lastDurationLabel) return;
|
|
197
|
+
lastDurationLabel = label;
|
|
198
|
+
refresh();
|
|
199
|
+
}, 1000);
|
|
200
|
+
timer.unref?.();
|
|
201
|
+
stopSessionTimer = () => {
|
|
202
|
+
clearInterval(timer);
|
|
203
|
+
stopSessionTimer = () => {};
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const installStatusLine = (ctx: ExtensionContext) => {
|
|
208
|
+
if (footerInstalled) return;
|
|
209
|
+
installFooter(ctx, state, getCurrentConfig, {
|
|
210
|
+
setRequestRender: (fn) => {
|
|
211
|
+
requestFooterRender = fn;
|
|
212
|
+
},
|
|
213
|
+
scheduleProjectRefresh,
|
|
214
|
+
setExtensionStatusesGetter(fn) {
|
|
215
|
+
getActiveExtensionStatuses = fn ?? (() => new Map());
|
|
216
|
+
},
|
|
217
|
+
getLiveContext: () => liveContext.get(),
|
|
218
|
+
});
|
|
219
|
+
footerInstalled = true;
|
|
220
|
+
stopProjectRefresh();
|
|
221
|
+
stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
|
|
222
|
+
scheduleProjectRefresh(ctx),
|
|
223
|
+
);
|
|
224
|
+
scheduleProjectRefresh(ctx, { force: true });
|
|
225
|
+
refresh();
|
|
226
|
+
startSessionTimer();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const uninstallStatusLine = (ctx: ExtensionContext) => {
|
|
230
|
+
stopSessionTimer();
|
|
231
|
+
stopProjectRefresh();
|
|
232
|
+
ctx.ui.setFooter(undefined);
|
|
233
|
+
footerInstalled = false;
|
|
234
|
+
requestFooterRender = undefined;
|
|
235
|
+
getActiveExtensionStatuses = () => new Map();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const applyConfiguredUi = (ctx: ExtensionContext) => {
|
|
239
|
+
if (!isTuiContext(ctx)) return;
|
|
240
|
+
if (currentConfig.features.statusLine) installStatusLine(ctx);
|
|
241
|
+
else if (footerInstalled) uninstallStatusLine(ctx);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const installUi = (ctx: ExtensionContext) => {
|
|
245
|
+
if (!isTuiContext(ctx)) return;
|
|
246
|
+
footerInstalled = false;
|
|
247
|
+
ensureConfigExists();
|
|
248
|
+
currentConfig = loadConfig();
|
|
249
|
+
syncFooterState(ctx);
|
|
250
|
+
stopProjectRefresh();
|
|
251
|
+
applyConfiguredUi(ctx);
|
|
252
|
+
refresh();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const cleanupUi = (ctx?: ExtensionContext) => {
|
|
256
|
+
if (!ctx || !sessionLifecycle.isCurrent()) return;
|
|
257
|
+
sessionLifecycle.shutdown();
|
|
258
|
+
stopSessionTimer();
|
|
259
|
+
stopProjectRefresh();
|
|
260
|
+
requestFooterRender = undefined;
|
|
261
|
+
getActiveExtensionStatuses = () => new Map();
|
|
262
|
+
if (isTuiContext(ctx)) ctx.ui.setFooter(undefined);
|
|
263
|
+
footerInstalled = false;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const syncInteractiveState = (_event: unknown, ctx: ExtensionContext) => {
|
|
267
|
+
refreshInteractiveState(ctx);
|
|
268
|
+
};
|
|
269
|
+
const syncInteractiveAndProjectState = (_event: unknown, ctx: ExtensionContext) => {
|
|
270
|
+
refreshInteractiveState(ctx, true);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
274
|
+
sessionLifecycle.start();
|
|
275
|
+
liveContext.clear();
|
|
276
|
+
skillActivity.reset();
|
|
277
|
+
restoreSkillActivity(ctx);
|
|
278
|
+
state.sessionStartEpoch = Date.now();
|
|
279
|
+
state.codexUsageStatus = undefined;
|
|
280
|
+
state.telemetry = resetTelemetryState(state.telemetry, {
|
|
281
|
+
sessionName: ctx.sessionManager.getSessionName?.(),
|
|
282
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
283
|
+
modelSupportsReasoning: Boolean(ctx.model?.reasoning),
|
|
284
|
+
});
|
|
285
|
+
invalidateUsageTotalsCache();
|
|
286
|
+
lastProjectCwd = undefined;
|
|
287
|
+
installUi(ctx);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
pi.on("resources_discover", (_event, ctx) => {
|
|
291
|
+
sessionLifecycle.defer(() => {
|
|
292
|
+
if (!isActiveSessionContext(ctx)) return;
|
|
293
|
+
updateSkillCounts(ctx);
|
|
294
|
+
refresh();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
299
|
+
updateSkillCounts(ctx, event.systemPromptOptions.skills ?? []);
|
|
300
|
+
skillActivity.activateFromPrompt(event.prompt, ctx.cwd);
|
|
301
|
+
state.skillCounts = skillActivity.counts();
|
|
302
|
+
refreshInteractiveState(ctx);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
registerZentuiSettingsCommand(pi, {
|
|
306
|
+
getConfig: getCurrentConfig,
|
|
307
|
+
setColorSources(patch: Partial<ColorSourcesConfig>) {
|
|
308
|
+
currentConfig = saveColorSourcesPatch(patch);
|
|
309
|
+
},
|
|
310
|
+
setUiFeatures(patch: Partial<UiFeaturesConfig>, ctx: ExtensionContext) {
|
|
311
|
+
currentConfig = saveUiFeaturesPatch(patch);
|
|
312
|
+
applyConfiguredUi(ctx);
|
|
313
|
+
},
|
|
314
|
+
setFooterSegments(patch: Partial<FooterSegmentsConfig>) {
|
|
315
|
+
currentConfig = saveFooterSegmentsPatch(patch);
|
|
316
|
+
startSessionTimer();
|
|
317
|
+
},
|
|
318
|
+
setFooterFormat(value: string) {
|
|
319
|
+
currentConfig = saveFooterFormatPatch(value);
|
|
320
|
+
startSessionTimer();
|
|
321
|
+
},
|
|
322
|
+
setIconMode(mode: IconMode) {
|
|
323
|
+
currentConfig = saveIconsModePatch(mode);
|
|
324
|
+
},
|
|
325
|
+
setContextStyle(style: ContextStyle) {
|
|
326
|
+
currentConfig = saveContextStylePatch(style);
|
|
327
|
+
},
|
|
328
|
+
setSeparator(separator: SeparatorStyle) {
|
|
329
|
+
currentConfig = saveSeparatorPatch(separator);
|
|
330
|
+
},
|
|
331
|
+
setPathDisplay(patch: Partial<PathDisplayConfig>) {
|
|
332
|
+
currentConfig = savePathDisplayPatch(patch);
|
|
333
|
+
},
|
|
334
|
+
setGitBranch(patch: Partial<GitBranchConfig>) {
|
|
335
|
+
currentConfig = saveGitBranchPatch(patch);
|
|
336
|
+
},
|
|
337
|
+
getActiveExtensionStatuses() {
|
|
338
|
+
return getActiveExtensionStatuses();
|
|
339
|
+
},
|
|
340
|
+
setExtensionStatusPlacement(key: string, placement: ExtensionStatusPlacement) {
|
|
341
|
+
currentConfig = saveExtensionStatusPlacement(key, placement);
|
|
342
|
+
},
|
|
343
|
+
setExtensionStatusColorMode(key: string, colorMode: ExtensionStatusColorMode) {
|
|
344
|
+
currentConfig = saveExtensionStatusColorMode(key, colorMode);
|
|
345
|
+
},
|
|
346
|
+
requestRender() {
|
|
347
|
+
refresh();
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
registerCodexUsage(pi, (ctx, value) => {
|
|
352
|
+
if (!isActiveSessionContext(ctx)) return;
|
|
353
|
+
state.codexUsageStatus = value;
|
|
354
|
+
refresh();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
358
|
+
liveContext.clear();
|
|
359
|
+
state.codexUsageStatus = undefined;
|
|
360
|
+
cleanupUi(ctx);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const syncInteractiveAndProjectStateWithUsage = (_event: unknown, ctx: ExtensionContext) => {
|
|
364
|
+
invalidateUsageTotalsCache();
|
|
365
|
+
refreshInteractiveState(ctx, true);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
pi.on("agent_start", (event, ctx) => {
|
|
369
|
+
liveContext.clear();
|
|
370
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
371
|
+
type: "agent-start",
|
|
372
|
+
at: Date.now(),
|
|
373
|
+
});
|
|
374
|
+
syncInteractiveState(event, ctx);
|
|
375
|
+
});
|
|
376
|
+
pi.on("agent_end", (event, ctx) => {
|
|
377
|
+
liveContext.clear();
|
|
378
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
379
|
+
type: "agent-end",
|
|
380
|
+
at: Date.now(),
|
|
381
|
+
});
|
|
382
|
+
syncInteractiveAndProjectState(event, ctx);
|
|
383
|
+
});
|
|
384
|
+
pi.on("model_select", (event, ctx) => {
|
|
385
|
+
liveContext.clear();
|
|
386
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
387
|
+
type: "metadata",
|
|
388
|
+
modelSupportsReasoning: Boolean(event.model?.reasoning),
|
|
389
|
+
});
|
|
390
|
+
syncInteractiveState(event, ctx);
|
|
391
|
+
});
|
|
392
|
+
pi.on("thinking_level_select", (event, ctx) => {
|
|
393
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
394
|
+
type: "metadata",
|
|
395
|
+
thinkingLevel: event.level,
|
|
396
|
+
});
|
|
397
|
+
syncInteractiveState(event, ctx);
|
|
398
|
+
});
|
|
399
|
+
pi.on("session_info_changed", (event, ctx) => {
|
|
400
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
401
|
+
type: "metadata",
|
|
402
|
+
sessionName: event.name,
|
|
403
|
+
});
|
|
404
|
+
syncInteractiveState(event, ctx);
|
|
405
|
+
});
|
|
406
|
+
pi.on("turn_start", (event, ctx) => {
|
|
407
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
408
|
+
type: "turn-start",
|
|
409
|
+
turnIndex: event.turnIndex,
|
|
410
|
+
});
|
|
411
|
+
syncInteractiveState(event, ctx);
|
|
412
|
+
});
|
|
413
|
+
pi.on("message_update", (event) => {
|
|
414
|
+
liveContext.update(event.message);
|
|
415
|
+
});
|
|
416
|
+
pi.on("message_end", (event, ctx) => {
|
|
417
|
+
if (
|
|
418
|
+
event.message.role === "assistant" &&
|
|
419
|
+
(event.message.stopReason === "error" || event.message.stopReason === "aborted")
|
|
420
|
+
) {
|
|
421
|
+
liveContext.clear();
|
|
422
|
+
}
|
|
423
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
424
|
+
});
|
|
425
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
426
|
+
liveContext.clear();
|
|
427
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
428
|
+
type: "tool-call",
|
|
429
|
+
toolCallId: event.toolCallId,
|
|
430
|
+
name: event.toolName,
|
|
431
|
+
args:
|
|
432
|
+
event.args && typeof event.args === "object"
|
|
433
|
+
? (event.args as Record<string, unknown>)
|
|
434
|
+
: undefined,
|
|
435
|
+
at: Date.now(),
|
|
436
|
+
});
|
|
437
|
+
syncInteractiveState(event, ctx);
|
|
438
|
+
});
|
|
439
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
440
|
+
// `ToolExecutionEndEvent` does not carry `args`; recover the original
|
|
441
|
+
// `read` target recorded at `tool_execution_start` from telemetry state.
|
|
442
|
+
if (!event.isError && event.toolName === "read") {
|
|
443
|
+
const path = findToolTarget(state.telemetry, event.toolCallId);
|
|
444
|
+
if (typeof path === "string") {
|
|
445
|
+
skillActivity.activateFromRead(path, ctx.cwd);
|
|
446
|
+
state.skillCounts = skillActivity.counts();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
state.telemetry = updateTelemetryState(state.telemetry, {
|
|
450
|
+
type: "tool-result",
|
|
451
|
+
toolCallId: event.toolCallId,
|
|
452
|
+
isError: event.isError,
|
|
453
|
+
at: Date.now(),
|
|
454
|
+
});
|
|
455
|
+
syncInteractiveAndProjectState(event, ctx);
|
|
456
|
+
});
|
|
457
|
+
pi.on("session_compact", (event, ctx) => {
|
|
458
|
+
liveContext.clear();
|
|
459
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
460
|
+
});
|
|
461
|
+
pi.on("session_tree", (event, ctx) => {
|
|
462
|
+
liveContext.clear();
|
|
463
|
+
restoreSkillActivity(ctx);
|
|
464
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionStatusColorMode,
|
|
4
|
+
ExtensionStatusPlacement,
|
|
5
|
+
PolishedTuiConfig,
|
|
6
|
+
} from "../config/config.js";
|
|
7
|
+
import { getExtensionStatusColorMode, getExtensionStatusPlacement } from "../config/config.js";
|
|
8
|
+
|
|
9
|
+
export type ExtensionStatusSegment = {
|
|
10
|
+
key: string;
|
|
11
|
+
text: string;
|
|
12
|
+
placement: ExtensionStatusPlacement;
|
|
13
|
+
colorMode: ExtensionStatusColorMode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ExtensionStatusSegmentsByPlacement = {
|
|
17
|
+
left: ExtensionStatusSegment[];
|
|
18
|
+
middle: ExtensionStatusSegment[];
|
|
19
|
+
right: ExtensionStatusSegment[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const safeSgrPattern = /\x1b\[[0-9;:]*m/g;
|
|
23
|
+
const sgrPlaceholderPattern = /__ZENTUI_SGR_(\d+)__/g;
|
|
24
|
+
|
|
25
|
+
function compareKeys(a: ExtensionStatusSegment, b: ExtensionStatusSegment): number {
|
|
26
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeStatusWhitespace(value: string): string {
|
|
30
|
+
return value
|
|
31
|
+
.replace(/[\r\n\t\f\v]+/g, " ")
|
|
32
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
|
|
33
|
+
.replace(/\s+/g, " ")
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sanitizeExtensionStatusText(value: string): string {
|
|
38
|
+
return normalizeStatusWhitespace(stripVTControlCharacters(value));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasVisibleStatusText(value: string): boolean {
|
|
42
|
+
return sanitizeExtensionStatusText(value).length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sanitizeExtensionStatusOriginalText(value: string): string {
|
|
46
|
+
const safeSequences: string[] = [];
|
|
47
|
+
const protectedValue = value.replace(safeSgrPattern, (sequence) => {
|
|
48
|
+
const index = safeSequences.push(sequence) - 1;
|
|
49
|
+
return `__ZENTUI_SGR_${index}__`;
|
|
50
|
+
});
|
|
51
|
+
const cleaned = normalizeStatusWhitespace(stripVTControlCharacters(protectedValue));
|
|
52
|
+
const restored = cleaned.replace(sgrPlaceholderPattern, (_match, indexText: string) => {
|
|
53
|
+
const index = Number.parseInt(indexText, 10);
|
|
54
|
+
return safeSequences[index] ?? "";
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return hasVisibleStatusText(restored) ? restored : "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function collectExtensionStatusSegments(
|
|
61
|
+
statuses: ReadonlyMap<string, string>,
|
|
62
|
+
config: PolishedTuiConfig,
|
|
63
|
+
): ExtensionStatusSegmentsByPlacement {
|
|
64
|
+
const segments: ExtensionStatusSegmentsByPlacement = {
|
|
65
|
+
left: [],
|
|
66
|
+
middle: [],
|
|
67
|
+
right: [],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const [key, value] of statuses.entries()) {
|
|
71
|
+
const placement = getExtensionStatusPlacement(config, key);
|
|
72
|
+
if (placement === "off") continue;
|
|
73
|
+
|
|
74
|
+
const colorMode = getExtensionStatusColorMode(config, key);
|
|
75
|
+
const text =
|
|
76
|
+
colorMode === "original"
|
|
77
|
+
? sanitizeExtensionStatusOriginalText(value)
|
|
78
|
+
: sanitizeExtensionStatusText(value);
|
|
79
|
+
if (!text) continue;
|
|
80
|
+
|
|
81
|
+
segments[placement].push({ key, text, placement, colorMode });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
segments.left.sort(compareKeys);
|
|
85
|
+
segments.middle.sort(compareKeys);
|
|
86
|
+
segments.right.sort(compareKeys);
|
|
87
|
+
return segments;
|
|
88
|
+
}
|