@groeponline/pi-wishcraft 0.22.2 → 0.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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/extension/core/status-export.ts +11 -0
- package/src/extension/session/read-hints.ts +147 -0
- package/src/extension/session/session-lifecycle.ts +12 -4
- package/src/extension/settings/wishcraft-config.ts +1 -0
- package/src/extension/skills/skill-registry.ts +6 -0
- package/src/extension/skills/skill-status.ts +37 -0
- package/src/extension/ui/powerline-menu.ts +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.23.0] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `powerline.skills.count` status export, published on session start and after skill-cache invalidation.
|
|
9
|
+
- Read-tool continuation hints on partial core `read` results when the core output omits a range or offset summary.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Powerline Status menu keeps only ports, TPS, and toggle (removed unimplemented diagnostics entries).
|
|
13
|
+
|
|
5
14
|
## [0.22.2] - 2026-08-20
|
|
6
15
|
|
|
7
16
|
### Fixed
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ export const POWERLINE_STATUS_KEYS = {
|
|
|
9
9
|
preset: "powerline.preset",
|
|
10
10
|
tps: "powerline.tps",
|
|
11
11
|
ports: "powerline.ports",
|
|
12
|
+
skillsCount: "powerline.skills.count",
|
|
12
13
|
} as const;
|
|
13
14
|
|
|
14
15
|
/** Keys never rendered in powerline's own bar (they exist for other extensions). */
|
|
@@ -23,6 +24,8 @@ export interface PowerlineStatusSnapshot {
|
|
|
23
24
|
tps?: string | undefined;
|
|
24
25
|
/** open_ports count as text (`?` when a fleet host probe failed). */
|
|
25
26
|
ports?: string;
|
|
27
|
+
/** Skill catalog length as a decimal string. */
|
|
28
|
+
skillsCount?: string;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
type StatusPublisherCtx = {
|
|
@@ -47,9 +50,17 @@ export function buildPowerlineStatusExport(
|
|
|
47
50
|
if ("ports" in snapshot) {
|
|
48
51
|
entries.push([POWERLINE_STATUS_KEYS.ports, snapshot.ports]);
|
|
49
52
|
}
|
|
53
|
+
if ("skillsCount" in snapshot) {
|
|
54
|
+
entries.push([POWERLINE_STATUS_KEYS.skillsCount, snapshot.skillsCount]);
|
|
55
|
+
}
|
|
50
56
|
return entries;
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/** Format a skill-catalog length for the status export. */
|
|
60
|
+
export function formatSkillsCountStatusValue(count: number): string {
|
|
61
|
+
return String(count);
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
/** Publish only the snapshot keys present, via `ctx.ui.setStatus`. */
|
|
54
65
|
export function publishPowerlineStatuses(
|
|
55
66
|
ctx: StatusPublisherCtx | null | undefined,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readSettings } from "../settings/settings-io.ts";
|
|
2
|
+
|
|
3
|
+
export interface ReadToolInput {
|
|
4
|
+
offset?: number;
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ReadHintDetails {
|
|
9
|
+
truncation?: {
|
|
10
|
+
totalLines?: number;
|
|
11
|
+
outputLines?: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CORE_SHOWING_LINES =
|
|
16
|
+
/^\[Showing lines \d+[–-]\d+(?: of \d+)?\.(?: Use offset=\d+ to continue\.)?\]$/i;
|
|
17
|
+
const CORE_MORE_LINES =
|
|
18
|
+
/^\[?\d+ more lines in file\. Use offset=\d+ to continue\.?\]?$/i;
|
|
19
|
+
const OWN_READ_HINT = /^\d+ lines, showing \d+[–-]\d+, next offset \d+$/;
|
|
20
|
+
|
|
21
|
+
function isCoreRangeFooter(line: string): boolean {
|
|
22
|
+
const footer = line.trim();
|
|
23
|
+
return (
|
|
24
|
+
CORE_SHOWING_LINES.test(footer) ||
|
|
25
|
+
CORE_MORE_LINES.test(footer) ||
|
|
26
|
+
OWN_READ_HINT.test(footer)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function coreFooter(text: string): string {
|
|
31
|
+
const lines = text.split("\n");
|
|
32
|
+
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop();
|
|
33
|
+
return lines[lines.length - 1]?.trim() ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default on. `wishcraft.readHints: false` is the opt-out. */
|
|
37
|
+
export function readHintsEnabled(wishcraftSettings: unknown): boolean {
|
|
38
|
+
if (
|
|
39
|
+
!wishcraftSettings ||
|
|
40
|
+
typeof wishcraftSettings !== "object" ||
|
|
41
|
+
Array.isArray(wishcraftSettings)
|
|
42
|
+
) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return (wishcraftSettings as Record<string, unknown>).readHints !== false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when core read output already carries an offset/range continuation line. */
|
|
49
|
+
export function coreReadResultHasRangeSummary(
|
|
50
|
+
text: string,
|
|
51
|
+
_details?: ReadHintDetails,
|
|
52
|
+
): boolean {
|
|
53
|
+
return isCoreRangeFooter(coreFooter(text));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function countContentLines(text: string): number {
|
|
57
|
+
const lines = text.split("\n");
|
|
58
|
+
let end = lines.length;
|
|
59
|
+
while (end > 0 && lines[end - 1] === "") end--;
|
|
60
|
+
if (end > 0 && isCoreRangeFooter(lines[end - 1]!)) end--;
|
|
61
|
+
return end;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether to append a one-line read continuation hint to a core read result. */
|
|
65
|
+
export function shouldAppendReadHint(
|
|
66
|
+
input: ReadToolInput | null | undefined,
|
|
67
|
+
text: string,
|
|
68
|
+
details?: ReadHintDetails,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (!input) return false;
|
|
71
|
+
if (input.offset === undefined && input.limit === undefined) return false;
|
|
72
|
+
if (coreReadResultHasRangeSummary(text, details)) return false;
|
|
73
|
+
|
|
74
|
+
const lineCount = countContentLines(text);
|
|
75
|
+
if (lineCount === 0) return false;
|
|
76
|
+
|
|
77
|
+
const start = input.offset ?? 1;
|
|
78
|
+
const end = start + lineCount - 1;
|
|
79
|
+
const total = details?.truncation?.totalLines;
|
|
80
|
+
if (total !== undefined && end >= total) return false;
|
|
81
|
+
if (input.limit !== undefined && lineCount < input.limit) return false;
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Format the English continuation hint for a partial read window. */
|
|
87
|
+
export function formatReadHint(
|
|
88
|
+
input: ReadToolInput,
|
|
89
|
+
text: string,
|
|
90
|
+
details?: ReadHintDetails,
|
|
91
|
+
): string {
|
|
92
|
+
const start = input.offset ?? 1;
|
|
93
|
+
const lineCount = countContentLines(text);
|
|
94
|
+
const end = start + lineCount - 1;
|
|
95
|
+
const next = end + 1;
|
|
96
|
+
const total = details?.truncation?.totalLines;
|
|
97
|
+
if (total !== undefined) {
|
|
98
|
+
return `${total} lines, showing ${start}–${end}, next offset ${next}`;
|
|
99
|
+
}
|
|
100
|
+
return `${lineCount} lines, showing ${start}–${end}, next offset ${next}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function extractReadToolResultText(content: unknown): string {
|
|
104
|
+
if (typeof content === "string") return content;
|
|
105
|
+
if (!Array.isArray(content)) return "";
|
|
106
|
+
return content.map((block) =>
|
|
107
|
+
block && typeof block === "object" && "text" in block
|
|
108
|
+
? String((block as { text?: unknown }).text ?? "")
|
|
109
|
+
: "",
|
|
110
|
+
).join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Append a continuation hint to a core `read` tool_result.
|
|
115
|
+
* Returns a replacement payload; never mutates `event.input` or `event.content`.
|
|
116
|
+
*/
|
|
117
|
+
export function appendReadHintToEvent(event: {
|
|
118
|
+
input?: unknown;
|
|
119
|
+
content?: unknown;
|
|
120
|
+
details?: unknown;
|
|
121
|
+
}): { content: Array<{ type: "text"; text: string }> } | undefined {
|
|
122
|
+
const text = extractReadToolResultText(event.content);
|
|
123
|
+
const input = event.input as ReadToolInput | undefined;
|
|
124
|
+
const details = event.details as ReadHintDetails | undefined;
|
|
125
|
+
if (!shouldAppendReadHint(input, text, details) || !input) return undefined;
|
|
126
|
+
if (!Array.isArray(event.content)) return undefined;
|
|
127
|
+
const hint = formatReadHint(input, text, details);
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
...(event.content as Array<{ type: "text"; text: string }>),
|
|
131
|
+
{ type: "text", text: hint },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Apply the opt-out and append a hint without mutating `event.input`. */
|
|
137
|
+
export function maybeAppendReadHint(
|
|
138
|
+
event: {
|
|
139
|
+
input?: unknown;
|
|
140
|
+
content?: unknown;
|
|
141
|
+
details?: unknown;
|
|
142
|
+
},
|
|
143
|
+
cwd?: string,
|
|
144
|
+
): { content: Array<{ type: "text"; text: string }> } | undefined {
|
|
145
|
+
if (!readHintsEnabled(readSettings(cwd).wishcraft)) return undefined;
|
|
146
|
+
return appendReadHintToEvent(event);
|
|
147
|
+
}
|
|
@@ -70,10 +70,12 @@ import {
|
|
|
70
70
|
import { CONTEXT_STATUS_RENDER_MS } from "../core/constants.ts";
|
|
71
71
|
import type { RuntimeState } from "../core/types.ts";
|
|
72
72
|
import { isStaleExtensionContextError } from "./stale-context.ts";
|
|
73
|
+
import { dismissWelcome, scheduleDismissWelcome } from "../welcome/welcome-control.ts";
|
|
73
74
|
import {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} from "../
|
|
75
|
+
bindSkillsCountPublisher,
|
|
76
|
+
clearSkillsCountPublisher,
|
|
77
|
+
} from "../skills/skill-status.ts";
|
|
78
|
+
import { maybeAppendReadHint } from "./read-hints.ts";
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* Fire the configured `powerline.costAlert` warning at most once per session.
|
|
@@ -160,6 +162,7 @@ export function registerSessionLifecycle(
|
|
|
160
162
|
): void {
|
|
161
163
|
// Track session start
|
|
162
164
|
pi.on("session_start", async (event, ctx) => {
|
|
165
|
+
clearSkillsCountPublisher();
|
|
163
166
|
rt.shellSession?.dispose();
|
|
164
167
|
rt.shellSession = null;
|
|
165
168
|
rt.sessionGeneration++;
|
|
@@ -196,6 +199,7 @@ export function registerSessionLifecycle(
|
|
|
196
199
|
|
|
197
200
|
if (ctx.hasUI) {
|
|
198
201
|
ctx.ui.setStatus("stash", undefined);
|
|
202
|
+
bindSkillsCountPublisher(ctx);
|
|
199
203
|
const pendingIdeas = rt.queueStore
|
|
200
204
|
.activeItems(getQueueContext(ctx))
|
|
201
205
|
.filter((item) => item.intent === "idea").length;
|
|
@@ -226,6 +230,7 @@ export function registerSessionLifecycle(
|
|
|
226
230
|
});
|
|
227
231
|
|
|
228
232
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
233
|
+
clearSkillsCountPublisher();
|
|
229
234
|
rt.sessionGeneration++;
|
|
230
235
|
rt.dismissWelcomeOverlay?.();
|
|
231
236
|
rt.dismissWelcomeOverlay = null;
|
|
@@ -257,7 +262,7 @@ export function registerSessionLifecycle(
|
|
|
257
262
|
});
|
|
258
263
|
|
|
259
264
|
// Invalidate git status on file changes, trigger re-render on potential branch changes
|
|
260
|
-
pi.on("tool_result", async (event) => {
|
|
265
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
261
266
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
262
267
|
invalidateGitStatus();
|
|
263
268
|
requestStatusRender(rt);
|
|
@@ -266,6 +271,9 @@ export function registerSessionLifecycle(
|
|
|
266
271
|
if (event.toolName === "bash" && event.input?.command) {
|
|
267
272
|
invalidateGitForCommand(rt, String(event.input.command));
|
|
268
273
|
}
|
|
274
|
+
if (event.toolName === "read") {
|
|
275
|
+
return maybeAppendReadHint(event, ctx?.cwd);
|
|
276
|
+
}
|
|
269
277
|
});
|
|
270
278
|
|
|
271
279
|
// Also catch user escape commands (! prefix)
|
|
@@ -139,6 +139,7 @@ export function buildConfigGroups(settings: Record<string, unknown>): ConfigGrou
|
|
|
139
139
|
title: "Skills",
|
|
140
140
|
items: [
|
|
141
141
|
{ label: "Inline-expansie /command en $skill", path: "wishcraft.inlineSkills", kind: "toggle", hint: "nog niet actief zonder herstart" },
|
|
142
|
+
{ label: "Read hints", path: "wishcraft.readHints", kind: "toggle", hint: "off = no continuation hint after partial reads" },
|
|
142
143
|
],
|
|
143
144
|
},
|
|
144
145
|
{
|
|
@@ -96,6 +96,11 @@ let cachedAt = 0;
|
|
|
96
96
|
let cachedEntries: SkillEntry[] | null = null;
|
|
97
97
|
let cachedPathMap: Map<string, string> | null = null;
|
|
98
98
|
let cachedCwd: string | null = null;
|
|
99
|
+
let onCacheInvalidated: (() => void) | null = null;
|
|
100
|
+
|
|
101
|
+
export function setSkillCacheInvalidationHandler(handler: (() => void) | null): void {
|
|
102
|
+
onCacheInvalidated = handler;
|
|
103
|
+
}
|
|
99
104
|
|
|
100
105
|
const usageCache = new Map<string, SkillUsage>();
|
|
101
106
|
let usageLoaded = false;
|
|
@@ -106,6 +111,7 @@ export function invalidateSkillCache(): void {
|
|
|
106
111
|
cachedEntries = null;
|
|
107
112
|
cachedPathMap = null;
|
|
108
113
|
cachedCwd = null;
|
|
114
|
+
onCacheInvalidated?.();
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
/** Legacy prompts/loose-md dirs die inline-invocation altijd al scanden. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatSkillsCountStatusValue,
|
|
3
|
+
publishPowerlineStatuses,
|
|
4
|
+
} from "../core/status-export.ts";
|
|
5
|
+
import {
|
|
6
|
+
invalidateSkillCache,
|
|
7
|
+
loadSkillCatalog,
|
|
8
|
+
setSkillCacheInvalidationHandler,
|
|
9
|
+
} from "./skill-registry.ts";
|
|
10
|
+
|
|
11
|
+
type SkillsCountCtx = {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
ui?: { setStatus?: (key: string, value: string | undefined) => void };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function publishSkillsCount(ctx: SkillsCountCtx): void {
|
|
17
|
+
publishPowerlineStatuses(ctx, {
|
|
18
|
+
skillsCount: formatSkillsCountStatusValue(
|
|
19
|
+
loadSkillCatalog(ctx.cwd ?? process.cwd()).length,
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Drop the session-scoped publisher so later invalidations cannot use a stale UI. */
|
|
25
|
+
export function clearSkillsCountPublisher(): void {
|
|
26
|
+
setSkillCacheInvalidationHandler(null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Publish `powerline.skills.count` now and after every skill-cache invalidation.
|
|
31
|
+
* Replaces any previous session callback first.
|
|
32
|
+
*/
|
|
33
|
+
export function bindSkillsCountPublisher(ctx: SkillsCountCtx): void {
|
|
34
|
+
clearSkillsCountPublisher();
|
|
35
|
+
setSkillCacheInvalidationHandler(() => publishSkillsCount(ctx));
|
|
36
|
+
invalidateSkillCache();
|
|
37
|
+
}
|
|
@@ -21,13 +21,6 @@ export function buildPowerlineMenuItems(): PowerlineMenuNode[] {
|
|
|
21
21
|
{ id: "ports", label: "Open ports", description: "Full listening list" },
|
|
22
22
|
{ id: "tps", label: "TPS detail", description: "Live 1s window or override" },
|
|
23
23
|
{ id: "toggle", label: "Toggle powerline", description: "Use /powerline to enable or disable" },
|
|
24
|
-
{ id: "cpu", label: "CPU usage", description: "Current process CPU usage" },
|
|
25
|
-
{ id: "memory", label: "Memory usage", description: "Current process memory" },
|
|
26
|
-
{ id: "network", label: "Network status", description: "Network interface status" },
|
|
27
|
-
{ id: "uptime", label: "System uptime", description: "Host uptime" },
|
|
28
|
-
{ id: "version", label: "Version info", description: "Runtime and application versions" },
|
|
29
|
-
{ id: "logs", label: "Recent logs", description: "Show recent status messages" },
|
|
30
|
-
{ id: "diagnostics", label: "Diagnostics", description: "Run powerline diagnostics" },
|
|
31
24
|
],
|
|
32
25
|
},
|
|
33
26
|
];
|