Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,31 +200,34 @@ This project uses **Make** as the primary build orchestrator. See `Makefile` for
const config = env.config.loadConfigOrDefault();
config.projects.set(projectPath, { path: projectPath, workspaces: [] });
env.config.saveConfig(config);

// ❌ BAD - Directly accessing services
const history = await env.historyService.getHistory(workspaceId);
await env.historyService.appendToHistory(workspaceId, message);
```

**Correct approach (DO THIS):**
// ❌ BAD - Directly accessing services
const history = await env.historyService.getHistory(workspaceId);
await env.historyService.appendToHistory(workspaceId, message);

```typescript
// ✅ GOOD - Use IPC to save config
await env.mockIpcRenderer.invoke(IPC_CHANNELS.CONFIG_SAVE, {
projects: Array.from(projectsConfig.projects.entries()),
});

// ✅ GOOD - Use IPC to interact with services
await env.mockIpcRenderer.invoke(IPC_CHANNELS.HISTORY_GET, workspaceId);
await env.mockIpcRenderer.invoke(IPC_CHANNELS.WORKSPACE_CREATE, projectPath, branchName);
```
````

**Correct approach (DO THIS):**

```typescript
// ✅ GOOD - Use IPC to save config
await env.mockIpcRenderer.invoke(IPC_CHANNELS.CONFIG_SAVE, {
projects: Array.from(projectsConfig.projects.entries()),
});

**Acceptable exceptions:**
- Reading context (like `env.config.loadConfigOrDefault()`) to prepare IPC call parameters
- Verifying filesystem state (like checking if files exist) after IPC operations complete
- Loading existing data to avoid expensive API calls in test setup
// ✅ GOOD - Use IPC to interact with services
await env.mockIpcRenderer.invoke(IPC_CHANNELS.HISTORY_GET, workspaceId);
await env.mockIpcRenderer.invoke(IPC_CHANNELS.WORKSPACE_CREATE, projectPath, branchName);
````

If IPC is hard to test, fix the test infrastructure or IPC layer, don't work around it by bypassing IPC.
**Acceptable exceptions:**

- Reading context (like `env.config.loadConfigOrDefault()`) to prepare IPC call parameters
- Verifying filesystem state (like checking if files exist) after IPC operations complete
- Loading existing data to avoid expensive API calls in test setup

If IPC is hard to test, fix the test infrastructure or IPC layer, don't work around it by bypassing IPC.

## Command Palette (Cmd+Shift+P)

Expand Down Expand Up @@ -486,3 +489,7 @@ should go through `log.debug()`.
- If you're fixing via simplifcation, a new test case is generally not necessary.
- If fixing through additional complexity, add a test case if an existing convenient harness exists.
- Otherwise if creating complexity, propose a new test harness to contain the new tests.

## Mode: Exec

If a user requests `wait_pr_checks`, treat it as a directive to keep running that process and address failures continuously. Do not return to the user until the checks succeed or you encounter a blocker you cannot resolve alone. This mode signals that the user expects persistent execution without further prompting. If static checks fail remotely, reproduce the error locally with `make static-check` before responding. If formatting issues are flagged, run `make fmt` to fix them before retrying CI.
57 changes: 51 additions & 6 deletions scripts/wait_pr_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,58 @@ if [ $# -eq 0 ]; then
fi

PR_NUMBER=$1
echo "⏳ Waiting for PR #$PR_NUMBER checks to complete..."

# Warn if there are unstaged changes
if ! git diff --quiet 2>/dev/null; then
echo "⚠️ Warning: You have unstaged changes that are not pushed!"
echo " Run 'git status' to see what changes are pending."
echo ""
# Check for dirty working tree
if ! git diff-index --quiet HEAD --; then
echo "❌ Error: You have uncommitted changes in your working directory." >&2
echo "" >&2
git status --short >&2
echo "" >&2
echo "Please commit or stash your changes before checking PR status." >&2
exit 1
fi

# Get current branch name
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

# Get remote tracking branch
REMOTE_BRANCH=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "")

if [[ -z "$REMOTE_BRANCH" ]]; then
echo "❌ Error: Current branch '$CURRENT_BRANCH' has no upstream branch." >&2
echo "Set an upstream with: git push -u origin $CURRENT_BRANCH" >&2
exit 1
fi

# Check if local and remote are in sync
LOCAL_HASH=$(git rev-parse HEAD)
REMOTE_HASH=$(git rev-parse "$REMOTE_BRANCH")

