Skip to content
Merged
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
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_group_close_required` を返します。明示的なグループクローズは、閉じた各ワークスペースについて `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 @@ -412,6 +412,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`, returning `workspace_group_close_required` when explicit group intent is missing. 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_group_close_required`。显式关闭组时,每个被关闭的工作区都会发出一个 `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
67 changes: 37 additions & 30 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,31 +1967,55 @@ 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 begin_workspace_close_confirmation(&mut self, ws_idx: usize) -> bool {
let Some(workspace_id) = self
.workspaces
.get(ws_idx)
.map(|workspace| workspace.id.clone())
else {
return false;
};
self.selected = ws_idx;
self.confirm_close_workspace_id = Some(workspace_id);
self.mode = Mode::ConfirmClose;
true
}

pub(crate) fn take_confirmed_workspace_close_index(&mut self) -> Option<usize> {
let workspace_id = self.confirm_close_workspace_id.take()?;
self.workspaces
.iter()
.position(|workspace| workspace.id == workspace_id)
}

pub(crate) fn confirm_implicit_worktree_group_close(&mut self, ws_idx: usize) -> bool {
if self.confirm_close && self.workspace_close_would_close_worktree_group(ws_idx) {
self.selected = ws_idx;
self.mode = Mode::ConfirmClose;
true
} else {
false
}
self.confirm_close
&& self.workspace_close_would_close_worktree_group(ws_idx)
&& self.begin_workspace_close_confirmation(ws_idx)
}

#[cfg(test)]
Expand Down
Loading
Loading