Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ bytes.
## Features

**Editor and input.** pi-tui differential rendering with a CJK-aware
multi-line editor; slash-command, unified `@` workspace/Plugin references and
`$` Skill completion; persisted prompt history through ZCode's history API;
multi-line editor; slash-command completion, unified `@` workspace/Plugin
references with recursive file search, and `$` Skill completion;
persisted prompt history through ZCode's history API;
`--no-color` and `NO_COLOR` support.

**Streaming and conversation.** Streamed assistant text from official ZCode
Expand Down
16 changes: 16 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ patch plan as required verification points: Desktop 3.16.3 already supplies
their behavior, so they report `already_present` and never rewrite the runtime.
Synchronization still fails if a future upstream build drops the behavior.

## Workspace file completion

The required `recursive-workspace-paths` runtime patch extends the native
`createWorkspacePathSuggestionProvider` through `src/runtime-workspace-paths.ts`.
An unqualified `@name` searches all workspace directory levels; empty tokens and
explicit paths retain the native directory completion. Searches use the runtime's
filesystem port and excluded-directory set, skip symlinks, and forward cancellation.
Only the best requested number of matches are retained, with exact names before
prefix and substring matches. Hidden entries require a query beginning with `.`;
this preserves the runtime's existing rules rather than adding `.gitignore` parsing.

`bun test test/runtime-workspace-paths.test.ts test/runtime-workspace-path-patches.test.ts`
covers real file trees and patch compatibility. After syncing the built runtime,
`bun test test/runtime/workspace-paths.test.ts` checks the actual native provider
and filesystem adapter in Node, including the nested file case from #240.

## TUI startup compatibility

Recent upstream runtimes pass session metadata and slash commands through
Expand Down
2 changes: 2 additions & 0 deletions scripts/check-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
hasRuntimeHttpNoContentGuard,
hasRuntimeNetworkRetryGuard,
hasRuntimeResolvedContextWindow,
hasRuntimeRecursiveWorkspacePaths,
hasRuntimeSharedOAuth,
hasRuntimeSqliteBusyTimeout,
hasRuntimeSqliteWriteRecovery,
Expand Down Expand Up @@ -103,6 +104,7 @@ if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource
|| !hasRuntimeBigmodelOAuth(runtimeSource)
|| !hasRuntimeBrowserUrlOpening(runtimeSource)
|| !hasRuntimeConfigurationDiagnostics(runtimeSource)
|| !hasRuntimeRecursiveWorkspacePaths(runtimeSource)
|| !hasRuntimeResolvedContextWindow(runtimeSource)
|| !hasRuntimeSharedOAuth(runtimeSource)
|| !hasRuntimeCliSettingsFile(runtimeSource)
Expand Down
66 changes: 66 additions & 0 deletions scripts/runtime-workspace-path-patches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { parseExpressionAt } from "acorn";

const identifier = "[A-Za-z_$][\\w$]*";
const bridge = 'require(require("node:path").join(__dirname,"cli-config.cjs"))';
const wrapper = `${bridge}.withRecursiveWorkspacePathSuggestions`;

function incompatible(detail: string): never {
throw new Error(`ZCode runtime is incompatible with recursive workspace paths (${detail}).`);
}

function unique<T>(items: T[], detail: string): T {
if (items.length !== 1) incompatible(`${detail} missing or ambiguous`);
return items[0]!;
}

function escape(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}

