Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
eadf74e
fix(shell): repair mobile.rs for-else syntax broken by i18n edit
yuloop Sep 6, 2026
f0d2723
fix(shell): align client/shell to upstream refactored API
yuloop Sep 6, 2026
0ad1d17
fix(shell): restore mobile machines section and align test assertion
yuloop Sep 6, 2026
955dd23
chore(shell): silence dead-code after feature trim for clippy
yuloop Sep 6, 2026
68b37bb
test(auto-detect): accept zh localized nested-guard message
yuloop Sep 6, 2026
263f6a4
test(cli): accept zh localized nested-guard message in surface test
yuloop Sep 6, 2026
be4168b
fix(server): drop unused windows status retry helpers (clippy -D warn…
yuloop Sep 6, 2026
15e861c
docs(config): sync config-reference.json with ui.language and copy_on…
yuloop Sep 6, 2026
b83b4c8
test(scripts): remove stale cross-platform gate test (sync-build-depl…
yuloop Sep 6, 2026
8bd129d
test(scripts): restore bundle round-trip contract test (workflow remo…
yuloop Sep 6, 2026
27bda9e
ci(distribution): fetch tags so validate can resolve release tags
yuloop Sep 6, 2026
b219797
ci(windows): install just on Windows so windows_check can run
yuloop Sep 7, 2026
7d333e4
Merge remote-tracking branch 'origin/main' into fix/shell-api-align
yuloop Sep 7, 2026
78a55d0
fix(shell): align selection copy API after merging main
yuloop Sep 7, 2026
0fc84ab
test(shell): use CopyOnSelectModeConfig enum in copy tests after merge
yuloop Sep 7, 2026
3d1c995
fix(shell): retained copy keys apply to non-clipboard modes; align co…
yuloop Sep 7, 2026
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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ jobs:
targets: x86_64-pc-windows-msvc

- name: Install Rust tools
if: matrix.kind == 'unix'
uses: taiki-e/install-action@fd2f5e3d644b484055ebf4268f474c565f148f25 # v2.81.9
with:
tool: just,cargo-nextest
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/distribution.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false

- name: Install Bun
Expand Down
19 changes: 15 additions & 4 deletions docs/next/website/src/data/config-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -901,9 +901,20 @@
},
{
"key": "ui.copy_on_select",
"type": "boolean",
"default": "true",
"description": "Automatically copy text selected by mouse drag or double-click. When disabled, Ctrl+C or a host-forwarded Cmd+C copies and clears the retained selection."
"type": "enum",
"default": "\"manual\"",
"description": "Mouse selection behavior: clipboard, manual, or disabled. Also accepts true/false. Default: manual.",
"values": [
"disabled",
"clipboard",
"manual"
]
},
{
"key": "ui.language",
"type": "string",
"default": "\"zh\"",
"description": "UI language: \"zh\" for 简体中文 (default) or \"en\" for English."
},
{
"key": "ui.host_cursor",
Expand Down Expand Up @@ -1503,4 +1514,4 @@
]
}
]
}
}
85 changes: 5 additions & 80 deletions scripts/test_cross_platform_gate.py
Original file line number Diff line number Diff line change
@@ -1,96 +1,21 @@
from __future__ import annotations

import re
import subprocess
import tempfile
import unittest
from pathlib import Path


PROJECT_ROOT = Path(__file__).resolve().parent.parent
SYNC_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "sync-build-deploy.yml"
CI_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "ci.yml"
WINDOWS_CHECK = PROJECT_ROOT / "scripts" / "windows_check.ps1"


def job_body(workflow: str, job: str) -> str:
match = re.search(
rf"^ {re.escape(job)}:\n(?P<body>.*?)(?=^ [A-Za-z0-9_-]+:\n|\Z)",
workflow,
flags=re.MULTILINE | re.DOTALL,
)
if match is None:
raise AssertionError(f"workflow job {job!r} is missing")
return match.group("body")