if [[ "$LOCAL_HASH" != "$REMOTE_HASH" ]]; then
echo "❌ Error: Local branch is not in sync with remote." >&2
echo "" >&2
echo "Local: $LOCAL_HASH" >&2
echo "Remote: $REMOTE_HASH" >&2
echo "" >&2

# Check if we're ahead, behind, or diverged
if git merge-base --is-ancestor "$REMOTE_HASH" HEAD 2>/dev/null; then
AHEAD=$(git rev-list --count "$REMOTE_BRANCH"..HEAD)
echo "Your branch is $AHEAD commit(s) ahead of '$REMOTE_BRANCH'." >&2
echo "Push your changes with: git push" >&2
elif git merge-base --is-ancestor HEAD "$REMOTE_HASH" 2>/dev/null; then
BEHIND=$(git rev-list --count HEAD.."$REMOTE_BRANCH")
echo "Your branch is $BEHIND commit(s) behind '$REMOTE_BRANCH'." >&2
echo "Pull the latest changes with: git pull" >&2
else
echo "Your branch has diverged from '$REMOTE_BRANCH'." >&2
echo "You may need to rebase or merge." >&2
fi

exit 1
fi

echo "⏳ Waiting for PR #$PR_NUMBER checks to complete..."
echo ""

while true; do
Expand Down Expand Up @@ -83,6 +126,7 @@ while true; do
if ! ./scripts/check_pr_reviews.sh "$PR_NUMBER" >/dev/null 2>&1; then
echo ""
echo "❌ Unresolved review comments found!"
echo " 👉 Tip: run ./scripts/check_pr_reviews.sh "$PR_NUMBER" to list them."
./scripts/check_pr_reviews.sh "$PR_NUMBER"
exit 1
fi
Expand All @@ -103,6 +147,7 @@ while true; do
else
echo ""
echo "❌ Please resolve Codex comments before merging."
echo " 👉 Tip: use ./scripts/resolve_codex_comment.sh "$PR_NUMBER" to apply Codex suggestions from the CLI."
exit 1
fi
elif [ "$MERGE_STATE" = "BLOCKED" ]; then
Expand Down
42 changes: 34 additions & 8 deletions src/components/Messages/ToolMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import type {
BashToolResult,
FileReadToolArgs,
FileReadToolResult,
FileEditReplaceToolArgs,
FileEditInsertToolArgs,
FileEditReplaceToolResult,
FileEditInsertToolResult,
FileEditReplaceStringToolArgs,
FileEditReplaceStringToolResult,
FileEditReplaceLinesToolArgs,
FileEditReplaceLinesToolResult,
ProposePlanToolArgs,
ProposePlanToolResult,
} from "@/types/tools";
Expand All @@ -37,9 +39,20 @@ function isFileReadTool(toolName: string, args: unknown): args is FileReadToolAr
return TOOL_DEFINITIONS.file_read.schema.safeParse(args).success;
}

function isFileEditReplaceTool(toolName: string, args: unknown): args is FileEditReplaceToolArgs {
if (toolName !== "file_edit_replace") return false;
return TOOL_DEFINITIONS.file_edit_replace.schema.safeParse(args).success;
function isFileEditReplaceStringTool(
toolName: string,
args: unknown
): args is FileEditReplaceStringToolArgs {
if (toolName !== "file_edit_replace_string") return false;
return TOOL_DEFINITIONS.file_edit_replace_string.schema.safeParse(args).success;
}

function isFileEditReplaceLinesTool(
toolName: string,
args: unknown
): args is FileEditReplaceLinesToolArgs {
if (toolName !== "file_edit_replace_lines") return false;
return TOOL_DEFINITIONS.file_edit_replace_lines.schema.safeParse(args).success;
}

function isFileEditInsertTool(toolName: string, args: unknown): args is FileEditInsertToolArgs {
Expand Down Expand Up @@ -79,13 +92,26 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className, wo
);
}

