j-cli 12.9.21

A fast CLI tool for alias management, daily reports, and productivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# Plan: Deferred Tool Loading(延迟工具加载)— 用户自主掌控方案

## 概述

对齐 Claude Code 的 Deferred Tool Loading 机制,但核心思路改变:**不通过代码硬编码工具分类,而是通过 AgentConfig 配置 + Config Panel UI 面板让用户自主掌控哪些工具延迟加载**。

核心设计:
1. `AgentConfig` 新增 `deferred_tools: Vec<String>` 字段,持久化到 session config
2. Config Panel 的 Tools Tab 增加三态切换:`启用` / `禁用` / `延迟`
3. 新增 `ToolSearch` 工具,让模型按需发现并获取延迟工具的完整 schema
4. 请求构建时根据 `deferred_tools` 列表决定发送全量还是简化 schema

---

## 改动清单

### Step 1: AgentConfig 新增 `deferred_tools` 字段

**文件**: `src/command/chat/storage.rs`

在 `AgentConfig` 中新增字段(位于 `disabled_tools` 之后):

```rust
pub struct AgentConfig {
    // ... 现有字段 ...
    pub disabled_tools: Vec<String>,
    /// 新增:延迟加载的工具名称列表
    /// 列表中的工具不发送完整 schema,通过 ToolSearch 按需发现
    #[serde(default)]
    pub deferred_tools: Vec<String>,
    // ... 后续字段 ...
}
```

**初始化位置**: `src/command/chat/handler/tui_loop.rs:242`

```rust
// 在 agent_config 初始化处添加
deferred_tools: Vec::new(),
```

**理由**: 与 `disabled_tools` 并列,用户通过同一配置面板管理。`serde(default)` 保证向后兼容旧配置文件。

---

### Step 2: Config Panel Tools Tab 三态 UI

**目标**: 在 Tools Tab 中,每个工具项从 `ON/OFF` 二态变为 `ON/DEFER/OFF` 三态。

#### 2.1 新增 Action

**文件**: `src/command/chat/app/types.rs`(或 Action enum 所在位置)

```rust
// 在现有 Action 枚举中添加
Action::ToggleMenuToggleDefer,  // 将选中工具标记为"延迟"状态
```

#### 2.2 工具状态判断函数

**文件**: `src/command/chat/app/chat_app.rs`(或独立为 `tools_state.rs`)

```rust
/// 工具在 Config Panel 中的三态
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ToolState {
    /// 启用:全量发送 schema
    Enabled,
    /// 禁用:不发送
    Disabled,
    /// 延迟:只发名称,通过 ToolSearch 发现
    Deferred,
}

impl ToolState {
    pub fn from_config(name: &str, disabled: &[String], deferred: &[String]) -> Self {
        if disabled.iter().any(|d| d == name) {
            Self::Disabled
        } else if deferred.iter().any(|d| d == name) {
            Self::Deferred
        } else {
            Self::Enabled
        }
    }
}
```

#### 2.3 更新 ToggleMenuToggle 处理(现有 Enter 键)

**文件**: `src/command/chat/app/chat_app.rs`

修改 `Action::ToggleMenuToggle` 的处理逻辑(约 1453 行):

```rust
// 原逻辑:在 disabled 和 enabled 之间切换
// 新逻辑:循环三态 Enabled → Disabled → Deferred → Enabled
Action::ToggleMenuToggle => {
    if self.ui.config_tab == ConfigTab::Tools {
        let tool_names = self.tool_registry.tool_names();
        if let Some(name) = tool_names.get(self.ui.config_field_idx) {
            let name = name.to_string();
            let state = ToolState::from_config(
                &name,
                &self.state.agent_config.disabled_tools,
                &self.state.agent_config.deferred_tools,
            );
            match state {
                ToolState::Enabled => {
                    // Enabled → Disabled
                    self.state.agent_config.disabled_tools.push(name);
                }
                ToolState::Disabled => {
                    // Disabled → Deferred
                    if let Some(pos) = self.state.agent_config.disabled_tools.iter().position(|d| d == &name) {
                        self.state.agent_config.disabled_tools.remove(pos);
                    }
                    self.state.agent_config.deferred_tools.push(name);
                }
                ToolState::Deferred => {
                    // Deferred → Enabled
                    if let Some(pos) = self.state.agent_config.deferred_tools.iter().position(|d| d == &name) {
                        self.state.agent_config.deferred_tools.remove(pos);
                    }
                }
            }
        }
    }
    // ... Skills 和 Commands 部分保持不变 ...
}
```

