Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2628,6 +2628,11 @@ export default function ChatView(props: ChatViewProps) {
const threadFindEnabled =
isServerThread && serverConfig?.threadFind === true && !paintOnlyDisplayedTimeline;
const isThreadFindActive = useThreadFindOpen(activeThreadRef, threadFindEnabled);
const openThreadFindStable = useStableCallback((threadRef: ScopedThreadRef) => {
if (threadFindEnabled && scopedThreadKey(threadRef) === activeThreadKey) {
openThreadFind(activeThreadKey);
}
});

useChatShortcuts({
enabled: activeThreadId !== null && !backgroundThread,
Expand Down Expand Up @@ -3168,6 +3173,7 @@ export default function ChatView(props: ChatViewProps) {
{...(activeDraftLogicalProjectKey
? { onOpenProjectSettings: handleOpenDraftProjectSettingsStable }
: {})}
{...(threadFindEnabled ? { onFindInThread: openThreadFindStable } : {})}
/>
</header>

Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AuthOrchestrationOperateScope,
type EnvironmentId,
type ScopedThreadRef,
type ThreadId,
} from "@t3tools/contracts";
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
Expand Down Expand Up @@ -52,6 +53,7 @@ interface ChatHeaderProps {
rightPanelOpen: boolean;
onNewThreadInProject: () => void;
onOpenProjectSettings?: (() => void) | undefined;
onFindInThread?: ((threadRef: ScopedThreadRef) => void) | undefined;
}

function resolveRenameCommit(input: {
Expand All @@ -77,6 +79,7 @@ export const ChatHeader = memo(function ChatHeader({
rightPanelOpen,
onNewThreadInProject,
onOpenProjectSettings,
onFindInThread,
}: ChatHeaderProps) {
const activeProjectName = activeProject?.isScratch ? "No project" : activeProject?.title;
const activeProjectCwd = activeProject?.workspaceRoot ?? null;
Expand Down Expand Up @@ -192,6 +195,7 @@ export const ChatHeader = memo(function ChatHeader({
threadRef: isServerThread ? activeThreadRef : null,
projectCwd: activeProjectCwd,
onStartRename: startRename,
onFind: onFindInThread,
canShareWithGuest,
});
const titleButtonRef = useRef<HTMLButtonElement | null>(null);
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/threadActionMenu.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ContextMenuItem } from "@t3tools/contracts";
import type { SnoozePreset } from "@t3tools/client-runtime/state/thread-settled";

export type ThreadActionMenuId =
| "find"
| "new-thread-on-branch"
| "filter-by-project"
| "project-settings"
Expand Down Expand Up @@ -66,6 +67,7 @@ export function buildDraftActionMenuItems(options: {

export interface ThreadActionMenuState {
readonly canOperate: boolean;
readonly canFind?: boolean;
readonly branch: string | null;
readonly projectFilter: {
readonly label: string;
Expand Down Expand Up @@ -94,6 +96,7 @@ export interface ThreadActionMenuState {
/** Local navigation, read markers, and copying remain available to read-only clients. */
export function threadActionRequiresOperate(action: ThreadActionMenuId): boolean {
return ![
"find",
"new-thread-on-branch",
"project-settings",
"mark-unread",
Expand All @@ -109,6 +112,9 @@ export function buildThreadActionMenuItems(
state: ThreadActionMenuState,
): ReadonlyArray<ContextMenuItem<ThreadActionMenuId>> {
const items: ReadonlyArray<ContextMenuItem<ThreadActionMenuId>> = [
...(state.canFind
? [{ id: "find" as const, label: "Find in thread", icon: "text-search" }]
: []),
...(state.branch
? [
{
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/contextMenuFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ const ICON_PATHS: Record<string, ReadonlyArray<{ tag: string; attrs: Record<stri
{ tag: "path", attrs: { d: "M3 5a2 2 0 0 0 2 2h3" } },
{ tag: "path", attrs: { d: "M3 3v13a2 2 0 0 0 2 2h3" } },
],
"text-search": [
{ tag: "path", attrs: { d: "M21 5H3" } },
{ tag: "path", attrs: { d: "M10 12H3" } },
{ tag: "path", attrs: { d: "M10 19H3" } },
{ tag: "circle", attrs: { cx: "17", cy: "15", r: "3" } },
{ tag: "path", attrs: { d: "m21 19-1.9-1.9" } },
],
trash: [
{ tag: "path", attrs: { d: "M3 6h18" } },
{ tag: "path", attrs: { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" } },
Expand Down
17 changes: 16 additions & 1 deletion apps/web/src/hooks/useThreadActionMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,12 @@ const target = {
threadId: ThreadId.make("thread"),
};
const position = { x: 10, y: 20 };
const createMenu = () =>
const createMenu = (options: { readonly onFind?: (threadRef: typeof target) => void } = {}) =>
useThreadActionMenu({
threadRef: target,
projectCwd: "/project",
onStartRename: () => recordEffect("rename"),
onFind: options.onFind,
canShareWithGuest: false,
});

Expand All @@ -180,4 +181,18 @@ describe("thread menu permissions", () => {
expect(state.effects).toEqual([]);
},
);

it("offers find only to a header that can open it, without task permission", async () => {
createMenu().openMenu(position);
expect(state.show.mock.calls[0]![0].some((item) => item.id === "find")).toBe(false);
state.show.mockResolvedValue("find");
const onFind = vi.fn(() => recordEffect("find"));
createMenu({ onFind }).openMenu(position);
await state.completed.promise;
const items = state.show.mock.calls[1]![0];
expect(items[0]).toMatchObject({ id: "find", label: "Find in thread" });
expect(items[0]?.disabled).not.toBe(true);
expect(state.effects).toEqual(["find"]);
expect(onFind).toHaveBeenCalledExactlyOnceWith(target);
});
});
7 changes: 6 additions & 1 deletion apps/web/src/hooks/useThreadActionMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ export function useThreadActionMenu(input: {
readonly threadRef: ScopedThreadRef | null;
readonly projectCwd: string | null;
readonly onStartRename: () => void;
readonly onFind?: ((threadRef: ScopedThreadRef) => void) | undefined;
readonly canShareWithGuest: boolean;
}) {
const { threadRef, projectCwd, onStartRename, canShareWithGuest } = input;
const { threadRef, projectCwd, onStartRename, onFind, canShareWithGuest } = input;
const router = useRouter();
const projects = useProjects();
const primaryEnvironmentId = usePrimaryEnvironmentId();
Expand Down Expand Up @@ -132,6 +133,7 @@ export function useThreadActionMenu(input: {
const snoozePresets = resolveSnoozePresets(now, timestampFormat);
const items = buildThreadActionMenuItems({
canOperate: readEnvironmentScope(threadRef.environmentId, AuthOrchestrationOperateScope),
canFind: onFind !== undefined,
branch: thread.branch ?? null,
projectFilter: null,
isPinned: thread.pinnedAt != null,
Expand Down Expand Up @@ -180,6 +182,9 @@ export function useThreadActionMenu(input: {
}
};
switch (action) {
case "find":
onFind?.(threadRef);
return;
case "project-settings": {
const project = projects.find(
(candidate) =>
Expand Down
Loading