function inspect(runtime: string) {
const name = unique([...runtime.matchAll(new RegExp(
`${identifier}\\((${identifier}),"createWorkspacePathSuggestionProvider"\\)`, "gu"
))], "provider label")[1]!;
const declaration = unique([...runtime.matchAll(new RegExp(`function ${escape(name)}\\(`, "gu"))], "provider function");
const fn = parseExpressionAt(runtime, declaration.index!, { ecmaVersion: "latest" });
if (fn.type !== "FunctionExpression" || fn.async || fn.params.length !== 1 || fn.params[0]?.type !== "Identifier") {
incompatible("provider signature changed");
}
const source = (node: { start: number; end: number }) => runtime.slice(node.start, node.end);
const [bindings, returned] = fn.body.body;
if (fn.body.body.length !== 2 || bindings?.type !== "VariableDeclaration" || bindings.declarations.length !== 2
|| returned?.type !== "ReturnStatement" || !returned.argument) incompatible("provider body changed");
const [fileSystem, root] = bindings.declarations;
if (fileSystem?.id.type !== "Identifier" || !fileSystem.init || root?.id.type !== "Identifier" || !root.init) {
incompatible("provider bindings changed");
}
const options = escape(fn.params[0].name);
if (!new RegExp(`^${options}\\.fileSystemPort\\?\\?${identifier}\\(\\)$`, "u").test(source(fileSystem.init))
|| !new RegExp(`^\\(0,${identifier}\\.resolve\\)\\(${options}\\.workspaceDirectory\\)$`, "u").test(source(root.init))) {
incompatible("filesystem or workspace binding changed");
}
const argument = returned.argument;
const native = argument.type === "CallExpression" ? argument.arguments[0] : argument;
if (native?.type !== "ArrowFunctionExpression" || !native.async || native.params.length !== 1
|| native.params[0]?.type !== "Identifier") incompatible("native callback changed");
const nativeSource = source(native);
const exclusions = unique([...nativeSource.matchAll(new RegExp(
`(${identifier})\\.has\\(${identifier}\\.name\\)`, "gu"
))], "directory exclusions")[1]!;
const listing = new RegExp(
`${escape(fileSystem.id.name)}\\.listDirectory\\(\\{path:${identifier}\\},\\{signal:${escape(native.params[0].name)}\\.abortSignal\\}\\)`, "u"
);
if (!listing.test(nativeSource)) incompatible("native directory listing changed");
const replacement = `${wrapper}(${nativeSource},{workspaceDirectory:${root.id.name},fileSystemPort:${fileSystem.id.name},excludedDirectories:${exclusions}})`;
if (argument.type === "CallExpression" && source(argument) !== replacement) incompatible("recursive wrapper changed");
return { start: argument.start, end: argument.end, replacement, patched: argument.type === "CallExpression" };
}

export function hasRuntimeRecursiveWorkspacePaths(runtime: string): boolean {
try { return inspect(runtime).patched; } catch { return false; }
}

