@nextclaw/ui 0.15.23 → 0.15.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/assets/{api-Glw2OGXK.js → api-FK7tikI1.js} +4 -4
- package/dist/assets/appearance-settings-page-BYbvl2wj.js +1 -0
- package/dist/assets/{channels-list-page-C7E4Sxwd.js → channels-list-page-CocdcIko.js} +2 -2
- package/dist/assets/{chat-page-BT6dNI-4.js → chat-page-C5E-LLVd.js} +1 -1
- package/dist/assets/config-split-page-CA_a0AJ5.js +1 -0
- package/dist/assets/confirm-dialog-CxZfenjK.js +5 -0
- package/dist/assets/desktop-update-config-BEKC8Ly9.js +1 -0
- package/dist/assets/{dist-dv_SUR54.js → dist-BH7W5yCi.js} +1 -1
- package/dist/assets/{dist-CtLyEJeO.js → dist-CaYdVVeO.js} +1 -1
- package/dist/assets/doc-browser-Cl2ldE3f.js +1 -0
- package/dist/assets/doc-browser-DXCSd63E.js +1 -0
- package/dist/assets/{doc-browser-context-BIBjkwfV.js → doc-browser-context-FDPs3gJf.js} +1 -1
- package/dist/assets/form-actions-0c0aw_bl.js +1 -0
- package/dist/assets/i18n-provider-DkITJNz8.js +1 -0
- package/dist/assets/index-CQ2LcI2p.css +1 -0
- package/dist/assets/index-CrdfvBVg.js +107 -0
- package/dist/assets/loader-circle-DHtUxCIA.js +1 -0
- package/dist/assets/mcp-marketplace-page-Bc_jI5iH.js +9 -0
- package/dist/assets/mcp-marketplace-page-CzCieD9r.js +1 -0
- package/dist/assets/model-config-page-C4jw-pcj.js +1 -0
- package/dist/assets/{navigation-link-CB0OQbnY.js → navigation-link-CVy0_kVl.js} +2 -2
- package/dist/assets/plus-CG1RwZQq.js +1 -0
- package/dist/assets/provider-scoped-model-input-CmH4BFvc.js +1 -0
- package/dist/assets/providers-config-page-CJQjwzaw.js +1 -0
- package/dist/assets/react-DjFBVA0n.js +8 -0
- package/dist/assets/{refresh-cw-Ba0LZou4.js → refresh-cw-j2bl79Ea.js} +1 -1
- package/dist/assets/remote-BIlpD3yL.js +1 -0
- package/dist/assets/rotate-cw-bKG8zH8M.js +1 -0
- package/dist/assets/runtime-config-page-B_x2Glz5.js +1 -0
- package/dist/assets/{save-DTOAvqzt.js → save-BKNrtK81.js} +1 -1
- package/dist/assets/{search-D4X5DNMe.js → search-CkOm65W_.js} +1 -1
- package/dist/assets/{search-config-page-Cr6254K5.js → search-config-page-BaIijtmO.js} +1 -1
- package/dist/assets/{secrets-config-page-W2Hx332d.js → secrets-config-page-CJjtdFAl.js} +2 -2
- package/dist/assets/security-config-DB8apj09.js +1 -0
- package/dist/assets/{select-CDv0g2Pj.js → select-Ea7ffKCE.js} +2 -2
- package/dist/assets/{settings-2-CoxzNABp.js → settings-2-CtyN3m_V.js} +1 -1
- package/dist/assets/skeleton-Bt6zxcAB.js +1 -0
- package/dist/assets/{tag-chip-CjkZEoS2.js → tag-chip-C9l4xsKp.js} +1 -1
- package/dist/assets/x-CowtCu9Z.js +1 -0
- package/dist/index.html +20 -20
- package/package.json +8 -8
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +30 -1
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +1 -1
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-provider-model-catalog.test.tsx +54 -2
- package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +10 -1
- package/src/features/chat/features/conversation/hooks/use-session-provider-model-catalog.ts +76 -1
- package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +6 -1
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +29 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +12 -3
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
- package/src/features/chat/features/input/utils/chat-conversation-excerpt-token.utils.ts +59 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +42 -16
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +50 -14
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +40 -0
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +5 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +40 -3
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.selection.test.tsx +66 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +13 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +573 -90
- package/src/features/chat/features/workspace/components/__tests__/workspace-text-selection-menu.test.tsx +11 -4
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +389 -288
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +54 -35
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +79 -148
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +149 -94
- package/src/features/chat/features/workspace/components/project-files/__tests__/workspace-directory-entry-menu.test.tsx +50 -0
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-entry-menu.tsx +209 -0
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +411 -0
- package/src/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item.tsx +112 -0
- package/src/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar.tsx +64 -0
- package/src/features/chat/features/workspace/components/workspace-text-selection-menu.tsx +17 -195
- package/src/features/chat/features/workspace/hooks/use-workspace-explorer-layout.ts +88 -0
- package/src/features/chat/features/workspace/hooks/use-workspace-file-actions.ts +335 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-file-path-mutation.utils.ts +58 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +23 -22
- package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -4
- package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +94 -45
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +103 -99
- package/src/features/chat/managers/chat-composer-intent.manager.ts +83 -34
- package/src/features/chat/managers/chat-thread.manager.ts +132 -128
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +20 -40
- package/src/features/chat/stores/chat-thread.store.ts +47 -74
- package/src/features/chat/types/chat-input-bar.types.ts +3 -0
- package/src/features/side-dock/components/__tests__/side-dock.test.tsx +1 -0
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +0 -9
- package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +35 -38
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +25 -19
- package/src/shared/components/ui/context-menu/__tests__/context-menu.test.tsx +60 -34
- package/src/shared/components/ui/context-menu/context-menu.tsx +95 -68
- package/src/shared/lib/api/managers/client.manager.ts +31 -17
- package/src/shared/lib/api/server-path/server-path.types.ts +66 -0
- package/src/shared/lib/api/types.ts +145 -116
- package/src/shared/lib/api/utils/server-path.utils.ts +35 -11
- package/src/shared/lib/i18n/locales/en-US/chat.json +49 -1
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +49 -1
- package/dist/assets/appearance-settings-page-Bl_W_9m7.js +0 -1
- package/dist/assets/config-split-page-D5BgtKrm.js +0 -1
- package/dist/assets/confirm-dialog-Bf907Azu.js +0 -5
- package/dist/assets/desktop-update-config-BwuyBPJb.js +0 -1
- package/dist/assets/doc-browser-C23NDGh5.js +0 -1
- package/dist/assets/doc-browser-DlOOhBxI.js +0 -1
- package/dist/assets/form-actions-DAzi68Cn.js +0 -1
- package/dist/assets/index-BLNg2yg0.css +0 -1
- package/dist/assets/index-D3EKjEyK.js +0 -107
- package/dist/assets/loader-circle-BSg5u0r4.js +0 -1
- package/dist/assets/mcp-marketplace-page-ByhvQQj5.js +0 -9
- package/dist/assets/mcp-marketplace-page-CytXwnUs.js +0 -1
- package/dist/assets/model-config-page-bgEx3MjQ.js +0 -1
- package/dist/assets/plus-CNjWdLhZ.js +0 -1
- package/dist/assets/provider-scoped-model-input-8F6JMd2n.js +0 -1
- package/dist/assets/providers-config-page-CxpTjR81.js +0 -1
- package/dist/assets/react-Bk2ydNTe.js +0 -8
- package/dist/assets/remote-Dv38PuMV.js +0 -1
- package/dist/assets/rotate-cw-IkYVk9JX.js +0 -1
- package/dist/assets/runtime-config-page-C0ByyUVr.js +0 -1
- package/dist/assets/security-config-1E8OF4kR.js +0 -1
- package/dist/assets/skeleton-DW018fJv.js +0 -1
- package/dist/assets/use-confirm-dialog-CavtY1vJ.js +0 -1
- package/dist/assets/x-PTXYTEwz.js +0 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, type FormEvent } from 'react';
|
|
2
|
+
import { Check, File, Folder, X } from 'lucide-react';
|
|
3
|
+
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
4
|
+
import { Input } from '@/shared/components/ui/input';
|
|
5
|
+
import { t } from '@/shared/lib/i18n';
|
|
6
|
+
|
|
7
|
+
function isValidFolderName(value: string): boolean {
|
|
8
|
+
const name = value.trim();
|
|
9
|
+
return Boolean(name && name !== '.' && name !== '..' && !name.includes('/') && !name.includes('\\'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function WorkspaceNewFolderTreeItem({
|
|
13
|
+
error,
|
|
14
|
+
kind,
|
|
15
|
+
isCreating,
|
|
16
|
+
level,
|
|
17
|
+
name,
|
|
18
|
+
onCancel,
|
|
19
|
+
onNameChange,
|
|
20
|
+
onSubmit,
|
|
21
|
+
}: {
|
|
22
|
+
error: string | null;
|
|
23
|
+
kind: 'directory' | 'file';
|
|
24
|
+
isCreating: boolean;
|
|
25
|
+
level: number;
|
|
26
|
+
name: string;
|
|
27
|
+
onCancel: () => void;
|
|
28
|
+
onNameChange: (name: string) => void;
|
|
29
|
+
onSubmit: () => void;
|
|
30
|
+
}) {
|
|
31
|
+
const rowRef = useRef<HTMLFormElement | null>(null);
|
|
32
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
33
|
+
const valid = isValidFolderName(name);
|
|
34
|
+
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
const row = rowRef.current;
|
|
37
|
+
if (typeof row?.scrollIntoView === 'function') {
|
|
38
|
+
row.scrollIntoView({ block: 'nearest' });
|
|
39
|
+
}
|
|
40
|
+
inputRef.current?.focus();
|
|
41
|
+
inputRef.current?.select();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const submit = (event: FormEvent) => {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
if (valid && !isCreating) {
|
|
47
|
+
onSubmit();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<form
|
|
53
|
+
ref={rowRef}
|
|
54
|
+
role="treeitem"
|
|
55
|
+
aria-label={t(kind === 'file' ? 'chatWorkspaceNewFile' : 'chatWorkspaceNewFolder')}
|
|
56
|
+
className="bg-primary/5"
|
|
57
|
+
onSubmit={submit}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
className="flex min-h-8 min-w-0 items-center gap-1 py-0.5 pr-1"
|
|
61
|
+
style={{ paddingLeft: `${8 + level * 14}px` }}
|
|
62
|
+
>
|
|
63
|
+
<span className="inline-flex h-4 w-4 shrink-0" />
|
|
64
|
+
{kind === 'file' ? (
|
|
65
|
+
<File className="h-4 w-4 shrink-0 text-gray-500" />
|
|
66
|
+
) : (
|
|
67
|
+
<Folder className="h-4 w-4 shrink-0 text-amber-500" />
|
|
68
|
+
)}
|
|
69
|
+
<Input
|
|
70
|
+
ref={inputRef}
|
|
71
|
+
aria-label={t(kind === 'file' ? 'chatWorkspaceFileName' : 'chatWorkspaceFolderName')}
|
|
72
|
+
className="h-7 min-w-0 flex-1 rounded-md px-2 text-xs"
|
|
73
|
+
disabled={isCreating}
|
|
74
|
+
placeholder={t(kind === 'file' ? 'chatWorkspaceFileNamePlaceholder' : 'chatWorkspaceFolderNamePlaceholder')}
|
|
75
|
+
value={name}
|
|
76
|
+
onChange={(event) => onNameChange(event.target.value)}
|
|
77
|
+
onKeyDown={(event) => {
|
|
78
|
+
if (event.key === 'Escape') {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
onCancel();
|
|
81
|
+
}
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
<IconActionButton
|
|
85
|
+
disabled={!valid || isCreating}
|
|
86
|
+
icon={<Check className="h-3.5 w-3.5" />}
|
|
87
|
+
label={t('chatWorkspaceCreateFolder')}
|
|
88
|
+
size="sm"
|
|
89
|
+
tooltipSide="top"
|
|
90
|
+
onClick={onSubmit}
|
|
91
|
+
/>
|
|
92
|
+
<IconActionButton
|
|
93
|
+
disabled={isCreating}
|
|
94
|
+
icon={<X className="h-3.5 w-3.5" />}
|
|
95
|
+
label={t('cancel')}
|
|
96
|
+
size="sm"
|
|
97
|
+
tooltipSide="top"
|
|
98
|
+
onClick={onCancel}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
{error ? (
|
|
102
|
+
<p
|
|
103
|
+
role="alert"
|
|
104
|
+
className="pb-1 pr-2 text-[11px] text-destructive"
|
|
105
|
+
style={{ paddingLeft: `${42 + level * 14}px` }}
|
|
106
|
+
>
|
|
107
|
+
{error}
|
|
108
|
+
</p>
|
|
109
|
+
) : null}
|
|
110
|
+
</form>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ChevronsUp, FilePlus2, FolderPlus, MoreHorizontal, RefreshCw } from 'lucide-react';
|
|
2
|
+
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
3
|
+
import { ContextMenuTrigger } from '@/shared/components/ui/context-menu/context-menu';
|
|
4
|
+
import { t } from '@/shared/lib/i18n';
|
|
5
|
+
|
|
6
|
+
export function WorkspaceProjectFilesToolbar({
|
|
7
|
+
disabled,
|
|
8
|
+
rootLabel,
|
|
9
|
+
onCollapseAll,
|
|
10
|
+
onNewFile,
|
|
11
|
+
onNewFolder,
|
|
12
|
+
onRefresh,
|
|
13
|
+
}: {
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
rootLabel: string;
|
|
16
|
+
onCollapseAll: () => void;
|
|
17
|
+
onNewFile: () => void;
|
|
18
|
+
onNewFolder: () => void;
|
|
19
|
+
onRefresh: () => void;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex h-9 shrink-0 items-center gap-0.5 border-b border-gray-200/80 bg-gray-50/55 px-1.5">
|
|
23
|
+
<span
|
|
24
|
+
className="min-w-0 flex-1 truncate px-0.5 text-[11px] font-semibold uppercase tracking-[0.03em] text-gray-800"
|
|
25
|
+
title={rootLabel}
|
|
26
|
+
>
|
|
27
|
+
{rootLabel}
|
|
28
|
+
</span>
|
|
29
|
+
<IconActionButton
|
|
30
|
+
icon={<FilePlus2 className="h-4 w-4" />}
|
|
31
|
+
label={t('chatWorkspaceNewFile')}
|
|
32
|
+
size="sm"
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
onClick={onNewFile}
|
|
35
|
+
/>
|
|
36
|
+
<IconActionButton
|
|
37
|
+
icon={<FolderPlus className="h-4 w-4" />}
|
|
38
|
+
label={t('chatWorkspaceNewFolder')}
|
|
39
|
+
size="sm"
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
onClick={onNewFolder}
|
|
42
|
+
/>
|
|
43
|
+
<IconActionButton
|
|
44
|
+
icon={<RefreshCw className="h-4 w-4" />}
|
|
45
|
+
label={t('chatWorkspaceRefreshExplorer')}
|
|
46
|
+
size="sm"
|
|
47
|
+
onClick={onRefresh}
|
|
48
|
+
/>
|
|
49
|
+
<IconActionButton
|
|
50
|
+
icon={<ChevronsUp className="h-4 w-4" />}
|
|
51
|
+
label={t('chatWorkspaceCollapseAll')}
|
|
52
|
+
size="sm"
|
|
53
|
+
onClick={onCollapseAll}
|
|
54
|
+
/>
|
|
55
|
+
<ContextMenuTrigger>
|
|
56
|
+
<IconActionButton
|
|
57
|
+
icon={<MoreHorizontal className="h-4 w-4" />}
|
|
58
|
+
label={t('chatWorkspaceRootMoreActions')}
|
|
59
|
+
size="sm"
|
|
60
|
+
/>
|
|
61
|
+
</ContextMenuTrigger>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
useEffect,
|
|
4
|
-
useRef,
|
|
5
|
-
useState,
|
|
6
|
-
type ReactNode,
|
|
7
|
-
} from "react";
|
|
8
|
-
import { createPortal } from "react-dom";
|
|
9
|
-
import { MessageSquarePlus } from "lucide-react";
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { ChatTextSelectionAction } from "@nextclaw/agent-chat-ui";
|
|
10
3
|
import {
|
|
11
4
|
buildWorkspaceTextExcerpt,
|
|
12
5
|
WORKSPACE_TEXT_EXCERPT_MAX_CHARACTERS,
|
|
@@ -14,44 +7,6 @@ import {
|
|
|
14
7
|
} from "@/features/chat/features/workspace/utils/workspace-text-excerpt.utils";
|
|
15
8
|
import { t } from "@/shared/lib/i18n";
|
|
16
9
|
|
|
17
|
-
type SelectionMenuState = {
|
|
18
|
-
anchorBottom: number;
|
|
19
|
-
anchorCenter: number;
|
|
20
|
-
anchorTop: number;
|
|
21
|
-
excerpt: WorkspaceTextExcerpt;
|
|
22
|
-
tooLong: boolean;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type SelectionMenuPosition = {
|
|
26
|
-
left: number;
|
|
27
|
-
top: number;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const SELECTION_MENU_VIEWPORT_GAP = 12;
|
|
31
|
-
const SELECTION_MENU_ANCHOR_GAP = 10;
|
|
32
|
-
|
|
33
|
-
function containsSelection(
|
|
34
|
-
root: HTMLElement,
|
|
35
|
-
selection: Selection,
|
|
36
|
-
): selection is Selection & { rangeCount: number } {
|
|
37
|
-
if (selection.rangeCount === 0 || selection.isCollapsed) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
const range = selection.getRangeAt(0);
|
|
41
|
-
return root.contains(range.commonAncestorContainer);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getSelectionAnchorRect(range: Range): DOMRect {
|
|
45
|
-
const rects = typeof range.getClientRects === "function"
|
|
46
|
-
? Array.from(range.getClientRects()).filter((rect) => rect.width > 0 || rect.height > 0)
|
|
47
|
-
: [];
|
|
48
|
-
return rects.at(-1) ?? range.getBoundingClientRect();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function clamp(value: number, minimum: number, maximum: number): number {
|
|
52
|
-
return Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
10
|
export function WorkspaceTextSelectionMenu({
|
|
56
11
|
children,
|
|
57
12
|
fileLabel,
|
|
@@ -67,157 +22,24 @@ export function WorkspaceTextSelectionMenu({
|
|
|
67
22
|
sourceStartLine?: number | null;
|
|
68
23
|
sourceText: string;
|
|
69
24
|
}) {
|
|
70
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
71
|
-
const menuButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
72
|
-
const selectionReadFrameRef = useRef<number | null>(null);
|
|
73
|
-
const [menu, setMenu] = useState<SelectionMenuState | null>(null);
|
|
74
|
-
const [menuPosition, setMenuPosition] = useState<SelectionMenuPosition | null>(null);
|
|
75
|
-
|
|
76
|
-
const readSelection = useCallback(() => {
|
|
77
|
-
const root = rootRef.current;
|
|
78
|
-
const selection = window.getSelection();
|
|
79
|
-
if (!root || !selection || !containsSelection(root, selection)) {
|
|
80
|
-
setMenu(null);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const excerpt = buildWorkspaceTextExcerpt({
|
|
84
|
-
path: filePath,
|
|
85
|
-
label: fileLabel,
|
|
86
|
-
selectedText: selection.toString(),
|
|
87
|
-
sourceText,
|
|
88
|
-
sourceStartLine,
|
|
89
|
-
});
|
|
90
|
-
if (!excerpt) {
|
|
91
|
-
setMenu(null);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const rect = getSelectionAnchorRect(selection.getRangeAt(0));
|
|
95
|
-
setMenuPosition(null);
|
|
96
|
-
setMenu({
|
|
97
|
-
anchorBottom: rect.bottom,
|
|
98
|
-
anchorCenter: rect.left + rect.width / 2,
|
|
99
|
-
anchorTop: rect.top,
|
|
100
|
-
excerpt,
|
|
101
|
-
tooLong: excerpt.excerpt.length > WORKSPACE_TEXT_EXCERPT_MAX_CHARACTERS,
|
|
102
|
-
});
|
|
103
|
-
}, [fileLabel, filePath, sourceStartLine, sourceText]);
|
|
104
|
-
|
|
105
|
-
const scheduleSelectionRead = useCallback(() => {
|
|
106
|
-
if (selectionReadFrameRef.current !== null) {
|
|
107
|
-
window.cancelAnimationFrame(selectionReadFrameRef.current);
|
|
108
|
-
}
|
|
109
|
-
selectionReadFrameRef.current = window.requestAnimationFrame(() => {
|
|
110
|
-
selectionReadFrameRef.current = null;
|
|
111
|
-
readSelection();
|
|
112
|
-
});
|
|
113
|
-
}, [readSelection]);
|
|
114
|
-
|
|
115
|
-
const captureMenuButton = useCallback((button: HTMLButtonElement | null) => {
|
|
116
|
-
menuButtonRef.current = button;
|
|
117
|
-
if (!menu || !button) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const buttonRect = button.getBoundingClientRect();
|
|
121
|
-
const left = clamp(
|
|
122
|
-
menu.anchorCenter - buttonRect.width / 2,
|
|
123
|
-
SELECTION_MENU_VIEWPORT_GAP,
|
|
124
|
-
window.innerWidth - SELECTION_MENU_VIEWPORT_GAP - buttonRect.width,
|
|
125
|
-
);
|
|
126
|
-
const belowTop = menu.anchorBottom + SELECTION_MENU_ANCHOR_GAP;
|
|
127
|
-
const aboveTop = menu.anchorTop - SELECTION_MENU_ANCHOR_GAP - buttonRect.height;
|
|
128
|
-
const fitsBelow = belowTop + buttonRect.height <= window.innerHeight - SELECTION_MENU_VIEWPORT_GAP;
|
|
129
|
-
const preferredTop = fitsBelow ? belowTop : aboveTop;
|
|
130
|
-
const top = clamp(
|
|
131
|
-
preferredTop,
|
|
132
|
-
SELECTION_MENU_VIEWPORT_GAP,
|
|
133
|
-
window.innerHeight - SELECTION_MENU_VIEWPORT_GAP - buttonRect.height,
|
|
134
|
-
);
|
|
135
|
-
setMenuPosition({ left, top });
|
|
136
|
-
}, [menu]);
|
|
137
|
-
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
const handleSelectionChange = () => {
|
|
140
|
-
const selection = window.getSelection();
|
|
141
|
-
if (!selection || selection.isCollapsed) {
|
|
142
|
-
setMenu(null);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
scheduleSelectionRead();
|
|
146
|
-
};
|
|
147
|
-
const handlePointerUp = (event: PointerEvent) => {
|
|
148
|
-
if (menuButtonRef.current?.contains(event.target as Node)) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
scheduleSelectionRead();
|
|
152
|
-
};
|
|
153
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
154
|
-
if (event.key === "Escape") {
|
|
155
|
-
setMenu(null);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
const closeMenu = () => setMenu(null);
|
|
159
|
-
document.addEventListener("selectionchange", handleSelectionChange);
|
|
160
|
-
document.addEventListener("pointerup", handlePointerUp, true);
|
|
161
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
162
|
-
document.addEventListener("scroll", closeMenu, true);
|
|
163
|
-
window.addEventListener("resize", closeMenu);
|
|
164
|
-
return () => {
|
|
165
|
-
document.removeEventListener("selectionchange", handleSelectionChange);
|
|
166
|
-
document.removeEventListener("pointerup", handlePointerUp, true);
|
|
167
|
-
document.removeEventListener("keydown", handleKeyDown);
|
|
168
|
-
document.removeEventListener("scroll", closeMenu, true);
|
|
169
|
-
window.removeEventListener("resize", closeMenu);
|
|
170
|
-
if (selectionReadFrameRef.current !== null) {
|
|
171
|
-
window.cancelAnimationFrame(selectionReadFrameRef.current);
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}, [scheduleSelectionRead]);
|
|
175
|
-
|
|
176
|
-
const addSelectionToChat = () => {
|
|
177
|
-
if (!menu || menu.tooLong) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
onAddToChat?.(menu.excerpt);
|
|
181
|
-
window.getSelection()?.removeAllRanges();
|
|
182
|
-
setMenu(null);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const menuNode = menu && onAddToChat
|
|
186
|
-
? createPortal(
|
|
187
|
-
<button
|
|
188
|
-
ref={captureMenuButton}
|
|
189
|
-
type="button"
|
|
190
|
-
className="fixed z-[var(--z-popover,10100)] inline-flex max-w-[calc(100vw-24px)] items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-xl border border-border bg-popover px-3 py-2 text-xs font-medium text-popover-foreground shadow-lg transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:text-muted-foreground"
|
|
191
|
-
style={{
|
|
192
|
-
left: menuPosition?.left ?? 0,
|
|
193
|
-
top: menuPosition?.top ?? 0,
|
|
194
|
-
visibility: menuPosition ? "visible" : "hidden",
|
|
195
|
-
}}
|
|
196
|
-
disabled={menu.tooLong}
|
|
197
|
-
onMouseDown={(event) => event.preventDefault()}
|
|
198
|
-
onClick={addSelectionToChat}
|
|
199
|
-
>
|
|
200
|
-
<MessageSquarePlus aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
|
|
201
|
-
<span className="truncate">
|
|
202
|
-
{menu.tooLong
|
|
203
|
-
? t("chatWorkspaceExcerptSelectionTooLong")
|
|
204
|
-
: t("chatWorkspaceAddToChat")}
|
|
205
|
-
</span>
|
|
206
|
-
</button>,
|
|
207
|
-
document.body,
|
|
208
|
-
)
|
|
209
|
-
: null;
|
|
210
|
-
|
|
211
25
|
return (
|
|
212
|
-
<
|
|
213
|
-
|
|
26
|
+
<ChatTextSelectionAction
|
|
27
|
+
actionLabel={t("chatWorkspaceAddToChat")}
|
|
214
28
|
className="h-full min-h-0"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
29
|
+
maxCharacters={WORKSPACE_TEXT_EXCERPT_MAX_CHARACTERS}
|
|
30
|
+
selectionTooLongLabel={t("chatWorkspaceExcerptSelectionTooLong")}
|
|
31
|
+
onAddToChat={({ text }) => {
|
|
32
|
+
const excerpt = buildWorkspaceTextExcerpt({
|
|
33
|
+
path: filePath,
|
|
34
|
+
label: fileLabel,
|
|
35
|
+
selectedText: text,
|
|
36
|
+
sourceText,
|
|
37
|
+
sourceStartLine,
|
|
38
|
+
});
|
|
39
|
+
if (excerpt) onAddToChat?.(excerpt);
|
|
40
|
+
}}
|
|
218
41
|
>
|
|
219
42
|
{children}
|
|
220
|
-
|
|
221
|
-
</div>
|
|
43
|
+
</ChatTextSelectionAction>
|
|
222
44
|
);
|
|
223
45
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react';
|
|
2
|
+
import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
|
|
3
|
+
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
4
|
+
import {
|
|
5
|
+
CHAT_WORKSPACE_EXPLORER_COMPACT_THRESHOLD,
|
|
6
|
+
normalizeChatWorkspaceExplorerWidth,
|
|
7
|
+
} from '@/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils';
|
|
8
|
+
|
|
9
|
+
export function useWorkspaceExplorerLayout({
|
|
10
|
+
fileActive,
|
|
11
|
+
projectFilesActive,
|
|
12
|
+
sessionProjectRoot,
|
|
13
|
+
sessionWorkingDir,
|
|
14
|
+
}: {
|
|
15
|
+
fileActive: boolean;
|
|
16
|
+
projectFilesActive: boolean;
|
|
17
|
+
sessionProjectRoot: string | null;
|
|
18
|
+
sessionWorkingDir: string | null;
|
|
19
|
+
}) {
|
|
20
|
+
const presenter = usePresenter();
|
|
21
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
22
|
+
const resizeRef = useRef<{ startX: number; startWidth: number; width: number } | null>(null);
|
|
23
|
+
const persistedExplorerWidth = useChatThreadStore((state) => state.snapshot.workspaceExplorerWidth);
|
|
24
|
+
const [compact, setCompact] = useState(false);
|
|
25
|
+
const [explorerDragWidth, setExplorerDragWidth] = useState<number | null>(null);
|
|
26
|
+
const [explorerOpen, setExplorerOpen] = useState(true);
|
|
27
|
+
const explorerWidth = explorerDragWidth ?? persistedExplorerWidth;
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const node = containerRef.current;
|
|
31
|
+
if (!node || typeof ResizeObserver === 'undefined') return;
|
|
32
|
+
const update = () => setCompact(node.getBoundingClientRect().width < CHAT_WORKSPACE_EXPLORER_COMPACT_THRESHOLD);
|
|
33
|
+
update();
|
|
34
|
+
const observer = new ResizeObserver(update);
|
|
35
|
+
observer.observe(node);
|
|
36
|
+
return () => observer.disconnect();
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setExplorerOpen(true);
|
|
41
|
+
}, [sessionProjectRoot, sessionWorkingDir]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!fileActive || !compact || !explorerOpen) return;
|
|
45
|
+
const closeOnEscape = (event: KeyboardEvent) => {
|
|
46
|
+
if (event.key === 'Escape') setExplorerOpen(false);
|
|
47
|
+
};
|
|
48
|
+
window.addEventListener('keydown', closeOnEscape);
|
|
49
|
+
return () => window.removeEventListener('keydown', closeOnEscape);
|
|
50
|
+
}, [compact, explorerOpen, fileActive]);
|
|
51
|
+
|
|
52
|
+
const onResizeStart = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
53
|
+
if (compact || !fileActive) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
56
|
+
resizeRef.current = { startX: event.clientX, startWidth: explorerWidth, width: explorerWidth };
|
|
57
|
+
const onMove = (moveEvent: PointerEvent) => {
|
|
58
|
+
const resizing = resizeRef.current;
|
|
59
|
+
if (!resizing) return;
|
|
60
|
+
const width = normalizeChatWorkspaceExplorerWidth(resizing.startWidth + moveEvent.clientX - resizing.startX);
|
|
61
|
+
resizing.width = width;
|
|
62
|
+
setExplorerDragWidth(width);
|
|
63
|
+
};
|
|
64
|
+
const onUp = () => {
|
|
65
|
+
const width = resizeRef.current?.width;
|
|
66
|
+
if (typeof width === 'number') presenter.chatThreadManager.setWorkspaceExplorerWidth(width);
|
|
67
|
+
setExplorerDragWidth(null);
|
|
68
|
+
resizeRef.current = null;
|
|
69
|
+
window.removeEventListener('pointermove', onMove);
|
|
70
|
+
window.removeEventListener('pointerup', onUp);
|
|
71
|
+
window.removeEventListener('pointercancel', onUp);
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener('pointermove', onMove);
|
|
74
|
+
window.addEventListener('pointerup', onUp);
|
|
75
|
+
window.addEventListener('pointercancel', onUp);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
compact,
|
|
80
|
+
containerRef,
|
|
81
|
+
explorerOpen,
|
|
82
|
+
explorerOverlay: fileActive && compact && explorerOpen,
|
|
83
|
+
explorerWidth,
|
|
84
|
+
onResizeStart,
|
|
85
|
+
setExplorerOpen,
|
|
86
|
+
showExplorer: projectFilesActive || (fileActive && explorerOpen),
|
|
87
|
+
};
|
|
88
|
+
}
|