Skip to content

Commit f0cbd47

Browse files
author
Test
committed
feat: add script execution timeline entries
Change-Id: I5beb91709a98f4b18d13a2d263c2c6a088b7af72 Signed-off-by: Test <test@example.com>
1 parent f1d022a commit f0cbd47

File tree

18 files changed

+436
-58
lines changed

18 files changed

+436
-58
lines changed

.cmux/scripts/demo

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@ echo "Running demo script..."
77
echo "Current workspace: $(pwd)"
88
echo "Timestamp: $(date)"
99

10-
# Write formatted output to CMUX_OUTPUT for toast display
11-
cat >> "$CMUX_OUTPUT" << 'EOF'
10+
# Write formatted output to MUX_OUTPUT for toast display
11+
cat >>"$MUX_OUTPUT" <<'EOF'
1212
## 🎉 Script Execution Demo
1313
1414
✅ Script executed successfully!
1515
1616
**Environment Variables Available:**
17-
- `CMUX_OUTPUT`: Custom toast display
18-
- `CMUX_PROMPT`: Send messages to agent
17+
- `MUX_OUTPUT`: Custom toast display
18+
- `MUX_PROMPT`: Send messages to agent
1919
EOF
2020

21-
# Write a prompt to CMUX_PROMPT to send a message to the agent
22-
cat >> "$CMUX_PROMPT" << 'EOF'
21+
# Write a prompt to MUX_PROMPT to send a message to the agent
22+
cat >>"$MUX_PROMPT" <<'EOF'
2323
The demo script has completed successfully. The script execution feature is working correctly with:
24-
1. Custom toast output via CMUX_OUTPUT
25-
2. Agent prompting via CMUX_PROMPT
24+
1. Custom toast output via MUX_OUTPUT
25+
2. Agent prompting via MUX_PROMPT
2626
2727
You can now create workspace-specific scripts to automate tasks and interact with the agent.
2828
EOF

.cmux/scripts/echo

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -euo pipefail
44

