pi-readseek 0.4.27 → 0.4.28
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/package.json +1 -1
- package/src/read.ts +2 -2
- package/src/readseek-client.ts +37 -3
- package/src/readseek-settings.ts +30 -4
- package/src/tui-render-utils.ts +20 -0
package/package.json
CHANGED
package/src/read.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
|
29
29
|
import { readseekRead, readseekDetect, type ReadSeekDetection } from "./readseek-client.js";
|
|
30
30
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
31
31
|
import { resolveReadSeekOcrMode } from "./readseek-settings.js";
|
|
32
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine,
|
|
32
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidthCached } from "./tui-render-utils.js";
|
|
33
33
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
34
34
|
import { filePathParam, mapParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
35
35
|
|
|
@@ -543,7 +543,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
543
543
|
for (const badge of info.badges) summaryParts.push(badge);
|
|
544
544
|
const summary = summaryParts.join(" • ");
|
|
545
545
|
let text = summaryLine(summary, { hidden: !!textContent && !expanded });
|
|
546
|
-
if (expanded && textContent) text += "\n" +
|
|
546
|
+
if (expanded && textContent) text += "\n" + wrapReadHashlinesForWidthCached(content, textContent, width);
|
|
547
547
|
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
548
548
|
},
|
|
549
549
|
});
|
package/src/readseek-client.ts
CHANGED
|
@@ -286,23 +286,36 @@ function directoryExists(dirPath: string): boolean {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
const ownRepositoryByDir = new Map<string, boolean>();
|
|
290
|
+
|
|
289
291
|
function isOwnReadSeekRepository(cwd = process.cwd()): boolean {
|
|
290
|
-
|
|
292
|
+
const start = path.resolve(cwd);
|
|
293
|
+
const cached = ownRepositoryByDir.get(start);
|
|
294
|
+
if (cached !== undefined) return cached;
|
|
295
|
+
|
|
296
|
+
let dir = start;
|
|
297
|
+
let result = false;
|
|
291
298
|
while (true) {
|
|
292
299
|
const packageJsonPath = path.join(dir, "package.json");
|
|
293
300
|
if (existsSync(packageJsonPath)) {
|
|
294
301
|
try {
|
|
295
302
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { name?: unknown };
|
|
296
|
-
if (typeof packageJson.name === "string" && READSEEK_REPO_PACKAGE_NAMES.has(packageJson.name))
|
|
303
|
+
if (typeof packageJson.name === "string" && READSEEK_REPO_PACKAGE_NAMES.has(packageJson.name)) {
|
|
304
|
+
result = true;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
297
307
|
} catch {
|
|
298
308
|
// Ignore unreadable or invalid package manifests while walking up.
|
|
299
309
|
}
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
const parent = path.dirname(dir);
|
|
303
|
-
if (parent === dir)
|
|
313
|
+
if (parent === dir) break;
|
|
304
314
|
dir = parent;
|
|
305
315
|
}
|
|
316
|
+
|
|
317
|
+
ownRepositoryByDir.set(start, result);
|
|
318
|
+
return result;
|
|
306
319
|
}
|
|
307
320
|
|
|
308
321
|
function defaultReadSeekDir(): string | null {
|
|
@@ -310,23 +323,43 @@ function defaultReadSeekDir(): string | null {
|
|
|
310
323
|
return home ? path.join(home, ".pi", "readseek") : null;
|
|
311
324
|
}
|
|
312
325
|
|
|
326
|
+
const DEFAULT_READSEEK_TIMEOUT_MS = 120_000;
|
|
327
|
+
|
|
328
|
+
function readseekTimeoutMs(): number {
|
|
329
|
+
const raw = process.env.READSEEK_TIMEOUT_MS;
|
|
330
|
+
if (raw !== undefined && raw !== "") {
|
|
331
|
+
const parsed = Number(raw);
|
|
332
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
333
|
+
}
|
|
334
|
+
return DEFAULT_READSEEK_TIMEOUT_MS;
|
|
335
|
+
}
|
|
336
|
+
|
|
313
337
|
async function spawnReadSeekRaw(args: string[], options: RunReadSeekOptions = {}): Promise<string> {
|
|
314
338
|
return new Promise<string>((resolve, reject) => {
|
|
315
339
|
let settled = false;
|
|
340
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
316
341
|
const fail = (error: Error): void => {
|
|
317
342
|
if (settled) return;
|
|
318
343
|
settled = true;
|
|
344
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
319
345
|
reject(error);
|
|
320
346
|
};
|
|
321
347
|
const succeed = (value: string): void => {
|
|
322
348
|
if (settled) return;
|
|
323
349
|
settled = true;
|
|
350
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
324
351
|
resolve(value);
|
|
325
352
|
};
|
|
326
353
|
|
|
327
354
|
const stdin = options.stdin;
|
|
328
355
|
const stdio: StdioOptions = [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"];
|
|
329
356
|
const child = spawn(readseekBinaryPath(), args, { stdio, signal: options.signal });
|
|
357
|
+
const timeoutMs = options.timeoutMs ?? readseekTimeoutMs();
|
|
358
|
+
timeout = setTimeout(() => {
|
|
359
|
+
child.kill("SIGKILL");
|
|
360
|
+
fail(new Error(`readseek timed out after ${timeoutMs} ms`));
|
|
361
|
+
}, timeoutMs);
|
|
362
|
+
timeout.unref?.();
|
|
330
363
|
const childStdout = child.stdout;
|
|
331
364
|
const childStderr = child.stderr;
|
|
332
365
|
const childStdin = child.stdin;
|
|
@@ -396,6 +429,7 @@ async function readseekInvocationArgs(args: string[]): Promise<string[]> {
|
|
|
396
429
|
interface RunReadSeekOptions {
|
|
397
430
|
signal?: AbortSignal;
|
|
398
431
|
stdin?: string;
|
|
432
|
+
timeoutMs?: number;
|
|
399
433
|
}
|
|
400
434
|
|
|
401
435
|
async function runReadSeekRaw(args: string[], options: RunReadSeekOptions = {}): Promise<string> {
|
package/src/readseek-settings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, statSync, type Stats } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -89,16 +89,42 @@ function validateSettings(raw: unknown, source: string): ReadSeekSettingsResult
|
|
|
89
89
|
return { settings, warnings };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
interface SettingsFileCacheEntry {
|
|
93
|
+
mtimeMs: number;
|
|
94
|
+
size: number;
|
|
95
|
+
result: ReadSeekSettingsResult;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const settingsFileCache = new Map<string, SettingsFileCacheEntry>();
|
|
99
|
+
|
|
100
|
+
function statFileOrNull(path: string): Stats | null {
|
|
101
|
+
try {
|
|
102
|
+
return statSync(path);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
function readSettingsFile(path: string): ReadSeekSettingsResult {
|
|
93
|
-
|
|
109
|
+
const stats = statFileOrNull(path);
|
|
110
|
+
if (!stats) {
|
|
111
|
+
settingsFileCache.delete(path);
|
|
112
|
+
return { settings: {}, warnings: [] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const cached = settingsFileCache.get(path);
|
|
116
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) return cached.result;
|
|
94
117
|
|
|
118
|
+
let result: ReadSeekSettingsResult;
|
|
95
119
|
try {
|
|
96
120
|
const text = readFileSync(path, "utf8");
|
|
97
|
-
|
|
121
|
+
result = validateSettings(JSON.parse(text) as unknown, path);
|
|
98
122
|
} catch (error) {
|
|
99
123
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
|
|
124
|
+
result = { settings: {}, warnings: [{ source: path, message: `Invalid JSON: ${message}` }] };
|
|
101
125
|
}
|
|
126
|
+
settingsFileCache.set(path, { mtimeMs: stats.mtimeMs, size: stats.size, result });
|
|
127
|
+
return result;
|
|
102
128
|
}
|
|
103
129
|
|
|
104
130
|
function mergeSettings(base: ReadSeekJsonSettings, override: ReadSeekJsonSettings): ReadSeekJsonSettings {
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -149,6 +149,26 @@ export function wrapReadHashlinesForWidth(text: string, width: number | undefine
|
|
|
149
149
|
return output.join("\n");
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
const wrappedHashlinesCache = new WeakMap<object, { text: string; width: number | undefined; wrapped: string }>();
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Memoized {@link wrapReadHashlinesForWidth} for render paths that re-run on
|
|
156
|
+
* every TUI frame: the wrapped output is cached per result content object so
|
|
157
|
+
* an expanded read result is only re-wrapped when its text or the terminal
|
|
158
|
+
* width changes.
|
|
159
|
+
*/
|
|
160
|
+
export function wrapReadHashlinesForWidthCached(
|
|
161
|
+
cacheKey: object,
|
|
162
|
+
text: string,
|
|
163
|
+
width: number | undefined,
|
|
164
|
+
): string {
|
|
165
|
+
const cached = wrappedHashlinesCache.get(cacheKey);
|
|
166
|
+
if (cached && cached.text === text && cached.width === width) return cached.wrapped;
|
|
167
|
+
const wrapped = wrapReadHashlinesForWidth(text, width);
|
|
168
|
+
wrappedHashlinesCache.set(cacheKey, { text, width, wrapped });
|
|
169
|
+
return wrapped;
|
|
170
|
+
}
|
|
171
|
+
|
|
152
172
|
/**
|
|
153
173
|
* Render the `isPartial` placeholder line shared by every tool's `renderResult`.
|
|
154
174
|
*/
|