#### 2.4 更新 ToggleMenuEnableAll / ToggleMenuDisableAll

**文件**: `src/command/chat/app/chat_app.rs`

```rust
Action::ToggleMenuEnableAll => {
    if self.ui.config_tab == ConfigTab::Tools {
        self.state.agent_config.disabled_tools.clear();
        self.state.agent_config.deferred_tools.clear();  // 新增:清除延迟列表
        self.show_toast("已启用全部工具", false);
    }
    // ...
}

Action::ToggleMenuDisableAll => {
    if self.ui.config_tab == ConfigTab::Tools {
        self.state.agent_config.disabled_tools = self
            .tool_registry
            .tool_names()
            .iter()
            .map(|n| n.to_string())
            .collect();
        self.state.agent_config.deferred_tools.clear();  // 新增:禁用时清除延迟
        self.show_toast("已禁用全部工具", false);
    }
    // ...
}
```

#### 2.5 更新 Tools Tab 渲染

**文件**: `src/command/chat/ui/config.rs`

修改 `render_tools_tab` 函数中每个工具项的渲染:

```rust
// 现有代码(约 705 行):
// let is_enabled = !app.state.agent_config.disabled_tools.iter().any(|d| d == *name);

// 改为三态:
let state = ToolState::from_config(
    name,
    &app.state.agent_config.disabled_tools,
    &app.state.agent_config.deferred_tools,
);

// 根据三态渲染不同的标记
let (toggle_text, toggle_style) = match state {
    ToolState::Enabled => (
        TOGGLE_ON,
        Style::default().fg(t.config_toggle_on).add_modifier(Modifier::BOLD),
    ),
    ToolState::Deferred => (
        "◐",  // 半亮标记,表示延迟
        Style::default().fg(t.title_loading).add_modifier(Modifier::BOLD),  // 黄/橙色
    ),
    ToolState::Disabled => (
        TOGGLE_OFF,
        Style::default().fg(t.config_toggle_off),
    ),
};
```

同时更新底部快捷键提示:

```rust
// 现有提示
// "Enter/Space: 启用/禁用  a: 全部启用  d: 全部禁用  t: 总开关  Esc: 保存"

// 改为
// "Enter: 切换(启用→禁用→延迟)  a: 全部启用  d: 全部禁用  t: 总开关  Esc: 保存"
```

#### 2.6 更新 handler/config.rs 保存时机

**文件**: `src/command/chat/handler/config.rs`

Tools Tab 的 Esc 键保存已有,不需要额外改动。`save_agent_config` 会自动序列化 `deferred_tools`。

---

### Step 3: ToolSearch 工具

#### 3.1 新建 `src/command/chat/tools/tool_search.rs`

