pi-message-sidebar 1.2.0 → 1.3.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.
@@ -125,8 +125,8 @@ export default function messageSidebar(pi: ExtensionAPI): void {
125
125
 
126
126
  ctx.ui.onTerminalInput((data) => {
127
127
  // Slash commands such as /goal mutate session entries without firing
128
- // turn events; a coalesced refresh on any keystroke keeps the dock live.
129
- scheduleRefresh();
128
+ // turn events. Refresh on submit, not every keystroke.
129
+ if (matchesKey(data, "return") || matchesKey(data, "enter")) scheduleRefresh();
130
130
  if (tui && !isSidebarVisible(tui.terminal.columns) && sidebar?.isFocused()) {
131
131
  sidebar.setFocused(false);
132
132
  tui.requestRender();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-message-sidebar",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Persistent responsive message history sidebar for Pi",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/goal.ts CHANGED
@@ -15,22 +15,26 @@ type BranchEntry = { type?: string; customType?: string; data?: unknown };
15
15
 
16
16
  const GOAL_ENTRY_TYPE = "pi-codex-goal";
17
17
 
18
- function isThreadGoal(value: unknown): value is ThreadGoal {
19
- const goal = value as ThreadGoal | null;
18
+ function isThreadGoal(value: unknown): value is RawGoal {
19
+ const goal = value as RawGoal | null;
20
+ if (!goal || typeof goal !== "object") return false;
21
+ const usage = goal.usage as { tokensUsed?: unknown; activeSeconds?: unknown } | undefined;
20
22
  return (
21
- !!goal &&
22
- typeof goal === "object" &&
23
23
  typeof goal.goalId === "string" &&
24
24
  typeof goal.objective === "string" &&
25
25
  (goal.status === "active" || goal.status === "paused" || goal.status === "budgetLimited" || goal.status === "complete") &&
26
26
  (goal.tokenBudget === null || typeof goal.tokenBudget === "number") &&
27
- typeof goal.tokensUsed === "number" &&
28
- typeof goal.activeSeconds === "number" &&
29
27
  typeof goal.createdAt === "number" &&
30
- typeof goal.updatedAt === "number"
28
+ typeof goal.updatedAt === "number" &&
29
+ typeof usage?.tokensUsed === "number" &&
30
+ typeof usage.activeSeconds === "number"
31
31
  );
32
32
  }
33
33
 
34
+ type RawGoal = Omit<ThreadGoal, "tokensUsed" | "activeSeconds"> & {
35
+ usage: { tokensUsed: number; activeSeconds: number };
36
+ };
37
+
34
38
  /**
35
39
  * Reconstructs the current thread goal from pi-codex-goal session entries.
36
40
  * Mirrors reconstructGoal() from pi-codex-goal dist/state.js (schema version 1):
@@ -46,31 +50,51 @@ export function readThreadGoal(entries: Iterable<BranchEntry>): ThreadGoal | nul
46
50
  if (data.kind === "clear") {
47
51
  goal = null;
48
52
  } else if (data.kind === "set" && isThreadGoal(data.goal)) {
49
- goal = { ...(data.goal as ThreadGoal) };
53
+ const raw = data.goal as RawGoal;
54
+ goal = {
55
+ goalId: raw.goalId,
56
+ objective: raw.objective,
57
+ status: raw.status,
58
+ tokenBudget: raw.tokenBudget,
59
+ tokensUsed: raw.usage.tokensUsed,
60
+ activeSeconds: raw.usage.activeSeconds,
61
+ createdAt: raw.createdAt,
62
+ updatedAt: raw.updatedAt,
63
+ };
50
64
  } else if (data.kind === "usage" && goal) {
51
65
  const usage = data.usage as { tokensUsed?: number; activeSeconds?: number } | undefined;
52
66
  const updatedAt = data.updatedAt;
67
+ const status = data.status;
53
68
  if (data.goalId !== goal.goalId) continue;
69
+ if (status !== "active" && status !== "budgetLimited") continue;
54
70
  if (goal.status !== "active" && goal.status !== "budgetLimited") continue;
55
- if (goal.status === "budgetLimited" && data.status === "active") continue;
71
+ if (goal.status === "budgetLimited" && status === "active") continue;
56
72
  if (typeof updatedAt !== "number" || typeof usage?.tokensUsed !== "number" || typeof usage.activeSeconds !== "number") continue;
57
73
  if (updatedAt < goal.updatedAt || usage.tokensUsed < goal.tokensUsed || usage.activeSeconds < goal.activeSeconds) continue;
58
- goal = { ...goal, status: data.status as ThreadGoal["status"], tokensUsed: usage.tokensUsed, activeSeconds: usage.activeSeconds, updatedAt } as ThreadGoal;
74
+ const current: ThreadGoal = goal;
75
+ goal = {
76
+ ...current,
77
+ status,
78
+ tokensUsed: usage.tokensUsed,
79
+ activeSeconds: usage.activeSeconds,
80
+ updatedAt,
81
+ };
59
82
  }
60
83
  }
61
84
  return goal;
62
85
  }
63
86
 
64
- type GoalCache = { key: string; goal: ThreadGoal | null };
65
- let cache: GoalCache | null = null;
87
+ type GoalCache = { branch: object; key: string; goal: ThreadGoal | null };
88
+ const cache = new WeakMap<ExtensionContext, GoalCache>();
66
89
 
67
90
  /** Cached readThreadGoal over the current session branch. */
68
91
  export function readSessionGoal(ctx: ExtensionContext): ThreadGoal | null {
69
92
  const branch = ctx.sessionManager.getBranch();
70
93
  const last = branch.at(-1);
71
94
  const key = `${branch.length}:${last?.id ?? ""}:${last?.timestamp ?? ""}`;
72
- if (cache && cache.key === key) return cache.goal;
95
+ const previous = cache.get(ctx);
96
+ if (previous?.branch === branch && previous.key === key) return previous.goal;
73
97
  const goal = readThreadGoal(branch);
74
- cache = { key, goal };
98
+ cache.set(ctx, { branch, key, goal });
75
99
  return goal;
76
100
  }