@groeponline/pi-wishcraft 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +68 -0
  2. package/CHANGELOG.md +724 -0
  3. package/CONTRIBUTING.md +37 -0
  4. package/README.md +648 -0
  5. package/RELEASE.md +117 -0
  6. package/ROADMAP.md +52 -0
  7. package/bash-mode/completion-providers.ts +269 -0
  8. package/bash-mode/completion.ts +416 -0
  9. package/bash-mode/editor-ghost.ts +40 -0
  10. package/bash-mode/editor-input.ts +80 -0
  11. package/bash-mode/editor.ts +437 -0
  12. package/bash-mode/history.ts +263 -0
  13. package/bash-mode/shell-session.ts +286 -0
  14. package/bash-mode/transcript.ts +108 -0
  15. package/bash-mode/types.ts +80 -0
  16. package/index.ts +6 -0
  17. package/package.json +55 -0
  18. package/queue/store.ts +443 -0
  19. package/queue/types.ts +54 -0
  20. package/src/config/custom-items.ts +182 -0
  21. package/src/config/extension-statuses.ts +51 -0
  22. package/src/config/layout.ts +60 -0
  23. package/src/config/parse.ts +127 -0
  24. package/src/config/powerline-config.ts +18 -0
  25. package/src/config/presets.ts +245 -0
  26. package/src/config/primitives.ts +117 -0
  27. package/src/config/segment-ids.ts +114 -0
  28. package/src/config/segment-options.ts +128 -0
  29. package/src/config/settings-patch.ts +26 -0
  30. package/src/config/types.ts +277 -0
  31. package/src/core/frontmatter.ts +40 -0
  32. package/src/editor/autocomplete-chain.ts +41 -0
  33. package/src/extension/activate.ts +28 -0
  34. package/src/extension/bash-mode-actions.ts +104 -0
  35. package/src/extension/commands.ts +268 -0
  36. package/src/extension/constants.ts +46 -0
  37. package/src/extension/custom-editor.ts +406 -0
  38. package/src/extension/git-invalidation.ts +40 -0
  39. package/src/extension/layout.ts +160 -0
  40. package/src/extension/menu-views.ts +393 -0
  41. package/src/extension/powerline-widgets.ts +95 -0
  42. package/src/extension/prompt-history.ts +219 -0
  43. package/src/extension/queue-commands.ts +245 -0
  44. package/src/extension/queue-context.ts +12 -0
  45. package/src/extension/queue-integration.ts +434 -0
  46. package/src/extension/segment-context.ts +212 -0
  47. package/src/extension/session-lifecycle.ts +373 -0
  48. package/src/extension/settings-io.ts +202 -0
  49. package/src/extension/shortcuts-config.ts +357 -0
  50. package/src/extension/shortcuts-router.ts +383 -0
  51. package/src/extension/skills/inline-invocation.ts +174 -0
  52. package/src/extension/skills/ook.md +6 -0
  53. package/src/extension/skills/test.md +6 -0
  54. package/src/extension/stale-context.ts +10 -0
  55. package/src/extension/stash-history.ts +103 -0
  56. package/src/extension/state.ts +159 -0
  57. package/src/extension/status-line-renderers.ts +222 -0
  58. package/src/extension/types.ts +97 -0
  59. package/src/extension/vibe-command.ts +160 -0
  60. package/src/extension/welcome-control.ts +27 -0
  61. package/src/extension/welcome-integration.ts +153 -0
  62. package/src/git/status.ts +332 -0
  63. package/src/paths/agent-dirs.ts +67 -0
  64. package/src/render/timer.ts +46 -0
  65. package/src/segments/core.ts +256 -0
  66. package/src/segments/custom.ts +114 -0
  67. package/src/segments/index.ts +3 -0
  68. package/src/segments/registry.ts +87 -0
  69. package/src/segments/shared.ts +36 -0
  70. package/src/segments/system.ts +235 -0
  71. package/src/segments/usage.ts +178 -0
  72. package/src/shell/cd-command.ts +190 -0
  73. package/src/shortcuts/matching.ts +61 -0
  74. package/src/theme/colors.ts +60 -0
  75. package/src/theme/icons.ts +175 -0
  76. package/src/theme/separators.ts +41 -0
  77. package/src/theme/theme.ts +211 -0
  78. package/src/tools/graph.ts +75 -0
  79. package/src/tools/patch.ts +179 -0
  80. package/src/tools/ripgrep.ts +104 -0
  81. package/src/usage/context.ts +97 -0
  82. package/src/usage/ledger.ts +293 -0
  83. package/src/usage/rates.ts +155 -0
  84. package/src/welcome/auto-dismiss.ts +43 -0
  85. package/src/welcome/banner.ts +68 -0
  86. package/src/welcome/discover.ts +234 -0
  87. package/src/welcome/format.ts +18 -0
  88. package/src/welcome/index.ts +5 -0
  89. package/src/welcome/layout.ts +36 -0
  90. package/src/welcome/overlay.ts +80 -0
  91. package/src/welcome/renderer.ts +157 -0
  92. package/src/welcome/sessions.ts +107 -0
  93. package/src/welcome/types.ts +41 -0
  94. package/src/welcome/widgets/graph-widget.ts +25 -0
  95. package/src/welcome/widgets/index.ts +20 -0
  96. package/src/welcome/widgets/queue-widget.ts +26 -0
  97. package/src/welcome/widgets/sessions-widget.ts +23 -0
  98. package/src/welcome/widgets/shortcuts-widget.ts +17 -0
  99. package/src/welcome/widgets/system-widget.ts +29 -0
  100. package/src/working-vibes/generate.ts +144 -0
  101. package/src/working-vibes/index.ts +24 -0
  102. package/src/working-vibes/manager.ts +198 -0
  103. package/src/working-vibes/provider.ts +163 -0
  104. package/src/working-vibes/storage.ts +357 -0
  105. package/theme.example.json +24 -0
  106. package/tsconfig.json +13 -0
