fix: interrupt scaffold drain and desktop CLI bin layout
This commit is contained in:
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
RESOURCES_DIR=$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)
|
||||||
|
|
||||||
|
# macOS: MacOS/Paseo is a sibling of Resources/
|
||||||
|
# Linux: the executable sits next to resources/
|
||||||
|
if [ -x "${RESOURCES_DIR}/../MacOS/Paseo" ]; then
|
||||||
|
APP_EXECUTABLE="${RESOURCES_DIR}/../MacOS/Paseo"
|
||||||
|
elif [ -x "${RESOURCES_DIR}/../Paseo" ]; then
|
||||||
|
APP_EXECUTABLE="${RESOURCES_DIR}/../Paseo"
|
||||||
|
else
|
||||||
|
echo "Bundled Paseo executable not found relative to ${RESOURCES_DIR}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CLI_ENTRY="${RESOURCES_DIR}/app.asar/node_modules/@getpaseo/cli/dist/index.js"
|
||||||
|
|
||||||
|
exec env ELECTRON_RUN_AS_NODE=1 "${APP_EXECUTABLE}" "${CLI_ENTRY}" "$@"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set "SCRIPT_DIR=%~dp0"
|
||||||
|
set "RESOURCES_DIR=%SCRIPT_DIR%.."
|
||||||
|
set "APP_EXECUTABLE=%RESOURCES_DIR%\..\Paseo.exe"
|
||||||
|
set "CLI_ENTRY=%RESOURCES_DIR%\app.asar\node_modules\@getpaseo\cli\dist\index.js"
|
||||||
|
|
||||||
|
if not exist "%APP_EXECUTABLE%" (
|
||||||
|
echo Bundled Paseo executable not found at %APP_EXECUTABLE% 1>&2
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set "ELECTRON_RUN_AS_NODE=1"
|
||||||
|
"%APP_EXECUTABLE%" "%CLI_ENTRY%" %*
|
||||||
@@ -19,26 +19,26 @@ mac:
|
|||||||
notarize: true
|
notarize: true
|
||||||
entitlements: build/entitlements.mac.plist
|
entitlements: build/entitlements.mac.plist
|
||||||
entitlementsInherit: build/entitlements.mac.inherit.plist
|
entitlementsInherit: build/entitlements.mac.inherit.plist
|
||||||
extraFiles:
|
extraResources:
|
||||||
- from: scripts/paseo
|
- from: bin/paseo
|
||||||
to: MacOS/paseo
|
to: bin/paseo
|
||||||
target:
|
target:
|
||||||
- dmg
|
- dmg
|
||||||
- zip
|
- zip
|
||||||
linux:
|
linux:
|
||||||
category: Development
|
category: Development
|
||||||
icon: assets
|
icon: assets
|
||||||
extraFiles:
|
extraResources:
|
||||||
- from: scripts/paseo
|
- from: bin/paseo
|
||||||
to: paseo
|
to: bin/paseo
|
||||||
target:
|
target:
|
||||||
- AppImage
|
- AppImage
|
||||||
- tar.gz
|
- tar.gz
|
||||||
win:
|
win:
|
||||||
icon: assets/icon.ico
|
icon: assets/icon.ico
|
||||||
extraFiles:
|
extraResources:
|
||||||
- from: scripts/paseo.cmd
|
- from: bin/paseo.cmd
|
||||||
to: paseo.cmd
|
to: bin/paseo.cmd
|
||||||
target:
|
target:
|
||||||
- nsis
|
- nsis
|
||||||
- zip
|
- zip
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
||||||
APP_EXECUTABLE="${SCRIPT_DIR}/Paseo"
|
|
||||||
|
|
||||||
if [ -d "${SCRIPT_DIR}/../Resources" ]; then
|
|
||||||
RESOURCES_DIR="${SCRIPT_DIR}/../Resources"
|
|
||||||
elif [ -d "${SCRIPT_DIR}/resources" ]; then
|
|
||||||
RESOURCES_DIR="${SCRIPT_DIR}/resources"
|
|
||||||
else
|
|
||||||
echo "Unable to locate Paseo resources directory next to ${SCRIPT_DIR}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
CLI_ENTRY="${RESOURCES_DIR}/app.asar/node_modules/@getpaseo/cli/dist/index.js"
|
|
||||||
|
|
||||||
if [ ! -x "${APP_EXECUTABLE}" ]; then
|
|
||||||
echo "Bundled Paseo executable not found at ${APP_EXECUTABLE}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "${CLI_ENTRY}" ]; then
|
|
||||||
echo "Bundled Paseo CLI entrypoint not found at ${CLI_ENTRY}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec env ELECTRON_RUN_AS_NODE=1 "${APP_EXECUTABLE}" "${CLI_ENTRY}" "$@"
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal
|
|
||||||
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
|
||||||
set "APP_EXECUTABLE=%SCRIPT_DIR%Paseo.exe"
|
|
||||||
set "CLI_ENTRY=%SCRIPT_DIR%resources\app.asar\node_modules\@getpaseo\cli\dist\index.js"
|
|
||||||
|
|
||||||
if not exist "%APP_EXECUTABLE%" (
|
|
||||||
echo Bundled Paseo executable not found at %APP_EXECUTABLE% 1>&2
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if not exist "%CLI_ENTRY%" (
|
|
||||||
echo Bundled Paseo CLI entrypoint not found at %CLI_ENTRY% 1>&2
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
set "ELECTRON_RUN_AS_NODE=1"
|
|
||||||
"%APP_EXECUTABLE%" "%CLI_ENTRY%" %*
|
|
||||||
@@ -158,6 +158,8 @@ function buildDesktopDaemonCorsOriginsEnv(): string | undefined {
|
|||||||
.filter((value) => value.length > 0)
|
.filter((value) => value.length > 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
origins.add("paseo://app");
|
||||||
|
|
||||||
const devServerUrl = process.env.EXPO_DEV_URL ?? DEFAULT_ELECTRON_DEV_SERVER_URL;
|
const devServerUrl = process.env.EXPO_DEV_URL ?? DEFAULT_ELECTRON_DEV_SERVER_URL;
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(devServerUrl);
|
const parsed = new URL(devServerUrl);
|
||||||
@@ -479,7 +481,7 @@ function getCliSymlinkInstructions(): CliSymlinkInstructions {
|
|||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
const appBundle = electronExePath.replace(/\/Contents\/MacOS\/.+$/, "");
|
const appBundle = electronExePath.replace(/\/Contents\/MacOS\/.+$/, "");
|
||||||
const cliPath = path.join(appBundle, "Contents", "MacOS", cliShimFilename);
|
const cliPath = path.join(appBundle, "Contents", "Resources", "bin", cliShimFilename);
|
||||||
return {
|
return {
|
||||||
title: "Add paseo to your shell",
|
title: "Add paseo to your shell",
|
||||||
detail: "Create a symlink to the bundled Paseo CLI shim.",
|
detail: "Create a symlink to the bundled Paseo CLI shim.",
|
||||||
@@ -488,18 +490,20 @@ function getCliSymlinkInstructions(): CliSymlinkInstructions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
|
const cliPath = path.join(path.dirname(electronExePath), "resources", "bin", cliShimFilename);
|
||||||
return {
|
return {
|
||||||
title: "Add paseo to your PATH",
|
title: "Add paseo to your PATH",
|
||||||
detail: "Add the Paseo installation directory to your system PATH so paseo.cmd is available.",
|
detail: "Add the Paseo installation directory to your system PATH so paseo.cmd is available.",
|
||||||
commands: `setx PATH "%PATH%;${path.dirname(electronExePath)}"`,
|
commands: `setx PATH "%PATH%;${path.dirname(cliPath)}"`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux
|
// Linux
|
||||||
|
const cliPath = path.join(path.dirname(electronExePath), "resources", "bin", cliShimFilename);
|
||||||
return {
|
return {
|
||||||
title: "Add paseo to your shell",
|
title: "Add paseo to your shell",
|
||||||
detail: "Create a symlink to the bundled Paseo CLI shim.",
|
detail: "Create a symlink to the bundled Paseo CLI shim.",
|
||||||
commands: `sudo ln -sf "${path.join(path.dirname(electronExePath), cliShimFilename)}" /usr/local/bin/paseo`,
|
commands: `sudo ln -sf "${cliPath}" /usr/local/bin/paseo`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { app, BrowserWindow, nativeImage } from "electron";
|
import { app, BrowserWindow, nativeImage, net, protocol } from "electron";
|
||||||
import { registerDaemonManager } from "./daemon/daemon-manager.js";
|
import { registerDaemonManager } from "./daemon/daemon-manager.js";
|
||||||
import { parseCliPassthroughArgsFromArgv, runCliPassthroughCommand } from "./daemon/runtime-paths.js";
|
import { parseCliPassthroughArgsFromArgv, runCliPassthroughCommand } from "./daemon/runtime-paths.js";
|
||||||
import { closeAllTransportSessions } from "./daemon/local-transport.js";
|
import { closeAllTransportSessions } from "./daemon/local-transport.js";
|
||||||
@@ -11,8 +12,13 @@ import { registerOpenerHandlers } from "./features/opener.js";
|
|||||||
import { setupApplicationMenu } from "./features/menu.js";
|
import { setupApplicationMenu } from "./features/menu.js";
|
||||||
|
|
||||||
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
||||||
|
const APP_SCHEME = "paseo";
|
||||||
app.setName("Paseo");
|
app.setName("Paseo");
|
||||||
|
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{ scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
||||||
|
]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Window creation
|
// Window creation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -21,12 +27,12 @@ function getPreloadPath(): string {
|
|||||||
return path.join(__dirname, "preload.js");
|
return path.join(__dirname, "preload.js");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductionEntryPath(): string {
|
function getAppDistDir(): string {
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
return path.join(process.resourcesPath, "app-dist", "index.html");
|
return path.join(process.resourcesPath, "app-dist");
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.resolve(__dirname, "../../app/dist/index.html");
|
return path.resolve(__dirname, "../../app/dist");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWindowIconPath(): string | null {
|
function getWindowIconPath(): string | null {
|
||||||
@@ -94,7 +100,7 @@ async function createMainWindow(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await mainWindow.loadFile(getProductionEntryPath());
|
await mainWindow.loadURL(`${APP_SCHEME}://app/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -148,6 +154,34 @@ async function bootstrap(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await app.whenReady();
|
await app.whenReady();
|
||||||
|
|
||||||
|
const appDistDir = getAppDistDir();
|
||||||
|
protocol.handle(APP_SCHEME, (request) => {
|
||||||
|
const { pathname, search, hash } = new URL(request.url);
|
||||||
|
const decodedPath = decodeURIComponent(pathname);
|
||||||
|
|
||||||
|
// Chromium can occasionally request the exported entrypoint directly.
|
||||||
|
// Canonicalize it back to the route URL so Expo Router sees `/`, not `/index.html`.
|
||||||
|
if (decodedPath.endsWith("/index.html")) {
|
||||||
|
const normalizedPath = decodedPath.slice(0, -"/index.html".length) || "/";
|
||||||
|
return Response.redirect(`${APP_SCHEME}://app${normalizedPath}${search}${hash}`, 307);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(appDistDir, decodedPath);
|
||||||
|
const relativePath = path.relative(appDistDir, filePath);
|
||||||
|
|
||||||
|
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPA fallback: serve index.html for routes without a file extension
|
||||||
|
if (!relativePath || !path.extname(relativePath)) {
|
||||||
|
return net.fetch(pathToFileURL(path.join(appDistDir, "index.html")).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.fetch(pathToFileURL(filePath).toString());
|
||||||
|
});
|
||||||
|
|
||||||
applyAppIcon();
|
applyAppIcon();
|
||||||
setupApplicationMenu();
|
setupApplicationMenu();
|
||||||
ensureNotificationCenterRegistration();
|
ensureNotificationCenterRegistration();
|
||||||
|
|||||||
+180
@@ -224,6 +224,16 @@ function collectAssistantText(events: AgentStreamEvent[]): string {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectUserText(events: AgentStreamEvent[]): string {
|
||||||
|
return events
|
||||||
|
.filter(
|
||||||
|
(event): event is Extract<AgentStreamEvent, { type: "timeline" }> =>
|
||||||
|
event.type === "timeline" && event.item.type === "user_message"
|
||||||
|
)
|
||||||
|
.map((event) => event.item.text)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
function createTimedIteratorReader<T>(params: { iterator: AsyncIterator<T> }) {
|
function createTimedIteratorReader<T>(params: { iterator: AsyncIterator<T> }) {
|
||||||
const { iterator } = params;
|
const { iterator } = params;
|
||||||
let pendingNext: Promise<IteratorResult<T>> | null = null;
|
let pendingNext: Promise<IteratorResult<T>> | null = null;
|
||||||
@@ -314,6 +324,176 @@ describe("ClaudeAgentSession interrupt restart regression", () => {
|
|||||||
await session.close();
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("restarts after interrupt scaffold query drain without surfacing placeholder transcript noise", async () => {
|
||||||
|
const logger = createTestLogger();
|
||||||
|
let queryCreateCount = 0;
|
||||||
|
|
||||||
|
sdkMocks.query.mockImplementation(({ prompt }: { prompt: AsyncIterable<unknown> }) => {
|
||||||
|
queryCreateCount += 1;
|
||||||
|
|
||||||
|
if (queryCreateCount === 1) {
|
||||||
|
let step = 0;
|
||||||
|
return {
|
||||||
|
next: vi.fn(async () => {
|
||||||
|
if (step === 0) {
|
||||||
|
step += 1;
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "system",
|
||||||
|
subtype: "init",
|
||||||
|
session_id: "interrupt-scaffold-session",
|
||||||
|
permissionMode: "default",
|
||||||
|
model: "opus",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step === 1) {
|
||||||
|
step += 1;
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "[Request interrupted by user]" }],
|
||||||
|
},
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
uuid: "interrupt-scaffold-1",
|
||||||
|
session_id: "interrupt-scaffold-session",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step === 2) {
|
||||||
|
step += 1;
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
content: "Got it. Sorry for the mess.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step === 3) {
|
||||||
|
step += 1;
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
content: "No response requested.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { done: true, value: undefined };
|
||||||
|
}),
|
||||||
|
interrupt: vi.fn(async () => undefined),
|
||||||
|
return: vi.fn(async () => undefined),
|
||||||
|
setPermissionMode: vi.fn(async () => undefined),
|
||||||
|
setModel: vi.fn(async () => undefined),
|
||||||
|
supportedModels: vi.fn(async () => [{ value: "opus", displayName: "Opus" }]),
|
||||||
|
supportedCommands: vi.fn(async () => []),
|
||||||
|
rewindFiles: vi.fn(async () => ({ canRewind: true })),
|
||||||
|
} satisfies QueryMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readPromptUuid = createPromptUuidReader(prompt);
|
||||||
|
let step = 0;
|
||||||
|
return {
|
||||||
|
next: vi.fn(async () => {
|
||||||
|
if (step === 0) {
|
||||||
|
step += 1;
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "system",
|
||||||
|
subtype: "init",
|
||||||
|
session_id: "interrupt-scaffold-session",
|
||||||
|
permissionMode: "default",
|
||||||
|
model: "opus",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step === 1) {
|
||||||
|
step += 1;
|
||||||
|
const promptUuid = (await readPromptUuid()) ?? "missing-prompt-uuid";
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "user",
|
||||||
|
message: { role: "user", content: "fresh prompt" },
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
uuid: promptUuid,
|
||||||
|
session_id: "interrupt-scaffold-session",
|
||||||
|
isReplay: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step === 2) {
|
||||||
|
step += 1;
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
content: "FRESH_RESPONSE_AFTER_INTERRUPT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (step === 3) {
|
||||||
|
step += 1;
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: {
|
||||||
|
type: "result",
|
||||||
|
subtype: "success",
|
||||||
|
usage: buildUsage(),
|
||||||
|
total_cost_usd: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { done: true, value: undefined };
|
||||||
|
}),
|
||||||
|
interrupt: vi.fn(async () => undefined),
|
||||||
|
return: vi.fn(async () => undefined),
|
||||||
|
setPermissionMode: vi.fn(async () => undefined),
|
||||||
|
setModel: vi.fn(async () => undefined),
|
||||||
|
supportedModels: vi.fn(async () => [{ value: "opus", displayName: "Opus" }]),
|
||||||
|
supportedCommands: vi.fn(async () => []),
|
||||||
|
rewindFiles: vi.fn(async () => ({ canRewind: true })),
|
||||||
|
} satisfies QueryMock;
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new ClaudeAgentClient({ logger });
|
||||||
|
const session = await client.createSession({
|
||||||
|
provider: "claude",
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = await collectUntilTerminal(session.stream("fresh prompt"));
|
||||||
|
const assistantText = collectAssistantText(events);
|
||||||
|
const userText = collectUserText(events);
|
||||||
|
await session.close();
|
||||||
|
|
||||||
|
expect(queryCreateCount).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(assistantText).toContain("FRESH_RESPONSE_AFTER_INTERRUPT");
|
||||||
|
expect(assistantText).not.toContain("Got it. Sorry for the mess.");
|
||||||
|
expect(assistantText).not.toContain("No response requested.");
|
||||||
|
expect(userText).not.toContain("[Request interrupted by user]");
|
||||||
|
expect(
|
||||||
|
events.some(
|
||||||
|
(event) =>
|
||||||
|
event.type === "turn_failed" &&
|
||||||
|
event.error.includes("Claude stream ended before terminal result")
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
expect(events.some((event) => event.type === "turn_completed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("ignores stale interrupted query completion after the replacement run starts", async () => {
|
test("ignores stale interrupted query completion after the replacement run starts", async () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
const releaseOldDone = deferred<void>();
|
const releaseOldDone = deferred<void>();
|
||||||
|
|||||||
@@ -112,6 +112,31 @@ describe("convertClaudeHistoryEntry", () => {
|
|||||||
expect(mapBlocks).not.toHaveBeenCalled();
|
expect(mapBlocks).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("skips interrupt placeholder transcript noise", () => {
|
||||||
|
const interruptEntry = {
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "[Request interrupted by user]" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const assistantNoiseEntry = {
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
role: "assistant",
|
||||||
|
content: "No response requested.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapBlocks = vi.fn().mockReturnValue([
|
||||||
|
{ type: "assistant_message", text: "No response requested." },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(convertClaudeHistoryEntry(interruptEntry, mapBlocks)).toEqual([]);
|
||||||
|
expect(convertClaudeHistoryEntry(assistantNoiseEntry, mapBlocks)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("maps task notifications to synthetic tool calls", () => {
|
test("maps task notifications to synthetic tool calls", () => {
|
||||||
const entry = {
|
const entry = {
|
||||||
type: "system",
|
type: "system",
|
||||||
|
|||||||
@@ -334,6 +334,9 @@ const REWIND_COMMAND: AgentSlashCommand = {
|
|||||||
argumentHint: "[user_message_uuid]",
|
argumentHint: "[user_message_uuid]",
|
||||||
};
|
};
|
||||||
const INTERRUPT_TOOL_USE_PLACEHOLDER = "[Request interrupted by user for tool use]";
|
const INTERRUPT_TOOL_USE_PLACEHOLDER = "[Request interrupted by user for tool use]";
|
||||||
|
const INTERRUPT_PLACEHOLDER_PATTERN =
|
||||||
|
/^\[Request interrupted by user(?:[^\]]*)\]$/;
|
||||||
|
const NO_RESPONSE_REQUESTED_PLACEHOLDER = "No response requested.";
|
||||||
const UUID_PATTERN =
|
const UUID_PATTERN =
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
@@ -584,11 +587,88 @@ function coerceToolResultContentToString(content: unknown): string {
|
|||||||
return deterministicStringify(content);
|
return deterministicStringify(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeClaudeTranscriptText(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = value.trim();
|
||||||
|
return normalized.length > 0 ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClaudeInterruptPlaceholderText(value: unknown): boolean {
|
||||||
|
const normalized = normalizeClaudeTranscriptText(value);
|
||||||
|
return normalized !== null && INTERRUPT_PLACEHOLDER_PATTERN.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClaudeNoResponsePlaceholderText(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
normalizeClaudeTranscriptText(value) === NO_RESPONSE_REQUESTED_PLACEHOLDER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClaudeTranscriptNoiseText(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
isClaudeInterruptPlaceholderText(value) ||
|
||||||
|
isClaudeNoResponsePlaceholderText(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectClaudeTextContentParts(content: unknown): string[] {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
const normalized = normalizeClaudeTranscriptText(content);
|
||||||
|
return normalized ? [normalized] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const block of content) {
|
||||||
|
if (!block || typeof block !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const text = normalizeClaudeTranscriptText(
|
||||||
|
(block as { text?: unknown }).text
|
||||||
|
);
|
||||||
|
if (text) {
|
||||||
|
parts.push(text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const input = normalizeClaudeTranscriptText(
|
||||||
|
(block as { input?: unknown }).input
|
||||||
|
);
|
||||||
|
if (input) {
|
||||||
|
parts.push(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClaudeInterruptScaffoldContent(content: unknown): boolean {
|
||||||
|
const parts = collectClaudeTextContentParts(content);
|
||||||
|
return (
|
||||||
|
parts.length > 0 &&
|
||||||
|
parts.every((part) => isClaudeInterruptPlaceholderText(part))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClaudeTranscriptNoiseContent(content: unknown): boolean {
|
||||||
|
const parts = collectClaudeTextContentParts(content);
|
||||||
|
return (
|
||||||
|
parts.length > 0 &&
|
||||||
|
parts.every((part) => isClaudeTranscriptNoiseText(part))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function extractUserMessageText(content: unknown): string | null {
|
export function extractUserMessageText(content: unknown): string | null {
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
const normalized = content.trim();
|
const normalized = content.trim();
|
||||||
return normalized.length > 0 ? normalized : null;
|
if (!normalized || isClaudeTranscriptNoiseText(normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(content)) {
|
if (!Array.isArray(content)) {
|
||||||
@@ -602,12 +682,18 @@ export function extractUserMessageText(content: unknown): string | null {
|
|||||||
}
|
}
|
||||||
const text = typeof block.text === "string" ? block.text : undefined;
|
const text = typeof block.text === "string" ? block.text : undefined;
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
parts.push(text.trim());
|
const trimmed = text.trim();
|
||||||
|
if (!isClaudeTranscriptNoiseText(trimmed)) {
|
||||||
|
parts.push(trimmed);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const input = typeof block.input === "string" ? block.input : undefined;
|
const input = typeof block.input === "string" ? block.input : undefined;
|
||||||
if (input && input.trim()) {
|
if (input && input.trim()) {
|
||||||
parts.push(input.trim());
|
const trimmed = input.trim();
|
||||||
|
if (!isClaudeTranscriptNoiseText(trimmed)) {
|
||||||
|
parts.push(trimmed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,7 +1294,8 @@ class TimelineAssembler {
|
|||||||
const nextAssistantText = state.assistantText.slice(state.emittedAssistantLength);
|
const nextAssistantText = state.assistantText.slice(state.emittedAssistantLength);
|
||||||
if (
|
if (
|
||||||
nextAssistantText.length > 0 &&
|
nextAssistantText.length > 0 &&
|
||||||
nextAssistantText !== INTERRUPT_TOOL_USE_PLACEHOLDER
|
nextAssistantText !== INTERRUPT_TOOL_USE_PLACEHOLDER &&
|
||||||
|
!isClaudeTranscriptNoiseText(nextAssistantText)
|
||||||
) {
|
) {
|
||||||
state.emittedAssistantLength = state.assistantText.length;
|
state.emittedAssistantLength = state.assistantText.length;
|
||||||
items.push({ type: "assistant_message", text: nextAssistantText });
|
items.push({ type: "assistant_message", text: nextAssistantText });
|
||||||
@@ -1332,12 +1419,21 @@ function isMetadataOnlySdkMessage(message: SDKMessage): boolean {
|
|||||||
if (message.type === "system") {
|
if (message.type === "system") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
message.type === "assistant" &&
|
||||||
|
isClaudeTranscriptNoiseContent(message.message?.content)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (message.type !== "user") {
|
if (message.type !== "user") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isSyntheticUserEntry(message)) {
|
if (isSyntheticUserEntry(message)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (isClaudeInterruptScaffoldContent(message.message?.content)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return isTaskNotificationUserContent(message.message?.content);
|
return isTaskNotificationUserContent(message.message?.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1491,6 +1587,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
private toolUseInputBuffers = new Map<string, string>();
|
private toolUseInputBuffers = new Map<string, string>();
|
||||||
private pendingPermissions = new Map<string, PendingPermission>();
|
private pendingPermissions = new Map<string, PendingPermission>();
|
||||||
private activeForegroundTurn: ForegroundTurnState | null = null;
|
private activeForegroundTurn: ForegroundTurnState | null = null;
|
||||||
|
private activeForegroundPrompt: SDKUserMessage | null = null;
|
||||||
private liveEventQueue = new Pushable<AgentStreamEvent>();
|
private liveEventQueue = new Pushable<AgentStreamEvent>();
|
||||||
private readonly runTracker = new RunTracker();
|
private readonly runTracker = new RunTracker();
|
||||||
private readonly timelineAssembler = new TimelineAssembler();
|
private readonly timelineAssembler = new TimelineAssembler();
|
||||||
@@ -1501,6 +1598,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
private historyLineFragment = "";
|
private historyLineFragment = "";
|
||||||
private turnState: TurnState = "idle";
|
private turnState: TurnState = "idle";
|
||||||
private preReplayMetadataSeen = false;
|
private preReplayMetadataSeen = false;
|
||||||
|
private preReplayDoneRecoveryUsed = false;
|
||||||
private pendingAutonomousWakeReservations = 0;
|
private pendingAutonomousWakeReservations = 0;
|
||||||
private nextRunOrdinal = 1;
|
private nextRunOrdinal = 1;
|
||||||
private cancelCurrentTurn: (() => void) | null = null;
|
private cancelCurrentTurn: (() => void) | null = null;
|
||||||
@@ -1627,6 +1725,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
this.suppressLocalReplayActivity = false;
|
this.suppressLocalReplayActivity = false;
|
||||||
this.pendingAutonomousWakeReservations = 0;
|
this.pendingAutonomousWakeReservations = 0;
|
||||||
|
this.preReplayDoneRecoveryUsed = false;
|
||||||
|
|
||||||
const slashCommand = this.resolveSlashCommandInvocation(prompt);
|
const slashCommand = this.resolveSlashCommandInvocation(prompt);
|
||||||
if (slashCommand?.commandName === REWIND_COMMAND_NAME) {
|
if (slashCommand?.commandName === REWIND_COMMAND_NAME) {
|
||||||
@@ -1655,6 +1754,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
queue,
|
queue,
|
||||||
};
|
};
|
||||||
this.activeForegroundTurn = foregroundTurn;
|
this.activeForegroundTurn = foregroundTurn;
|
||||||
|
this.activeForegroundPrompt = sdkMessage;
|
||||||
this.preReplayMetadataSeen = false;
|
this.preReplayMetadataSeen = false;
|
||||||
this.transitionTurnState("foreground", "foreground stream started");
|
this.transitionTurnState("foreground", "foreground stream started");
|
||||||
this.clearRecentStderr();
|
this.clearRecentStderr();
|
||||||
@@ -1672,6 +1772,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
cancelIssued = true;
|
cancelIssued = true;
|
||||||
if (this.activeForegroundTurn?.runId === run.id) {
|
if (this.activeForegroundTurn?.runId === run.id) {
|
||||||
this.activeForegroundTurn = null;
|
this.activeForegroundTurn = null;
|
||||||
|
this.activeForegroundPrompt = null;
|
||||||
}
|
}
|
||||||
if (this.cancelCurrentTurn === requestCancel) {
|
if (this.cancelCurrentTurn === requestCancel) {
|
||||||
this.cancelCurrentTurn = null;
|
this.cancelCurrentTurn = null;
|
||||||
@@ -1925,6 +2026,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.cancelCurrentTurn?.();
|
this.cancelCurrentTurn?.();
|
||||||
this.activeForegroundTurn?.queue.end();
|
this.activeForegroundTurn?.queue.end();
|
||||||
this.activeForegroundTurn = null;
|
this.activeForegroundTurn = null;
|
||||||
|
this.activeForegroundPrompt = null;
|
||||||
this.cancelCurrentTurn = null;
|
this.cancelCurrentTurn = null;
|
||||||
this.turnState = "idle";
|
this.turnState = "idle";
|
||||||
this.suppressLocalReplayActivity = false;
|
this.suppressLocalReplayActivity = false;
|
||||||
@@ -2533,7 +2635,9 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
if (this.activeForegroundTurn?.runId === run.id) {
|
if (this.activeForegroundTurn?.runId === run.id) {
|
||||||
this.activeForegroundTurn = null;
|
this.activeForegroundTurn = null;
|
||||||
|
this.activeForegroundPrompt = null;
|
||||||
this.preReplayMetadataSeen = false;
|
this.preReplayMetadataSeen = false;
|
||||||
|
this.preReplayDoneRecoveryUsed = false;
|
||||||
}
|
}
|
||||||
this.logger.trace(
|
this.logger.trace(
|
||||||
{
|
{
|
||||||
@@ -2930,6 +3034,31 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.input = null;
|
this.input = null;
|
||||||
}
|
}
|
||||||
const activeRuns = this.runTracker.listActiveRuns();
|
const activeRuns = this.runTracker.listActiveRuns();
|
||||||
|
if (this.shouldRecoverFromPreReplayQueryDone(activeRuns)) {
|
||||||
|
this.logger.warn(
|
||||||
|
{
|
||||||
|
claudeSessionId: this.claudeSessionId,
|
||||||
|
activeRunCount: activeRuns.length,
|
||||||
|
preReplayMetadataSeen: this.preReplayMetadataSeen,
|
||||||
|
},
|
||||||
|
"Claude query ended before prompt replay after interrupt scaffold; retrying fresh query"
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await this.ensureQuery();
|
||||||
|
if (this.input && this.activeForegroundPrompt) {
|
||||||
|
this.input.push(this.activeForegroundPrompt);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
for (const run of activeRuns) {
|
||||||
|
this.failRun(
|
||||||
|
run,
|
||||||
|
error instanceof Error ? error.message : "Claude stream failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.waitForLiveHistoryPoll();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (activeRuns.length > 0) {
|
if (activeRuns.length > 0) {
|
||||||
for (const run of activeRuns) {
|
for (const run of activeRuns) {
|
||||||
this.failRun(run, "Claude stream ended before terminal result");
|
this.failRun(run, "Claude stream ended before terminal result");
|
||||||
@@ -3133,6 +3262,28 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldRecoverFromPreReplayQueryDone(activeRuns: RunRecord[]): boolean {
|
||||||
|
if (
|
||||||
|
this.preReplayDoneRecoveryUsed ||
|
||||||
|
!this.preReplayMetadataSeen ||
|
||||||
|
!this.activeForegroundTurn ||
|
||||||
|
activeRuns.length !== 1
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foregroundRun = this.runTracker.getRun(this.activeForegroundTurn.runId);
|
||||||
|
if (!foregroundRun || activeRuns[0]?.id !== foregroundRun.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (foregroundRun.owner !== "foreground" || foregroundRun.promptReplaySeen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.preReplayDoneRecoveryUsed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private updateRunLifecycleForMessage(
|
private updateRunLifecycleForMessage(
|
||||||
run: RunRecord,
|
run: RunRecord,
|
||||||
message: SDKMessage,
|
message: SDKMessage,
|
||||||
@@ -3146,6 +3297,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
) {
|
) {
|
||||||
run.promptReplaySeen = true;
|
run.promptReplaySeen = true;
|
||||||
this.preReplayMetadataSeen = false;
|
this.preReplayMetadataSeen = false;
|
||||||
|
this.preReplayDoneRecoveryUsed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (run.state === "queued") {
|
if (run.state === "queued") {
|
||||||
@@ -3185,11 +3337,22 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldSuppressLocalReplayActivity(message: SDKMessage): boolean {
|
private shouldSuppressLocalReplayActivity(message: SDKMessage): boolean {
|
||||||
const localReplay = this.isLocalReplayUserMessage(message);
|
const replayUuid = this.getLocalReplayUserMessageUuid(message);
|
||||||
if (!this.activeForegroundTurn && localReplay) {
|
if (replayUuid) {
|
||||||
|
// Don't suppress the echo of the current foreground turn's prompt —
|
||||||
|
// updateRunLifecycleForMessage needs it to set promptReplaySeen.
|
||||||
|
if (this.activeForegroundTurn) {
|
||||||
|
const foregroundRun = this.runTracker.getRun(
|
||||||
|
this.activeForegroundTurn.runId
|
||||||
|
);
|
||||||
|
if (foregroundRun?.messageIds.has(replayUuid)) {
|
||||||
|
this.suppressLocalReplayActivity = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.suppressLocalReplayActivity = true;
|
this.suppressLocalReplayActivity = true;
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
{ uuid: (message as { uuid?: string }).uuid },
|
{ uuid: replayUuid },
|
||||||
"Suppressing local replay user message from live pump"
|
"Suppressing local replay user message from live pump"
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -3201,7 +3364,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
// Suppress only replay scaffolding. Do not suppress autonomous
|
// Suppress only replay scaffolding. Do not suppress autonomous
|
||||||
// assistant/result events; otherwise task-notification replies can be dropped.
|
// assistant/result events; otherwise task-notification replies can be dropped.
|
||||||
if (localReplay) {
|
if (replayUuid) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3231,17 +3394,17 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLocalReplayUserMessage(message: SDKMessage): boolean {
|
private getLocalReplayUserMessageUuid(message: SDKMessage): string | null {
|
||||||
if (message.type !== "user") {
|
if (message.type !== "user") {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
const uuid = readTrimmedString(
|
const uuid = readTrimmedString(
|
||||||
(message as unknown as { uuid?: unknown }).uuid
|
(message as unknown as { uuid?: unknown }).uuid
|
||||||
);
|
);
|
||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
return this.localUserMessageIds.has(uuid);
|
return this.localUserMessageIds.has(uuid) ? uuid : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async interruptActiveTurn(): Promise<void> {
|
private async interruptActiveTurn(): Promise<void> {
|
||||||
@@ -3652,15 +3815,17 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
if (typeof content === "string" && content.length > 0) {
|
if (typeof content === "string" && content.length > 0) {
|
||||||
// String content from user messages (e.g., local command output)
|
// String content from user messages (e.g., local command output)
|
||||||
events.push({
|
if (!isClaudeTranscriptNoiseText(content)) {
|
||||||
type: "timeline",
|
events.push({
|
||||||
item: {
|
type: "timeline",
|
||||||
type: "user_message",
|
item: {
|
||||||
text: content,
|
type: "user_message",
|
||||||
...(messageId ? { messageId } : {}),
|
text: content,
|
||||||
},
|
...(messageId ? { messageId } : {}),
|
||||||
provider: "claude",
|
},
|
||||||
});
|
provider: "claude",
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (Array.isArray(content)) {
|
} else if (Array.isArray(content)) {
|
||||||
// User SDK entries with array content (e.g. interrupt messages, replayed JSONL entries)
|
// User SDK entries with array content (e.g. interrupt messages, replayed JSONL entries)
|
||||||
// must declare textMessageType so mapBlocksToTimeline emits user_message, not assistant_message.
|
// must declare textMessageType so mapBlocksToTimeline emits user_message, not assistant_message.
|
||||||
@@ -4210,7 +4375,11 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
const suppressReasoning = options?.suppressReasoning ?? false;
|
const suppressReasoning = options?.suppressReasoning ?? false;
|
||||||
|
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) {
|
if (
|
||||||
|
!content ||
|
||||||
|
content === INTERRUPT_TOOL_USE_PLACEHOLDER ||
|
||||||
|
isClaudeTranscriptNoiseText(content)
|
||||||
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (suppressText) {
|
if (suppressText) {
|
||||||
@@ -4226,7 +4395,11 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case "text":
|
case "text":
|
||||||
case "text_delta":
|
case "text_delta":
|
||||||
if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
|
if (
|
||||||
|
block.text &&
|
||||||
|
block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER &&
|
||||||
|
!isClaudeTranscriptNoiseText(block.text)
|
||||||
|
) {
|
||||||
if (textMessageType === "user_message") {
|
if (textMessageType === "user_message") {
|
||||||
const trimmed = block.text.trim();
|
const trimmed = block.text.trim();
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
@@ -4802,6 +4975,12 @@ export function convertClaudeHistoryEntry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
|
if (
|
||||||
|
(entry.type === "user" || entry.type === "assistant") &&
|
||||||
|
isClaudeTranscriptNoiseContent(content)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const normalizedBlocks = normalizeHistoryBlocks(content);
|
const normalizedBlocks = normalizeHistoryBlocks(content);
|
||||||
const contentValue =
|
const contentValue =
|
||||||
typeof content === "string"
|
typeof content === "string"
|
||||||
@@ -5040,15 +5219,24 @@ function extractClaudeUserText(message: any): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (typeof message.content === "string") {
|
if (typeof message.content === "string") {
|
||||||
return message.content.trim();
|
const normalized = message.content.trim();
|
||||||
|
return normalized && !isClaudeTranscriptNoiseText(normalized)
|
||||||
|
? normalized
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
if (typeof message.text === "string") {
|
if (typeof message.text === "string") {
|
||||||
return message.text.trim();
|
const normalized = message.text.trim();
|
||||||
|
return normalized && !isClaudeTranscriptNoiseText(normalized)
|
||||||
|
? normalized
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
if (Array.isArray(message.content)) {
|
if (Array.isArray(message.content)) {
|
||||||
for (const block of message.content) {
|
for (const block of message.content) {
|
||||||
if (block && typeof block.text === "string") {
|
if (block && typeof block.text === "string") {
|
||||||
return block.text.trim();
|
const normalized = block.text.trim();
|
||||||
|
if (normalized && !isClaudeTranscriptNoiseText(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user