From 103132a359360999ed861d4f59311ef32d2cfdc5 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Sun, 15 Nov 2026 14:37:29 +0100 Subject: [PATCH] Sidebar chat menu: add Delete, remove colour and icon pickers Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 7 +++++ web/src/chatActions.ts | 46 +++++++++++++++++++++++++++++++++ web/src/components/ChatView.tsx | 30 +++------------------ web/src/components/Sidebar.tsx | 16 +++++++++--- web/src/whatsnew.ts | 4 +++ 5 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 web/src/chatActions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 104eca9..0ff8ad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/); until 1.0, minor versions may contain breaking changes. +## [Unreleased] + +### Changed + +- The chat menu in the sidebar has a **Delete** entry (with the question about the chat's own folder); + colour and icon selection moved out of it – they remain in the ⋯ menu inside the chat. + ## [v202611-00037] – 2026-11-15 ### Added diff --git a/web/src/chatActions.ts b/web/src/chatActions.ts new file mode 100644 index 0000000..4a6a13d --- /dev/null +++ b/web/src/chatActions.ts @@ -0,0 +1,46 @@ +import { del, get } from "./api"; +import { choiceDialog, confirmDialog, toast } from "./dialogs"; +import { t } from "./i18n"; +import { formatSize } from "./components/FileFigure"; + +interface FolderInfo { + own: boolean; + path?: string; + files?: number; + bytes?: number; + git?: boolean; + truncated?: boolean; +} + +/** + * Delete a chat after asking. A chat with a folder of its own (not shared with other chats, not a + * project) gets the question whether the folder goes with it. Resolves to true when it was deleted. + */ +export async function deleteChat(session: { id: string; parent_id?: string | null }): Promise { + const folder = session.parent_id ? undefined : await get(`/sessions/${session.id}/folder`).catch(() => undefined); + let withFolder = false; + if (folder?.own) { + const size = `${folder.truncated ? "> " : ""}${t("chat.folderFiles", { n: folder.files ?? 0 })}, ${formatSize(folder.bytes ?? 0)}`; + const choice = await choiceDialog({ + title: t("chat.deleteTitle"), + message: `${t("chat.deleteWithFolder", { path: folder.path ?? "", size })}${folder.git ? `\n\n${t("chat.folderHasGit")}` : ""}`, + choices: [ + { value: "chat", label: t("chat.deleteOnlyChat") }, + { value: "both", label: t("chat.deleteChatAndFolder"), danger: true }, + ], + }); + if (!choice) return false; + withFolder = choice === "both"; + } else if (!(await confirmDialog({ message: t("chat.deleteConfirm"), danger: true, confirmLabel: t("common.delete") }))) { + return false; + } + try { + const r = await del<{ folder_deleted?: boolean; folder_error?: string }>(`/sessions/${session.id}${withFolder ? "?delete_folder=true" : ""}`); + if (r?.folder_error) toast(r.folder_error); + else if (r?.folder_deleted) toast(t("chat.folderDeleted"), "success"); + return true; + } catch (e: any) { + toast(e.message); + return false; + } +} diff --git a/web/src/components/ChatView.tsx b/web/src/components/ChatView.tsx index 1f34d3d..481557e 100644 --- a/web/src/components/ChatView.tsx +++ b/web/src/components/ChatView.tsx @@ -1,6 +1,6 @@ import { takePrefill } from "../share"; import { Icon } from "./Icon"; -import { FileChip, FileFigure, fileType, imageSize, imageSrc, formatSize } from "./FileFigure"; +import { FileChip, FileFigure, fileType, imageSize, imageSrc } from "./FileFigure"; import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react"; import { del, get, patch, post, put } from "../api"; import { useFetch, useModels } from "../hooks"; @@ -24,7 +24,8 @@ import { UsageOverlay } from "./UsageOverlay"; import { RewindDialog } from "./RewindDialog"; import { ShellsChip } from "./ShellsPanel"; import { TodoList, ToolCard } from "./ToolCard"; -import { choiceDialog, confirmDialog, promptDialog, toast } from "../dialogs"; +import { promptDialog, toast } from "../dialogs"; +import { deleteChat } from "../chatActions"; import { ChatTabBar, ChatTabPanes, useChatTabs } from "./ChatTabs"; import { ColorSwatches, IconPicker } from "./ColorSwatches"; import { ProjectPermissions } from "./ProjectPermissions"; @@ -757,30 +758,7 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string }; const remove = async () => { - // Does the chat have a folder of its own? Then ask whether it goes too. - const folder = session.parent_id ? undefined : await get<{ own: boolean; path?: string; files?: number; bytes?: number; git?: boolean; truncated?: boolean }>(`/sessions/${sessionId}/folder`).catch(() => undefined); - let withFolder = false; - if (folder?.own) { - const size = `${folder.truncated ? "> " : ""}${t("chat.folderFiles", { n: folder.files ?? 0 })}, ${formatSize(folder.bytes ?? 0)}`; - const choice = await choiceDialog({ - title: t("chat.deleteTitle"), - message: `${t("chat.deleteWithFolder", { path: folder.path ?? "", size })}${folder.git ? `\n\n${t("chat.folderHasGit")}` : ""}`, - choices: [ - { value: "chat", label: t("chat.deleteOnlyChat") }, - { value: "both", label: t("chat.deleteChatAndFolder"), danger: true }, - ], - }); - if (!choice) return; - withFolder = choice === "both"; - } else if (!(await confirmDialog({ message: t("chat.deleteConfirm"), danger: true, confirmLabel: t("common.delete") }))) return; - try { - const r = await del<{ folder_deleted?: boolean; folder_error?: string }>(`/sessions/${sessionId}${withFolder ? "?delete_folder=true" : ""}`); - if (r?.folder_error) toast(r.folder_error); - else if (r?.folder_deleted) toast(t("chat.folderDeleted"), "success"); - } catch (e: any) { - return void toast(e.message); - } - navigate(session.parent_id ? `/s/${session.parent_id}` : "/new"); + if (await deleteChat(session)) navigate(session.parent_id ? `/s/${session.parent_id}` : "/new"); }; // Context usage of the last request relative to the model's window. diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index c4f9785..675a86b 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import { ColorSwatches, IconPicker } from "./ColorSwatches"; +import { deleteChat } from "../chatActions"; import { Icon } from "./Icon"; import { StatusLeds } from "./StatusLeds"; import { cycleThemeMode, getThemeMode, onThemeChange } from "../theme"; @@ -98,11 +98,21 @@ function SessionRow({ s, active, attention, busy, folders }: { s: Session; activ {t("sidebar.removeFromFolder")} )} - (setMenu(false), organise(s.id, { color }))} /> - (setMenu(false), organise(s.id, { icon }))} /> + {!s.shared && ( + + )} )} diff --git a/web/src/whatsnew.ts b/web/src/whatsnew.ts index d5be667..04b509a 100644 --- a/web/src/whatsnew.ts +++ b/web/src/whatsnew.ts @@ -3,6 +3,10 @@ * under "unreleased"; scripts/release.sh renames the key to the release version. */ export const WHATS_NEW: Record = { + unreleased: { + en: ["Sidebar chat menu: Delete added; colour and icon are now chosen in the chat's own menu"], + de: ["Chat-Menü in der Sidebar: Löschen hinzugefügt; Farbe und Icon wählst du jetzt im Menü des Chats"], + }, "202611-00037": { en: ["Deleting a chat now asks whether to delete its own folder too"], de: ["Beim Löschen eines Chats wirst du gefragt, ob sein eigener Ordner mitgelöscht werden soll"],