@getpaseo/server 0.1.97-beta.3 → 0.1.98

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.
Files changed (96) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +11 -3
  2. package/dist/server/server/agent/agent-manager.js +95 -23
  3. package/dist/server/server/agent/agent-prompt.d.ts +1 -1
  4. package/dist/server/server/agent/agent-prompt.js +3 -10
  5. package/dist/server/server/agent/agent-response-loop.js +9 -3
  6. package/dist/server/server/agent/agent-sdk-types.d.ts +9 -3
  7. package/dist/server/server/agent/agent-storage.d.ts +20 -240
  8. package/dist/server/server/agent/agent-storage.js +6 -6
  9. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  10. package/dist/server/server/agent/create-agent/create.js +8 -7
  11. package/dist/server/server/agent/lifecycle-command.d.ts +15 -1
  12. package/dist/server/server/agent/lifecycle-command.js +9 -2
  13. package/dist/server/server/agent/mcp-server.js +263 -119
  14. package/dist/server/server/agent/mcp-shared.d.ts +35 -179
  15. package/dist/server/server/agent/provider-notices.d.ts +3 -0
  16. package/dist/server/server/agent/provider-notices.js +5 -0
  17. package/dist/server/server/agent/provider-registry.d.ts +2 -0
  18. package/dist/server/server/agent/provider-registry.js +10 -3
  19. package/dist/server/server/agent/provider-snapshot-manager.d.ts +3 -0
  20. package/dist/server/server/agent/provider-snapshot-manager.js +11 -2
  21. package/dist/server/server/agent/providers/claude/agent.js +257 -143
  22. package/dist/server/server/agent/providers/claude/models.js +7 -3
  23. package/dist/server/server/agent/providers/claude/project-dir.js +9 -6
  24. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -22
  25. package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +8 -118
  26. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +4 -3
  27. package/dist/server/server/agent/providers/codex-app-server-agent.js +43 -1
  28. package/dist/server/server/agent/providers/copilot-acp-agent.js +4 -1
  29. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +9 -0
  30. package/dist/server/server/agent/providers/diagnostic-utils.js +188 -0
  31. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +1 -5
  32. package/dist/server/server/agent/providers/mock-slow-provider.js +1 -1
  33. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +29 -2
  34. package/dist/server/server/agent/providers/opencode/server-manager.js +83 -17
  35. package/dist/server/server/agent/providers/opencode-agent.d.ts +2 -0
  36. package/dist/server/server/agent/providers/opencode-agent.js +14 -9
  37. package/dist/server/server/agent/providers/pi/agent.d.ts +1 -5
  38. package/dist/server/server/agent/providers/pi/agent.js +27 -14
  39. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +391 -1261
  40. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +26 -16
  41. package/dist/server/server/bootstrap.d.ts +2 -0
  42. package/dist/server/server/bootstrap.js +32 -2
  43. package/dist/server/server/loop-service.d.ts +60 -359
  44. package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
  45. package/dist/server/server/managed-processes/managed-processes.js +326 -0
  46. package/dist/server/server/migrations/backfill-workspace-id.migration.js +10 -6
  47. package/dist/server/server/package-version.d.ts +1 -7
  48. package/dist/server/server/paseo-worktree-service.js +15 -1
  49. package/dist/server/server/persisted-config.d.ts +138 -1009
  50. package/dist/server/server/persisted-config.js +1 -1
  51. package/dist/server/server/pid-lock.d.ts +1 -15
  52. package/dist/server/server/resolve-worktree-creation-intent.d.ts +3 -0
  53. package/dist/server/server/resolve-worktree-creation-intent.js +3 -3
  54. package/dist/server/server/session.d.ts +18 -1
  55. package/dist/server/server/session.js +424 -64
  56. package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +2 -2
  57. package/dist/server/server/speech/providers/openai/runtime.js +3 -4
  58. package/dist/server/server/speech/speech-types.d.ts +9 -11
  59. package/dist/server/server/websocket-server.d.ts +1 -0
  60. package/dist/server/server/websocket-server.js +15 -0
  61. package/dist/server/server/workspace-archive-service.js +2 -3
  62. package/dist/server/server/workspace-directory.js +5 -5
  63. package/dist/server/server/workspace-reconciliation-service.js +2 -2
  64. package/dist/server/server/workspace-registry.d.ts +17 -48
  65. package/dist/server/server/workspace-registry.js +9 -0
  66. package/dist/server/server/worktree-core.d.ts +1 -0
  67. package/dist/server/server/worktree-core.js +5 -1
  68. package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
  69. package/dist/server/services/quota-fetcher/manifest.js +47 -0
  70. package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
  71. package/dist/server/services/quota-fetcher/provider.js +2 -0
  72. package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
  73. package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
  74. package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
  75. package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
  76. package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
  77. package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
  78. package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
  79. package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
  80. package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
  81. package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
  82. package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
  83. package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
  84. package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
  85. package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
  86. package/dist/server/services/quota-fetcher/service.d.ts +28 -0
  87. package/dist/server/services/quota-fetcher/service.js +58 -0
  88. package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
  89. package/dist/server/services/quota-fetcher/usage.js +49 -0
  90. package/dist/server/terminal/terminal-session-controller.d.ts +8 -0
  91. package/dist/server/terminal/terminal-session-controller.js +23 -3
  92. package/dist/server/utils/checkout-git.js +36 -76
  93. package/dist/server/utils/directory-suggestions.js +98 -2
  94. package/dist/server/utils/worktree-metadata.d.ts +7 -59
  95. package/dist/src/server/persisted-config.js +1 -1
  96. package/package.json +9 -9
