pi-jar 0.3.0 → 0.3.1

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.
@@ -168,10 +168,13 @@ export default function piJar(pi: ExtensionAPI): void {
168
168
  if (open.length) todosAcknowledged = false;
169
169
  const visible = enabled && items.length > 0 && (open.length > 0 || !todosAcknowledged);
170
170
  try {
171
- // Rows are rebuilt only when the list changes (every change reinstalls the widget), the width or the theme.
172
171
  ctx.ui.setWidget("pi-jar.todos", !visible ? undefined : (tui, theme) =>
173
- taskWidget(tui, theme, () => todos?.all() ?? [], taskWidgetState));
174
- } catch { /* Optional widget; task data remains available via /jar tasks and jar_todo. */ }
172
+ taskWidget(tui, theme, () => todos?.all() ?? [], taskWidgetState, {
173
+ render: (width) => composer.enabled ? composer.renderMascot(width) : undefined,
174
+ poke: () => composer.pokeMascot()
175
+ }));
176
+ composer.setTasksVisible(visible);
177
+ } catch { composer.setTasksVisible(false); /* Optional widget; task data remains available via /jar tasks and jar_todo. */ }
175
178
  };
176
179
  registerTaskTool(pi, () => todos, (ctx) => updateTaskWidget(ctx));
177
180
  let changes: ChangeTracker | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jar",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A warm, role-aware Pi terminal workspace with switchable profiles, animated UI, plan and goal workflows, tasks, change review, model roles, background shells and subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/composer.ts CHANGED
@@ -318,6 +318,7 @@ export class ComposerStyle {
318
318
  private phase: WorkingPhase = "idle";
319
319
  private animations = true;
320
320
  private mascotOn = true;
321
+ private tasksVisible = false;
321
322
  private session = "";
322
323
  private profile = "Default";
323
324
  private cwd = process.cwd();
@@ -338,16 +339,7 @@ export class ComposerStyle {
338
339
  const mood = this.animations ? this.mascot.mood() : "idle";
339
340
  return paintFace(mascotFace(mood), mood, this.colors ? (color, text) => this.colors!.fg(color, text) : undefined);
340
341
  },
341
- perch: (width) => {
342
- if (!this.mascotOn || width < 40) return undefined;
343
- const mood = this.animations ? this.mascot.mood() : "idle";
344
- const frame = this.animations ? Math.floor(Date.now() / COMPOSER_ACTIVE_INTERVAL_MS) : 0;
345
- if (this.fullSprite(width)) {
346
- return mascotPerch(mood, frame, this.colors ? (color, text) => this.colors!.fg(color, text) : undefined)
347
- .map(row => " " + row);
348
- }
349
- return [" " + paintTip(mascotTip(mood, frame), mood, this.colors ? (color, text) => this.colors!.fg(color, text) : undefined)];
350
- },
342
+ perch: (width) => this.tasksVisible ? undefined : this.renderMascot(width),
351
343
  session: () => this.session,
352
344
  profile: () => this.profile,
353
345
  ghost: () => this.suggestions?.text,
@@ -374,6 +366,20 @@ export class ComposerStyle {
374
366
  }
375
367
  };
376
368
 
369
+ /** Tasks share the mascot's perch instead of reserving a separate vertical widget. */
370
+ setTasksVisible(visible: boolean): void { this.tasksVisible = visible; }
371
+ pokeMascot(): void { this.decor.poke(); }
372
+ renderMascot(width: number): string[] | undefined {
373
+ if (!this.mascotOn || width < 40) return undefined;
374
+ const mood = this.animations ? this.mascot.mood() : "idle";
375
+ const frame = this.animations ? Math.floor(Date.now() / COMPOSER_ACTIVE_INTERVAL_MS) : 0;
376
+ if (this.fullSprite(width)) {
377
+ return mascotPerch(mood, frame, this.colors ? (color, text) => this.colors!.fg(color, text) : undefined)
378
+ .map(row => " " + row);
379
+ }
380
+ return [" " + paintTip(mascotTip(mood, frame), mood, this.colors ? (color, text) => this.colors!.fg(color, text) : undefined)];
381
+ }
382
+
377
383
  /** Keep short/narrow terminals usable; the pet and mood spark reserve six rows. */
