Skip to content
Open
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
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;

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

Reducing SIDEBAR_VISIBLE_MIN_WIDTH to 60 allows the sidebar to be displayed on very narrow terminals. However, at 60 columns, splitting the screen between the chat area and the sidebar (and potentially the file tree) will make the UI extremely cramped and unreadable. Consider keeping a higher minimum width (e.g., 80 or 100) to ensure a usable layout.

Suggested change
const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 60;
const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 80;

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
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.

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

Committing raw .patch files to the repository to apply hotfixes is generally an anti-pattern. It is highly recommended to apply these changes directly to the source files (e.g., crates/tui/src/tools/file.rs) so they are properly tracked by git and part of the codebase.


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
39 changes: 39 additions & 0 deletions fix_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import re

file_path = r'C:\project\F_project1\CodeWhale\crates\tui\src\core\engine.rs'

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.

high

The file path is hardcoded to an absolute Windows path (C:\\project\F_project1\CodeWhale\...), which will fail on any other machine or in CI environments. Consider using a relative path or, better yet, applying these changes directly to crates/tui/src/core/engine.rs instead of using a script to patch it.

Suggested change
file_path = r'C:\project\F_project1\CodeWhale\crates\tui\src\core\engine.rs'
file_path = 'crates/tui/src/core/engine.rs'

with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

# 1. Add helper function after runtime_prompt_text
marker = ' </runtime_prompt>"\n )\n}\n\n/// Spawn the engine'
helper_fn = ''' </runtime_prompt>"
)
}

/// Check if a user message contains real user input (not just runtime metadata).
/// Returns true if the message has actual user text content beyond internal tags.
fn has_real_user_content(text: &str) -> bool {
// Strip known internal tags and check if meaningful content remains
let stripped = text
.replace("<turn_meta>", "")
.replace("</turn_meta>", "")
.replace("<runtime_prompt", "")
.replace("</runtime_prompt>", "")
.replace("<codewhale:runtime_event", "")
.replace("</codewhale:runtime_event>", "");
Comment on lines +20 to +23

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 .replace("<runtime_prompt", "") and .replace("<codewhale:runtime_event", "") calls do not match the closing > of the opening tags. This will leave trailing > characters (and any attributes) in the stripped string, which can interfere with the content check.

Suggested change
.replace("<runtime_prompt", "")
.replace("</runtime_prompt>", "")
.replace("<codewhale:runtime_event", "")
.replace("</codewhale:runtime_event>", "");
.replace("<runtime_prompt>", "")
.replace("</runtime_prompt>", "")
.replace("<codewhale:runtime_event>", "")
.replace("</codewhale:runtime_event>", "");


// Check if there's non-whitespace content after stripping tags
let trimmed = stripped.trim();
!trimmed.is_empty() && trimmed.len() > 10 // Allow for minimal metadata

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.

high

The condition trimmed.len() > 10 will incorrectly return false for any legitimate user message that is 10 characters or fewer (e.g., "Hello", "yes", "no", "help"). If the internal tags are already stripped, any remaining non-whitespace content should be considered real user content. Consider removing the length check and only checking !trimmed.is_empty().

Suggested change
!trimmed.is_empty() && trimmed.len() > 10 // Allow for minimal metadata
!trimmed.is_empty()

}

/// Spawn the engine'''

if marker in content:
content = content.replace(marker, helper_fn)
print("OK: helper function added")
else:
print("WARN: marker not found for helper function")

with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
33 changes: 33 additions & 0 deletions pr-body-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Summary

Auto-collapse completed sub-agents in the Agents sidebar panel. Non-running agents now show only a single line (label), freeing vertical space for active agents.

## Problem

Completed/failed/interrupted/cancelled sub-agents each occupied **2 lines** in the sidebar (label + detail line), wasting space that could be used for running agents or other content. With many agents, the sidebar became unnecessarily crowded.

## Solution

In `subagent_panel_lines()`, check the agent status before rendering the detail line. If the agent is not running (i.e. completed, failed, interrupted, cancelled), skip the detail line entirely and only render the single-line label.

**Before:**
```
✓ explore foo ← 2 lines per agent
abc123 · 3 steps · 12.3s
✗ build failed ← 2 lines per agent
def456 · 7 steps · 45.6s
● analysis running ← 2 lines per agent
ghi789 · 2 steps · 5.1s · parsing output...
```

**After:**
```
✓ explore foo ← 1 line (collapsed)
✗ build failed ← 1 line (collapsed)
● analysis running ← 2 lines (expanded: label + detail)
ghi789 · 2 steps · 5.1s · parsing output...
```

## File changed

`crates/tui/src/tui/sidebar.rs` — 5 lines added: `is_completed` check + early `continue` before the detail line.
27 changes: 27 additions & 0 deletions pr-body-cjk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Bug

When `assistant_text` contains CJK characters (3-byte UTF-8), the byte slice
`&assistant_text[..SUMMARY_LIMIT.saturating_sub(3)]` (byte 277) can land in
the middle of a multi-byte sequence, causing a panic:

```
byte index 277 is not a char boundary (it is inside a 3-byte UTF-8 sequence)
```

## Fix

Replace the raw byte-index slice with `truncate_with_ellipsis()` which already
finds the nearest safe char boundary via `char_indices()`.

## Change

```diff
-format!("{}...", &assistant_text[..SUMMARY_LIMIT.saturating_sub(3)])
+crate::utils::truncate_with_ellipsis(&assistant_text, SUMMARY_LIMIT, "...")
```

1 line changed.

## Files

- crates/tui/src/runtime_threads.rs:1437
9 changes: 9 additions & 0 deletions pr-body-fuzz.md
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
21 changes: 21 additions & 0 deletions pr-body-paste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Problem

Pasting large text into the CodeWhale TUI composer (e.g. pasting a 20KB log file or code block) immediately converts it to an `@.deepseek/pastes/paste-xxx.md` mention. The user's text vanishes from the composer and is replaced by a cryptic `@file` reference they did not ask for. This is confusing — the user expected to see their pasted text and be able to review/edit it before sending.

## Root Cause

`insert_paste_text()` calls `consolidate_large_input_if_oversized()` at paste time, which checks if `char_count(input) > MAX_SUBMITTED_INPUT_CHARS` (16,000 chars). When exceeded, it immediately writes the text to a paste file and replaces the composer input with `@.deepseek/pastes/paste-xxx.md`.

The consolidation is useful at submit time (safety net), but at paste time it is surprising and confusing.

## Fix

1. **Remove the immediate consolidation from `insert_paste_text()`** — the text now stays in the composer as-is after pasting.
2. **Keep the consolidation as a safety net at submit time** — the same logic already runs when the user presses Enter, so the model still gets the `@file` reference if needed.
3. **Improve the toast message** — explain what happened when the submit-time consolidation fires.

The user now sees their full pasted text in the composer and can review/edit before sending. The consolidation only triggers when they press Enter if the text is still over the 16K char limit.

## Files

- `crates/tui/src/tui/app.rs`: commented out `self.consolidate_large_input_if_oversized()` in `insert_paste_text`, updated toast message
Loading