# Session List Rename Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add inline session renaming to the sidebar, set "新会话" as the default session name, and auto-rename sessions to the first 10 chars of the first message.

**Architecture:** Three new reactive state fields on `AppViewState`/`app.ts` drive a rewritten `session-sidebar.ts` that renders a MoreMenu (hover `...` → dropdown) and inline edit mode. `createChatSession` passes `label: "新会话"` on create; `sendChatMessageNow` fires a `sessions.patch` RPC after the first message in a "新会话" session.

**Tech Stack:** Lit (HTML templates, `@state()`, `ref` directive), TypeScript, CSS custom properties, existing `sessions.patch` gateway RPC.

---

## File Map

| File | Change |
|------|--------|
| `ui/src/ui/app-view-state.ts` | Add 3 new state fields to type |
| `ui/src/ui/app.ts` | Add 3 `@state()` reactive properties |
| `ui/src/styles/layout.css` | Add CSS for MoreMenu + edit mode |
| `ui/src/ui/chat/session-sidebar.ts` | Full rewrite with MoreMenu + edit mode |
| `ui/src/ui/app-render.helpers.ts` | Pass `label: "新会话"` on create; wire rename handler |
| `ui/src/ui/app-chat.ts` | Auto-rename on first message in `sendChatMessageNow` |

---

## Task 1: AppViewState type + app.ts reactive state

**Files:**
- Modify: `ui/src/ui/app-view-state.ts` (add 3 fields to type)
- Modify: `ui/src/ui/app.ts` (add 3 `@state()` properties near line 252)

- [ ] **Step 1.1: Add fields to AppViewState type**

In `ui/src/ui/app-view-state.ts`, find the `sidebarOpen: boolean;` line and add three lines after `sidebarError: string | null;`:

```ts
  sidebarOpen: boolean;
  sidebarContent: SidebarContent | null;
  sidebarError: string | null;
  sidebarEditingSessionKey: string | null;
  sidebarEditingSessionLabel: string;
  sidebarMoreMenuKey: string | null;
```

- [ ] **Step 1.2: Add @state() properties to app.ts**

In `ui/src/ui/app.ts`, find `@state() sidebarOpen = false;` (around line 252) and add three lines after `@state() sidebarError: string | null = null;`:

```ts
  @state() sidebarOpen = false;
  @state() sidebarContent: SidebarContent | null = null;
  @state() sidebarError: string | null = null;
  @state() sidebarEditingSessionKey: string | null = null;
  @state() sidebarEditingSessionLabel = "";
  @state() sidebarMoreMenuKey: string | null = null;
```

- [ ] **Step 1.3: Verify TypeScript compiles**

```bash
cd /Users/mac/code/sealclaw && pnpm tsgo 2>&1 | head -30
```

Expected: no new errors for the added fields.

---

## Task 2: CSS styles for MoreMenu and edit mode

**Files:**
- Modify: `ui/src/styles/layout.css` (append after `.sidebar-session-item__name` block, around line 737)

- [ ] **Step 2.1: Add CSS**

After the `.sidebar-session-item__name { ... }` block (ends around line 737), add:

```css
/* ── Session sidebar: MoreMenu ── */

.sidebar-session-more-menu-wrapper {
  position: relative;
  flex-shrink: 0;
}

.sidebar-session-item__more {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-sm);
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--muted);
  padding: 0;
  opacity: 0;
  transition: opacity 0.1s, color 0.1s, background 0.1s;
  flex-shrink: 0;
}

.sidebar-session-item:hover .sidebar-session-item__more,
.sidebar-session-item--active .sidebar-session-item__more,
.sidebar-session-item__more:focus-visible {
  opacity: 1;
}

.sidebar-session-item__more:hover {
  color: var(--text);
  background: var(--accent-subtle);
}

.sidebar-session-item__more svg {
  width: 14px;
  height: 14px;
}

.sidebar-session-more-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 2px);
  z-index: 100;
  background: var(--popover, var(--card));
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.15),
    0 1px 3px rgba(0, 0, 0, 0.1);
  padding: 3px;
  min-width: 100px;
}

.sidebar-session-more-menu__item {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  padding: 5px 8px;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 12px;
  color: var(--text);
  text-align: left;
  transition: background 0.08s;
  white-space: nowrap;
}

.sidebar-session-more-menu__item:hover {
  background: var(--accent-subtle);
}

.sidebar-session-more-menu__item svg {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}

/* ── Session sidebar: edit mode ── */

.sidebar-session-item--editing {
  gap: 5px;
  padding: 4px 6px;
  cursor: default;
}

.sidebar-session-item__edit-input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
  font-size: 13px;
  color: var(--text);
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
}

.sidebar-session-item__edit-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

.sidebar-session-item__edit-confirm,
.sidebar-session-item__edit-cancel {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-sm);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  color: var(--muted);
  transition: color 0.1s, background 0.1s;
}

.sidebar-session-item__edit-confirm:hover {
  color: var(--success, #22c55e);
  background: var(--accent-subtle);
}

.sidebar-session-item__edit-cancel:hover {
  color: var(--destructive, #ef4444);
  background: var(--accent-subtle);
}

.sidebar-session-item__edit-confirm svg,
.sidebar-session-item__edit-cancel svg {
  width: 12px;
  height: 12px;
}
```