378
384
  private fullSprite(width: number): boolean {
379
385
  return width >= 60 && (this.tui?.terminal?.rows ?? process.stdout.rows ?? 24) >= 24;
@@ -1,12 +1,16 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { Key, matchesKey, truncateToWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
2
+ import { Key, matchesKey, truncateToWidth, visibleWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
3
3
  import { todoProgress, todoTotals, type Todo } from "./tasks.ts";
4
4
  import { todoRow } from "./task-tool.ts";
5
5
 
6
6
  /** Retained across widget refreshes. Compact by default so tasks never bury the composer. */
7
7
  export interface TaskWidgetState { expanded: boolean; scroll: number | undefined }
8
- export function taskWidget(tui: TUI, theme: Theme, items: () => Todo[], state: TaskWidgetState) {
8
+ export function taskWidget(tui: TUI, theme: Theme, items: () => Todo[], state: TaskWidgetState,
9
+ mascot?: { render(width: number): string[] | undefined; poke(): void }) {
9
10
  let page = 4;
11
+ let taskX = 0;
12
+ let taskY = 0;
13
+ let mascotRows: string[] = [];
10
14
  const toggle = () => { state.expanded = !state.expanded; tui.requestRender(); };
11
15
  const move = (delta: number) => {
12
16
  state.scroll = Math.max(0, Math.min(Math.max(0, items().length - page), (state.scroll ?? 0) + delta));
@@ -22,10 +26,21 @@ export function taskWidget(tui: TUI, theme: Theme, items: () => Todo[], state: T
22
26
  else if (matchesKey(data, Key.pageUp)) move(-page);
23
27
  },
24
28
  handleMouse(event: TuiMouseEvent) {
29
+ if (event.y < mascotRows.length && event.x >= 3 && event.x < visibleWidth(mascotRows[event.y] ?? "")) {
30
+ if (event.type === "click" && event.button === "left") { mascot?.poke(); return { handled: true }; }
31
+ return;
32
+ }
33
+ if (event.x < taskX || event.y < taskY) return;
34
+ event = { ...event, x: event.x - taskX, y: event.y - taskY };
25
35
  if (event.type === "click" && event.button === "left" && event.y === 0) { toggle(); return { handled: true }; }
26
36
  if (event.type === "wheel" && event.wheelDelta && state.expanded) { move(Math.sign(event.wheelDelta) * 3); return { handled: true }; }
27
37
  },
28
38
  render(width: number) {
39
+ mascotRows = mascot?.render(width) ?? [];
40
+ const mascotWidth = Math.max(0, ...mascotRows.map(visibleWidth));
41
+ taskX = mascotRows.length && width >= 60 && width - mascotWidth - 2 >= 24 ? mascotWidth + 2 : 0;
42
+ taskY = taskX ? 0 : mascotRows.length;
43
+ const taskWidth = Math.max(0, width - taskX);
29
44
  const all = items();
30
45
  const { done, total } = todoTotals(all);
31
46
  const parents = new Set(all.flatMap((item) => item.parentId ? [item.parentId] : []));
@@ -45,7 +60,12 @@ export function taskWidget(tui: TUI, theme: Theme, items: () => Todo[], state: T
45
60
  rows.push(...all.slice(start, start + page).map((item) => todoRow(item, (color, text) => theme.fg(color, text), (text) => theme.bold(text), parents.has(item.id) ? todoProgress(all, item.id) : undefined)));
46
61
  }
47
62
  rows.push(" ");
48
- return rows.slice(0, Math.max(1, budget)).map((line) => truncateToWidth(line, Math.max(0, width)));
63
+ const tasks = rows.slice(0, Math.max(1, budget)).map((line) => truncateToWidth(line, taskWidth));
64
+ if (!taskX) return [...mascotRows, ...tasks];
65
+ return Array.from({ length: Math.max(mascotRows.length, tasks.length) }, (_, index) => {
66
+ const pet = mascotRows[index] ?? "";
67
+ return pet + " ".repeat(taskX - visibleWidth(pet)) + (tasks[index] ?? "");
68
+ });
49
69
  }
50
70
  };
51
71
  }