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
26 changes: 26 additions & 0 deletions .github/clawpatch/factory-zc.config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Isolated ZeroClaw config for the clawpatch CI "brain" (the agent that reviews PRs).
# __ZC_TOKEN__ is replaced at CI time from the ANTHROPIC_OAUTH_TOKEN repo secret.
# The Anthropic provider auto-detects the sk-ant-oat01- prefix and uses the OAuth path.
schema_version = 3

[providers.models.anthropic.factory]
model = "claude-sonnet-4-6"
api_key = "__ZC_TOKEN__"

[agents.factory]
model_provider = "anthropic.factory"
enabled = true
risk_profile = "default"
runtime_profile = "default"

# Review is read-only; a default (supervised) risk profile is sufficient for CI.
[risk_profiles.default]

[runtime_profiles.default]
agentic = true
max_tool_iterations = 50

[acp]
default_agent = "factory"
max_sessions = 5
session_timeout_secs = 3600
85 changes: 85 additions & 0 deletions .github/workflows/clawpatch-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: clawpatch (zeroclaw self-review)

# Reviews the code changed in each PR using ZeroClaw's OWN coding agent as the brain:
# clawpatch (provider=acpx) -> acpx -> `zeroclaw acp` (stdio JSON-RPC) -> ZeroClaw agent -> Claude
#
# Requirements:
# - Repo secret ANTHROPIC_OAUTH_TOKEN: a Claude subscription OAuth token (sk-ant-oat01-...,
# minted with `claude setup-token`). ZeroClaw's Anthropic provider auto-detects the prefix.
#
# Behavior: non-blocking. Findings are posted to the job summary and uploaded as an artifact.
# This is a best-effort first cut — validate on a real PR run and tune timeouts/model as needed.

on:
pull_request:
branches: [master]

permissions:
contents: read

concurrency:
group: clawpatch-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install zeroclaw (prebuilt) + acpx + clawpatch
run: |
set -euo pipefail
ZC_VERSION="v0.8.0"
curl -fsSL "https://github.com/zeroclaw-labs/zeroclaw/releases/download/${ZC_VERSION}/zeroclaw-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo install -m 0755 zeroclaw /usr/local/bin/zeroclaw
zeroclaw --version
# acpx pinned to clawpatch's tested range; clawpatch from npm (swap for your fork if desired)
npm install -g 'acpx@^0.8.0' clawpatch
acpx --version
clawpatch --version

- name: Configure the zeroclaw ACP brain
env:
ZC_TOKEN: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${ZC_TOKEN:-}" ]; then
echo "::warning::ANTHROPIC_OAUTH_TOKEN secret is not set — the review step will be skipped."
fi
ZC_DIR="$RUNNER_TEMP/factory-zc"
mkdir -p "$ZC_DIR"
umask 077
sed "s|__ZC_TOKEN__|${ZC_TOKEN:-}|" .github/clawpatch/factory-zc.config.toml > "$ZC_DIR/config.toml"
mkdir -p "$HOME/.acpx"
printf '{"defaultAgent":"zeroclaw","agents":{"zeroclaw":{"command":"zeroclaw","args":["acp","--config-dir","%s"]}}}\n' "$ZC_DIR" > "$HOME/.acpx/config.json"
echo "ZC_DIR=$ZC_DIR" >> "$GITHUB_ENV"

- name: Review changed code via zeroclaw
continue-on-error: true
env:
CLAWPATCH_PROVIDER: acpx
CLAWPATCH_MODEL: zeroclaw
CLAWPATCH_ACPX_TIMEOUT_MS: '600000'
run: |
set -uo pipefail
clawpatch init || true
clawpatch ci --since "origin/${{ github.base_ref }}" --output clawpatch-report.md || true
if [ -s clawpatch-report.md ]; then
{ echo '## 🦞 clawpatch — zeroclaw self-review'; echo; cat clawpatch-report.md; } >> "$GITHUB_STEP_SUMMARY"
else
echo 'clawpatch produced no report (no changed features, or the brain was unavailable).' >> "$GITHUB_STEP_SUMMARY"
fi

- uses: actions/upload-artifact@v4
if: always()
with:
name: clawpatch-report
path: clawpatch-report.md
if-no-files-found: ignore
35 changes: 35 additions & 0 deletions .github/workflows/sync-upstream.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: sync fork with upstream

# Keeps this fork's `master` current with zeroclaw-labs/zeroclaw by merging upstream
# in daily. Fork-only files (the clawpatch CI) have no upstream counterpart, so the
# merge is clean; if a real conflict ever appears the run fails so you can resolve it.
on:
schedule:
- cron: "17 7 * * *"
workflow_dispatch: {}

permissions:
contents: write

jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Merge upstream/master into master
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add upstream https://github.com/zeroclaw-labs/zeroclaw.git
git fetch upstream master
BEFORE=$(git rev-parse HEAD)
git merge --no-edit upstream/master
if [ "$(git rev-parse HEAD)" != "$BEFORE" ]; then
git push origin HEAD:master
echo "synced: merged upstream/master into master"
else
echo "already up to date with upstream/master"
fi
4 changes: 4 additions & 0 deletions apps/zerocode/locales/en/zerocode.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ zc-pane-quickstart = Quickstart

zc-app-help-cycle-mode = Cycle mode
zc-app-help-reload = Reload daemon
zc-app-help-selection-mode = Selection mode (mouse copy/paste)
zc-app-help-native-select = Select text natively (most terminals)
zc-app-help-quit = Quit
zc-app-selection-mode = ▣ SELECTION MODE · drag to copy, paste with your terminal · { $key } to exit
zc-app-selection-mode-compact = ▣ { $key } = exit selection mode

zc-app-press-any-key-to-close = Press any key to close
zc-app-reload-line-1 = The daemon process stays running (same PID), but every
Expand Down
61 changes: 59 additions & 2 deletions apps/zerocode/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ pub async fn run(
let mut show_help = false;
let mut reload_confirm = false;
let mut quit_confirm = false;
// Whether the terminal is reporting mouse events to the TUI. Toggling this
// off (selection mode) hands the mouse back to the terminal so the user can
// drag-select and copy/paste natively. Mirrors the capture enabled at
// startup in `config_manager::init_terminal`.
let mut mouse_capture_on = true;
let mut reload_status: Option<String> = None;
let mut bar_area = Rect::default();
let mut content_area = Rect::default();
Expand Down Expand Up @@ -348,6 +353,7 @@ pub async fn run(
rpc.tui_id(),
CtxBar::new(ctx_input, ctx_max),
needs_intervention,
!mouse_capture_on,
);

// Help modal overlay (drawn last so it sits on top).
Expand All @@ -369,6 +375,14 @@ pub async fn run(
chord_keys(crate::keymap::GlobalAction::ReloadDaemon.resolved()),
crate::i18n::t("zc-app-help-reload"),
),
HelpEntry::new(
chord_keys(crate::keymap::GlobalAction::ToggleMouseCapture.resolved()),
crate::i18n::t("zc-app-help-selection-mode"),
),
HelpEntry::new(
vec!["Shift+drag".to_string()],
crate::i18n::t("zc-app-help-native-select"),
),
HelpEntry::new(
chord_keys(crate::keymap::GlobalAction::Quit.resolved()),
crate::i18n::t("zc-app-help-quit"),
Expand Down Expand Up @@ -594,6 +608,17 @@ pub async fn run(
continue;
}

// Selection mode: flip terminal mouse capture so the user can
// drag-select and copy/paste with the terminal's own clipboard.
// Intentionally not gated on `in_text_input` — it's a meta
// action that must work from any pane, even mid-compose — and it
// never falls through to a pane handler.
if global == Some(GlobalAction::ToggleMouseCapture) {
mouse_capture_on = !mouse_capture_on;
let _ = config_manager::set_mouse_capture(term, mouse_capture_on);
continue;
}

let switch_to: Option<Mode> = match global {
Some(GlobalAction::PaneNavLeft) => Some(mode.cycle(-1)),
Some(GlobalAction::PaneNavRight) => Some(mode.cycle(1)),
Expand Down Expand Up @@ -790,6 +815,7 @@ fn draw_status_bar(
tui_id: Option<&str>,
ctx: CtxBar,
needs_intervention: bool,
selection_mode: bool,
) {
let (dot, label, style) = match state {
ConnectionState::Connected => (
Expand Down Expand Up @@ -840,8 +866,39 @@ fn draw_status_bar(
spans.push(Span::styled(label, style));
frame.render_widget(Paragraph::new(Line::from(spans)), right_area);

// Left: ctx bar, left-aligned in its own column. The bar is held back
// until the context-accounting feature is ready to show; there is no
// Left: selection-mode banner takes priority — when mouse capture is off
// the user needs a visible reminder of the mode and how to leave it. The
// exit key is read from the resolved keymap (`resolved_bindings`), so a
// rebind of the toggle stays accurate — matching the help modal.
if selection_mode {
use unicode_width::UnicodeWidthStr;
let exit_key = chords_for(
GlobalAction::resolved_bindings(),
GlobalAction::ToggleMouseCapture,
);
let full = crate::i18n::t_args("zc-app-selection-mode", &[("key", &exit_key)]);
// When disconnected, the connection status can crowd the left column;
// fall back to a compact banner that leads with the exit key so the
// one instruction for leaving the mode is never the part clipped.
let text = if UnicodeWidthStr::width(full.as_str()) as u16 <= left_area.width {
full
} else {
crate::i18n::t_args("zc-app-selection-mode-compact", &[("key", &exit_key)])
};
frame.render_widget(
Paragraph::new(Span::styled(
text,
Style::default()
.fg(Color::Rgb(255, 200, 80))
.add_modifier(Modifier::BOLD),
)),
left_area,
);
return;
}

// Otherwise the ctx bar, left-aligned in its own column. The bar is held
// back until the context-accounting feature is ready to show; there is no
// user-facing switch — the gate flips when the work lands.
const SHOW_CTX_BAR: bool = false;
if SHOW_CTX_BAR && let Some(w) = ctx.widget() {
Expand Down
17 changes: 17 additions & 0 deletions apps/zerocode/src/config_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ pub(crate) fn init_terminal() -> Result<Term> {
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
}

/// Toggle terminal mouse-event reporting at runtime.
///
/// With capture on (the default), the TUI receives mouse events and the
/// terminal's own click-drag text selection is suppressed — so a user cannot
/// select/copy with the mouse the usual way. Turning capture off hands the
/// mouse back to the terminal for native selection, copy, and paste; the app
/// stops seeing mouse events until it is turned back on. Best-effort: a
/// terminal that ignores the sequence simply keeps its current behavior.
pub(crate) fn set_mouse_capture(term: &mut Term, on: bool) -> Result<()> {
if on {
execute!(term.backend_mut(), EnableMouseCapture)?;
} else {
execute!(term.backend_mut(), DisableMouseCapture)?;
}
Ok(())
}

pub(crate) fn restore_terminal(term: &mut Term) -> Result<()> {
disable_raw_mode()?;
// Pop the enhancement flags best-effort — if they were never pushed (or the
Expand Down
30 changes: 29 additions & 1 deletion apps/zerocode/src/input_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ impl InputBarState {

match action {
Some(IbWidgetAction::Paste) => {
return self.handle_clipboard_image();
return self.handle_paste_action();
}
Some(IbWidgetAction::AutocompleteCancel) if self.autocomplete_active => {
self.dismiss_autocomplete();
Expand Down Expand Up @@ -1288,6 +1288,34 @@ impl InputBarState {
}
}

/// Ctrl+V paste — text first.
///
/// A Ctrl+V almost always means "paste text", so read the clipboard's
/// text and insert it directly; only when the clipboard holds no text
/// (e.g. a copied screenshot) fall back to attaching a clipboard image.
/// Reading text first also skips the image-tool subprocess on the common
/// path. This complements bracketed paste (`Event::Paste`), which most
/// terminals deliver for a native paste; Ctrl+V covers terminals that do
/// not, and the explicit image-attach fallback.
///
/// Tradeoff: when the clipboard exposes BOTH a text flavor and an image
/// (e.g. a browser "Copy Image" that also sets a URL), the text wins. To
/// attach such an image, save it and use `/attach <path>` or the Ctrl+A
/// file browser — text is what the overwhelming majority of pastes want.
fn handle_paste_action(&mut self) -> InputBarAction {
if let Some(text) = clipboard::read_clipboard_text() {
// Strip a single trailing CRLF/LF so a one-line paste stays one
// line; interior newlines (genuine multi-line paste) are kept.
let text = text.strip_suffix('\n').unwrap_or(&text);
let text = text.strip_suffix('\r').unwrap_or(text);
if !text.is_empty() {
return self.handle_paste(text);
}
}
// No usable text on the clipboard → try an image attachment instead.
self.handle_clipboard_image()
}

fn handle_clipboard_image(&mut self) -> InputBarAction {
match clipboard::read_clipboard_image() {
Some((bytes, mime)) => {
Expand Down
1 change: 1 addition & 0 deletions apps/zerocode/src/keymap/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ keyactions! {
PaneNavLeft [Chord::with(KeyCode::Left, KeyModifiers::ALT), Chord::with(KeyCode::Char('b'), KeyModifiers::ALT)] => "prev pane",
PaneNavRight [Chord::with(KeyCode::Right, KeyModifiers::ALT), Chord::with(KeyCode::Char('f'), KeyModifiers::ALT)] => "next pane",
ReloadDaemon [Chord::ctrl('r')] => "reload daemon",
ToggleMouseCapture [Chord::ctrl('o')] => "selection mode (mouse copy/paste)",
ConfirmYes [] => "confirm",
ConfirmNo [] => "cancel",
}
Expand Down
30 changes: 30 additions & 0 deletions clawpatch.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"schemaVersion": 1,
"stateDir": ".clawpatch",
"include": ["**/*"],
"exclude": [
"node_modules/**",
"dist/**",
"build/**",
"target/**",
".build/**",
".git/**",
".clawpatch/**",
".context/**"
],
"provider": {
"name": "acpx",
"model": "zeroclaw",
"reasoningEffort": null,
"codexConfig": {}
},
"commands": { "typecheck": null, "lint": null, "format": null, "test": null },
"review": {
"maxContextFiles": 24,
"maxOwnedFiles": 12,
"maxFindingsPerFeature": 10,
"minConfidenceToFix": "medium"
},
"git": { "requireCleanWorktreeForFix": true, "commit": false, "openPr": false },
"registryVerifier": { "enabled": false }
}
Loading