Skip to content
Closed
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
61 changes: 54 additions & 7 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ on:
permissions:
contents: write

# Serialize auto-tag runs per branch to prevent race conditions when
# multiple version bumps land on main in quick succession.
concurrency:
group: auto-tag-${{ github.ref_name }}
cancel-in-progress: false

jobs:
tag:
runs-on: ubuntu-latest
Expand All @@ -39,11 +45,20 @@ jobs:
- name: Read workspace version
id: ver
run: |
# Robust version parsing: extract version from workspace Cargo.toml
# and validate it matches semver format vX.Y.Z (without v prefix).
v="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')"
if [ -z "$v" ]; then
echo "::error::Could not parse workspace version from Cargo.toml" >&2
exit 1
fi

# Validate semver format (digits.digits.digits)
if ! echo "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Workspace version '$v' is not valid semver (expected X.Y.Z)" >&2
exit 1
fi

echo "version=$v" >> "$GITHUB_OUTPUT"
echo "tag=v$v" >> "$GITHUB_OUTPUT"
echo "Workspace version: $v"
Expand All @@ -54,18 +69,26 @@ jobs:
TAG: ${{ steps.ver.outputs.tag }}
run: |
git fetch --tags --quiet
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null \
|| git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then

# Check both local tags and remote tags for idempotency
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null 2>&1 || \
git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} already exists; nothing to do."
echo "::notice::Tag ${TAG} already exists; nothing to do (idempotent)."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} does not exist; will create."
fi

- name: Verify version consistency
if: steps.check.outputs.exists == 'false'
run: ./scripts/release/check-versions.sh
run: |
# Run version consistency check before creating tag
# This prevents tagging inconsistent versions
./scripts/release/check-versions.sh || {
echo "::error::Version consistency check failed. Aborting tag creation." >&2
exit 1
}

- name: Create and push tag
if: steps.check.outputs.exists == 'false'
Expand All @@ -74,9 +97,33 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag "${TAG}"
git push origin "${TAG}"
echo "Pushed ${TAG}. release.yml should now run (requires RELEASE_TAG_PAT for trigger)."

# Create annotated tag with release information
git tag -a "${TAG}" -m "Release ${TAG}

Automatically created by auto-tag workflow.
Commit: ${GITHUB_SHA}
Repository: ${GITHUB_REPOSITORY}
"

# Push with retry logic for network resilience
max_retries=3
retry_count=0
while [[ ${retry_count} -lt ${max_retries} ]]; do
if git push origin "${TAG}" 2>/dev/null; then
echo "Successfully pushed ${TAG}."
echo "release.yml should now run (requires RELEASE_TAG_PAT for trigger)."
exit 0
fi
retry_count=$((retry_count + 1))
if [[ ${retry_count} -lt ${max_retries} ]]; then
echo "Push attempt ${retry_count} failed; retrying in 10s..."
sleep 10
fi
done

echo "::error::Failed to push tag ${TAG} after ${max_retries} attempts." >&2
exit 1

- name: Warn if PAT missing
if: steps.check.outputs.exists == 'false' && env.HAS_PAT != 'true'
Expand Down
93 changes: 91 additions & 2 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ on:
permissions:
contents: read

# Prevent concurrent nightly builds for the same branch; cancel older runs
# when a new push arrives. The group key includes the branch name so that
# nightly builds on main are serialized, but manual dispatches from other
# branches can still run independently.
concurrency:
group: nightly-${{ github.ref }}
group: nightly-${{ github.ref_name }}
cancel-in-progress: true

env:
Expand All @@ -18,7 +22,61 @@ env:
DEEPSEEK_BUILD_SHA: ${{ github.sha }}

