# sgClaw Service Console Enhancement Design

## Background

The current `sg_claw_service_console.html` provides a basic UI for connecting to the sgClaw service WebSocket and submitting tasks. However, it requires manual connection on first load and has no way to configure the sgClaw settings (API key, model, base URL, skills directory) from the UI.

Users need to manually edit `sgclaw_config.json` before using the console, which is inconvenient for routine operations.

## Problem Statement

1. Page requires manual "Connect" button click on first load
2. No UI for configuring sgClaw runtime settings (model, API key, base URL, skills dir)
3. Users must manually edit `sgclaw_config.json` file to change configuration

## Goal

Enhance the service console page with:

1. **Auto-connect on page load** - attempt WebSocket connection immediately
2. **Settings panel** - edit sgClaw configuration fields through a friendly UI
3. **Config save via WebSocket** - send configuration updates to the running sgClaw service, which writes them to `sgclaw_config.json`

## Non-goals

- Auto-starting `sg_claw.exe` process (browser security limitation, deferred)
- Changing existing `submit_task` protocol or execution flow
- Modifying browser-helper.html or browser execution logic
- Adding authentication or multi-user support
- Configuration validation beyond basic field checks

## Architecture

### Component Overview

```
┌─────────────────────────────────────────┐
│  sg_claw_service_console.html           │
│  ┌───────────────────────────────────┐  │
│  │ Auto-connect on load              │  │
│  │ (ws://127.0.0.1:42321 default)    │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ Settings Panel (Modal)            │  │
│  │ - API Key                         │  │
│  │ - Base URL                        │  │
│  │ - Model                           │  │
│  │ - Skills Directory                │  │
│  │ - Direct Submit Skill (optional)  │  │
│  │ - Runtime Profile (dropdown)      │  │
│  │ - Browser Backend (dropdown)      │  │
│  │ [Save] [Cancel]                   │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ Existing: Connection + Composer   │  │
│  └───────────────────────────────────┘  │
└──────────────┬──────────────────────────┘
               │ WebSocket
               │ submit_task / update_config
               ▼
┌─────────────────────────────────────────┐
│  sg_claw.exe (service)                  │
│  ┌───────────────────────────────────┐  │
│  │ ClientMessage handler             │  │
│  │ - SubmitTask (existing)           │  │
│  │ - UpdateConfig (new)              │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ Config writer                     │  │
│  │ Writes to sgclaw_config.json      │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘
```

### Data Flow

1. **Auto-connect flow:**
   - Page loads → JavaScript calls `connect()` automatically
   - If WS opens → show "已连接" chip, enable send button
   - If WS fails → show "未连接" chip, keep send disabled
   - Reconnect logic remains unchanged (existing heartbeat/reconnect)

2. **Config save flow:**
   - User clicks "设置" button → modal opens with current config values
   - User edits fields → clicks "保存"
   - Page sends `update_config` message via WS:
     ```json
     {
       "type": "update_config",
       "config": {
         "apiKey": "...",
         "baseUrl": "...",
         "model": "...",
         "skillsDir": "...",
         "directSubmitSkill": "...",
         "runtimeProfile": "...",
         "browserBackend": "..."
       }
     }
     ```
   - sgClaw service receives message → validates → writes to `sgclaw_config.json`
   - Service responds with success/error → page shows notification
   - Service reloads config in-memory (or requires restart - see below)

### Protocol Changes

#### New ClientMessage variant

Add to `src/service/protocol.rs`:

```rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
    Connect,
    Start,
    Stop,
    SubmitTask { ... },
    Ping,
    UpdateConfig {  // NEW
        config: ConfigUpdatePayload,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigUpdatePayload {
    pub api_key: Option<String>,
    pub base_url: Option<String>,
    pub model: Option<String>,
    pub skills_dir: Option<String>,
    pub direct_submit_skill: Option<String>,
    pub runtime_profile: Option<String>,
    pub browser_backend: Option<String>,
}
```

#### New ServiceMessage variant (optional)

Add to `src/service/protocol.rs`:

```rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServiceMessage {
    StatusChanged { state: String },
    LogEntry { level: String, message: String },
    TaskComplete { success: bool, summary: String },
    Busy { message: String },
    Pong,
    ConfigUpdated { success: bool, message: String },  // NEW
}
```

### Config Persistence

The service will:

1. Load current `sgclaw_config.json` from the config path (derived from process args)
2. Merge incoming `ConfigUpdatePayload` fields (only non-null fields are updated)
3. Write the merged config back to the same file
4. Respond with success/error message
5. **Hot reload**: The service should reload config in-memory without requiring restart

**Important:** If the config file path cannot be resolved (no `--config-path` arg), the service should respond with an error message indicating that config updates are not supported in env-var-only mode.

### UI Design

#### Settings Button

