From 158d9e2b18bed147626bac63973d97f44f063d72 Mon Sep 17 00:00:00 2001 From: Harbor404 <2657212322@qq.com> Date: Fri, 2 Oct 2026 13:31:01 +0800 Subject: [PATCH 1/2] feat(studio): copy session handoff information --- desktop/frontend-next/src/actions.ts | 1 + desktop/frontend-next/src/i18n/en.ts | 5 ++ desktop/frontend-next/src/ui/Workspaces.tsx | 5 ++ .../ui/session-copy-info.interaction.test.tsx | 53 +++++++++++++++++++ .../frontend-next/src/ui/sessionInfo.test.ts | 23 ++++++++ desktop/frontend-next/src/ui/sessionInfo.ts | 21 ++++++++ 6 files changed, 108 insertions(+) create mode 100644 desktop/frontend-next/src/ui/session-copy-info.interaction.test.tsx create mode 100644 desktop/frontend-next/src/ui/sessionInfo.test.ts create mode 100644 desktop/frontend-next/src/ui/sessionInfo.ts diff --git a/desktop/frontend-next/src/actions.ts b/desktop/frontend-next/src/actions.ts index c2e034d45c..fde8da2fc4 100644 --- a/desktop/frontend-next/src/actions.ts +++ b/desktop/frontend-next/src/actions.ts @@ -81,6 +81,7 @@ export const ACTIONS: UIAction[] = [ // refused while a turn runs, so it never stops work. { id: "session.close", kind: "kernel-mutation", target: "entity", proof: "interaction" }, { id: "session.archive", kind: "kernel-mutation", target: "entity", proof: "authority-effect" }, + { id: "session.copy-info", kind: "shell-native", target: "entity", proof: "interaction" }, { id: "session.export", kind: "shell-native", target: "entity", proof: "browser" }, { id: "session.delete", kind: "destructive", target: "entity", proof: "authority-effect" }, diff --git a/desktop/frontend-next/src/i18n/en.ts b/desktop/frontend-next/src/i18n/en.ts index 135822b1ca..26843d7d98 100644 --- a/desktop/frontend-next/src/i18n/en.ts +++ b/desktop/frontend-next/src/i18n/en.ts @@ -1116,6 +1116,11 @@ export const EN: Record = { "读取": "Read", "本地工作区": "Local workspace", "暂停执行": "Pause run", + "复制会话信息": "Copy session information", + "会话 ID": "Session ID", + "会话上下文路径": "Session context path", + "任务路径": "Task path", + "任务日志": "Task log", "导出会话": "Export session", "取消置顶": "Unpin", "置顶会话": "Pin session", diff --git a/desktop/frontend-next/src/ui/Workspaces.tsx b/desktop/frontend-next/src/ui/Workspaces.tsx index 6e1a94b7eb..3afd8f3bb4 100644 --- a/desktop/frontend-next/src/ui/Workspaces.tsx +++ b/desktop/frontend-next/src/ui/Workspaces.tsx @@ -7,6 +7,8 @@ import type { Adder } from "./addws"; import { useRailQuery } from "./railsearch"; import { StudioIcon } from "./StudioIcon"; import { Cross } from "./glyphs"; +import { copyText } from "./CopyButton"; +import { sessionInfoText } from "./sessionInfo"; import { download } from "../port/download"; import { host } from "../port/host"; import { useTreeKeys } from "./tree"; @@ -598,6 +600,9 @@ function WorkspacesView({ hub, tree, treeRead, runtimes, active, folded, reload,
+ diff --git a/desktop/frontend-next/src/ui/session-copy-info.interaction.test.tsx b/desktop/frontend-next/src/ui/session-copy-info.interaction.test.tsx new file mode 100644 index 0000000000..ae472aa15b --- /dev/null +++ b/desktop/frontend-next/src/ui/session-copy-info.interaction.test.tsx @@ -0,0 +1,53 @@ +// @vitest-environment jsdom +import { afterEach, expect, it, vi } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "./testkit"; +import { Workspaces } from "./Workspaces"; +import type { TreeWorkspace } from "../port/hub"; + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +const SESSION = "/w/.reasonix/sessions/20260901-120000.jsonl"; + +it("copies the four handoff fields from the session menu", async () => { + const user = userEvent.setup(); + const write = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(); + const tree: TreeWorkspace[] = [ + { root: "/w/project", name: "project", sessions: [{ path: SESSION, name: "20260901-120000", title: "handoff me" }] }, + ]; + render( + {}} + reload={async () => {}} + onOpen={async () => {}} + onFocus={() => {}} + onClose={async () => {}} + liveIds={() => []} + runs={{}} + onRename={() => {}} + onError={() => {}} + adder={{ add: () => {}, close: () => {}, at: null } as never} + />, + ); + + await user.pointer({ keys: "[MouseRight]", target: screen.getByRole("treeitem", { name: /handoff me/ }) }); + await user.click(screen.getByRole("menuitem", { name: "复制会话信息" })); + + expect(write).toHaveBeenCalledWith([ + "会话 ID: 20260901-120000", + "会话上下文路径: /w/.reasonix/sessions", + "任务路径: /w/project", + "任务日志: /w/.reasonix/sessions/20260901-120000.jsonl", + ].join("\n")); + expect(screen.queryByRole("menu")).toBeNull(); +}); diff --git a/desktop/frontend-next/src/ui/sessionInfo.test.ts b/desktop/frontend-next/src/ui/sessionInfo.test.ts new file mode 100644 index 0000000000..9d8df8651f --- /dev/null +++ b/desktop/frontend-next/src/ui/sessionInfo.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { sessionInfoText } from "./sessionInfo"; + +describe("session handoff information", () => { + it("names the four paths another agent needs, with the context directory kept distinct from the task log", () => { + expect(sessionInfoText( + { name: "20261002-handoff", path: "/home/me/.reasonix/sessions/20261002-handoff.jsonl" }, + "/home/me/project", + )).toBe([ + "会话 ID: 20261002-handoff", + "会话上下文路径: /home/me/.reasonix/sessions", + "任务路径: /home/me/project", + "任务日志: /home/me/.reasonix/sessions/20261002-handoff.jsonl", + ].join("\n")); + }); + + it("keeps a Windows context directory intact", () => { + expect(sessionInfoText( + { name: "20261002-win", path: String.raw`C:\Users\me\.reasonix\sessions\20261002-win.jsonl` }, + String.raw`C:\work\project`, + )).toContain(String.raw`会话上下文路径: C:\Users\me\.reasonix\sessions`); + }); +}); diff --git a/desktop/frontend-next/src/ui/sessionInfo.ts b/desktop/frontend-next/src/ui/sessionInfo.ts new file mode 100644 index 0000000000..490e02cc6b --- /dev/null +++ b/desktop/frontend-next/src/ui/sessionInfo.ts @@ -0,0 +1,21 @@ +import { t } from "../i18n"; + +interface SessionInfo { + name: string; + path: string; +} + +function parentPath(path: string): string { + const at = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")); + return at > 0 ? path.slice(0, at) : path; +} + +/** The clipboard block that names one conversation's files and workspace. */ +export function sessionInfoText(session: SessionInfo, taskPath: string): string { + return [ + `${t("会话 ID")}: ${session.name}`, + `${t("会话上下文路径")}: ${parentPath(session.path)}`, + `${t("任务路径")}: ${taskPath}`, + `${t("任务日志")}: ${session.path}`, + ].join("\n"); +} From 63bbb1334dd274ad27c6015d0c974d311d8476de Mon Sep 17 00:00:00 2001 From: Harbor404 <2657212322@qq.com> Date: Sat, 3 Oct 2026 02:59:04 +0800 Subject: [PATCH 2/2] docs(ui): remove session info helper comment --- desktop/frontend-next/src/ui/sessionInfo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/desktop/frontend-next/src/ui/sessionInfo.ts b/desktop/frontend-next/src/ui/sessionInfo.ts index 490e02cc6b..c8a48a4a51 100644 --- a/desktop/frontend-next/src/ui/sessionInfo.ts +++ b/desktop/frontend-next/src/ui/sessionInfo.ts @@ -10,7 +10,6 @@ function parentPath(path: string): string { return at > 0 ? path.slice(0, at) : path; } -/** The clipboard block that names one conversation's files and workspace. */ export function sessionInfoText(session: SessionInfo, taskPath: string): string { return [ `${t("会话 ID")}: ${session.name}`,