55
# Check if arguments were provided
66
if [ $# -eq 0 ]; then
7-
cat >> "$CMUX_OUTPUT" << 'EOF'
7+
cat >>"$MUX_OUTPUT" <<'EOF'
88
## ⚠️ No Arguments Provided
99
1010
Usage: `/s echo <message...>`
@@ -17,7 +17,7 @@ fi
1717
# Access arguments using standard bash positional parameters
1818
# $1 = first arg, $2 = second arg, $@ = all args, $# = number of args
1919

20-
cat >> "$CMUX_OUTPUT" << EOF
20+
cat >>"$MUX_OUTPUT" <<EOF
2121
## 🔊 Echo Script
2222
2323
**You said:** $@
@@ -33,12 +33,12 @@ EOF
3333

3434
# Loop through each argument
3535
for i in $(seq 1 $#); do
36-
echo "- Arg $i: ${!i}" >> "$CMUX_OUTPUT"
36+
echo "- Arg $i: ${!i}" >>"$MUX_OUTPUT"
3737
done
3838

3939
# Optionally send a message to the agent
4040
if [ $# -gt 3 ]; then
41-
cat >> "$CMUX_PROMPT" << EOF
41+
cat >>"$MUX_PROMPT" <<EOF
4242
The user passed more than 3 arguments to the echo script. They seem to be testing the argument passing feature extensively!
4343
EOF
4444
fi

docs/scripts.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Scripts run with:
8383

8484
Scripts receive special environment variables for controlling cmux behavior:
8585

86-
#### `CMUX_OUTPUT`
86+
#### `MUX_OUTPUT`
8787

8888
Path to a temporary file for custom toast display content. Write markdown here for rich formatting in the UI toast:
8989

@@ -94,7 +94,7 @@ Path to a temporary file for custom toast display content. Write markdown here f
9494
echo "Deploying..." # Regular stdout for logs
9595

9696
# Write formatted output for toast display
97-
cat >> "$CMUX_OUTPUT" << 'EOF'
97+
cat >> "$MUX_OUTPUT" << 'EOF'
9898
## 🚀 Deployment Complete
9999
100100
✅ Successfully deployed to staging
@@ -108,7 +108,7 @@ EOF
108108

109109
The toast will display the markdown-formatted content instead of the default "Script completed successfully" message.
110110

111-
#### `CMUX_PROMPT`
111+
#### `MUX_PROMPT`
112112

113113
Path to a temporary file for sending messages to the agent. Write prompts here to trigger agent actions:
114114

@@ -117,12 +117,12 @@ Path to a temporary file for sending messages to the agent. Write prompts here t
117117
# Description: Rebase with conflict handling
118118

119119
if git pull --rebase origin main; then
120-
echo "✅ Successfully rebased onto main" >> "$CMUX_OUTPUT"
120+
echo "✅ Successfully rebased onto main" >> "$MUX_OUTPUT"
121121
else
122-
echo "⚠️ Rebase conflicts detected" >> "$CMUX_OUTPUT"
122+
echo "⚠️ Rebase conflicts detected" >> "$MUX_OUTPUT"
123123

124124
# Send conflict details to agent for analysis
125-
cat >> "$CMUX_PROMPT" << 'EOF'
125+
cat >> "$MUX_PROMPT" << 'EOF'
126126
The rebase encountered conflicts. Please help resolve them:
127127
128128
```
@@ -147,13 +147,13 @@ You can use both environment files together:
147147
# Description: Run tests and report failures
148148

149149
if npm test > test-output.txt 2>&1; then
150-
echo "✅ All tests passed" >> "$CMUX_OUTPUT"
150+
echo "✅ All tests passed" >> "$MUX_OUTPUT"
151151
else
152152
# Show summary in toast
153-
echo "❌ Tests failed" >> "$CMUX_OUTPUT"
153+
echo "❌ Tests failed" >> "$MUX_OUTPUT"
154154

155155
# Ask agent to analyze and fix
156-
cat >> "$CMUX_PROMPT" << EOF
156+
cat >> "$MUX_PROMPT" << EOF
157157
The test suite failed. Please analyze and fix:
158158
159159
\`\`\`
@@ -171,8 +171,12 @@ fi
171171

172172
### File Size Limits
173173

174-
- **CMUX_OUTPUT**: Maximum 10KB (truncated if exceeded)
175-
- **CMUX_PROMPT**: Maximum 100KB (truncated if exceeded)
174+
- **MUX_OUTPUT**: Maximum 10KB (truncated if exceeded)
175+
- **MUX_PROMPT**: Maximum 100KB (truncated if exceeded)
176+
177+
### Chat Timeline Entry
178+
179+
Every `/script …` invocation now adds a private card to the chat transcript that shows the exit code, stdout/stderr, and the contents of `MUX_OUTPUT` / `MUX_PROMPT`. This card borrows the bash tool styling but is never forwarded to the LLM, so you can inspect script diagnostics without polluting the model context.
176180

177181
## Example Scripts
178182

src/browser/components/AIView.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,15 @@ const AIViewInner: React.FC<AIViewProps> = ({
276276

277277
const mergedMessages = mergeConsecutiveStreamErrors(workspaceState.messages);
278278
const editCutoffHistoryId = mergedMessages.find(
279-
(msg): msg is Exclude<DisplayedMessage, { type: "history-hidden" | "workspace-init" }> =>
279+
(
280+
msg
281+
): msg is Exclude<
282+
DisplayedMessage,
283+
{ type: "history-hidden" | "workspace-init" | "script-execution" }
284+
> =>
280285
msg.type !== "history-hidden" &&
281286
msg.type !== "workspace-init" &&
287+
msg.type !== "script-execution" &&
282288
msg.historyId === editingMessage.id
283289
)?.historyId;
284290

@@ -321,9 +327,15 @@ const AIViewInner: React.FC<AIViewProps> = ({
321327
// When editing, find the cutoff point
322328
const editCutoffHistoryId = editingMessage
323329
? mergedMessages.find(
324-
(msg): msg is Exclude<DisplayedMessage, { type: "history-hidden" | "workspace-init" }> =>
330+
(
331+
msg
332+
): msg is Exclude<
333+
DisplayedMessage,
334+
{ type: "history-hidden" | "workspace-init" | "script-execution" }
335+
> =>
325336
msg.type !== "history-hidden" &&
326337
msg.type !== "workspace-init" &&
338+
msg.type !== "script-execution" &&
327339
msg.historyId === editingMessage.id
328340
)?.historyId
329341
: undefined;
@@ -418,13 +430,16 @@ const AIViewInner: React.FC<AIViewProps> = ({
418430
editCutoffHistoryId !== undefined &&
419431
msg.type !== "history-hidden" &&
420432
msg.type !== "workspace-init" &&
433+
msg.type !== "script-execution" &&
421434
msg.historyId === editCutoffHistoryId;
422435

423436
return (
424437
<React.Fragment key={msg.id}>
425438
<div
426439
data-message-id={
427-
msg.type !== "history-hidden" && msg.type !== "workspace-init"
440+
msg.type !== "history-hidden" &&
441+
msg.type !== "workspace-init" &&
442+
msg.type !== "script-execution"
428443
? msg.historyId
429444
: undefined
430445
}

src/browser/components/ChatInput/index.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757

5858
import type { ThinkingLevel } from "@/common/types/thinking";
5959
import type { MuxFrontendMetadata } from "@/common/types/message";
60+
import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore";
6061
import { useTelemetry } from "@/browser/hooks/useTelemetry";
6162
import { setTelemetryEnabled } from "@/common/telemetry";
6263
import { getTokenCountPromise } from "@/browser/utils/tokenizer/rendererClient";
@@ -141,6 +142,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
141142
const [mode, setMode] = useMode();
142143
const { recentModels, addModel, evictModel } = useModelLRU();
143144
const commandListId = useId();
145+
const workspaceStore = useWorkspaceStoreRaw();
144146
const telemetry = useTelemetry();
145147
const [vimEnabled, setVimEnabled] = usePersistedState<boolean>(VIM_ENABLED_KEY, false, {
146148
listener: true,
@@ -720,14 +722,22 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
720722
const toolResult = result.data;
721723
const exitCode = toolResult.exitCode;
722724

723-
// Use CMUX_OUTPUT content if present, otherwise fall back to default message
725+
workspaceStore.recordScriptExecution(currentWorkspaceId, {
726+
rawCommand: messageText,
727+
scriptName: parsed.scriptName,
728+
args: parsed.args,
729+
result: toolResult,
730+
timestamp: Date.now(),
731+
});
732+
733+
// Use MUX_OUTPUT content if present, otherwise fall back to default message
724734
const toastMessage =
725735
toolResult.outputFile ??
726736
(exitCode === 0
727737
? `Script completed successfully`
728738
: `Script exited with code ${exitCode}`);
729739

730-
// If CMUX_PROMPT has content, send it as a new user message to the agent
740+
// If MUX_PROMPT has content, send it as a new user message to the agent
731741
if (toolResult.promptFile && toolResult.promptFile.trim().length > 0) {
732742
const sendResult = await window.api.workspace.sendMessage(
733743
currentWorkspaceId,

src/browser/components/Messages/MessageRenderer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ToolMessage } from "./ToolMessage";
66
import { ReasoningMessage } from "./ReasoningMessage";
77
import { StreamErrorMessage } from "./StreamErrorMessage";
88
import { HistoryHiddenMessage } from "./HistoryHiddenMessage";
9+
import { ScriptExecutionMessage } from "./ScriptExecutionMessage";
910
import { InitMessage } from "./InitMessage";
1011

1112
interface MessageRendererProps {
@@ -50,6 +51,8 @@ export const MessageRenderer = React.memo<MessageRendererProps>(
5051
return <HistoryHiddenMessage message={message} className={className} />;
5152
case "workspace-init":
5253
return <InitMessage message={message} className={className} />;
54+
case "script-execution":
55+
return <ScriptExecutionMessage message={message} className={className} />;
5356
default:
5457
console.error("don't know how to render message", message);
5558
return null;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React from "react";
2+
import type { DisplayedMessage } from "@/common/types/message";
3+
import { cn } from "@/common/lib/utils";
4+
import {
5+
ToolContainer,
6+
ToolHeader,
7+
ExpandIcon,
8+
ToolDetails,
9+
DetailSection,
10+
DetailLabel,
11+
DetailContent,
12+
StatusIndicator,
13+
} from "../tools/shared/ToolPrimitives";
14+
import { useToolExpansion } from "../tools/shared/toolUtils";
15+
16+
interface ScriptExecutionMessageProps {
17+
message: Extract<DisplayedMessage, { type: "script-execution" }>;
18+
className?: string;
19+
}
20+
21+
function formatDuration(ms: number): string {
22+
if (!Number.isFinite(ms) || ms < 0) {
23+
return "unknown";
24+
}
25+
if (ms < 1000) {
26+
return `${Math.round(ms)}ms`;
27+
}
28+
return `${Math.round(ms / 1000)}s`;
29+
}
30+
31+
export const ScriptExecutionMessage: React.FC<ScriptExecutionMessageProps> = ({
32+
message,
33+
className,
34+
}) => {
35+
const { expanded, toggleExpanded } = useToolExpansion();
36+
const { result } = message;
37+
38+
const exitBadgeClass = cn(
39+
"ml-2 inline-block shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium whitespace-nowrap",
40+
result.exitCode === 0 ? "bg-success text-on-success" : "bg-danger text-on-danger"
41+
);
42+
43+
const argsPreview = message.args.length > 0 ? ` ${message.args.join(" ")}` : "";
44+
45+
return (
46+
<ToolContainer expanded={expanded} className={className}>
47+
<ToolHeader onClick={toggleExpanded}>
48+
<ExpandIcon expanded={expanded}></ExpandIcon>
49+
<span aria-hidden="true">📝</span>
50+
<span className="font-monospace max-w-96 truncate">
51+
{message.command || `/script ${message.scriptName}${argsPreview}`}
52+
</span>
53+
<span className="text-[10px] text-foreground-secondary ml-2 whitespace-nowrap">
54+
took {formatDuration(result.wall_duration_ms)}
55+
</span>
56+
<span className={exitBadgeClass}>exit {result.exitCode}</span>
57+
<StatusIndicator status="completed">script</StatusIndicator>
58+
</ToolHeader>
59+
60+
{expanded && (
61+
<ToolDetails>
62+
<DetailSection>
63+
<DetailLabel>Command</DetailLabel>
64+
<DetailContent>{message.command}</DetailContent>
65+
</DetailSection>
66+
67+
<DetailSection>
68+
<DetailLabel>Runtime info</DetailLabel>
69+
<div className="text-[11px] text-foreground-secondary">
70+
{new Date(message.timestamp).toLocaleString()}{" "}
71+
{formatDuration(result.wall_duration_ms)}
72+
</div>
73+
<div className="text-[11px] text-foreground-secondary">
74+
Visible only to you; never sent to the model.
75+
</div>
76+
</DetailSection>
77+
78+
{result.success === false && result.error && (
79+
<DetailSection>
80+
<DetailLabel>Error</DetailLabel>
81+
<div className="text-danger bg-danger-overlay border-danger rounded border-l-2 px-2 py-1.5 text-[11px]">
82+
{result.error}
83+
</div>
84+
</DetailSection>
85+
)}
86+
87+
{result.output && (
88+
<DetailSection>
89+
<DetailLabel>Stdout / Stderr</DetailLabel>
90+
<DetailContent>{result.output}</DetailContent>
91+
</DetailSection>
92+
)}
93+
94+
{result.outputFile && (
95+
<DetailSection>
96+
<DetailLabel>MUX_OUTPUT</DetailLabel>
97+
<DetailContent>{result.outputFile}</DetailContent>
98+
</DetailSection>
99+
)}
100+
101+
{result.promptFile && (
102+
<DetailSection>
103+
<DetailLabel>MUX_PROMPT</DetailLabel>
104+
<DetailContent>{result.promptFile}</DetailContent>
105+
</DetailSection>
106+
)}
107+
108+
{result.truncated && (
109+
<DetailSection>
110+
<DetailLabel>Truncation</DetailLabel>
111+
<div className="text-[11px] text-foreground-secondary">
112+
Output truncated: {result.truncated.reason} ({result.truncated.totalLines} lines
113+
preserved)
114+
</div>
115+
</DetailSection>
116+
)}
117+
</ToolDetails>
118+
)}
119+
</ToolContainer>
120+
);
121+
};

src/browser/hooks/useAutoCompactContinue.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ export function useAutoCompactContinue() {
4242
for (const [workspaceId, state] of newStates) {
4343
// Detect if workspace is in "single compacted message" state
4444
// Skip workspace-init messages since they're UI-only metadata
45-
const muxMessages = state.messages.filter((m) => m.type !== "workspace-init");
45+
const muxMessages = state.messages.filter(
46+
(m) => m.type !== "workspace-init" && m.type !== "script-execution"
47+
);
4648
const isSingleCompacted =
4749
muxMessages.length === 1 &&
4850
muxMessages[0]?.type === "assistant" &&

0 commit comments

Comments
 (0)