diff --git a/CHANGELOG.md b/CHANGELOG.md index c4aa57199..bdb5c2688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.clawd.bot - Auth: dedupe codex-cli profiles when tokens match custom openai-codex entries. (#1264) — thanks @odrobnik. - Agents: avoid misclassifying context-window-too-small errors as context overflow. (#1266) — thanks @humanwritten. - Slack: resolve Bolt default-export shapes for monitor startup. (#1208) — thanks @24601. +- UI: allow editing session labels in the Sessions table. (#1294) — thanks @bradleypriest. ## 2026.1.19-3 diff --git a/src/gateway/server.health.test.ts b/src/gateway/server.health.test.ts index 92f71e0e4..173e654f9 100644 --- a/src/gateway/server.health.test.ts +++ b/src/gateway/server.health.test.ts @@ -10,7 +10,6 @@ import { signDevicePayload, } from "../infra/device-identity.js"; import { emitHeartbeatEvent } from "../infra/heartbeat-events.js"; -import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { connectOk, diff --git a/src/process/command-queue.ts b/src/process/command-queue.ts index 51b21881c..8e4e7377d 100644 --- a/src/process/command-queue.ts +++ b/src/process/command-queue.ts @@ -111,7 +111,7 @@ export function enqueueCommand( return enqueueCommandInLane(CommandLane.Main, task, opts); } -export function getQueueSize(lane = CommandLane.Main) { +export function getQueueSize(lane: string = CommandLane.Main) { const state = lanes.get(lane); if (!state) return 0; return state.queue.length + state.active; @@ -125,7 +125,7 @@ export function getTotalQueueSize() { return total; } -export function clearCommandLane(lane = CommandLane.Main) { +export function clearCommandLane(lane: string = CommandLane.Main) { const cleaned = lane.trim() || CommandLane.Main; const state = lanes.get(cleaned); if (!state) return 0; diff --git a/ui/src/ui/views/sessions.test.ts b/ui/src/ui/views/sessions.test.ts new file mode 100644 index 000000000..d96cae797 --- /dev/null +++ b/ui/src/ui/views/sessions.test.ts @@ -0,0 +1,93 @@ +import { render } from "lit"; +import { describe, expect, it, vi } from "vitest"; + +import type { GatewaySessionRow, SessionsListResult } from "../types"; +import { renderSessions, type SessionsProps } from "./sessions"; + +function createRow(overrides: Partial = {}): GatewaySessionRow { + return { + key: "session-1", + kind: "direct", + updatedAt: 0, + ...overrides, + }; +} + +function createResult(rows: GatewaySessionRow[]): SessionsListResult { + return { + ts: 0, + path: "/sessions", + count: rows.length, + defaults: { + model: null, + contextTokens: null, + }, + sessions: rows, + }; +} + +function createProps(overrides: Partial = {}): SessionsProps { + return { + loading: false, + result: createResult([]), + error: null, + activeMinutes: "", + limit: "", + includeGlobal: true, + includeUnknown: true, + basePath: "/", + onFiltersChange: () => undefined, + onRefresh: () => undefined, + onPatch: () => undefined, + onDelete: () => undefined, + ...overrides, + }; +} + +describe("sessions view", () => { + it("skips patching when the label is unchanged", () => { + const container = document.createElement("div"); + const onPatch = vi.fn(); + const row = createRow({ label: "Alpha" }); + + render( + renderSessions( + createProps({ + result: createResult([row]), + onPatch, + }), + ), + container, + ); + + const input = container.querySelector("input") as HTMLInputElement | null; + expect(input).not.toBeNull(); + input?.dispatchEvent(new Event("change", { bubbles: true })); + + expect(onPatch).not.toHaveBeenCalled(); + }); + + it("clears labels when the input is empty", () => { + const container = document.createElement("div"); + const onPatch = vi.fn(); + const row = createRow({ label: "Alpha" }); + + render( + renderSessions( + createProps({ + result: createResult([row]), + onPatch, + }), + ), + container, + ); + + const input = container.querySelector("input") as HTMLInputElement | null; + expect(input).not.toBeNull(); + if (!input) return; + input.value = " "; + input.dispatchEvent(new Event("change", { bubbles: true })); + + expect(onPatch).toHaveBeenCalledWith("session-1", { label: null }); + }); +}); diff --git a/ui/src/ui/views/sessions.ts b/ui/src/ui/views/sessions.ts index 7b5e97eb7..397373d05 100644 --- a/ui/src/ui/views/sessions.ts +++ b/ui/src/ui/views/sessions.ts @@ -203,6 +203,8 @@ function renderRow( placeholder="(optional)" @change=${(e: Event) => { const value = (e.target as HTMLInputElement).value.trim(); + const current = (row.label ?? "").trim(); + if (value === current) return; onPatch(row.key, { label: value || null }); }} />