@danypops/pi-lector 0.13.11 → 0.15.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.
- package/extension/src/apply-patch/rendering.ts +2 -1
- package/extension/src/code-intelligence/rendering.ts +12 -7
- package/extension/src/cross-workspace-search/model-content.ts +69 -0
- package/extension/src/cross-workspace-search/rendering.ts +7 -2
- package/extension/src/external-search/rendering.ts +46 -12
- package/extension/src/find-files/rendering.ts +2 -1
- package/extension/src/find-symbols/rendering.ts +2 -1
- package/extension/src/git/rendering.ts +7 -5
- package/extension/src/index.ts +143 -64
- package/extension/src/line-edit/rendering.ts +2 -1
- package/extension/src/package-source/rendering.ts +4 -3
- package/extension/src/presentation/model-content.ts +62 -0
- package/extension/src/presentation/presentation-contract.ts +148 -0
- package/extension/src/presentation/tool-presentation.ts +171 -0
- package/extension/src/reference-based-rename/rendering.ts +21 -0
- package/extension/src/repo-cache/rendering.ts +4 -3
- package/extension/src/search/rendering.ts +2 -1
- package/extension/src/workspace-cache/operations.ts +24 -3
- package/extension/src/workspace-cache/rendering.ts +23 -5
- package/package.json +2 -2
package/extension/src/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import type {
|
|
5
5
|
CachedRepositoryPage,
|
|
6
6
|
ContentHash,
|
|
7
|
-
ContextBundleResult,
|
|
8
7
|
Diagnostic,
|
|
9
8
|
DocumentSymbolEntry,
|
|
10
9
|
EditOutcome,
|
|
@@ -83,6 +82,7 @@ import {
|
|
|
83
82
|
formatWorkspaceMapCall,
|
|
84
83
|
formatWorkspaceMapResult,
|
|
85
84
|
} from "./code-intelligence/rendering.ts";
|
|
85
|
+
import { formatFindSymbolsAcrossProjectsModelContent, formatSearchTextAcrossProjectsModelContent } from "./cross-workspace-search/model-content.ts";
|
|
86
86
|
import { type CrossWorkspaceOutcome, createLectorCrossWorkspaceSearchOperations } from "./cross-workspace-search/operations.ts";
|
|
87
87
|
import { formatCrossWorkspaceCall, formatFindSymbolsAcrossProjectsResult, formatSearchTextAcrossProjectsResult } from "./cross-workspace-search/rendering.ts";
|
|
88
88
|
import { createLectorEditOperations } from "./edit/operations.ts";
|
|
@@ -121,8 +121,16 @@ import {
|
|
|
121
121
|
PACKAGE_SOURCE_LIST_VISIBLE_ROWS,
|
|
122
122
|
packageSourceListMoreLine,
|
|
123
123
|
} from "./package-source/rendering.ts";
|
|
124
|
+
import { formatSemanticModelContent } from "./presentation/model-content.ts";
|
|
125
|
+
import { withLectorPresentation } from "./presentation/presentation-contract.ts";
|
|
126
|
+
import { presentationTitle } from "./presentation/tool-presentation.ts";
|
|
124
127
|
import { createLectorReadOperations } from "./read/operations.ts";
|
|
125
128
|
import { createReferenceBasedRenameOperations } from "./reference-based-rename/operations.ts";
|
|
129
|
+
import {
|
|
130
|
+
formatReferenceBasedRenameModelContent,
|
|
131
|
+
formatReferenceBasedRenameResult,
|
|
132
|
+
type ReferenceBasedRenameOutcome,
|
|
133
|
+
} from "./reference-based-rename/rendering.ts";
|
|
126
134
|
import { createRenameOperations } from "./rename/operations.ts";
|
|
127
135
|
import { createRepoCacheEvictOperations } from "./repo-cache/evict-operations.ts";
|
|
128
136
|
import { createRepoCacheListOperations } from "./repo-cache/list-operations.ts";
|
|
@@ -152,7 +160,13 @@ import {
|
|
|
152
160
|
monitorWorkspaceCache,
|
|
153
161
|
waitForJobCompletion,
|
|
154
162
|
} from "./workspace-cache/operations.ts";
|
|
155
|
-
import {
|
|
163
|
+
import {
|
|
164
|
+
formatJobSnapshotResult,
|
|
165
|
+
formatWorkspaceCacheCall,
|
|
166
|
+
formatWorkspaceCacheStatusResult,
|
|
167
|
+
formatWorkspaceReleaseModelContent,
|
|
168
|
+
formatWorkspaceReleaseResult,
|
|
169
|
+
} from "./workspace-cache/rendering.ts";
|
|
156
170
|
import { createLectorWriteOperations } from "./write/operations.ts";
|
|
157
171
|
|
|
158
172
|
function describeIntelligenceSource(provenance: IntelligenceProvenance): string {
|
|
@@ -189,7 +203,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
189
203
|
const customToolNames = new Set<string>();
|
|
190
204
|
function registerLectorTool<TParams extends TSchema, TDetails = unknown, TState = unknown>(tool: ToolDefinition<TParams, TDetails, TState>): void {
|
|
191
205
|
customToolNames.add(tool.name);
|
|
192
|
-
pi.registerTool(tool);
|
|
206
|
+
pi.registerTool(withLectorPresentation(tool));
|
|
193
207
|
}
|
|
194
208
|
pi.on("tool_result", (event) => {
|
|
195
209
|
if (!customToolNames.has(event.toolName)) return;
|
|
@@ -471,7 +485,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
471
485
|
return { content: [{ type: "text", text }], details: result };
|
|
472
486
|
},
|
|
473
487
|
renderCall(args, theme) {
|
|
474
|
-
return new Text(
|
|
488
|
+
return new Text(
|
|
489
|
+
`${theme.fg("toolTitle", theme.bold(presentationTitle("localize_context")))} ${theme.fg("accent", typeof args.query === "string" ? `"${args.query}"` : "")}`,
|
|
490
|
+
0,
|
|
491
|
+
0,
|
|
492
|
+
);
|
|
475
493
|
},
|
|
476
494
|
renderResult(result, { isPartial }, theme, context) {
|
|
477
495
|
if (isPartial) return new Text(theme.fg("warning", "Localizing..."), 0, 0);
|
|
@@ -482,8 +500,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
482
500
|
.join("\n");
|
|
483
501
|
return new Text(theme.fg("error", errorText || "localize_context failed"), 0, 0);
|
|
484
502
|
}
|
|
485
|
-
const
|
|
486
|
-
|
|
503
|
+
const contentText = result.content
|
|
504
|
+
.filter((block) => block.type === "text")
|
|
505
|
+
.map((block) => block.text)
|
|
506
|
+
.join("\n");
|
|
507
|
+
return new Text(contentText || "Localization complete", 0, 0);
|
|
487
508
|
},
|
|
488
509
|
});
|
|
489
510
|
|
|
@@ -1181,21 +1202,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1181
1202
|
const fromPath = resolve(cwd, params.fromPath);
|
|
1182
1203
|
const toPath = resolve(cwd, params.toPath);
|
|
1183
1204
|
const outcome = await referenceBasedRenameOperations.rename(fromPath, toPath, params.maxFiles, params.maxSymbolsPerFile);
|
|
1184
|
-
|
|
1185
|
-
`moved to ${outcome.movedTo}`,
|
|
1186
|
-
outcome.filesUpdated.length === 0
|
|
1187
|
-
? "no other files referenced it"
|
|
1188
|
-
: `updated imports in ${outcome.filesUpdated.length} file(s): ${outcome.filesUpdated.join(", ")}`,
|
|
1189
|
-
...outcome.caveats.map((caveat) => `caveat: ${caveat}`),
|
|
1190
|
-
];
|
|
1191
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { outcome } };
|
|
1205
|
+
return { content: [{ type: "text", text: formatReferenceBasedRenameModelContent(outcome) }], details: { outcome } };
|
|
1192
1206
|
},
|
|
1193
1207
|
renderCall(args, theme, context) {
|
|
1194
1208
|
const fromPath = typeof args.fromPath === "string" ? args.fromPath : "";
|
|
1195
1209
|
const toPath = typeof args.toPath === "string" ? args.toPath : "";
|
|
1196
1210
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1197
1211
|
text.setText(
|
|
1198
|
-
`${theme.fg("toolTitle", theme.bold("reference_based_rename"))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
|
|
1212
|
+
`${theme.fg("toolTitle", theme.bold(presentationTitle("reference_based_rename")))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
|
|
1199
1213
|
);
|
|
1200
1214
|
return text;
|
|
1201
1215
|
},
|
|
@@ -1208,13 +1222,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1208
1222
|
.join("\n");
|
|
1209
1223
|
return new Text(theme.fg("error", errorText || "reference_based_rename failed"), 0, 0);
|
|
1210
1224
|
}
|
|
1211
|
-
const details = result.details as { outcome?:
|
|
1225
|
+
const details = result.details as { outcome?: ReferenceBasedRenameOutcome } | undefined;
|
|
1212
1226
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1213
|
-
text.setText(
|
|
1214
|
-
details?.outcome
|
|
1215
|
-
? `${theme.fg("success", "moved")} ${theme.fg("accent", details.outcome.movedTo)} ${theme.fg("dim", `(${details.outcome.filesUpdated.length} import(s) updated)`)}`
|
|
1216
|
-
: theme.fg("success", "rename complete"),
|
|
1217
|
-
);
|
|
1227
|
+
text.setText(details?.outcome ? formatReferenceBasedRenameResult(details.outcome, theme) : theme.fg("success", "rename complete"));
|
|
1218
1228
|
return text;
|
|
1219
1229
|
},
|
|
1220
1230
|
});
|
|
@@ -1260,7 +1270,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1260
1270
|
const action = typeof args.action === "string" ? args.action : "";
|
|
1261
1271
|
const path = typeof args.path === "string" ? args.path : "";
|
|
1262
1272
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1263
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("rename"
|
|
1273
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("rename", action)))} ${theme.fg("accent", path)}`);
|
|
1264
1274
|
return text;
|
|
1265
1275
|
},
|
|
1266
1276
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1441,10 +1451,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1441
1451
|
? ` ${args.rootId}`
|
|
1442
1452
|
: "";
|
|
1443
1453
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1444
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("symbol_annotations"
|
|
1454
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("symbol_annotations", action)))}${theme.fg("accent", id)}`);
|
|
1445
1455
|
return text;
|
|
1446
1456
|
},
|
|
1447
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
1457
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1448
1458
|
if (isPartial) return new Text(theme.fg("warning", "Working on annotation..."), 0, 0);
|
|
1449
1459
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1450
1460
|
if (context.isError) {
|
|
@@ -1457,11 +1467,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1457
1467
|
}
|
|
1458
1468
|
const details = result.details as SymbolAnnotationToolDetails | undefined;
|
|
1459
1469
|
if (details?.annotations) {
|
|
1460
|
-
text.setText(formatAnnotationListSummary(details.annotations, theme));
|
|
1470
|
+
text.setText(expanded ? details.annotations.map(formatAnnotationDetail).join("\n\n") : formatAnnotationListSummary(details.annotations, theme));
|
|
1461
1471
|
return text;
|
|
1462
1472
|
}
|
|
1463
1473
|
if (details?.annotation) {
|
|
1464
|
-
text.setText(formatAnnotationSummary(details.annotation, theme));
|
|
1474
|
+
text.setText(expanded ? formatAnnotationDetail(details.annotation) : formatAnnotationSummary(details.annotation, theme));
|
|
1465
1475
|
return text;
|
|
1466
1476
|
}
|
|
1467
1477
|
if (details?.scrubbed !== undefined) {
|
|
@@ -1595,25 +1605,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
1595
1605
|
});
|
|
1596
1606
|
|
|
1597
1607
|
interface WorkspaceCacheToolDetails {
|
|
1598
|
-
readonly action: "status" | "populate" | "wait" | "job_status";
|
|
1608
|
+
readonly action: "status" | "populate" | "wait" | "job_status" | "release";
|
|
1599
1609
|
readonly status?: WorkspaceCacheStatus;
|
|
1600
1610
|
readonly job?: JobSnapshot<PopulateSymbolGraphResult>;
|
|
1611
|
+
readonly release?: OperationOutputs["workspace.release"];
|
|
1601
1612
|
}
|
|
1602
1613
|
|
|
1603
1614
|
registerLectorTool({
|
|
1604
1615
|
name: "workspace_cache",
|
|
1605
1616
|
label: "Workspace Cache",
|
|
1606
1617
|
description:
|
|
1607
|
-
"Checks or drives population of the workspace's persisted symbol graph -- the store reachable_from, symbol_annotations (anchor resolution), reference_based_rename, and workspace_map all read from, separate from the live language-server index find_symbols/hover/go_to_definition use. action=status reports not-cached/caching/partial/cached for the given bounds, without starting work. action=populate requests a scan and briefly waits for fast completion; a source file changing mid-scan (e.g. a concurrent edit or rename) is retried automatically in the background for up to a minute before surfacing as a real failure, no manual re-run needed. action=wait subscribes to daemon job completion, with bounded status polling only when push delivery is unavailable. action=job_status is a point-in-time diagnostic read.",
|
|
1618
|
+
"Checks or drives population of the workspace's persisted symbol graph -- the store reachable_from, symbol_annotations (anchor resolution), reference_based_rename, and workspace_map all read from, separate from the live language-server index find_symbols/hover/go_to_definition use. action=status reports not-cached/caching/partial/cached for the given bounds, without starting work. action=populate requests a scan and briefly waits for fast completion; a source file changing mid-scan (e.g. a concurrent edit or rename) is retried automatically in the background for up to a minute before surfacing as a real failure, no manual re-run needed. action=wait subscribes to daemon job completion, with bounded status polling only when push delivery is unavailable. action=job_status is a point-in-time diagnostic read. action=release safely closes idle workspace resources and unregisters it so fetched/package source cleanup can proceed.",
|
|
1608
1619
|
promptSnippet: "Check or force-populate the workspace's persisted symbol graph",
|
|
1609
1620
|
promptGuidelines: [
|
|
1610
1621
|
"Use action=populate with a larger maxFiles/maxSymbolsPerFile before relying on reachable_from/symbol_annotations/reference_based_rename against a workspace bigger than the default 500-file auto-scan -- their own errors (empty results, UnknownAnnotationAnchor, ReferenceBasedRenameRequiresFreshGraph) usually mean the graph never reached the files you need, not that population is simply still catching up.",
|
|
1611
1622
|
"When action=populate returns a queued/running job, call action=wait once with that jobId; do not run shell sleep or manually poll job_status.",
|
|
1623
|
+
"Use action=release after finishing with a fetched or package-source workspace so package_source remove/clean and repo_cache evict can reclaim it; active leases, jobs, or watches fail closed.",
|
|
1612
1624
|
],
|
|
1613
1625
|
parameters: Type.Object({
|
|
1614
|
-
action: Type.Union([Type.Literal("status"), Type.Literal("populate"), Type.Literal("wait"), Type.Literal("job_status")]),
|
|
1626
|
+
action: Type.Union([Type.Literal("status"), Type.Literal("populate"), Type.Literal("wait"), Type.Literal("job_status"), Type.Literal("release")]),
|
|
1615
1627
|
directory: Type.Optional(
|
|
1616
|
-
Type.String({ description: "Required for action=status/populate -- absolute or cwd-relative path used to resolve the workspace" }),
|
|
1628
|
+
Type.String({ description: "Required for action=status/populate/release -- absolute or cwd-relative path used to resolve the workspace" }),
|
|
1617
1629
|
),
|
|
1618
1630
|
maxFiles: Type.Optional(
|
|
1619
1631
|
Type.Number({ description: "action=status/populate only -- defaults to 500, the same bound the automatic first-touch scan uses" }),
|
|
@@ -1629,11 +1641,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1629
1641
|
),
|
|
1630
1642
|
jobId: Type.Optional(Type.String({ description: "Required for action=wait/job_status -- a jobId returned by action=populate" })),
|
|
1631
1643
|
}),
|
|
1632
|
-
async execute(
|
|
1644
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<WorkspaceCacheToolDetails>> {
|
|
1633
1645
|
if (params.action === "job_status") {
|
|
1634
1646
|
if (!params.jobId) throw new Error("workspace_cache action=job_status requires jobId");
|
|
1635
1647
|
const job = await cacheOperations.jobStatus(params.jobId);
|
|
1636
|
-
return {
|
|
1648
|
+
return {
|
|
1649
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "job_status"), job) }],
|
|
1650
|
+
details: { action: "job_status", job },
|
|
1651
|
+
};
|
|
1637
1652
|
}
|
|
1638
1653
|
if (params.action === "wait") {
|
|
1639
1654
|
if (!params.jobId) throw new Error("workspace_cache action=wait requires jobId");
|
|
@@ -1655,21 +1670,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
1655
1670
|
);
|
|
1656
1671
|
}
|
|
1657
1672
|
const job = outcome.kind === "terminal" ? outcome.job : await cacheOperations.jobStatus(params.jobId);
|
|
1658
|
-
return {
|
|
1673
|
+
return {
|
|
1674
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "wait"), job) }],
|
|
1675
|
+
details: { action: "wait", job },
|
|
1676
|
+
};
|
|
1659
1677
|
}
|
|
1660
1678
|
if (!params.directory) throw new Error(`workspace_cache action=${params.action} requires directory`);
|
|
1661
1679
|
const directory = resolve(cwd, params.directory);
|
|
1680
|
+
if (params.action === "release") {
|
|
1681
|
+
const release = await cacheOperations.release(directory, {
|
|
1682
|
+
toolName: "workspace_cache",
|
|
1683
|
+
toolCallId,
|
|
1684
|
+
signal,
|
|
1685
|
+
context: ctx,
|
|
1686
|
+
});
|
|
1687
|
+
return {
|
|
1688
|
+
content: [{ type: "text", text: formatWorkspaceReleaseModelContent(release) }],
|
|
1689
|
+
details: { action: "release", release },
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1662
1692
|
const maxFiles = params.maxFiles ?? 500;
|
|
1663
1693
|
const maxSymbolsPerFile = params.maxSymbolsPerFile ?? 100;
|
|
1664
1694
|
if (params.action === "status") {
|
|
1665
1695
|
const status = await cacheOperations.status(directory, maxFiles, maxSymbolsPerFile);
|
|
1666
|
-
return {
|
|
1696
|
+
return {
|
|
1697
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "status"), status) }],
|
|
1698
|
+
details: { action: "status", status },
|
|
1699
|
+
};
|
|
1667
1700
|
}
|
|
1668
1701
|
const job = await cacheOperations.submit(directory, maxFiles, maxSymbolsPerFile, params.waitMs ?? 3_000);
|
|
1669
|
-
return {
|
|
1702
|
+
return {
|
|
1703
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "populate"), job) }],
|
|
1704
|
+
details: { action: "populate", job },
|
|
1705
|
+
};
|
|
1670
1706
|
},
|
|
1671
1707
|
renderCall(args, theme, context) {
|
|
1672
|
-
const action =
|
|
1708
|
+
const action =
|
|
1709
|
+
args.action === "populate" || args.action === "wait" || args.action === "job_status" || args.action === "release" ? args.action : "status";
|
|
1673
1710
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1674
1711
|
text.setText(formatWorkspaceCacheCall(action, args, theme));
|
|
1675
1712
|
return text;
|
|
@@ -1685,7 +1722,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1685
1722
|
}
|
|
1686
1723
|
const details = result.details as WorkspaceCacheToolDetails | undefined;
|
|
1687
1724
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1688
|
-
text.setText(
|
|
1725
|
+
text.setText(
|
|
1726
|
+
details?.action === "status"
|
|
1727
|
+
? formatWorkspaceCacheStatusResult(details.status, theme)
|
|
1728
|
+
: details?.action === "release"
|
|
1729
|
+
? formatWorkspaceReleaseResult(details.release, theme)
|
|
1730
|
+
: formatJobSnapshotResult(details?.job, theme),
|
|
1731
|
+
);
|
|
1689
1732
|
return text;
|
|
1690
1733
|
},
|
|
1691
1734
|
});
|
|
@@ -1764,7 +1807,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1764
1807
|
if (params.action === "status") {
|
|
1765
1808
|
const summary = await gitOperations.status(directory, vehicleCall);
|
|
1766
1809
|
const details: GitToolDetails = { action: "status", summary };
|
|
1767
|
-
return { content: [{ type: "text", text:
|
|
1810
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "status"), summary) }], details };
|
|
1768
1811
|
}
|
|
1769
1812
|
if (params.action === "log") {
|
|
1770
1813
|
if (params.maxCount === undefined) throw new Error("git action=log requires maxCount");
|
|
@@ -1785,18 +1828,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1785
1828
|
if (params.maxBytes === undefined) throw new Error("git action=compare-symbol requires maxBytes");
|
|
1786
1829
|
const comparison = await gitOperations.compareSymbol(directory, params.path, params.symbol, params.fromRef, params.toRef, params.maxBytes);
|
|
1787
1830
|
const details: GitToolDetails = { action: "compare-symbol", comparison };
|
|
1788
|
-
return { content: [{ type: "text", text:
|
|
1831
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "compare-symbol"), comparison) }], details };
|
|
1789
1832
|
}
|
|
1790
1833
|
if (params.action === "worktree-add") {
|
|
1791
1834
|
if (!params.ref) throw new Error("git action=worktree-add requires ref");
|
|
1792
1835
|
const worktreeAdd = await gitOperations.worktreeAdd(directory, params.ref, params.forceRefresh, vehicleCall);
|
|
1793
1836
|
const details: GitToolDetails = { action: "worktree-add", worktreeAdd };
|
|
1794
|
-
return { content: [{ type: "text", text:
|
|
1837
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "worktree-add"), worktreeAdd) }], details };
|
|
1795
1838
|
}
|
|
1796
1839
|
if (params.action === "worktree-remove") {
|
|
1797
1840
|
const worktreeRemove = await gitOperations.worktreeRemove(directory, vehicleCall);
|
|
1798
1841
|
const details: GitToolDetails = { action: "worktree-remove", worktreeRemove };
|
|
1799
|
-
return { content: [{ type: "text", text:
|
|
1842
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "worktree-remove"), worktreeRemove) }], details };
|
|
1800
1843
|
}
|
|
1801
1844
|
if (params.action === "show") {
|
|
1802
1845
|
if (!params.ref || !params.path) throw new Error("git action=show requires ref and path");
|
|
@@ -1809,7 +1852,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1809
1852
|
if (params.maxMatches === undefined || params.maxBytes === undefined) throw new Error("git action=grep-ref requires maxMatches and maxBytes");
|
|
1810
1853
|
const grep = await gitOperations.grep(directory, params.ref, params.pattern, params.pathspecs, params.maxMatches, params.maxBytes, vehicleCall);
|
|
1811
1854
|
const details: GitToolDetails = { action: "grep-ref", grep };
|
|
1812
|
-
return { content: [{ type: "text", text:
|
|
1855
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "grep-ref"), grep) }], details };
|
|
1813
1856
|
}
|
|
1814
1857
|
if (params.action === "grep-history") {
|
|
1815
1858
|
if (!params.pattern) throw new Error("git action=grep-history requires pattern");
|
|
@@ -1835,20 +1878,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1835
1878
|
vehicleCall,
|
|
1836
1879
|
);
|
|
1837
1880
|
const details: GitToolDetails = { action: "grep-history", historyGrep };
|
|
1838
|
-
return { content: [{ type: "text", text:
|
|
1881
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "grep-history"), historyGrep) }], details };
|
|
1839
1882
|
}
|
|
1840
1883
|
if (params.action === "ls-ref") {
|
|
1841
1884
|
if (!params.ref) throw new Error("git action=ls-ref requires ref");
|
|
1842
1885
|
if (params.maxResults === undefined) throw new Error("git action=ls-ref requires maxResults");
|
|
1843
1886
|
const listFiles = await gitOperations.listFiles(directory, params.ref, params.pathspecs, params.maxResults, vehicleCall);
|
|
1844
1887
|
const details: GitToolDetails = { action: "ls-ref", listFiles };
|
|
1845
|
-
return { content: [{ type: "text", text:
|
|
1888
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "ls-ref"), listFiles) }], details };
|
|
1846
1889
|
}
|
|
1847
1890
|
if (params.action === "is-ancestor") {
|
|
1848
1891
|
if (!params.ancestorRef || !params.ref) throw new Error("git action=is-ancestor requires ancestorRef and ref");
|
|
1849
1892
|
const result = await gitOperations.isAncestor(directory, params.ancestorRef, params.ref, vehicleCall);
|
|
1850
1893
|
const details: GitToolDetails = { action: "is-ancestor", isAncestor: { ancestorRef: params.ancestorRef, ref: params.ref, result } };
|
|
1851
|
-
return { content: [{ type: "text", text:
|
|
1894
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "is-ancestor"), { isAncestor: result }) }], details };
|
|
1852
1895
|
}
|
|
1853
1896
|
throw new Error(`unknown git action: ${String(params.action)}`);
|
|
1854
1897
|
},
|
|
@@ -2134,7 +2177,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2134
2177
|
const action = typeof args.action === "string" ? args.action : "";
|
|
2135
2178
|
const path = typeof args.path === "string" ? args.path : "";
|
|
2136
2179
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2137
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("mutation_history"
|
|
2180
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("mutation_history", action)))} ${theme.fg("accent", path)}`);
|
|
2138
2181
|
return text;
|
|
2139
2182
|
},
|
|
2140
2183
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -2210,7 +2253,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2210
2253
|
maxResults: params.maxResults,
|
|
2211
2254
|
cursor: params.cursor,
|
|
2212
2255
|
});
|
|
2213
|
-
return {
|
|
2256
|
+
return {
|
|
2257
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "list"), page) }],
|
|
2258
|
+
details: { action: "list", page },
|
|
2259
|
+
};
|
|
2214
2260
|
}
|
|
2215
2261
|
if (params.action === "remove") {
|
|
2216
2262
|
if (!params.name || !params.resolvedVersion) throw new Error("package_source action=remove requires ecosystem, name, and resolvedVersion");
|
|
@@ -2220,11 +2266,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
2220
2266
|
params.name,
|
|
2221
2267
|
params.resolvedVersion,
|
|
2222
2268
|
);
|
|
2223
|
-
return {
|
|
2269
|
+
return {
|
|
2270
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "remove"), result) }],
|
|
2271
|
+
details: { action: "remove", result },
|
|
2272
|
+
};
|
|
2224
2273
|
}
|
|
2225
2274
|
if (params.action === "clean") {
|
|
2226
2275
|
const result = await packageSourceOperations.clean(optionalPackageEcosystem(params.ecosystem));
|
|
2227
|
-
return {
|
|
2276
|
+
return {
|
|
2277
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "clean"), result) }],
|
|
2278
|
+
details: { action: "clean", result },
|
|
2279
|
+
};
|
|
2228
2280
|
}
|
|
2229
2281
|
if (!params.directory || !params.name) throw new Error("package_source action=resolve requires directory and name");
|
|
2230
2282
|
const directory = resolve(cwd, params.directory);
|
|
@@ -2235,7 +2287,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2235
2287
|
params.registry ?? null,
|
|
2236
2288
|
optionalPackageEcosystem(params.ecosystem),
|
|
2237
2289
|
);
|
|
2238
|
-
return {
|
|
2290
|
+
return {
|
|
2291
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "resolve"), result) }],
|
|
2292
|
+
details: { action: "resolve", result },
|
|
2293
|
+
};
|
|
2239
2294
|
},
|
|
2240
2295
|
renderCall(args, theme, context) {
|
|
2241
2296
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
@@ -2332,12 +2387,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
2332
2387
|
params.forceRefresh,
|
|
2333
2388
|
vehicleCall,
|
|
2334
2389
|
);
|
|
2335
|
-
return {
|
|
2390
|
+
return {
|
|
2391
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "fetch"), result) }],
|
|
2392
|
+
details: { action: "fetch", result },
|
|
2393
|
+
};
|
|
2336
2394
|
}
|
|
2337
2395
|
if (params.action === "evict") {
|
|
2338
2396
|
if (!params.owner || !params.repo) throw new Error("repo_cache action=evict requires owner and repo");
|
|
2339
2397
|
const result = await repoCacheEvictOperations.evict(params.host ?? "github.com", params.owner, params.repo, params.ref ?? null, vehicleCall);
|
|
2340
|
-
return {
|
|
2398
|
+
return {
|
|
2399
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "evict"), result) }],
|
|
2400
|
+
details: { action: "evict", result },
|
|
2401
|
+
};
|
|
2341
2402
|
}
|
|
2342
2403
|
if (params.maxResults === undefined) throw new Error("repo_cache action=list requires maxResults");
|
|
2343
2404
|
const page = await repoCacheListOperations.list(
|
|
@@ -2346,7 +2407,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2346
2407
|
params.cursor,
|
|
2347
2408
|
vehicleCall,
|
|
2348
2409
|
);
|
|
2349
|
-
return {
|
|
2410
|
+
return {
|
|
2411
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "list"), page) }],
|
|
2412
|
+
details: { action: "list", page },
|
|
2413
|
+
};
|
|
2350
2414
|
},
|
|
2351
2415
|
renderCall(args, theme, context) {
|
|
2352
2416
|
const action = args.action === "list" || args.action === "evict" ? args.action : "fetch";
|
|
@@ -2419,14 +2483,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
2419
2483
|
};
|
|
2420
2484
|
if (params.action === "github_repos") {
|
|
2421
2485
|
const result = await externalSearchOperations.githubRepos(params.query, maxResults, vehicleCall);
|
|
2422
|
-
return {
|
|
2486
|
+
return {
|
|
2487
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "github_repos"), result) }],
|
|
2488
|
+
details: { action: "github_repos", result },
|
|
2489
|
+
};
|
|
2423
2490
|
}
|
|
2424
2491
|
if (params.action === "npm_packages") {
|
|
2425
2492
|
const result = await externalSearchOperations.npmPackages(params.query, maxResults, vehicleCall);
|
|
2426
|
-
return {
|
|
2493
|
+
return {
|
|
2494
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "npm_packages"), result) }],
|
|
2495
|
+
details: { action: "npm_packages", result },
|
|
2496
|
+
};
|
|
2427
2497
|
}
|
|
2428
2498
|
const result = await externalSearchOperations.sourcegraphCode(params.query, maxResults, vehicleCall);
|
|
2429
|
-
return {
|
|
2499
|
+
return {
|
|
2500
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "sourcegraph_code"), result) }],
|
|
2501
|
+
details: { action: "sourcegraph_code", result },
|
|
2502
|
+
};
|
|
2430
2503
|
},
|
|
2431
2504
|
renderCall(args, theme, context) {
|
|
2432
2505
|
const action = args.action === "npm_packages" || args.action === "sourcegraph_code" ? args.action : "github_repos";
|
|
@@ -2434,7 +2507,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2434
2507
|
text.setText(formatExternalSearchCall(action, args, theme));
|
|
2435
2508
|
return text;
|
|
2436
2509
|
},
|
|
2437
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
2510
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2438
2511
|
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 0, 0);
|
|
2439
2512
|
if (context.isError) {
|
|
2440
2513
|
const errorText = result.content
|
|
@@ -2445,9 +2518,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2445
2518
|
}
|
|
2446
2519
|
const details = result.details as ExternalSearchToolDetails | undefined;
|
|
2447
2520
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2448
|
-
if (details?.action === "npm_packages") text.setText(formatNpmPackageSearchResult(details.result, theme));
|
|
2449
|
-
else if (details?.action === "sourcegraph_code") text.setText(formatSourcegraphCodeSearchResult(details.result, theme));
|
|
2450
|
-
else text.setText(formatGithubRepoSearchResult(details?.action === "github_repos" ? details.result : undefined, theme));
|
|
2521
|
+
if (details?.action === "npm_packages") text.setText(formatNpmPackageSearchResult(details.result, expanded, theme));
|
|
2522
|
+
else if (details?.action === "sourcegraph_code") text.setText(formatSourcegraphCodeSearchResult(details.result, expanded, theme));
|
|
2523
|
+
else text.setText(formatGithubRepoSearchResult(details?.action === "github_repos" ? details.result : undefined, expanded, theme));
|
|
2451
2524
|
return text;
|
|
2452
2525
|
},
|
|
2453
2526
|
});
|
|
@@ -2470,11 +2543,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2470
2543
|
async execute(_toolCallId, params) {
|
|
2471
2544
|
const directories = params.directories.map((directory) => resolve(cwd, directory));
|
|
2472
2545
|
const results = await crossWorkspaceSearchOperations.findSymbols(params.query, directories, params.timeoutMs, params.maxResults);
|
|
2473
|
-
return {
|
|
2546
|
+
return {
|
|
2547
|
+
content: [{ type: "text", text: formatFindSymbolsAcrossProjectsModelContent(results) }],
|
|
2548
|
+
details: { results },
|
|
2549
|
+
};
|
|
2474
2550
|
},
|
|
2475
2551
|
renderCall(args, theme, context) {
|
|
2476
2552
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2477
|
-
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
2553
|
+
text.setText(formatCrossWorkspaceCall("find_symbols_across_projects", args, theme));
|
|
2478
2554
|
return text;
|
|
2479
2555
|
},
|
|
2480
2556
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -2509,11 +2585,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2509
2585
|
async execute(_toolCallId, params) {
|
|
2510
2586
|
const directories = params.directories.map((directory) => resolve(cwd, directory));
|
|
2511
2587
|
const results = await crossWorkspaceSearchOperations.searchText(params.query, directories, params.maxMatches, params.maxBytes, params.timeoutMs);
|
|
2512
|
-
return {
|
|
2588
|
+
return {
|
|
2589
|
+
content: [{ type: "text", text: formatSearchTextAcrossProjectsModelContent(results) }],
|
|
2590
|
+
details: { results },
|
|
2591
|
+
};
|
|
2513
2592
|
},
|
|
2514
2593
|
renderCall(args, theme, context) {
|
|
2515
2594
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2516
|
-
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
2595
|
+
text.setText(formatCrossWorkspaceCall("search_code_across_projects", args, theme));
|
|
2517
2596
|
return text;
|
|
2518
2597
|
},
|
|
2519
2598
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { LineEditOutcome } from "@danypops/lector";
|
|
2
2
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
3
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
3
4
|
|
|
4
5
|
export function formatLineEditCall(args: { path?: unknown; edits?: unknown }, theme: LectorTheme): string {
|
|
5
6
|
const path = typeof args.path === "string" ? args.path : "";
|
|
6
7
|
const count = Array.isArray(args.edits) ? args.edits.length : 0;
|
|
7
|
-
return `${theme.fg("toolTitle", theme.bold("line_edit"))} ${theme.fg("accent", path)} ${theme.fg("dim", `(${count} edit${count === 1 ? "" : "s"})`)}`;
|
|
8
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("line_edit")))} ${theme.fg("accent", path)} ${theme.fg("dim", `(${count} edit${count === 1 ? "" : "s"})`)}`;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function formatLineEditResult(result: LineEditOutcome | undefined, theme: LectorTheme): string {
|
|
@@ -2,6 +2,7 @@ import type { PackageSourceListEntry, PackageSourceOperationResult } from "@dany
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList, type TableColumn } from "malevich-tui-components";
|
|
4
4
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
5
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_VISIBLE_CANDIDATES = 5;
|
|
7
8
|
|
|
@@ -14,18 +15,18 @@ export function formatPackageSourceCall(
|
|
|
14
15
|
args: { action?: unknown; directory?: unknown; name?: unknown; version?: unknown; ecosystem?: unknown; resolvedVersion?: unknown; text?: unknown },
|
|
15
16
|
theme: LectorTheme,
|
|
16
17
|
): string {
|
|
17
|
-
const label = theme.fg("toolTitle", theme.bold("package_source"));
|
|
18
18
|
const action: PackageSourceAction = args.action === "list" || args.action === "remove" || args.action === "clean" ? args.action : "resolve";
|
|
19
|
+
const label = theme.fg("toolTitle", theme.bold(presentationTitle("package_source", action)));
|
|
19
20
|
if (action === "list") {
|
|
20
21
|
const text = typeof args.text === "string" && args.text.length > 0 ? ` ${theme.fg("dim", args.text)}` : "";
|
|
21
|
-
return `${label}
|
|
22
|
+
return `${label}${text}`;
|
|
22
23
|
}
|
|
23
24
|
if (action === "remove" || action === "clean") {
|
|
24
25
|
const name = typeof args.name === "string" ? args.name : "";
|
|
25
26
|
const version = typeof args.resolvedVersion === "string" ? `@${args.resolvedVersion}` : "";
|
|
26
27
|
const ecosystem = typeof args.ecosystem === "string" ? args.ecosystem : "";
|
|
27
28
|
const identity = name ? `${name}${version}` : ecosystem;
|
|
28
|
-
return `${label}
|
|
29
|
+
return `${label}${identity ? ` ${theme.fg("accent", identity)}` : ""}`;
|
|
29
30
|
}
|
|
30
31
|
const name = typeof args.name === "string" ? args.name : "";
|
|
31
32
|
const version = typeof args.version === "string" ? `@${args.version}` : "";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const DEFAULT_MODEL_CONTENT_BYTES = 32_768;
|
|
2
|
+
const MAX_COLLECTION_ENTRIES = 24;
|
|
3
|
+
const MAX_DEPTH = 4;
|
|
4
|
+
|
|
5
|
+
function scalarText(value: unknown): string | undefined {
|
|
6
|
+
if (value === null) return "none";
|
|
7
|
+
if (typeof value === "string") return value;
|
|
8
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendSemanticLines(lines: string[], value: unknown, path: string, depth: number): void {
|
|
13
|
+
const scalar = scalarText(value);
|
|
14
|
+
if (scalar !== undefined) {
|
|
15
|
+
lines.push(`${path}: ${scalar}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (depth >= MAX_DEPTH) {
|
|
19
|
+
lines.push(`${path}: [nested value omitted]`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
lines.push(`${path} (${value.length})`);
|
|
24
|
+
for (const [index, entry] of value.slice(0, MAX_COLLECTION_ENTRIES).entries()) appendSemanticLines(lines, entry, `${path}[${index}]`, depth + 1);
|
|
25
|
+
if (value.length > MAX_COLLECTION_ENTRIES) lines.push(`${path}: ${value.length - MAX_COLLECTION_ENTRIES} more entries omitted`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "object" && value !== null) {
|
|
29
|
+
const entries = Object.entries(value).slice(0, MAX_COLLECTION_ENTRIES);
|
|
30
|
+
if (entries.length === 0) lines.push(`${path}: none`);
|
|
31
|
+
for (const [key, entry] of entries) appendSemanticLines(lines, entry, path ? `${path}.${key}` : key, depth + 1);
|
|
32
|
+
if (Object.keys(value).length > MAX_COLLECTION_ENTRIES) lines.push(`${path || "result"}: additional fields omitted`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
lines.push(`${path}: unavailable`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Bounds UTF-8 model-facing text independently from presentation details. */
|
|
39
|
+
export function boundModelContentText(full: string, maxBytes = DEFAULT_MODEL_CONTENT_BYTES): string {
|
|
40
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 64) throw new Error("model content maxBytes must be an integer of at least 64");
|
|
41
|
+
if (Buffer.byteLength(full, "utf8") <= maxBytes) return full;
|
|
42
|
+
const suffix = "\n[model content truncated]";
|
|
43
|
+
const budget = maxBytes - Buffer.byteLength(suffix, "utf8");
|
|
44
|
+
let bytes = Buffer.from(full, "utf8").subarray(0, Math.max(0, budget));
|
|
45
|
+
let prefix = "";
|
|
46
|
+
while (bytes.length > 0) {
|
|
47
|
+
try {
|
|
48
|
+
prefix = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
49
|
+
break;
|
|
50
|
+
} catch {
|
|
51
|
+
bytes = bytes.subarray(0, -1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `${prefix}${suffix}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Formats an operation outcome as bounded, semantic plain text for model consumption. */
|
|
58
|
+
export function formatSemanticModelContent(title: string, value: unknown, maxBytes = DEFAULT_MODEL_CONTENT_BYTES): string {
|
|
59
|
+
const lines = [title];
|
|
60
|
+
appendSemanticLines(lines, value, "", 0);
|
|
61
|
+
return boundModelContentText(lines.join("\n"), maxBytes);
|
|
62
|
+
}
|