Skip to content

Commit 4276d40

Browse files
authored
🤖 Add update notification UI (#329)
Adds a subtle update indicator to the title bar using electron-updater. ## Changes - Install electron-updater package for automatic update detection - Create UpdaterService to manage update checks, downloads, and installations - Add update IPC channels and handlers in main process - Expose update API through preload script - Add subtle update indicator icon to TitleBar (left of 'cmux') - 🟢 Green download icon when update available - 🔵 Blue spinning icon during download with progress - 🟠 Orange icon when ready to install - Update indicator shows version info in tooltip and supports click-to-download/install - Only runs in packaged builds (gracefully skips dev mode) - Periodic checks every 4 hours, starting 10s after launch ## UX The indicator appears to the left of "cmux" in the title bar. It's intentionally subtle - only visible when an update is available, downloading, or ready to install. Users have full control: 1. Update available → Click to download 2. Downloading → Shows progress 3. Downloaded → Click to install and restart ## Technical Details - Uses electron-updater's autoUpdater with blockmap support - Type-safe IPC communication with proper status types - Clean separation: UpdaterService handles logic, main.ts wires it up - All IPC channels centralized in constants - Works seamlessly with existing GitHub Releases distribution _Generated with `cmux`_
1 parent 518be83 commit 4276d40

File tree

16 files changed

+1351
-17
lines changed

16 files changed

+1351
-17
lines changed

bun.lock

Lines changed: 278 additions & 6 deletions
Large diffs are not rendered by default.

dev-app-update.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
provider: github
2+
owner: coder
3+
repo: cmux
4+
releaseType: release

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
setupFilesAfterEnv: ["<rootDir>/tests/setup.ts"],
1212
moduleNameMapper: {
1313
"^@/(.*)$": "<rootDir>/src/$1",
14+
"^chalk$": "<rootDir>/tests/__mocks__/chalk.js",
1415
},
1516
transform: {
1617
"^.+\\.tsx?$": [

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@
4040
"@types/react-syntax-highlighter": "^15.5.13",
4141
"ai": "^5.0.72",
4242
"ai-tokenizer": "^1.0.3",
43+
"chalk": "^5.6.2",
4344
"cmdk": "^1.0.0",
4445
"crc-32": "^1.2.2",
4546
"diff": "^8.0.2",
4647
"disposablestack": "^1.1.7",
48+
"electron-updater": "^6.6.2",
4749
"jsonc-parser": "^3.3.1",
4850
"lru-cache": "^11.2.2",
4951
"markdown-it": "^14.1.0",

scripts/extract_pr_logs.sh

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env bash
2+
# Extract logs from failed GitHub Actions runs for a PR
3+
# Usage: ./scripts/extract_pr_logs.sh <pr_number_or_run_id> [job_name_pattern] [--wait]
4+
#
5+
# Examples:
6+
# ./scripts/extract_pr_logs.sh 329 # Latest failed run for PR #329
7+
# ./scripts/extract_pr_logs.sh 329 Integration # Only Integration Test jobs
8+
# ./scripts/extract_pr_logs.sh 329 --wait # Wait for logs to be available
9+
# ./scripts/extract_pr_logs.sh 18640062283 # Specific run ID
10+
11+
set -euo pipefail
12+
13+
INPUT="${1:-}"
14+
JOB_PATTERN="${2:-}"
15+
WAIT_FOR_LOGS=false
16+
17+
# Parse flags
18+
if [[ "$JOB_PATTERN" == "--wait" ]]; then
19+
WAIT_FOR_LOGS=true
20+
JOB_PATTERN=""
21+
elif [[ "${3:-}" == "--wait" ]]; then
22+
WAIT_FOR_LOGS=true
23+
fi
24+
25+
if [[ -z "$INPUT" ]]; then
26+
echo "❌ Usage: $0 <pr_number_or_run_id> [job_name_pattern]" >&2
27+
echo "" >&2
28+
echo "Examples:" >&2
29+
echo " $0 329 # Latest failed run for PR #329 (RECOMMENDED)" >&2
30+
echo " $0 329 Integration # Only Integration Test jobs from PR #329" >&2
31+
echo " $0 18640062283 # Specific run ID" >&2
32+
exit 1
33+
fi
34+
35+
# Detect if input is PR number or run ID (run IDs are much longer)
36+
if [[ "$INPUT" =~ ^[0-9]{1,5}$ ]]; then
37+
PR_NUMBER="$INPUT"
38+
echo "🔍 Finding latest failed run for PR #$PR_NUMBER..." >&2
39+
40+
# Get the latest failed run for this PR
41+
RUN_ID=$(gh pr checks "$PR_NUMBER" --json name,link,state --jq '.[] | select(.state == "FAILURE") | .link' | head -1 | sed -E 's|.*/runs/([0-9]+).*|\1|' || echo "")
42+
43+
if [[ -z "$RUN_ID" ]]; then
44+
echo "❌ No failed runs found for PR #$PR_NUMBER" >&2
45+
echo "" >&2
46+
echo "Current check status:" >&2
47+
gh pr checks "$PR_NUMBER" 2>&1 || true
48+
exit 1
49+
fi
50+
51+
echo "📋 Found failed run: $RUN_ID" >&2
52+
else
53+
RUN_ID="$INPUT"
54+
echo "📋 Fetching logs for run $RUN_ID..." >&2
55+
fi
56+
57+
# Get all jobs for this run
58+
JOBS=$(gh run view "$RUN_ID" --json jobs -q '.jobs[]' 2>/dev/null)
59+
60+
if [[ -z "$JOBS" ]]; then
61+
echo "❌ No jobs found for run $RUN_ID" >&2
62+
echo "" >&2
63+
echo "Check if run ID is correct:" >&2
64+
echo " gh run list --limit 10" >&2
65+
exit 1
66+
fi
67+
68+
# Filter to failed jobs only (unless specific pattern requested)
69+
if [[ -z "$JOB_PATTERN" ]]; then
70+
FAILED_JOBS=$(echo "$JOBS" | jq -r 'select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED")')
71+
if [[ -n "$FAILED_JOBS" ]]; then
72+
echo "🎯 Showing only failed jobs (use job_pattern to see others)" >&2
73+
JOBS="$FAILED_JOBS"
74+
fi
75+
fi
76+
77+
# Parse jobs and filter by pattern if provided
78+
if [[ -n "$JOB_PATTERN" ]]; then
79+
MATCHING_JOBS=$(echo "$JOBS" | jq -r "select(.name | test(\"$JOB_PATTERN\"; \"i\")) | .databaseId")
80+
if [[ -z "$MATCHING_JOBS" ]]; then
81+
echo "❌ No jobs matching pattern '$JOB_PATTERN'" >&2
82+
echo "" >&2
83+
echo "Available jobs:" >&2
84+
echo "$JOBS" | jq -r '.name' >&2
85+
exit 1
86+
fi
87+
JOB_IDS="$MATCHING_JOBS"
88+
else
89+
JOB_IDS=$(echo "$JOBS" | jq -r '.databaseId')
90+
fi
91+
92+
# Map job names to local commands for reproduction
93+
suggest_local_command() {
94+
local job_name="$1"
95+
case "$job_name" in
96+
*"Static Checks"* | *"lint"* | *"typecheck"* | *"fmt"*)
97+
echo "💡 Reproduce locally: make static-check"
98+
;;
99+
*"Integration Tests"*)
100+
echo "💡 Reproduce locally: make test-integration"
101+
;;
102+
*"Test"*)
103+
echo "💡 Reproduce locally: make test"
104+
;;
105+
*"Build"*)
106+
echo "💡 Reproduce locally: make build"
107+
;;
108+
*"End-to-End"*)
109+
echo "💡 Reproduce locally: make test-e2e"
110+
;;
111+
esac
112+
}
113+
114+
# Extract and display logs for each job
115+
for JOB_ID in $JOB_IDS; do
116+
JOB_INFO=$(echo "$JOBS" | jq -r "select(.databaseId == $JOB_ID)")
117+
JOB_NAME=$(echo "$JOB_INFO" | jq -r '.name')
118+
JOB_STATUS=$(echo "$JOB_INFO" | jq -r '.conclusion // .status')
119+
120+
echo "" >&2
121+
echo "════════════════════════════════════════════════════════════" >&2
122+
echo "Job: $JOB_NAME (ID: $JOB_ID) - $JOB_STATUS" >&2
123+
echo "════════════════════════════════════════════════════════════" >&2
124+
125+
# Suggest local reproduction command
126+
suggest_local_command "$JOB_NAME" >&2
127+
echo "" >&2
128+
129+
# Fetch logs with retry logic if --wait flag is set
130+
MAX_RETRIES=3
131+
RETRY_COUNT=0
132+
133+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
134+
# Use gh api to fetch logs (works for individual completed jobs even if run is in progress)
135+
if gh api "/repos/coder/cmux/actions/jobs/$JOB_ID/logs" 2>/dev/null; then
136+
break
137+
else
138+
RETRY_COUNT=$((RETRY_COUNT + 1))
139+
if [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$WAIT_FOR_LOGS" = true ]; then
140+
echo "⏳ Logs not ready yet, waiting 5 seconds... (attempt $RETRY_COUNT/$MAX_RETRIES)" >&2
141+
sleep 5
142+
else
143+
echo "⚠️ Could not fetch logs for job $JOB_ID" >&2
144+
if [ "$WAIT_FOR_LOGS" = false ]; then
145+
echo " Tip: Use --wait flag to retry if logs are still processing" >&2
146+
else
147+
echo " (logs may have expired or are still processing)" >&2
148+
fi
149+
break
150+
fi
151+
fi
152+
done
153+
done

scripts/wait_pr_checks.sh

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ fi
2525
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
2626

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

3030
if [[ -z "$REMOTE_BRANCH" ]]; then
3131
echo "❌ Error: Current branch '$CURRENT_BRANCH' has no upstream branch." >&2
@@ -119,14 +119,18 @@ while true; do
119119
echo "❌ Some checks failed:"
120120
echo ""
121121
gh pr checks "$PR_NUMBER"
122+
echo ""
123+
echo "💡 To extract detailed logs from the failed run:"
124+
echo " ./scripts/extract_pr_logs.sh $PR_NUMBER"
125+
echo " ./scripts/extract_pr_logs.sh $PR_NUMBER <job_pattern> # e.g., Integration"
122126
exit 1
123127
fi
124128

125129
# Check for unresolved review comments in the hot loop
126130
if ! ./scripts/check_pr_reviews.sh "$PR_NUMBER" >/dev/null 2>&1; then
127131
echo ""
128132
echo "❌ Unresolved review comments found!"
129-
echo " 👉 Tip: run ./scripts/check_pr_reviews.sh "$PR_NUMBER" to list them."
133+
echo " 👉 Tip: run ./scripts/check_pr_reviews.sh $PR_NUMBER to list them."
130134
./scripts/check_pr_reviews.sh "$PR_NUMBER"
131135
exit 1
132136
fi
@@ -147,7 +151,7 @@ while true; do
147151
else
148152
echo ""
149153
echo "❌ Please resolve Codex comments before merging."
150-
echo " 👉 Tip: use ./scripts/check_pr_reviews.sh "$PR_NUMBER" to list unresolved comments."
154+
echo " 👉 Tip: use ./scripts/check_pr_reviews.sh $PR_NUMBER to list unresolved comments."
151155
exit 1
152156
fi
153157
elif [ "$MERGE_STATE" = "BLOCKED" ]; then

0 commit comments

Comments
 (0)