```rust
use super::definition::{Tool, ToolResult, parse_tool_args, schema_to_tool_params, PlanDecision};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use std::sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}};

#[derive(Debug, Deserialize, JsonSchema)]
pub struct ToolSearchInput {
    /// 搜索查询。支持两种模式:
    /// - "select:ToolA,ToolB" — 精确选择指定工具
    /// - "file read" — 关键词搜索匹配工具名和描述
    query: String,
}

pub const NAME: &str = "ToolSearch";

pub struct ToolSearchTool {
    /// 持有 ToolRegistry 的引用,用于查找延迟工具
    registry: Arc<Mutex<ToolRegistrySnapshot>>,
}

/// ToolRegistry 的只读快照(由 ToolRegistry 在构建请求时更新)
pub struct ToolRegistrySnapshot {
    pub deferred_tool_names: Vec<String>,
    /// 延迟工具的完整 schema 缓存
    pub deferred_tool_schemas: Vec<(String, String, Value)>,  // (name, description, parameters)
}

impl ToolSearchTool {
    pub fn new() -> Self {
        Self {
            registry: Arc::new(Mutex::new(ToolRegistrySnapshot {
                deferred_tool_names: Vec::new(),
                deferred_tool_schemas: Vec::new(),
            })),
        }
    }

    pub fn snapshot_arc(&self) -> Arc<Mutex<ToolRegistrySnapshot>> {
        Arc::clone(&self.registry)
    }
}

impl Tool for ToolSearchTool {
    fn name(&self) -> &str { NAME }
    fn description(&self) -> &str {
        "搜索延迟加载的工具并获取完整定义。延迟工具只发送了名称,调用此工具获取完整参数定义。\n\
         查询模式:\n\
         - 'select:ToolA,ToolB' — 精确选择指定工具\n\
         - 'task todo' — 关键词搜索匹配工具"
    }
    fn parameters_schema(&self) -> Value {
        schema_to_tool_params::<ToolSearchInput>()
    }

    fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult {
        if cancelled.load(Ordering::Relaxed) {
            return ToolResult {
                output: "已取消".to_string(),
                is_error: true,
                images: vec![],
                plan_decision: PlanDecision::None,
            };
        }

        let input = match parse_tool_args::<ToolSearchInput>(arguments) {
            Ok(i) => i,
            Err(e) => return e,
        };

        let snapshot = self.registry.lock().unwrap();
        let result = search_deferred_tools(&input.query, &snapshot);

        ToolResult {
            output: result,
            is_error: false,
            images: vec![],
            plan_decision: PlanDecision::None,
        }
    }
}

/// 搜索延迟工具并返回格式化结果
fn search_deferred_tools(query: &str, snapshot: &ToolRegistrySnapshot) -> String {
    let query_lower = query.to_lowercase();
    let matched_schemas: Vec<&(String, String, Value)>;

    // 精确选择模式:select:ToolA,ToolB
    if let Some(names) = query_lower.strip_prefix("select:") {
        let requested: Vec<&str> = names.split(',')
            .map(|s| s.trim())
            .filter(|s| !s.is_empty())
            .collect();
        matched_schemas = snapshot.deferred_tool_schemas.iter()
            .filter(|(name, _, _)| {
                requested.iter().any(|r| r.eq_ignore_ascii_case(name))
            })
            .collect();
    } else {
        // 关键词搜索
        let keywords: Vec<&str> = query_lower.split_whitespace().collect();
        matched_schemas = snapshot.deferred_tool_schemas.iter()
            .filter(|(name, desc, _)| {
                let search_text = format!("{} {}", name.to_lowercase(), desc.to_lowercase());
                keywords.iter().all(|kw| search_text.contains(kw))
            })
            .collect();
    }

    if matched_schemas.is_empty() {
        return format!(
            "未找到匹配的延迟工具。当前可用的延迟工具:\n{}",
            snapshot.deferred_tool_names.join(", ")
        );
    }

    let mut output = String::new();
    output.push_str(&format!(
        "找到 {} 个延迟工具(共 {} 个):\n\n",
        matched_schemas.len(),
        snapshot.deferred_tool_names.len()
    ));

    for (name, desc, params) in &matched_schemas {
        output.push_str(&format!("## {}\n{}\n\n参数定义:\n", name, desc.trim()));
        output.push_str(&serde_json::to_string_pretty(params).unwrap_or_default());
        output.push_str("\n\n");
    }

    output
}
```

#### 3.2 在 tools.rs 中导出

**文件**: `src/command/chat/tools.rs`

```rust
pub mod tool_search;  // 新增
```

#### 3.3 在 tool_names 中添加常量

**文件**: `src/command/chat/tools.rs`

```rust
pub mod tool_names {
    // ... 现有常量 ...
    pub const TOOL_SEARCH: &str = super::tool_search::NAME;  // 新增
}
```

#### 3.4 在 ToolRegistry::new 中注册 ToolSearchTool

**文件**: `src/command/chat/tools/definition.rs`

```rust
// 在 ToolRegistry::new 的 tools vec 中添加
Box::new(super::tool_search::ToolSearchTool::new()),
```

注意:ToolSearchTool 的 snapshot 需要在构建请求前更新。ToolRegistry 需要持有 snapshot 的引用。

