feat(core): recover truncated tool output via the Session scratchpad (#145)

Co-authored-by: Yaowei Zheng <hiyouga@buaa.edu.cn>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jingzhe Xu
2026-08-03 13:17:42 +08:00
committed by GitHub
parent 84949882ab
commit a798bca87f
30 changed files with 1393 additions and 47 deletions
+23 -16
View File
@@ -87,15 +87,17 @@ const MAX_PATH_LEN = 512;
* the Workspace root (used for file-card display and stat lookups). Returns
* null (no card rendered) for anything that can't be resolved into the
* current Workspace:
* - An absolute path is stripped only when prefixed with
* `${workspace}${sep}` (assistants commonly report absolute paths); if it
* equals the workspace itself or the prefix doesn't match → null. A
* Windows deployment's Workspace (core supports win32) uses backslash
* paths: the prefix is joined with its own separator, and the stripped
* relative segment is normalized to "/" (the browser-side directory
* navigation splits on "/"). Conversion only happens on a matched Windows
* prefix — backslash is a legal character in POSIX filenames, so no
* global replacement is done;
* - An absolute path is stripped only when prefixed with the Workspace
* (assistants commonly report absolute paths); if it equals the workspace
* itself or the prefix doesn't match → null. A Windows deployment's
* Workspace (core supports win32) may be reported with backslashes
* (path.join) while the assistant spells the same path with forward
* slashes (core's model-visible spelling) or mixes both — so when the
* Workspace itself looks like a Windows path, both sides are compared on
* "/" with a case-insensitive drive letter, and the stripped relative
* segment uses "/" (the browser-side directory navigation splits on
* "/"). A POSIX Workspace never converts anything — backslash is a legal
* character in POSIX filenames, so no global replacement is done;
* - A path starting with `~` (home directory) can't be resolved → null;
* - A relative path is lexically normalized by splitting on "/": drop "."
* and empty segments, pop the stack on "..", and return null if popping
@@ -106,14 +108,19 @@ export function toWorkspaceRelative(path: string, workspace: string | null): str
if (s.length === 0 || s.length > MAX_PATH_LEN) return null;
if (s.startsWith("~")) return null;
const ws = workspace !== null && workspace.length > 0 ? workspace : null;
const winWs = ws?.includes("\\") ?? false;
const sep = winWs ? "\\" : "/";
const absolute = s.startsWith("/") || (winWs && (/^[A-Za-z]:/.test(s) || s.startsWith("\\")));
let rel = s;
const winWs = ws !== null && (ws.includes("\\") || /^[A-Za-z]:/.test(ws));
const normalize = (p: string): string => {
if (!winWs) return p;
const slashed = p.replaceAll("\\", "/");
return /^[A-Za-z]:/.test(slashed) ? slashed[0]!.toLowerCase() + slashed.slice(1) : slashed;
};
const normalizedInput = normalize(s);
const normalizedWs = ws === null ? null : normalize(ws);
const absolute = normalizedInput.startsWith("/") || (winWs && /^[a-z]:/.test(normalizedInput));
let rel = normalizedInput;
if (absolute) {
if (ws === null || !s.startsWith(`${ws}${sep}`)) return null;
rel = s.slice(ws.length + sep.length);
if (sep === "\\") rel = rel.replaceAll("\\", "/");
if (normalizedWs === null || !normalizedInput.startsWith(`${normalizedWs}/`)) return null;
rel = normalizedInput.slice(normalizedWs.length + 1);
}
const stack: string[] = [];
for (const seg of rel.split("/")) {
+18
View File
@@ -80,6 +80,24 @@ describe("toWorkspaceRelative", () => {
expect(toWorkspaceRelative("C:\\Users\\me\\ws", win)).toBe(null);
});
it("Windows Workspace: forward-slash and mixed spellings of the same path also match", () => {
const win = "C:\\Users\\me\\ws";
// Core's model-visible spelling uses forward slashes on Windows; the card must still strip.
expect(toWorkspaceRelative("C:/Users/me/ws/sub/a.txt", win)).toBe("sub/a.txt");
expect(toWorkspaceRelative("C:/Users/me/ws\\sub/a.txt", win)).toBe("sub/a.txt");
// Drive letters are case-insensitive on Windows; the rest of the path is not.
expect(toWorkspaceRelative("c:/Users/me/ws/sub/a.txt", win)).toBe("sub/a.txt");
expect(toWorkspaceRelative("C:/Users/ME/ws/sub/a.txt", win)).toBe(null);
// A forward-slash Workspace value matches a backslash assistant path too.
expect(toWorkspaceRelative("C:\\Users\\me\\ws\\sub\\a.txt", "C:/Users/me/ws")).toBe(
"sub/a.txt",
);
});
it("Windows Workspace: relative backslash paths split into segments", () => {
expect(toWorkspaceRelative("dir\\name.txt", "C:\\Users\\me\\ws")).toBe("dir/name.txt");
});
it("backslashes in POSIX filenames are not globally replaced (converted only on a Windows prefix match)", () => {
expect(toWorkspaceRelative("dir\\name.txt", WS)).toBe("dir\\name.txt");
});