---

## Task 3: Rewrite session-sidebar.ts

**Files:**
- Modify: `ui/src/ui/chat/session-sidebar.ts` (full replacement)

- [ ] **Step 3.1: Replace session-sidebar.ts**

Replace the entire contents of `ui/src/ui/chat/session-sidebar.ts` with:

```ts
import { html, nothing } from "lit";
import { ref } from "lit/directives/ref.js";
import { repeat } from "lit/directives/repeat.js";
import type { AppViewState } from "../app-view-state.ts";
import { icons } from "../icons.ts";
import { resolveSessionDisplayName, resolveSessionOptionGroups } from "./session-controls.ts";

type TimeGroup = {
  label: string;
  keys: string[];
};

function groupSessionKeysByTime(
  keys: string[],
  updatedAtMap: Map<string, number | null>,
): TimeGroup[] {
  const now = new Date();
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  const weekStart = todayStart - now.getDay() * 86400000;
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime();

  const buckets: [string, string[]][] = [
    ["今天", []],
    ["本周", []],
    ["本月", []],
    ["更早", []],
  ];

  for (const key of keys) {
    const t = updatedAtMap.get(key) ?? 0;
    if (t >= todayStart) buckets[0][1].push(key);
    else if (t >= weekStart) buckets[1][1].push(key);
    else if (t >= monthStart) buckets[2][1].push(key);
    else buckets[3][1].push(key);
  }

  return buckets.filter(([, arr]) => arr.length > 0).map(([label, keys]) => ({ label, keys }));
}

function openMoreMenu(state: AppViewState, key: string) {
  if (state.sidebarMoreMenuKey === key) {
    state.sidebarMoreMenuKey = null;
    return;
  }
  state.sidebarMoreMenuKey = key;
  document.addEventListener(
    "click",
    () => {
      state.sidebarMoreMenuKey = null;
    },
    { once: true },
  );
}

function startEditing(state: AppViewState, key: string, currentLabel: string) {
  state.sidebarMoreMenuKey = null;
  state.sidebarEditingSessionKey = key;
  state.sidebarEditingSessionLabel = currentLabel;
}

function confirmEdit(
  state: AppViewState,
  key: string,
  onRenameSession: (key: string, label: string) => void,
) {
  const trimmed = state.sidebarEditingSessionLabel.trim();
  state.sidebarEditingSessionKey = null;
  state.sidebarEditingSessionLabel = "";
  if (trimmed) {
    onRenameSession(key, trimmed);
  }
}

function cancelEdit(state: AppViewState) {
  state.sidebarEditingSessionKey = null;
  state.sidebarEditingSessionLabel = "";
}

export function renderSidebarSessionList(
  state: AppViewState,
  onSwitchSession: (key: string) => void,
  onNewSession: () => void,
  onRenameSession: (key: string, label: string) => void = () => undefined,
) {
  const sessionGroups = resolveSessionOptionGroups(state, state.sessionKey, state.sessionsResult);
  const allKeys = sessionGroups.flatMap((g) => g.options.map((o) => o.key));
  const labelMap = new Map(
    sessionGroups.flatMap((g) => g.options.map((o) => [o.key, o.label] as const)),
  );
  const rowMap = new Map((state.sessionsResult?.sessions ?? []).map((s) => [s.key, s] as const));
  const updatedAtMap = new Map(allKeys.map((k) => [k, rowMap.get(k)?.updatedAt ?? null]));

  const groups = groupSessionKeysByTime(allKeys, updatedAtMap);

  const autoFocusRef = (el: Element | undefined) => {
    if (el instanceof HTMLInputElement) {
      setTimeout(() => {
        el.focus();
        el.select();
      }, 0);
    }
  };

  return html`
    <div class="sidebar-session-list">
      <div class="sidebar-session-list__header">
        <span class="sidebar-session-list__title">会话</span>
        <button
          class="btn btn--sm btn--icon sidebar-session-list__new-btn"
          ?disabled=${!state.connected}
          title="新建会话"
          aria-label="新建会话"
          @click=${() => onNewSession()}
        >
          ${icons.plus}
        </button>
      </div>
      <div class="sidebar-session-list__scroll">
        ${allKeys.length === 0
          ? html`<div class="sidebar-session-list__empty">
              ${state.connected ? "暂无会话" : "未连接"}
            </div>`
          : groups.map(
              (group) =>
                html`<div class="sidebar-session-time-group">
                  <div class="sidebar-session-time-group__label">${group.label}</div>
                  ${repeat(
                    group.keys,
                    (k) => k,
                    (k) => {
                      const isActive = k === state.sessionKey;
                      const label =
                        labelMap.get(k) ?? resolveSessionDisplayName(k, rowMap.get(k));
                      const isEditing = state.sidebarEditingSessionKey === k;

                      if (isEditing) {
                        return html`
                          <div
                            class="sidebar-session-item sidebar-session-item--editing ${isActive
                              ? "sidebar-session-item--active"
                              : ""}"
                          >
                            <span class="sidebar-session-item__icon" aria-hidden="true">
                              ${icons.messageSquare}
                            </span>
                            <input
                              class="sidebar-session-item__edit-input"
                              type="text"
                              maxlength="100"
                              .value=${state.sidebarEditingSessionLabel}
                              ${ref(autoFocusRef)}
                              @input=${(e: Event) => {
                                state.sidebarEditingSessionLabel = (
                                  e.target as HTMLInputElement
                                ).value;
                              }}
                              @keydown=${(e: KeyboardEvent) => {
                                if (e.key === "Enter") {
                                  e.preventDefault();
                                  confirmEdit(state, k, onRenameSession);
                                }
                                if (e.key === "Escape") {
                                  e.stopPropagation();
                                  cancelEdit(state);
                                }
                              }}
                              @blur=${() => confirmEdit(state, k, onRenameSession)}
                              @click=${(e: Event) => e.stopPropagation()}
                            />
                            <div class="sidebar-session-item__edit-actions">
                              <button
                                class="sidebar-session-item__edit-confirm"
                                @mousedown=${(e: Event) => e.preventDefault()}
                                @click=${(e: Event) => {
                                  e.stopPropagation();
                                  confirmEdit(state, k, onRenameSession);
                                }}
                                title="确认"
                                aria-label="确认重命名"
                              >
                                ${icons.check}
                              </button>
                              <button
                                class="sidebar-session-item__edit-cancel"
                                @mousedown=${(e: Event) => e.preventDefault()}
                                @click=${(e: Event) => {
                                  e.stopPropagation();
                                  cancelEdit(state);
                                }}
                                title="取消"
                                aria-label="取消重命名"
                              >
                                ${icons.x}
                              </button>
                            </div>
                          </div>
                        `;
                      }

                      return html`
                        <div
                          class="sidebar-session-item ${isActive
                            ? "sidebar-session-item--active"
                            : ""}"
                          title=${k}
                          role="button"
                          tabindex="0"
                          @click=${() => onSwitchSession(k)}
                          @dblclick=${(e: Event) => {
                            e.stopPropagation();
                            startEditing(state, k, label);
                          }}
                          @keydown=${(e: KeyboardEvent) => {
                            if (e.key === "Enter" || e.key === " ") {
                              e.preventDefault();
                              onSwitchSession(k);
                            }
                          }}
                        >
                          <span class="sidebar-session-item__icon" aria-hidden="true">
                            ${icons.messageSquare}
                          </span>
                          <span class="sidebar-session-item__name">${label}</span>
                          <div
                            class="sidebar-session-more-menu-wrapper"
                            @click=${(e: Event) => e.stopPropagation()}
                            @dblclick=${(e: Event) => e.stopPropagation()}
                          >
                            <button
                              class="sidebar-session-item__more"
                              title="更多选项"
                              aria-label="会话选项"
                              @click=${() => openMoreMenu(state, k)}
                            >
                              ${icons.moreHorizontal}
                            </button>
                            ${state.sidebarMoreMenuKey === k
                              ? html`
                                  <div class="sidebar-session-more-menu">
                                    <button
                                      class="sidebar-session-more-menu__item"
                                      @mousedown=${(e: Event) => e.preventDefault()}
                                      @click=${() => startEditing(state, k, label)}
                                    >
                                      ${icons.edit} 重命名
                                    </button>
                                  </div>
                                `
                              : nothing}
                          </div>
                        </div>
                      `;
                    },
                  )}
                </div>`,
            )}
      </div>
    </div>
  `;
}
```

