Skip to content

Commit 340dbbb

Browse files
authored
fix(web): unwrap windows shell command wrappers (#1719)
1 parent 53a552e commit 340dbbb

3 files changed

Lines changed: 312 additions & 25 deletions

File tree

apps/web/src/components/chat/MessagesTimeline.tsx

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
type MessagesTimelineRow,
4949
} from "./MessagesTimeline.logic";
5050
import { TerminalContextInlineChip } from "./TerminalContextInlineChip";
51+
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
5152
import {
5253
deriveDisplayedUserMessageState,
5354
type ParsedTerminalContextEntry,
@@ -792,6 +793,16 @@ function workEntryPreview(
792793
: `${firstPath} +${workEntry.changedFiles!.length - 1} more`;
793794
}
794795

796+
function workEntryRawCommand(
797+
workEntry: Pick<TimelineWorkEntry, "command" | "rawCommand">,
798+
): string | null {
799+
const rawCommand = workEntry.rawCommand?.trim();
800+
if (!rawCommand || !workEntry.command) {
801+
return null;
802+
}
803+
return rawCommand === workEntry.command.trim() ? null : rawCommand;
804+
}
805+
795806
function workEntryIcon(workEntry: TimelineWorkEntry): LucideIcon {
796807
if (workEntry.requestKind === "command") return TerminalIcon;
797808
if (workEntry.requestKind === "file-read") return EyeIcon;
@@ -840,6 +851,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
840851
const EntryIcon = workEntryIcon(workEntry);
841852
const heading = toolWorkEntryHeading(workEntry);
842853
const preview = workEntryPreview(workEntry);
854+
const rawCommand = workEntryRawCommand(workEntry);
843855
const displayText = preview ? `${heading} - ${preview}` : heading;
844856
const hasChangedFiles = (workEntry.changedFiles?.length ?? 0) > 0;
845857
const previewIsChangedFiles = hasChangedFiles && !workEntry.command && !workEntry.detail;
@@ -853,19 +865,46 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
853865
<EntryIcon className="size-3" />
854866
</span>
855867
<div className="min-w-0 flex-1 overflow-hidden">
856-
<p
857-
className={cn(
858-
"truncate text-[11px] leading-5",
859-
workToneClass(workEntry.tone),
860-
preview ? "text-muted-foreground/70" : "",
861-
)}
862-
title={displayText}
863-
>
864-
<span className={cn("text-foreground/80", workToneClass(workEntry.tone))}>
865-
{heading}
866-
</span>
867-
{preview && <span className="text-muted-foreground/55"> - {preview}</span>}
868-
</p>
868+
<div className="max-w-full">
869+
<p
870+
className={cn(
871+
"truncate text-[11px] leading-5",
872+
workToneClass(workEntry.tone),
873+
preview ? "text-muted-foreground/70" : "",
874+
)}
875+
title={rawCommand ? undefined : displayText}
876+
>
877+
<span className={cn("text-foreground/80", workToneClass(workEntry.tone))}>
878+
{heading}
879+
</span>
880+
{preview &&
881+
(rawCommand ? (
882+
<Tooltip>
883+
<TooltipTrigger
884+
closeDelay={0}
885+
delay={75}
886+
render={
887+
<span className="max-w-full cursor-default text-muted-foreground/55 transition-colors hover:text-muted-foreground/75 focus-visible:text-muted-foreground/75">
888+
{" "}
889+
- {preview}
890+
</span>
891+
}
892+
/>
893+
<TooltipPopup
894+
align="start"
895+
className="max-w-[min(56rem,calc(100vw-2rem))] px-0 py-0"
896+
side="top"
897+
>
898+
<div className="max-w-[min(56rem,calc(100vw-2rem))] overflow-x-auto px-1.5 py-1 font-mono text-[11px] leading-4 whitespace-nowrap">
899+
{rawCommand}
900+
</div>
901+
</TooltipPopup>
902+
</Tooltip>
903+
) : (
904+
<span className="text-muted-foreground/55"> - {preview}</span>
905+
))}
906+
</p>
907+
</div>
869908
</div>
870909
</div>
871910
{hasChangedFiles && !previewIsChangedFiles && (

apps/web/src/session-logic.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,97 @@ describe("deriveWorkLogEntries", () => {
709709
expect(entry?.command).toBe("bun run lint");
710710
});
711711

712+
it("unwraps PowerShell command wrappers for displayed command text", () => {
713+
const activities: OrchestrationThreadActivity[] = [
714+
makeActivity({
715+
id: "command-tool-windows-wrapper",
716+
kind: "tool.completed",
717+
summary: "Ran command",
718+
payload: {
719+
itemType: "command_execution",
720+
data: {
721+
item: {
722+
command: "\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\" -Command 'bun run lint'",
723+
},
724+
},
725+
},
726+
}),
727+
];
728+
729+
const [entry] = deriveWorkLogEntries(activities, undefined);
730+
expect(entry?.command).toBe("bun run lint");
731+
expect(entry?.rawCommand).toBe(
732+
"\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\" -Command 'bun run lint'",
733+
);
734+
});
735+
736+
it("unwraps PowerShell command wrappers from argv-style command payloads", () => {
737+
const activities: OrchestrationThreadActivity[] = [
738+
makeActivity({
739+
id: "command-tool-windows-wrapper-argv",
740+
kind: "tool.completed",
741+
summary: "Ran command",
742+
payload: {
743+
itemType: "command_execution",
744+
data: {
745+
item: {
746+
command: ["C:\\Program Files\\PowerShell\\7\\pwsh.exe", "-Command", "rg -n foo ."],
747+
},
748+
},
749+
},
750+
}),
751+
];
752+
753+
const [entry] = deriveWorkLogEntries(activities, undefined);
754+
expect(entry?.command).toBe("rg -n foo .");
755+
expect(entry?.rawCommand).toBe(
756+
'"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command "rg -n foo ."',
757+
);
758+
});
759+
760+
it("extracts command text from command detail when structured command metadata is missing", () => {
761+
const activities: OrchestrationThreadActivity[] = [
762+
makeActivity({
763+
id: "command-tool-windows-detail-fallback",
764+
kind: "tool.completed",
765+
summary: "Ran command",
766+
payload: {
767+
itemType: "command_execution",
768+
detail:
769+
'"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoLogo -NoProfile -Command \'rg -n -F "new Date()" .\' <exited with exit code 0>',
770+
},
771+
}),
772+
];
773+
774+
const [entry] = deriveWorkLogEntries(activities, undefined);
775+
expect(entry?.command).toBe('rg -n -F "new Date()" .');
776+
expect(entry?.rawCommand).toBe(
777+
`"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoLogo -NoProfile -Command 'rg -n -F "new Date()" .'`,
778+
);
779+
});
780+
781+
it("does not unwrap shell commands when no wrapper flag is present", () => {
782+
const activities: OrchestrationThreadActivity[] = [
783+
makeActivity({
784+
id: "command-tool-shell-script",
785+
kind: "tool.completed",
786+
summary: "Ran command",
787+
payload: {
788+
itemType: "command_execution",
789+
data: {
790+
item: {
791+
command: "bash script.sh",
792+
},
793+
},
794+
},
795+
}),
796+
];
797+
798+
const [entry] = deriveWorkLogEntries(activities, undefined);
799+
expect(entry?.command).toBe("bash script.sh");
800+
expect(entry?.rawCommand).toBeUndefined();
801+
});
802+
712803
it("keeps compact Codex tool metadata used for icons and labels", () => {
713804
const activities: OrchestrationThreadActivity[] = [
714805
makeActivity({

0 commit comments

Comments
 (0)