From dd9cc2f78d8231364e9e25d2125696c883b24555 Mon Sep 17 00:00:00 2001 From: jaeko44 Date: Thu, 26 Mar 2026 20:43:55 +1100 Subject: [PATCH 1/2] chore: auto-commit agent work (ac13245d-327) --- site/ui/styles.css | 48 ++++++++++++++ site/ui/tabs/agents.js | 42 ++++++++++++ tests/fleet-tab-render.test.mjs | 10 +++ tests/ui-agents-session-pill.test.mjs | 93 +++++++++++++++++++++++++++ ui/styles.css | 48 ++++++++++++++ ui/tabs/agents.js | 42 ++++++++++++ 6 files changed, 283 insertions(+) create mode 100644 tests/ui-agents-session-pill.test.mjs diff --git a/site/ui/styles.css b/site/ui/styles.css index 8a9fb8737..4ce4eff3e 100644 --- a/site/ui/styles.css +++ b/site/ui/styles.css @@ -646,3 +646,51 @@ font-size: 12px; } + + +.fleet-session-id-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 10px; + border-radius: 999px; + border: 1px solid var(--border-color, rgba(255,255,255,0.12)); + background: color-mix(in srgb, var(--surface-elevated, rgba(255,255,255,0.06)) 88%, transparent); + color: var(--text-secondary); + cursor: pointer; + transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease, transform 160ms ease; +} + +.fleet-session-id-pill:hover, +.fleet-session-id-pill:focus-visible { + border-color: var(--accent-color, #7c3aed); + color: var(--text-primary); + outline: none; +} + +.fleet-session-id-pill:active { + transform: translateY(1px); +} + +.fleet-session-id-pill-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.1em; +} + +.fleet-session-id-pill[data-copied="true"] { + color: var(--color-done); + border-color: color-mix(in srgb, var(--color-done) 70%, transparent); + background: color-mix(in srgb, var(--color-done) 14%, transparent); + animation: fleet-session-pill-copied 1.5s ease; +} + +@keyframes fleet-session-pill-copied { + 0%, 100% { + transform: translateY(0); + } + 20% { + transform: translateY(-1px); + } +} diff --git a/site/ui/tabs/agents.js b/site/ui/tabs/agents.js index fe11ab808..811d6ea1a 100644 --- a/site/ui/tabs/agents.js +++ b/site/ui/tabs/agents.js @@ -1315,6 +1315,18 @@ export function AgentsTab() { }, [slots, fleetSearch]); const allSessions = sessionsData.value || []; + + const copySessionId = (sessionId) => { + if (!sessionId) return; + setCopiedSessionId(sessionId); + navigator.clipboard + .writeText(sessionId) + .then(() => showToast("Session ID copied", "success")) + .catch(() => { + setCopiedSessionId(""); + showToast("Copy failed", "error"); + }); + }; const activeSessionCount = allSessions.filter((session) => { if (!session || typeof session !== "object") return false; if (session.active === true) return true; @@ -2087,10 +2099,23 @@ function FleetSessionsPanel({ slots, taskFallbackEntries = [], onOpenWorkspace, const [sessionScope, setSessionScope] = useState(FLEET_SESSION_SCOPE.all); const [sessionSearch, setSessionSearch] = useState(""); const [selectedEntryKey, setSelectedEntryKey] = useState(null); + const [copiedSessionId, setCopiedSessionId] = useState(""); const [logText, setLogText] = useState("(no logs yet)"); const logRef = useRef(null); const allSessions = sessionsData.value || []; + const copySessionId = (sessionId) => { + if (!sessionId) return; + setCopiedSessionId(sessionId); + navigator.clipboard + .writeText(sessionId) + .then(() => showToast("Session ID copied", "success")) + .catch(() => { + setCopiedSessionId(""); + showToast("Copy failed", "error"); + }); + }; + /* Stabilise entries with useMemo so the reference only changes when the underlying data actually changes – prevents infinite render loops that previously caused "insertBefore" DOM errors. */ @@ -2385,6 +2410,23 @@ function FleetSessionsPanel({ slots, taskFallbackEntries = [], onOpenWorkspace, ${entryStatus || "unknown"} + ${sessionId + ? html`` + : null} ${entry.slot?.branch ? html`${entry.slot.branch}` : entry.session?.branch diff --git a/tests/fleet-tab-render.test.mjs b/tests/fleet-tab-render.test.mjs index d5b6b1f59..5354454dc 100644 --- a/tests/fleet-tab-render.test.mjs +++ b/tests/fleet-tab-render.test.mjs @@ -31,6 +31,16 @@ const sessionListSourceFiles = [ for (const { relPath, source } of sourceFiles) { describe(`FleetSessionsPanel render stability (${relPath})`, () => { + it("renders a keyboard-accessible session id pill with copy feedback state", () => { + expect(source).toContain("fleet-session-id-pill"); + expect(source).toContain("type=\"button\""); + expect(source).toContain("aria-label=${`Copy session ID ${sessionId}`}\"); + expect(source).toContain("data-copied=${copiedSessionId === sessionId ? \"true\" : \"false\"}"); + expect(source).toContain("sessionId.slice(0, 8)"); + expect(source).toContain("copySessionId(sessionId)"); + expect(source).toContain("fleet-session-id-pill-icon"); + expect(source).toContain('copiedSessionId === sessionId ? "✓" : ICONS.copy'); + }); it("never fabricates session ids for task-only fallback entries", () => { expect(source).toContain("function resolveFleetEntrySessionId(entry)"); expect(source).toContain("if (entry?.isTaskFallback || entry?.slot?.synthetic) return \"\";"); diff --git a/tests/ui-agents-session-pill.test.mjs b/tests/ui-agents-session-pill.test.mjs new file mode 100644 index 000000000..d809e32f0 --- /dev/null +++ b/tests/ui-agents-session-pill.test.mjs @@ -0,0 +1,93 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { h } from "preact"; +import { cleanup, fireEvent, render } from "@testing-library/preact"; +import htm from "htm"; + +const html = htm.bind(h); + +vi.mock("../ui/modules/telegram.js", () => ({ + haptic: vi.fn(), + showConfirm: vi.fn(), +})); + +vi.mock("../ui/modules/api.js", () => ({ + apiFetch: vi.fn(() => Promise.resolve({ data: [] })), + sendCommandToChat: vi.fn(), +})); + +vi.mock("../ui/modules/state.js", async () => { + const actual = await vi.importActual("../ui/modules/state.js"); + return { + ...actual, + executorData: { + value: { + data: { + slots: [ + { + taskId: "task-1", + taskTitle: "Agent task", + branch: "feature/test", + status: "busy", + sessionId: "12345678-1234-1234-1234-1234567890ab", + startedAt: "2026-03-21T00:00:00.000Z", + }, + ], + }, + }, + }, + showToast: vi.fn(), + scheduleRefresh: vi.fn(), + refreshTab: vi.fn(), + }; +}); + +vi.mock("../ui/components/session-list.js", () => ({ + loadSessions: vi.fn(), + loadSessionMessages: vi.fn(), + selectedSessionId: { value: null }, + sessionsData: { + value: [ + { + id: "12345678-1234-1234-1234-1234567890ab", + taskId: "task-1", + title: "Agent task", + branch: "feature/test", + status: "active", + createdAt: "2026-03-21T00:00:00.000Z", + lastActiveAt: "2026-03-21T00:00:10.000Z", + }, + ], + }, + sessionMessages: { value: [] }, + sessionMessagesSessionId: { value: null }, +})); + +vi.mock("../ui/components/chat-view.js", () => ({ ChatView: () => html`
` })); +vi.mock("../ui/components/diff-viewer.js", () => ({ DiffViewer: () => html`
` })); + +describe("agents session ID pill", () => { + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + }); + + it("copies the full session id and clears copied state after animation end", async () => { + const clipboardWrite = vi.fn().mockResolvedValue(); + Object.defineProperty(globalThis, "navigator", { + value: { clipboard: { writeText: clipboardWrite } }, + configurable: true, + }); + + const mod = await import("../ui/tabs/agents.js"); + const FleetSessionsTab = mod.FleetSessionsTab; + const { findByRole } = render(html`<${FleetSessionsTab} />`); + const pill = await findByRole("button", { name: /copy session id 12345678-1234-1234-1234-1234567890ab/i }); + + await fireEvent.click(pill); + expect(clipboardWrite).toHaveBeenCalledWith("12345678-1234-1234-1234-1234567890ab"); + expect(pill.getAttribute("data-copied")).toBe("true"); + + fireEvent.animationEnd(pill); + expect(pill.getAttribute("data-copied")).toBe("false"); + }); +}); diff --git a/ui/styles.css b/ui/styles.css index 3edf6eb10..cf01fb7ed 100644 --- a/ui/styles.css +++ b/ui/styles.css @@ -890,3 +890,51 @@ button.task-related-link-chip { appearance: none; } + + +.fleet-session-id-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 10px; + border-radius: 999px; + border: 1px solid var(--border-color, rgba(255,255,255,0.12)); + background: color-mix(in srgb, var(--surface-elevated, rgba(255,255,255,0.06)) 88%, transparent); + color: var(--text-secondary); + cursor: pointer; + transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease, transform 160ms ease; +} + +.fleet-session-id-pill:hover, +.fleet-session-id-pill:focus-visible { + border-color: var(--accent-color, #7c3aed); + color: var(--text-primary); + outline: none; +} + +.fleet-session-id-pill:active { + transform: translateY(1px); +} + +.fleet-session-id-pill-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.1em; +} + +.fleet-session-id-pill[data-copied="true"] { + color: var(--color-done); + border-color: color-mix(in srgb, var(--color-done) 70%, transparent); + background: color-mix(in srgb, var(--color-done) 14%, transparent); + animation: fleet-session-pill-copied 1.5s ease; +} + +@keyframes fleet-session-pill-copied { + 0%, 100% { + transform: translateY(0); + } + 20% { + transform: translateY(-1px); + } +} diff --git a/ui/tabs/agents.js b/ui/tabs/agents.js index fe11ab808..811d6ea1a 100644 --- a/ui/tabs/agents.js +++ b/ui/tabs/agents.js @@ -1315,6 +1315,18 @@ export function AgentsTab() { }, [slots, fleetSearch]); const allSessions = sessionsData.value || []; + + const copySessionId = (sessionId) => { + if (!sessionId) return; + setCopiedSessionId(sessionId); + navigator.clipboard + .writeText(sessionId) + .then(() => showToast("Session ID copied", "success")) + .catch(() => { + setCopiedSessionId(""); + showToast("Copy failed", "error"); + }); + }; const activeSessionCount = allSessions.filter((session) => { if (!session || typeof session !== "object") return false; if (session.active === true) return true; @@ -2087,10 +2099,23 @@ function FleetSessionsPanel({ slots, taskFallbackEntries = [], onOpenWorkspace, const [sessionScope, setSessionScope] = useState(FLEET_SESSION_SCOPE.all); const [sessionSearch, setSessionSearch] = useState(""); const [selectedEntryKey, setSelectedEntryKey] = useState(null); + const [copiedSessionId, setCopiedSessionId] = useState(""); const [logText, setLogText] = useState("(no logs yet)"); const logRef = useRef(null); const allSessions = sessionsData.value || []; + const copySessionId = (sessionId) => { + if (!sessionId) return; + setCopiedSessionId(sessionId); + navigator.clipboard + .writeText(sessionId) + .then(() => showToast("Session ID copied", "success")) + .catch(() => { + setCopiedSessionId(""); + showToast("Copy failed", "error"); + }); + }; + /* Stabilise entries with useMemo so the reference only changes when the underlying data actually changes – prevents infinite render loops that previously caused "insertBefore" DOM errors. */ @@ -2385,6 +2410,23 @@ function FleetSessionsPanel({ slots, taskFallbackEntries = [], onOpenWorkspace, ${entryStatus || "unknown"} + ${sessionId + ? html`` + : null} ${entry.slot?.branch ? html`${entry.slot.branch}` : entry.session?.branch From 78682d0ae906ad63d33e15c37f31616c292c8b13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 04:17:28 +0000 Subject: [PATCH 2/2] fix(agents): address review feedback on session-id pill copy button - Fix syntax error in fleet-tab-render.test.mjs (unclosed string literal) - Rewrite ui-agents-session-pill.test.mjs as source-level test (no @testing-library/preact) - Remove erroneous copySessionId from AgentsTab (referenced undefined setCopiedSessionId) - Add const sessionId = resolveFleetEntrySessionId(entry) in visibleEntries.map callback - Add navigator?.clipboard?.writeText guard to copySessionId in both ui/ and site/ui/ - Fix nested