- [ ] **Step 3.2: Verify TypeScript compiles**

```bash
cd /Users/mac/code/sealclaw && pnpm tsgo 2>&1 | head -30
```

Expected: no errors in `session-sidebar.ts`.

---

## Task 4: Wire rename handler + default label "新会话"

**Files:**
- Modify: `ui/src/ui/app-render.helpers.ts`

- [ ] **Step 4.1: Add patchSession import**

In `ui/src/ui/app-render.helpers.ts`, change the sessions import line (line 17):

```ts
import { createSessionAndRefresh, loadSessions, patchSession } from "./controllers/sessions.ts";
```

- [ ] **Step 4.2: Update renderSidebarSessionList to pass rename handler**

Find `renderSidebarSessionList` (around line 237) and replace it:

```ts
export function renderSidebarSessionList(state: AppViewState) {
  return renderSidebarSessionListBase(
    state,
    (key) => switchChatSession(state, key),
    () => createChatSession(state),
    (key, label) => {
      void patchSession(
        state as unknown as Parameters<typeof patchSession>[0],
        key,
        { label },
      );
    },
  );
}
```

- [ ] **Step 4.3: Add default label to createChatSession**

Find the `createSessionAndRefresh` call inside `createChatSession` (around line 639) and add `label: "新会话"`:

