@pi-unipi/info-screen 0.1.3 → 0.1.5

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/core-groups.ts CHANGED
@@ -180,6 +180,7 @@ const loadTimes: Array<{ name: string; type: string; ms: number }> = [];
180
180
  let totalLoadTimeMs = 0;
181
181
  let loadTrackingStarted = false;
182
182
  let loadTrackingStartMs = 0;
183
+ const moduleStartTimes = new Map<string, number>();
183
184
 
184
185
  /** Start load time tracking */
185
186
  export function startLoadTracking(): void {
@@ -189,10 +190,28 @@ export function startLoadTracking(): void {
189
190
  }
190
191
  }
191
192
 
193
+ /** Record when a module starts loading */
194
+ export function recordModuleStart(name: string): void {
195
+ moduleStartTimes.set(name, Date.now());
196
+ }
197
+
192
198
  /** Record a load time */
193
- export function recordLoadTime(name: string, type: string, ms: number): void {
194
- loadTimes.push({ name, type, ms });
195
- totalLoadTimeMs += ms;
199
+ export function recordLoadTime(name: string, type: string, ms?: number): void {
200
+ // If no ms provided, calculate from start time
201
+ if (ms === undefined || ms === 0) {
202
+ const startTime = moduleStartTimes.get(name);
203
+ if (startTime) {
204
+ ms = Date.now() - startTime;
205
+ } else {
206
+ ms = 0;
207
+ }
208
+ }
209
+ // Avoid duplicates
210
+ const existing = loadTimes.find(t => t.name === name && t.type === type);
211
+ if (!existing) {
212
+ loadTimes.push({ name, type, ms });
213
+ totalLoadTimeMs += ms;
214
+ }
196
215
  }
197
216
 
198
217
  /** Finish load tracking */
@@ -209,7 +228,7 @@ export function getLoadTimes(): Array<{ name: string; type: string; ms: number }
209
228
 
210
229
  /** Get total load time */
211
230
  export function getTotalLoadTime(): number {
212
- return totalLoadTimeMs;
231
+ return totalLoadTimeMs > 0 ? totalLoadTimeMs : (loadTrackingStarted ? Date.now() - loadTrackingStartMs : 0);
213
232
  }
214
233
 
215
234
  /**
@@ -388,17 +407,13 @@ export function registerCoreGroups(): void {
388
407
  const times = getLoadTimes();
389
408
  const total = getTotalLoadTime();
390
409
 
391
- // Sort by load time descending
392
- const sorted = [...times].sort((a, b) => b.ms - a.ms);
410
+ // Filter out 0ms entries and sort by load time descending
411
+ const validTimes = times.filter(t => t.ms > 0);
412
+ const sorted = [...validTimes].sort((a, b) => b.ms - a.ms);
393
413
 
394
- // Build list as comma-separated values
395
- const listStr = sorted.length > 0
396
- ? sorted.map(t => `${t.name} (${t.ms}ms)`).join(", ")
397
- : "none";
398
-
399
414
  return {
400
415
  total: { value: `${total}ms` },
401
- count: { value: String(times.length) },
416
+ count: { value: String(validTimes.length) },
402
417
  list: {
403
418
  value: sorted.length > 0 ? `${sorted[0].name} (${sorted[0].ms}ms)` : "none",
404
419
  detail: sorted.length > 1 ? sorted.slice(1).map(t => `${t.name} (${t.ms}ms)`).join(", ") : undefined,
@@ -510,16 +525,28 @@ export function registerCoreGroups(): void {
510
525
  return source === "sdk";
511
526
  });
512
527
 
513
- // Build tool list as comma-separated values
528
+ // Build tool list as comma-separated values with wrapping
514
529
  const toolNames = tools.map((t) => `${t.name}`);
515
- const toolListStr = toolNames.join(", ");
530
+ // Split into chunks of ~60 chars for wrapping
531
+ const chunks: string[] = [];
532
+ let current = "";
533
+ for (const name of toolNames) {
534
+ if (current && (current.length + name.length + 2) > 60) {
535
+ chunks.push(current);
536
+ current = name;
537
+ } else {
538
+ current = current ? `${current}, ${name}` : name;
539
+ }
540
+ }
541
+ if (current) chunks.push(current);
516
542
 
517
543
  return {
518
544
  total: { value: String(tools.length) },
519
545
  builtin: { value: String(builtin.length) },
520
546
  registered: { value: String(extension.length + sdk.length) },
521
547
  list: {
522
- value: toolListStr.length > 0 ? toolListStr : "none",
548
+ value: chunks.length > 0 ? chunks[0] : "none",
549
+ detail: chunks.length > 1 ? chunks.slice(1).join("\n") : undefined,
523
550
  },
524
551
  };
525
552
  },
@@ -585,16 +612,28 @@ export function registerCoreGroups(): void {
585
612
  const global = skills.filter((s) => s.source === "global");
586
613
  const project = skills.filter((s) => s.source === "project");
587
614
 
588
- // Build skill list as comma-separated values
615
+ // Build skill list as comma-separated values with wrapping
589
616
  const skillNames = skills.map((s) => `${s.name} (${s.source})`);
590
- const skillListStr = skillNames.join(", ");
617
+ // Split into chunks of ~60 chars for wrapping
618
+ const chunks: string[] = [];
619
+ let current = "";
620
+ for (const name of skillNames) {
621
+ if (current && (current.length + name.length + 2) > 60) {
622
+ chunks.push(current);
623
+ current = name;
624
+ } else {
625
+ current = current ? `${current}, ${name}` : name;
626
+ }
627
+ }
628
+ if (current) chunks.push(current);
591
629
 
592
630
  return {
593
631
  count: { value: String(skills.length) },
594
632
  global: { value: String(global.length) },
595
633
  project: { value: String(project.length) },
596
634
  list: {
597
- value: skillListStr.length > 0 ? skillListStr : "none",
635
+ value: chunks.length > 0 ? chunks[0] : "none",
636
+ detail: chunks.length > 1 ? chunks.slice(1).join("\n") : undefined,
598
637
  },
599
638
  };
600
639
  },
package/index.ts CHANGED
@@ -12,10 +12,10 @@
12
12
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
13
13
  import { UNIPI_EVENTS, MODULES, UNIPI_PREFIX, emitEvent, getPackageVersion } from "@pi-unipi/core";
14
14
  import { infoRegistry } from "./registry.js";
15
- import { registerCoreGroups, trackModule, trackTool, setPiApi, registerSkillDir, startLoadTracking, recordLoadTime, finishLoadTracking } from "./core-groups.js";
15
+ import { registerCoreGroups, trackModule, trackTool, setPiApi, registerSkillDir, startLoadTracking, recordLoadTime, finishLoadTracking, recordModuleStart } from "./core-groups.js";
16
16
 
17
17
  /** Re-export infoRegistry, registerSkillDir, and load tracking for external use */
18
- export { infoRegistry, registerSkillDir, startLoadTracking, recordLoadTime, finishLoadTracking };
18
+ export { infoRegistry, registerSkillDir, startLoadTracking, recordLoadTime, finishLoadTracking, recordModuleStart };
19
19
  import { getInfoSettings } from "./config.js";
20
20
  import { InfoOverlay } from "./tui/info-overlay.js";
21
21
  import { SettingsOverlay } from "./settings/settings-tui.js";
@@ -62,7 +62,7 @@ export default function (pi: ExtensionAPI) {
62
62
  if (event.name && event.name !== MODULES.INFO_SCREEN) {
63
63
  // Track the module
64
64
  trackModule(event.name, event.version || "unknown");
65
- recordLoadTime(event.name, "module", event.loadTimeMs || 0);
65
+ recordLoadTime(event.name, "module", event.loadTimeMs);
66
66
 
67
67
  // Track tools from this module
68
68
  if (event.tools && Array.isArray(event.tools)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,7 @@ export class SettingsOverlay implements Component {
35
35
  private settings: InfoScreenSettings;
36
36
  private groups: Array<{ id: string; name: string; icon: string }>;
37
37
  private selectedIndex = 0;
38
+ private savedGroupIndex = 0; // Saved position before entering stats mode
38
39
  private mode: "groups" | "stats" = "groups";
39
40
  private selectedGroupId: string | null = null;
40
41
  /** Callback when overlay should close */
@@ -90,9 +91,9 @@ export class SettingsOverlay implements Component {
90
91
  this.selectedIndex = (this.selectedIndex + 1) % this.groups.length;
91
92
  break;
92
93
  case " ": // Space - toggle visibility
93
- case "\r": // Enter - toggle visibility
94
94
  this.toggleGroupVisibility(this.groups[this.selectedIndex].id);
95
95
  break;
96
+ case "\r": // Enter - enter stats mode
96
97
  case "\x1b[C": // Right - enter stats mode
97
98
  case "l":
98
99
  this.enterStatsMode(this.groups[this.selectedIndex].id);
@@ -129,14 +130,12 @@ export class SettingsOverlay implements Component {
129
130
  this.selectedIndex = (this.selectedIndex + 1) % group.config.stats.length;
130
131
  break;
131
132
  case " ": // Space - toggle stat
132
- case "\r": // Enter - toggle stat
133
133
  this.toggleStatVisibility(this.selectedGroupId, group.config.stats[this.selectedIndex].id);
134
134
  break;
135
135
  case "\x1b[D": // Left - back to groups
136
136
  case "h":
137
- this.mode = "groups";
138
- this.selectedGroupId = null;
139
- this.selectedIndex = this.groups.findIndex((g) => g.id === this.selectedGroupId) ?? 0;
137
+ case "\r": // Enter - also go back
138
+ this.backToGroups();
140
139
  break;
141
140
  case "q": // Quit from stats mode
142
141
  case "\x1b":
@@ -145,6 +144,15 @@ export class SettingsOverlay implements Component {
145
144
  }
146
145
  }
147
146
 
147
+ /**
148
+ * Return to groups mode, restoring cursor position.
149
+ */
150
+ private backToGroups(): void {
151
+ this.mode = "groups";
152
+ this.selectedIndex = this.savedGroupIndex; // Restore saved position
153
+ this.selectedGroupId = null;
154
+ }
155
+
148
156
  /**
149
157
  * Toggle group visibility.
150
158
  */
@@ -176,6 +184,7 @@ export class SettingsOverlay implements Component {
176
184
  * Enter stats editing mode for a group.
177
185
  */
178
186
  private enterStatsMode(groupId: string): void {
187
+ this.savedGroupIndex = this.selectedIndex; // Save position for later
179
188
  this.mode = "stats";
180
189
  this.selectedGroupId = groupId;
181
190
  this.selectedIndex = 0;
@@ -228,20 +237,28 @@ export class SettingsOverlay implements Component {
228
237
  }
229
238
  }
230
239
 
240
+ /**
241
+ * Pad a line to fill a target visual width.
242
+ */
243
+ private padToWidth(line: string, targetWidth: number): string {
244
+ const visLen = visibleWidth(line);
245
+ const pad = Math.max(0, targetWidth - visLen);
246
+ return line + " ".repeat(pad);
247
+ }
248
+
231
249
  /**
232
250
  * Render groups mode.
233
251
  */
234
252
  private renderGroupsMode(width: number): string[] {
235
253
  const lines: string[] = [];
254
+ const innerWidth = width - 2; // Subtract border chars
236
255
 
237
- // Header
238
- lines.push("");
239
- lines.push(this.renderCentered(`${ansi.bold}⚙️ Info Screen Settings${ansi.reset}`, width));
240
- lines.push("");
256
+ // Top border
257
+ lines.push(`${ansi.dim}╭${"".repeat(innerWidth)}╮${ansi.reset}`);
241
258
 
242
- // Separator
243
- lines.push(ansi.dim + "─".repeat(width) + ansi.reset);
244
- lines.push("");
259
+ // Header
260
+ lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${ansi.bold}⚙️ Info Screen Settings${ansi.reset}`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
261
+ lines.push(`${ansi.dim}├${"".repeat(innerWidth)}┤${ansi.reset}`);
245
262
 
246
263
  // Group list
247
264
  for (let i = 0; i < this.groups.length; i++) {
@@ -259,18 +276,17 @@ export class SettingsOverlay implements Component {
259
276
  line += ` ${ansi.dim}→ stats${ansi.reset}`;
260
277
  }
261
278
 
262
- if (visibleWidth(line) > width - 2) {
263
- line = truncateToWidth(line, width - 2);
279
+ if (visibleWidth(line) > innerWidth - 2) {
280
+ line = truncateToWidth(line, innerWidth - 2);
264
281
  }
265
282
 
266
- lines.push(line);
283
+ lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth)}${ansi.dim}│${ansi.reset}`);
267
284
  }
268
285
 
269
286
  // Footer
270
- lines.push("");
271
- lines.push(ansi.dim + "─".repeat(width) + ansi.reset);
272
- lines.push(this.renderCentered(`${ansi.dim}↑↓ select Space toggle → stats J/K reorder q close${ansi.reset}`, width));
273
- lines.push("");
287
+ lines.push(`${ansi.dim}├${"".repeat(innerWidth)}┤${ansi.reset}`);
288
+ lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${ansi.dim}↑↓ select Space toggle Enter/→ stats J/K reorder q close${ansi.reset}`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
289
+ lines.push(`${ansi.dim}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
274
290
 
275
291
  return lines;
276
292
  }
@@ -288,15 +304,14 @@ export class SettingsOverlay implements Component {
288
304
  }
289
305
 
290
306
  const groupSettings = getGroupSettings(group.id);
307
+ const innerWidth = width - 2;
291
308
 
292
- // Header
293
- lines.push("");
294
- lines.push(this.renderCentered(`${group.icon} ${group.name} Stats`, width));
295
- lines.push("");
309
+ // Top border
310
+ lines.push(`${ansi.dim}╭${"".repeat(innerWidth)}╮${ansi.reset}`);
296
311
 
297
- // Separator
298
- lines.push(ansi.dim + "─".repeat(width) + ansi.reset);
299
- lines.push("");
312
+ // Header
313
+ lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${group.icon} ${group.name} Stats`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
314
+ lines.push(`${ansi.dim}├${"".repeat(innerWidth)}┤${ansi.reset}`);
300
315
 
301
316
  // Stats list
302
317
  for (let i = 0; i < group.config.stats.length; i++) {
@@ -309,18 +324,17 @@ export class SettingsOverlay implements Component {
309
324
 
310
325
  let line = ` ${indicator} ${toggle} ${stat.label}`;
311
326
 
312
- if (visibleWidth(line) > width - 2) {
313
- line = truncateToWidth(line, width - 2);
327
+ if (visibleWidth(line) > innerWidth - 2) {
328
+ line = truncateToWidth(line, innerWidth - 2);
314
329
  }
315
330
 
316
- lines.push(line);
331
+ lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth)}${ansi.dim}│${ansi.reset}`);
317
332
  }
318
333
 
319
334
  // Footer
320
- lines.push("");
321
- lines.push(ansi.dim + "─".repeat(width) + ansi.reset);
322
- lines.push(this.renderCentered(`${ansi.dim}↑↓ select Space toggle ← back q close${ansi.reset}`, width));
323
- lines.push("");
335
+ lines.push(`${ansi.dim}├${"".repeat(innerWidth)}┤${ansi.reset}`);
336
+ lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${ansi.dim}↑↓ select Space toggle ←/Enter back q close${ansi.reset}`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
337
+ lines.push(`${ansi.dim}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
324
338
 
325
339
  return lines;
326
340
  }
@@ -153,12 +153,12 @@ export class InfoOverlay implements Component {
153
153
  return this.renderError(width);
154
154
  }
155
155
 
156
- // Check for new groups (but don't re-trigger loading)
156
+ // Check for new groups and re-fetch data
157
157
  const allGroups = infoRegistry.getAllGroups();
158
158
  const groupIds = allGroups.map(g => g.id).join(",");
159
159
  const currentIds = this.groups.map(g => g.id).join(",");
160
160
 
161
- if (groupIds !== currentIds) {
161
+ if (groupIds !== currentIds || this.groups.length !== allGroups.length) {
162
162
  this.groups = allGroups;
163
163
  // Apply saved order
164
164
  const settings = getInfoSettings();
@@ -170,10 +170,11 @@ export class InfoOverlay implements Component {
170
170
  return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
171
171
  });
172
172
  }
173
- // Load data for any new groups (non-blocking)
174
- this.loadDataForNewGroups(allGroups);
175
173
  }
176
174
 
175
+ // Always re-fetch data for all groups to catch late updates
176
+ this.refreshAllData();
177
+
177
178
  if (this.groups.length === 0) {
178
179
  return this.renderEmpty(width);
179
180
  }
@@ -197,6 +198,22 @@ export class InfoOverlay implements Component {
197
198
  }
198
199
  }
199
200
 
201
+ /**
202
+ * Refresh data for all groups (non-blocking).
203
+ */
204
+ private refreshAllData(): void {
205
+ for (const group of this.groups) {
206
+ // Invalidate cache to get fresh data
207
+ infoRegistry.invalidateCache(group.id);
208
+ // Fetch fresh data (non-blocking)
209
+ infoRegistry.getGroupData(group.id).then(data => {
210
+ this.groupData.set(group.id, data);
211
+ }).catch(() => {
212
+ // Ignore errors
213
+ });
214
+ }
215
+ }
216
+
200
217
  /**
201
218
  * Render loading state.
202
219
  */