export function patchRuntimeRecursiveWorkspacePaths(runtime: string): string {
const { start, end, replacement, patched } = inspect(runtime);
return patched ? runtime : runtime.slice(0, start) + replacement + runtime.slice(end);
}
8 changes: 8 additions & 0 deletions scripts/sync-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export { hasRuntimeAppServerStandaloneAuth, patchRuntimeAppServerStandaloneAuth
import { hasRuntimeConfigurationDiagnostics, patchRuntimeConfigurationDiagnostics } from "./runtime-config-diagnostic-patches.ts";
import { hasRuntimeResolvedContextWindow, patchRuntimeResolvedContextWindow } from "./runtime-context-window-patches.ts";
export { hasRuntimeResolvedContextWindow, patchRuntimeResolvedContextWindow } from "./runtime-context-window-patches.ts";
import { hasRuntimeRecursiveWorkspacePaths, patchRuntimeRecursiveWorkspacePaths } from "./runtime-workspace-path-patches.ts";
export { hasRuntimeRecursiveWorkspacePaths, patchRuntimeRecursiveWorkspacePaths } from "./runtime-workspace-path-patches.ts";
import { patchRuntimeWorkflowManagement } from "./runtime-workflow-patches.ts";
export { patchRuntimeWorkflowManagement } from "./runtime-workflow-patches.ts";
export { hasRuntimeConfigurationDiagnostics, patchRuntimeConfigurationDiagnostics } from "./runtime-config-diagnostic-patches.ts";
Expand Down Expand Up @@ -1829,6 +1831,12 @@ export const runtimePatchPlan: readonly RuntimePatchDefinition[] = [
apply: patchRuntimeResolvedContextWindow,
verify: hasRuntimeResolvedContextWindow
},
{
id: "recursive-workspace-paths",
requirement: "required",
apply: patchRuntimeRecursiveWorkspacePaths,
verify: hasRuntimeRecursiveWorkspacePaths
},
{
id: "tui-bridge",
requirement: "required",
Expand Down
1 change: 1 addition & 0 deletions src/runtime-config-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { assertResumableSession, ensureSessionForTitle, readSessionResumeTarget
export { readTuiRuntimeProjection, sendTuiBackgroundTaskMessage } from "./runtime-tui-bridge.ts";
export { createTuiWorkflowManagement, startTuiSavedWorkflow } from "./runtime-workflow-bridge.ts";
export { restoreTuiBackgroundTasks } from "./runtime-background-restore.ts";
export { withRecursiveWorkspacePathSuggestions } from "./runtime-workspace-paths.ts";
export { installSqliteWriteRecovery, pruneSqliteUsage, sqliteRecoveryStats } from "./runtime-sqlite-recovery.ts";
export { appServerRegistryOptions } from "./app-server-auth.ts";
export { exchangeBigmodelOAuthCode } from "./bigmodel-oauth.ts";
Expand Down
101 changes: 101 additions & 0 deletions src/runtime-workspace-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { isAbsolute, relative, sep } from "node:path";

interface PathSuggestion {
kind: "file" | "directory";
path: string;
}

interface SuggestionRequest {
token: string;
limit?: number;
abortSignal?: AbortSignal;
}

interface SuggestionResult {
items: PathSuggestion[];
truncated: boolean;
}

type PathSuggestionProvider = (request: SuggestionRequest) => Promise<SuggestionResult>;

interface WorkspacePathOptions {
workspaceDirectory: string;
fileSystemPort: {
listDirectory(request: { path: string }, options?: { signal?: AbortSignal }): Promise<{
entries: { name: string; path: string; kind: string }[];
}>;
};
excludedDirectories: ReadonlySet<string>;
}

interface RankedSuggestion {
item: PathSuggestion;
rank: number;
depth: number;
}

function compare(first: RankedSuggestion, second: RankedSuggestion): number {
return first.rank - second.rank
|| Number(first.item.kind === "file") - Number(second.item.kind === "file")
|| first.depth - second.depth
|| first.item.path.localeCompare(second.item.path);
}

/** Extend the runtime's native path completion with workspace-wide name searches. */
export function withRecursiveWorkspacePathSuggestions(
native: PathSuggestionProvider,
options: WorkspacePathOptions
): PathSuggestionProvider {
return async (request) => {
const empty: SuggestionResult = { items: [], truncated: false };
if (request.abortSignal?.aborted) return empty;
const query = request.token.replace(/^@/u, "");
// Keep directory browsing, token validation and explicit paths upstream-owned.
if (!query || query === ".." || /[\\/]/u.test(query)) return native(request);

const normalizedQuery = query.toLowerCase();
const includeHidden = query.startsWith(".");
const limit = Number.isFinite(request.limit) ? Math.max(1, Math.floor(request.limit!)) : 50;
const pending = [options.workspaceDirectory];
const best: RankedSuggestion[] = [];
let matches = 0;

while (pending.length > 0) {
if (request.abortSignal?.aborted) return empty;
// Bound concurrent filesystem work; cancellation stops scheduling new reads.
const listings = await Promise.all(pending.splice(-8).map(async (path) => {
try {
return (await options.fileSystemPort.listDirectory({ path }, { signal: request.abortSignal })).entries;
} catch {
// An unreadable or removed subdirectory must not hide other results.
return [];
}
}));
if (request.abortSignal?.aborted) return empty;
for (const entries of listings) for (const entry of entries) {
// The native adapter reports symlinks separately. Do not follow them.
if (entry.kind !== "file" && entry.kind !== "directory") continue;
if (entry.kind === "directory" && options.excludedDirectories.has(entry.name)) continue;
if (!includeHidden && entry.name.startsWith(".")) continue;
const path = relative(options.workspaceDirectory, entry.path);
if (!path || path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) continue;
if (entry.kind === "directory") pending.push(entry.path);

const name = entry.name.toLowerCase();
if (!name.includes(normalizedQuery)) continue;
matches += 1;
const displayPath = path.split(sep).join("/");
const candidate: RankedSuggestion = {
item: { kind: entry.kind, path: entry.kind === "directory" ? `${displayPath}/` : displayPath },
rank: name === normalizedQuery ? 0 : name.startsWith(normalizedQuery) ? 1 : 2,
depth: displayPath.split("/").length
};
const index = best.findIndex((item) => compare(candidate, item) < 0);
if (index >= 0) best.splice(index, 0, candidate);
else if (best.length < limit) best.push(candidate);
if (best.length > limit) best.pop();
}
}
return { items: best.map(({ item }) => item), truncated: matches > limit };
};
}
57 changes: 57 additions & 0 deletions test/fixtures/workspace-path-runtime.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const Module = require("node:module");
const path = require("node:path");

// Expose the native provider from the shipped bundle without starting the CLI.
const file = path.resolve(__dirname, "../../vendor/zcode.cjs");
let source = fs.readFileSync(file, "utf8");
const label = /[A-Za-z_$][\w$]*\(([A-Za-z_$][\w$]*),"createWorkspacePathSuggestionProvider"\)/u.exec(source);
assert.ok(label, "Missing native workspace path provider");
const init = [...source.slice(0, label.index).matchAll(/([A-Za-z_$][\w$]*)=[A-Za-z_$][\w$]*\(\(\)=>\{/gu)].at(-1)?.[1];
assert.ok(init, "Missing native workspace path initialization");
const main = /async function [A-Za-z_$][\w$]*\(\)\{let [A-Za-z_$][\w$]*=process\.argv\.slice\(2\);/u.exec(source);
assert.ok(main, "Missing native runtime test entry");
source = source.replace(main[0], `${main[0]}module.exports=(${init}(),${label[1]});return;`);
const runtime = new Module(file, module);
runtime.filename = file;
runtime.paths = Module._nodeModulePaths(path.dirname(file));
runtime._compile(source, file);
const createProvider = runtime.exports;
assert.equal(typeof createProvider, "function");

async function run() {
const workspace = process.cwd();
for (const name of [
"First_folder/one.py", "First_folder/two.py", "Second_folder/one.py", "Second_folder/three.py",
"docs/my file.md", "src/node_modules/one.py", ".git/one.py", ".hidden/one.py", "src/.one.py"
]) {
await fs.promises.mkdir(path.dirname(path.join(workspace, name)), { recursive: true });
await fs.promises.writeFile(path.join(workspace, name), "");
}
const search = createProvider({ workspaceDirectory: workspace });
const result = await search({ token: "@on" });
assert.deepEqual(result, {
items: [
{ kind: "file", path: "First_folder/one.py" },
{ kind: "file", path: "Second_folder/one.py" },
{ kind: "directory", path: "Second_folder/" }
], truncated: false
});
assert.deepEqual(await search({ token: "@one.py", limit: 1 }), {
items: [{ kind: "file", path: "First_folder/one.py" }], truncated: true
});
for (const token of ["@First_folder/on", "@First_folder\\on"]) {
assert.deepEqual(await search({ token }), { items: [{ kind: "file", path: "First_folder/one.py" }], truncated: false });
}
assert.ok((await search({ token: "@" })).items.every(item => !item.path.replace(/\/$/u, "").includes("/")));
assert.deepEqual((await search({ token: "@my f" })).items, [{ kind: "file", path: "docs/my file.md" }]);
assert.deepEqual((await search({ token: "@.one" })).items, [{ kind: "file", path: "src/.one.py" }]);
assert.deepEqual(await search({ token: "@../" }), { items: [], truncated: false });
const controller = new AbortController();
controller.abort();
assert.deepEqual(await search({ token: "@on", abortSignal: controller.signal }), { items: [], truncated: false });
process.stdout.write("Native workspace search passed: recursive files, limits, scoped paths, exclusions and cancellation.\n");
}

run().catch(error => { console.error(error); process.exitCode = 1; });
48 changes: 48 additions & 0 deletions test/runtime-workspace-path-patches.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, test } from "bun:test";
import {
hasRuntimeRecursiveWorkspacePaths as has,
patchRuntimeRecursiveWorkspacePaths as patch
} from "../scripts/runtime-workspace-path-patches.ts";
import { applyRuntimePatchPlan, runtimePatchPlan } from "../scripts/sync-runtime.ts";

const fixture = 'function provider(e){let fs=e.fileSystemPort??defaultFS(),root=(0,path.resolve)(e.workspaceDirectory);'
+ 'return async request=>{let entries=(await fs.listDirectory({path:root},{signal:request.abortSignal})).entries;'
+ 'return{items:entries.filter(entry=>!excluded.has(entry.name)),truncated:false}}}'
+ 'label(provider,"createWorkspacePathSuggestionProvider");';

test("wraps the native callback idempotently and keeps its filesystem and exclusions", () => {
const patched = patch(fixture);
expect(has(fixture)).toBe(false);
expect(has(patched)).toBe(true);
expect(patch(patched)).toBe(patched);
expect(patched).toContain("{workspaceDirectory:root,fileSystemPort:fs,excludedDirectories:excluded}");
expect(patched).toContain("entries.filter(entry=>!excluded.has(entry.name))");
const renamed = fixture.replaceAll("provider", "$factory").replaceAll("root", "$root").replaceAll("fs", "$fs");
expect(has(patch(renamed))).toBe(true);
});

test("rejects changed, missing and ambiguous native anchors and incomplete wrappers", () => {
for (const source of [
"const runtime = {};",
`${fixture}label(other,"createWorkspacePathSuggestionProvider");`,
fixture.replace("function provider(e)", "function provider(e, other)"),
fixture.replace("e.fileSystemPort", "e.otherPort"),
fixture.replace(".listDirectory(", ".searchFiles("),
fixture.replace("excluded.has(entry.name)", "false"),
patch(fixture).replace("excludedDirectories:excluded", "excludedDirectories:other"),
patch(fixture).replace("withRecursiveWorkspacePathSuggestions", "wrongWrapper")
]) {
expect(has(source)).toBe(false);
expect(() => patch(source)).toThrow("incompatible with recursive workspace paths");
}
});

test("registers recursion as a required, verified runtime patch", () => {
const entry = runtimePatchPlan.find(item => item.id === "recursive-workspace-paths");
expect(entry?.requirement).toBe("required");
const first = applyRuntimePatchPlan(fixture, [entry!]);
expect(first.reports[0]?.status).toBe("applied");
expect(applyRuntimePatchPlan(first.runtime, [entry!]).reports[0]?.status).toBe("already_present");
expect(() => applyRuntimePatchPlan("incompatible runtime", [entry!]))
.toThrow("Required runtime patch recursive-workspace-paths failed");
});
Loading
Loading