- Add a "设置" button in the sidebar, below the existing connect button
- Styled as a ghost button with a gear icon (using unicode ⚙ or CSS-only icon)

#### Settings Modal

- Overlay modal with centered card
- Form fields with labels in Chinese:
  - `API 密钥` (apiKey) - password input type with show/hide toggle
  - `模型服务地址` (baseUrl) - text input
  - `模型名称` (model) - text input
  - `Skills 目录路径` (skillsDir) - text input with path validation
  - `直接提交技能` (directSubmitSkill) - text input (optional, can be empty)
  - `运行模式` (runtimeProfile) - dropdown: `browser-attached` / `service-standalone`
  - `浏览器后端` (browserBackend) - dropdown: `super-rpa` / `pipe` / `none`
- [保存] primary button, [取消] ghost button
- Validation:
  - API Key and Model are required (show red error if empty on save)
  - Base URL must be a valid URL format
  - Skills Dir must be a valid path format
  - Other fields are optional

#### Connection State Auto-detection

- On page load, call `connect()` automatically
- Connection state chip updates as before
- Reconnect logic (existing) remains unchanged

### File Changes

| File | Change |
|------|--------|
| `frontend/service-console/sg_claw_service_console.html` | Add auto-connect on load, settings modal UI, save logic |
| `src/service/protocol.rs` | Add `UpdateConfig` variant and `ConfigUpdatePayload` struct |
| `src/service/protocol.rs` | Add `ConfigUpdated` service message variant |
| `src/service/server.rs` | Handle `UpdateConfig` message, merge config, write file |
| `src/agent/task_runner.rs` | Add `pub fn config_path(&self) -> Option<&Path>` getter to `AgentRuntimeContext` |
| `src/config/settings.rs` | Add `save_to_path()` method for writing config to file |
| `tests/service_console_html_test.rs` | Add assertions for settings modal and update_config message |

### Config Save Implementation

In `src/service/server.rs`, when handling `UpdateConfig`:

```rust
ClientMessage::UpdateConfig { config } => {
    // 1. Load current config from config_path
    let config_path = runtime_context.config_path(); // needs to be exposed
    let current = SgClawSettings::load(config_path.as_deref())?;

    // 2. Merge: only overwrite fields that are Some in the payload
    let mut merged = current.unwrap_or_default();
    if let Some(v) = config.api_key { merged.provider_api_key = v; }
    if let Some(v) = config.base_url { merged.provider_base_url = v; }
    if let Some(v) = config.model { merged.provider_model = v; }
    if let Some(v) = config.skills_dir { merged.skills_dir = Some(PathBuf::from(v)); }
    // ... etc for other fields

    // 3. Write back to file
    merged.save_to_path(config_path.as_ref().ok_or("no config path")?)?;

    // 4. Respond
    sink.send_service_message(ServiceMessage::ConfigUpdated {
        success: true,
        message: "配置已保存".to_string(),
    })?;
}
```

### Hot Reload Consideration

After saving config, the service should reload its in-memory settings. This requires:

1. Storing the loaded `SgClawSettings` in a reloadable container (e.g., `Arc<Mutex<SgClawSettings>>` or `Arc<RwLock<...>>`)
2. Or, the service can respond with "配置已保存，请重启 sg_claw 以应用更改" (simpler, avoids hot reload complexity)

**Recommended:** Start with "requires restart" approach. Hot reload can be added later if needed.

### Error Handling

| Scenario | Response |
|----------|----------|
| WS not connected when saving | Show inline error: "请先连接服务" |
| Config file not found | Service responds: "未找到配置文件，请通过 --config-path 指定" |
| Invalid config values | Service validates and responds with specific error |
| Write permission denied | Service responds: "无法写入配置文件，请检查文件权限" |
| WS disconnected during save | Show error: "连接断开，保存失败，请重试" |

### Test Strategy

1. **Integration test** (`tests/service_console_html_test.rs`):
   - Assert page contains settings modal HTML
   - Assert page contains "设置" button
   - Assert page sends `update_config` message shape
   - Assert page auto-connects on load (contains `window.onload` or equivalent)

2. **Protocol test** (new or existing test file):
   - Assert `ClientMessage::UpdateConfig` serializes correctly
   - Assert `ServiceMessage::ConfigUpdated` deserializes correctly

3. **Config save test** (new test in `tests/compat_config_test.rs` or new file):
   - Create temp config file
   - Send UpdateConfig message
   - Verify file contents match expected merged config

## Acceptance Criteria

1. Page auto-connects to WS on load without manual button click
2. Settings button visible in sidebar
3. Settings modal opens with form fields for all configurable options
4. Clicking "保存" sends `update_config` message via WS
5. Service receives message and writes to `sgclaw_config.json`
6. Service responds with success/error message
7. Page displays save result notification
8. Existing task submission flow unchanged
9. Existing heartbeat/reconnect logic unchanged
10. Automated tests pass