class CrossPlatformGateTests(unittest.TestCase):
def test_custom_branch_push_runs_native_ci_matrix(self) -> None:
workflow = CI_WORKFLOW.read_text(encoding="utf-8")
"""Verify the candidate bundle handoff contract used by fork automation.

self.assertRegex(
workflow,
r"branches:\s*\[[^\]]*deploy/zh-with-perf[^\]]*\]",
)
self.assertIn("os: ubuntu-latest", workflow)
self.assertIn("os: windows-latest", workflow)
self.assertIn(".\\scripts\\windows_check.ps1 -Mode check", workflow)

def test_windows_local_check_lints_the_linux_release_target(self) -> None:
script = WINDOWS_CHECK.read_text(encoding="utf-8")

self.assertGreaterEqual(script.count("x86_64-unknown-linux-musl"), 2)
linux_gate = script.split("$previousLibghosttyVtSimd", maxsplit=1)[1]
self.assertIn('"clippy",', linux_gate)
self.assertIn('"-D",\n "warnings"', linux_gate)
self.assertIn("cfg(unix)", script)

def test_windows_local_check_validates_shared_translation_keys(self) -> None:
script = WINDOWS_CHECK.read_text(encoding="utf-8")

self.assertIn('"scripts.test_i18n_key_check"', script)

def test_windows_local_check_runs_portable_vendor_patch_checks(self) -> None:
script = WINDOWS_CHECK.read_text(encoding="utf-8")

self.assertIn('"scripts.test_vendor_libghostty_vt"', script)
self.assertIn('"scripts.test_vendor_portable_pty"', script)

def test_windows_local_check_runs_shared_ui_regressions(self) -> None:
script = WINDOWS_CHECK.read_text(encoding="utf-8")

self.assertIn('Invoke-CargoTestFilter "ui::"', script)

def test_promotion_waits_for_linux_and_windows_candidate_gates(self) -> None:
workflow = SYNC_WORKFLOW.read_text(encoding="utf-8")
linux = job_body(workflow, "prepare-linux-candidate")
windows = job_body(workflow, "build-windows")
promotion = job_body(workflow, "promote-and-deploy")

self.assertIn("needs: prepare-linux-candidate", windows)
self.assertIn("needs: [prepare-linux-candidate, build-windows]", promotion)
self.assertIn("needs.build-windows.result == 'success'", promotion)
self.assertIn("Run native Windows checks", windows)
self.assertIn("Build and verify the x86_64 musl artifact", linux)

push = 'git push origin "HEAD:refs/heads/$CUSTOM_BRANCH"'
self.assertNotIn(push, linux)
self.assertNotIn("Deploy Linux with live handoff", linux)
self.assertIn(push, promotion)
self.assertIn("Deploy Linux with live handoff", promotion)
self.assertEqual(workflow.count(push), 1)

def test_unpushed_candidate_is_verified_by_both_platform_jobs(self) -> None:
workflow = SYNC_WORKFLOW.read_text(encoding="utf-8")
linux = job_body(workflow, "prepare-linux-candidate")
windows = job_body(workflow, "build-windows")
promotion = job_body(workflow, "promote-and-deploy")

self.assertIn("git bundle create", linux)
self.assertIn("candidate.bundle", windows)
self.assertIn("candidate SHA mismatch", windows)
self.assertIn("candidate.bundle", promotion)
self.assertIn('[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]]', promotion)
The sync-build-deploy workflow was removed with the 31.4 deploy target,
but the bundle round-trip contract is still what future automation relies
on, so it is kept as a pure git-level test.
"""

