diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index f73794482d..173f1ff2ec 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -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 @@ -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" @@ -54,10 +69,12 @@ 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." @@ -65,7 +82,13 @@ jobs: - 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' @@ -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' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 53fcd34a71..bece6780b8 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -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: @@ -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 @@ -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 @@ -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 diff --git a/codewhale b/codewhale new file mode 160000 index 0000000000..5dffecef40 --- /dev/null +++ b/codewhale @@ -0,0 +1 @@ +Subproject commit 5dffecef40a876cae8d1bdf27750fb48183ae76a diff --git a/crates/tui/src/core/engine/tool_catalog.rs b/crates/tui/src/core/engine/tool_catalog.rs index 65b194ce03..3a44619e77 100644 --- a/crates/tui/src/core/engine/tool_catalog.rs +++ b/crates/tui/src/core/engine/tool_catalog.rs @@ -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", @@ -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", diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index fb89de619a..14530bf30d 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -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 = ""; diff --git a/fix-edit_file-fuzz.patch b/fix-edit_file-fuzz.patch new file mode 100644 index 0000000000..7d69aae87a --- /dev/null +++ b/fix-edit_file-fuzz.patch @@ -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 diff --git a/fix_engine.py b/fix_engine.py new file mode 100644 index 0000000000..556066307c --- /dev/null +++ b/fix_engine.py @@ -0,0 +1,39 @@ +import re + +file_path = r'C:\project\F_project1\CodeWhale\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 = ' "\n )\n}\n\n/// Spawn the engine' +helper_fn = ''' " + ) +} + +/// 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("", "") + .replace("", "") + .replace("", "") + .replace("", ""); + + // Check if there's non-whitespace content after stripping tags + let trimmed = stripped.trim(); + !trimmed.is_empty() && trimmed.len() > 10 // Allow for minimal metadata +} + +/// 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) diff --git a/pr-body-agents.md b/pr-body-agents.md new file mode 100644 index 0000000000..065bf3ca34 --- /dev/null +++ b/pr-body-agents.md @@ -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. diff --git a/pr-body-cjk.md b/pr-body-cjk.md new file mode 100644 index 0000000000..4204ad8553 --- /dev/null +++ b/pr-body-cjk.md @@ -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 diff --git a/pr-body-fuzz.md b/pr-body-fuzz.md new file mode 100644 index 0000000000..7d69aae87a --- /dev/null +++ b/pr-body-fuzz.md @@ -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 diff --git a/pr-body-paste.md b/pr-body-paste.md new file mode 100644 index 0000000000..51c84c445d --- /dev/null +++ b/pr-body-paste.md @@ -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 diff --git a/pr-body-rlm.md b/pr-body-rlm.md new file mode 100644 index 0000000000..7dd7c6c47a --- /dev/null +++ b/pr-body-rlm.md @@ -0,0 +1,9 @@ +RLM sessions that produce large stdout/stderr (e.g. reading a local log file, dumping a large JSON table, or printing diagnostic output) currently inline the full preview into the parent tool result. On long-running RLM sessions this bloat accumulates and pressures the parent context window. + +Fix: when `rlm_eval` stdout or stderr exceeds 1000 characters, the full body is stored as a `var_handle` in the handle store. The tool result returns a short inline note (`"N chars; retrieve via handle_read"`) plus `stdout_handle` / `stderr_handle` fields containing the handle reference. The model calls `handle_read` for bounded projections. + +Changes: +- `rlm.rs`: Added `STDOUT_HANDLE_THRESHOLD_CHARS` constant +- `rlm.rs`: Added `route_output()` helper that stores large text as a var_handle +- `rlm.rs`: Modified `rlm_eval` execute to route stdout/stderr >= 1k chars into handles +- `rlm.rs`: Updated description to document the new handle-routing behavior diff --git a/pr-body-voice.md b/pr-body-voice.md new file mode 100644 index 0000000000..414b4c544f --- /dev/null +++ b/pr-body-voice.md @@ -0,0 +1,32 @@ +## Summary + +Validate terminal-safe voice shortcut and STT helper setup for the voice input feature shipping in v0.8.45/v0.8.46. + +## Completed + +- **`voice_input.rs`**: Added `diagnose_voice_setup()` async function that checks: + - Missing/invalid `voice_input_command` config + - Inexecutable binary (spawns `--version` as probe) + - Permission/spawn failures with clear error messages +- **`voice_input.rs`**: Added `terminal_detection_tests` module with: + - `chord_likely_reaches_tui()` heuristic function documenting known-consumed chords + - Tests for common chords (Ctrl-K consumed, Ctrl-L/Ctrl-C safe) + - Candidate shortcut test matrix (F2, F3, Alt-Space, Ctrl-] etc.) +- **`docs/VOICE_INPUT_TERMINALS.md`**: Terminal compatibility matrix documenting Ctrl-K behavior across 13 terminal emulators + STT helper setup checklist + recommended safe chords +- **`docs/KEYBINDINGS.md`**: Added Voice input section documenting the Ctrl-K caveat with cross-reference to the new terminal matrix doc + +## Not completed (needs manual verification) + +- **Actual terminal testing** on macOS Terminal.app, iTerm2, Ghostty, Warp, Windows Terminal, Linux terminals +- **Final default chord selection** — the matrix lists candidates but the final binding needs human verification +- **Manual QA checklist** for terminals that consume modifier keys +- **Compilation verification** — changes made to a feature branch (work/v0.8.45-flash) without Rust CI available + +## Files changed + +``` +crates/tui/src/tui/voice_input.rs | 110 +++++++++++++++++ +docs/KEYBINDINGS.md | 22 ++++ +docs/VOICE_INPUT_TERMINALS.md | 87 +++++++++++++ +3 files changed, 219 insertions(+) +``` diff --git a/pr-body.md b/pr-body.md new file mode 100644 index 0000000000..8eb17c9974 --- /dev/null +++ b/pr-body.md @@ -0,0 +1,30 @@ +## Summary + +Add real-time incremental output display for shell execution commands. The TUI now displays shell output **while the command is still running**, instead of hiding all output until completion. + +## Changes + +- **`ExecCell`**: Added `live_output: Option` field to store incremental output during execution +- **`history.rs`**: Modified render logic to show `live_output` when `output` is not yet available (priority: final output > live output > hints) +- **`app.rs`**: Added `poll_shell_progress()` method that polls `ShellManager` during idle frames and updates matching `ExecCell` entries +- **`ui.rs`**: Wired `poll_shell_progress()` into the event loop after `tick_quit_armed()` +- **All ExecCell construction sites**: Added `live_output: None` to 8 files + +## How it works + +1. During idle frames, the TUI polls `ShellManager.list_jobs()` for running shell processes +2. Running exec cells are matched to shell jobs by command prefix +3. Live output (tail of stdout/stderr) is written to `ExecCell.live_output` +4. The renderer displays it in the transcript immediately + +## Files changed +``` +crates/tui/src/tui/active_cell.rs | 1 + +crates/tui/src/tui/app.rs | 69 +++++++++++++++++++++++++++++ +crates/tui/src/tui/history.rs | 11 ++++ +crates/tui/src/tui/sidebar.rs | 5 ++ +crates/tui/src/tui/tool_routing.rs | 2 + +crates/tui/src/tui/transcript.rs | 1 + +crates/tui/src/tui/ui.rs | 2 + +crates/tui/src/tui/ui/tests.rs | 3 + +``` diff --git a/shell-live-progress.patch b/shell-live-progress.patch new file mode 100644 index 0000000000..e69de29bb2