---

### Step 4: ToolRegistry 集成 snapshot 更新

**文件**: `src/command/chat/tools/definition.rs`

#### 4.1 ToolRegistry 新增 snapshot 字段

```rust
pub struct ToolRegistry {
    tools: Vec<Box<dyn Tool>>,
    pub todo_manager: Arc<super::todo::TodoManager>,
    pub plan_mode_state: Arc<super::plan::PlanModeState>,
    pub worktree_state: Arc<super::worktree::WorktreeState>,
    pub permission_queue: Option<Arc<PermissionQueue>>,
    pub plan_approval_queue: Option<Arc<super::plan::PlanApprovalQueue>>,
    /// 新增:ToolSearch 工具的快照引用
    tool_search_snapshot: Option<Arc<Mutex<super::tool_search::ToolRegistrySnapshot>>>,
}
```

#### 4.2 ToolRegistry::new 中获取 snapshot

```rust
// 在构建 ToolSearchTool 时保存 snapshot 引用
let tool_search = super::tool_search::ToolSearchTool::new();
let tool_search_snapshot = tool_search.snapshot_arc();
// ...
tools.push(Box::new(tool_search));

let mut registry = Self {
    // ...
    tool_search_snapshot: Some(tool_search_snapshot),
};
```

#### 4.3 新增 `to_openai_tools_with_defer` 方法

```rust
impl ToolRegistry {
    /// 构建带延迟加载的工具 schema,同时更新 ToolSearch snapshot
    pub fn to_openai_tools_with_defer(
        &self,
        disabled: &[String],
        deferred: &[String],
    ) -> Vec<ChatCompletionTools> {
        // 1. 更新 ToolSearch snapshot
        if let Some(ref snapshot_arc) = self.tool_search_snapshot {
            let deferred_schemas: Vec<(String, String, Value)> = self.tools.iter()
                .filter(|t| deferred.iter().any(|d| d == t.name()))
                .map(|t| (t.name().to_string(), t.description().to_string(), t.parameters_schema()))
                .collect();
            if let Ok(mut snapshot) = snapshot_arc.lock() {
                snapshot.deferred_tool_names = deferred.to_vec();
                snapshot.deferred_tool_schemas = deferred_schemas;
            }
        }

        // 2. 构建 tools schema
        self.tools.iter()
            .filter(|t| !disabled.iter().any(|d| d == t.name()))
            .map(|t| {
                let is_deferred = deferred.iter().any(|d| d == &t.name());

                if is_deferred {
                    ChatCompletionTools::Function(ChatCompletionTool {
                        function: FunctionObject {
                            name: t.name().to_string(),
                            description: Some(format!(
                                "(延迟加载) 使用 ToolSearch 'select:{}' 获取完整参数定义。",
                                t.name()
                            )),
                            parameters: Some(serde_json::json!({
                                "type": "object",
                                "properties": {}
                            })),
                            strict: None,
                        },
                    })
                } else {
                    ChatCompletionTools::Function(ChatCompletionTool {
                        function: FunctionObject {
                            name: t.name().to_string(),
                            description: Some(t.description().trim().to_string()),
                            parameters: Some(t.parameters_schema()),
                            strict: None,
                        },
                    })
                }
            })
            .collect()
    }
}
```

#### 4.4 新增 `build_tools_summary_with_defer` 方法

类似 `build_tools_summary`,但延迟工具只输出名称和提示:

```rust
pub fn build_tools_summary_with_defer(&self, disabled: &[String], deferred: &[String]) -> String {
    let mut md = String::new();
    for t in self.tools.iter()
        .filter(|t| !disabled.iter().any(|d| d == t.name()))
    {
        let name = t.name();
        if deferred.iter().any(|d| d == name) {
            // 延迟工具:只输出名称提示
            md.push_str(&format!("<{}>\n(延迟加载,使用 ToolSearch 'select:{}' 获取完整定义)\n<{}/>\n\n", name, name, name));
        } else {
            md.push_str(&format!("<{}>\n", name));
            md.push_str(&format!("description:\n{}\n", t.description().trim()));
            md.push_str(&json_schema_to_xml_params(&t.parameters_schema()));
            md.push_str(&format!("<{}/>\n\n", name));
        }
    }
    md.trim_end().to_string()
}
```