jobs:
# Idempotency guard: skip the entire workflow if nightly artifacts for
# this exact commit already exist. This prevents wasteful rebuilds when
# multiple commits land on main in quick succession or when a workflow
# is re-run manually.
check-artifacts:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check.outputs.skip }}
steps:
- uses: actions/checkout@v4
- name: Check if nightly artifacts already exist
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_SHA: ${{ github.sha }}
run: |
short_sha="${COMMIT_SHA::12}"
# Check for any artifact from this commit in the last 14 days
# (matching the retention-days setting below). If all platform
# artifacts exist, we can safely skip the build.
artifact_count=$(gh run list --workflow nightly.yml \
--branch main \
--created ">=$(date -d '14 days ago' +%Y-%m-%d)" \
--json databaseId \
--jq 'length')

if [[ "${artifact_count}" -gt 0 ]]; then
latest_run_id=$(gh run list --workflow nightly.yml \
--branch main \
--created ">=$(date -d '14 days ago' +%Y-%m-%d)" \
--json databaseId,headSha \
--jq '[.[] | select(.headSha == env.COMMIT_SHA)] | first | .databaseId' 2>/dev/null || true)

if [[ -n "${latest_run_id}" ]]; then
artifacts=$(gh run view "${latest_run_id}" --json artifacts --jq '.artifacts | length')
# We expect 10 artifacts (5 platforms × 2 binaries: codewhale + codewhale-tui)
if [[ "${artifacts}" -ge 10 ]]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Nightly artifacts for commit ${short_sha} already exist; skipping build."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "Nightly artifacts incomplete (${artifacts}/10); rebuilding."
fi
else
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "No nightly run found for this commit; building."
fi
else
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "No recent nightly runs found; building."
fi

