@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "0.22.2",
3
+ "version": "0.23.0",
4
4
  "description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
5
5
  "type": "module",
6
6
  "files": [
@@ -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
- dismissWelcome,
75
- scheduleDismissWelcome,
76
- } from "../welcome/welcome-control.ts";
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
  ];