---

### Step 5: 请求构建调用更新

#### 5.1 chat_app.rs — send_message 方法(约 2408 行)

```rust
// 原代码
// let tools = if tools_enabled {
//     self.tool_registry.to_openai_tools_filtered(&self.state.agent_config.disabled_tools)
// } else { vec![] };

// 改为
let disabled_tools = self.state.agent_config.disabled_tools.clone();
let deferred_tools = self.state.agent_config.deferred_tools.clone();
let tools = if tools_enabled {
    self.tool_registry.to_openai_tools_with_defer(&disabled_tools, &deferred_tools)
} else { vec![] };
```

#### 5.2 chat_app.rs — system_prompt_fn(约 2424 行)

```rust
// 在 system_prompt_fn 闭包中
// 原代码
// let tools_summary = tool_registry.build_tools_summary(&disabled_tools);

// 改为
let tools_summary = tool_registry.build_tools_summary_with_defer(&disabled_tools, &deferred_tools);

// 并在 prompt 末尾添加延迟工具列表提示
if !deferred_tools.is_empty() {
    prompt.push_str(&format!(
        "\n\n<available-deferred-tools>\n{}\n</available-deferred-tools>",
        deferred_tools.join("\n")
    ));
}
```

#### 5.3 chat_app.rs — wake_from_inbox 方法(约 3152 行)

同 5.1 和 5.2,修改 tools 构建和 system prompt。

#### 5.4 oneshot.rs — oneshot 模式(约 419 行)

```rust
// 原代码
// let openai_tools = tool_registry.to_openai_tools_filtered(&agent_config.disabled_tools);

// 改为
let openai_tools = tool_registry.to_openai_tools_with_defer(
    &agent_config.disabled_tools,
    &agent_config.deferred_tools,
);
```

同时更新 `resolve_oneshot_system_prompt` 使用 `build_tools_summary_with_defer`。

#### 5.5 子 Agent 工具构建

**文件**: `src/command/chat/tools/sub_agent.rs`(约 203 行)

```rust
// 原代码
// let tools = child_registry.to_openai_tools_filtered(&disabled);

// 改为
let deferred = self.shared.deferred_tools.as_ref().clone();  // 需要在 DerivedAgentShared 中添加
let tools = child_registry.to_openai_tools_with_defer(&disabled, &deferred);
```

**文件**: `src/command/chat/tools/derived_shared.rs`

```rust
pub struct DerivedAgentShared {
    // ... 现有字段 ...
    pub disabled_tools: Arc<Vec<String>>,
    pub deferred_tools: Arc<Vec<String>>,  // 新增
}
```

**文件**: `src/command/chat/app/chat_app.rs`(DerivedAgentShared 构建处)

```rust
let deferred_tools_arc = Arc::new(agent_config.deferred_tools.clone());
let derived_agent_shared = DerivedAgentShared {
    // ... 现有字段 ...
    disabled_tools: Arc::clone(&disabled_tools_arc),
    deferred_tools: Arc::clone(&deferred_tools_arc),  // 新增
};
```

---

### Step 6: Plan Mode 兼容性

**文件**: `src/command/chat/tools/plan.rs`

确保 ToolSearch 在 plan mode 下可用(它是只读工具):

```rust
// 在 plan.rs 的 is_allowed_in_plan_mode 函数中添加 ToolSearch
pub fn is_allowed_in_plan_mode(name: &str) -> bool {
    matches!(name,
        // ... 现有允许列表 ...
        | tool_names::TOOL_SEARCH  // 新增
    )
}
```

---

## 文件改动清单