@@ -0,0 +1,212 @@
1
+ import type {
2
+ ReadonlyFooterDataProvider,
3
+ Theme,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ import type { SegmentContext, ColorScheme } from "../config/types.ts";
7
+ import { getPreset } from "../config/presets.ts";
8
+ import {
9
+ collectHiddenExtensionStatusKeys,
10
+ mergeSegmentOptions,
11
+ } from "../config/powerline-config.ts";
12
+ import { getDefaultColors } from "../theme/theme.ts";
13
+ import { getGitStatus } from "../git/status.ts";
14
+ import { getQueueContext } from "./queue-context.ts";
15
+ import { getUsageTokenTotal } from "../usage/ledger.ts";
16
+ import {
17
+ CUSTOM_COMPACTION_STATUS_KEY,
18
+ EDITOR_STATUS_DEFER_MS,
19
+ } from "./constants.ts";
20
+ import {
21
+ config,
22
+ customCompactionEnabled,
23
+ } from "./state.ts";
24
+ import type { RuntimeState } from "./types.ts";
25
+
26
+ /**
27
+ * Reset the cached layout + underlying session-scan caches. Called on
28
+ * session boundaries and whenever config that affects layout changes.
29
+ */
30
+ export function resetLayoutCache(rt: RuntimeState): void {
31
+ rt.lastLayoutResult = null;
32
+ rt.layoutDirty = true;
33
+ rt.sessionBranchCache.reset();
34
+ rt.tokenStatsCache.reset();
35
+ rt.coreContextUsageCache.reset();
36
+ }
37
+
38
+ export function requestStatusRender(rt: RuntimeState, delayMs?: number): void {
39
+ rt.layoutDirty = true;
40
+ rt.statusRenderScheduler.schedule(delayMs);
41
+ }
42
+
43
+ export function requestImmediateStatusRender(
44
+ rt: RuntimeState,
45
+ options: { deferDuringTyping?: boolean } = {},
46
+ ): void {
47
+ rt.layoutDirty = true;
48
+ if (
49
+ options.deferDuringTyping !== false &&
50
+ Date.now() - rt.lastEditorInputAt < EDITOR_STATUS_DEFER_MS
51
+ ) {
52
+ rt.statusRenderScheduler.schedule();
53
+ return;
54
+ }
55
+
56
+ rt.forceNextLayoutRecompute = true;
57
+ rt.statusRenderScheduler.cancel();
58
+ rt.statusRenderScheduler.schedule(0);
59
+ }
60
+
61
+ export function installFooterStatusRepaintHook(
62
+ rt: RuntimeState,
63
+ footerData: ReadonlyFooterDataProvider,
64
+ ): void {
65
+ rt.restoreFooterStatusRepaintHook?.();
66
+ rt.restoreFooterStatusRepaintHook = null;
67
+
68
+ const writableFooterData = footerData as ReadonlyFooterDataProvider & {
69
+ setExtensionStatus?: (key: string, text: string | undefined) => void;
70
+ clearExtensionStatuses?: () => void;
71
+ };
72
+ if (typeof writableFooterData.setExtensionStatus !== "function") return;
73
+
74
+ const originalSetExtensionStatus = writableFooterData.setExtensionStatus;
75
+ const originalClearExtensionStatuses =
76
+ writableFooterData.clearExtensionStatuses;
77
+ const setExtensionStatusAndRepaint = function setExtensionStatusAndRepaint(
78
+ this: unknown,
79
+ key: string,
80
+ text: string | undefined,
81
+ ) {
82
+ originalSetExtensionStatus.call(this, key, text);
83
+ requestImmediateStatusRender(rt);
84
+ };
85
+ writableFooterData.setExtensionStatus = setExtensionStatusAndRepaint;
86
+
87
+ let clearExtensionStatusesAndRepaint: (() => void) | null = null;
88
+ if (typeof originalClearExtensionStatuses === "function") {
89
+ clearExtensionStatusesAndRepaint =
90
+ function clearExtensionStatusesAndRepaint(this: unknown) {
91
+ originalClearExtensionStatuses.call(this);
92
+ requestImmediateStatusRender(rt);
93
+ };
94
+ writableFooterData.clearExtensionStatuses =
95
+ clearExtensionStatusesAndRepaint;
96
+ }
97
+
98
+ rt.restoreFooterStatusRepaintHook = () => {
99
+ if (
100
+ writableFooterData.setExtensionStatus === setExtensionStatusAndRepaint
101
+ ) {
102
+ writableFooterData.setExtensionStatus = originalSetExtensionStatus;
103
+ }
104
+ if (
105
+ clearExtensionStatusesAndRepaint &&
106
+ writableFooterData.clearExtensionStatuses ===
107
+ clearExtensionStatusesAndRepaint
108
+ ) {
109
+ writableFooterData.clearExtensionStatuses =
110
+ originalClearExtensionStatuses;
111
+ }
112
+ };
113
+ }
114
+
115
+ export function buildSegmentContext(
116
+ rt: RuntimeState,
117
+ ctx: any,
118
+ theme: Theme,
119
+ ): SegmentContext {
120
+ const presetDef = getPreset(config.preset);
121
+ const colors: ColorScheme = presetDef.colors ?? getDefaultColors();
122
+
123
+ // Build usage stats and get thinking level from session (cached; the full
124
+ // event list is only re-scanned when events are appended or the trailing
125
+ // event's stats-relevant fields change, e.g. in-place streaming updates)
126
+ const sessionEvents = rt.sessionBranchCache.get(ctx.sessionManager);
127
+ const tokenStats = rt.tokenStatsCache.get(sessionEvents);
128
+ const { input, output, cacheRead, cacheWrite, cost, subagentCost } =
129
+ tokenStats;
130
+ const lastAssistant = tokenStats.lastAssistant;
131
+ const thinkingLevelFromSession = tokenStats.thinkingLevelFromSession;
132
+
133
+ // Calculate context percentage.
134
+ const latestUsage = rt.isStreaming
135
+ ? (rt.liveAssistantUsage ?? lastAssistant?.usage)
136
+ : lastAssistant?.usage;
137
+ const coreContextUsage =
138
+ rt.isStreaming && rt.liveAssistantUsage
139
+ ? null
140
+ : rt.coreContextUsageCache.get(ctx);
141
+ const contextTokens =
142
+ coreContextUsage?.contextTokens ??
143
+ (latestUsage ? getUsageTokenTotal(latestUsage) : 0);
144
+ const contextWindow =
145
+ coreContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
146
+ const contextPercent =
147
+ coreContextUsage?.contextPercent ??
148
+ (contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0);
149
+
150
+ const segmentOptions = mergeSegmentOptions(
151
+ presetDef.segmentOptions,
152
+ config.segmentOptions,
153
+ );
154
+
155
+ // Get git status (cached)
156
+ const gitBranch = rt.footerDataRef?.getGitBranch() ?? null;
157
+ const gitStatus = getGitStatus(gitBranch, segmentOptions.git?.polling);
158
+ const extensionStatuses =
159
+ rt.footerDataRef?.getExtensionStatuses() ?? new Map();
160
+ const customItemsById = new Map(
161
+ config.customItems.map((item) => [item.id, item]),
162
+ );
163
+ const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(
164
+ config.customItems,
165
+ );
166
+
167
+ // Check if using OAuth subscription
168
+ const usingSubscription = ctx.model
169
+ ? (ctx.modelRegistry?.isUsingOAuth?.(ctx.model) ?? false)
170
+ : false;
171
+
172
+ const thinkingLevel =
173
+ rt.currentThinkingLevel ??
174
+ thinkingLevelFromSession ??
175
+ rt.getThinkingLevelFn?.() ??
176
+ "off";
177
+ const queueSummary = rt.queueStore.summarize(
178
+ getQueueContext(ctx),
179
+ rt.powerlineCompacting,
180
+ );
181
+
182
+ return {
183
+ model: ctx.model,
184
+ thinkingLevel,
185
+ sessionId: ctx.sessionManager?.getSessionId?.(),
186
+ cwd: ctx.cwd,
187
+ usageStats: { input, output, cacheRead, cacheWrite, cost, subagentCost },
188
+ contextTokens,
189
+ contextPercent,
190
+ contextWindow,
191
+ autoCompactEnabled:
192
+ ctx.settingsManager?.getCompactionSettings?.()?.enabled ?? true,
193
+ customCompactionEnabled:
194
+ customCompactionEnabled ||
195
+ extensionStatuses.has(CUSTOM_COMPACTION_STATUS_KEY),
196
+ usingSubscription,
197
+ queueSummary,
198
+ sessionStartTime: rt.sessionStartTime,
199
+ shellModeActive: rt.bashModeActive,
200
+ shellRunning: rt.shellSession?.state.running ?? false,
201
+ shellName: rt.shellSession?.state.shellName ?? null,
202
+ shellCwd: rt.shellSession?.state.cwd ?? null,
203
+ git: gitStatus,
204
+ extensionStatuses,
205
+ hiddenExtensionStatusKeys,
206
+ customItemsById,
207
+ options: segmentOptions,
208
+ segmentLabels: new Map(Object.entries(config.segmentLabels)),
209
+ theme,
210
+ colors,
211
+ };
212
+ }
@@ -0,0 +1,373 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { BashTranscriptStore } from "../../bash-mode/transcript.ts";
4
+ import { BashCompletionEngine } from "../../bash-mode/completion.ts";
5
+ import { parsePowerlineConfig } from "../config/powerline-config.ts";
6
+ import { registerCustomSegments } from "../segments/index.ts";
7
+ import { registerCustomPresets } from "../config/presets.ts";
8
+ import { invalidateGitStatus } from "../git/status.ts";
9
+ import { invalidateGitForCommand } from "./git-invalidation.ts";
10
+ import {
11
+ initVibeManager,
12
+ onVibeAgentEnd,
13
+ onVibeAgentStart,
14
+ onVibeBeforeAgentStart,
15
+ onVibeToolCall,
16
+ } from "../working-vibes/index.ts";
17
+ import {
18
+ getUsageTokenTotal,
19
+ hasSessionAssistantUsage,
20
+ isSessionAssistantMessage,
21
+ } from "../usage/ledger.ts";
22
+ import {
23
+ detectCustomCompactionEnabled,
24
+ readSettings,
25
+ } from "./settings-io.ts";
26
+ import {
27
+ resolveShortcutConfig,
28
+ parseBashModeSettings,
29
+ } from "./shortcuts-config.ts";
30
+ import { warnInvalidSegmentSettings } from "./layout.ts";
31
+ import { readPersistedStashHistory } from "./stash-history.ts";
32
+ import {
33
+ requestImmediateStatusRender,
34
+ requestStatusRender,
35
+ resetLayoutCache,
36
+ } from "./segment-context.ts";
37
+ import { getQueueContext } from "./queue-context.ts";
38
+ import {
39
+ requestQueueRender,
40
+ schedulePostCompactionDelivery,
41
+ finishFailedCompaction,
42
+ } from "./queue-integration.ts";
43
+ import { setupCustomEditor } from "./custom-editor.ts";
44
+ import {
45
+ setupWelcomeHeader,
46
+ setupWelcomeOverlay,
47
+ } from "./welcome-integration.ts";
48
+ import {
49
+ config,
50
+ PRESET_NAMES,
51
+ setConfig,
52
+ setCustomCompactionEnabled,
53
+ } from "./state.ts";
54
+ import { CONTEXT_STATUS_RENDER_MS } from "./constants.ts";
55
+ import type { RuntimeState } from "./types.ts";
56
+ import { isStaleExtensionContextError } from "./stale-context.ts";
57
+ import {
58
+ dismissWelcome,
59
+ scheduleDismissWelcome,
60
+ } from "./welcome-control.ts";
61
+
62
+ // Helper to extract recent agent response text (skipping thinking blocks)
63
+ function getRecentAgentContext(ctx: any): string | undefined {
64
+ const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
65
+
66
+ // Find the most recent assistant message
67
+ for (let i = sessionEvents.length - 1; i >= 0; i--) {
68
+ const e = sessionEvents[i];
69
+ if (e.type === "message" && e.message?.role === "assistant") {
70
+ const content = e.message.content;
71
+ if (!Array.isArray(content)) continue;
72
+
73
+ // Extract text content, skip thinking blocks
74
+ for (const block of content) {
75
+ if (block.type === "text" && block.text) {
76
+ // Return first ~200 chars of non-empty text
77
+ const text = block.text.trim();
78
+ if (text.length > 0) {
79
+ return text.slice(0, 200);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ export function shouldShowStartupWelcome(reason: unknown, welcomeEnabled: boolean): boolean {
89
+ return reason === "startup" && welcomeEnabled;
90
+ }
91
+
92
+ export function registerSessionLifecycle(
93
+ pi: ExtensionAPI,
94
+ rt: RuntimeState,
95
+ ): void {
96
+ // Track session start
97
+ pi.on("session_start", async (event, ctx) => {
98
+ rt.shellSession?.dispose();
99
+ rt.shellSession = null;
100
+ rt.sessionGeneration++;
101
+ rt.sessionStartTime = Date.now();
102
+ rt.currentCtx = ctx;
103
+ setCustomCompactionEnabled(detectCustomCompactionEnabled(ctx.cwd));
104
+ rt.lastUserPrompt = "";
105
+ rt.isStreaming = false;
106
+ rt.liveAssistantUsage = null;
107
+ rt.powerlineCompacting = false;
108
+ rt.deliverAfterRetrySettles = false;
109
+ rt.stashedEditorText = null;
110
+
111
+ const settings = readSettings(ctx.cwd);
112
+ rt.resolvedShortcuts = resolveShortcutConfig(settings);
113
+ rt.bashModeSettings = parseBashModeSettings(settings, rt.resolvedShortcuts);
114
+ rt.showLastPrompt = settings.showLastPrompt !== false;
115
+ setConfig(parsePowerlineConfig(settings.powerline, PRESET_NAMES));
116
+ registerCustomSegments(config.segments);
117
+ registerCustomPresets(config.presets);
118
+ warnInvalidSegmentSettings(ctx);
119
+ rt.stashedPromptHistory = readPersistedStashHistory();
120
+ rt.bashModeActive = false;
121
+ rt.bashTranscript = new BashTranscriptStore(rt.bashModeSettings);
122
+ rt.bashCompletionEngine = new BashCompletionEngine();
123
+
124
+ rt.getThinkingLevelFn = () => ctx.thinkingLevel ?? "off";
125
+ rt.currentThinkingLevel = rt.getThinkingLevelFn();
126
+
127
+ if (ctx.hasUI) {
128
+ ctx.ui.setStatus("stash", undefined);
129
+ const pendingIdeas = rt.queueStore
130
+ .activeItems(getQueueContext(ctx))
131
+ .filter((item) => item.intent === "idea").length;
132
+ if (pendingIdeas > 0) {
133
+ ctx.ui.notify(
134
+ `${pendingIdeas} idea${pendingIdeas === 1 ? "" : "s"} waiting — /ideas`,
135
+ "info",
136
+ );
137
+ }
138
+ }
139
+
140
+ // Initialize vibe manager (needs modelRegistry from ctx)
141
+ initVibeManager(ctx);
142
+
143
+ if (rt.enabled && ctx.hasUI) {
144
+ setupCustomEditor(pi, rt, ctx);
145
+ if (shouldShowStartupWelcome(event.reason, config.welcome)) {
146
+ if (settings.quietStartup === true) {
147
+ setupWelcomeHeader(rt, ctx);
148
+ } else {
149
+ setupWelcomeOverlay(rt, ctx);
150
+ }
151
+ } else {
152
+ dismissWelcome(rt, ctx);
153
+ }
154
+ }
155
+ });
156
+
157
+ pi.on("session_shutdown", async (_event, ctx) => {
158
+ rt.sessionGeneration++;
159
+ rt.dismissWelcomeOverlay?.();
160
+ rt.dismissWelcomeOverlay = null;
161
+ rt.welcomeHeaderActive = false;
162
+ rt.welcomeOverlayShouldDismiss = false;
163
+ rt.welcomeDismissScheduler.cancel();
164
+ rt.statusRenderScheduler.cancel();
165
+ rt.restoreFooterStatusRepaintHook?.();
166
+ rt.restoreFooterStatusRepaintHook = null;
167
+ rt.stashShortcutInputUnsubscribe?.();
168
+ rt.stashShortcutInputUnsubscribe = null;
169
+ rt.shellSession?.dispose();
170
+ rt.shellSession = null;
171
+ if (rt.queueDeliveryTimer) {
172
+ clearTimeout(rt.queueDeliveryTimer);
173
+ rt.queueDeliveryTimer = null;
174
+ }
175
+ rt.powerlineCompacting = false;
176
+ rt.deliverAfterRetrySettles = false;
177
+ rt.bashModeActive = false;
178
+ rt.currentCtx = null;
179
+ rt.footerDataRef = null;
180
+ rt.getThinkingLevelFn = null;
181
+ rt.currentThinkingLevel = null;
182
+ rt.liveAssistantUsage = null;
183
+ rt.tuiRef = null;
184
+ rt.currentEditor = null;
185
+ resetLayoutCache(rt);
186
+ });
187
+
188
+ // Invalidate git status on file changes, trigger re-render on potential branch changes
189
+ pi.on("tool_result", async (event) => {
190
+ if (event.toolName === "write" || event.toolName === "edit") {
191
+ invalidateGitStatus();
192
+ requestStatusRender(rt);
193
+ }
194
+ // Check for bash commands that might change git branch
195
+ if (event.toolName === "bash" && event.input?.command) {
196
+ invalidateGitForCommand(rt, String(event.input.command));
197
+ }
198
+ });
199
+
200
+ // Also catch user escape commands (! prefix)
201
+ // Note: This fires BEFORE execution, so we use a longer delay and multiple re-renders
202
+ // to ensure we catch the update after the command completes.
203
+ pi.on("user_bash", async (event) => {
204
+ invalidateGitForCommand(rt, event.command, { stagger: true });
205
+ });
206
+
207
+ pi.on("model_select", async (_event, ctx) => {
208
+ rt.currentCtx = ctx;
209
+ rt.coreContextUsageCache.reset();
210
+ requestStatusRender(rt);
211
+ });
212
+
213
+ pi.on("thinking_level_select", async (event, ctx) => {
214
+ rt.currentCtx = ctx;
215
+ rt.currentThinkingLevel =
216
+ rt.getThinkingLevelFn?.() ??
217
+ (typeof event.level === "string" ? event.level : null);
218
+ requestImmediateStatusRender(rt, { deferDuringTyping: false });
219
+ });
220
+
221
+ pi.on("session_tree", async (_event, ctx) => {
222
+ rt.currentCtx = ctx;
223
+ rt.currentThinkingLevel = null;
224
+ rt.liveAssistantUsage = null;
225
+ requestImmediateStatusRender(rt, { deferDuringTyping: false });
226
+ });
227
+
228
+ // Generate themed working message before agent starts (has access to user's prompt)
229
+ pi.on("before_agent_start", async (event, ctx) => {
230
+ rt.lastUserPrompt = event.prompt;
231
+ if (ctx.hasUI) {
232
+ onVibeBeforeAgentStart(event.prompt, ctx.ui.setWorkingMessage);
233
+ }
234
+ });
235
+
236
+ // Track streaming state (footer only shows status during streaming)
237
+ // Also dismiss welcome when agent starts responding (handles `p "command"` case)
238
+ pi.on("agent_start", async (_event, ctx) => {
239
+ rt.isStreaming = true;
240
+ rt.liveAssistantUsage = null;
241
+ onVibeAgentStart();
242
+ dismissWelcome(rt, ctx);
243
+ rt.currentCtx = ctx;
244
+ });
245
+
246
+ pi.on("message_update", async (event, ctx) => {
247
+ if (
248
+ isSessionAssistantMessage(event.message) &&
249
+ event.message.stopReason !== "error" &&
250
+ event.message.stopReason !== "aborted" &&
251
+ getUsageTokenTotal(event.message.usage) > 0
252
+ ) {
253
+ rt.liveAssistantUsage = event.message.usage;
254
+ rt.currentCtx = ctx;
255
+ rt.layoutDirty = true;
256
+ rt.statusRenderScheduler.schedule(CONTEXT_STATUS_RENDER_MS);
257
+ }
258
+ });
259
+
260
+ pi.on("message_end", async (event, ctx) => {
261
+ rt.currentCtx = ctx;
262
+ rt.coreContextUsageCache.reset();
263
+ if (isSessionAssistantMessage(event.message)) {
264
+ if (
265
+ event.message.stopReason === "error" ||
266
+ event.message.stopReason === "aborted"
267
+ ) {
268
+ rt.liveAssistantUsage = null;
269
+ } else if (getUsageTokenTotal(event.message.usage) > 0) {
270
+ rt.liveAssistantUsage = event.message.usage;
271
+ }
272
+ }
273
+ requestImmediateStatusRender(rt, { deferDuringTyping: false });
274
+ });
275
+
276
+ pi.on("turn_end", async (_event, ctx) => {
277
+ rt.currentCtx = ctx;
278
+ rt.coreContextUsageCache.reset();
279
+ requestImmediateStatusRender(rt, { deferDuringTyping: false });
280
+ });
281
+
282
+ pi.on("session_before_compact", async (_event, ctx) => {
283
+ rt.powerlineCompacting = true;
284
+ rt.currentCtx = ctx;
285
+ requestQueueRender(rt);
286
+ });
287
+
288
+ pi.on("session_compact", async (event, ctx) => {
289
+ rt.powerlineCompacting = false;
290
+ rt.currentCtx = ctx;
291
+ // Compaction rewrites the conversation, so the cached context-usage (tokens/window/percent)
292
+ // is stale. Reset it and force a redraw — otherwise the bar keeps showing the pre-compact fill.
293
+ rt.coreContextUsageCache.reset();
294
+ requestImmediateStatusRender(rt, { deferDuringTyping: false });
295
+ if (event.willRetry) {
296
+ rt.deliverAfterRetrySettles = true;
297
+ } else {
298
+ rt.deliverAfterRetrySettles = false;
299
+ schedulePostCompactionDelivery(pi, rt, ctx);
300
+ }
301
+ requestQueueRender(rt);
302
+ });
303
+
304
+ pi.on("agent_settled", async (_event, ctx) => {
305
+ if (rt.powerlineCompacting) {
306
+ finishFailedCompaction(rt, ctx, "Compaction did not complete");
307
+ return;
308
+ }
309
+ if (rt.deliverAfterRetrySettles) {
310
+ rt.deliverAfterRetrySettles = false;
311
+ schedulePostCompactionDelivery(pi, rt, ctx);
312
+ }
313
+ });
314
+
315
+ // Also dismiss on tool calls (agent is working) + refresh vibe if rate limit allows
316
+ pi.on("tool_call", async (event, ctx) => {
317
+ dismissWelcome(rt, ctx);
318
+ if (ctx.hasUI) {
319
+ // Extract recent agent context from session for richer vibe generation
320
+ const agentContext = getRecentAgentContext(ctx);
321
+ onVibeToolCall(
322
+ event.toolName,
323
+ event.input,
324
+ ctx.ui.setWorkingMessage,
325
+ agentContext,
326
+ );
327
+ }
328
+ });
329
+
330
+ pi.on("agent_end", async (_event, ctx) => {
331
+ rt.isStreaming = false;
332
+ rt.liveAssistantUsage = null;
333
+ rt.coreContextUsageCache.reset();
334
+
335
+ let hasUI = false;
336
+ try {
337
+ hasUI = Boolean(ctx.hasUI);
338
+ } catch (error) {
339
+ if (!isStaleExtensionContextError(error)) throw error;
340
+ rt.currentCtx = null;
341
+ return;
342
+ }
343
+
344
+ rt.currentCtx = ctx;
345
+ try {
346
+ if (hasUI) {
347
+ onVibeAgentEnd(ctx.ui.setWorkingMessage); // working-vibes internal state + reset message
348
+ if (rt.stashedEditorText !== null) {
349
+ if (ctx.ui.getEditorText().trim() === "") {
350
+ ctx.ui.setEditorText(rt.stashedEditorText);
351
+ rt.stashedEditorText = null;
352
+ ctx.ui.setStatus("stash", undefined);
353
+ ctx.ui.notify("Stash restored", "info");
354
+ } else {
355
+ ctx.ui.notify(
356
+ "Stash preserved — clear editor then Alt+S to restore",
357
+ "info",
358
+ );
359
+ }
360
+ }
361
+ }
362
+ } catch (error) {
363
+ if (!isStaleExtensionContextError(error)) throw error;
364
+ rt.currentCtx = null;
365
+ return;
366
+ }
367
+
368
+ requestStatusRender(rt);
369
+ if (!rt.powerlineCompacting && !rt.deliverAfterRetrySettles) {
370
+ schedulePostCompactionDelivery(pi, rt, ctx);
371
+ }
372
+ });
373
+ }