From 0fac1bc402b249510f06d6e5361f624fc49dec5b Mon Sep 17 00:00:00 2001 From: allthewayeast Date: Tue, 29 Sep 2026 07:04:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BD=AECodeGraph=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E8=B7=AF=E5=BE=84=E5=92=8C=E5=8F=82=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9C=A8=E4=BC=9A=E8=AF=9D=E4=B8=AD=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=A4=A7=E6=AE=B5=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/index.js | 99 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/lib/index.js b/lib/index.js index b3c28e8..e150fc8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -20,6 +20,12 @@ // — the agent's grep/read reflex has nothing left to // find. Every failure path is a silent no-op. // +// The steered message keeps `role: 'user'` (so the model reads it as prompt +// context) but MUST NOT use `source.kind: 'user'` — that kind means "a human +// typed this" and the Chat UI prints it as a full user bubble. It carries +// `plugin:dsh-codegraph` instead, which the UI files as a collapsed +// injected-context row. See FRONTLOAD_SOURCE_KIND. +// // Why a native plugin instead of a plain MCP server: codegraph's MCP server // exposes ZERO tools while the workspace is unindexed (and tells the agent it // should not index itself). This plugin always exposes the tools, including @@ -40,8 +46,11 @@ // in the wild: intermittent "neither subprocess nor shell mounted" boot // crashes). Lazy resolution also lets the tools start working if an executor // mounts after this plugin. The `codegraph` executable must be on PATH (e.g. -// global `npm i -g @colbymchenry/codegraph` or the npm thin shim); a clearer -// error is thrown when it is missing. +// global `npm i -g @colbymchenry/codegraph` or the npm thin shim), or its +// full path given via the `codegraphPath` config option (cordis.patch.yml), +// with optional extra CLI arguments via `codegraphArgs` (an array of strings, +// prepended to every invocation); a clearer error is thrown when it is +// missing. import { existsSync } from 'node:fs' import { dirname, join } from 'node:path' @@ -69,10 +78,20 @@ export const inject = ['tools', 'systemPrompt'] // steers the result into the turn as — so the agent's // grep/read reflex has nothing left to find. Every failure path is a // silent no-op; it never breaks or blocks the user's prompt. +// - codegraphPath (default ''): full path to the `codegraph` executable. When +// set (e.g. in cordis.patch.yml), the tools invoke that executable directly +// (both the subprocess and shell execution paths). When empty, the +// executable is resolved from the system PATH as before. +// - codegraphArgs (default []): extra CLI arguments passed to the codegraph +// executable on every invocation, e.g. ['--no-color', '--threads', '4']. +// Only meaningful together with `codegraphPath` (with a PATH-resolved +// executable, pass such flags through the tool arguments instead). export const Config = z.object({ guideSearch: z.boolean().default(true), surface: z.union([z.const('core'), z.const('full')]).default('core'), - frontload: z.boolean().default(true) + frontload: z.boolean().default(true), + codegraphPath: z.string().default(''), + codegraphArgs: z.array(z.string()).default([]) }) // System-prompt guidance that makes the codegraph tools preferred for code @@ -115,7 +134,7 @@ function shQuote(value) { } function executableHint() { - return '`codegraph` was not found on PATH. Install it once, e.g. `npm i -g @colbymchenry/codegraph` (or use the official install.sh / npm thin shim), then retry.' + return '`codegraph` was not found on PATH. Install it once, e.g. `npm i -g @colbymchenry/codegraph` (or use the official install.sh / npm thin shim), or set the `codegraphPath` option in cordis.patch.yml to the full path of the codegraph executable, then retry.' } // The core tool surface (see Config.surface): the index bootstrap/maintenance @@ -135,6 +154,42 @@ const CORE_TOOLS = new Set([ // own output. const FRONTLOAD_MARKER = ' a "context" node: a COLLAPSED one-line row +// (icon + "injected context" + producer label), expanded +// only on demand, and folded into the turn's process +// disclosure. `source.form`/`source.summary` only shape +// how that row reads. +// kind === 'user' -> a real user/steering BUBBLE: the message body is +// printed in full in the transcript. +// +// So an injection that claims to be a user message renders as if the human had +// pasted the whole 12 KB explore dump — the exact complaint this fixes. Every +// DSH-native injection uses a non-'user' kind (`plugin:`, +// `runtime-context`, `skill-catalog`, `agent-instructions`, `subagent-settled`, +// `tool-jobs`, ...), and so do the other plugins +// (`plugin:graph-memory`, `plugin:dsh-mcp-manager`). This plugin must too. +const FRONTLOAD_SOURCE_KIND = `plugin:${name}` + +// The `notice` form gives the collapsed row a one-line account of what was +// injected, so the reader can tell an explore dump from any other injection +// without expanding it. The 120-character bound mirrors DSH's own +// CONTEXT_SUMMARY_MAX_CHARS, clamped here rather than imported so the plugin +// keeps working on host versions that predate that export. +const SUMMARY_MAX_CHARS = 120 +function summarizeExplore(body) { + const found = /Found (\d+) symbols? across (\d+) files?/i.exec(body) + if (found) return `explore: ${found[1]} symbols in ${found[2]} files` + const firstLine = body.split('\n').map((line) => line.trim()).find((line) => line.length > 0) + const summary = firstLine ? firstLine.replace(/[*#>]/g, '').trim() : 'explore result' + if (!summary) return 'explore result' + return summary.length <= SUMMARY_MAX_CHARS ? summary : summary.slice(0, SUMMARY_MAX_CHARS - 1) + '…' +} + // Structural keywords (EN + zh) signalling a code-structure/flow question — // the HIGH-confidence gate tier: fire explore without further verification. const STRUCTURAL_KEYWORDS = [ @@ -179,7 +234,7 @@ function findIndexRoot(cwd) { export function apply(ctx, config = {}) { const tools = ctx.tools - const { guideSearch = true, surface = 'core', frontload = true } = config + const { guideSearch = true, surface = 'core', frontload = true, codegraphPath = '', codegraphArgs = [] } = config const toolEnabled = (name) => surface === 'full' || CORE_TOOLS.has(name) // Make the codegraph_* tools the preferred route for code search: instruct @@ -220,14 +275,25 @@ export function apply(ctx, config = {}) { } if (sub !== undefined) { if (resolvedExe.value === null) { - try { - resolvedExe.value = await sub.resolveExecutable('codegraph') - } catch { - throw new Error('dsh-codegraph: ' + executableHint()) + if (codegraphPath) { + // Explicit full path from config (cordis.patch.yml) — use it as-is. + if (!existsSync(codegraphPath)) { + throw new Error( + `dsh-codegraph: configured codegraphPath "${codegraphPath}" does not exist. ` + + 'Fix the path in cordis.patch.yml, or remove the option to resolve `codegraph` from PATH.' + ) + } + resolvedExe.value = codegraphPath + } else { + try { + resolvedExe.value = await sub.resolveExecutable('codegraph') + } catch { + throw new Error('dsh-codegraph: ' + executableHint()) + } } } const proc = sub.spawn({ - argv: [resolvedExe.value].concat(argv), + argv: [resolvedExe.value].concat(codegraphArgs).concat(argv), cwd: cwd || '/', stdio: { stdin: 'ignore', @@ -248,7 +314,8 @@ export function apply(ctx, config = {}) { } return text } - const command = 'codegraph ' + argv.map(shQuote).join(' ') + const executable = codegraphPath || 'codegraph' + const command = executable + ' ' + [...codegraphArgs, ...argv].map(shQuote).join(' ') const spec = shell.resolve({ command, workdir: cwd || '/', @@ -596,7 +663,15 @@ export function apply(ctx, config = {}) { type: 'text', text: `${FRONTLOAD_MARKER} note="Structural context from CodeGraph for this prompt — treat the returned source as already read; call codegraph_explore for more.">\n${body}\n` }], - source: { kind: 'user' } + // NOT `{ kind: 'user' }`: a user-role injection must still carry a + // non-'user' source kind, or the Chat UI renders it as a human bubble + // instead of a collapsed injected-context row. See FRONTLOAD_SOURCE_KIND. + source: { + kind: FRONTLOAD_SOURCE_KIND, + plugin: name, + form: 'notice', + summary: summarizeExplore(body0) + } })) }