Update files
This commit is contained in:
@@ -363,6 +363,7 @@ export function AgentStreamView({
|
|||||||
return (
|
return (
|
||||||
<ToolCall
|
<ToolCall
|
||||||
toolName={data.name}
|
toolName={data.name}
|
||||||
|
provider={data.provider}
|
||||||
args={data.input}
|
args={data.input}
|
||||||
result={data.result}
|
result={data.result}
|
||||||
error={data.error}
|
error={data.error}
|
||||||
@@ -907,8 +908,12 @@ function PermissionRequestCard({
|
|||||||
if (isPlanRequest) {
|
if (isPlanRequest) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parseToolCallDisplay({ name: request.name ?? "unknown", input: request.input });
|
return parseToolCallDisplay({
|
||||||
}, [isPlanRequest, request.name, request.input]);
|
name: request.name ?? "unknown",
|
||||||
|
provider: request.provider,
|
||||||
|
input: request.input,
|
||||||
|
});
|
||||||
|
}, [isPlanRequest, request.name, request.provider, request.input]);
|
||||||
|
|
||||||
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||||
|
|
||||||
|
|||||||
@@ -420,7 +420,6 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: 100,
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1017,7 +1016,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
shimmer.value = -1;
|
shimmer.value = -1;
|
||||||
shimmer.value = withRepeat(
|
shimmer.value = withRepeat(
|
||||||
withTiming(1, { duration: 2400, easing: Easing.bezier(0.4, 0, 0.6, 1) }),
|
withTiming(1, { duration: 3200, easing: Easing.linear }),
|
||||||
-1
|
-1
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -1026,7 +1025,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
}
|
}
|
||||||
}, [isLoading]);
|
}, [isLoading]);
|
||||||
|
|
||||||
const shimmerBandWidth = 100;
|
const shimmerBandWidth = 14;
|
||||||
const shimmerStyle = useAnimatedStyle(() => {
|
const shimmerStyle = useAnimatedStyle(() => {
|
||||||
const travel = badgeWidth + shimmerBandWidth;
|
const travel = badgeWidth + shimmerBandWidth;
|
||||||
return {
|
return {
|
||||||
@@ -1098,7 +1097,11 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
{isLoading && badgeWidth > 0 ? (
|
{isLoading && badgeWidth > 0 ? (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
style={[expandableBadgeStylesheet.shimmerOverlay, shimmerStyle]}
|
style={[
|
||||||
|
expandableBadgeStylesheet.shimmerOverlay,
|
||||||
|
{ width: shimmerBandWidth },
|
||||||
|
shimmerStyle,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
||||||
<Defs>
|
<Defs>
|
||||||
@@ -1109,10 +1112,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
x2="1"
|
x2="1"
|
||||||
y2="0"
|
y2="0"
|
||||||
>
|
>
|
||||||
<Stop offset="0" stopColor={theme.colors.surface1} stopOpacity="0" />
|
<Stop offset="0" stopColor={baseColors.white} stopOpacity="0" />
|
||||||
<Stop offset="0.35" stopColor={theme.colors.surface1} stopOpacity="1" />
|
<Stop offset="0.34" stopColor={baseColors.white} stopOpacity="0" />
|
||||||
<Stop offset="0.65" stopColor={theme.colors.surface1} stopOpacity="1" />
|
<Stop offset="0.46" stopColor={baseColors.white} stopOpacity="0.12" />
|
||||||
<Stop offset="1" stopColor={theme.colors.surface1} stopOpacity="0" />
|
<Stop offset="0.5" stopColor={baseColors.white} stopOpacity="0.34" />
|
||||||
|
<Stop offset="0.54" stopColor={baseColors.white} stopOpacity="0.12" />
|
||||||
|
<Stop offset="0.66" stopColor={baseColors.white} stopOpacity="0" />
|
||||||
|
<Stop offset="1" stopColor={baseColors.white} stopOpacity="0" />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Defs>
|
</Defs>
|
||||||
<Rect width="100%" height="100%" fill="url(#shimmerGrad)" />
|
<Rect width="100%" height="100%" fill="url(#shimmerGrad)" />
|
||||||
@@ -1149,6 +1155,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
|
|
||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
|
provider?: string;
|
||||||
args: any;
|
args: any;
|
||||||
result?: any;
|
result?: any;
|
||||||
error?: any;
|
error?: any;
|
||||||
@@ -1176,6 +1183,7 @@ const TOOL_CALL_COMMIT_THRESHOLD_MS = 16;
|
|||||||
|
|
||||||
export const ToolCall = memo(function ToolCall({
|
export const ToolCall = memo(function ToolCall({
|
||||||
toolName,
|
toolName,
|
||||||
|
provider,
|
||||||
args,
|
args,
|
||||||
result,
|
result,
|
||||||
error,
|
error,
|
||||||
@@ -1197,8 +1205,17 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
UnistylesRuntime.breakpoint === "sm";
|
UnistylesRuntime.breakpoint === "sm";
|
||||||
|
|
||||||
const displayInfo = useMemo(
|
const displayInfo = useMemo(
|
||||||
() => parseToolCallDisplay({ name: toolName, input: args, output: result, error, metadata, cwd }),
|
() =>
|
||||||
[toolName, args, result, error, metadata, cwd]
|
parseToolCallDisplay({
|
||||||
|
name: toolName,
|
||||||
|
provider,
|
||||||
|
input: args,
|
||||||
|
output: result,
|
||||||
|
error,
|
||||||
|
metadata,
|
||||||
|
cwd,
|
||||||
|
}),
|
||||||
|
[toolName, provider, args, result, error, metadata, cwd]
|
||||||
);
|
);
|
||||||
const { kind, displayName, summary, detail, errorText } = displayInfo;
|
const { kind, displayName, summary, detail, errorText } = displayInfo;
|
||||||
const IconComponent = toolKindIcons[kind] || Wrench;
|
const IconComponent = toolKindIcons[kind] || Wrench;
|
||||||
|
|||||||
@@ -384,6 +384,54 @@ function testToolCallParsedPayloadHydration() {
|
|||||||
assert.ok(commandPass, 'Command payload should persist across hydration');
|
assert.ok(commandPass, 'Command payload should persist across hydration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testNullToolPayloadDoesNotEraseKnownInput() {
|
||||||
|
const timestampStart = new Date("2025-01-01T10:36:00Z");
|
||||||
|
const timestampFinish = new Date("2025-01-01T10:36:05Z");
|
||||||
|
const callId = "null-input-preserve";
|
||||||
|
|
||||||
|
const updates: Array<{ event: AgentStreamEventPayload; timestamp: Date }> = [
|
||||||
|
{
|
||||||
|
event: {
|
||||||
|
type: "timeline",
|
||||||
|
provider: "claude",
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
name: "shell",
|
||||||
|
status: "pending",
|
||||||
|
callId,
|
||||||
|
input: { command: "pwd" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestamp: timestampStart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: {
|
||||||
|
type: "timeline",
|
||||||
|
provider: "claude",
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
name: "shell",
|
||||||
|
status: "completed",
|
||||||
|
callId,
|
||||||
|
input: null,
|
||||||
|
output: { type: "command", output: "/tmp" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestamp: timestampFinish,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const state = hydrateStreamState(updates);
|
||||||
|
const commandEntry = state.find(
|
||||||
|
(item): item is AgentToolCallItem =>
|
||||||
|
isAgentToolCallItem(item) && item.payload.data.callId === callId
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(commandEntry, "Tool call should exist");
|
||||||
|
assert.strictEqual(commandEntry.payload.data.status, "completed");
|
||||||
|
assert.deepStrictEqual(commandEntry.payload.data.input, { command: "pwd" });
|
||||||
|
}
|
||||||
|
|
||||||
function buildClaudeToolUseBlock({
|
function buildClaudeToolUseBlock({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -1116,6 +1164,7 @@ describe('stream timeline reducers', () => {
|
|||||||
it('infers failure from error payloads', testToolCallFailureInferenceFromError);
|
it('infers failure from error payloads', testToolCallFailureInferenceFromError);
|
||||||
it('reconciles late call IDs against pending entries', testToolCallLateCallIdReconciliation);
|
it('reconciles late call IDs against pending entries', testToolCallLateCallIdReconciliation);
|
||||||
it('persists parsed read/edit/command payloads after hydration', testToolCallParsedPayloadHydration);
|
it('persists parsed read/edit/command payloads after hydration', testToolCallParsedPayloadHydration);
|
||||||
|
it('preserves known input when later tool updates send null input', testNullToolPayloadDoesNotEraseKnownInput);
|
||||||
it('hydrates Claude tool bodies with parsed content', testClaudeHydratedToolBodies);
|
it('hydrates Claude tool bodies with parsed content', testClaudeHydratedToolBodies);
|
||||||
it('preserves whitespace in assistant chunk concatenation', testAssistantWhitespacePreservation);
|
it('preserves whitespace in assistant chunk concatenation', testAssistantWhitespacePreservation);
|
||||||
it('hydrates user messages and deduplicates optimistic/live entries', testUserMessageHydration);
|
it('hydrates user messages and deduplicates optimistic/live entries', testUserMessageHydration);
|
||||||
|
|||||||
@@ -431,15 +431,15 @@ function appendAgentToolCall(
|
|||||||
const next = [...state];
|
const next = [...state];
|
||||||
const existing = next[existingIndex] as AgentToolCallItem;
|
const existing = next[existingIndex] as AgentToolCallItem;
|
||||||
const mergedInput =
|
const mergedInput =
|
||||||
payloadData.input !== undefined
|
hasValue(payloadData.input)
|
||||||
? payloadData.input
|
? payloadData.input
|
||||||
: existing.payload.data.input;
|
: existing.payload.data.input;
|
||||||
const mergedResult =
|
const mergedResult =
|
||||||
payloadData.result !== undefined
|
hasValue(payloadData.result)
|
||||||
? payloadData.result
|
? payloadData.result
|
||||||
: existing.payload.data.result;
|
: existing.payload.data.result;
|
||||||
const mergedError =
|
const mergedError =
|
||||||
payloadData.error !== undefined
|
hasValue(payloadData.error)
|
||||||
? payloadData.error
|
? payloadData.error
|
||||||
: existing.payload.data.error;
|
: existing.payload.data.error;
|
||||||
const mergedStatus = mergeToolCallStatus(
|
const mergedStatus = mergeToolCallStatus(
|
||||||
|
|||||||
@@ -64,6 +64,35 @@ describe("parseToolCallDisplay", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("falls back to output command when shell input is missing", () => {
|
||||||
|
const output = { type: "command", command: "pwd", output: "/some/path" };
|
||||||
|
|
||||||
|
const info: ToolCallDisplayInfo = parseToolCallDisplay({ name: "Bash", output });
|
||||||
|
expect(info.summary).toBe("pwd");
|
||||||
|
expect(info.detail.type).toBe("shell");
|
||||||
|
if (info.detail.type === "shell") {
|
||||||
|
expect(info.detail.command).toBe("pwd");
|
||||||
|
expect(info.detail.output).toBe("/some/path");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to read output path when input is missing", () => {
|
||||||
|
const info: ToolCallDisplayInfo = parseToolCallDisplay({
|
||||||
|
name: "Read",
|
||||||
|
output: {
|
||||||
|
type: "file_read",
|
||||||
|
filePath: "/some/file.txt",
|
||||||
|
content: "hello",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(info.summary).toBe("/some/file.txt");
|
||||||
|
expect(info.detail.type).toBe("read");
|
||||||
|
if (info.detail.type === "read") {
|
||||||
|
expect(info.detail.filePath).toBe("/some/file.txt");
|
||||||
|
expect(info.detail.content).toBe("hello");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("strips shell + cd wrapper from command (Codex exec_command style)", () => {
|
test("strips shell + cd wrapper from command (Codex exec_command style)", () => {
|
||||||
const input = {
|
const input = {
|
||||||
command:
|
command:
|
||||||
@@ -110,6 +139,14 @@ describe("parseToolCallDisplay", () => {
|
|||||||
expect(info.displayName).toBe("Read");
|
expect(info.displayName).toBe("Read");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("uses stable label for Edit in frontend", () => {
|
||||||
|
const input = { file_path: "/some/file.txt", old_string: "a", new_string: "b" };
|
||||||
|
const first = parseToolCallDisplay({ name: "Edit", input });
|
||||||
|
const second = parseToolCallDisplay({ name: "Edit", input });
|
||||||
|
expect(first.displayName).toBe("Edit");
|
||||||
|
expect(second.displayName).toBe("Edit");
|
||||||
|
});
|
||||||
|
|
||||||
test("normalizes tool names - paseo_voice.speak to Speak", () => {
|
test("normalizes tool names - paseo_voice.speak to Speak", () => {
|
||||||
const input = { text: "hello from namespaced speak" };
|
const input = { text: "hello from namespaced speak" };
|
||||||
const info = parseToolCallDisplay({ name: "paseo_voice.speak", input });
|
const info = parseToolCallDisplay({ name: "paseo_voice.speak", input });
|
||||||
@@ -128,6 +165,15 @@ describe("parseToolCallDisplay", () => {
|
|||||||
expect(info.displayName).toBe("MyCustomTool");
|
expect(info.displayName).toBe("MyCustomTool");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("does not let Task metadata override non-Task summary", () => {
|
||||||
|
const info = parseToolCallDisplay({
|
||||||
|
name: "shell",
|
||||||
|
input: { command: "pwd" },
|
||||||
|
metadata: { subAgentActivity: "Read" },
|
||||||
|
});
|
||||||
|
expect(info.summary).toBe("pwd");
|
||||||
|
});
|
||||||
|
|
||||||
test("parses non-command tool call into generic detail", () => {
|
test("parses non-command tool call into generic detail", () => {
|
||||||
const input = { file_path: "/some/file.txt" };
|
const input = { file_path: "/some/file.txt" };
|
||||||
const output = { content: "file contents here", lineCount: 42 };
|
const output = { content: "file contents here", lineCount: 42 };
|
||||||
@@ -141,12 +187,16 @@ describe("parseToolCallDisplay", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles file_write output as generic", () => {
|
test("handles file_write output as edit", () => {
|
||||||
const input = { file_path: "/some/file.txt", content: "new content" };
|
const input = { file_path: "/some/file.txt", content: "new content" };
|
||||||
const output = { type: "file_write", filePath: "/some/file.txt" };
|
const output = { type: "file_write", filePath: "/some/file.txt" };
|
||||||
|
|
||||||
const info: ToolCallDisplayInfo = parseToolCallDisplay({ name: "Write", input, output });
|
const info: ToolCallDisplayInfo = parseToolCallDisplay({ name: "Write", input, output });
|
||||||
expect(info.detail.type).toBe("generic");
|
expect(info.detail.type).toBe("edit");
|
||||||
|
expect(info.summary).toBe("/some/file.txt");
|
||||||
|
if (info.detail.type === "edit") {
|
||||||
|
expect(info.detail.filePath).toBe("/some/file.txt");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles undefined input and output gracefully", () => {
|
test("handles undefined input and output gracefully", () => {
|
||||||
@@ -366,14 +416,13 @@ describe("parseToolCallDisplay - read_file (Codex)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Codex read_file falls through to generic when result is missing", () => {
|
test("Codex read_file stays read when result is missing", () => {
|
||||||
const input = {
|
const input = {
|
||||||
path: "/some/file.txt",
|
path: "/some/file.txt",
|
||||||
};
|
};
|
||||||
|
|
||||||
const info = parseToolCallDisplay({ name: "read_file", input });
|
const info = parseToolCallDisplay({ name: "read_file", input });
|
||||||
// Without result, it can't match the schema so falls through to generic
|
expect(info.detail.type).toBe("read");
|
||||||
expect(info.detail.type).toBe("generic");
|
|
||||||
expect(info.displayName).toBe("Read");
|
expect(info.displayName).toBe("Read");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"generate:config-schema": "tsx scripts/generate-config-schema.ts",
|
"generate:config-schema": "tsx scripts/generate-config-schema.ts",
|
||||||
"speech:models": "tsx scripts/list-speech-models.ts",
|
"speech:models": "tsx scripts/list-speech-models.ts",
|
||||||
"speech:download": "tsx scripts/download-speech-models.ts",
|
"speech:download": "tsx scripts/download-speech-models.ts",
|
||||||
|
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { STTManager } from "../src/server/agent/stt-manager.js";
|
||||||
|
import { createRootLogger } from "../src/server/logger.js";
|
||||||
|
import { resolvePaseoHome } from "../src/server/paseo-home.js";
|
||||||
|
import {
|
||||||
|
DEFAULT_LOCAL_STT_MODEL,
|
||||||
|
DEFAULT_LOCAL_TTS_MODEL,
|
||||||
|
LocalSttModelIdSchema,
|
||||||
|
type LocalSttModelId,
|
||||||
|
} from "../src/server/speech/providers/local/models.js";
|
||||||
|
import { initializeLocalSpeechServices } from "../src/server/speech/providers/local/runtime.js";
|
||||||
|
import type { RequestedSpeechProviders } from "../src/server/speech/speech-types.js";
|
||||||
|
|
||||||
|
type CliOptions = {
|
||||||
|
wavPath: string;
|
||||||
|
outPath?: string;
|
||||||
|
model: LocalSttModelId;
|
||||||
|
modelsDir: string;
|
||||||
|
autoDownload: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function usage(): string {
|
||||||
|
return [
|
||||||
|
"Usage: npm run speech:transcribe:local -- <wavPath> [--out <outPath>] [--model <modelId>] [--models-dir <dir>] [--no-auto-download]",
|
||||||
|
"",
|
||||||
|
"Examples:",
|
||||||
|
" npm run speech:transcribe:local -- ./sample.wav",
|
||||||
|
" npm run speech:transcribe:local -- ./sample.wav --out ./tmp/sample.transcript.txt",
|
||||||
|
"",
|
||||||
|
"Env fallbacks:",
|
||||||
|
" PASEO_LOCAL_MODELS_DIR, PASEO_LOCAL_STT_MODEL",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv: string[]): CliOptions {
|
||||||
|
if (argv.includes("--help") || argv.includes("-h")) {
|
||||||
|
process.stdout.write(`${usage()}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv.length === 0) {
|
||||||
|
throw new Error(`Missing <wavPath>\n\n${usage()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const paseoHome = resolvePaseoHome();
|
||||||
|
const defaultModelsDir =
|
||||||
|
process.env.PASEO_LOCAL_MODELS_DIR ?? path.join(paseoHome, "models", "local-speech");
|
||||||
|
|
||||||
|
const positional: string[] = [];
|
||||||
|
let outPath: string | undefined;
|
||||||
|
let model = LocalSttModelIdSchema.parse(process.env.PASEO_LOCAL_STT_MODEL ?? DEFAULT_LOCAL_STT_MODEL);
|
||||||
|
let modelsDir = defaultModelsDir;
|
||||||
|
let autoDownload = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i];
|
||||||
|
|
||||||
|
if (arg === "--out") {
|
||||||
|
const next = argv[i + 1];
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("--out requires a value");
|
||||||
|
}
|
||||||
|
outPath = path.resolve(next);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--model") {
|
||||||
|
const next = argv[i + 1];
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("--model requires a value");
|
||||||
|
}
|
||||||
|
model = LocalSttModelIdSchema.parse(next);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--models-dir") {
|
||||||
|
const next = argv[i + 1];
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("--models-dir requires a value");
|
||||||
|
}
|
||||||
|
modelsDir = path.resolve(next);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--no-auto-download") {
|
||||||
|
autoDownload = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith("-")) {
|
||||||
|
throw new Error(`Unknown option: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
positional.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positional.length === 0) {
|
||||||
|
throw new Error(`Missing <wavPath>\n\n${usage()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
wavPath: path.resolve(positional[0]),
|
||||||
|
...(outPath ? { outPath } : {}),
|
||||||
|
model,
|
||||||
|
modelsDir,
|
||||||
|
autoDownload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
let options: CliOptions;
|
||||||
|
|
||||||
|
try {
|
||||||
|
options = parseArgs(process.argv.slice(2));
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
process.stderr.write(`${message}\n`);
|
||||||
|
process.exit(2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = createRootLogger({ level: "info", format: "pretty" });
|
||||||
|
|
||||||
|
const providers: RequestedSpeechProviders = {
|
||||||
|
dictationStt: { provider: "local", explicit: true },
|
||||||
|
voiceStt: { provider: "local", explicit: true },
|
||||||
|
// Not used here, but required by the shared runtime config shape.
|
||||||
|
voiceTts: { provider: "openai", explicit: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtime = await initializeLocalSpeechServices({
|
||||||
|
providers,
|
||||||
|
speechConfig: {
|
||||||
|
providers,
|
||||||
|
local: {
|
||||||
|
modelsDir: options.modelsDir,
|
||||||
|
autoDownload: options.autoDownload,
|
||||||
|
models: {
|
||||||
|
dictationStt: options.model,
|
||||||
|
voiceStt: options.model,
|
||||||
|
voiceTts: DEFAULT_LOCAL_TTS_MODEL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!runtime.sttService) {
|
||||||
|
throw new Error(
|
||||||
|
"Local STT service is unavailable. Check model files or run `npm run speech:download -- --model " +
|
||||||
|
options.model +
|
||||||
|
"`."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const audio = await readFile(options.wavPath);
|
||||||
|
const manager = new STTManager("dev-local-wav-transcribe", logger, runtime.sttService);
|
||||||
|
const result = await manager.transcribe(audio, "audio/wav", { label: "dev-local-wav-transcribe" });
|
||||||
|
|
||||||
|
const transcript = result.text.trim();
|
||||||
|
|
||||||
|
if (options.outPath) {
|
||||||
|
await mkdir(path.dirname(options.outPath), { recursive: true });
|
||||||
|
await writeFile(options.outPath, `${transcript}\n`, "utf8");
|
||||||
|
logger.info({ outPath: options.outPath }, "Wrote transcript");
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(`${transcript}\n`);
|
||||||
|
} finally {
|
||||||
|
runtime.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -159,7 +159,11 @@ export function curateAgentActivity(
|
|||||||
case "tool_call": {
|
case "tool_call": {
|
||||||
flushBuffers(lines, buffers);
|
flushBuffers(lines, buffers);
|
||||||
const inputJson = formatToolInputJson(item.input);
|
const inputJson = formatToolInputJson(item.input);
|
||||||
const { displayName, summary } = parseToolCallDisplay({ name: item.name, input: item.input, metadata: item.metadata });
|
const { displayName, summary } = parseToolCallDisplay({
|
||||||
|
name: item.name,
|
||||||
|
input: item.input,
|
||||||
|
metadata: item.metadata,
|
||||||
|
});
|
||||||
if (isLikelyExternalToolName(item.name) && inputJson) {
|
if (isLikelyExternalToolName(item.name) && inputJson) {
|
||||||
lines.push(`[${displayName}] ${inputJson}`);
|
lines.push(`[${displayName}] ${inputJson}`);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ describe("parseToolCallDisplay", () => {
|
|||||||
describe("summary (was extractPrincipalParam)", () => {
|
describe("summary (was extractPrincipalParam)", () => {
|
||||||
test("extracts and strips shell wrapper from command string", () => {
|
test("extracts and strips shell wrapper from command string", () => {
|
||||||
const result = parseToolCallDisplay({
|
const result = parseToolCallDisplay({
|
||||||
|
provider: "claude",
|
||||||
name: "Bash",
|
name: "Bash",
|
||||||
input: { command: "/bin/zsh -lc cd /Users/dev/project && npm run format" },
|
input: { command: "/bin/zsh -lc cd /Users/dev/project && npm run format" },
|
||||||
});
|
});
|
||||||
@@ -120,6 +121,14 @@ describe("parseToolCallDisplay", () => {
|
|||||||
expect(result.summary).toBe("npm run build");
|
expect(result.summary).toBe("npm run build");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("falls back to output command when shell input is missing", () => {
|
||||||
|
const result = parseToolCallDisplay({
|
||||||
|
name: "Bash",
|
||||||
|
output: { type: "command", command: "pwd", output: "/tmp" },
|
||||||
|
});
|
||||||
|
expect(result.summary).toBe("pwd");
|
||||||
|
});
|
||||||
|
|
||||||
test("extracts file_path and strips cwd", () => {
|
test("extracts file_path and strips cwd", () => {
|
||||||
const result = parseToolCallDisplay({
|
const result = parseToolCallDisplay({
|
||||||
name: "Read",
|
name: "Read",
|
||||||
@@ -129,6 +138,34 @@ describe("parseToolCallDisplay", () => {
|
|||||||
expect(result.summary).toBe("src/file.ts");
|
expect(result.summary).toBe("src/file.ts");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("falls back to read output path when input is missing", () => {
|
||||||
|
const result = parseToolCallDisplay({
|
||||||
|
provider: "codex",
|
||||||
|
name: "Read",
|
||||||
|
output: {
|
||||||
|
type: "file_read",
|
||||||
|
filePath: "/Users/dev/project/src/file.ts",
|
||||||
|
content: "hello",
|
||||||
|
},
|
||||||
|
cwd: "/Users/dev/project",
|
||||||
|
});
|
||||||
|
expect(result.summary).toBe("src/file.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to edit output path when input is missing", () => {
|
||||||
|
const result = parseToolCallDisplay({
|
||||||
|
name: "Edit",
|
||||||
|
output: {
|
||||||
|
type: "file_edit",
|
||||||
|
filePath: "/Users/dev/project/src/file.ts",
|
||||||
|
oldContent: "a",
|
||||||
|
newContent: "b",
|
||||||
|
},
|
||||||
|
cwd: "/Users/dev/project",
|
||||||
|
});
|
||||||
|
expect(result.summary).toBe("src/file.ts");
|
||||||
|
});
|
||||||
|
|
||||||
test("extracts pattern without modification", () => {
|
test("extracts pattern without modification", () => {
|
||||||
const result = parseToolCallDisplay({
|
const result = parseToolCallDisplay({
|
||||||
name: "Grep",
|
name: "Grep",
|
||||||
@@ -168,7 +205,7 @@ describe("parseToolCallDisplay", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("summary from metadata", () => {
|
describe("summary from metadata", () => {
|
||||||
test("subAgentActivity in metadata takes priority over input", () => {
|
test("subAgentActivity in metadata takes priority for Task", () => {
|
||||||
const result = parseToolCallDisplay({
|
const result = parseToolCallDisplay({
|
||||||
name: "Task",
|
name: "Task",
|
||||||
input: { description: "Explore codebase" },
|
input: { description: "Explore codebase" },
|
||||||
@@ -185,6 +222,15 @@ describe("parseToolCallDisplay", () => {
|
|||||||
});
|
});
|
||||||
expect(result.summary).toBe("Bash");
|
expect(result.summary).toBe("Bash");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("non-Task tools keep their parsed summary even when metadata is present", () => {
|
||||||
|
const result = parseToolCallDisplay({
|
||||||
|
name: "Bash",
|
||||||
|
input: { command: "pwd" },
|
||||||
|
metadata: { subAgentActivity: "Read" },
|
||||||
|
});
|
||||||
|
expect(result.summary).toBe("pwd");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("kind", () => {
|
describe("kind", () => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
> paseo@0.1.0 cli
|
||||||
|
> npx tsx packages/cli/src/index.js logs cli --filter tools
|
||||||
|
|
||||||
Reference in New Issue
Block a user