@oh-my-pi/pi-coding-agent 15.5.11 → 15.5.13

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.
@@ -139,6 +139,70 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
139
139
  return [];
140
140
  }
141
141
 
142
+ /**
143
+ * Pick the actionable window of tasks to display in the sticky todo panel.
144
+ *
145
+ * Returns up to `maxVisible` open (pending / in_progress) tasks in their
146
+ * original phase order, plus the count of remaining open tasks not shown so
147
+ * the caller can render a `+N more` hint. When every task in `tasks` is
148
+ * closed (completed or abandoned), returns the trailing `maxVisible` tasks
149
+ * with `hiddenOpenCount = 0`, so the panel keeps useful context until the
150
+ * active-phase pointer advances on the next `todo_write`.
151
+ *
152
+ * Task identity and order are preserved — this is a slice, never a sort.
153
+ */
154
+ export function selectStickyTodoWindow(
155
+ tasks: TodoItem[],
156
+ maxVisible = 5,
157
+ ): { visible: TodoItem[]; hiddenOpenCount: number } {
158
+ const openTasks = tasks.filter(t => t.status === "pending" || t.status === "in_progress");
159
+ if (openTasks.length > 0) {
160
+ const visible = openTasks.slice(0, maxVisible);
161
+ return { visible, hiddenOpenCount: openTasks.length - visible.length };
162
+ }
163
+ const start = Math.max(0, tasks.length - maxVisible);
164
+ return { visible: tasks.slice(start), hiddenOpenCount: 0 };
165
+ }
166
+
167
+ /** Minimum overlap (after normalization) required for a substring match.
168
+ * Picked at six chars to admit single-word identifiers like "review" /
169
+ * "Sonnet" without admitting tiny common substrings like "test" / "fix"
170
+ * that would collide across unrelated todos. */
171
+ const TODO_DESCRIPTION_MIN_OVERLAP = 6;
172
+
173
+ function normalizeForTodoMatch(value: string): string {
174
+ return value
175
+ .toLowerCase()
176
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
177
+ .trim();
178
+ }
179
+
180
+ /**
181
+ * Report whether `content` likely names the same work as any entry in
182
+ * `descriptions`. Used by the sticky todo panel to light up a pending todo
183
+ * when an in-flight subagent is doing the work for it, without requiring
184
+ * the caller to flip the todo's status.
185
+ *
186
+ * Matching is normalize-then-equal first (lowercased; punctuation and
187
+ * whitespace runs both collapsed to a single space; trimmed), with a
188
+ * substring fallback in either direction so minor wording drift
189
+ * ("Sonnet #2: bug scan" vs "Sonnet #2") still links up. The substring
190
+ * fallback requires at least {@link TODO_DESCRIPTION_MIN_OVERLAP} chars on
191
+ * the contained side.
192
+ */
193
+ export function todoMatchesAnyDescription(content: string, descriptions: readonly string[]): boolean {
194
+ const target = normalizeForTodoMatch(content);
195
+ if (!target) return false;
196
+ for (const desc of descriptions) {
197
+ const candidate = normalizeForTodoMatch(desc);
198
+ if (!candidate) continue;
199
+ if (target === candidate) return true;
200
+ if (target.length >= TODO_DESCRIPTION_MIN_OVERLAP && candidate.includes(target)) return true;
201
+ if (candidate.length >= TODO_DESCRIPTION_MIN_OVERLAP && target.includes(candidate)) return true;
202
+ }
203
+ return false;
204
+ }
205
+
142
206
  function resolveTaskOrError(
143
207
  phases: TodoPhase[],
144
208
  content: string | undefined,
@@ -130,9 +130,7 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
130
130
  function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, content: string): string | undefined {
131
131
  if (!resolveFileDisplayMode(session).hashLines) return undefined;
132
132
  const normalized = normalizeToLF(content);
133
- const tag = getFileSnapshotStore(session).recordContiguous(absolutePath, 1, normalized.split("\n"), {
134
- fullText: normalized,
135
- });
133
+ const tag = getFileSnapshotStore(session).record(absolutePath, normalized);
136
134
  return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag);
137
135
  }
138
136
 
@@ -359,9 +359,7 @@ export async function generateFileMentionMessages(
359
359
  const normalized = snapshotStore ? normalizeToLF(content) : content;
360
360
  let { output, lineCount } = buildTextOutput(normalized);
361
361
  if (snapshotStore) {
362
- const tag = snapshotStore.recordContiguous(absolutePath, 1, normalized.split("\n"), {
363
- fullText: normalized,
364
- });
362
+ const tag = snapshotStore.record(absolutePath, normalized);
365
363
  output = `${formatHashlineHeader(resolvedPath, tag)}\n${formatNumberedLines(output)}`;
366
364
  }
367
365
  files.push({ path: resolvedPath, content: output, lineCount });