From 5a034ba216eb3bd8319831cd817ec3ac519a6312 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 21:51:29 +0000 Subject: [PATCH 1/3] Bring your history: Chats and Memories toggles, sources one level deeper Leo (2026-11-05): "why isn't it just import memories and chats? ... import clone chats, import clone memory, and then you can go deeper if you want". The first view is now one list of two groups, Chats and Memories, each a switch covering every detected source in it, with a one-line summary of what it brings. Each group's Sources disclosure opens the per-source rows (checkbox, mark, title, path or file under it, Choose export / Paste a summary), sorted alphabetically in the page language. "Found on this Mac" labels are gone. The header, list and notes share one scroll region above the footer, so no card is clipped under a fixed header. The footer paints no band, only a hairline, with the live status between Skip and Import; the keycap is spaced and one step quieter (approach from #34594 by austinywang). Type and spacing use the shared scale (heading, body, detail, caption; 4-32 steps). Every source, the paste flow, Find exports, Undo and the credentials note stay. Regression: test/import-history-groups.test.tsx, 5 fail on base and 5 pass on head; model.test.ts 18 pass. Co-authored-by: Austin Wang Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-c5293b061743fb106b9f --- .../import-history/ImportHistory.gallery.tsx | 28 +- .../pages/import-history/ImportHistory.tsx | 352 +++++++--- .../import-history/Localizable.xcstrings | 655 ++++++++++++++++++ .../pages/import-history/galleryFixture.tsx | 14 +- webviews/src/pages/import-history/main.tsx | 1 + webviews/src/pages/import-history/styles.css | 393 +++++++---- webviews/test/import-history-groups.test.tsx | 137 ++++ 7 files changed, 1325 insertions(+), 255 deletions(-) create mode 100644 webviews/test/import-history-groups.test.tsx diff --git a/webviews/src/pages/import-history/ImportHistory.gallery.tsx b/webviews/src/pages/import-history/ImportHistory.gallery.tsx index fb070e30063f..5ec3b39106f0 100644 --- a/webviews/src/pages/import-history/ImportHistory.gallery.tsx +++ b/webviews/src/pages/import-history/ImportHistory.gallery.tsx @@ -9,6 +9,7 @@ type Props = { empty?: boolean; paste?: boolean; autodetected?: boolean; + open?: boolean; }; export default componentEntry({ @@ -24,31 +25,36 @@ export default componentEntry({ ], load: async () => (await import("./galleryFixture")).ImportHistoryGallery, styles: async () => { + await import("../../ui/ui.css"); await import("../shared/pageBase.css"); await import("./styles.css"); }, variants: { - list: { - note: "Recommended: one aligned list keeps every source visible and its action beside it.", - props: { layout: "list" }, + groups: { + note: "Recommended: two toggles, Chats and Memories, in one list; each source sits one level deeper behind Sources.", + props: { layout: "groups" }, }, - cards: { - note: "Two columns trade a shorter list for more eye movement.", - props: { layout: "cards" }, + "groups-open": { + note: "The same list with both groups' Sources open: sources in alphabetical order, each path under its name.", + props: { layout: "groups", open: true }, }, - compact: { - note: "A denser checklist for narrow or short panes.", - props: { layout: "compact" }, + sections: { + note: "Each group its own card with every source always listed; more to read before the first click.", + props: { layout: "sections" }, + }, + tiles: { + note: "The two groups side by side; the summaries wrap sooner and the sources open under one tile only.", + props: { layout: "tiles" }, }, "nothing-detected": { props: { empty: true } }, detecting: { props: { phase: "detecting" } }, - importing: { props: { phase: "importing" } }, + importing: { props: { phase: "importing", open: true } }, done: { props: { phase: "done" } }, error: { props: { phase: "error" } }, "paste-summary": { props: { paste: true } }, autodetected: { note: "Archives found in the usual export folders are checked inline; manual pick remains available on each archive row.", - props: { autodetected: true }, + props: { autodetected: true, open: true }, }, }, }); diff --git a/webviews/src/pages/import-history/ImportHistory.tsx b/webviews/src/pages/import-history/ImportHistory.tsx index 339840de5072..e2df28f1f9fd 100644 --- a/webviews/src/pages/import-history/ImportHistory.tsx +++ b/webviews/src/pages/import-history/ImportHistory.tsx @@ -1,18 +1,37 @@ -import { useSyncExternalStore } from "react"; +import { useId, useState, useSyncExternalStore } from "react"; +import { ChevronRight } from "../../agent-session/acpmux/conversation/icons"; import { AgentMark } from "../../agent-session/shared/AgentMark"; import { DialogButton } from "../../ui/DialogButton"; +import { Switch } from "../../ui/Switch"; import type { Strings } from "../shared/i18n"; -import { canImport, type HistoryRow, type ImportHistoryStore, type Origin } from "./model"; +import { canImport, type HistoryRow, type ImportHistoryStore, type Origin, type RowID } from "./model"; -export type HistoryLayout = "list" | "cards" | "compact"; +/** `groups` ships: two toggles (Chats, Memories) in one list, each source one level deeper behind + * its group's Sources disclosure. `sections` and `tiles` are gallery alternatives. */ +export type HistoryLayout = "groups" | "sections" | "tiles"; export interface ImportHistoryProps { store: ImportHistoryStore; strings: Strings; layout?: HistoryLayout; + /** Opens every group's Sources at first draw (gallery states). */ + defaultOpen?: boolean; } -/** The list is the shipped layout: all sources share one scan line, with no wizard or card grid. */ -export function ImportHistory({ store, strings, layout = "list" }: ImportHistoryProps) { +export type GroupID = "chats" | "memories"; +export const GROUPS: readonly GroupID[] = ["chats", "memories"]; + +/** Conversations and chat exports are chats; memory notes and pasted summaries are memories. */ +export function groupOf(row: HistoryRow): GroupID { + if (row.kind === "text" || row.id === "claude-memory" || row.id === "codex") return "memories"; + return "chats"; +} + +/** A row the importer can take now: found on this Mac, picked, or holding a pasted summary. */ +export function isAvailable(row: HistoryRow): boolean { + return row.kind === "text" ? Boolean(row.text?.trim()) : Boolean(row.source); +} + +export function ImportHistory({ store, strings, layout = "groups", defaultOpen = false }: ImportHistoryProps) { const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot); const t = strings.t; const busy = snapshot.phase === "importing" || snapshot.undoing; @@ -26,53 +45,52 @@ export function ImportHistory({ store, strings, layout = "list" }: ImportHistory else void store.run(); }} > -
-

{t("history.title")}

-

{t("history.subtitle")}

-
-
- - {t(`history.state.${snapshot.phase}`)} - - -
-
- {snapshot.rows.map((row) => ( - - ))} -
- {(snapshot.error || snapshot.phase === "error") && ( -
-

- {t("history.error")} - {detail && {detail}} -

- {snapshot.error && !busy && ( - + {/* One scroll region holds everything above the footer, so nothing scrolls under a fixed + header and no card is clipped beneath it. */} +
+
+
+

{t("history.title")}

+

{t("history.subtitle")}

+
+
+ {GROUPS.map((group) => ( + groupOf(row) === group)} + store={store} + strings={strings} + busy={busy || snapshot.phase === "detecting"} + alwaysOpen={layout === "sections"} + defaultOpen={defaultOpen} + /> + ))} +
+ {(snapshot.error || snapshot.phase === "error") && ( +
+

+ {t("history.error")} + {detail && {detail}} +

+ {snapshot.error && !busy && ( + + )} +
)} + {snapshot.notice &&

{snapshot.notice}

} +

{t("history.safety")}

- )} - {snapshot.notice &&

{snapshot.notice}

} -

{t("history.safety")}

+