| author | |
| committer | |
| log | cb007469161ff0db2bc2dc8123c4b30e186aae50 |
| tree | dfc6a65a61b8e02470dbf7d9f88f8f662862dc59 |
| parent | 9d1879b142a2f5d01383357646a4679d1a2bd202 |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 105 insertions(+), 7 deletions(-)
apps/web/src/components/LegacySidebar.tsx+14| ... | ... | @@ -2165,11 +2165,21 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2165 | 2165 | { id: "mark-unread", label: "Mark unread" }, |
| 2166 | 2166 | { id: "copy-path", label: "Copy Path" }, |
| 2167 | 2167 | { id: "copy-thread-id", label: "Copy Thread ID" }, |
| 2168 | { id: "project-settings", label: "Project settings" }, | |
| 2168 | 2169 | { id: "delete", label: "Delete", destructive: true, icon: "trash" }, |
| 2169 | 2170 | ], |
| 2170 | 2171 | position, |
| 2171 | 2172 | ); |
| 2172 | 2173 | |
| 2174 | if (clicked === "project-settings") { | |
| 2175 | if (isMobile) setOpenMobile(false); | |
| 2176 | void router.navigate({ | |
| 2177 | to: "/projects/$projectKey", | |
| 2178 | params: { projectKey: project.projectKey }, | |
| 2179 | }); | |
| 2180 | return; | |
| 2181 | } | |
| 2182 | ||
| 2173 | 2183 | if (clicked === "new-thread-on-branch") { |
| 2174 | 2184 | // Explicit branch carry-over: reuse the thread's worktree when it |
| 2175 | 2185 | // has one, otherwise its branch on the local checkout. |
| ... | ... | @@ -2252,9 +2262,13 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2252 | 2262 | copyThreadIdToClipboard, |
| 2253 | 2263 | deleteThread, |
| 2254 | 2264 | handleNewThread, |
| 2265 | isMobile, | |
| 2255 | 2266 | markThreadUnread, |
| 2256 | 2267 | memberProjectByScopedKey, |
| 2268 | project.projectKey, | |
| 2257 | 2269 | project.workspaceRoot, |
| 2270 | router, | |
| 2271 | setOpenMobile, | |
| 2258 | 2272 | startThreadRename, |
| 2259 | 2273 | ], |
| 2260 | 2274 | ); |
apps/web/src/components/Sidebar.tsx+25-5| ... | ... | @@ -1888,6 +1888,8 @@ export default function Sidebar() { |
| 1888 | 1888 | () => sortLogicalProjectsForSidebar(unsortedProjectGroups, threads, sidebarProjectSortOrder), |
| 1889 | 1889 | [sidebarProjectSortOrder, threads, unsortedProjectGroups], |
| 1890 | 1890 | ); |
| 1891 | const projectGroupsRef = useRef(projectGroups); | |
| 1892 | projectGroupsRef.current = projectGroups; | |
| 1891 | 1893 | const serverConfigs = useAtomValue(environmentServerConfigsAtom); |
| 1892 | 1894 | // Threads on non-primary environments (T3 Connect, hosted) resolve their |
| 1893 | 1895 | // provider entry from their own environment's config: default instance ids |
| ... | ... | @@ -2043,11 +2045,8 @@ export default function Sidebar() { |
| 2043 | 2045 | clearSelection(); |
| 2044 | 2046 | }, [clearSelection, projectScopeKey]); |
| 2045 | 2047 | |
| 2046 | const handleProjectSettings = useCallback( | |
| 2047 | (event: ReactMouseEvent<HTMLButtonElement>, projectGroup: SidebarProjectSnapshot) => { | |
| 2048 | event.preventDefault(); | |
| 2049 | event.stopPropagation(); | |
| 2050 | dispatchProjectScopeMenu({ type: "project-settings-opened" }); | |
| 2048 | const openProjectSettings = useCallback( | |
| 2049 | (projectGroup: SidebarProjectSnapshot) => { | |
| 2051 | 2050 | if (isMobile) { |
| 2052 | 2051 | setOpenMobile(false); |
| 2053 | 2052 | } |
| ... | ... | @@ -2058,6 +2057,15 @@ export default function Sidebar() { |
| 2058 | 2057 | }, |
| 2059 | 2058 | [isMobile, router, setOpenMobile], |
| 2060 | 2059 | ); |
| 2060 | const handleProjectSettings = useCallback( | |
| 2061 | (event: ReactMouseEvent<HTMLButtonElement>, projectGroup: SidebarProjectSnapshot) => { | |
| 2062 | event.preventDefault(); | |
| 2063 | event.stopPropagation(); | |
| 2064 | dispatchProjectScopeMenu({ type: "project-settings-opened" }); | |
| 2065 | openProjectSettings(projectGroup); | |
| 2066 | }, | |
| 2067 | [openProjectSettings], | |
| 2068 | ); | |
| 2061 | 2069 | |
| 2062 | 2070 | // Settled threads stay in the live shell stream (settled ≠ archived), so |
| 2063 | 2071 | // the partition works directly off live shells: no archived-snapshot |
| ... | ... | @@ -3122,6 +3130,17 @@ export default function Sidebar() { |
| 3122 | 3130 | return; |
| 3123 | 3131 | } |
| 3124 | 3132 | switch (clicked.value) { |
| 3133 | case "project-settings": { | |
| 3134 | const projectGroup = projectGroupsRef.current.find((group) => | |
| 3135 | group.memberProjectRefs.some( | |
| 3136 | (projectRef) => | |
| 3137 | projectRef.environmentId === thread.environmentId && | |
| 3138 | projectRef.projectId === thread.projectId, | |
| 3139 | ), | |
| 3140 | ); | |
| 3141 | if (projectGroup) openProjectSettings(projectGroup); | |
| 3142 | return; | |
| 3143 | } | |
| 3125 | 3144 | case "new-thread-on-branch": { |
| 3126 | 3145 | // Explicit branch carry-over: reuse the thread's worktree when it |
| 3127 | 3146 | // has one, otherwise its branch on the local checkout. |
| ... | ... | @@ -3281,6 +3300,7 @@ export default function Sidebar() { |
| 3281 | 3300 | deleteThread, |
| 3282 | 3301 | handleMultiSelectContextMenu, |
| 3283 | 3302 | markThreadUnread, |
| 3303 | openProjectSettings, | |
| 3284 | 3304 | projectCwdByKey, |
| 3285 | 3305 | serverConfigs, |
| 3286 | 3306 | startThreadRename, |
apps/web/src/components/threadActionMenu.logic.test.ts+12-1| ... | ... | @@ -33,7 +33,18 @@ describe("buildThreadActionMenuItems", () => { |
| 33 | 33 | ...baseState, |
| 34 | 34 | supports: { settlement: false, snooze: false, pinning: false, titleRegeneration: false }, |
| 35 | 35 | }), |
| 36 | ).toEqual(["rename", "mark-unread", "copy", "archive", "delete"]); | |
| 36 | ).toEqual(["rename", "mark-unread", "copy", "project-settings", "archive", "delete"]); | |
| 37 | }); | |
| 38 | ||
| 39 | it("groups project settings with utility actions before archive", () => { | |
| 40 | const items = buildThreadActionMenuItems(baseState); | |
| 41 | const copyIndex = items.findIndex((item) => item.id === "copy"); | |
| 42 | expect(items[copyIndex + 1]).toMatchObject({ | |
| 43 | id: "project-settings", | |
| 44 | label: "Project settings", | |
| 45 | icon: "settings", | |
| 46 | }); | |
| 47 | expect(items[copyIndex + 2]?.id).toBe("archive"); | |
| 37 | 48 | }); |
| 38 | 49 | |
| 39 | 50 | it("includes branch items only for threads with a branch", () => { |
apps/web/src/components/threadActionMenu.logic.ts+2| ... | ... | @@ -8,6 +8,7 @@ import type { SnoozePreset } from "@t3tools/client-runtime/state/thread-settled" |
| 8 | 8 | */ |
| 9 | 9 | export type ThreadActionMenuId = |
| 10 | 10 | | "new-thread-on-branch" |
| 11 | | "project-settings" | |
| 11 | 12 | | "pin" |
| 12 | 13 | | "unpin" |
| 13 | 14 | | "settle" |
| ... | ... | @@ -119,6 +120,7 @@ export function buildThreadActionMenuItems( |
| 119 | 120 | { id: "copy-thread-id", label: "Thread ID", icon: "hash" }, |
| 120 | 121 | ], |
| 121 | 122 | }, |
| 123 | { id: "project-settings", label: "Project settings", icon: "settings" }, | |
| 122 | 124 | // Archive removes the thread from the sidebar while keeping its |
| 123 | 125 | // conversation under Settings > Archived threads — distinct from Settle |
| 124 | 126 | // (stays visible in the Settled shelf) and Delete (clears history for |
apps/web/src/contextMenuFallback.ts+9| ... | ... | @@ -96,6 +96,15 @@ const ICON_PATHS: Record<string, ReadonlyArray<{ tag: string; attrs: Record<stri |
| 96 | 96 | { tag: "path", attrs: { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" } }, |
| 97 | 97 | { tag: "path", attrs: { d: "M8 16H3v5" } }, |
| 98 | 98 | ], |
| 99 | settings: [ | |
| 100 | { | |
| 101 | tag: "path", | |
| 102 | attrs: { | |
| 103 | d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.51a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z", | |
| 104 | }, | |
| 105 | }, | |
| 106 | { tag: "circle", attrs: { cx: "12", cy: "12", r: "3" } }, | |
| 107 | ], | |
| 99 | 108 | "folder-tree": [ |
| 100 | 109 | { |
| 101 | 110 | tag: "path", |
apps/web/src/hooks/useThreadActionMenu.ts+43-1| ... | ... | @@ -7,7 +7,8 @@ import { |
| 7 | 7 | } from "@t3tools/client-runtime/state/runtime"; |
| 8 | 8 | import { canSnooze, effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled"; |
| 9 | 9 | import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts"; |
| 10 | import { useCallback } from "react"; | |
| 10 | import { useRouter } from "@tanstack/react-router"; | |
| 11 | import { useCallback, useMemo } from "react"; | |
| 11 | 12 | |
| 12 | 13 | import { resolveSnoozePresets, snoozeWakeDescription } from "../components/Sidebar.snooze"; |
| 13 | 14 | import { |
| ... | ... | @@ -23,8 +24,16 @@ import { |
| 23 | 24 | readEnvironmentSupportsSnooze, |
| 24 | 25 | readEnvironmentSupportsTitleRegeneration, |
| 25 | 26 | readThreadShell, |
| 27 | useProjects, | |
| 26 | 28 | } from "../state/entities"; |
| 29 | import { usePrimaryEnvironmentId } from "../state/environments"; | |
| 27 | 30 | import { readLocalApi } from "../localApi"; |
| 31 | import { | |
| 32 | deriveLogicalProjectKeyFromSettings, | |
| 33 | derivePhysicalProjectKey, | |
| 34 | selectProjectGroupingSettings, | |
| 35 | } from "../logicalProject"; | |
| 36 | import { buildPhysicalToLogicalProjectKeyMap } from "../sidebarProjectGrouping"; | |
| 28 | 37 | import { useUiStateStore } from "../uiStateStore"; |
| 29 | 38 | import { useCopyToClipboard } from "./useCopyToClipboard"; |
| 30 | 39 | import { useNewThreadHandler } from "./useHandleNewThread"; |
| ... | ... | @@ -58,6 +67,19 @@ export function useThreadActionMenu(input: { |
| 58 | 67 | readonly onStartRename: () => void; |
| 59 | 68 | }) { |
| 60 | 69 | const { threadRef, projectCwd, onStartRename } = input; |
| 70 | const router = useRouter(); | |
| 71 | const projects = useProjects(); | |
| 72 | const primaryEnvironmentId = usePrimaryEnvironmentId(); | |
| 73 | const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); | |
| 74 | const logicalProjectKeyByPhysicalKey = useMemo( | |
| 75 | () => | |
| 76 | buildPhysicalToLogicalProjectKeyMap({ | |
| 77 | projects, | |
| 78 | settings: projectGroupingSettings, | |
| 79 | primaryEnvironmentId, | |
| 80 | }), | |
| 81 | [primaryEnvironmentId, projectGroupingSettings, projects], | |
| 82 | ); | |
| 61 | 83 | const { |
| 62 | 84 | settleThread, |
| 63 | 85 | unsettleThread, |
| ... | ... | @@ -168,6 +190,22 @@ export function useThreadActionMenu(input: { |
| 168 | 190 | } |
| 169 | 191 | }; |
| 170 | 192 | switch (action) { |
| 193 | case "project-settings": { | |
| 194 | const project = projects.find( | |
| 195 | (candidate) => | |
| 196 | candidate.environmentId === thread.environmentId && | |
| 197 | candidate.id === thread.projectId, | |
| 198 | ); | |
| 199 | if (!project) return; | |
| 200 | const projectKey = | |
| 201 | logicalProjectKeyByPhysicalKey.get(derivePhysicalProjectKey(project)) ?? | |
| 202 | deriveLogicalProjectKeyFromSettings(project, projectGroupingSettings); | |
| 203 | void router.navigate({ | |
| 204 | to: "/projects/$projectKey", | |
| 205 | params: { projectKey }, | |
| 206 | }); | |
| 207 | return; | |
| 208 | } | |
| 171 | 209 | case "new-thread-on-branch": { |
| 172 | 210 | // Explicit branch carry-over: reuse the thread's worktree when it |
| 173 | 211 | // has one, otherwise its branch on the local checkout. |
| ... | ... | @@ -300,10 +338,14 @@ export function useThreadActionMenu(input: { |
| 300 | 338 | copyThreadIdToClipboard, |
| 301 | 339 | deleteThread, |
| 302 | 340 | handleNewThread, |
| 341 | logicalProjectKeyByPhysicalKey, | |
| 303 | 342 | markThreadUnread, |
| 304 | 343 | onStartRename, |
| 305 | 344 | pinThread, |
| 306 | 345 | projectCwd, |
| 346 | projectGroupingSettings, | |
| 347 | projects, | |
| 348 | router, | |
| 307 | 349 | settleThread, |
| 308 | 350 | snoozeThread, |
| 309 | 351 | threadRef, |