```ts
  const nextSessionKey = await createSessionAndRefresh(
    state as unknown as Parameters<typeof createSessionAndRefresh>[0],
    {
      agentId: resolveAgentIdFromSessionKey(previousSessionKey),
      parentSessionKey,
      label: "新会话",
    },
    {
      activeMinutes: 0,
      limit: 0,
      includeGlobal: true,
      includeUnknown: true,
      showArchived: state.sessionsShowArchived,
    },
  );
```

- [ ] **Step 4.4: Verify TypeScript compiles**

```bash
cd /Users/mac/code/sealclaw && pnpm tsgo 2>&1 | head -30
```

Expected: no new errors.

---

## Task 5: Auto-rename on first message

**Files:**
- Modify: `ui/src/ui/app-chat.ts`

- [ ] **Step 5.1: Add auto-rename in sendChatMessageNow**

In `ui/src/ui/app-chat.ts`, find `sendChatMessageNow` (around line 219). Inside the first `if (ok)` block (the one with `setLastActiveSessionKey`), add the auto-rename after `resetChatInputHistoryNavigation(host)`:

```ts
  if (ok) {
    setLastActiveSessionKey(
      host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
      host.sessionKey,
    );
    resetChatInputHistoryNavigation(host);
    // Auto-rename sessions that still have the default "新会话" label
    const sessionRow = host.sessionsResult?.sessions?.find((s) => s.key === host.sessionKey);
    if (sessionRow?.label === "新会话") {
      const newLabel = message.trim().slice(0, 10);
      if (newLabel && host.client && host.connected) {
        void host.client.request("sessions.patch", { key: host.sessionKey, label: newLabel });
      }
    }
  }
```

- [ ] **Step 5.2: Verify TypeScript compiles**

```bash
cd /Users/mac/code/sealclaw && pnpm tsgo 2>&1 | head -30
```

Expected: no new errors.

---

## Task 6: Full compile + format check

- [ ] **Step 6.1: Run changed gate**

```bash
cd /Users/mac/code/sealclaw && pnpm check:changed 2>&1 | tail -30
```

Expected: passes without errors on the changed UI files.

- [ ] **Step 6.2: Format changed files**

```bash
cd /Users/mac/code/sealclaw && pnpm exec oxfmt --write --threads=1 \
  ui/src/ui/app-view-state.ts \
  ui/src/ui/app.ts \
  ui/src/ui/chat/session-sidebar.ts \
  ui/src/ui/app-render.helpers.ts \
  ui/src/ui/app-chat.ts
```

Expected: exits 0.

- [ ] **Step 6.3: Commit**

```bash
cd /Users/mac/code/sealclaw && scripts/committer "feat(ui): session sidebar rename, default name '新会话', auto-rename on first message" \
  ui/src/ui/app-view-state.ts \
  ui/src/ui/app.ts \
  ui/src/styles/layout.css \
  ui/src/ui/chat/session-sidebar.ts \
  ui/src/ui/app-render.helpers.ts \
  ui/src/ui/app-chat.ts
```
