feat: add hashline editing and TypeScript LSP

This commit is contained in:
云服务部-叶林立
2026-08-19 10:24:03 +08:00
parent 3d431a353d
commit bf8bb42a09
14 changed files with 438 additions and 36 deletions
+23
View File
@@ -0,0 +1,23 @@
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const BUNDLE_CONFIG_PATH = fileURLToPath(new URL("../config/pi-hashline-edit.json", import.meta.url));
export function deployBundleHashlineConfig(agentDir: string): void {
const targetPath = join(agentDir, "hashline.json");
const bundledConfig = readFileSync(BUNDLE_CONFIG_PATH, "utf8");
try {
if (readFileSync(targetPath, "utf8") === bundledConfig) {
return;
}
} catch {
// Missing or unreadable target: replace it with the bundle-owned defaults.
}
mkdirSync(dirname(targetPath), { recursive: true });
const temporaryPath = `${targetPath}.my-pi.tmp`;
writeFileSync(temporaryPath, bundledConfig, "utf8");
renameSync(temporaryPath, targetPath);
}
+9
View File
@@ -0,0 +1,9 @@
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { deployBundleHashlineConfig } from "./hashline-config.ts";
/** Deploy bundle defaults before pi-hashline-edit reads its module-level config. */
export default async function bundledHashlineExtension(pi: ExtensionAPI): Promise<void> {
deployBundleHashlineConfig(getAgentDir());
const { default: hashlineExtension } = await import("../node_modules/pi-hashline-edit/index.ts");
hashlineExtension(pi);
}
+49
View File
@@ -0,0 +1,49 @@
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const BUNDLE_CONFIG_PATH = fileURLToPath(new URL("../config/lsp.json", import.meta.url));
const TYPESCRIPT_SERVER_ID = "typescript-language-server";
interface LspConfigFile {
version?: number;
servers?: Array<{
id?: string;
bin?: string;
args?: string[];
[key: string]: unknown;
}>;
}
export interface TypeScriptLspRuntime {
nodeBinary: string;
serverCliPath: string;
}
export function materializeBundleLspConfig(source: string, runtime: TypeScriptLspRuntime): string {
const config = JSON.parse(source) as LspConfigFile;
const server = config.servers?.find((entry) => entry.id === TYPESCRIPT_SERVER_ID);
if (server === undefined) {
throw new Error(`Missing ${TYPESCRIPT_SERVER_ID} entry in bundled LSP config`);
}
server.bin = runtime.nodeBinary;
server.args = [runtime.serverCliPath, "--stdio"];
return `${JSON.stringify(config, null, 2)}\n`;
}
export function deployBundleLspConfig(agentDir: string, runtime: TypeScriptLspRuntime): void {
const targetPath = join(agentDir, "lsp.json");
const bundledConfig = materializeBundleLspConfig(readFileSync(BUNDLE_CONFIG_PATH, "utf8"), runtime);
try {
if (readFileSync(targetPath, "utf8") === bundledConfig) return;
} catch {
// Missing or unreadable target: replace it with the bundle-owned baseline.
}
mkdirSync(dirname(targetPath), { recursive: true });
const temporaryPath = `${targetPath}.my-pi.tmp`;
writeFileSync(temporaryPath, bundledConfig, "utf8");
renameSync(temporaryPath, targetPath);
}
+8 -19
View File
@@ -1,28 +1,17 @@
import lspExtension from "../node_modules/pi-lsp/extensions/pi-lsp/index.ts";
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { deployBundleLspConfig } from "./lsp-config.ts";
const BUNDLE_CONFIG_PATH = fileURLToPath(new URL("../config/lsp.json", import.meta.url));
function deployBundleLspConfig(): void {
const targetPath = join(getAgentDir(), "lsp.json");
const bundledConfig = readFileSync(BUNDLE_CONFIG_PATH, "utf8");
try {
if (readFileSync(targetPath, "utf8") === bundledConfig) return;
} catch {
// Missing or unreadable target: replace it with the bundle-owned baseline.
}
const temporaryPath = `${targetPath}.my-pi.tmp`;
writeFileSync(temporaryPath, bundledConfig, "utf8");
renameSync(temporaryPath, targetPath);
}
const TYPESCRIPT_LANGUAGE_SERVER_CLI = fileURLToPath(
new URL("../node_modules/typescript-language-server/lib/cli.mjs", import.meta.url),
);
/** Deploy the bundle-owned LSP config before registering pi-lsp. */
export default function bundledLspExtension(pi: ExtensionAPI): void {
deployBundleLspConfig();
deployBundleLspConfig(getAgentDir(), {
nodeBinary: process.execPath,
serverCliPath: TYPESCRIPT_LANGUAGE_SERVER_CLI,
});
lspExtension(pi);
}
+18 -2
View File
@@ -20,6 +20,16 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string {
);
}
if (
hasTool(selectedTools, "lsp_definition") ||
hasTool(selectedTools, "lsp_references") ||
hasTool(selectedTools, "lsp_diagnostics")
) {
rules.push(
"- Use LSP for symbol definitions, references, hover/type information, and post-edit diagnostics when the language server supports the file.",
);
}
if (hasTool(selectedTools, "find") || hasTool(selectedTools, "grep") || hasTool(selectedTools, "multi_grep")) {
rules.push(
"- For literal search, narrow in stages instead of requesting a repository-wide dump:",
@@ -32,7 +42,7 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string {
if (hasTool(selectedTools, "ctx_execute") || hasTool(selectedTools, "ctx_execute_file")) {
rules.push(
"- Use Context Mode for logs, test/build output, generated data, large-file analysis, and any command whose output may be large or unpredictable. Print only the derived answer needed for the task.",
"- Use Context Mode for logs, test/build output, generated data, large-file exploration/analysis/summarization, and any command whose output may be large or unpredictable. Prefer ctx_execute_file over read when deriving an answer from a large file inside the workspace, and print only the derived answer needed for the task.",
);
}
@@ -44,7 +54,13 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string {
if (hasTool(selectedTools, "read")) {
rules.push(
"- Use read directly when exact text is needed for editing or when a file is small. Do not read a complete large file merely to copy, compare, hash, count, or summarize it.",
"- Use read only when exact source is needed or the file is small. Before editing, request the smallest useful offset/limit range; do not read a complete large file merely to copy, compare, hash, count, explore, or summarize it.",
);
}
if (hasTool(selectedTools, "read") && hasTool(selectedTools, "edit")) {
rules.push(
"- For existing-file changes, obtain fresh LINE#HASH anchors with a small read, then use anchored edit operations. Reuse fresh anchors returned by successful edits for chained changes; re-read only when anchors are stale or the next target region was not shown.",
);
}