| 文件 | 改动类型 | 改动内容 |
|------|----------|----------|
| `storage.rs` | 扩展 | AgentConfig 新增 `deferred_tools` 字段 |
| `handler/tui_loop.rs` | 扩展 | 初始化 `deferred_tools: Vec::new()` |
| `tools/tool_search.rs` | **新建** | ToolSearchTool 实现 |
| `tools.rs` | 扩展 | 添加 `tool_search` 模块和 `TOOL_SEARCH` 常量 |
| `tools/definition.rs` | 扩展 | ToolRegistry 新增 snapshot 字段,`to_openai_tools_with_defer``build_tools_summary_with_defer` |
| `app/chat_app.rs` | 修改 | ToggleMenuToggle 三态逻辑,send_message/wake_from_inbox 调用更新,DerivedAgentShared 新增字段 |
| `app/types.rs` | 扩展 | ToolState 枚举 |
| `ui/config.rs` | 修改 | Tools Tab 三态渲染 |
| `handler/config.rs` | 修改 | Tools Tab 键盘提示文本 |
| `tools/derived_shared.rs` | 扩展 | 新增 `deferred_tools` 字段 |
| `tools/sub_agent.rs` | 修改 | 使用 `to_openai_tools_with_defer` |
| `tools/create_teammate.rs` | 修改 | 使用 `to_openai_tools_with_defer` |
| `oneshot.rs` | 修改 | 使用 `to_openai_tools_with_defer``build_tools_summary_with_defer` |
| `tools/plan.rs` | 修改 | `is_allowed_in_plan_mode` 添加 ToolSearch |

---

## 用户交互设计

### Config Panel — Tools Tab

```
╔══════════════════════════════════════════════════════╗
║  Model  │  Global  │ [Tools] │  Skills  │  Hooks    ║
╠══════════════════════════════════════════════════════╣
║                                                      ║
║  ✓ Bash               执行 shell 命令                ║
║  ✓ Read               读取文件                       ║
║  ✓ Write              写入文件                       ║
║  ✓ Edit               编辑文件                       ║
║  ✓ Glob               文件搜索                       ║
║  ✓ Grep               内容搜索                       ║
║  ✓ WebFetch           获取 URL 内容                  ║
║  ✓ WebSearch          网页搜索                       ║
║  ✓ Ask                向用户提问                     ║
║  ✓ ToolSearch         搜索延迟工具                   ║
║  ◐ Task               [延迟] 使用 ToolSearch 加载    ║
║  ◐ TodoWrite          [延迟] 使用 ToolSearch 加载    ║
║  ◐ TodoRead           [延迟] 使用 ToolSearch 加载    ║
║  ◐ Compact            [延迟] 使用 ToolSearch 加载    ║
║  ✗ RegisterHook       [禁用]                         ║
║  ✓ EnterPlanMode      进入计划模式                   ║
║  ...                                                 ║
║                                                      ║
║  Enter: 切换(启用→禁用→延迟)  a: 全部启用            ║
║  d: 全部禁用  t: 总开关  Esc: 保存返回               ║
╚══════════════════════════════════════════════════════╝
```

**三态循环**: Enter 键按下时 `启用(✓) → 禁用(✗) → 延迟(◐) → 启用(✓)`

**快捷键**:
- `Enter/Space`: 循环切换三态
- `a`: 全部启用(清除 disabled + deferred)
- `d`: 全部禁用(所有工具加入 disabled,清除 deferred)
- `t`: 工具调用总开关
- `Esc`: 保存并退出

---

## 测试计划

1. **配置持久化**: 设置 deferred_tools 后重启,验证配置恢复
2. **三态切换**: Enter 键正确在三个状态间循环
3. **ToolSearch 调用**: 模型调用 ToolSearch 返回正确 schema
4. **延迟工具请求**: deferred 工具发送简化 schema,核心工具发送全量
5. **子 Agent 传递**: 子 Agent 正确继承 deferred_tools 配置
6. **Plan Mode**: ToolSearch 在 plan mode 下可用
7. **向后兼容**: 无 deferred_tools 字段的旧配置正常加载

---

## 实施优先级

1. **Step 1** (基础设施): AgentConfig 新增字段 + 初始化
2. **Step 2** (UI): Config Panel 三态渲染
3. **Step 3** (核心): ToolSearch 工具实现
4. **Step 4** (集成): ToolRegistry snapshot + to_openai_tools_with_defer
5. **Step 5** (调用更新): 所有调用点切换到新方法
6. **Step 6** (兼容): Plan Mode + 子 Agent