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
2 changes: 2 additions & 0 deletions docs/next/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
- Headless servers now use a configurable 120×40 virtual terminal instead of 80×24 when no client is attached, giving newly created panes a practical default size. (#2828)
- Desktop tab labels are now centered in their tabs, so the active-tab highlight has symmetric padding.
- Bumped the client/server protocol version to 20 for pane terminal bell forwarding.
- Bumped the client/server protocol version to 21 for explicit workspace worktree-group closure.
- Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay.

### Fixed
- The Windows ARM64 installer now waits for x64 emulation to release the verified executable before activating the downloaded release. (#2916)
- On Unix, Ctrl-click URL openers are now reaped after they exit, preventing defunct child processes from accumulating on long-running servers. (#2903)
- `workspace close` now rejects implicit worktree-group closure and supports explicit group closure with `--group`. (#2874)
- Herdr no longer sends the full OSC 4 palette query burst under WSL, preventing reply fragments from leaking into the shell through ConPTY. (#2440)
- Qwen Code panes now use locale-independent terminal-title states and localized confirmation fallbacks, preventing active or blocked turns from appearing idle. (#2756)
- Closing a terminal running `herdr --remote` no longer produces a local client core dump while the remote session stays alive. (#2424)
Expand Down
18 changes: 16 additions & 2 deletions docs/next/api/herdr-api.schema.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"protocol": 20,
"protocol": 21,
"schema_version": 1,
"schemas": {
"error_response": {
Expand Down Expand Up @@ -4145,6 +4145,20 @@
],
"type": "string"
},
"WorkspaceCloseParams": {
"properties": {
"close_group": {
"type": "boolean"
},
"workspace_id": {
"type": "string"
}
},
"required": [
"workspace_id"
],
"type": "object"
},
"WorkspaceCreateParams": {
"properties": {
"cwd": {
Expand Down Expand Up @@ -4722,7 +4736,7 @@
"type": "string"
},
"params": {
"$ref": "#/schemas/request/$defs/WorkspaceTarget"
"$ref": "#/schemas/request/$defs/WorkspaceCloseParams"
}
},
"required": [
Expand Down
4 changes: 2 additions & 2 deletions docs/next/website/src/content/docs/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ herdr workspace get <workspace_id>
herdr workspace focus <workspace_id>
herdr workspace rename <workspace_id> <label>
herdr workspace report-metadata <workspace_id> --source ID [--token NAME=VALUE] [--clear-token NAME] [--seq N] [--ttl-ms N]
herdr workspace close <workspace_id>
herdr workspace close <workspace_id> [--group]
```

Create a workspace without stealing focus:
Expand All @@ -143,7 +143,7 @@ herdr worktree remove --workspace ID [--force]

Worktrees are normal Herdr workspaces with Git checkout provenance. `worktree create` creates a Git worktree checkout, opens it as a workspace, and groups it with the parent repo workspace. If `--branch` names an existing local branch, Herdr checks it out; otherwise it creates the branch from `--base` or `HEAD`. Without `--path`, Herdr creates the checkout under `<worktrees.directory>/<repo>/<branch-slug>`.

`workspace close` closes only Herdr state. To delete the checkout, run `worktree remove`. It runs `git worktree remove`, never deletes the branch, and requires `--force` when Git refuses a dirty checkout.
`workspace close` closes only Herdr state. Closing a primary workspace while linked-worktree workspaces are open requires `--group`; without it, the command leaves the group open and returns `workspace_group_close_required`. To delete the checkout, run `worktree remove`. It runs `git worktree remove`, never deletes the branch, and requires `--force` when Git refuses a dirty checkout.

## Tabs

Expand Down
4 changes: 2 additions & 2 deletions docs/next/website/src/content/docs/ja/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ herdr workspace get <workspace_id>
herdr workspace focus <workspace_id>
herdr workspace rename <workspace_id> <label>
herdr workspace report-metadata <workspace_id> --source ID [--token NAME=VALUE] [--clear-token NAME] [--seq N] [--ttl-ms N]
herdr workspace close <workspace_id>
herdr workspace close <workspace_id> [--group]
```

フォーカスを奪わずにワークスペースを作成します:
Expand All @@ -139,7 +139,7 @@ herdr worktree remove --workspace ID [--force]

worktree は、Git チェックアウトの出自情報を持つ通常の Herdr ワークスペースです。`worktree create` は Git worktree のチェックアウトを作成し、ワークスペースとして開き、親リポジトリのワークスペースとグループ化します。`--branch` が既存のローカルブランチを指す場合はそれをチェックアウトし、そうでなければ `--base` または `HEAD` からブランチを作成します。`--path` がない場合、チェックアウトは `<worktrees.directory>/<repo>/<branch-slug>` の下に作成されます。

`workspace close` は Herdr の状態だけを閉じます。`worktree remove` が明示的なチェックアウト削除の経路です。`git worktree remove` を実行し、ブランチは決して削除せず、Git がダーティなチェックアウトを拒否する場合は `--force` が必要です。
`workspace close` は Herdr の状態だけを閉じます。リンクされた worktree ワークスペースが開いている間に親ワークスペースを閉じるには `--group` が必要です。指定しない場合、グループは開いたままになり `workspace_group_close_required` が返されます。`worktree remove` が明示的なチェックアウト削除の経路です。`git worktree remove` を実行し、ブランチは決して削除せず、Git がダーティなチェックアウトを拒否する場合は `--force` が必要です。

## タブ

Expand Down
2 changes: 2 additions & 0 deletions docs/next/website/src/content/docs/ja/socket-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ Worktree メソッドは Git チェックアウトを Herdr ワークスペー

worktree コマンドはライフサイクルイベントも発行します。`worktree.create` は `workspace.created`、`tab.created`、`pane.created`、`worktree.created` を発行します。`worktree.open` は `worktree.opened` を発行し、新しい Herdr ワークスペースを開いた場合はワークスペース/タブ/ペインの作成イベントも発行します。`worktree.remove` は `worktree.removed` を発行し、リンクされたワークスペースがまだ開いている場合は `workspace.closed` も発行します。

リンクされた worktree ワークスペースが開いている親ワークスペースを `workspace.close` で閉じるには、params に `"close_group": true` が必要です。明示的なグループクローズは、閉じた各ワークスペースについて `workspace.closed` イベントを 1 件ずつ発行します。

## エージェントビューのクエリ

`agent.view.set` は、組み込みの Agents ビューに一つの一時的な宣言型プロジェクションを
Expand Down
2 changes: 2 additions & 0 deletions docs/next/website/src/content/docs/socket-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ Use at most one of `workspace_id` or `cwd` for `worktree.list`, `worktree.create

Worktree commands also emit lifecycle events. `worktree.create` emits `workspace.created`, `tab.created`, `pane.created`, and `worktree.created`. `worktree.open` emits `worktree.opened`, and it also emits workspace/tab/pane creation events when it opens a new Herdr workspace. `worktree.remove` emits `worktree.removed`; if the linked workspace is still open, it also emits `workspace.closed`.

`workspace.close` rejects closing a primary workspace while linked-worktree workspaces are open unless its params include `"close_group": true`. An explicit group close emits one `workspace.closed` event for each workspace it closes.

## Agent view queries

`agent.view.set` installs one transient declarative projection for the built-in
Expand Down
4 changes: 2 additions & 2 deletions docs/next/website/src/content/docs/zh-cn/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ herdr workspace get <workspace_id>
herdr workspace focus <workspace_id>
herdr workspace rename <workspace_id> <label>
herdr workspace report-metadata <workspace_id> --source ID [--token NAME=VALUE] [--clear-token NAME] [--seq N] [--ttl-ms N]
herdr workspace close <workspace_id>
herdr workspace close <workspace_id> [--group]
```

不抢占焦点地创建工作区:
Expand All @@ -139,7 +139,7 @@ herdr worktree remove --workspace ID [--force]

worktree 是带有 Git 检出来源信息的普通 Herdr 工作区。`worktree create` 创建一个 Git worktree 检出,作为工作区打开,并与父仓库工作区分到一组。如果 `--branch` 指向已有的本地分支,Herdr 检出它;否则从 `--base` 或 `HEAD` 创建分支。没有 `--path` 时,Herdr 在 `<worktrees.directory>/<repo>/<branch-slug>` 下创建检出。

`workspace close` 只关闭 Herdr 状态。`worktree remove` 是显式的检出删除路径;它运行 `git worktree remove`,从不删除分支,并在 Git 拒绝脏检出时要求 `--force`。
`workspace close` 只关闭 Herdr 状态。当关联的 worktree 工作区仍然打开时,关闭父工作区需要 `--group`;否则命令会保留整个组并返回 `workspace_group_close_required`。`worktree remove` 是显式的检出删除路径;它运行 `git worktree remove`,从不删除分支,并在 Git 拒绝脏检出时要求 `--force`。

## 标签页

Expand Down
2 changes: 2 additions & 0 deletions docs/next/website/src/content/docs/zh-cn/socket-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ Worktree 方法把 Git 检出作为 Herdr 工作区管理。`worktree.create`

Worktree 命令也发出生命周期事件。`worktree.create` 发出 `workspace.created`、`tab.created`、`pane.created` 和 `worktree.created`。`worktree.open` 发出 `worktree.opened`,并在打开新的 Herdr 工作区时同时发出工作区/标签页/窗格创建事件。`worktree.remove` 发出 `worktree.removed`;如果关联的工作区仍然打开,还会发出 `workspace.closed`。

当关联的 worktree 工作区仍然打开时,`workspace.close` 只有在 params 中包含 `"close_group": true` 才会关闭父工作区。显式关闭组时,每个被关闭的工作区都会发出一个 `workspace.closed` 事件。

## 智能体视图查询

`agent.view.set` 为内置 Agents 视图设置一个临时的声明式投影。智能体信息或当前
Expand Down
2 changes: 1 addition & 1 deletion src/api/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub enum Method {
#[serde(rename = "workspace.report_metadata")]
WorkspaceReportMetadata(WorkspaceReportMetadataParams),
#[serde(rename = "workspace.close")]
WorkspaceClose(WorkspaceTarget),
WorkspaceClose(WorkspaceCloseParams),
#[serde(rename = "worktree.list")]
WorktreeList(WorktreeListParams),
#[serde(rename = "worktree.create")]
Expand Down
28 changes: 28 additions & 0 deletions src/api/schema/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,34 @@ fn request_uses_dot_method_names() {
assert_eq!(json["method"], "workspace.create");
}

#[test]
fn workspace_close_group_intent_defaults_false_and_round_trips() {
let request: Request = serde_json::from_value(serde_json::json!({
"id": "close",
"method": "workspace.close",
"params": { "workspace_id": "w1" }
}))
.unwrap();
assert!(matches!(
request.method,
Method::WorkspaceClose(WorkspaceCloseParams {
close_group: false,
..
})
));

let explicit = Request {
id: "close-group".into(),
method: Method::WorkspaceClose(WorkspaceCloseParams {
workspace_id: "w1".into(),
close_group: true,
}),
};
let json = serde_json::to_value(&explicit).unwrap();
assert_eq!(json["params"]["close_group"], true);
assert_eq!(serde_json::from_value::<Request>(json).unwrap(), explicit);
}

#[test]
fn agent_start_and_prompt_requests_round_trip() {
let start = Request {
Expand Down
7 changes: 7 additions & 0 deletions src/api/schema/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ pub struct WorkspaceCreateParams {
pub env: HashMap<String, String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WorkspaceCloseParams {
pub workspace_id: String,
#[serde(default, skip_serializing_if = "super::is_false")]
pub close_group: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WorkspaceRenameParams {
pub workspace_id: String,
Expand Down
36 changes: 13 additions & 23 deletions src/app/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1677,24 +1677,7 @@ impl AppState {
self.selection = None;
self.selection_autoscroll = None;
self.mark_session_dirty();
let close_indices = self
.workspaces
.get(self.selected)
.and_then(|ws| ws.worktree_space())
.filter(|space| !space.is_linked_worktree)
.map(|space| {
self.workspaces
.iter()
.enumerate()
.filter_map(|(idx, ws)| {
ws.worktree_space()
.is_some_and(|member| member.key == space.key)
.then_some(idx)
})
.collect::<Vec<_>>()
})
.filter(|indices| indices.len() >= 2)
.unwrap_or_else(|| vec![self.selected]);
let close_indices = self.workspace_close_indices(self.selected);

let mut terminal_ids = Vec::new();
let mut pane_ids = Vec::new();
Expand Down Expand Up @@ -1984,21 +1967,28 @@ impl AppState {
self.apply_pane_zoom(ws_idx, pane_id, PaneZoomCommand::Toggle);
}

pub(crate) fn workspace_close_would_close_worktree_group(&self, ws_idx: usize) -> bool {
pub(crate) fn workspace_close_indices(&self, ws_idx: usize) -> Vec<usize> {
self.workspaces
.get(ws_idx)
.and_then(|ws| ws.worktree_space())
.filter(|space| !space.is_linked_worktree)
.is_some_and(|space| {
.map(|space| {
self.workspaces
.iter()
.filter(|ws| {
.enumerate()
.filter_map(|(idx, ws)| {
ws.worktree_space()
.is_some_and(|member| member.key == space.key)
.then_some(idx)
})
.count()
>= 2
.collect::<Vec<_>>()
})
.filter(|indices| indices.len() >= 2)
.unwrap_or_else(|| vec![ws_idx])
}

pub(crate) fn workspace_close_would_close_worktree_group(&self, ws_idx: usize) -> bool {
self.workspace_close_indices(ws_idx).len() >= 2
}

pub(crate) fn confirm_implicit_worktree_group_close(&mut self, ws_idx: usize) -> bool {
Expand Down
Loading
Loading