if (isFileEditReplaceTool(message.toolName, message.args)) {
if (isFileEditReplaceStringTool(message.toolName, message.args)) {
return (
<div className={className}>
<FileEditToolCall
toolName="file_edit_replace_string"
args={message.args}
result={message.result as FileEditReplaceStringToolResult | undefined}
status={message.status}
/>
</div>
);
}

if (isFileEditReplaceLinesTool(message.toolName, message.args)) {
return (
<div className={className}>
<FileEditToolCall
toolName="file_edit_replace"
toolName="file_edit_replace_lines"
args={message.args}
result={message.result as FileEditReplaceToolResult | undefined}
result={message.result as FileEditReplaceLinesToolResult | undefined}
status={message.status}
/>
</div>
Expand Down
38 changes: 35 additions & 3 deletions src/components/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ const BuildInfo = styled.div`
cursor: default;
`;

interface VersionMetadata {
buildTime: string;
git_describe?: unknown;
}

function hasBuildInfo(value: unknown): value is VersionMetadata {
if (typeof value !== "object" || value === null) {
return false;
}

const candidate = value as Record<string, unknown>;
return typeof candidate.buildTime === "string";
}

function formatUSDate(isoDate: string): string {
const date = new Date(isoDate);
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
Expand All @@ -51,13 +65,31 @@ function formatExtendedTimestamp(isoDate: string): string {
});
}

function parseBuildInfo(version: unknown) {
if (hasBuildInfo(version)) {
const { buildTime, git_describe } = version;
const gitDescribe = typeof git_describe === "string" ? git_describe : undefined;

return {
buildDate: formatUSDate(buildTime),
extendedTimestamp: formatExtendedTimestamp(buildTime),
gitDescribe,
};
}

return {
buildDate: "unknown",
extendedTimestamp: "Unknown build time",
gitDescribe: undefined,
};
}

export function TitleBar() {
const buildDate = formatUSDate(VERSION.buildTime);
const extendedTimestamp = formatExtendedTimestamp(VERSION.buildTime);
const { buildDate, extendedTimestamp, gitDescribe } = parseBuildInfo(VERSION satisfies unknown);

return (
<TitleBarContainer>
<TitleText>cmux {VERSION.git_describe}</TitleText>
<TitleText>cmux {gitDescribe ?? "(dev)"}</TitleText>
<TooltipWrapper>
<BuildInfo>{buildDate}</BuildInfo>
<Tooltip align="right">Built at {extendedTimestamp}</Tooltip>
Expand Down
23 changes: 16 additions & 7 deletions src/components/tools/FileEditToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import React from "react";
import styled from "@emotion/styled";
import { parsePatch } from "diff";
import type {
FileEditReplaceToolArgs,
FileEditReplaceToolResult,
FileEditInsertToolArgs,
FileEditInsertToolResult,
FileEditReplaceStringToolArgs,
FileEditReplaceStringToolResult,
FileEditReplaceLinesToolArgs,
FileEditReplaceLinesToolResult,
} from "@/types/tools";
import {
ToolContainer,
Expand Down Expand Up @@ -177,12 +179,19 @@ const ButtonGroup = styled.div`
margin-right: 8px;
`;

type FileEditToolArgs = FileEditReplaceToolArgs | FileEditInsertToolArgs;
type FileEditToolResult = FileEditReplaceToolResult | FileEditInsertToolResult;
type FileEditOperationArgs =
| FileEditReplaceStringToolArgs
| FileEditReplaceLinesToolArgs
| FileEditInsertToolArgs;

type FileEditToolResult =
| FileEditReplaceStringToolResult
| FileEditReplaceLinesToolResult
| FileEditInsertToolResult;

interface FileEditToolCallProps {
toolName: "file_edit_replace" | "file_edit_insert";
args: FileEditToolArgs;
toolName: "file_edit_replace_string" | "file_edit_replace_lines" | "file_edit_insert";
args: FileEditOperationArgs;
result?: FileEditToolResult;
status?: ToolStatus;
}
Expand Down Expand Up @@ -262,7 +271,7 @@ export const FileEditToolCall: React.FC<FileEditToolCallProps> = ({
const [showRaw, setShowRaw] = React.useState(false);
const [copied, setCopied] = React.useState(false);

const filePath = args.file_path;
const filePath = "file_path" in args ? args.file_path : undefined;

const handleCopyPatch = async () => {
if (result && result.success && result.diff) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/streamManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ describe("StreamManager - Unavailable Tool Handling", () => {
type: "tool-call",
toolCallId: "test-call-1",
toolName: "file_edit_replace",
input: { file_path: "/test", edits: [] },
input: { file_path: "/test", old_string: "foo", new_string: "bar" },
};
// SDK emits tool-error when tool execution fails
yield {
Expand Down
Loading