def test_candidate_bundle_round_trip_needs_only_the_custom_base(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
Expand Down
2 changes: 1 addition & 1 deletion src/cli/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ fn write_requested_help(
Ok(true)
}

#[allow(dead_code)]
fn completion_command() -> Command {
Command::new("completion")
.visible_alias("completions")
Expand Down Expand Up @@ -1038,7 +1039,6 @@ fn path_arg(name: &'static str, value_name: &'static str) -> Arg {
#[cfg(test)]
mod tests {
use clap::{Arg, Command};
use rust_i18n::t;

fn command_path<'a>(cmd: &'a Command, path: &[&str]) -> &'a Command {
let mut current = cmd;
Expand Down
2 changes: 1 addition & 1 deletion src/client/shell/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ impl ClientShellState {
"endpoint_cancelled" => (
ClientEndpointNoticeKind::Unavailable,
"cancelled".to_owned(),
"Action interrupted",
"Action interrupted".to_string(),
error.message.clone(),
),
"server_unavailable" => (
Expand Down
22 changes: 7 additions & 15 deletions src/client/shell/agent_sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,14 @@ pub(super) fn render_agent_panel_header(
if area.height < 2 {
return false;
}
let sort_label: String =
snapshot
.agent_view_label
.clone()
.unwrap_or(match config.agent_panel_sort {
crate::config::AgentPanelSortConfig::Spaces => {
rust_i18n::t!("status.grouped").to_string()
}
crate::config::AgentPanelSortConfig::Priority => {
rust_i18n::t!("status.priority").to_string()
}
});
let sort_width = display_width(&sort_label).min(area.width as usize) as u16;
let sort_label = agent_view_label.unwrap_or(match config.agent_panel_sort {
crate::config::AgentPanelSortConfig::Spaces => "grouped",
crate::config::AgentPanelSortConfig::Priority => "priority",
});
let sort_width = display_width(sort_label).min(area.width as usize) as u16;
let left_width = area.width.saturating_sub(sort_width);
let left_text = crate::ui::truncate_end(
&rust_i18n::t!("sidebar.agents").to_string(),
rust_i18n::t!("sidebar.agents").as_ref(),
left_width as usize,
);
put_text(
Expand Down Expand Up @@ -152,7 +144,7 @@ pub(super) fn render_agent_panel_header(
sort_rect.x,
sort_rect.y,
sort_rect.width,
&crate::ui::truncate_end(&sort_label, sort_width as usize),
&crate::ui::truncate_end(sort_label, sort_width as usize),
Style::default()
.fg(if agent_view_label.is_some() {
config.palette.accent
Expand Down
2 changes: 1 addition & 1 deletion src/client/shell/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ impl ClientShellState {
self.pending_word_selection = None;
if self.mode != ClientShellMode::Copy
&& self.copy_or_terminal_mode() != ClientShellMode::Copy
&& self.config.copy_on_select == crate::config::CopyOnSelectModeConfig::Manual
&& self.config.copy_on_select != crate::config::CopyOnSelectModeConfig::Clipboard
&& is_retained_selection_copy_key(key)
&& self
.selection
Expand Down
80 changes: 49 additions & 31 deletions src/client/shell/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ fn render_header_status(
area.x,
area.y,
area.width,
&rust_i18n::t!("mobile.no_workspace").to_string(),
rust_i18n::t!("mobile.no_workspace").as_ref(),
Style::default().fg(palette.text).bg(palette.panel_bg),
);
return;
Expand Down Expand Up @@ -277,7 +277,7 @@ fn render_agent_summary(
area.x,
area.y,
area.width,
&rust_i18n::t!("mobile.no_agents").to_string(),
rust_i18n::t!("mobile.no_agents").as_ref(),
Style::default()
.fg(config.palette.overlay1)
.bg(config.palette.panel_bg),
Expand All @@ -290,7 +290,7 @@ fn render_agent_summary(
area.x,
area.y,
area.width,
&rust_i18n::t!("mobile.all_idle").to_string(),
rust_i18n::t!("mobile.all_idle").as_ref(),
Style::default()
.fg(config.palette.overlay1)
.bg(config.palette.panel_bg),
Expand Down Expand Up @@ -419,7 +419,7 @@ pub(super) fn render_mobile_switcher(
area.x,
area.y,
close.x.saturating_sub(area.x),
&rust_i18n::t!("mobile.switch").to_string(),
rust_i18n::t!("mobile.switch").as_ref(),
Style::default()
.fg(palette.text)
.bg(palette.panel_bg)
Expand Down Expand Up @@ -563,7 +563,7 @@ fn render_close_button(buffer: &mut Buffer, area: Rect, palette: &Palette) {
label_x,
area.y,
area.width.saturating_sub(1),
&rust_i18n::t!("mobile.close_btn").to_string(),
rust_i18n::t!("mobile.close_btn").as_ref(),
Style::default()
.fg(palette.overlay1)
.bg(palette.surface0)
Expand Down Expand Up @@ -594,9 +594,37 @@ fn mobile_items(
) -> Vec<MobileItem> {
let palette = &config.palette;
let mut items = Vec::new();
let ordered_agents =
super::agent_sidebar::ordered_agent_pane_ids(snapshot, config.agent_panel_sort);
if !ordered_agents.is_empty() || snapshot.agent_view_label.is_some() {
if endpoints.len() > 1 {
items.push(MobileItem::section("machines", palette));
for endpoint in endpoints {
let background = palette.panel_bg;
let (symbol, state, color) = endpoint_status_presentation(endpoint.status, palette);
items.push(MobileItem {
lines: vec![
Line::from(vec![
Span::styled(" ", Style::default().bg(background)),
Span::styled(symbol, Style::default().fg(color).bg(background)),
Span::styled(
format!(" {}", endpoint.label),
Style::default()
.fg(palette.text)
.bg(background)
.add_modifier(Modifier::BOLD),
),
]),
Line::from(Span::styled(
format!(" {state}"),
Style::default().fg(palette.overlay0).bg(background),
)),
],
background,
target: Some(ClientMobileTarget::Machine(endpoint.endpoint_id.clone())),
});
}
}
let agents =
super::aggregate_navigation::aggregate_agent_rows(endpoints, config.agent_panel_sort);
if !agents.is_empty() || snapshot.agent_view_label.is_some() {
let title = snapshot
.agent_view_label
.as_deref()
Expand Down Expand Up @@ -750,28 +778,14 @@ fn mobile_items(
} else if endpoint.endpoint_id == active_endpoint_id && workspace.focused {
palette.surface_dim
} else {
"├─ "
}
} else {
""
};
let name = if entry.indented && !workspace.custom_label {
workspace
.branch
.as_deref()
.and_then(|branch| branch.strip_prefix("worktree/").or(Some(branch)))
.unwrap_or(&workspace.label)
} else {
&workspace.label
};
let branch: String = workspace
.branch
.as_deref()
.map(str::to_owned)
.unwrap_or_else(|| rust_i18n::t!("nav.shell").to_string());
let detail_prefix = if entry.indented {
if entry.last_child {
" "
palette.panel_bg
};
let connector = if entry.indented {
if entry.last_child {
"└─ "
} else {
"├─ "
}
} else {
""
};
Expand All @@ -784,7 +798,11 @@ fn mobile_items(
} else {
&workspace.label
};
let branch = workspace.branch.as_deref().unwrap_or("shell");
let branch: String = workspace
.branch
.as_deref()
.map(str::to_owned)
.unwrap_or_else(|| rust_i18n::t!("nav.shell").to_string());
let detail_prefix = if entry.indented {
if entry.last_child {
" "
Expand Down
2 changes: 1 addition & 1 deletion src/client/shell/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1711,7 +1711,7 @@ impl ClientShellState {
if copied
&& self.config.copy_on_select == crate::config::CopyOnSelectModeConfig::Clipboard
{
self.request_selection_copy(outcome);
self.request_selection_copy(outcome, false);
self.selection = None;
} else if !copied {
self.selection = None;
Expand Down
Loading
Loading