build:
needs: [check-artifacts]
if: ${{ needs.check-artifacts.outputs.skip != 'true' }}
name: Build ${{ matrix.artifact_name }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -86,7 +144,20 @@ jobs:
sudo apt-get install -y libdbus-1-dev pkg-config
- name: Build
shell: bash
run: cargo build --release --locked --target ${{ matrix.target }}
run: |
# Retry build up to 2 times on transient failure (e.g. network
# issues fetching crates, OOM kills on shared runners).
for attempt in 1 2 3; do
if cargo build --release --locked --target ${{ matrix.target }}; then
exit 0
fi
if [[ ${attempt} -lt 3 ]]; then
echo "Build attempt ${attempt} failed; retrying in 30s..."
sleep 30
fi
done
echo "Build failed after 3 attempts" >&2
exit 1
- name: Stage artifact
id: stage
shell: bash
Expand All @@ -113,3 +184,21 @@ jobs:
name: ${{ steps.stage.outputs.name }}
path: nightly/*
retention-days: 14

# Summary job that aggregates the matrix build results and provides a
# single status indicator for branch protection rules.
nightly-complete:
needs: [build]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check matrix build status
run: |
if [[ "${{ needs.build.result }}" == "success" ]]; then
echo "✅ All nightly builds completed successfully"
elif [[ "${{ needs.build.result }}" == "skipped" ]]; then
echo "ℹ️ Nightly builds were skipped (artifacts already exist)"
else
echo "❌ Nightly builds failed or were cancelled"
exit 1
fi
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Check the [releases page](https://github.com/Hmbown/CodeWhale/releases) for the
Report privately via one of:

- **GitHub private advisory**: [github.com/Hmbown/CodeWhale/security/advisories/new](https://github.com/Hmbown/CodeWhale/security/advisories/new)
- **Email**: [security@deepseek-tui.com](mailto:security@deepseek-tui.com) — include `[SECURITY]` in the subject line
- **Email**: [security@codewhale.com](mailto:security@codewhale.com) — include `[SECURITY]` in the subject line

Include in your report:

Expand Down
1 change: 1 addition & 0 deletions codewhale
Submodule codewhale added at 5dffec
6 changes: 6 additions & 0 deletions crates/tui/src/core/engine/tool_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ pub(super) const DEFAULT_ACTIVE_NATIVE_TOOLS: &[&str] = &[
"apply_patch",
"checklist_write",
"edit_file",
"exec_interact",
"exec_shell",
"exec_shell_interact",
"exec_shell_wait",
"exec_wait",
"fetch_url",
"file_search",
"git_diff",
Expand All @@ -46,6 +50,8 @@ pub(super) const DEFAULT_ACTIVE_NATIVE_TOOLS: &[&str] = &[
"task_create",
"task_list",
"task_read",
"task_shell_start",
"task_shell_wait",
"update_plan",
"web_search",
"write_file",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const DISPATCH_WATCHDOG_TIMEOUT: Duration = Duration::from_secs(30);
// the per-tool spinner pulse — keep this fast enough that the spout reads as
// motion (~12 fps) instead of teleport-frames.
const UI_STATUS_ANIMATION_MS: u64 = 80;
const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 100;
const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 60;
const DEFAULT_TERMINAL_PROBE_TIMEOUT_MS: u64 = 500;
const PERIODIC_FULL_REPAINT_EVERY_N: u64 = 50;
const TURN_META_PREFIX: &str = "<turn_meta>";
Expand Down
70 changes: 61 additions & 9 deletions crates/tui/src/tui/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,8 +1182,20 @@ impl Renderable for ApprovalWidget<'_> {
lines.push(Line::from(""));

let options = approval_options_for(risk, locale);
let separator = locale_separator(locale);

for (i, opt) in options.iter().enumerate() {
// Insert a visual separator between the approve group (0-1)
// and the deny/abort group (2-3) so the decision surface
// reads as two distinct choice clusters rather than a flat
// list.
if i == 2 {
lines.push(Line::from(vec![Span::styled(
separator,
Style::default().fg(palette::TEXT_MUTED),
)]));
}
Comment on lines +1185 to +1197

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The separator line is currently hardcoded to 40 characters. However, the approval card width is dynamic and can be as small as APPROVAL_CARD_MIN_WIDTH (40), which results in a content width of 36. In such cases, the 40-character separator line will wrap to the next line, breaking the visual layout.

We can dynamically construct the separator line using card_area.width to ensure it perfectly spans the available content width without wrapping.

Suggested change
let separator = locale_separator(locale);
for (i, opt) in options.iter().enumerate() {
// Insert a visual separator between the approve group (0-1)
// and the deny/abort group (2-3) so the decision surface
// reads as two distinct choice clusters rather than a flat
// list.
if i == 2 {
lines.push(Line::from(vec![Span::styled(
separator,
Style::default().fg(palette::TEXT_MUTED),
)]));
}
let content_width = card_area.width.saturating_sub(4) as usize;
for (i, opt) in options.iter().enumerate() {
// Insert a visual separator between the approve group (0-1)
// and the deny/abort group (2-3) so the decision surface
// reads as two distinct choice clusters rather than a flat
// list.
if i == 2 {
let sep_chars = "─".repeat(content_width.saturating_sub(2));
lines.push(Line::from(vec![Span::styled(
format!(" {sep_chars}"),
Style::default().fg(palette::TEXT_MUTED),
)]));
}


let is_selected = i == self.view.selected();
let label_color = if opt.dangerous {
palette_colors.accent
Expand All @@ -1194,15 +1206,41 @@ impl Renderable for ApprovalWidget<'_> {
let option_style = approval_option_style(is_selected, label_color);
let shortcut_style = approval_option_style(is_selected, palette_colors.shortcut);

let spans = vec![
Span::raw(" "),
Span::styled(
format!("[{}] ", opt.key_hint),
shortcut_style.add_modifier(Modifier::BOLD),
),
Span::styled(opt.label.to_string(), option_style),
];
lines.push(Line::from(spans));
if is_selected {
// Selected option: render as a solid button row with a
// distinct background strip so the user can instantly see
// which action will fire on Enter.
lines.push(Line::from(vec![
Span::styled(" ", Style::default().bg(palette::SELECTION_BG)),
Span::styled(
format!("[{}] ", opt.key_hint),
shortcut_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
),
Span::styled(
opt.label.to_string(),
option_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
),
Span::styled(" ", Style::default().bg(palette::SELECTION_BG)),
Span::styled(
selected_indicator(locale),
Style::default()
.fg(palette::SELECTION_TEXT)
.bg(palette::SELECTION_BG)
.add_modifier(Modifier::BOLD),
),
]));
Comment on lines +1209 to +1231

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The selected option background is currently applied only to the individual text spans. This means the background highlight will only cover the text itself, rather than forming a solid, full-width button row as described in the PR. Additionally, the ◄ EXECUTE indicator is placed immediately after the label instead of being right-aligned to the edge of the card.

We can resolve both issues by calculating the remaining width of the row and inserting a padding span of spaces with the SELECTION_BG background between the label and the indicator. This right-aligns the indicator and extends the background highlight across the entire width of the card.

            if is_selected {
                // Selected option: render as a solid button row with a
                // distinct background strip so the user can instantly see
                // which action will fire on Enter.
                let shortcut_text = format!("[{}] ", opt.key_hint);
                let label_text = opt.label.to_string();
                let indicator_text = selected_indicator(locale);
                let text_width = 2 + shortcut_text.width() + label_text.width() + indicator_text.width();
                let padding_width = content_width.saturating_sub(text_width);

                lines.push(Line::from(vec![
                    Span::styled("  ", Style::default().bg(palette::SELECTION_BG)),
                    Span::styled(
                        shortcut_text,
                        shortcut_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
                    ),
                    Span::styled(
                        label_text,
                        option_style.add_modifier(Modifier::BOLD).bg(palette::SELECTION_BG),
                    ),
                    Span::styled(
                        " ".repeat(padding_width),
                        Style::default().bg(palette::SELECTION_BG),
                    ),
                    Span::styled(
                        indicator_text,
                        Style::default()
                            .fg(palette::SELECTION_TEXT)
                            .bg(palette::SELECTION_BG)
                            .add_modifier(Modifier::BOLD),
                    ),
                ]));
            } else {

} else {
// Unselected option: plain row with subtle shortcut emphasis.
let spans = vec![
Span::raw(" "),
Span::styled(
format!("[{}] ", opt.key_hint),
shortcut_style.add_modifier(Modifier::BOLD),
),
Span::styled(opt.label.to_string(), option_style),
];
lines.push(Line::from(spans));
}
}

// Footer: Enter commits the highlighted row; y/a/d remain direct
Expand Down Expand Up @@ -1481,6 +1519,20 @@ fn option_abort(locale: Locale) -> &'static str {
}
}

fn locale_separator(locale: Locale) -> &'static str {
match locale {
Locale::ZhHans => " ────────────────────────────────────────",
_ => " ────────────────────────────────────────",
}
}
Comment on lines +1522 to +1527

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the separator line is now constructed dynamically based on the card width, the locale_separator helper function is no longer used and can be safely removed.


fn selected_indicator(locale: Locale) -> &'static str {
match locale {
Locale::ZhHans => " ◄ 执行",
_ => " ◄ EXECUTE",
}
}

pub struct ElevationWidget<'a> {
request: &'a ElevationRequest,
selected: usize,
Expand Down
9 changes: 9 additions & 0 deletions fix-edit_file-fuzz.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The optional `fuzz` parameter was required to attempt the leading-indentation fuzzy fallback when exact search found zero matches. This forced the model to make two calls on every edit that needed fuzzy matching (first without fuzz -> error -> second with fuzz: true), causing a round-trip delay.

Fix: remove the `fuzz` gate from the count == 0 branch. The tool now automatically retries with indentation-tolerant fuzzy matching when exact search produces no results. The `fuzz` parameter is kept in the schema for backward compatibility but marked deprecated.

Changes:
- crates/tui/src/tools/file.rs: `if count == 0 && fuzz` -> `if count == 0` (always retry fuzzy fallback)
- crates/tui/src/tools/file.rs: removed dead `else if count == 0 { error }` branch
- crates/tui/src/tools/file.rs: updated description to note automatic fuzzy fallback
- crates/tui/src/tools/file.rs: marked fuzz parameter as deprecated in schema
Loading