@@ -0,0 +1,17 @@
1
+ import type { Logger } from "pino";
2
+ import type { ProviderUsage } from "../../../server/messages.js";
3
+ import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
4
+ interface ZaiQuotaProviderOptions {
5
+ logger: Logger;
6
+ fetch?: ProviderApiFetch;
7
+ }
8
+ export declare class ZaiQuotaProvider implements ProviderUsageFetcher {
9
+ readonly providerId = "zai";
10
+ readonly displayName = "Z.ai";
11
+ private readonly logger;
12
+ private readonly fetchApi;
13
+ constructor(options: ZaiQuotaProviderOptions);
14
+ fetchUsage(): Promise<ProviderUsage>;
15
+ }
16
+ export {};
17
+ //# sourceMappingURL=zai.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { ApiOptionalStringSchema, fetchProviderApi, unavailableUsage } from "../usage.js";
3
+ const ZaiUsageResponseSchema = z.object({
4
+ data: z
5
+ .array(z.object({
6
+ productName: ApiOptionalStringSchema,
7
+ status: ApiOptionalStringSchema,
8
+ purchaseTime: ApiOptionalStringSchema,
9
+ valid: ApiOptionalStringSchema,
10
+ }))
11
+ .optional(),
12
+ });
13
+ export class ZaiQuotaProvider {
14
+ constructor(options) {
15
+ this.providerId = "zai";
16
+ this.displayName = "Z.ai";
17
+ this.logger = options.logger;
18
+ this.fetchApi = options.fetch ?? fetch;
19
+ }
20
+ async fetchUsage() {
21
+ const token = process.env["ZAI_API_KEY"] || process.env["GLM_API_KEY"];
22
+ if (!token)
23
+ return unavailableUsage(this);
24
+ const res = await fetchProviderApi(this.fetchApi, "https://api.z.ai/api/biz/subscription/list", {
25
+ headers: {
26
+ Authorization: `Bearer ${token}`,
27
+ Accept: "application/json",
28
+ },
29
+ });
30
+ if (!res.ok) {
31
+ this.logger.debug({ status: res.status }, "Z.ai usage fetch failed");
32
+ return unavailableUsage(this);
33
+ }
34
+ const resp = ZaiUsageResponseSchema.parse(await res.json());
35
+ const sub = resp.data?.[0];
36
+ if (!sub)
37
+ return unavailableUsage(this);
38
+ const details = [];
39
+ if (sub.status)
40
+ details.push({ id: "status", label: "Status", value: sub.status });
41
+ if (sub.valid)
42
+ details.push({ id: "valid", label: "Valid", value: sub.valid });
43
+ if (sub.purchaseTime) {
44
+ details.push({ id: "purchase_time", label: "Purchased", value: sub.purchaseTime });
45
+ }
46
+ return {
47
+ providerId: this.providerId,
48
+ displayName: this.displayName,
49
+ status: "available",
50
+ planLabel: sub.productName || null,
51
+ windows: [],
52
+ balances: [],
53
+ details,
54
+ error: null,
55
+ };
56
+ }
57
+ }
58
+ //# sourceMappingURL=zai.js.map
@@ -0,0 +1,28 @@
1
+ import type { Logger } from "pino";
2
+ import type { ProviderUsage } from "../../server/messages.js";
3
+ import type { ProviderApiFetch, ProviderUsageFetcher } from "./provider.js";
4
+ export interface ProviderUsageServiceOptions {
5
+ logger: Logger;
6
+ fetchers?: ProviderUsageFetcher[];
7
+ fetch?: ProviderApiFetch;
8
+ cacheTtlMs?: number;
9
+ now?: () => number;
10
+ }
11
+ export interface ProviderUsageListResult {
12
+ fetchedAt: string;
13
+ providers: ProviderUsage[];
14
+ }
15
+ export declare class ProviderUsageService {
16
+ private readonly logger;
17
+ private readonly fetchers;
18
+ private readonly cacheTtlMs;
19
+ private readonly now;
20
+ private cached;
21
+ private inFlight;
22
+ constructor(options: ProviderUsageServiceOptions);
23
+ listUsage(options?: {
24
+ forceRefresh?: boolean;
25
+ }): Promise<ProviderUsageListResult>;
26
+ private fetchFreshUsage;
27
+ }
28
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { createProviderUsageFetchers } from "./manifest.js";
2
+ import { unavailableUsage } from "./usage.js";
3
+ const DEFAULT_PROVIDER_USAGE_CACHE_TTL_MS = 5 * 60 * 1000;
4
+ export class ProviderUsageService {
5
+ constructor(options) {
6
+ this.cached = null;
7
+ this.inFlight = null;
8
+ this.logger = options.logger.child({ module: "provider-usage-service" });
9
+ this.fetchers =
10
+ options.fetchers ??
11
+ createProviderUsageFetchers({
12
+ logger: this.logger,
13
+ fetch: options.fetch,
14
+ });
15
+ this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_PROVIDER_USAGE_CACHE_TTL_MS;
16
+ this.now = options.now ?? Date.now;
17
+ }
18
+ async listUsage(options) {
19
+ const nowMs = this.now();
20
+ if (!options?.forceRefresh &&
21
+ this.cached &&
22
+ nowMs - this.cached.fetchedAtMs < this.cacheTtlMs) {
23
+ return this.cached.result;
24
+ }
25
+ if (this.inFlight) {
26
+ return this.inFlight;
27
+ }
28
+ const request = this.fetchFreshUsage(nowMs);
29
+ this.inFlight = request;
30
+ try {
31
+ return await request;
32
+ }
33
+ finally {
34
+ if (this.inFlight === request) {
35
+ this.inFlight = null;
36
+ }
37
+ }
38
+ }
39
+ async fetchFreshUsage(nowMs) {
40
+ const settled = await Promise.allSettled(this.fetchers.map((fetcher) => fetcher.fetchUsage()));
41
+ const providers = settled.map((result, index) => {
42
+ const fetcher = this.fetchers[index];
43
+ if (result.status === "fulfilled") {
44
+ return result.value;
45
+ }
46
+ this.logger.debug({ err: result.reason, providerId: fetcher.providerId }, "Provider usage fetch failed");
47
+ return unavailableUsage({
48
+ providerId: fetcher.providerId,
49
+ displayName: fetcher.displayName,
50
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
51
+ });
52
+ });
53
+ const result = { fetchedAt: new Date(nowMs).toISOString(), providers };
54
+ this.cached = { fetchedAtMs: nowMs, result };
55
+ return result;
56
+ }
57
+ }
58
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import type { ProviderUsage, ProviderUsageBalance, ProviderUsageWindow } from "../../server/messages.js";
3
+ import type { ProviderApiFetch } from "./provider.js";
4
+ export declare const ApiNumberSchema: z.ZodCoercedNumber<unknown>;
5
+ export declare const ApiNullableNumberSchema: z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>;
6
+ export declare const ApiOptionalStringSchema: z.ZodPreprocess<z.ZodOptional<z.ZodCoercedString<unknown>>>;
7
+ export declare function fetchProviderApi(fetchApi: ProviderApiFetch, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
8
+ export declare function unavailableUsage(provider: {
9
+ providerId: string;
10
+ displayName: string;
11
+ error?: string | null;
12
+ }): ProviderUsage;
13
+ export declare function windowFromUsedPct(input: {
14
+ id: string;
15
+ label: string;
16
+ utilizationPct: number | null | undefined;
17
+ resetsAt?: string | null;
18
+ tone?: ProviderUsageWindow["tone"];
19
+ }): ProviderUsageWindow;
20
+ export declare function balanceToneFromRemaining(remaining: number | null | undefined): ProviderUsageBalance["tone"];
21
+ export declare function toIsoStringOrNull(timestampMs: number): string | null;
22
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ const PROVIDER_HTTP_TIMEOUT_MS = 15000;
3
+ export const ApiNumberSchema = z.coerce.number().finite();
4
+ export const ApiNullableNumberSchema = z.preprocess((value) => (value == null ? null : value), ApiNumberSchema.nullable());
5
+ export const ApiOptionalStringSchema = z.preprocess((value) => (value == null ? undefined : value), z.coerce.string().optional());
6
+ export function fetchProviderApi(fetchApi, input, init = {}) {
7
+ return fetchApi(input, {
8
+ ...init,
9
+ signal: init.signal ?? AbortSignal.timeout(PROVIDER_HTTP_TIMEOUT_MS),
10
+ });
11
+ }
12
+ export function unavailableUsage(provider) {
13
+ return {
14
+ providerId: provider.providerId,
15
+ displayName: provider.displayName,
16
+ status: provider.error ? "error" : "unavailable",
17
+ planLabel: null,
18
+ windows: [],
19
+ balances: [],
20
+ details: [],
21
+ error: provider.error ?? null,
22
+ };
23
+ }
24
+ export function windowFromUsedPct(input) {
25
+ const usedPct = typeof input.utilizationPct === "number" ? input.utilizationPct : null;
26
+ const window = {
27
+ id: input.id,
28
+ label: input.label,
29
+ usedPct,
30
+ remainingPct: usedPct === null ? null : Math.max(0, 100 - usedPct),
31
+ resetsAt: input.resetsAt ?? null,
32
+ };
33
+ if (input.tone) {
34
+ window.tone = input.tone;
35
+ }
36
+ return window;
37
+ }
38
+ export function balanceToneFromRemaining(remaining) {
39
+ if (typeof remaining !== "number")
40
+ return "default";
41
+ if (remaining <= 0)
42
+ return "danger";
43
+ return "ok";
44
+ }
45
+ export function toIsoStringOrNull(timestampMs) {
46
+ const date = new Date(timestampMs);
47
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
48
+ }
49
+ //# sourceMappingURL=usage.js.map
@@ -9,10 +9,15 @@ export interface TerminalSessionControllerOptions {
9
9
  hasBinaryChannel: () => boolean;
10
10
  isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
11
11
  sessionLogger: pino.Logger;
12
+ listTerminalWorkspaceRefs?: () => Promise<readonly TerminalWorkspaceRef[]>;
12
13
  listTerminalWorkspaceRoots?: () => Promise<readonly string[]>;
13
14
  clientSupportsWrapReflow?: () => boolean;
14
15
  getClientBufferedAmount?: () => number | null;
15
16
  }
17
+ interface TerminalWorkspaceRef {
18
+ workspaceId: string;
19
+ cwd: string;
20
+ }
16
21
  export interface TerminalSessionControllerMetrics {
17
22
  directorySubscriptionCount: number;
18
23
  streamSubscriptionCount: number;
@@ -24,6 +29,7 @@ export declare class TerminalSessionController {
24
29
  private readonly hasBinaryChannel;
25
30
  private readonly isPathWithinRoot;
26
31
  private readonly sessionLogger;
32
+ private readonly listTerminalWorkspaceRefs;
27
33
  private readonly listTerminalWorkspaceRoots;
28
34
  private readonly clientSupportsWrapReflow;
29
35
  private readonly getClientBufferedAmount;
@@ -59,6 +65,7 @@ export declare class TerminalSessionController {
59
65
  private resolveTerminalOwnerRoot;
60
66
  private isSamePath;
61
67
  private handleCreateTerminalRequest;
68
+ private resolveLegacyTerminalWorkspaceId;
62
69
  private handleRenameTerminalRequest;
63
70
  private handleSubscribeTerminalRequest;
64
71
  private handleUnsubscribeTerminalRequest;
@@ -74,4 +81,5 @@ export declare class TerminalSessionController {
74
81
  private allocateSlot;
75
82
  private detachStream;
76
83
  }
84
+ export {};
77
85
  //# sourceMappingURL=terminal-session-controller.d.ts.map
@@ -34,7 +34,10 @@ export class TerminalSessionController {
34
34
  this.hasBinaryChannel = options.hasBinaryChannel;
35
35
  this.isPathWithinRoot = options.isPathWithinRoot;
36
36
  this.sessionLogger = options.sessionLogger;
37
- this.listTerminalWorkspaceRoots = options.listTerminalWorkspaceRoots ?? (async () => []);
37
+ this.listTerminalWorkspaceRefs = options.listTerminalWorkspaceRefs ?? (async () => []);
38
+ this.listTerminalWorkspaceRoots =
39
+ options.listTerminalWorkspaceRoots ??
40
+ (async () => (await this.listTerminalWorkspaceRefs()).map((workspace) => workspace.cwd));
38
41
  this.clientSupportsWrapReflow = options.clientSupportsWrapReflow ?? (() => false);
39
42
  this.getClientBufferedAmount = options.getClientBufferedAmount ?? (() => 0);
40
43
  }
@@ -331,7 +334,8 @@ export class TerminalSessionController {
331
334
  });
332
335
  return;
333
336
  }
334
- if (!msg.workspaceId) {
337
+ const workspaceId = msg.workspaceId ?? (await this.resolveLegacyTerminalWorkspaceId(msg.cwd));
338
+ if (!workspaceId) {
335
339
  this.emit({
336
340
  type: "create_terminal_response",
337
341
  payload: {
@@ -344,7 +348,7 @@ export class TerminalSessionController {
344
348
  }
345
349
  const session = await this.terminalManager.createTerminal({
346
350
  cwd: msg.cwd,
347
- workspaceId: msg.workspaceId,
351
+ workspaceId,
348
352
  name: msg.name,
349
353
  command: msg.command,
350
354
  args: msg.args,
@@ -378,6 +382,22 @@ export class TerminalSessionController {
378
382
  });
379
383
  }
380
384
  }
385
+ async resolveLegacyTerminalWorkspaceId(cwd) {
386
+ const workspaceRefs = await this.listTerminalWorkspaceRefs();
387
+ if (workspaceRefs.length === 0) {
388
+ return null;
389
+ }
390
+ const exactMatch = workspaceRefs.find((workspace) => this.isSamePath(workspace.cwd, cwd));
391
+ if (exactMatch) {
392
+ return exactMatch.workspaceId;
393
+ }
394
+ const ownerRoot = this.resolveTerminalOwnerRoot(cwd, workspaceRefs.map((workspace) => workspace.cwd));
395
+ if (!ownerRoot) {
396
+ return null;
397
+ }
398
+ return (workspaceRefs.find((workspace) => this.isSamePath(workspace.cwd, ownerRoot))?.workspaceId ??
399
+ null);
400
+ }
381
401
  async handleRenameTerminalRequest(msg) {
382
402
  const respond = (success, error) => {
383
403
  this.emit({
@@ -375,7 +375,6 @@ function buildGitDiffArgs(args) {
375
375
  return ["diff", ...(args.ignoreWhitespace ? ["-w"] : []), ...args.extra];
376
376
  }
377
377
  const TRACKED_DIFF_NUMSTAT_MAX_BYTES = 2 * 1024 * 1024; // 2MB
378
- const TRACKED_DIFF_PER_FILE_MAX_CHARS = 1024 * 1024;
379
378
  const EMPTY_TREE_OBJECT_ID = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
380
379
  function isUnbornHeadDiffError(error) {
381
380
  return (error instanceof Error &&
@@ -423,56 +422,20 @@ async function getTrackedNumstatByPath(cwd, refs, ignoreWhitespace = false) {
423
422
  }
424
423
  return stats;
425
424
  }
426
- function extractTrackedDiffMetadataPath(section, prefix) {
427
- const line = section.split("\n").find((candidate) => candidate.startsWith(prefix));
428
- if (!line) {
429
- return null;
430
- }
431
- const path = line.slice(prefix.length).replace(/\t.*$/, "").trimEnd();
432
- if (path === "/dev/null") {
433
- return null;
434
- }
435
- return path.startsWith("a/") || path.startsWith("b/") ? path.slice(2) : path;
436
- }
437
- function extractTrackedDiffSectionPath(section) {
438
- const firstLineEnd = section.indexOf("\n");
439
- const firstLine = firstLineEnd === -1 ? section : section.slice(0, firstLineEnd);
440
- const header = firstLine.startsWith("diff --git ") ? firstLine.slice("diff --git ".length) : "";
441
- const prefixedPathMatch = header.match(/^a\/(.+) b\/(.+)$/);
442
- if (prefixedPathMatch) {
443
- return prefixedPathMatch[2] ?? null;
444
- }
445
- const metadataPath = extractTrackedDiffMetadataPath(section, "+++ ") ??
446
- extractTrackedDiffMetadataPath(section, "--- ");
447
- if (metadataPath) {
448
- return metadataPath;
449
- }
450
- const pathMatch = header.match(/^(\S+)\s+(\S+)$/);
451
- return pathMatch?.[2] ?? null;
452
- }
453
- function splitTrackedDiffSections(diffText) {
454
- const starts = [];
455
- const diffHeaderPattern = /^diff --git /gm;
456
- let match;
457
- while ((match = diffHeaderPattern.exec(diffText))) {
458
- starts.push(match.index);
459
- }
460
- const sections = [];
461
- for (let index = 0; index < starts.length; index += 1) {
462
- const start = starts[index];
463
- const end = starts[index + 1] ?? diffText.length;
464
- const text = diffText.slice(start, end);
465
- const path = extractTrackedDiffSectionPath(text);
466
- if (!path) {
467
- continue;
468
- }
469
- sections.push({
470
- path,
471
- text,
472
- isTooLarge: text.length > TRACKED_DIFF_PER_FILE_MAX_CHARS,
473
- });
474
- }
475
- return sections;
425
+ async function getTrackedDiffTextForPath(input) {
426
+ const result = await runGitCommand(buildGitDiffArgs({
427
+ ignoreWhitespace: input.ignoreWhitespace,
428
+ extra: [...getCheckoutDiffRefArgs(input.refsForDiff), "--", input.path],
429
+ }), {
430
+ cwd: input.cwd,
431
+ envOverlay: READ_ONLY_GIT_ENV,
432
+ maxOutputBytes: PER_FILE_DIFF_MAX_BYTES,
433
+ });
434
+ return {
435
+ path: input.path,
436
+ text: result.stdout,
437
+ truncated: result.truncated,
438
+ };
476
439
  }
477
440
  export class NotGitRepoError extends Error {
478
441
  constructor(cwd) {
@@ -1441,7 +1404,7 @@ export function warmCheckoutShortstatInBackground(cwd, context, onComplete) {
1441
1404
  });
1442
1405
  }
1443
1406
  async function appendStructuredTrackedDiffs(input) {
1444
- const { cwd, trackedChanges, trackedChangeByPath, trackedNumstatByPath, trackedPlaceholderByPath, trackedDiffText, trackedDiffTruncated, refsForDiff, ignoreWhitespace, structured, appendTrackedPlaceholderComment, } = input;
1407
+ const { cwd, trackedChanges, trackedChangeByPath, trackedNumstatByPath, trackedPlaceholderByPath, trackedDiffText, refsForDiff, ignoreWhitespace, structured, appendTrackedPlaceholderComment, } = input;
1445
1408
  const parsedTrackedFiles = trackedDiffText.length > 0
1446
1409
  ? await parseAndHighlightDiff(trackedDiffText, cwd, {
1447
1410
  getOldFileContent: async (file) => {
@@ -1487,7 +1450,6 @@ async function appendStructuredTrackedDiffs(input) {
1487
1450
  // whitespace-filtered patch and numstat are both empty. Skip emitting a
1488
1451
  // structured placeholder in that case so whitespace-only edits truly disappear.
1489
1452
  if (ignoreWhitespace &&
1490
- !trackedDiffTruncated &&
1491
1453
  change.status.startsWith("M") &&
1492
1454
  (!stat || (!stat.isBinary && stat.additions === 0 && stat.deletions === 0))) {
1493
1455
  continue;
@@ -1499,7 +1461,7 @@ async function appendStructuredTrackedDiffs(input) {
1499
1461
  additions: stat?.additions ?? 0,
1500
1462
  deletions: stat?.deletions ?? 0,
1501
1463
  hunks: [],
1502
- status: trackedDiffTruncated ? "too_large" : "ok",
1464
+ status: "ok",
1503
1465
  });
1504
1466
  }
1505
1467
  }
@@ -1564,43 +1526,42 @@ async function processTrackedChanges(input) {
1564
1526
  trackedDiffPaths.push(change.path);
1565
1527
  }
1566
1528
  let trackedDiffText = "";
1567
- let trackedDiffTruncated = false;
1529
+ let trackedDiffBytes = 0;
1568
1530
  if (trackedDiffPaths.length > 0) {
1569
- const trackedDiffResult = await runGitCommand(buildGitDiffArgs({
1570
- ignoreWhitespace,
1571
- extra: [...getCheckoutDiffRefArgs(refsForDiff), "--", ...trackedDiffPaths],
1572
- }), {
1531
+ const trackedDiffs = await Promise.all(trackedDiffPaths.map((path) => getTrackedDiffTextForPath({
1573
1532
  cwd,
1574
- envOverlay: READ_ONLY_GIT_ENV,
1575
- maxOutputBytes: TOTAL_DIFF_MAX_BYTES,
1576
- });
1577
- trackedDiffTruncated = trackedDiffResult.truncated;
1533
+ refsForDiff,
1534
+ path,
1535
+ ignoreWhitespace,
1536
+ })));
1578
1537
  const visibleTrackedDiffs = [];
1579
- const sections = splitTrackedDiffSections(trackedDiffResult.stdout);
1580
- for (let index = 0; index < sections.length; index += 1) {
1581
- const section = sections[index];
1582
- const isTruncatedTail = trackedDiffTruncated && index === sections.length - 1;
1583
- if (section.isTooLarge || isTruncatedTail) {
1584
- trackedPlaceholderByPath.set(section.path, {
1538
+ for (const fileDiff of trackedDiffs) {
1539
+ if (fileDiff.truncated) {
1540
+ trackedPlaceholderByPath.set(fileDiff.path, {
1585
1541
  status: "too_large",
1586
- stat: trackedNumstatByPath.get(section.path) ?? null,
1542
+ stat: trackedNumstatByPath.get(fileDiff.path) ?? null,
1587
1543
  });
1588
1544
  continue;
1589
1545
  }
1590
- visibleTrackedDiffs.push(section.text);
1546
+ const diffBytes = Buffer.byteLength(fileDiff.text, "utf8");
1547
+ if (trackedDiffBytes + diffBytes > TOTAL_DIFF_MAX_BYTES) {
1548
+ trackedPlaceholderByPath.set(fileDiff.path, {
1549
+ status: "too_large",
1550
+ stat: trackedNumstatByPath.get(fileDiff.path) ?? null,
1551
+ });
1552
+ continue;
1553
+ }
1554
+ trackedDiffBytes += diffBytes;
1555
+ visibleTrackedDiffs.push(fileDiff.text);
1591
1556
  }
1592
1557
  trackedDiffText = visibleTrackedDiffs.join("");
1593
1558
  appendDiff(trackedDiffText);
1594
- if (trackedDiffTruncated) {
1595
- appendDiff("# tracked diff truncated\n");
1596
- }
1597
1559
  }
1598
1560
  return {
1599
1561
  trackedChangeByPath,
1600
1562
  trackedNumstatByPath,
1601
1563
  trackedPlaceholderByPath,
1602
1564
  trackedDiffText,
1603
- trackedDiffTruncated,
1604
1565
  };
1605
1566
  }
1606
1567
  async function resolveCheckoutDiffRefs(cwd, compare, context) {
@@ -1690,7 +1651,6 @@ export async function getCheckoutDiff(cwd, compare, context) {
1690
1651
  trackedNumstatByPath: trackedDiff.trackedNumstatByPath,
1691
1652
  trackedPlaceholderByPath: trackedDiff.trackedPlaceholderByPath,
1692
1653
  trackedDiffText: trackedDiff.trackedDiffText,
1693
- trackedDiffTruncated: trackedDiff.trackedDiffTruncated,
1694
1654
  refsForDiff: effectiveRefsForDiff,
1695
1655
  ignoreWhitespace,
1696
1656
  structured,
@@ -24,6 +24,15 @@ const IGNORED_SUGGESTION_DIRECTORY_NAMES = new Set([
24
24
  "coverage",
25
25
  "vendor",
26
26
  "__pycache__",
27
+ ".git",
28
+ ]);
29
+ const TRAVERSABLE_HIDDEN_WORKSPACE_DIRECTORY_NAMES = new Set([
30
+ ".agents",
31
+ ".claude",
32
+ ".codex",
33
+ ".github",
34
+ ".paseo",
35
+ ".vscode",
27
36
  ]);
28
37
  export async function searchHomeDirectories(options) {
29
38
  const query = options.query.trim();
@@ -71,6 +80,17 @@ export async function searchWorkspaceEntries(options) {
71
80
  return [];
72
81
  }
73
82
  const matchMode = options.matchMode ?? "fuzzy";
83
+ const exactEntry = queryParts.isPathQuery && matchMode === "suffix"
84
+ ? await resolveWorkspaceExactEntry({
85
+ workspaceRoot,
86
+ query: options.query,
87
+ includeDirectories,
88
+ includeFiles,
89
+ })
90
+ : null;
91
+ if (exactEntry && limit <= 1) {
92
+ return [exactEntry];
93
+ }
74
94
  if (queryParts.isPathQuery && matchMode !== "suffix") {
75
95
  return searchWorkspaceWithinParentDirectory({
76
96
  workspaceRoot,
@@ -84,7 +104,7 @@ export async function searchWorkspaceEntries(options) {
84
104
  const searchTerm = matchMode === "suffix"
85
105
  ? [queryParts.parentPart, queryParts.searchTerm].filter(Boolean).join("/")
86
106
  : queryParts.searchTerm;
87
- return searchWorkspaceAcrossTree({
107
+ const entries = await searchWorkspaceAcrossTree({
88
108
  workspaceRoot,
89
109
  searchTerm,
90
110
  limit,
@@ -94,6 +114,53 @@ export async function searchWorkspaceEntries(options) {
94
114
  maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
95
115
  maxEntriesScanned: options.maxEntriesScanned ?? DEFAULT_MAX_DIRECTORIES_SCANNED,
96
116
  });
117
+ return exactEntry ? prependWorkspaceEntry(exactEntry, entries).slice(0, limit) : entries;
118
+ }
119
+ async function resolveWorkspaceExactEntry(input) {
120
+ const normalized = input.query
121
+ .trim()
122
+ .replace(/\\/g, "/")
123
+ .replace(/^\.\/+/, "")
124
+ .replace(/\/{2,}/g, "/");
125
+ if (!normalized) {
126
+ return null;
127
+ }
128
+ const candidatePath = path.isAbsolute(normalized)
129
+ ? path.resolve(normalized)
130
+ : path.resolve(input.workspaceRoot, normalized);
131
+ let resolvedPath;
132
+ try {
133
+ resolvedPath = await realpath(candidatePath);
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ if (!isPathInsideRoot(input.workspaceRoot, resolvedPath)) {
139
+ return null;
140
+ }
141
+ const stats = await stat(resolvedPath).catch(() => null);
142
+ if (!stats) {
143
+ return null;
144
+ }
145
+ if (stats.isFile() && input.includeFiles) {
146
+ return {
147
+ path: normalizeRelativePath(input.workspaceRoot, resolvedPath),
148
+ kind: "file",
149
+ };
150
+ }
151
+ if (stats.isDirectory() && input.includeDirectories) {
152
+ return {
153
+ path: normalizeRelativePath(input.workspaceRoot, resolvedPath),
154
+ kind: "directory",
155
+ };
156
+ }
157
+ return null;
158
+ }
159
+ function prependWorkspaceEntry(entry, entries) {
160
+ return [
161
+ entry,
162
+ ...entries.filter((candidate) => candidate.kind !== entry.kind || candidate.path !== entry.path),
163
+ ];
97
164
  }
98
165
  function normalizeLimit(limit) {
99
166
  const candidate = limit ?? DEFAULT_LIMIT;
@@ -185,6 +252,9 @@ async function searchWorkspaceWithinParentDirectory(input) {
185
252
  if (entry.kind === "file" && !input.includeFiles) {
186
253
  continue;
187
254
  }
255
+ if (isHiddenWorkspaceSuggestion(entry)) {
256
+ continue;
257
+ }
188
258
  const rankedEntry = rankWorkspaceEntry({
189
259
  absolutePath: entry.absolutePath,
190
260
  kind: entry.kind,
@@ -230,6 +300,10 @@ async function searchWorkspaceAcrossTree(input) {
230
300
  if (entry.kind === "directory" && !input.includeDirectories) {
231
301
  continue;
232
302
  }
303
+ // Hidden directories are traversed, but not offered as suggestions.
304
+ if (isHiddenWorkspaceSuggestion(entry)) {
305
+ continue;
306
+ }
233
307
  if (entry.kind === "file" && !input.includeFiles) {
234
308
  continue;
235
309
  }
@@ -618,7 +692,23 @@ async function listWorkspaceChildEntries(input) {
618
692
  return cached.entries;
619
693
  }
620
694
  const dirents = await readdir(input.directory, { withFileTypes: true }).catch(() => []);
621
- const candidates = dirents.filter((dirent) => !isHiddenDirectoryName(dirent.name) && !isIgnoredSuggestionDirectoryName(dirent.name));
695
+ const candidates = dirents.filter((dirent) => {
696
+ if (isIgnoredSuggestionDirectoryName(dirent.name)) {
697
+ return false;
698
+ }
699
+ if (isHiddenDirectoryName(dirent.name) &&
700
+ !dirent.isFile() &&
701
+ !isTraversableHiddenWorkspaceDirectoryName(dirent.name)) {
702
+ return false;
703
+ }
704
+ // Allowlisted hidden directories remain traversable so file links like
705
+ // `.claude/settings.local.json` can resolve, but hidden files (e.g.
706
+ // `.DS_Store`) should never be suggested.
707
+ if (dirent.isFile() && isHiddenDirectoryName(dirent.name)) {
708
+ return false;
709
+ }
710
+ return true;
711
+ });
622
712
  const resolved = await Promise.all(candidates.map(async (dirent) => {
623
713
  const candidatePath = path.join(input.directory, dirent.name);
624
714
  const entry = await resolveWorkspaceCandidate({
@@ -687,9 +777,15 @@ async function resolveWorkspaceCandidate(input) {
687
777
  function isHiddenDirectoryName(name) {
688
778
  return name.startsWith(".");
689
779
  }
780
+ function isHiddenWorkspaceSuggestion(entry) {
781
+ return isHiddenDirectoryName(entry.name);
782
+ }
690
783
  function isIgnoredSuggestionDirectoryName(name) {
691
784
  return IGNORED_SUGGESTION_DIRECTORY_NAMES.has(name);
692
785
  }
786
+ function isTraversableHiddenWorkspaceDirectoryName(name) {
787
+ return TRAVERSABLE_HIDDEN_WORKSPACE_DIRECTORY_NAMES.has(name);
788
+ }
693
789
  function setDirectoryListCache(cacheKey, entry) {
694
790
  directoryListCache.set(cacheKey, entry);
695
791
  pruneDirectoryListCache();