@groeponline/pi-wishcraft 1.17.0 → 1.23.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/AGENTS.md +42 -22
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +12 -1
- package/RELEASE.md +142 -49
- package/bash-mode/completion.ts +2 -2
- package/build-stamp.json +1 -1
- package/docs/configuration.md +6 -1
- package/docs/design/regression-testing.md +1 -1
- package/docs/testing.md +81 -0
- package/package.json +17 -10
- package/src/config/settings-registry.ts +2 -1
- package/src/config/structural-preset-table.ts +0 -1
- package/src/extension/commands/commands.ts +0 -1
- package/src/extension/core/peer-guard.ts +5 -0
- package/src/extension/core/segment-context.ts +139 -44
- package/src/extension/core/state.ts +1 -0
- package/src/extension/core/types.ts +5 -0
- package/src/extension/queue/queue-integration.ts +20 -2
- package/src/extension/session/session-lifecycle.ts +24 -3
- package/src/extension/skills/inline-invocation.ts +1 -1
- package/src/extension/ui/custom-editor.ts +1 -5
- package/src/extension/ui/deck/config-diagnostic-lines.ts +0 -6
- package/src/extension/ui/ports-panel.ts +0 -1
- package/src/extension/ui/token-overlays.ts +1 -1
- package/src/extension/welcome/welcome-integration.ts +65 -5
- package/src/i18n/nl.ts +3 -0
- package/src/segments/core.ts +0 -1
- package/src/segments/system.ts +12 -3
- package/src/studio/component.ts +0 -1
- package/src/theme/theme.ts +1 -1
- package/src/tools/graph.ts +0 -3
- package/src/usage/tps-ring.ts +172 -32
- package/src/welcome/banner.ts +14 -0
- package/src/welcome/overlay.ts +139 -7
- package/src/welcome/quick-launch.ts +44 -0
- package/src/welcome/types.ts +13 -0
- package/src/welcome/widgets/sessions-widget.ts +13 -9
- package/src/welcome/widgets/shortcuts-widget.ts +13 -5
- package/src/welcome/widgets/whats-new-widget.ts +16 -3
- package/src/working-vibes/frames.ts +81 -5
- package/src/working-vibes/index.ts +6 -1
- package/src/working-vibes/native-indicator.ts +71 -0
- package/src/working-vibes/storage.ts +5 -3
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "Pi coding agent extension: operator cockpit with a live powerline status bar, idea queue, skill search, sticky Bash, hooks, policy guardrails, and TUI session UX.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "bun@1.4.2",
|
|
6
7
|
"engines": {
|
|
7
8
|
"node": ">=22.19"
|
|
8
9
|
},
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
"*.md",
|
|
12
13
|
"*.json",
|
|
13
14
|
"!audit-baseline.json",
|
|
15
|
+
"!.oxlintrc.json",
|
|
14
16
|
"banner.png",
|
|
15
17
|
"docs/**/*.md",
|
|
16
18
|
"src/**/*.ts",
|
|
@@ -60,14 +62,18 @@
|
|
|
60
62
|
},
|
|
61
63
|
"scripts": {
|
|
62
64
|
"typecheck": "tsc --noEmit",
|
|
65
|
+
"lint": "oxlint --deny-warnings src tests scripts index.ts bash-mode queue",
|
|
66
|
+
"lint:fix": "oxlint --fix src tests scripts index.ts bash-mode queue",
|
|
63
67
|
"test": "node --experimental-strip-types --experimental-test-coverage --test-coverage-lines=70 --test-coverage-functions=60 --test tests/**/*.test.ts",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
68
|
+
"check": "bun run typecheck && bun run lint && bun run test",
|
|
69
|
+
"release": "bun scripts/release.mjs",
|
|
70
|
+
"prepublishOnly": "bun run typecheck && bun run lint && bun scripts/verify-package.mjs && bun run verify:pi-package",
|
|
71
|
+
"verify:package": "bun scripts/verify-package.mjs && bun scripts/verify-pi-package-contract.mjs",
|
|
67
72
|
"circular": "madge --circular src index.ts bash-mode queue",
|
|
68
|
-
"audit": "
|
|
69
|
-
"preview": "
|
|
70
|
-
"verify:pi-package": "
|
|
73
|
+
"audit": "bun scripts/audit-gate.mjs",
|
|
74
|
+
"preview": "bun scripts/preview.mjs",
|
|
75
|
+
"verify:pi-package": "bun scripts/verify-pi-package-contract.mjs",
|
|
76
|
+
"hooks:install": "git config core.hooksPath .githooks"
|
|
71
77
|
},
|
|
72
78
|
"peerDependencies": {
|
|
73
79
|
"@earendil-works/pi-ai": "*",
|
|
@@ -75,11 +81,12 @@
|
|
|
75
81
|
"@earendil-works/pi-tui": "*"
|
|
76
82
|
},
|
|
77
83
|
"devDependencies": {
|
|
78
|
-
"@earendil-works/pi-ai": "^
|
|
79
|
-
"@earendil-works/pi-coding-agent": "^
|
|
80
|
-
"@earendil-works/pi-tui": "^
|
|
84
|
+
"@earendil-works/pi-ai": "^1.5.0",
|
|
85
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
86
|
+
"@earendil-works/pi-tui": "^1.5.0",
|
|
81
87
|
"@types/node": "24.13.3",
|
|
82
88
|
"madge": "^8.0.0",
|
|
89
|
+
"oxlint": "^1.91.0",
|
|
83
90
|
"typescript": "5.9.3"
|
|
84
91
|
},
|
|
85
92
|
"pi": {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { tr } from "../i18n/index.ts";
|
|
8
8
|
import { MOTION_LEVELS } from "../motion/accessibility.ts";
|
|
9
|
+
import { workingIndicatorStyles } from "../working-vibes/frames.ts";
|
|
9
10
|
import { STRUCTURAL_PRESET_NAMES } from "./types.ts";
|
|
10
11
|
|
|
11
12
|
export type SettingValue = boolean | string | number;
|
|
@@ -108,7 +109,7 @@ export const SETTINGS_REGISTRY = [
|
|
|
108
109
|
{ id: "status.ports.host", path: "powerline.segmentOptions.openPorts.host", group: "status", label: "Ports probe host", kind: "text", hint: "SSH host to probe (fleet) · empty = this machine" },
|
|
109
110
|
{ id: "welcome.enabled", path: "powerline.welcome", group: "vibes", label: "Welcome overlay", kind: "toggle", defaultValue: SETTING_DEFAULTS["powerline.welcome"], hint: "on = overlay at startup, off = no welcome" },
|
|
110
111
|
{ id: "welcome.lanternMotion", path: "wishcraft.welcome.animateLantern", group: "vibes", label: "Animate wishcraft lantern", kind: "toggle", hint: "flicker on the lantern" },
|
|
111
|
-
{ id: "vibes.indicatorStyle", path: "wishcraft.workingIndicatorStyle", group: "vibes", label: "Working indicator", kind: "select", choices:
|
|
112
|
+
{ id: "vibes.indicatorStyle", path: "wishcraft.workingIndicatorStyle", group: "vibes", label: "Working indicator", kind: "select", choices: workingIndicatorStyles(), defaultValue: SETTING_DEFAULTS["wishcraft.workingIndicatorStyle"], hint: "frame style for Pi's own working spinner while the agent runs" },
|
|
112
113
|
{ id: "skills.inline", path: "wishcraft.inlineSkills", group: "skills", label: "Inline expand /command and $skill", kind: "toggle", hint: "needs a restart to take effect", restartRequired: true },
|
|
113
114
|
{ id: "skills.readHints", path: "wishcraft.readHints", group: "skills", label: "Read hints", kind: "toggle", defaultValue: SETTING_DEFAULTS["wishcraft.readHints"], hint: "off = no continuation hint after partial reads" },
|
|
114
115
|
{ id: "harness.hooks", path: "wishcraft.hooksEnabled", group: "harness", label: "Hooks enabled", kind: "toggle", defaultValue: SETTING_DEFAULTS["wishcraft.hooksEnabled"], hint: "gate for configured preToolUse / postToolUse / sessionStart hooks" },
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type KeyId } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { StatusLinePreset } from "../../config/types.ts";
|
|
4
3
|
import { PRESETS } from "../../config/presets.ts";
|
|
5
4
|
import { registerCdCommand } from "../../shell/cd-command.ts";
|
|
6
5
|
import { registerVibeCommand } from "./vibe-command.ts";
|
|
@@ -90,6 +90,11 @@ export const PEER_SYMBOL_REQUIREMENTS: readonly PeerSymbolRequirement[] = [
|
|
|
90
90
|
symbol: "truncateToWidth",
|
|
91
91
|
usedBy: "status line rendering",
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
pkg: "@earendil-works/pi-tui",
|
|
95
|
+
symbol: "wrapTextWithAnsi",
|
|
96
|
+
usedBy: "welcome news expansion",
|
|
97
|
+
},
|
|
93
98
|
{
|
|
94
99
|
pkg: "@earendil-works/pi-tui",
|
|
95
100
|
symbol: "SelectList",
|
|
@@ -15,6 +15,12 @@ import { getDefaultColors } from "../../theme/theme.ts";
|
|
|
15
15
|
import { getGitStatus } from "../../git/status.ts";
|
|
16
16
|
import { getQueueContext } from "../queue/queue-context.ts";
|
|
17
17
|
import { getUsageTokenTotal } from "../../usage/ledger.ts";
|
|
18
|
+
import type { SessionAssistantUsage } from "../../usage/ledger.ts";
|
|
19
|
+
import {
|
|
20
|
+
pushTpsSample,
|
|
21
|
+
ringMsForWindow,
|
|
22
|
+
tpsSamples,
|
|
23
|
+
} from "../../usage/tps-ring.ts";
|
|
18
24
|
import { tokenBudgetSnapshotForDay } from "../../usage/token-budget.ts";
|
|
19
25
|
import { dayKey } from "../../usage/usage-store.ts";
|
|
20
26
|
import {
|
|
@@ -119,9 +125,90 @@ export interface BuildSegmentContextOptions {
|
|
|
119
125
|
includeTokenBudget?: boolean;
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Structural subset of Pi's extension context that the context builder reads.
|
|
130
|
+
* Typed rather than `any` so a renamed Pi accessor fails the typecheck here
|
|
131
|
+
* instead of silently rendering defaults at runtime.
|
|
132
|
+
*/
|
|
133
|
+
export interface SegmentSourceContext {
|
|
134
|
+
sessionManager?: { getSessionId?: () => string | undefined };
|
|
135
|
+
model?: SegmentContext["model"];
|
|
136
|
+
modelRegistry?: {
|
|
137
|
+
// Pi types this as `(model: Model<Api>) => boolean`. Method-bivariance
|
|
138
|
+
// keeps that signature assignable without widening the segment model
|
|
139
|
+
// shape to the full SDK `Model` type.
|
|
140
|
+
isUsingOAuth?: {
|
|
141
|
+
bivariance(model: NonNullable<SegmentContext["model"]>): boolean;
|
|
142
|
+
}["bivariance"];
|
|
143
|
+
};
|
|
144
|
+
cwd?: string;
|
|
145
|
+
settingsManager?: {
|
|
146
|
+
getCompactionSettings?: () => { enabled?: boolean } | undefined;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface ContextMetrics {
|
|
151
|
+
contextTokens: number;
|
|
152
|
+
contextPercent: number;
|
|
153
|
+
contextWindow: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Token/context numbers: cached core usage wins, the live assistant usage is
|
|
158
|
+
* the fallback, and the model's window bounds the percentage.
|
|
159
|
+
*/
|
|
160
|
+
function resolveContextMetrics(
|
|
161
|
+
rt: RuntimeState,
|
|
162
|
+
ctx: SegmentSourceContext,
|
|
163
|
+
usage: SessionAssistantUsage | undefined,
|
|
164
|
+
): ContextMetrics {
|
|
165
|
+
const core =
|
|
166
|
+
rt.isStreaming && rt.liveAssistantUsage ? null : rt.coreContextUsageCache.get(ctx);
|
|
167
|
+
const contextTokens =
|
|
168
|
+
core?.contextTokens ?? (usage ? getUsageTokenTotal(usage) : 0);
|
|
169
|
+
const contextWindow = core?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
170
|
+
const contextPercent =
|
|
171
|
+
core?.contextPercent ??
|
|
172
|
+
(contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0);
|
|
173
|
+
return { contextTokens, contextPercent, contextWindow };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface CustomItemState {
|
|
177
|
+
effectiveCustomItems: SegmentContext["effectiveCustomItems"];
|
|
178
|
+
customItemsById: SegmentContext["customItemsById"];
|
|
179
|
+
hiddenExtensionStatusKeys: Set<string>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Merge configured + auto-discovered custom items and hide claimed statuses. */
|
|
183
|
+
function resolveCustomItemState(
|
|
184
|
+
extensionStatuses: ReadonlyMap<string, string>,
|
|
185
|
+
): CustomItemState {
|
|
186
|
+
const excludedStatusKeys = new Set<string>([
|
|
187
|
+
CUSTOM_COMPACTION_STATUS_KEY,
|
|
188
|
+
"stash",
|
|
189
|
+
...HIDDEN_POWERLINE_STATUS_KEYS,
|
|
190
|
+
]);
|
|
191
|
+
const effectiveCustomItems = deriveAutoCustomItems(
|
|
192
|
+
config.customItems,
|
|
193
|
+
extensionStatuses,
|
|
194
|
+
config.customItemsAuto,
|
|
195
|
+
excludedStatusKeys,
|
|
196
|
+
);
|
|
197
|
+
const customItemsById = new Map(
|
|
198
|
+
effectiveCustomItems.map((item) => [item.id, item]),
|
|
199
|
+
);
|
|
200
|
+
const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(
|
|
201
|
+
effectiveCustomItems,
|
|
202
|
+
);
|
|
203
|
+
for (const key of HIDDEN_POWERLINE_STATUS_KEYS) {
|
|
204
|
+
hiddenExtensionStatusKeys.add(key);
|
|
205
|
+
}
|
|
206
|
+
return { effectiveCustomItems, customItemsById, hiddenExtensionStatusKeys };
|
|
207
|
+
}
|
|
208
|
+
|
|
122
209
|
export function buildSegmentContext(
|
|
123
210
|
rt: RuntimeState,
|
|
124
|
-
ctx:
|
|
211
|
+
ctx: SegmentSourceContext,
|
|
125
212
|
theme: Theme,
|
|
126
213
|
options: BuildSegmentContextOptions = {},
|
|
127
214
|
): SegmentContext {
|
|
@@ -133,72 +220,39 @@ export function buildSegmentContext(
|
|
|
133
220
|
getDefaultColors,
|
|
134
221
|
);
|
|
135
222
|
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
223
|
+
// Usage stats are cached; the full event list is only re-scanned when events
|
|
224
|
+
// are appended or the trailing event's stats-relevant fields change (e.g.
|
|
225
|
+
// in-place streaming updates).
|
|
139
226
|
const sessionEvents = rt.sessionBranchCache.get(ctx.sessionManager);
|
|
140
227
|
const tokenStats = rt.tokenStatsCache.get(sessionEvents);
|
|
141
228
|
const { input, output, cacheRead, cacheWrite, cost, subagentCost } =
|
|
142
229
|
tokenStats;
|
|
143
230
|
const lastAssistant = tokenStats.lastAssistant;
|
|
144
|
-
const thinkingLevelFromSession = tokenStats.thinkingLevelFromSession;
|
|
145
231
|
|
|
146
|
-
// Calculate context percentage.
|
|
147
232
|
const latestUsage = rt.isStreaming
|
|
148
233
|
? (rt.liveAssistantUsage ?? lastAssistant?.usage)
|
|
149
234
|
: lastAssistant?.usage;
|
|
150
|
-
const
|
|
151
|
-
rt
|
|
152
|
-
? null
|
|
153
|
-
: rt.coreContextUsageCache.get(ctx);
|
|
154
|
-
const contextTokens =
|
|
155
|
-
coreContextUsage?.contextTokens ??
|
|
156
|
-
(latestUsage ? getUsageTokenTotal(latestUsage) : 0);
|
|
157
|
-
const contextWindow =
|
|
158
|
-
coreContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
159
|
-
const contextPercent =
|
|
160
|
-
coreContextUsage?.contextPercent ??
|
|
161
|
-
(contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0);
|
|
235
|
+
const { contextTokens, contextPercent, contextWindow } =
|
|
236
|
+
resolveContextMetrics(rt, ctx, latestUsage);
|
|
162
237
|
|
|
163
238
|
const segmentOptions = mergeSegmentOptions(
|
|
164
239
|
presetDef.segmentOptions,
|
|
165
240
|
config.segmentOptions,
|
|
166
241
|
);
|
|
167
242
|
|
|
168
|
-
// Get git status (cached)
|
|
169
243
|
const gitBranch = rt.footerDataRef?.getGitBranch() ?? null;
|
|
170
244
|
const gitStatus = getGitStatus(gitBranch, segmentOptions.git?.polling);
|
|
171
|
-
const extensionStatuses =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
CUSTOM_COMPACTION_STATUS_KEY,
|
|
175
|
-
"stash",
|
|
176
|
-
...HIDDEN_POWERLINE_STATUS_KEYS,
|
|
177
|
-
]);
|
|
178
|
-
const effectiveCustomItems = deriveAutoCustomItems(
|
|
179
|
-
config.customItems,
|
|
180
|
-
extensionStatuses,
|
|
181
|
-
config.customItemsAuto,
|
|
182
|
-
excludedStatusKeys,
|
|
183
|
-
);
|
|
184
|
-
const customItemsById = new Map(
|
|
185
|
-
effectiveCustomItems.map((item) => [item.id, item]),
|
|
186
|
-
);
|
|
187
|
-
const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(
|
|
188
|
-
effectiveCustomItems,
|
|
189
|
-
);
|
|
190
|
-
for (const key of HIDDEN_POWERLINE_STATUS_KEYS) {
|
|
191
|
-
hiddenExtensionStatusKeys.add(key);
|
|
192
|
-
}
|
|
245
|
+
const extensionStatuses = rt.footerDataRef?.getExtensionStatuses() ?? new Map();
|
|
246
|
+
const { effectiveCustomItems, customItemsById, hiddenExtensionStatusKeys } =
|
|
247
|
+
resolveCustomItemState(extensionStatuses);
|
|
193
248
|
|
|
194
|
-
// Check if using OAuth subscription
|
|
195
249
|
const usingSubscription = ctx.model
|
|
196
250
|
? (ctx.modelRegistry?.isUsingOAuth?.(ctx.model) ?? false)
|
|
197
251
|
: false;
|
|
198
252
|
|
|
199
253
|
const thinkingLevel =
|
|
200
254
|
rt.currentThinkingLevel ??
|
|
201
|
-
thinkingLevelFromSession ??
|
|
255
|
+
tokenStats.thinkingLevelFromSession ??
|
|
202
256
|
rt.getThinkingLevelFn?.() ??
|
|
203
257
|
"off";
|
|
204
258
|
const queueSummary = rt.queueStore.summarize(
|
|
@@ -248,4 +302,45 @@ export function buildSegmentContext(
|
|
|
248
302
|
theme,
|
|
249
303
|
colors,
|
|
250
304
|
};
|
|
251
|
-
}
|
|
305
|
+
}
|
|
306
|
+
const TPS_EVENT_MIN_GAP_MS = 30;
|
|
307
|
+
let lastTpsEventAt = 0;
|
|
308
|
+
let lastMarkedTurnStart: number | null = null;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Event-driven TPS sampling, called from `message_update`. The ring used to be
|
|
312
|
+
* filled only while the `tps` segment rendered, so presets without that segment
|
|
313
|
+
* left `/tps` empty and every unrelated repaint added a duplicate sample.
|
|
314
|
+
*
|
|
315
|
+
* This is also the only writer of the turn marker: the first sample of a turn
|
|
316
|
+
* carries `turnStartAt`, the rest do not, so `measureTurn` can find both the
|
|
317
|
+
* turn boundary and the sample that produced its first token.
|
|
318
|
+
*/
|
|
319
|
+
export function sampleTpsFromSession(
|
|
320
|
+
rt: RuntimeState,
|
|
321
|
+
ctx: SegmentSourceContext,
|
|
322
|
+
now: number = Date.now(),
|
|
323
|
+
): void {
|
|
324
|
+
if (now - lastTpsEventAt < TPS_EVENT_MIN_GAP_MS) return;
|
|
325
|
+
lastTpsEventAt = now;
|
|
326
|
+
const events = rt.sessionBranchCache.get(ctx.sessionManager);
|
|
327
|
+
const { input, output, cacheRead } = rt.tokenStatsCache.get(events);
|
|
328
|
+
const windowMs =
|
|
329
|
+
mergeSegmentOptions(getPreset(config.preset).segmentOptions, config.segmentOptions)
|
|
330
|
+
.tps?.windowMs ?? 1000;
|
|
331
|
+
const turnStartAt =
|
|
332
|
+
rt.turnStartedAt !== null && rt.turnStartedAt !== lastMarkedTurnStart
|
|
333
|
+
? (lastMarkedTurnStart = rt.turnStartedAt)
|
|
334
|
+
: undefined;
|
|
335
|
+
pushTpsSample(
|
|
336
|
+
tpsSamples,
|
|
337
|
+
{
|
|
338
|
+
at: now,
|
|
339
|
+
output,
|
|
340
|
+
input,
|
|
341
|
+
uncachedInput: Math.max(0, input - cacheRead),
|
|
342
|
+
...(turnStartAt === undefined ? {} : { turnStartAt }),
|
|
343
|
+
},
|
|
344
|
+
ringMsForWindow(windowMs),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
@@ -111,6 +111,7 @@ export function createRuntimeState(
|
|
|
111
111
|
welcomeHeaderActive: false,
|
|
112
112
|
welcomeOverlayShouldDismiss: false,
|
|
113
113
|
lastUserPrompt: "",
|
|
114
|
+
turnStartedAt: null,
|
|
114
115
|
showLastPrompt: true,
|
|
115
116
|
stashedEditorText: null,
|
|
116
117
|
stashedPromptHistory: readPersistedStashHistory(),
|
|
@@ -61,6 +61,11 @@ export interface RuntimeState {
|
|
|
61
61
|
welcomeHeaderActive: boolean;
|
|
62
62
|
welcomeOverlayShouldDismiss: boolean;
|
|
63
63
|
lastUserPrompt: string;
|
|
64
|
+
/**
|
|
65
|
+
* Timestamp (ms) of the current turn's `agent_start`; null between turns.
|
|
66
|
+
* Only the first TPS sample of a turn carries it as a ring marker.
|
|
67
|
+
*/
|
|
68
|
+
turnStartedAt: number | null;
|
|
64
69
|
showLastPrompt: boolean;
|
|
65
70
|
stashedEditorText: string | null;
|
|
66
71
|
stashedPromptHistory: string[];
|
|
@@ -18,6 +18,9 @@ import { showSelectOverlay } from "../ui/menu-views.ts";
|
|
|
18
18
|
import { getQueueContext } from "./queue-context.ts";
|
|
19
19
|
import { isStaleExtensionContextError } from "../session/stale-context.ts";
|
|
20
20
|
import { config } from "../core/state.ts";
|
|
21
|
+
import { allWorkingIndicatorFrames } from "../../working-vibes/frames.ts";
|
|
22
|
+
import { MOTION_CATALOG } from "../../motion/catalog.ts";
|
|
23
|
+
import { framesOf } from "../../motion/frames.ts";
|
|
21
24
|
import type { RuntimeState } from "../core/types.ts";
|
|
22
25
|
|
|
23
26
|
export function requestQueueRender(rt: RuntimeState): void {
|
|
@@ -40,9 +43,24 @@ export function queueItemDescription(item: PowerlineQueueItem): string {
|
|
|
40
43
|
* Pi's working indicator (spinner frame + "Working...") can end up at the start
|
|
41
44
|
* of captured text when the editor buffer is captured while the agent runs.
|
|
42
45
|
* Strip that prefix so ideas never render as a permanently "working" card.
|
|
46
|
+
*
|
|
47
|
+
* The frame alternation is built from the same tables the indicator draws from
|
|
48
|
+
* (`working-vibes/frames.ts` plus the Motion Catalog the `motion` style
|
|
49
|
+
* borrows), so a new style or motion can never leak its glyphs into an idea.
|
|
43
50
|
*/
|
|
44
|
-
const
|
|
45
|
-
|
|
51
|
+
const SPINNER_FRAMES = [
|
|
52
|
+
...new Set([
|
|
53
|
+
...allWorkingIndicatorFrames(),
|
|
54
|
+
...MOTION_CATALOG.flatMap((def) => framesOf(def)),
|
|
55
|
+
]),
|
|
56
|
+
]
|
|
57
|
+
.filter((frame) => frame.trim().length > 0)
|
|
58
|
+
.sort((a, b) => b.length - a.length)
|
|
59
|
+
.map((frame) => frame.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
60
|
+
|
|
61
|
+
const CAPTURE_NOISE_PREFIX = new RegExp(
|
|
62
|
+
`^\\s*(?:(?:${SPINNER_FRAMES.join("|")})\\s*)*Working[.…]+\\s*`,
|
|
63
|
+
);
|
|
46
64
|
|
|
47
65
|
export function stripCaptureNoisePrefix(text: string): string {
|
|
48
66
|
let out = text;
|
|
@@ -4,6 +4,7 @@ import { BashTranscriptStore } from "../../../bash-mode/transcript.ts";
|
|
|
4
4
|
import { BashCompletionEngine } from "../../../bash-mode/completion.ts";
|
|
5
5
|
import { parsePowerlineConfig } from "../../config/powerline-config.ts";
|
|
6
6
|
import { policyFromEnvironment } from "../../motion/accessibility.ts";
|
|
7
|
+
import { defaultMotionFor } from "../../motion/catalog.ts";
|
|
7
8
|
import { registerCustomSegments } from "../../segments/index.ts";
|
|
8
9
|
import { registerCustomPresets } from "../../config/presets.ts";
|
|
9
10
|
import { invalidateGitStatus } from "../../git/status.ts";
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
requestImmediateStatusRender,
|
|
35
36
|
requestStatusRender,
|
|
36
37
|
resetLayoutCache,
|
|
38
|
+
sampleTpsFromSession,
|
|
37
39
|
} from "../core/segment-context.ts";
|
|
38
40
|
import { getQueueContext } from "../queue/queue-context.ts";
|
|
39
41
|
import {
|
|
@@ -62,6 +64,10 @@ import {
|
|
|
62
64
|
clearSkillsCountPublisher,
|
|
63
65
|
} from "../skills/skill-status.ts";
|
|
64
66
|
import { maybeAppendReadHint } from "./read-hints.ts";
|
|
67
|
+
import {
|
|
68
|
+
applyNativeWorkingIndicator,
|
|
69
|
+
resetNativeWorkingIndicator,
|
|
70
|
+
} from "../../working-vibes/native-indicator.ts";
|
|
65
71
|
import {
|
|
66
72
|
dispatchSignalEvent,
|
|
67
73
|
settleSignal,
|
|
@@ -181,7 +187,7 @@ export function registerSessionLifecycle(
|
|
|
181
187
|
}
|
|
182
188
|
});
|
|
183
189
|
|
|
184
|
-
pi.on("session_shutdown", async (_event,
|
|
190
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
185
191
|
clearSkillsCountPublisher();
|
|
186
192
|
rt.sessionGeneration++;
|
|
187
193
|
rt.dismissWelcomeOverlay?.();
|
|
@@ -260,9 +266,16 @@ export function registerSessionLifecycle(
|
|
|
260
266
|
|
|
261
267
|
// Generate themed working message before agent starts (has access to user's prompt)
|
|
262
268
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
263
|
-
|
|
269
|
+
const prompt =
|
|
270
|
+
event.messageType === "user"
|
|
271
|
+
? event.prompt
|
|
272
|
+
: event.message.display && typeof event.message.content === "string"
|
|
273
|
+
? event.message.content
|
|
274
|
+
: "";
|
|
275
|
+
rt.lastUserPrompt = prompt;
|
|
264
276
|
if (ctx.hasUI) {
|
|
265
|
-
onVibeBeforeAgentStart(
|
|
277
|
+
onVibeBeforeAgentStart(prompt, ctx.ui.setWorkingMessage);
|
|
278
|
+
applyNativeWorkingIndicator(ctx.ui, rt.motionPolicy, defaultMotionFor("thinking"));
|
|
266
279
|
}
|
|
267
280
|
});
|
|
268
281
|
|
|
@@ -270,6 +283,7 @@ export function registerSessionLifecycle(
|
|
|
270
283
|
// Also dismiss welcome when agent starts responding (handles `p "command"` case)
|
|
271
284
|
pi.on("agent_start", async (_event, ctx) => {
|
|
272
285
|
rt.isStreaming = true;
|
|
286
|
+
rt.turnStartedAt = Date.now();
|
|
273
287
|
rt.liveAssistantUsage = null;
|
|
274
288
|
onVibeAgentStart();
|
|
275
289
|
dismissWelcome(rt, ctx);
|
|
@@ -287,6 +301,11 @@ export function registerSessionLifecycle(
|
|
|
287
301
|
rt.liveAssistantUsage = event.message.usage;
|
|
288
302
|
rt.currentCtx = ctx;
|
|
289
303
|
rt.layoutDirty = true;
|
|
304
|
+
try {
|
|
305
|
+
sampleTpsFromSession(rt, ctx);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
308
|
+
}
|
|
290
309
|
rt.statusRenderScheduler.schedule(CONTEXT_STATUS_RENDER_MS);
|
|
291
310
|
if (rt.signal.event !== "streaming") {
|
|
292
311
|
dispatchSignalEvent(rt, config.appearance, "streaming");
|
|
@@ -406,6 +425,7 @@ export function registerSessionLifecycle(
|
|
|
406
425
|
|
|
407
426
|
pi.on("agent_end", async (_event, ctx) => {
|
|
408
427
|
rt.isStreaming = false;
|
|
428
|
+
rt.turnStartedAt = null;
|
|
409
429
|
rt.liveAssistantUsage = null;
|
|
410
430
|
rt.coreContextUsageCache.reset();
|
|
411
431
|
dispatchSignalEvent(rt, config.appearance, "success");
|
|
@@ -423,6 +443,7 @@ export function registerSessionLifecycle(
|
|
|
423
443
|
try {
|
|
424
444
|
if (hasUI) {
|
|
425
445
|
onVibeAgentEnd(ctx.ui.setWorkingMessage); // working-vibes internal state + reset message
|
|
446
|
+
resetNativeWorkingIndicator(ctx.ui);
|
|
426
447
|
if (rt.stashedEditorText !== null) {
|
|
427
448
|
if (ctx.ui.getEditorText().trim() === "") {
|
|
428
449
|
ctx.ui.setEditorText(rt.stashedEditorText);
|
|
@@ -162,7 +162,7 @@ export function expandInlineTriggers(text: string): string {
|
|
|
162
162
|
*/
|
|
163
163
|
export function setupInlineInvocation(
|
|
164
164
|
pi: ExtensionAPI,
|
|
165
|
-
|
|
165
|
+
_rt: RuntimeState,
|
|
166
166
|
): void {
|
|
167
167
|
pi.on("input", async (event: any) => {
|
|
168
168
|
// Alleen interactive input verwerken
|
|
@@ -48,14 +48,10 @@ import {
|
|
|
48
48
|
runPowerlineShortcut,
|
|
49
49
|
stashOrRestoreEditorText,
|
|
50
50
|
} from "../shortcuts/shortcuts-router.ts";
|
|
51
|
-
import {
|
|
52
|
-
dismissWelcome,
|
|
53
|
-
scheduleDismissWelcome,
|
|
54
|
-
} from "../welcome/welcome-control.ts";
|
|
51
|
+
import { scheduleDismissWelcome } from "../welcome/welcome-control.ts";
|
|
55
52
|
import { config } from "../core/state.ts";
|
|
56
53
|
import type { RuntimeState } from "../core/types.ts";
|
|
57
54
|
import {
|
|
58
|
-
ensureShellSession,
|
|
59
55
|
getShellCwd,
|
|
60
56
|
getShellHistoryEntries,
|
|
61
57
|
getShellPath,
|
|
@@ -19,12 +19,6 @@ const MAX_CONFLICTS = 3;
|
|
|
19
19
|
const MAX_UNKNOWN = 3;
|
|
20
20
|
const MAX_INVALID = 4;
|
|
21
21
|
|
|
22
|
-
const MARK: Record<"ok" | "warn" | "fail", string> = {
|
|
23
|
-
ok: "✓",
|
|
24
|
-
warn: "!",
|
|
25
|
-
fail: "✗",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
22
|
/** Localised heading for the config diagnosis block. */
|
|
29
23
|
export function configDiagnosticTitle(): string {
|
|
30
24
|
return tr("diag.title", "Config diagnosis");
|
|
@@ -35,7 +35,7 @@ export function buildTpsOverlayItems(rt: RuntimeState, ctx: any): SelectItem[] {
|
|
|
35
35
|
const summary = summarizeTpsRing(
|
|
36
36
|
tpsSamples,
|
|
37
37
|
Date.now(),
|
|
38
|
-
{ input: snap.input, output: snap.output },
|
|
38
|
+
{ input: snap.input, output: snap.output, cacheRead: snap.cacheRead },
|
|
39
39
|
{
|
|
40
40
|
windowMs: config.segmentOptions?.tps?.windowMs ?? 1000,
|
|
41
41
|
override: process.env.POWERLINE_TPS,
|
|
@@ -183,7 +183,7 @@ export function setupWelcomeOverlay(rt: RuntimeState, ctx: any) {
|
|
|
183
183
|
(
|
|
184
184
|
tui: any,
|
|
185
185
|
_theme: any,
|
|
186
|
-
|
|
186
|
+
keybindings: any,
|
|
187
187
|
done: (result: void) => void,
|
|
188
188
|
) => {
|
|
189
189
|
const welcome = new WelcomeComponent(
|
|
@@ -205,22 +205,51 @@ export function setupWelcomeOverlay(rt: RuntimeState, ctx: any) {
|
|
|
205
205
|
let countdown = 30;
|
|
206
206
|
let dismissed = false;
|
|
207
207
|
let interval: ReturnType<typeof setInterval> | null = null;
|
|
208
|
+
let revealTimer: ReturnType<typeof setInterval> | null = null;
|
|
209
|
+
|
|
210
|
+
const stopTimers = () => {
|
|
211
|
+
if (interval) clearInterval(interval);
|
|
212
|
+
if (revealTimer) clearInterval(revealTimer);
|
|
213
|
+
interval = null;
|
|
214
|
+
revealTimer = null;
|
|
215
|
+
};
|
|
208
216
|
|
|
209
217
|
const dismiss = () => {
|
|
210
218
|
if (dismissed) return;
|
|
211
219
|
dismissed = true;
|
|
212
|
-
|
|
220
|
+
stopTimers();
|
|
213
221
|
rt.dismissWelcomeOverlay = null;
|
|
214
222
|
done();
|
|
215
223
|
};
|
|
216
224
|
|
|
217
225
|
interval = setInterval(() => {
|
|
218
|
-
if (dismissed) return;
|
|
226
|
+
if (dismissed || welcome.isTimerPaused()) return;
|
|
219
227
|
countdown--;
|
|
220
228
|
welcome.setCountdown(countdown);
|
|
221
229
|
tui.requestRender();
|
|
222
230
|
if (countdown <= 0) dismiss();
|
|
223
231
|
}, 1000);
|
|
232
|
+
interval.unref?.();
|
|
233
|
+
|
|
234
|
+
// The countdown only repaints once a second, which stepped the boot
|
|
235
|
+
// reveal at 1 FPS. Repaint on the ambient heartbeat: bounded while
|
|
236
|
+
// the reveal window is open, continuous while the lantern animates.
|
|
237
|
+
const revealStartedAt = Date.now();
|
|
238
|
+
const animateArt = artSettings.animate;
|
|
239
|
+
revealTimer = setInterval(() => {
|
|
240
|
+
if (dismissed) {
|
|
241
|
+
if (revealTimer) clearInterval(revealTimer);
|
|
242
|
+
revealTimer = null;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!animateArt && !bootRevealActive(revealStartedAt, Date.now())) {
|
|
246
|
+
if (revealTimer) clearInterval(revealTimer);
|
|
247
|
+
revealTimer = null;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
tui.requestRender();
|
|
251
|
+
}, BOOT_REVEAL_HEARTBEAT_MS);
|
|
252
|
+
revealTimer.unref?.();
|
|
224
253
|
|
|
225
254
|
rt.dismissWelcomeOverlay = dismiss;
|
|
226
255
|
|
|
@@ -233,10 +262,41 @@ export function setupWelcomeOverlay(rt: RuntimeState, ctx: any) {
|
|
|
233
262
|
focused: false,
|
|
234
263
|
invalidate: () => welcome.invalidate(),
|
|
235
264
|
render: (width: number) => welcome.render(width),
|
|
236
|
-
handleInput: () =>
|
|
265
|
+
handleInput: (data: string) => {
|
|
266
|
+
// Only non-printable navigation is intercepted; Tab/Shift+Tab
|
|
267
|
+
// switch section, arrows move inside it, Enter (only once the
|
|
268
|
+
// countdown is paused) primes the focused quick-launch row.
|
|
269
|
+
// Every printable key still just dismisses, so typing is never
|
|
270
|
+
// swallowed by the overlay.
|
|
271
|
+
const nextSection = data === "\t";
|
|
272
|
+
const prevSection = data === "\x1b[Z";
|
|
273
|
+
if (nextSection || prevSection) {
|
|
274
|
+
welcome.cycleSection(prevSection ? -1 : 1);
|
|
275
|
+
tui.requestRender();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const up = keybindings?.matches?.(data, "tui.select.up") || data === "\x1b[A";
|
|
279
|
+
const down = keybindings?.matches?.(data, "tui.select.down") || data === "\x1b[B";
|
|
280
|
+
if (up || down) {
|
|
281
|
+
welcome.moveFocus(up ? -1 : 1);
|
|
282
|
+
tui.requestRender();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const enter =
|
|
286
|
+
keybindings?.matches?.(data, "tui.select.confirm") ||
|
|
287
|
+
data === "\r" ||
|
|
288
|
+
data === "\n";
|
|
289
|
+
if (enter && welcome.isTimerPaused()) {
|
|
290
|
+
const insert = welcome.activate();
|
|
291
|
+
dismiss();
|
|
292
|
+
if (insert) ctx.ui.setEditorText?.(insert);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
dismiss();
|
|
296
|
+
},
|
|
237
297
|
dispose: () => {
|
|
238
298
|
dismissed = true;
|
|
239
|
-
|
|
299
|
+
stopTimers();
|
|
240
300
|
},
|
|
241
301
|
};
|
|
242
302
|
},
|
package/src/i18n/nl.ts
CHANGED
|
@@ -311,6 +311,9 @@ export const NL: Record<string, string> = {
|
|
|
311
311
|
"welcome.tagline": "Schrijf het op, laat het stijgen, houd je focus helder.",
|
|
312
312
|
"welcome.title": " pi-wishcraft ",
|
|
313
313
|
"welcome.countdown": "Druk op een toets om verder te gaan ({n}s)",
|
|
314
|
+
"welcome.pausedHint": "↑/↓ nieuws bekijken · andere toets gaat verder",
|
|
315
|
+
"welcome.pausedHint.sessions": "↑/↓ sessies bekijken · tab wisselt sectie · andere toets gaat verder",
|
|
316
|
+
"welcome.pausedHint.shortcuts": "↑/↓ kiezen · enter vult de prompt · tab wisselt sectie",
|
|
314
317
|
"welcome.whatsNew": "Wat is nieuw",
|
|
315
318
|
"welcome.firstRun": "Aan de slag",
|
|
316
319
|
"welcome.firstRun.1": "# <idee> — leg een gedachte vast zonder te onderbreken",
|