pi-posthorse 0.10.2 → 0.10.4

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.
@@ -56,7 +56,7 @@ const LABEL_SOURCES: readonly (readonly [label: string, needles: readonly string
56
56
  */
57
57
  export function prefilterNeedles(query: string): string[] | undefined {
58
58
  // A match contains every fragment between characters that JSON escapes or labels may add.
59
- const [fragment] = query
59
+ const [fragment = ""] = query
60
60
  .split(/["\\[\](){}:$\p{Cc}\p{Cs}]/u)
61
61
  .map((part) => part.trim())
62
62
  .sort((a, b) => b.length - a.length);
@@ -143,7 +143,6 @@ function ripgrepFilter(
143
143
  for (const binary of binaries) {
144
144
  try {
145
145
  // Try the host-managed binary first; PATH is a fallback only when it is unavailable.
146
- // oxlint-disable-next-line no-await-in-loop
147
146
  return (await execFileText(binary, [...flags, ...paths], dir, signal)).split("\0");
148
147
  } catch (error) {
149
148
  const code = isRecord(error) ? error.code : undefined;
@@ -162,7 +161,7 @@ function ripgrepFilter(
162
161
  return async (files) => {
163
162
  const paths = files.map((file) => relative(dir, file));
164
163
  const found = new Set((await Promise.all(commandGroups(paths).map(matching))).flat());
165
- return files.filter((_file, index) => found.has(paths[index]));
164
+ return files.filter((file) => found.has(relative(dir, file)));
166
165
  };
167
166
  }
168
167
  function messageFrom(value: unknown): MessageLike | undefined {
@@ -178,13 +177,15 @@ function messageFrom(value: unknown): MessageLike | undefined {
178
177
  toolName: optionalString(value.toolName),
179
178
  namespace: optionalString(value.namespace),
180
179
  toolCallId: optionalString(value.toolCallId),
180
+ details: value.details,
181
+ nestedCalls: value.nestedCalls,
181
182
  isError: value.isError === true,
182
183
  command: optionalString(value.command),
183
184
  output: optionalString(value.output),
184
185
  excludeFromContext: value.excludeFromContext === true,
185
186
  };
186
187
  }
187
- function parsedEntry(line: string): EntryLike | undefined {
188
+ export function parseSessionLine(line: string): EntryLike | undefined {
188
189
  try {
189
190
  const value: unknown = JSON.parse(line);
190
191
  if (!isRecord(value)) {
@@ -203,6 +204,9 @@ function parsedEntry(line: string): EntryLike | undefined {
203
204
  customType: optionalString(value.customType),
204
205
  content: value.content,
205
206
  details: value.details,
207
+ data: value.data,
208
+ cwd: optionalString(value.cwd),
209
+ parentSession: optionalString(value.parentSession),
206
210
  display: value.display === true,
207
211
  handoff: optionalString(value.handoff),
208
212
  firstKeptEntryId: optionalString(value.firstKeptEntryId),
@@ -222,7 +226,7 @@ function entryHead(prefix: string): EntryLike | undefined {
222
226
  return {
223
227
  type: head[1],
224
228
  id: head[2],
225
- parentId: head[3] === "null" ? null : head[3].slice(1, -1),
229
+ parentId: head[3] === "null" ? null : head[3]?.slice(1, -1),
226
230
  };
227
231
  }
228
232
  /** Whether a caller may need a line; other lines only advance window ancestry. */
@@ -235,7 +239,7 @@ async function readEntry(
235
239
  if (head !== undefined && (!line.matches || !wanted(head))) {
236
240
  return { entry: head, selected: false };
237
241
  }
238
- const entry = parsedEntry(await line.text());
242
+ const entry = parseSessionLine(await line.text());
239
243
  return entry === undefined
240
244
  ? undefined
241
245
  : {
@@ -271,7 +275,6 @@ async function sessionFiles(dir: string): Promise<string[]> {
271
275
  let level = existsSync(dir) ? [dir] : [];
272
276
  while (level.length > 0) {
273
277
  // Each level lists the directories the previous level found.
274
- // oxlint-disable-next-line no-await-in-loop
275
278
  const listings = await Promise.all(
276
279
  level.map((path) => readdir(path, { withFileTypes: true })),
277
280
  );
@@ -293,16 +296,24 @@ async function sessionFiles(dir: string): Promise<string[]> {
293
296
  }
294
297
  type SessionFile = { readonly file: string; readonly size: number; readonly mtime: number };
295
298
  async function newestFirst(files: readonly string[]): Promise<SessionFile[]> {
296
- const stats = await Promise.all(files.map((file) => stat(file)));
297
- return files
298
- .map((file, index) => ({ file, size: stats[index].size, mtime: stats[index].mtimeMs }))
299
- .sort((a, b) => b.mtime - a.mtime);
299
+ const stats = await Promise.all(
300
+ files.map(async (file) => {
301
+ const info = await stat(file);
302
+ return { file, size: info.size, mtime: info.mtimeMs };
303
+ }),
304
+ );
305
+ return stats.sort((a, b) => b.mtime - a.mtime);
300
306
  }
301
307
  /** The newest files up to FIRST_SEARCH_BYTES, then the rest. */
302
308
  function searchPhases(files: readonly SessionFile[]): readonly (readonly string[])[] {
303
309
  let count = 0;
304
- for (let bytes = 0; count < files.length && bytes < FIRST_SEARCH_BYTES; count++) {
305
- bytes += files[count].size;
310
+ let bytes = 0;
311
+ for (const file of files) {
312
+ if (bytes >= FIRST_SEARCH_BYTES) {
313
+ break;
314
+ }
315
+ bytes += file.size;
316
+ count++;
306
317
  }
307
318
  const paths = files.map(({ file }) => file);
308
319
  return [paths.slice(0, count), paths.slice(count)];
@@ -310,7 +321,7 @@ function searchPhases(files: readonly SessionFile[]): readonly (readonly string[
310
321
  async function sessionHeader(
311
322
  file: string,
312
323
  signal?: AbortSignal,
313
- ): Promise<{ readonly cwd?: string } | undefined> {
324
+ ): Promise<{ readonly cwd?: string | undefined } | undefined> {
314
325
  for await (const line of lfLines(file, signal)) {
315
326
  try {
316
327
  const value: unknown = JSON.parse(line.toString("utf8"));
@@ -343,11 +354,11 @@ function parentSession(
343
354
  export type SessionScope = {
344
355
  readonly dir: string;
345
356
  readonly cwd: string;
346
- readonly currentFile?: string;
347
- readonly signal?: AbortSignal;
348
- readonly fileKey?: string;
357
+ readonly currentFile?: string | undefined;
358
+ readonly signal?: AbortSignal | undefined;
359
+ readonly fileKey?: string | undefined;
349
360
  /** Raw-text needles; files without any of them are skipped. */
350
- readonly needles?: readonly string[];
361
+ readonly needles?: readonly string[] | undefined;
351
362
  };
352
363
  /** Scope memoization is owned by one enumeration; nested runs inherit their parent's project. */
353
364
  function membership(
@@ -393,10 +404,8 @@ export async function* scopedSessionFiles(scope: SessionScope): AsyncGenerator<s
393
404
  : ripgrepFilter(scope.dir, scope.needles, scope.signal);
394
405
  for (const phase of searchPhases(selected)) {
395
406
  // Older files are searched only when the newest ones did not satisfy the caller.
396
- // oxlint-disable-next-line no-await-in-loop
397
407
  for (const file of prefilter === undefined ? phase : await prefilter(phase)) {
398
408
  // Preserve newest-first traversal while closing each header stream before opening another.
399
- // oxlint-disable-next-line no-await-in-loop
400
409
  if (await belongs(file)) {
401
410
  yield file;
402
411
  }
@@ -18,7 +18,7 @@ type LineRange = {
18
18
  readonly end: number;
19
19
  readonly head: string;
20
20
  readonly matches: boolean;
21
- readonly bytes?: Readonly<Pick<Buffer, "toString">>;
21
+ readonly bytes?: Readonly<Pick<Buffer, "toString">> | undefined;
22
22
  };
23
23
  async function* lineRanges(
24
24
  file: string,
@@ -73,7 +73,6 @@ async function rangeText(
73
73
  while (offset < buffer.length) {
74
74
  signal?.throwIfAborted();
75
75
  // Positioned reads may be short; each continues the same selected record.
76
- // oxlint-disable-next-line no-await-in-loop
77
76
  const { bytesRead } = await handle.read(
78
77
  buffer,
79
78
  offset,
package/src/ui/cards.ts CHANGED
@@ -61,11 +61,12 @@ function callDetails(name: ToolName, args: Readonly<Record<string, unknown>>): s
61
61
  name === "history" && args.op === "search"
62
62
  ? `Scope: ${args.all === true ? "all project sessions" : "current branch"}`
63
63
  : "";
64
- const offsets = [
64
+ const labels: readonly (readonly [string, string])[] = [
65
65
  ["offset", "Offset"],
66
66
  ["imageOffset", "Image offset"],
67
67
  ["limit", "Limit"],
68
- ].map(([key, label]) => {
68
+ ];
69
+ const offsets = labels.map(([key, label]) => {
69
70
  const value = args[key];
70
71
  return value === undefined
71
72
  ? ""
package/src/ui/display.ts CHANGED
@@ -21,7 +21,7 @@ export type PosthorseDisplay =
21
21
  readonly percent: number | null;
22
22
  };
23
23
  readonly rollover?: "enabled" | "disabled" | "unsupported";
24
- readonly rolloverAt?: number;
24
+ readonly rolloverAt?: number | undefined;
25
25
  }
26
26
  | {
27
27
  readonly kind: "notes-list" | "notes-search";
@@ -241,7 +241,7 @@ function historyReadSummary(
241
241
  ) {
242
242
  return pageSummary(display);
243
243
  }
244
- const summary = `${pageSummary(display).split("\n")[0]}\nImages ${n(display.imageOffset ?? 0)}–${n(display.imageEnd)} of ${n(display.imageTotal)}`;
244
+ const summary = `${pageSummary(display).split("\n", 1).join("")}\nImages ${n(display.imageOffset ?? 0)}–${n(display.imageEnd)} of ${n(display.imageTotal)}`;
245
245
  return display.end < display.total || display.imageEnd < display.imageTotal
246
246
  ? `Next offset ${n(display.end)}\nimageOffset ${n(display.imageEnd)}\n${summary}`
247
247
  : summary;
package/src/ui/text.ts CHANGED
@@ -5,7 +5,6 @@ export type OutputColor = "toolOutput" | "muted" | "dim" | "error" | "warning";
5
5
 
6
6
  export function clean(value: string): string {
7
7
  // Tool previews must not execute terminal controls or Unicode interlinear annotations.
8
- // oxlint-disable-next-line no-control-regex
9
8
  return stripTerminalSequences(value).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\ufff9-\ufffb]/g, "");
10
9
  }
11
10